-
-
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
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
class RemoveUser { | |
constructor ({ userRepository }) { | |
this.userRepository = userRepository | |
} | |
async exec (user) { | |
return this.userRepository.remove(user) | |
} | |
} |
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 container from './container' | |
const app = container.cradle.app | |
app. | |
.start(container) | |
.catch(error => { | |
app.server.logger.error(error.stack) | |
process.exit() | |
}) |
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
class Application { | |
constructor ({ db, server }) { | |
this.db = db | |
this.server = server | |
} | |
async start (container) { | |
await this.db.connect() | |
await this.server.start(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
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) | |
// ... | |
}) |
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 { 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 | |
} | |
} |
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
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 }) | |
} | |
} | |
} |
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 { createUser, userListener } = container.cradle | |
const listener = userListener.onUserCreated.bind(userListener) | |
createUser.on(createUser.events.USER_CREATED, listener) | |
// ... |
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
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) | |
} | |
} |
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
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() | |
} | |
} |
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 api from './mainController' | |
const manageUserAPI = ({ manageUser }) => ({ | |
createUser: api(manageUser, 'createUser'), | |
updateUser: api(manageUser, 'updateUser', ['id']), | |
// ... | |
}) |
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 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 | |
} |
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 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 | |
} |
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 { createController } from 'awilix-koa' | |
import manageUserAPI from '../api/manageUser' | |
const manageUserRoutes = createController(manageUserAPI) | |
.prefix('/user') | |
.post('/create', 'createUser') | |
.patch('/:id/update', 'updateUser') | |
// ... |
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 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 | |
}) |
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 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') | |
} | |
} |
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
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 }) | |
} |
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
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 | |
} | |
} |
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 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 | |
} | |
} |
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 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) |
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 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() | |
} | |
} |
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 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) |
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 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') | |
}) | |
} | |
} |
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 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 | |
}) | |
} |
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 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 | |
} |
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 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) | |
} | |
} |
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
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 | |
} |
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 notFoundHandler = async ctx => { | |
const msg = `${ctx.request.method} ${ctx.request.path}` | |
ctx.notFound({ message: `No endpoint matched the request: ${msg}` }) | |
} |
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
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] | |
) | |
) | |
} | |
} |
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
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) | |
} | |
} | |
} | |
} | |
} |
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
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 } | |
} |
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
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) | |
) | |
} |
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
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 | |
} |
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
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 | |
} | |
} |
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
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) | |
} | |
} |
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
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 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
/** | |
* 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)) | |
} | |
} |
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 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() |
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
/** | |
* @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') |
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 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) | |
}) | |
}) |
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 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 | |
} |
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 { 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 | |
}) | |
}) |
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 { 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) | |
}) | |
// ... | |
}) |
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 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 } | |
} | |
} |
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 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) | |
} | |
} |
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 { 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) | |
} | |
} |
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
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) | |
} | |
} |
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 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 | |
}] | |
} | |
} |
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 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)) | |
} |
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
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] | |
} |
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 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