-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
18 changed files
with
7,841 additions
and
0 deletions.
There are no files selected for viewing
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
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -102,3 +102,7 @@ dist | |
|
||
# TernJS port file | ||
.tern-port | ||
|
||
# other files | ||
data/ | ||
logs/ |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
FROM node:latest | ||
RUN mkdir -p /usr/src/app | ||
WORKDIR /usr/src/app | ||
COPY package.json /usr/src/app | ||
# RUN sed -i "s/mongodb:\/\/localhost/mongodb:\/\/mongo/g" config/services/mongoose.service.js | ||
RUN npm install | ||
COPY . /usr/src/app | ||
EXPOSE 3600 | ||
CMD ["node", "index.js"] |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
# Introduction | ||
|
||
This is the backend implementation of the "Spam Reporting Management System". | ||
It is a very simple backend implementation that allows to mainly do two things: | ||
|
||
- Retrieve a list of active tickets (whose related content is either active/open or removed/blocked) | ||
- Update the status of the tickets by either **resolving** it or **blocking** its related content. | ||
|
||
## Requirements | ||
|
||
As explained in the [documentation task](https://github.com/morkro/coding-challenge), we have the following requirements: | ||
|
||
- Provide a web-app UI where users can see active tickets | ||
- Each ticket can be: | ||
- Resolved/Closed - this means that the ticket won't be anymore visible on the platform | ||
- Block - this means that the content to which the ticket refers will be blocked and not visible on our social media platform. **The ticket is still visible in the system and can be further resolved.** | ||
|
||
### Technical Requirements | ||
|
||
- The whole task must be implemented in Javascript. | ||
- As per official documentation, _the resolving should be defined as a PUT request to an endpoint with this structure `/reports/:reportId`. An example request for how to update a report is in `data/update_ticket_request.json`_ | ||
- as a consequence this means we have to use RESTful APIs | ||
|
||
# Tenets | ||
|
||
- **Simplicity**: this project is simple, has basic requirements and it should be easy to understand. I should avoid overengineering stuff | ||
- **Time to market**: for this challenge, I shouldn't spend more than three hours | ||
- **Adaptable**: it should be fairly easy to quickly expand the report structure to a different format | ||
|
||
# Technology stack | ||
|
||
**The backend can be ran on Docker.** | ||
|
||
The technology chose **for the whole project** is based on the **MERN framework**: | ||
|
||
- Mongoose (Backend) | ||
- Express (Backend) | ||
- React (Frontend) | ||
- NodeJS (Backend) | ||
|
||
The implementation language is **JavaScript**. | ||
|
||
## Why this technology stack | ||
|
||
I had two main requirements: use **Javascript** and **RESTful APIs**. As a consequence I believe the MERN framework is the most ideal technology stack choice: | ||
|
||
- it's extremely well known in the market hence easy to ramp up | ||
- it's very intuitive, easy to use and a lot of features are available | ||
- go-to-market is fast: with these technologies you can create a service in few hours | ||
|
||
### What would I have chosen differently? | ||
|
||
I would have opted for **Typescript** and **GraphQL** (with Relay support for React): | ||
|
||
- TypeScript: it better allows static typing and it's a great alternative to Javascript as its strict syntactical superset. | ||
- GraphQL: in the real world, GraphQL is a great alternative to RESTful APIs when it's about dealing with a lot of data being passed from backend and frontend. In this case, I can imagine that the list of reports might grow quite a lot therefore the great support that GraphQL has for **pagination** would be a great advantage. | ||
- In addition to that: let's say that the definition of a report gets more complicated and you only need a few fields from it: with GraphQL you can avoid running multiple queries and retrieve all the data you need by using its graph query language. | ||
|
||
# How to use | ||
|
||
## Deploy on Docker | ||
|
||
You should be familiar with Docker, but in case these are the commands to use: | ||
|
||
- `cd <backend_folder>` | ||
- `docker build -t reporting-spam-backend .` | ||
- `docker-compose up` | ||
|
||
The server will listen on the port `3600`. | ||
|
||
## Call the service | ||
|
||
As it can be seen in the `sample_queries` file, you can call the server with these two APIs: | ||
|
||
``` | ||
curl -X GET \ | ||
-H "Content-type: application/json" \ | ||
-H "Accept: application/json" \ | ||
-d '{}' \ | ||
"http://localhost:3600/get_reports" | ||
``` | ||
|
||
``` | ||
curl -X POST \ | ||
-H "Content-type: application/json" \ | ||
-H "Accept: application/json" \ | ||
-d '{"ticketState": "CLOSED"}' \ | ||
"http://localhost:3600/reports/07b74660-b92e-4cd9-8ec8-016bbb6d6edc" | ||
``` | ||
|
||
As you can see, I expose **two endpoints**: | ||
|
||
- `GET get_reports` | ||
- `POST reports/{:reportId}` | ||
|
||
The POST request accepts one field in input, which is `ticketState`. **Any other field will be ignored** - if you don't specify the `ticketState` field the request will return a `400 bad request message` | ||
|
||
# Date layer | ||
|
||
I opted for a very simplified data layer, using Mongoose (hence MongoDB). | ||
|
||
There is only one collection which is defined by its main 4 fields visible on the UI: | ||
|
||
- Id | ||
- State | ||
- Type | ||
- Message | ||
|
||
This can be seen at `backend/models/mongoose/report.model.js`. | ||
|
||
## How is data loaded | ||
|
||
[This file](https://github.com/morkro/coding-challenge/blob/master/data/reports.json) is used to load data into the database whenever requested. There is an `.env` file which contains two properties that can let you decide if you want to dump and insert new data or start it with the existing data: | ||
|
||
- `RESET_DATABASE=true` | ||
- `NODE_ENV=dev` | ||
|
||
The `reports.json` file contains a lot of information that cannot be easily explained and seems to refer to a much bigger use case than the one being presented for this coding exercise. | ||
For the sake of simplicity, I ignored most of these fields when it's about fetching data from the server and the database. | ||
|
||
# Project structure | ||
|
||
The project structure should be easy to follow and quite similar to the standard structure: | ||
|
||
- The entry point of the project is `index.js` | ||
- `web/`: contains the `controllers/`, `middleware/` and `routes/` folders that are representing our API endpoints exposure | ||
- in the `routes/` folder you will see how the controller and middleware are related to the two endpoints we expose | ||
- the `reports.controller.js` file is probably the most important in this simple project because it contains the logic on how the data model is used when APIs are called. | ||
- `models/`: contains the file related to the (only) implementation of our data layer, done using Mongoose. | ||
|
||
This project uses `Winston` for logging, `dotenv` to store property configurations and `jest` for testing. | ||
|
||
# Testing | ||
|
||
A lot more could have been tested but I mainly wanted to write a basic test to prove: | ||
|
||
- my familiarity with testing, mocking, TDD and SOLID principles | ||
- the importance I give to testing | ||
|
||
# What to improve | ||
|
||
- Pagination needs to be implemented in order to manage too many results. | ||
- Authentication should be handled | ||
- `PUT /reports/:reportId` does not well indicate what is trying to do - I would advocate for more intuitive APIs | ||
- The `Resolve` button deletes the element from the database. In real applications, you might want to defer that process to a later time (with a batch job) and keep that archived for audit purposes. For privacy reasons, we are forced anyway to remove the content at some point. |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
"use strict"; | ||
|
||
const winston = require("winston"); | ||
|
||
const logger = winston.createLogger({ | ||
level: "info", | ||
format: winston.format.json(), | ||
// defaultMeta: { service: 'user-service' }, | ||
transports: [ | ||
// | ||
// - Write all logs with importance level of `error` or less to `error.log` | ||
// - Write all logs with importance level of `info` or less to `combined.log` | ||
// | ||
new winston.transports.File({ filename: "logs/error.log", level: "error" }), | ||
new winston.transports.File({ filename: "logs/combined.log" }), | ||
], | ||
}); | ||
|
||
// | ||
// If we're not in production then log to the `console` with the format: | ||
// `${info.level}: ${info.message} JSON.stringify({ ...rest }) ` | ||
// | ||
if (process.env.NODE_ENV !== "production") { | ||
logger.add( | ||
new winston.transports.Console({ | ||
format: winston.format.simple(), | ||
}) | ||
); | ||
} | ||
|
||
module.exports = 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
"use strict"; | ||
|
||
const logger = require("../components/winston.logger.js"); | ||
const mongoose = require("mongoose"); | ||
const DataFetcher = require("../../utils/data/fetcher.js"); | ||
|
||
mongoose.set("strictQuery", false); | ||
|
||
const options = { | ||
autoIndex: false, // Don't build indexes | ||
useNewUrlParser: true, | ||
useUnifiedTopology: true, | ||
}; | ||
|
||
async function connect() { | ||
logger.info("MongoDB connection with retry"); | ||
await mongoose.connect( | ||
`mongodb://${process.env.MONGOOSE_SERVER_ADDRESS}:${process.env.MONGOOSE_SERVER_PORT}/spam-reporting-service`, | ||
options | ||
); | ||
logger.info("MongoDB is connected"); | ||
|
||
mongoose.connection.once( | ||
"open", | ||
async (err, resp) => await DataFetcher.populateDatabase() | ||
); | ||
} | ||
|
||
connect().catch((err) => { | ||
logger.error(`MongoDB connection unsuccessful - ${err}`); | ||
}); | ||
|
||
exports.mongoose = mongoose; |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
version: "3" | ||
services: | ||
app: | ||
container_name: app | ||
image: reporting-spam-backend | ||
restart: always | ||
build: . | ||
ports: | ||
- "3600:3600" | ||
links: | ||
- mongo | ||
mongo: | ||
container_name: mongo | ||
image: mongo | ||
volumes: | ||
- ./data:/data/db | ||
ports: | ||
- "27017:27017" | ||
# api: | ||
# image: marcosuma/mckinsey-coding-challenge | ||
# build: . | ||
# networks: | ||
# - backend | ||
# ports: | ||
# - "3000:3000" | ||
# depends_on: | ||
# - mongo | ||
|
||
# mongo: | ||
# image: mongo | ||
# volumes: | ||
# - ./data:/data/db | ||
# networks: | ||
# - backend | ||
# ports: | ||
# - "27017:27017" | ||
|
||
# web-cli: | ||
# image: marcosuma/mckinsey-coding-challenge | ||
# links: | ||
# - mongo | ||
# networks: | ||
# - backend | ||
# command: sh | ||
|
||
networks: | ||
backend: | ||
driver: bridge |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
"use strict"; | ||
|
||
require("dotenv/config"); | ||
const express = require("express"); | ||
const app = express(); | ||
const routes = require("./web/routes/routes.js"); | ||
const logger = require("./config/components/winston.logger.js"); | ||
|
||
app.use(function (req, res, next) { | ||
res.header("Access-Control-Allow-Origin", "*"); | ||
res.header("Access-Control-Allow-Credentials", "true"); | ||
res.header("Access-Control-Allow-Methods", "GET,PUT"); | ||
res.header("Access-Control-Expose-Headers", "Content-Length"); | ||
res.header( | ||
"Access-Control-Allow-Headers", | ||
"Accept, Authorization, Content-Type, X-Requested-With, Range" | ||
); | ||
return next(); | ||
}); | ||
|
||
app.use(express.json()); | ||
routes.routesConfig(app); | ||
|
||
app.listen(process.env.PORT, function () { | ||
logger.info(`app listening at port ${process.env.PORT}.`); | ||
}); |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
module.exports = { | ||
clearMocks: true, | ||
// An array of file extensions your modules use | ||
moduleFileExtensions: ["js", "json", "jsx"], | ||
coveragePathIgnorePatterns: [ | ||
"<rootDir>/dist/", | ||
"<rootDir>/node_modules/", | ||
"<rootDir>/docs/", | ||
"<rootDir>/build/", | ||
], | ||
testPathIgnorePatterns: [ | ||
"<rootDir>/dist/", | ||
"<rootDir>/node_modules/", | ||
"<rootDir>/docs/", | ||
"<rootDir>/build/", | ||
], | ||
}; |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
"use strict"; | ||
|
||
const ReportModel = require("./mongoose/report.model.js"); | ||
|
||
exports.getReportModel = () => { | ||
if (process.env.DATA_MODEL == "mongoose") { | ||
return ReportModel; | ||
} | ||
throw new Error("No data model corresponding to: " + process.env.DATA_MODEL); | ||
}; |
Oops, something went wrong.