Skip to content

Commit bf59aae

Browse files
Merge pull request #10 from PeerPrep/implement-question-service
Implement Question Service
2 parents b74ba5e + 1865b1d commit bf59aae

File tree

16 files changed

+1584
-0
lines changed

16 files changed

+1584
-0
lines changed

.github/workflows/build-docker-images.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,33 @@ on:
77
- master
88

99
env:
10+
QUESTIONS_IMAGE_NAME: peerprep-questions-service
1011
FRONTEND_IMAGE_NAME: peerprep-frontend
1112
NGINX_IMAGE_NAME: peerprep-nginx
1213

1314
jobs:
15+
build-questions-image:
16+
runs-on: ubuntu-latest
17+
permissions:
18+
contents: read
19+
packages: write
20+
steps:
21+
- name: Check out Source
22+
uses: actions/checkout@v2
23+
- name: Log in to the Container Registry
24+
uses: docker/login-action@v1
25+
with:
26+
registry: ghcr.io
27+
username: peerprep
28+
password: ${{ secrets.GITHUB_TOKEN }}
29+
- name: Build and Push Docker Image
30+
uses: docker/build-push-action@v2
31+
with:
32+
context: .
33+
file: deployment/Dockerfile-questions
34+
push: true
35+
tags: ghcr.io/peerprep/${{ env.QUESTIONS_IMAGE_NAME }}:latest
36+
1437
build-frontend-image:
1538
runs-on: ubuntu-latest
1639
permissions:

deployment/Dockerfile-questions

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# To build a Docker image from this file, run from the root directory:
2+
# docker build -f deployment/Dockerfile-questions -t peerprep-questions-service .
3+
4+
# Intermediate image for building the Next app
5+
FROM node:18.17.1
6+
7+
# Environment variables
8+
ENV APP_ROOT /questions
9+
10+
# Copy source code into container
11+
RUN mkdir --parents $APP_ROOT
12+
WORKDIR $APP_ROOT
13+
COPY questions .
14+
15+
# Install dependencies
16+
RUN yarn install --frozen-lockfile
17+
18+
# Build app
19+
RUN yarn build
20+
21+
# Expose port
22+
EXPOSE 4000
23+
24+
# Final image for running the Express app
25+
CMD ["yarn", "start"]

deployment/docker-compose.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,40 @@
11
version: "3.8"
22

33
services:
4+
mongo:
5+
container_name: peerprep-mongo
6+
image: mongo
7+
restart: always
8+
volumes:
9+
- ./mongodb-data:/data/db
10+
logging:
11+
driver: journald
12+
networks:
13+
- peerprep-network
14+
expose:
15+
- "27017"
16+
17+
questions:
18+
image: ghcr.io/peerprep/peerprep-questions-service:latest
19+
container_name: peerprep-questions-service
20+
restart: always
21+
depends_on:
22+
- mongo
23+
networks:
24+
- peerprep-network
25+
logging:
26+
driver: journald
27+
expose:
28+
- "4000"
29+
environment:
30+
MONGODB_URL: "mongodb://peerprep-mongo:27017/questions"
31+
432
frontend:
533
image: ghcr.io/peerprep/peerprep-frontend:latest
634
container_name: peerprep-frontend
735
restart: always
36+
depends_on:
37+
- questions
838
networks:
939
- peerprep-network
1040
logging:

deployment/nginx/sites-enabled/peerprep.sivarn.com

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,14 @@ server {
5151
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
5252
proxy_set_header X-Forwarded-Proto $scheme;
5353
}
54+
55+
location /api/v1/questions/ {
56+
proxy_pass http://peerprep-questions-service:4000/api/v1/questions/;
57+
proxy_set_header Host $host;
58+
proxy_set_header X-Real-IP $remote_addr;
59+
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
60+
proxy_set_header X-Forwarded-Proto $scheme;
61+
}
5462
}
5563

5664
server {

questions/.gitignore

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
8+
# testing
9+
/coverage
10+
11+
# next.js
12+
/.next/
13+
/out/
14+
15+
# production
16+
/build
17+
18+
# misc
19+
.DS_Store
20+
*.pem
21+
22+
# debug
23+
npm-debug.log*
24+
yarn-debug.log*
25+
yarn-error.log*
26+
27+
# local env files
28+
.env*.local
29+
30+
# vercel
31+
.vercel
32+
33+
# typescript
34+
*.tsbuildinfo
35+
next-env.d.ts

questions/nodemon.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"watch": ["src"],
3+
"ext": ".ts,.js",
4+
"exec": "ts-node ./src/index.ts"
5+
}

questions/package.json

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"name": "questions",
3+
"version": "1.0.0",
4+
"description": "",
5+
"main": "index.js",
6+
"scripts": {
7+
"dev": "nodemon",
8+
"build": "tsc",
9+
"start": "node build/index.js"
10+
},
11+
"author": "",
12+
"license": "ISC",
13+
"dependencies": {
14+
"body-parser": "^1.20.2",
15+
"compression": "^1.7.4",
16+
"cors": "^2.8.5",
17+
"express": "^4.18.2",
18+
"mongoose": "^7.5.1"
19+
},
20+
"devDependencies": {
21+
"@types/body-parser": "^1.19.2",
22+
"@types/compression": "^1.7.3",
23+
"@types/cors": "^2.8.14",
24+
"@types/express": "^4.17.17",
25+
"@types/mongoose": "^5.11.97",
26+
"dotenv": "^16.3.1",
27+
"nodemon": "^3.0.1",
28+
"ts-node": "^10.9.1",
29+
"typescript": "^5.2.2"
30+
}
31+
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import express from "express";
2+
import { QuestionDao } from "../models/questions";
3+
import { ApiResponse, StatusMessageType } from "../types";
4+
import { handleCustomError, handleServerError } from "../utils";
5+
6+
// GET /questions/:id
7+
export const getQuestion = async (
8+
req: express.Request,
9+
res: express.Response
10+
) => {
11+
try {
12+
const id = req.params.id;
13+
if (!id) {
14+
handleCustomError(res, {
15+
type: StatusMessageType.ERROR,
16+
message: "Question ID must be provided",
17+
});
18+
}
19+
20+
const question = await QuestionDao.getQuestionById(id);
21+
if (!question) {
22+
handleCustomError(res, {
23+
type: StatusMessageType.ERROR,
24+
message: "Question not found",
25+
});
26+
}
27+
28+
const response: ApiResponse = {
29+
payload: question,
30+
statusMessage: null,
31+
};
32+
res.status(200).json(response);
33+
} catch (error) {
34+
handleServerError(error, res);
35+
}
36+
};
37+
38+
// POST /questions
39+
export const createQuestion = async (
40+
req: express.Request,
41+
res: express.Response
42+
) => {
43+
try {
44+
const { title, description, tags, difficulty } = req.body;
45+
if (!title || !description || !difficulty) {
46+
handleCustomError(res, {
47+
type: StatusMessageType.ERROR,
48+
message: "Title, description and difficulty must be provided",
49+
});
50+
}
51+
52+
if (!tags || tags.length === 0) {
53+
handleCustomError(res, {
54+
type: StatusMessageType.ERROR,
55+
message: "At least one tag must be provided",
56+
});
57+
}
58+
59+
const question = await QuestionDao.createQuestion(
60+
title,
61+
description,
62+
tags,
63+
difficulty
64+
);
65+
const response: ApiResponse = {
66+
payload: question,
67+
statusMessage: {
68+
type: StatusMessageType.SUCCESS,
69+
message: "Question created successfully",
70+
},
71+
};
72+
res.status(201).json(response);
73+
} catch (error) {
74+
handleServerError(error, res);
75+
}
76+
};
77+
78+
// PUT /questions/:id
79+
export const updateQuestion = async (
80+
req: express.Request,
81+
res: express.Response
82+
) => {
83+
try {
84+
const id = req.params.id;
85+
const { title, description, tags, difficulty } = req.body;
86+
if (!id || !title || !description || !difficulty) {
87+
handleCustomError(res, {
88+
type: StatusMessageType.ERROR,
89+
message:
90+
"Question ID, title, description and difficulty must be provided",
91+
});
92+
}
93+
94+
if (!tags || tags.length === 0) {
95+
handleCustomError(res, {
96+
type: StatusMessageType.ERROR,
97+
message: "At least one tag must be provided",
98+
});
99+
}
100+
101+
const question = await QuestionDao.updateQuestion(
102+
id,
103+
title,
104+
description,
105+
tags,
106+
difficulty
107+
);
108+
if (!question) {
109+
handleCustomError(res, {
110+
type: StatusMessageType.ERROR,
111+
message: "Question not found",
112+
});
113+
}
114+
115+
const response: ApiResponse = {
116+
payload: question,
117+
statusMessage: {
118+
type: StatusMessageType.SUCCESS,
119+
message: "Question updated successfully",
120+
},
121+
};
122+
res.status(200).json(response);
123+
} catch (error) {
124+
handleServerError(error, res);
125+
}
126+
};
127+
128+
// DELETE /questions/:id
129+
export const deleteQuestion = async (
130+
req: express.Request,
131+
res: express.Response
132+
) => {
133+
try {
134+
const id = req.params.id;
135+
if (!id) {
136+
handleCustomError(res, {
137+
type: StatusMessageType.ERROR,
138+
message: "Question ID must be provided",
139+
});
140+
}
141+
142+
const question = await QuestionDao.deleteQuestion(id);
143+
if (!question) {
144+
handleCustomError(res, {
145+
type: StatusMessageType.ERROR,
146+
message: "Question not found",
147+
});
148+
}
149+
150+
const response: ApiResponse = {
151+
payload: question,
152+
statusMessage: {
153+
type: StatusMessageType.SUCCESS,
154+
message: "Question deleted successfully",
155+
},
156+
};
157+
res.status(200).json(response);
158+
} catch (error) {
159+
handleServerError(error, res);
160+
}
161+
};

questions/src/index.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import express from "express";
2+
import bodyParser from "body-parser";
3+
import compression from "compression";
4+
import cors, { CorsOptions } from "cors";
5+
import http from "http";
6+
import mongoose from "mongoose";
7+
import router from "./router";
8+
9+
const MONGO_URL =
10+
process.env.MONGODB_URL || "mongodb://localhost:27017/questions";
11+
12+
mongoose.Promise = Promise;
13+
mongoose.connect(MONGO_URL);
14+
mongoose.connection.on("error", console.error);
15+
16+
const corsOptions: CorsOptions = {
17+
origin: "*",
18+
};
19+
20+
const app = express();
21+
22+
app.use(cors(corsOptions));
23+
app.use(compression());
24+
app.use(bodyParser.json());
25+
26+
const server = http.createServer(app);
27+
28+
server.listen(4000, () => {
29+
console.log("Server is listening on port 4000");
30+
});
31+
32+
app.use("/api/v1/", router());

0 commit comments

Comments
 (0)