Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Gael committed Dec 3, 2018
0 parents commit 70e60ef
Show file tree
Hide file tree
Showing 15 changed files with 4,505 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
node_modules
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Clean architecture for NodeJS Backends

Example project

## Install

yarn

## Run

yarn start

## Test

yarn test --watch
58 changes: 58 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{
"name": "clean-architecture-node",
"version": "1.0.0",
"description": "",
"scripts": {
"start": "nodemon ./src/index.js",
"lint": "eslint src",
"test": "jest"
},
"author": "Gael du Plessix",
"license": "ISC",
"dependencies": {
"apollo-server-express": "^1.3.6",
"body-parser": "^1.18.3",
"cors": "^2.8.4",
"dataloader": "^1.4.0",
"eslint": "^4.19.1",
"eslint-plugin-jest": "^21.17.0",
"express": "^4.16.3",
"graphql": "^0.13.2",
"graphql-iso-date": "^3.5.0",
"jest": "^23.2.0",
"mongodb": "^3.1.10",
"nodemon": "^1.17.5",
"prettier": "^1.13.5"
},
"prettier": {
"trailingComma": "es5",
"singleQuote": true
},
"eslintConfig": {
"extends": [
"eslint:recommended",
"plugin:jest/recommended"
],
"plugins": [
"jest"
],
"parserOptions": {
"ecmaVersion": 2018
},
"env": {
"node": true,
"jest/globals": true,
"es6": true
},
"rules": {
"no-console": 0,
"no-unused-vars": [
"error",
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_"
}
]
}
}
}
13 changes: 13 additions & 0 deletions src/context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const ports = require('./services/ports');
const accounts = require('./services/accounts');
const companies = require('./services/companies');
const networks = require('./services/networks');

module.exports = function createContext(_req) {
return {
ports: ports(),
accounts: accounts(),
companies: companies(),
networks: networks(),
};
};
49 changes: 49 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');

const { connectDb } = require('./infrastructure/db');

const user = require('./user');
const shopping = require('./shopping');

(async () => {
const dbClient = await connectDb();

const app = express();

app.use(cors());
app.use(bodyParser.json());

// setup a middleware to inject services into every request
app.use((req, _res, next) => {
req.services = {
user: user.getServices({ dbClient }),
shopping: shopping.getServices({ dbClient }),
};
next();
});

// Before defining modules routes, this is where we would define middlewares for:
// - auth
// - error handling
// - logging
// - ...

// add a dummy middleware that populates fake auth data
app.use((req, _res, next) => {
req.currentUser = {
_id: '42',
type: 'ADMIN',
};
next();
});

user.setupREST(app);
shopping.setupREST(app);

const port = process.env.PORT || 5000;
app.listen(port);

console.log(`Listening on port ${port}`);
})();
17 changes: 17 additions & 0 deletions src/infrastructure/db.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const { MongoClient } = require('mongodb');

async function connectDb() {
const client = await MongoClient.connect(
'mongodb://localhost',
{ useNewUrlParser: true }
);

console.log('Connected to mongo');

// return handle to database
return client.db('db');
}

module.exports = {
connectDb,
};
19 changes: 19 additions & 0 deletions src/infrastructure/dbHelpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
const ObjectID = require('mongodb').ObjectID;

/**
* Builds a mongoDB query filtering for a given list of object ids
* If ids is not an array, returns an empty query (i.e: no filter)
*
* @param {?Array<string>} ids
*/
const idsFilter = ids => {
if (!Array.isArray(ids)) {
return {};
}

return { _id: { $in: ids.map(ObjectID) } };
};

module.exports = {
idsFilter,
};
10 changes: 10 additions & 0 deletions src/shopping/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const getServices = ({ dbClient }) => {
return {};
};

const setupREST = app => {};

module.exports = {
getServices,
setupREST,
};
35 changes: 35 additions & 0 deletions src/user/account/__tests__/service.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
const serviceFactory = require('../service');

const getRepositoryMock = () => ({
getAccounts: jest.fn().mockResolvedValue([]),
});

describe('accountService', () => {
describe('getAccounts()', () => {
it('returns accounts list', async () => {
const service = serviceFactory({
accountRepository: getRepositoryMock(),
});

const testUser = {
type: 'ADMIN',
};

await expect(service.getAccounts(testUser)).resolves.toEqual([]);
});

it('throws if user is not admin', async () => {
const service = serviceFactory({
accountRepository: getRepositoryMock(),
});

const testUser = {
type: 'USER',
};

await expect(service.getAccounts(testUser)).rejects.toThrowError(
'Only admins can list accounts'
);
});
});
});
8 changes: 8 additions & 0 deletions src/user/account/model.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const ACCOUNT_TYPES = {
USER: 'USER',
ADMIN: 'ADMIN',
};

module.exports = {
ACCOUNT_TYPES,
};
40 changes: 40 additions & 0 deletions src/user/account/repository.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
const ObjectID = require('mongodb').ObjectID;

const { idsFilter } = require('../../infrastructure/dbHelpers');

module.exports = ({ dbClient }) => {
const getAccount = async accountId => {
const account = await dbClient.collection('accounts').findOne({
_id: ObjectID(accountId),
});

if (!account) {
// NOTE: here, we should probably throw instead so error handler can return a 404
return null;
}

return account;
};

const getAccounts = ({ accountIds } = {}) => {
return dbClient
.collection('accounts')
.find({
...idsFilter(accountIds),
})
.toArray();
};

const getAccountsForCompany = companyId => {
return dbClient
.collection('accounts')
.find({ company: companyId })
.toArray();
};

return {
getAccount,
getAccounts,
getAccountsForCompany,
};
};
46 changes: 46 additions & 0 deletions src/user/account/rest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
const handleGetAccount = async (req, res) => {
// extract service from context
const {
services: {
user: {
accountService: { getAccount },
},
},
} = req;

// extract input from query
const { accountId } = req.params;

// call service
const account = await getAccount(accountId);

// handle serialization of result from service
res.json(account);
};

const handleGetAccounts = async (req, res) => {
// extract service from context
const {
services: {
user: {
accountService: { getAccounts },
},
},
currentUser,
} = req;

// call service
const accounts = await getAccounts(currentUser);

// handle serialization of result from service
res.json(accounts);
};

const setupREST = app => {
app.get('/accounts/:accountId', handleGetAccount);
app.get('/accounts/', handleGetAccounts);
};

module.exports = {
setupREST,
};
25 changes: 25 additions & 0 deletions src/user/account/service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const { ACCOUNT_TYPES } = require('./model');

module.exports = ({ accountRepository }) => {
const getAccount = accountId => {
return accountRepository.getAccount(accountId);
};

const getAccounts = async currentUser => {
// only admins can list accounts
if (currentUser.type !== ACCOUNT_TYPES.ADMIN) {
throw new Error('Only admins can list accounts');
}
return accountRepository.getAccounts();
};

const getAccountsForCompany = companyId => {
return accountRepository.getAccountsForCompany(companyId);
};

return {
getAccount,
getAccounts,
getAccountsForCompany,
};
};
20 changes: 20 additions & 0 deletions src/user/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const getServices = ({ dbClient }) => {
const accountRepository = require('./account/repository')({
dbClient,
});

return {
accountService: require('./account/service')({
accountRepository,
}),
};
};

const setupREST = app => {
require('./account/rest').setupREST(app);
};

module.exports = {
getServices,
setupREST,
};
Loading

0 comments on commit 70e60ef

Please sign in to comment.