Last active
November 8, 2018 23:11
-
-
Save TimMensch/727407bdc522bc9900dae1786951c9e4 to your computer and use it in GitHub Desktop.
An alternative to the "Functional River"
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Easier to follow. Equally easy to test -- and with one fewer function in need of testing. | |
const Promise = require("bluebird"); | |
const { hashStringAsync } = Promise.promisifyAll(require("./lib/crypto")); | |
const { logEventAsync } = Promise.promisifyAll(require("./lib/log")); | |
const { openAsync } = Promise.promisifyAll(require("./lib/db")); | |
const { TimeoutError, ValidationError, NotFoundError } = require("./errors"); | |
const _openHandle = openAsync(); // FYI: Promises include memoization (caching) built into same API | |
module.exports = { auth }; | |
/* auth is our main function */ | |
async function auth({ username, password, | |
_onTimeoutError = errorHandler, | |
_onNotFoundError = errorHandler, | |
_onValidationError = errorHandler, | |
}) { | |
try { | |
// No more reusing "password" as "hashed password". | |
// Still starting the hash ASAP and not waiting for it. | |
const hashedPasswordPromise = hashStringAsync(password); | |
authValidate({ username, password }); | |
const users = await usersModel(); | |
const user = await users.findOneAsync({ | |
username, | |
// I would have renamed "password" here; bad form to have a field named | |
// password that's expecting a hash. | |
password: await hashedPasswordPromise, // Deferred await to when we need it. | |
}); | |
await userFound(user); | |
return userInfo; | |
} catch (e) { | |
// Left this in because Bluebird's extension for matching errors is cute. | |
Promise.reject(e) | |
.catch(TimeoutError, _onTimeoutError) | |
.catch(NotFoundError, _onNotFoundError) | |
.catch(ValidationError, _onValidationError) | |
.catch(errorHandler); | |
} | |
} | |
function authValidate({ username, password }) { | |
if (!username || username.length < 4) { | |
throw new ValidationError("Invalid username. Required, 4 char minimum."); | |
} | |
if (!password || password.length < 4) { | |
throw new ValidationError("Invalid password. Required, 4 char minimum."); | |
} | |
} | |
function userFound(results) { | |
if (results) return; | |
throw new NotFoundError("No users matched. Login failed"); | |
} | |
function usersModel() { | |
return _openHandle.then(({ models }) => models.users); | |
} | |
function errorHandler(err) { | |
console.error("Failed auth!", err); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I agree the interfaces I came up with were a little contrived - it's a balance I tried to strike while still being somewhat copy-pastable.
Too many articles talk about Functional Purity using un-relatable or super granular examples:
Math.max
.I wanted to show an applied & recognizable pattern to most (backend) devs. This necessarily means I have to make trade-offs: so it's both digestible, and not overly simplified.
Figuring out how to scope your closures & pass variables between steps is (to me) a separate qualitative discussion.
Differing priorities will dictate how this gets decided. In researching this area, I've been working on an (unpublished) article for about a year detailing how this plays out in Haskell, Erlang, Elm, and F#. (Each have varying ways to 'pass-through' values, or scope values using Monads, etc.) I believe this will address much of your misgivings.
I'm a little confused when I hear things like the "...functional approach you're advocating is actually an anti-pattern."
You're not the first person to tell me that - and I'm trying to understand where this value-judgement is coming from.
Before I continue, let me mention that the word 'Functional' gets thrown around to mean many different things. This is unfortunate. (And I know I'm not helping with a library called
Functional Promises
) - I don't want to get bogged down on whether I mean strictlyFunctional Programming ala Computer Science Purists
or referring toLambda calculus designs of math purists
... This is a debate for another day. For now, let's unpack the "functional/anti-pattern" thing...Anti-patterns in code are rarely absolutes. Everything is about trade-offs.
So, trying to figure out exactly you're talking about:
In Eric Elliot's article he alludes to the importance of return values (and that they ought to be useful) here:
Of course virtually no app is 100% pure functions, at some point you need to persist data or get data over a network; the key is when that happens it's tightly scoped w/ Single Purpose in mind. Ultimately any transformations, validation, or security checks should be alongside your side-effect functions - helps you avoid tightly coupled logic.