Skip to content

Instantly share code, notes, and snippets.

@iperiago
Last active January 20, 2024 20:47
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save iperiago/336dfd304dbbf68a09fbf4b9ade60a33 to your computer and use it in GitHub Desktop.
Save iperiago/336dfd304dbbf68a09fbf4b9ade60a33 to your computer and use it in GitHub Desktop.
Node.js app in the real world : what they never really tell you
class RemoveUser {
constructor ({ userRepository }) {
this.userRepository = userRepository
}
async exec (user) {
return this.userRepository.remove(user)
}
}
import container from './container'
const app = container.cradle.app
app.
.start(container)
.catch(error => {
app.server.logger.error(error.stack)
process.exit()
})
class Application {
constructor ({ db, server }) {
this.db = db
this.server = server
}
async start (container) {
await this.db.connect()
await this.server.start(container)
}
}
import { createContainer, asValue, asClass } from 'awilix'
import Server from './interfaces/http/server'
import Application from './app/app'
import CreateUser from './app/user/create'
import UpdateUser from './app/user/update'
import RemoveUser from './app/user/remove'
import GetUser from './app/user/get'
import SearchUser from './app/user/search'
import ManageDB from './infra/database'
import Repository from './infra/database/repository'
import logger from './infra/logger/'
// ...
const container = createContainer()
container.register({
server: asClass(Server).singleton(),
// Application layer
app: asClass(Application).singleton(),
createUser: asClass(CreateUser),
updateUser: asClass(UpdateUser),
removeUser: asClass(RemoveUser),
getUser: asClass(GetUser),
searchUser: asClass(SearchUser),
// Infrastructure layer
db: asClass(ManageDB).singleton(),
userRepository: asClass(Repository).singleton(),
logger: asValue(logger)
// ...
})
import { EventEmitter } from 'events'
import User from '../../domain/user'
class createUser extends EventEmitter {
constructor ({ userRepository }) {
super()
this.userRepository = userRepository
this.events = { USER_CREATED: 'USER_CREATED' }
}
async exec ({ email, username, password, sponsor }) {
// We're skipping password encoding here for simplicity
// but of course you wouldn't want to do that for real!
const userEntity = new User(email, username, password)
const user = await this.userRepository.create(userEntity)
this.emit(this.events.USER_CREATED, { user, sponsor })
return user
}
}
class UserListener {
constructor ({ updateUser }) {
this.updateUser = updateUser
}
async onUserCreated ({ user, sponsor }) {
if (sponsor && user.sponsor === sponsor) {
sponsor.badges.push('SPONSOR')
await this.updateUser.exec(sponsor, { badges })
}
}
}
const { createUser, userListener } = container.cradle
const listener = userListener.onUserCreated.bind(userListener)
createUser.on(createUser.events.USER_CREATED, listener)
// ...
class ManageUser {
// Of course in our real class we'll also inject and tie up
// our other user services (such as GetUser for instance...)
constructor ({ createUser, updateUser, userListener }) {
this.createUser = createUser.exec.bind(createUser)
this.updateUser = updateUser.exec.bind(updateUser)
const listener = userListener.onUserCreated.bind(userListener)
createUser.on(createUser.events.USER_CREATED, listener)
}
}
class MongooseRepository {
constructor ({ Model }) {
this.collection = Model
}
async count () {
return this.collection.estimatedDocumentCount()
}
async find (query = {}, { multiple = true, count, lean } = {}) {
const results = multiple
? this.collection.find(query)
: this.collection.findOne(query)
if (count) {
return results.countDocuments().exec()
} else if (lean) {
return results.lean().exec()
} else {
return results.exec()
}
}
async create (body) {
const document = new this.collection(body)
return document.save()
}
async update (document, body = {}) {
const id = (typeof document._id !== 'undefined')
? document._id
: document
return this.collection.findByIdAndUpdate(id, body, { new: true })
}
async remove (document) {
const reloadedDocument = await this.reload(document)
return reloadedDocument.remove()
}
}
import api from './mainController'
const manageUserAPI = ({ manageUser }) => ({
createUser: api(manageUser, 'createUser'),
updateUser: api(manageUser, 'updateUser', ['id']),
// ...
})
const processArgs = (ctx, params) => {
let args = ctx.params[param]
args.push({
user: ctx.state.user,
...ctx.method === 'GET'
? processQueryParametersForService(ctx.query)
: ctx.request.body
})
return args
}
const processQueryParametersForService = query => {
const maxResultsPerPage = 100
const allowedFilters = ['isPublished', 'isPrivate']
const allowedOptions = ['count', 'sort']
const allowedQueryParameters = [
...allowedFilters,
...allowedOptions,
'limit', 'skip', 'page', 'results'
]
let opts = {}
Object.keys(query).forEach(key => {
if (query[key] && allowedQueryParameters.includes(key)) {
if (config.allowedFilters.includes(key)) {
// Handle filters by composing an additional search query
opts.query = opts.query || {}
opts.query[key] = query[key]
} else {
// Handle options and prepare pagination
opts[key] = query[key]
}
}
})
// Handle pagination
if (opts.hasOwnProperty('page') && opts.hasOwnProperty('results')) {
opts.limit = Math.min(opts.results, maxResultsPerPage)
opts.skip = (opts.page - 1) * opts.limit
} else {
opts.limit = opts.limit ? Math.min(opts.limit, maxResultsPerPage) : maxResultsPerPage
}
return opts
}
import { createController } from 'awilix-koa'
import manageUserAPI from '../api/manageUser'
const manageUserRoutes = createController(manageUserAPI)
.prefix('/user')
.post('/create', 'createUser')
.patch('/:id/update', 'updateUser')
// ...
import fs from 'fs'
import path from 'path'
let models = []
fs
.readdirSync(__dirname)
.filter(file =>
(file.indexOf('.') !== 0) &&
(file !== path.basename(__filename)) &&
(file.slice(-3) === '.js')
)
.forEach(file => {
let model = require(path.join(__dirname, file)).default
models[model.modelName] = model
})
import mongoose from 'mongoose'
class ManageDB {
constructor ({ config, logger }) {
this.config = config
this.logger = logger
}
async connect () {
let credentials = ''
if (this.config.auth) {
credentials = `${this.config.user}:${this.config.password}@`
}
const connection = typeof this.config === 'string'
? this.config
: `mongodb://${credentials}${this.config.host}:${this.config.port}/${this.config.database}`
const options = this.config.ENV === 'prod'
? { autoIndex: false }
: {}
this.logger.debug('Connecting to the database...')
mongoose.set('useCreateIndex', true)
mongoose.set('useFindAndModify', false)
await mongoose
.connect(connection, { useNewUrlParser: true, ...options })
.catch(error => {
this.logger.error('Error while connecting to the database', error)
process.exit(1)
})
this.logger.debug('Connected to the database')
}
async close () {
this.logger.debug('Closing database...')
await mongoose.connection.close().catch(error => {
this.logger.error('Error while closing the database', error)
process.exit(1)
})
this.logger.debug('Database closed')
}
}
async reload (document, { select, populate, lean } = {}) {
// Only reload if necessary
if (!select && !populate && !lean && document instanceof this.collection) {
return document
}
return (typeof document._id !== 'undefined')
? this.findById(document._id, { select, populate, lean })
: this.findById(document, { select, populate, lean })
}
class Entity {
constructor () {
this.id = ''
}
equals (other) {
if (other instanceof Entity === false) {
return false
}
return other.id
? this.referenceEquals(other.id)
: this === other
}
referenceEquals (id) {
if (!this.id) {
// Try object equality
return this.equals(id)
}
const reference = typeof id !== 'string'
? id.toString()
: id
return this.id === reference
}
toString () {
return this.id
}
}
import Entity from './entity'
class User extends Entity {
constructor (email, username, encodedPassword, { roles = ['USER'] } = {}) {
super()
this.email = email
this.username = username
this.encodedPassword = encodedPassword
this.roles = roles
this.lastLogin = null
}
hasRole (role, roles = {}) {
if (!role || this.roles.includes(role)) {
return true
}
// Check for parent roles recursively
// based on the role hierarchy
let hasRole = false
Object.keys(roles).forEach(key => {
if (
!hasRole &&
roles[key] &&
roles[key].inherits &&
roles[key].inherits.includes(role)
) {
hasRole = this.hasRole(key, roles)
}
})
return hasRole
}
}
import mongoose from 'mongoose'
import UserClass from '../../../../domain/user'
const UserSchema = mongoose.Schema({
email: {
type: String,
unique: true,
required: true,
select: false
},
encodedPassword: {
type: String,
required: true,
select: false
},
username: {
type: String,
unique: true
},
lastLogin: {
type: Date,
default: null
},
roles: {
type: [String],
defaut: ['USER']
},
}, { timestamps: true })
// Indexes
UserSchema.index({ email: 1 })
UserSchema.index({ username: 1 })
UserSchema.index({ roles: 1 })
// Loads the User entity methods in the model
UserSchema.loadClass(UserClass)
export default mongoose.model('User', UserSchema)
import mongoose from 'mongoose'
const autoRemove: (model, parentCollection) =>
async document => {
const Model = mongoose.model(model)
const parentId = document._id
const query = removeChildren(parentCollection, parentId)
const children = await Model.find(query)
// Using mongoose remove() method
// to ensure post remove hooks are called
for (const child of children) {
await child.remove()
}
}
import mongoose from 'mongoose'
import { autoRemove } from '../mongooseMiddleware'
import GameClass from '../../../../domain/game'
const GameSchema = mongoose.Schema({
players: [{
type: mongoose.Schema.Types.ObjectId,
ref: 'Player'
}],
// ...
}, { timestamps: true })
GameSchema.post('remove', autoRemove('Player', 'game'))
GameSchema.loadClass(GameClass)
export default mongoose.model('Game', GameSchema)
import http from 'http'
import Koa from 'koa'
import respond from 'koa-respond'
import cors from '@koa/cors'
import bodyParser from 'koa-bodyparser'
import { scopePerRequest, loadControllers } from 'awilix-koa'
import authenticate from './auth/authenticate'
import accessControl from './auth/accessControl'
import httpLogger from './logger/httpLogger'
import errorHandler from './errors/errorHandler'
import cache from './cache/httpCache'
import notFoundHandler from './errors/notFoundHandler'
class Server {
constructor ({ config, logger }) {
this.config = config
this.logger = logger
this.app = new Koa()
this.app.keys = [this.config.secret]
}
async create (container) {
this.app
.use(errorHandler)
.user(respond())
.use(cors({ credentials: true }))
.use(bodyParser())
.use(scopePerRequest(container))
.use(httpLogger(this.logger))
.use(authenticate)
.use(cache)
.use(accessControl.protect)
.use(loadControllers('./routes/*.js', { cwd: __dirname }))
.use(notFoundHandler)
return http.createServer(this.app.callback())
}
async start (container) {
const appServer = await this
.create(container)
.catch((error) => {
this.logger.error('Error while starting up server', error)
process.exit(1)
})
appServer && appServer.listen(this.config.server.port, () => {
this.logger.info('App is running')
})
}
}
const api = (instance, method, params = []) =>
async ctx => {
const args = processArgs(ctx, params)
const data = await instance[method](...args)
const response = ctx[ctx.method === 'POST' ? 'created' : 'ok']
return response({
...ctx.body,
data
})
}
import accessControl from './accessControl'
const authenticate = async (ctx, next) => {
const { getUser, encrypt, config } = ctx.state.container.cradle
if (!config.app.publicRoutes.some(route =>
ctx.originalUrl.startsWith(route)
)) {
const token =
ctx.cookies.get('yourAppName', { signed: true }) ||
resolveToken(ctx.header)
if (!token) {
return ctx.unauthorized({
...ctx.body,
error: 'Authentication failed : no token provided'
})
}
const decoded = encrypt.decodeToken(token)
const user = await getUser.exec(decoded.id)
if (!user) {
return ctx.unauthorized({
...ctx.body,
error: 'Authentication failed : wrong token provided'
})
}
const isAllowed = await accessControl.check(ctx, user)
if (!isAllowed) {
return ctx.forbidden({
...ctx.body,
error: 'Permission denied'
})
}
ctx.state.user = user
}
await next()
}
function resolveToken (header) {
if (!header || !header.authorization) {
return false
}
const parts = header.authorization.split(' ')
if (parts.length === 2) {
const scheme = parts[0]
const credentials = parts[1]
if (/^Bearer$/i.test(scheme)) {
return credentials
}
}
return false
}
const errorHandler = async (ctx, next) => {
try {
await next()
} catch (error) {
ctx.status = error.statusCode || 500
ctx.body = {
...ctx.body,
error: process.env.NODE_ENV !== 'prod' ? error.stack : error.message
}
ctx.app.emit('SERVER_ERROR', error)
}
}
async create (container) {
this.app
.use(errorHandler)
.use(respond())
.use(cors({ credentials: true }))
.use(bodyParser())
.use(scopePerRequest(container))
.use(httpLogger(this.logger))
.use(this.meta())
.use(authenticate)
.use(cache)
.use(accessControl.protect)
.use(loadControllers('./routes/*.js', { cwd: __dirname }))
.use(notFoundHandler)
this.app.on('SERVER_ERROR', error =>
this.logger.error(error.stack)
)
const appServer = http.createServer(this.app.callback())
return appServer
}
const notFoundHandler = async ctx => {
const msg = `${ctx.request.method} ${ctx.request.path}`
ctx.notFound({ message: `No endpoint matched the request: ${msg}` })
}
class AccessControlList {
constructor (rules, resourceType, resourceReference = '', userReference = '', params = {}) {
this.RULES = rules
this.resourceType = resourceType
this.resourceReference = resourceReference
this.userReference = userReference
this.params = params
}
can (role, operation) {
const roles = this.RULES.roles
if (!roles || !roles[role] || !roles[role].can) {
return false
}
const resources = roles[role].can[operation]
if (resources && resources.some(allowed =>
allowed.resource ===
this.resourceType &&
this.match(allowed.when, allowed.except)
)) {
return true
}
if (!roles[role].inherits || roles[role].inherits.length < 1) {
return false
}
return roles[role].inherits.some(childRole =>
this.can(childRole, operation)
)
}
match (conditions = {}, exceptions = {}) {
return
this.isIn(exceptions, { disallowEmpty: true }) ||
this.isIn(conditions)
}
isIn (object, { disallowEmpty = false }) {
const params = Object.keys(this.params)
const keys = Object.keys(object)
return
(keys.length || !disallowEmpty) &&
keys.every(key =>
params.some(param =>
param === key && this.params[param] === object[key]
)
)
}
}
accessControl.protect = async (ctx, user) => {
await next()
if (ctx.method !== 'GET') {
const { createAcl, updateAcl, removeAcl, getAcl, config } = ctx.state.container.cradle
const aclRules = config.app.aclRules
const { resourceType } = resolveOperation(ctx.originalUrl)
const data = ctx.body.data
const user = ctx.state.user || {}
if (data && data.toString()) {
const userReference = ctx.originalUrl === config.app.userCreationRoute
? data.toString()
: user.toString()
const resourceReference = data.toString()
let params = {}
Object.keys(aclRules.defaultParams).forEach(param => {
if (typeof data[param] !== 'undefined') {
params[param] = data[param]
}
params.isOwner = true
})
if (
ctx.method === 'POST' &&
(!aclRules.dependencies[resourceType] ||
!aclRules.dependencies[resourceType].on)
) {
await createAcl.exec(resourceType, resourceReference, userReference, params)
} else {
const acl = await getAcl.exec(aclRules, resourceType, resourceReference, userReference)
if (acl && acl.toString()) {
ctx.method === 'DELETE' && await removeAcl.exec(acl.toString())
ctx.method === 'PATCH' && await updateAcl.exec(acl.toString(), params)
}
}
}
}
}
function resolveOperation (url, method) {
const parts = url.split('/')
let resourceType = ''
let resourceReference = ''
let operation = ''
if (parts.length > 1) {
resourceType = parts[1]
}
if (parts.length > 3 && (method === 'PATCH' || method === 'DELETE')) {
resourceReference = parts[2]
operation = parts[3]
} else if (parts.length > 2 && method === 'GET') {
resourceReference = parts[2]
operation = 'read'
} else if (parts.length > 2 && method === 'POST') {
operation = parts[2]
}
// Optionally, we can check if we have a valid id
resourceReference = resourceReference.match(/* Your pattern here */)
? resourceReference
: ''
return { resourceType, resourceReference, operation }
}
accessControl.check = async (ctx, user) => {
const { getAcl, config } = ctx.state.container.cradle
const aclRules = config.app.aclRules
const { resourceType, resourceReference, operation } = resolveOperation(ctx.originalUrl, ctx.method)
let acl =
await getParentAcl(ctx, user, resourceType, resourceReference) ||
await getAcl.exec(aclRules, resourceType, resourceReference, user.toString())
// If nothing is found yet, attempt to get the correct resource parameters
if (!acl.resourceReference && operation === 'read') {
acl = await getAcl.exec(resourceType, resourceReference)
acl.params.isOwner = false
}
// If a parent access control list has been found, make sure the child resource type is used
acl.resourceType = resourceType
acl.params = {
...!resourceReference ? aclRules.defaultParams : {},
...acl.params,
...processQueryParametersForAcl(ctx.query, aclRules)
}
return user.roles.some(role =>
acl.can(role, operation)
)
}
function processQueryParametersForAcl (query, aclRules) {
const allowedParams = [...Object.keys(aclRules.defaultParams), 'isOwner']
let opts = {}
Object.keys(query).forEach(key => {
if (query[key] && allowedParams.includes(key)) {
opts[key] = query[key] === 'true'
}
})
return opts
}
async function getParentAcl (ctx, user, resourceType, resourceReference) {
const { getAcl, config } = ctx.state.container.cradle
const aclRules = config.app.aclRules
let reloadedParentResource
let parentAcl
if (aclRules.dependencies[resourceType] && aclRules.dependencies[resourceType].on) {
const parentResourceType = aclRules.dependencies[resourceType].on
const parentResource = ctx.request.body[parentResourceType]
if (parentResource) {
// If the parent resource is sent with the request, use it
const repository = ctx.state.container.cradle[parentResourceType + 'Repository']
reloadedParentResource = await reload(repository, parentResource)
} else {
// If not, load the parent resource of the entity, provided there is a valid id as resource reference
const repository = ctx.state.container.cradle[resourceType + 'Repository']
const reloadedResource = await reload(repository, resourceReference)
reloadedParentResource = reloadedResource ? reloadedResource[parentResourceType] : null
}
if (reloadedParentResource) {
parentAcl = await getAcl.exec(aclRules, parentResourceType, reloadedParentResource.toString(), user.toString())
}
}
return parentAcl
}
async function reload (repository, resource) {
if (!repository || !resource) {
return false
}
try {
return await repository.reload(resource)
} catch(error) {
return false
}
}
class CreatePlayer
constructor ({ playerRepository }) {
this.playerRepository = playerRepository
}
async exec ({ user, game }) {
if (game.hasPlayer(user)) {
throw new Error('The user is already playing the game')
}
const player = new Player(game, user)
return this.playerRepository.create(player)
}
}
async create (body) {
try {
const document = new this.collection(body)
} catch(error) {
this.onError(error, 'An error occured while creating the resource')
}
return document.save()
}
onError (error, message) {
// We will need to inject a logger in our class constructor of course
this.logger.error(error.stack)
if (error.name === 'MongoError' && error.code === 11000) {
throw new Error('The resource already exists')
}
throw new Error(message || error.message)
}
/**
* This file is part of [Your application name]
*
* (c) 2019 [Your Company] <[your.company@e.mail]>
*
* --------------------------------------------------
*
* @module app.user.createUser
* @author [You] <[your@e.mail]>
*/
import User from '../../domain/user'
/**
* Create a user
*
* @alias app.user.CreateUser
*/
class CreateUser {
/**
* @param {Object} userRepository the user repository
* @param {Object} encrypt the encryption manager
*/
constructor ({ userRepository, encrypt }) {
super()
this.userRepository = userRepository
this.encrypt = encrypt
}
/**
* Create
*
* @param {String} email the user email
* @param {String} username the username
* @param {String} password the user password
* @return {Object} the created user @see {@link domain.User}
*/
async exec ({ email, username, password, options = {} }) {
const encodedPassword = this.encrypt.password(password)
return this.userRepository.create(new User(locale, email, username, encodedPassword, options))
}
}
import program from 'commander'
import addCommands from './commands/add'
import createUser from './commands/user/create'
// ...
import config from '../../../config'
addCommands(program, [createUser //...], container)
program
.version(config.app.info.version, '-v, --version')
.description('CLI commands to use selected application services')
.on('command:*', () => {
console.error('Invalid command: %s\nSee --help for a list of available commands.', program.args.join(' '))
process.exit(1)
})
.parse(process.argv)
!program.args.length && program.help()
/**
* @api {post} /user/create Create a user
* @apiGroup User
*
* @apiSuccess (201) {Object} data the created user
* @apiPermission User
* @apiVersion 1.0.0
*/
.post('/create', 'createUser')
import sinon from 'sinon'
import User from 'src/domain/user'
import CreateUser from 'src/app/user/create'
import repositoryFactory from '../repositoryFactory'
describe('App :: User : Create', () => {
const email = 'test'
const username = 'test'
const encodedPassword = 'test'
const user = new User(email, username, encodedPassword)
let encrypt = {}
let userRepository = repositoryFactory(user)
const createUser = new CreateUser({ userRepository, encrypt })
beforeEach(() => {
userRepository.stub()
encrypt.password = sinon.stub().returns(encodedPassword)
})
afterEach(async () => {
userRepository.reset()
encrypt.password.reset()
})
it('should create a new user', async () => {
await createUser.exec({ email, username, password: encodedPassword })
sinon.assert.calledWith(userRepository.create, user)
})
})
import sinon from 'sinon'
const repositoryFactory = entity => {
const methods = [
'create',
'reload',
'update',
'remove',
'find',
'findOne',
'findById'
]
let repository = {}
repository.stub = function () {
methods.forEach(method => {
this[method] = sinon.stub().returns(entity)
})
}
repository.reset = function () {
methods.forEach(method => {
this[method].reset()
})
}
return repository
}
import { expect } from 'chai'
import User from 'src/domain/user'
import config from 'config/'
describe('Domain :: User', () => {
let user = new User('test@test.com', 'test')
it('should give the user the default role USER', () => {
expect(user.hasRole('USER')).to.be.true
})
it('should assert the user has the role USER if he has the parent role ADMIN', () => {
user.roles = ['ADMIN']
expect(user.hasRole('USER', config.app.aclRules.roles)).to.be.true
})
it('should return true if no user role is provided when checking if the user has a given role', () => {
expect(user.hasRole()).to.be.true
})
})
import { expect } from 'chai'
import request from 'supertest'
import container from 'src/container'
describe('Integration :: API calls for managing users', () => {
const { db, server } = container.cradle
const email = 'test@test.com'
const username = 'test'
const password = 'test'
let api
before(async () => {
await server.create(container)
api = server.app.listen()
await db.connect()
})
after(async () => {
await db.close()
})
it('should create a user', function (done) {
request(api)
.post('/user/create')
.send({ email, username, password })
.expect(201)
.expect(response => {
expect(response.body.data.username).to.equal(username)
})
.end(done)
})
// ...
})
const createUser = {
name: 'create:user',
description: 'Create a new user',
options: [
['-m, --email [email]', 'Set the user email'],
['-p, --password [password]', 'Set the user password']
],
action: container => {
const { db, createUser } = container.cradle
const prompts = [
{ type: 'input', name: 'email', message: 'Enter the user e-mail :' },
{ type: 'input', name: 'username', message: 'Choose a username :' },
{ type: 'password', name: 'password', message: 'Choose a password :' },
{ type: 'multiselect', name: 'extraRoles', message: 'Attribute extra roles :', choices: [
{ message: 'Administrator', name: 'ADMIN' },
{ message: 'Super administrator', name: 'SUPER_ADMIN' }
] },
{ type: 'confirm', name: 'confirm', message: 'Please confirm', initial: true }
]
const handler = async params => {
const { email, username, password, extraRoles, confirm } = params
const roles = [...extraRoles, 'USER']
if (confirm) {
const user = await createUser.exec({ email, username, password, options: { roles } })
return `User created with username : ${user.username}`
} else {
return 'Operation cancelled : user not created'
}
}
return { db, prompts, handler }
}
}
import exec from './execution'
const add = (program, commandDefinitions, container) => {
for (const commandDefinition of commandDefinitions) {
const { name, description, options } = commandDefinition
const subProgram = program.command(name)
const execution = exec(commandDefinition, container)
subProgram.description(description)
for (const option of options) {
subProgram.option(option[0], option[1])
}
subProgram.action(execution)
}
}
import { prompt } from 'enquirer'
const exec = (commandDefinition, container) =>
async options => {
let { logger } = container.cradle
const { description, action } = commandDefinition
const { db, prompts, handler } = action(container)
db && await db.connect()
let response
try {
console.info(description)
const filteredPrompts = prompts.filter(prompt => !options[prompt.name])
const params = { ...options, ...await prompt(filteredPrompts) }
response = await handler(params)
} catch (error) {
logger.error(error)
response = 'An error occurred'
} finally {
db && await db.close()
console.info(response)
}
}
class CreatePlayer {
constructor ({ playerRepository, gameRepository, userRepository }) {
this.playerRepository = playerRepository
this.gameRepository = gameRepository
this.userRepository = userRepository
}
@autobind()
@reload('game')
@reload('user')
async exec ({ user, game }) {
if (game.hasPlayer(user)) {
throw new Error('The user is already playing the game')
}
const player = new Player(game, user)
return this.playerRepository.create(player)
}
}
const autobind = () =>
params => {
const { key, descriptor } = params
const boundDescriptor = { ...descriptor, value: undefined }
const method = descriptor.value
function initializer () {
return method.bind(this)
}
return {
...params,
extras: [{
kind: 'field',
key,
placement: 'own',
descriptor: boundDescriptor,
initializer
}]
}
}
const httpCache = async (ctx, next) => {
const { cache } = ctx.state.container.cradle
const data = await cache.get(ctx.url)
if (data && ctx.method === 'GET') {
return ctx.ok({
...ctx.body,
data: JSON.parse(data)
})
}
await next()
ctx.status === 200 && await cache.add(ctx.url, JSON.stringify(ctx.body.data))
}
require('dotenv').config()
const ENV = process.env.NODE_ENV || 'dev'
const fs = require('fs')
const path = require('path')
const dbConfig = loadConfig('DATABASE_URL', 'db')
const cacheConfig = loadConfig('CACHE_URL', 'cache')
const appConfig = require('./app')
const envConfig = require(path.join(__dirname, 'environments', ENV))
const config = Object.assign({
[ENV]: true,
env: ENV,
cache: cacheConfig,
db: dbConfig,
app: appConfig,
}, envConfig)
function loadConfig (url, file) {
if (fs.existsSync(path.join(__dirname, `./${file}.js`))) {
return require(`./${file}`)[ENV]
}
return process.env[url]
}
const reload = (entityName, populate) => {
return params => {
let { descriptor } = params
const method = descriptor.value
descriptor.value = async function (...args) {
const repository = this[entityName + 'Repository']
const entity = args[0][entityName] ? args[0][entityName] : args[0]
const reloadedEntity = await repository.reload(entity, { populate })
if (args[0][entityName]) {
args[0][entityName] = reloadedEntity || entity
} else {
args[0] = reloadedEntity || entity
}
return method.apply(this, args)
}
return { ...params, descriptor }
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment