Created
September 6, 2016 08:53
-
-
Save jeffijoe/211e9cbb64cc78d9490f0fa4e8cafaef to your computer and use it in GitHub Desktop.
Code snippets for my Medium article.
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
const currentUser = { | |
id: 123, | |
name: 'Jeff' | |
} | |
const todosRepository = new TodosRepository() | |
const todosService = makeTodosService({ | |
todosRepository, | |
currentUser | |
}) | |
export default { | |
todosService, | |
todosRepository, | |
currentUser | |
} |
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
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 | |
} |
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
var express = require('express') | |
var app = express() | |
var session = require('express-session') | |
app.use(session({ | |
store: require('connect-session-knex')() | |
})) |
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
import db from '../mydatabase' | |
export default { | |
getTodos: () => { | |
return db.query('select * from todos') | |
} | |
} |
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
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) | |
}) | |
}) |
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
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... |
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
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) |
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
// 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) | |
} | |
} |
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
export default class TodosService { | |
constructor({ db }) { | |
this.db = db | |
} | |
getTodos() { | |
return this.db.query('select * from todos') | |
} | |
} |
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
export default function makeTodosService ({ db }) { | |
return { | |
getTodos: () => { | |
return db.query('select * from todos') | |
} | |
} | |
} |
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
describe('Todos Manager', function () { | |
beforeEach(() { | |
subject = todosManager({ | |
db: testDatabaseSomehow | |
}) | |
}) | |
it('works', async function () { | |
const todos = await subject.findTodos() | |
expect(todos.length).to.equal(3) | |
}) | |
}) |
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
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