Skip to content

Instantly share code, notes, and snippets.

@jeffijoe
Created September 6, 2016 08:53
Show Gist options
  • Save jeffijoe/211e9cbb64cc78d9490f0fa4e8cafaef to your computer and use it in GitHub Desktop.
Save jeffijoe/211e9cbb64cc78d9490f0fa4e8cafaef to your computer and use it in GitHub Desktop.
Code snippets for my Medium article.
const currentUser = {
id: 123,
name: 'Jeff'
}
const todosRepository = new TodosRepository()
const todosService = makeTodosService({
todosRepository,
currentUser
})
export default {
todosService,
todosRepository,
currentUser
}
import { createContainer, asClass, asFunction } from 'awilix'
import makeTodosService from './todosService'
import TodosRepository from './todosRepository'
export default function configureContainer () {
const container = createContainer()
// Ordering does not matter.
container.register({
// Notice the scoped() at the end - this signals
// Awilix that we are gonna want a new instance per "scope"
todosService: asFunction(makeTodosService).scoped(),
// We only want a single instance of this
// for the apps lifetime (it does not deal with user context),
// so we can reuse it!
todosRepository: asClass(TodosRepository).singleton()
})
return container
}
var express = require('express')
var app = express()
var session = require('express-session')
app.use(session({
store: require('connect-session-knex')()
}))
import db from '../mydatabase'
export default {
getTodos: () => {
return db.query('select * from todos')
}
}
import makeTodosService from './todosService'
import TodosRepository from './todosRepository'
describe('Todos System', function () {
it('works', async function () {
// This is how DI is done manually
// a.k.a Poor Man's DI
const todosService = makeTodosService({
todosRepository: new TodosRepository(),
// Let's fake it til we make it!
currentUser: {
id: 123,
name: 'Jeff'
}
})
// Todos Service already knows who's creating it!
const created = await todosService.create({
text: 'Write Medium article'
})
const todos = await todosService.getTodos({
filter: 'ALL'
})
expect(todos.length).to.equal(1)
await todosService.update(todo.id, {
completed: true
})
const incompleteTodos = await todosService.getTodos({
filter: 'INCOMPLETED'
})
expect(incompleteTodos.length).to.equal(0)
const completedTodos = await todosService.getTodos({
filter: 'COMPLETED'
})
expect(completedTodos.length).to.equal(1)
})
})
const router = new KoaRouter()
router.get('/todos', async (ctx) => {
const todosService = makeTodosService({
todosRepository: new TodosRepository(),
// Our Koa request knows about the current user
currentUser: ctx.state.user
})
ctx.body = await todosService.getTodos(ctx.request.query)
ctx.status = 200
})
router.post('/todos', async (ctx) => {
const todosService = makeTodosService({
todosRepository: new TodosRepository(),
currentUser: ctx.state.user
})
// ...
})
// and so on...
import Koa from 'koa'
import KoaRouter from 'koa-router'
import { asValue } from 'awilix'
import { scopePerRequest, makeInvoker } from 'awilix-koa'
import configureContainer from './configureContainer'
const app = new Koa()
const router = new KoaRouter()
const container = configureContainer()
// This installs a scoped container into our
// context - we will use this to register our current user!
app.use(scopePerRequest(container))
// Let's do that now!
app.use((ctx, next) => {
ctx.state.container.registerValue({
// Imagine some auth middleware somewhere...
// This makes currentUser available to all services!
currentUser: ctx.state.user
})
return next()
})
// Now our route handlers will be able to resolve a todos service..
// using DEPENDENCY INJECTION! YEEEEHAW!
// P.S: be a good dev and use multiple files. ;)
const todosAPI = ({ todosService }) => {
return {
getTodos: async (ctx) => {
return todosService.getTodos(ctx.request.query)
},
createTodo: async (ctx) => {
return todosService.createTodo(ctx.request.body)
},
updateTodo: async (ctx) => {
return todosService.updateTodo(
ctx.params.id,
ctx.request.body
)
},
deleteTodo: async (ctx) => {
return todosService.deleteTodo(
ctx.params.id,
ctx.request.body
)
}
}
}
// Awilix magic will run the above function
// every time a request comes in, so we have
// a set of scoped services per request.
const api = makeInvoker(todosAPI)
router.get('/todos', api('getTodos'))
router.post('/todos', api('createTodo'))
router.patch('/todos/:id', api('updateTodo'))
router.delete('/todos/:id', api('deleteTodo'))
app.use(router.routes())
app.listen(1337)
// Let's do an in-memory implementation for now.
const _todos = []
export default class TodosRepository {
// Marking all methods async makes them return promises!
async find(query) {
const filtered = _todos.filter((todo) => {
// Check the user ID
if (todo.userId !== query.userId)
return false
// Check the filter
if (query.filter === 'COMPLETED')
return todo.completed === true
if (query.filter === 'INCOMPLETED')
return todo.completed === false
return true
})
return filtered
}
async get(id) {
const todo = _todos.find(x => x.id === id)
return todo
}
async create(data) {
const newTodo = {
id: Date.now(), // cheeky ID generation
text: data.text,
userId: data.userId,
completed: data.completed
}
_todos.push(newTodo)
return newTodo
}
async update(id, data) {
const todo = await this.get(id)
Object.assign(todo, data)
return Promise.resolve(todo)
}
async delete(id) {
const todo = await this.get(id)
_todos.splice(todo, 1)
}
}
export default class TodosService {
constructor({ db }) {
this.db = db
}
getTodos() {
return this.db.query('select * from todos')
}
}
export default function makeTodosService ({ db }) {
return {
getTodos: () => {
return db.query('select * from todos')
}
}
}
describe('Todos Manager', function () {
beforeEach(() {
subject = todosManager({
db: testDatabaseSomehow
})
})
it('works', async function () {
const todos = await subject.findTodos()
expect(todos.length).to.equal(3)
})
})
import assert from 'assert'
// Using object destructuring to make it look good.
export function makeTodosService({
// "repository" is a fancy term to descibe an object
// that is used to retrieve data from a datasource - the actual
// data source does not matter. Could be a database, a REST API,
// or some IoT things like sensors or whatever.
todosRepository,
// We also want info about the user that is using the service,
// so we can restrict access to only their own todos.
currentUser
}) {
// Let's assert that we got what we needed.
assert(todosRepository, 'opts.todosRepository is required.')
assert(currentUser, 'opts.currentUser is required.')
return {
// Gets todos for the current users.
getTodos: async (query) => {
// Query is just an object. In this case we can declare what
// todos we want - completed, uncompleted, or all.
const todos = await todosRepository.find({
// can be ALL, INCOMPLETED, COMPLETED
filter: query.filter,
userId: currentUser.id
})
return todos
},
// Creates a new todo for the current user
createTodo: async (data) => {
const newTodo = await todosRepository.create({
text: data.text,
userId: currentUser.id,
completed: false
})
return newTodo
},
// Updates a todo if allowed
updateTodo: async (todoId, data) {
const todo = await todosRepository.get(todoId)
// Verify that we are allowed to modify this todo
if (todo.userId !== currentUser.id) {
throw new Error('Forbidden!')
}
const updatedTodo = await todosRepository.update(todoId, {
text: data.text,
completed: data.completed
})
return updatedTodo
},
// Deletes a todo if allowed
deleteTodo: async (todoId) {
const todo = await todosRepository.get(todoId)
// Verify that we are allowed to delete this todo
if (todo.userId !== currentUser.id) {
throw new Error('Forbidden!')
}
await todosRepository.delete(todoId)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment