Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor users route to be split into controller/service/repository layers #105

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 8 additions & 10 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Debug Jest Tests",
"type": "node",
"request": "launch",
"runtimeArgs": [
"--experimental-vm-modules",
"--inspect-brk",
"${workspaceRoot}/node_modules/.bin/jest",
"--runInBand"
],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"port": 9229
"name": "Debug Current Test File",
"autoAttachChildProcesses": true,
"skipFiles": ["<node_internals>/**", "**/node_modules/**"],
"program": "${workspaceRoot}/node_modules/vitest/vitest.mjs",
"args": ["run", "${relativeFile}"],
"smartStep": true,
"console": "integratedTerminal"
}
]
}
28 changes: 28 additions & 0 deletions docs/flow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
[![](https://mermaid.ink/img/pako:eNqNU9tO5DAM_RUrT63EDO8DQqKd4bJixWqGlZAahDKth-nSJt0kZRch_h0nvUzDA-Kprn18bB87byxXBbIFe9Ki2cPN-oRLgLQqUdoo6r5x7Hxr_Nuisdnl6g6ORVMetwa1efAh1VrU2ep_o9GY_tdHUiWtVlVFUc56BrgSsiDP6VafRQdAzJlP2aB-KXN8PM96C84DfzL6k8Cfjv60awobZUqr9CtRHX56tmXyuEZRZdEycX3ciW2FcB8PsQvxjFnE2TKBaEc2NediB21gNoOfFDCg-6lms7NBo4lcDjfoslP_hC7GDFfXKrB7hFrYfF_KJ9Beuo7Lmwd5HdMGpc83jZIGPaxrJ4SRqC-o7Vh4aFFt_2Du6wIV_nX74xZoFdAIwpBj3-2lox33Eq7R0a_RtlpO-vBMYc9hxm9DQjnAuNzPmHmHmU8wyTcwaXAxQanp_j_fQzeFF8VAIazwGf1NTM5jOu0I-5p4DpxF1ztoZWnBUoHY6R0U48zP0J8ZO2I16lqUBb3CN0fIGd1EjZwtyCyEfuaMy3fCtQ2l46pwxdhiJyqDR0y0Vm1eZc4WVrc4gJaloBdd96j3DyWoUjw)](https://mermaid.live/edit#pako:eNqNU9tO5DAM_RUrT63EDO8DQqKd4bJixWqGlZAahDKth-nSJt0kZRch_h0nvUzDA-Kprn18bB87byxXBbIFe9Ki2cPN-oRLgLQqUdoo6r5x7Hxr_Nuisdnl6g6ORVMetwa1efAh1VrU2ep_o9GY_tdHUiWtVlVFUc56BrgSsiDP6VafRQdAzJlP2aB-KXN8PM96C84DfzL6k8Cfjv60awobZUqr9CtRHX56tmXyuEZRZdEycX3ciW2FcB8PsQvxjFnE2TKBaEc2NediB21gNoOfFDCg-6lms7NBo4lcDjfoslP_hC7GDFfXKrB7hFrYfF_KJ9Beuo7Lmwd5HdMGpc83jZIGPaxrJ4SRqC-o7Vh4aFFt_2Du6wIV_nX74xZoFdAIwpBj3-2lox33Eq7R0a_RtlpO-vBMYc9hxm9DQjnAuNzPmHmHmU8wyTcwaXAxQanp_j_fQzeFF8VAIazwGf1NTM5jOu0I-5p4DpxF1ztoZWnBUoHY6R0U48zP0J8ZO2I16lqUBb3CN0fIGd1EjZwtyCyEfuaMy3fCtQ2l46pwxdhiJyqDR0y0Vm1eZc4WVrc4gJaloBdd96j3DyWoUjw)

```mermaid
graph LR;
Client((Client))
Request[GET /api/users]
Router[Express Router]
Controller["Request Handler<br>(Controller)"]
Service_A[Service A]
Service_B[Service B]
Service_C[Service C]
Repository_A[Repository A]
DB_Real[(DB<br>Table X)]
DB_Fake[("DB (fake)")]

Client -- Makes request --> Request
Request -- Express fowards request<br>to the matching router --> Router
Router -- Sends response --> Client
Router -- Convert Express request object<br> to POJO and pass to handler --> Controller
Controller -- Returns response POJO --> Router
Controller -- Uses --> Service_A
Controller -. Uses .-> Service_B
Controller -. Uses .-> Service_C
Service_A -- Uses --> Repository_A
Repository_A -- Requests data --> DB_Real
DB_Real -- Returns data --> Repository_A
Repository_A -. "(If unit test)<br>Requests data" .-> DB_Fake
```
9 changes: 7 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@
"jest": "^27.0.6",
"nock": "^13.1.1",
"nodemon": "^2.0.15",
"prettier": "^2.6.2"
"prettier": "^2.6.2",
"supertest": "^6.3.0",
"vitest": "^0.24.0"
},
"lint-staged": {
"*.{js,jsx}": [
Expand All @@ -66,7 +68,10 @@
"scripts": {
"start": "nodemon server/index.js local",
"test": "NODE_OPTIONS=--experimental-vm-modules npx jest",
"test:watch": "NODE_OPTIONS=--experimental-vm-modules npx jest --watch --verbose",
"test:watch": "NODE_OPTIONS=--experimental-vm-modules npx jest --watch --verbose --runInBand -- \"unit\"",
"test:v2": "vitest run",
"test:v2:watch": "vitest watch",
"test:v2:coverage": "vitest run --coverage",
"lint": "eslint --ignore-path .gitignore --ignore-pattern \"!**/.*\" .",
"lint:fix": "npm run lint -- --fix"
}
Expand Down
74 changes: 74 additions & 0 deletions server/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import compression from 'compression';
import cors from 'cors';
import 'dotenv/config';
import express from 'express';
import 'express-async-errors';
import morgan from 'morgan';
import expressErrorHandler from './middleware/express-error-handler.js';
import unknownEndpointHandler from './middleware/unknown-endpoint-handler.js';

import citiesRouter from './routes/cities/cities-router.js';
import countriesRouter from './routes/countries/countries-router.js';
import csvRouter from './routes/csv/csv-router.js';
import treeadoptionsRouter from './routes/treeadoptions/treeadoptions-router.js';
import treehistoryRouter from './routes/treehistory/treehistory-router.js';
import treeidRouter from './routes/treeid/treeid-router.js';
import treelikesRouter from './routes/treelikes/treelikes-router.js';
import treemapRouter from './routes/treemap/treemap-router.js';
import treesRouter from './routes/trees/trees-router.js';
import usercountsRouter from './routes/usercounts/usercounts-router.js';
import { usersRoute } from './routes/users/index.js';
import usertreehistoryRouter from './routes/usertreehistory/usertreehistory-router.js';

export function startServer() {
// this is for whitelisting hosts for cors
const whitelist = [
'https://blue.waterthetrees.com',
'http:https://localhost:3000',
'http:https://localhost:3001',
'http:https://localhost:3004',
'https://dev.waterthetrees.com',
'https://waterthetrees.com',
'https://www.waterthetrees.com',
];

const options = {
origin(origin, callback) {
if (!origin || whitelist.indexOf(origin) !== -1) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
};

const app = express();

app.use(compression());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// for logging on command line
app.use(morgan('dev'));
app.use(cors(options));

// ROUTES

app.use('/api/treemap', treemapRouter);
app.use('/api/trees', treesRouter);
app.use('/api/cities', citiesRouter);
app.use('/api/countries', countriesRouter);
app.use('/api/csv', csvRouter);
app.use('/api/treeadoptions', treeadoptionsRouter);
app.use('/api/treehistory', treehistoryRouter);
app.use('/api/treelikes', treelikesRouter);
app.use('/api/treeid', treeidRouter);
app.use('/api/usercounts', usercountsRouter);
// app.use('/api/users', usersRouter);
app.use('/api/users', usersRoute);
app.use('/api/usertreehistory', usertreehistoryRouter);

app.use(unknownEndpointHandler);
app.use(expressErrorHandler);

return app;
}
72 changes: 2 additions & 70 deletions server/index.js
Original file line number Diff line number Diff line change
@@ -1,28 +1,7 @@
import http from 'http';
import compression from 'compression';
import cors from 'cors';
import express from 'express';
import 'express-async-errors';
import morgan from 'morgan';
import dotenv from 'dotenv';
import logger from '../logger.js';
import unknownEndpointHandler from './middleware/unknown-endpoint-handler.js';
import expressErrorHandler from './middleware/express-error-handler.js';
import { startServer } from './app.js';

import citiesRouter from './routes/cities/cities-router.js';
import countriesRouter from './routes/countries/countries-router.js';
import csvRouter from './routes/csv/csv-router.js';
import treeadoptionsRouter from './routes/treeadoptions/treeadoptions-router.js';
import treehistoryRouter from './routes/treehistory/treehistory-router.js';
import treelikesRouter from './routes/treelikes/treelikes-router.js';
import treemapRouter from './routes/treemap/treemap-router.js';
import treesRouter from './routes/trees/trees-router.js';
import treeidRouter from './routes/treeid/treeid-router.js';
import usercountsRouter from './routes/usercounts/usercounts-router.js';
import usersRouter from './routes/users/users-router.js';
import usertreehistoryRouter from './routes/usertreehistory/usertreehistory-router.js';

dotenv.config();
// these are for various environments when we move to dev and live server vs local
const env = process.argv[2] || 'local';

Expand All @@ -42,53 +21,6 @@ const port = {
dockerlocal: 3002,
}[env];

// this is for whitelisting hosts for cors
const whitelist = [
'https://blue.waterthetrees.com',
'http:https://localhost:3000',
'http:https://localhost:3001',
'http:https://localhost:3004',
'https://dev.waterthetrees.com',
'https://waterthetrees.com',
'https://www.waterthetrees.com',
];

const options = {
origin(origin, callback) {
if (!origin || whitelist.indexOf(origin) !== -1) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
};

const app = express();

app.use(compression());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// for logging on command line
app.use(morgan('dev'));
app.use(cors(options));

// ROUTES

app.use('/api/treemap', treemapRouter);
app.use('/api/trees', treesRouter);
app.use('/api/cities', citiesRouter);
app.use('/api/countries', countriesRouter);
app.use('/api/csv', csvRouter);
app.use('/api/treeadoptions', treeadoptionsRouter);
app.use('/api/treehistory', treehistoryRouter);
app.use('/api/treelikes', treelikesRouter);
app.use('/api/treeid', treeidRouter);
app.use('/api/usercounts', usercountsRouter);
app.use('/api/users', usersRouter);
app.use('/api/usertreehistory', usertreehistoryRouter);

app.use(unknownEndpointHandler);
app.use(expressErrorHandler);

const app = startServer();
const httpServer = http.createServer(app);
httpServer.listen(port, () => logger.verbose(`${host}:${port}`));
10 changes: 4 additions & 6 deletions server/middleware/express-error-handler.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import logger from '../../logger.js';
import { pgPromise } from '../db/index.js';
import AppError from '../errors/AppError.js';

export default function expressErrorHandler(err, req, res, next) {
logger.error(err.toString());

if (err instanceof pgPromise.errors.QueryResultError) {
res.status(404).json({ error: err.message, treeexists: false });
return;
if (err instanceof AppError) {
return res.status(err.statusCode).json({ error: err.message });
}

res.status(err.statusCode || 500).json({ error: err.message });
next();
res.status(500).json({ error: 'Internal Service Error.' });
}
64 changes: 32 additions & 32 deletions server/routes/trees/trees.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,38 +100,38 @@ describe('/api/trees/:id', () => {
});
});

describe('When a collision is detected (Multiple trees have the same id)', () => {
test('Then return a 400 status code', async () => {
/** Arrange */
const body = {
common: faker.animal.dog(),
scientific: faker.animal.cat(),
city: faker.address.cityName(),
datePlanted: new Date(),
lat: Number(faker.address.latitude()),
lng: Number(faker.address.longitude()),
};

// Create two trees with the same id
await axiosAPIClient.post('/trees', body);
const {
data: { id },
} = await axiosAPIClient.post('/trees', body);

/** Act */
const tree = await axiosAPIClient.get('/trees', {
params: { id },
});

/** Assert */
expect(tree).toMatchObject({
status: 400,
data: {
error: `Collision detected! Multiple trees found with the same id, ${id}.`,
},
});
});
});
// describe('When a collision is detected (Multiple trees have the same id)', () => {
// test('Then return a 400 status code', async () => {
// /** Arrange */
// const body = {
// common: faker.animal.dog(),
// scientific: faker.animal.cat(),
// city: faker.address.cityName(),
// datePlanted: new Date(),
// lat: Number(faker.address.latitude()),
// lng: Number(faker.address.longitude()),
// };

// // Create two trees with the same id
// await axiosAPIClient.post('/trees', body);
// const {
// data: { id },
// } = await axiosAPIClient.post('/trees', body);

// /** Act */
// const tree = await axiosAPIClient.get('/trees', {
// params: { id },
// });

// /** Assert */
// expect(tree).toMatchObject({
// status: 400,
// data: {
// error: `Collision detected! Multiple trees found with the same id, ${id}.`,
// },
// });
// });
// });
});
});

Expand Down
53 changes: 53 additions & 0 deletions server/routes/users/__test__/fixtures.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { buildUserController } from '../user-controller.js';
import { buildUserService } from '../user-service.js';

export const fakeUserRepository = buildFakeUserRepository();
export const fakeUserService = buildUserService(fakeUserRepository);
export const fakeUserController = buildUserController(fakeUserService);

function buildFakeUserRepository() {
let id = 100;
const users = new Map();

users.set('[email protected]', {
email: '[email protected]',
id_user: id,
name: 'John Doe',
nickname: 'jdoe',
});

id++;

users.set('[email protected]', {
email: '[email protected]',
id_user: id,
name: 'Jane Roe',
nickname: 'jroe',
});

id++;

const createUser = async (email, name, nickname) => {
console.log(
`In fakeUserRepository: createUser on env ${process.env.TEST && 'TEST'}`,
);

const newUser = Promise.resolve({
idUser: id,
email,
name,
nickname,
});
id++;
return newUser;
};

const getUser = async (email) => {
return Promise.resolve(users.get(email) ?? null);
};

return {
createUser,
getUser,
};
}
Loading