Skip to content

Commit 6e2086e

Browse files
committed
Resolve confict with dev branch
2 parents de9bb4e + e462714 commit 6e2086e

File tree

13 files changed

+536
-0
lines changed

13 files changed

+536
-0
lines changed
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
name: CI (backend - question service)
2+
3+
on:
4+
pull_request:
5+
branches: [dev, staging]
6+
paths:
7+
- "backend/question-service/**"
8+
- "pnpm-lock.yaml"
9+
- "package.json"
10+
- ".github/workflows/ci-question-service.yml"
11+
push:
12+
branches: [dev, staging]
13+
paths:
14+
- "backend/question-service/**"
15+
- "pnpm-lock.yaml"
16+
- "package.json"
17+
- ".github/workflows/ci-question-service.yml"
18+
19+
concurrency:
20+
group: ci-${{ github.workflow }}-${{ github.ref }}
21+
cancel-in-progress: true
22+
23+
jobs:
24+
build-test:
25+
name: Build & Smoke tests
26+
runs-on: ubuntu-latest
27+
env:
28+
CI: true
29+
services:
30+
mongodb:
31+
image: mongo:8
32+
ports:
33+
- 27017:27017
34+
35+
steps:
36+
- name: Checkout
37+
uses: actions/checkout@v4
38+
39+
- name: Setup Node
40+
uses: actions/setup-node@v4
41+
with:
42+
node-version: "20.x"
43+
44+
- name: Enable Corepack & pin pnpm
45+
run: |
46+
corepack enable
47+
corepack prepare [email protected] --activate
48+
pnpm -v
49+
50+
- name: Install dependencies
51+
run: pnpm install --frozen-lockfile
52+
53+
- name: Setup Docker Buildx
54+
uses: docker/setup-buildx-action@v3
55+
56+
- name: Build Docker image
57+
uses: docker/build-push-action@v6
58+
with:
59+
context: ./backend/question-service
60+
file: ./backend/question-service/Dockerfile.question
61+
push: false
62+
tags: question-service:test
63+
cache-from: type=gha
64+
cache-to: type=gha,mode=max
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Include any files or directories that you don't want to be copied to your
2+
# container here (e.g., local build artifacts, temporary files, etc.).
3+
#
4+
# For more help, visit the .dockerignore file reference guide at
5+
# https://docs.docker.com/go/build-context-dockerignore/
6+
7+
**/.classpath
8+
**/.dockerignore
9+
**/.env
10+
**/.git
11+
**/.gitignore
12+
**/.project
13+
**/.settings
14+
**/.toolstarget
15+
**/.vs
16+
**/.vscode
17+
**/.next
18+
**/.cache
19+
**/*.*proj.user
20+
**/*.dbmdl
21+
**/*.jfm
22+
**/charts
23+
**/docker-compose*
24+
**/compose.y*ml
25+
**/Dockerfile*
26+
**/node_modules
27+
**/npm-debug.log
28+
**/obj
29+
**/secrets.dev.yaml
30+
**/values.dev.yaml
31+
**/build
32+
**/dist
33+
LICENSE
34+
README.md
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Use Node.js 20 Alpine as base image
2+
FROM node:20-alpine
3+
4+
# Set working directory
5+
WORKDIR /app
6+
7+
# Install pnpm globally
8+
RUN npm install -g [email protected]
9+
10+
# Copy package files
11+
COPY package.json pnpm-lock.yaml* ./
12+
13+
# Install dependencies (including dev dependencies for development)
14+
RUN pnpm install --no-frozen-lockfile
15+
16+
# Copy source code
17+
COPY . .
18+
19+
# Create non-root user for security
20+
RUN addgroup -g 1001 -S nodejs
21+
RUN adduser -S question-service -u 1001
22+
23+
# Change ownership of the app directory
24+
RUN chown -R question-service:nodejs /app
25+
USER question-service
26+
27+
# Expose port the service uses
28+
EXPOSE 8003
29+
30+
# Start the application
31+
CMD ["pnpm", "run", "dev"]
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "question-service",
3+
"version": "1.0.0",
4+
"description": "",
5+
"main": "index.js",
6+
"scripts": {
7+
"test": "echo \"Error: no test specified\" && exit 1",
8+
"dev": "nodemon src/index.js"
9+
},
10+
"keywords": [],
11+
"author": "",
12+
"license": "ISC",
13+
"packageManager": "[email protected]",
14+
"dependencies": {
15+
"cors": "^2.8.5",
16+
"dotenv": "^17.2.3",
17+
"express": "^5.1.0",
18+
"mongoose": "^8.19.1"
19+
},
20+
"devDependencies": {
21+
"nodemon": "^3.1.10"
22+
}
23+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
const mongoose = require('mongoose')
2+
const Question = require('../models/questionModel')
3+
const seedData = require('../data/seed.json')
4+
5+
const connectDB = async () => {
6+
try {
7+
const con = await mongoose.connect(process.env.MONGODB_URI)
8+
console.log(`MongoDB Connected: ${con.connection.host}`)
9+
10+
const questionCount = await Question.countDocuments()
11+
if (questionCount === 0) {
12+
console.log('Database is empty, seeding with sample questions...')
13+
await Question.insertMany(seedData)
14+
console.log(`Seeded ${seedData.length} questions`)
15+
} else {
16+
console.log(`Database contains ${questionCount} questions`)
17+
}
18+
} catch (error) {
19+
console.log(error)
20+
process.exit(1)
21+
}
22+
}
23+
24+
module.exports = connectDB
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
const Question = require('../models/questionModel')
2+
const seedData = require('../data/seed.json')
3+
4+
const DIFFICULTIES = ['Easy', 'Medium', 'Hard']
5+
const TOPICS = ['String', 'Algorithms', 'Data Structures', 'Databases', 'Bit Manipulation', 'Recursion', 'Arrays', 'Brainteaser']
6+
7+
const fetchAllQuestions = async (req, res) => {
8+
const questions = await Question.find({})
9+
res.status(200).json(questions)
10+
}
11+
12+
const getQuestionById = async (req, res) => {
13+
try {
14+
const id = req.params.id
15+
const mongoose = require('mongoose')
16+
if (!mongoose.Types.ObjectId.isValid(id)) {
17+
return res.status(400).json({ error: 'Invalid question id' })
18+
}
19+
20+
const q = await Question.findById(id)
21+
if (!q) return res.status(404).json({ error: 'Question not found' })
22+
23+
const resp = {
24+
_id: q._id,
25+
title: q.title,
26+
description: q.description,
27+
difficulty: q.difficulty,
28+
topic: q.topic,
29+
examples: q.examples || [],
30+
templates: q.templates || [],
31+
link: q.link || null,
32+
}
33+
34+
return res.status(200).json(resp)
35+
} catch (err) {
36+
console.error('getQuestionById error', err)
37+
return res.status(500).json({ error: 'Internal server error' })
38+
}
39+
}
40+
41+
/**
42+
* GET /v1/questions/pick?topic=&difficulty=
43+
* Selection rules:
44+
* - both provided: match both
45+
* - only topic: match topic (difficulty random)
46+
* - only difficulty: match difficulty (topic random)
47+
* - neither: any random question
48+
*/
49+
const pickQuestion = async (req, res) => {
50+
try {
51+
const topic = req.query.topic
52+
const difficulty = req.query.difficulty
53+
54+
// Validate fields if provided
55+
if (topic && !TOPICS.includes(topic)) {
56+
return res.status(400).json({ error: 'Invalid topic/difficulty' })
57+
}
58+
if (difficulty && !DIFFICULTIES.includes(difficulty)) {
59+
return res.status(400).json({ error: 'Invalid topic/difficulty' })
60+
}
61+
62+
const pipeline = []
63+
const match = {}
64+
if (topic) match.topic = topic
65+
if (difficulty) match.difficulty = difficulty
66+
if (Object.keys(match).length > 0) pipeline.push({ $match: match })
67+
pipeline.push({ $sample: { size: 1 } })
68+
69+
const docs = await Question.aggregate(pipeline)
70+
if (!docs || docs.length === 0) {
71+
return res.status(404).json({ error: 'No question found for the given criteria' })
72+
}
73+
74+
const q = docs[0]
75+
// ensure the response is safe to expose
76+
const resp = {
77+
_id: q._id,
78+
title: q.title,
79+
description: q.description,
80+
difficulty: q.difficulty,
81+
topic: q.topic,
82+
examples: q.examples || [],
83+
templates: q.templates || [],
84+
link: q.link || null,
85+
}
86+
87+
return res.status(200).json(resp)
88+
} catch (error) {
89+
console.error('pickQuestion error', error)
90+
return res.status(500).json({ error: 'Internal server error' })
91+
}
92+
}
93+
94+
module.exports = {
95+
fetchAllQuestions,
96+
pickQuestion,
97+
getQuestionById,
98+
seedQuestions: async (req, res) => {
99+
try {
100+
await Question.deleteMany({})
101+
const created = await Question.insertMany(seedData)
102+
return res.status(200).json({ inserted: created.length })
103+
} catch (err) {
104+
console.error('seedQuestions error', err)
105+
return res.status(500).json({ error: 'Seeding failed' })
106+
}
107+
},
108+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
[
2+
{
3+
"title": "Two Sum",
4+
"difficulty": "Easy",
5+
"topic": "Arrays",
6+
"description": "Given an array of integers nums and an integer target, return indices of the two numbers such that they add up to target. You may assume that each input has exactly one solution.",
7+
"examples": [
8+
{ "input": "nums = [2,7,11,15], target = 9", "output": "[0,1]", "explanation": "nums[0] + nums[1] == 9" }
9+
],
10+
"templates": [
11+
{ "language": "JAVASCRIPT", "starterCode": "var twoSum = function(nums, target) { }" },
12+
{ "language": "PYTHON", "starterCode": "def two_sum(nums: List[int], target: int) -> List[int]: pass" }
13+
],
14+
"link": "https://leetcode.com/problems/two-sum/"
15+
},
16+
{
17+
"title": "Reverse Linked List",
18+
"difficulty": "Easy",
19+
"topic": "Data Structures",
20+
"description": "Reverse a singly linked list and return the reversed list.",
21+
"examples": [
22+
{ "input": "head = [1,2,3,4,5]", "output": "[5,4,3,2,1]" }
23+
],
24+
"templates": [
25+
{ "language": "JAVASCRIPT", "starterCode": "var reverseList = function(head) { }" },
26+
{ "language": "PYTHON", "starterCode": "def reverseList(head: Optional[ListNode]) -> Optional[ListNode]: pass" }
27+
],
28+
"link": "https://leetcode.com/problems/reverse-linked-list/"
29+
},
30+
{
31+
"title": "Binary Tree Level Order Traversal",
32+
"difficulty": "Medium",
33+
"topic": "Data Structures",
34+
"description": "Given the root of a binary tree, return the level order traversal of its nodes' values (from left to right, level by level).",
35+
"examples": [
36+
{ "input": "root = [3,9,20,null,null,15,7]", "output": "[[3],[9,20],[15,7]]" }
37+
],
38+
"templates": [
39+
{ "language": "JAVASCRIPT", "starterCode": "var levelOrder = function(root) { }" },
40+
{ "language": "PYTHON", "starterCode": "def levelOrder(root: Optional[TreeNode]) -> List[List[int]]: pass" }
41+
],
42+
"link": "https://leetcode.com/problems/binary-tree-level-order-traversal/"
43+
},
44+
{
45+
"title": "Longest Increasing Subsequence",
46+
"difficulty": "Medium",
47+
"topic": "Algorithms",
48+
"description": "Given an integer array nums, return the length of the longest strictly increasing subsequence.",
49+
"examples": [
50+
{ "input": "nums = [10,9,2,5,3,7,101,18]", "output": "4", "explanation": "The LIS is [2,3,7,101]" }
51+
],
52+
"templates": [
53+
{ "language": "JAVA", "starterCode": "class Solution { public int lengthOfLIS(int[] nums) { return 0; } }" },
54+
{ "language": "PYTHON", "starterCode": "def lengthOfLIS(nums: List[int]) -> int: pass" }
55+
],
56+
"link": "https://leetcode.com/problems/longest-increasing-subsequence/"
57+
},
58+
{
59+
"title": "Valid Parentheses",
60+
"difficulty": "Easy",
61+
"topic": "String",
62+
"description": "Given a string s containing just the characters '(', ')', '{', '}', '[' and ']', determine if the input string is valid.",
63+
"examples": [
64+
{ "input": "s = '()[]{}'", "output": "true" },
65+
{ "input": "s = '(]'", "output": "false" }
66+
],
67+
"templates": [
68+
{ "language": "JAVASCRIPT", "starterCode": "var isValid = function(s) { }" },
69+
{ "language": "PYTHON", "starterCode": "def isValid(s: str) -> bool: pass" }
70+
],
71+
"link": "https://leetcode.com/problems/valid-parentheses/"
72+
}
73+
]
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
const express = require('express')
2+
const cors = require('cors')
3+
const dotenv = require('dotenv').config()
4+
const connectDB = require('./config/db')
5+
6+
connectDB()
7+
8+
const app = express()
9+
10+
app.use(cors({
11+
origin: 'http://localhost:3000',
12+
credentials: true,
13+
optionsSuccessStatus: 200,
14+
}));
15+
16+
app.use(express.json())
17+
app.use(express.urlencoded({ extended: false }))
18+
19+
const PORT = process.env.PORT || 8080
20+
21+
app.listen(PORT, () => {
22+
console.log(`Question service is running on port ${PORT}...`)
23+
})
24+
25+
app.get('/', (req, res) => {
26+
res.json({ message: 'Question service is up and running!' })
27+
})
28+
29+
// Mount v1 routes for the question service
30+
app.use('/v1/questions', require('./routes/questionRoutes'))
31+
32+
module.exports = app

0 commit comments

Comments
 (0)