From b93f94a2de6e8e6fa0b83d231740391dba7558c3 Mon Sep 17 00:00:00 2001 From: oliverDev Date: Fri, 10 Apr 2026 22:54:35 +0800 Subject: [PATCH 1/2] submit 1-2 years backend test --- backend/controllers/todoController.js | 138 +++++++++++++++ backend/index.js | 31 +++- backend/models/todo.js | 116 +++++++++++++ backend/models/todos.json | 15 ++ backend/package.json | 5 + backend/routes/todoRoutes.js | 25 +++ package-lock.json | 238 +++++++++++++++++++++++++- package.json | 10 +- 8 files changed, 575 insertions(+), 3 deletions(-) create mode 100644 backend/controllers/todoController.js create mode 100644 backend/models/todo.js create mode 100644 backend/models/todos.json create mode 100644 backend/routes/todoRoutes.js diff --git a/backend/controllers/todoController.js b/backend/controllers/todoController.js new file mode 100644 index 00000000..17c1785c --- /dev/null +++ b/backend/controllers/todoController.js @@ -0,0 +1,138 @@ +import express from "express"; +import todoModel from "../models/todo.js"; + + +//List Todo +export const listTodos = async (req, res) => { + try { + const todos = await todoModel.getAll(); + res.json({ + success: true, + data: todos, + count: todos.length + }); + + } catch (error) { + res.status(500).json({ + success: false, + message: "Error fetching todos", + error: error.message + }) + } +}; + +//list by id +export const listTodoById = async (req, res) => { + try { + const todo = await todoModel.getById(req.params.id); + + //todo does not exsist + if(!todo){ + return res.status(404).json({ + success: false, + message: "Todo not found" + }); + } + res.json({ + success: true, + data: todo + }); + + + } catch (error) { + res.status(500).json({ + success: false, + message: "Error fetching todo", + error: error.message }) + } +}; + +//create +export const createTodo = async (req, res) => { + try { + const {title} = req.body; + + //null validation + if(!title || title.trim() === ' ') + return res.status(400).json({ + success: false, + message: "Title is required" + }); + + const newTodo = await todoModel.create({ + title: title.trim() + }); + + + res.status(201).json({ + success: true, + message: 'Todo created successfully', + data: newTodo + }); + + } catch (error) { + res.status(500).json({ + success: false, + message: "Error creating todo", + error: error.message + }) + } +}; + +//update +export const updateTodo = async (req, res) => { + try { + const update = {} + if(req.body.title) update.title = req.body.title.trim(); + if(req.body.completed !== undefined) update.completed = req.body.completed; + + const updatedTodo = await todoModel.update(req.params.id, update); + + if (!updatedTodo) { + return res.status(400).json({ + success: false, + message: "Todo not found" + + }) + } + res.json({ + success: true, + message: "Todo updated successfully", + data: updatedTodo + }); + + } catch (error) { + res.status(500).json({ + success: false, + message: "Error updating todo", + error: error.message + }) + + } +}; + +//delete +export const deleteTodo = async (req, res) => { + try { + const deleted = await todoModel.delete(req.params.id); + + if(!deleted) { + return res.status(404).json({ + success: false, + message: "Todo not found" + }); + } + res.json({ + success: true, + message: "Todo deleted successfully" + }); + + + } catch (error) { + res.status(500).json({ + success: false, + message: "Error deleting todo", + error: error.message + }) + } +}; \ No newline at end of file diff --git a/backend/index.js b/backend/index.js index ff6a6a3b..aa64ba52 100644 --- a/backend/index.js +++ b/backend/index.js @@ -1,13 +1,42 @@ -const express = require("express"); +import express from "express"; +import dotenv from "dotenv"; +import path from "path"; +import { fileURLToPath } from "url"; +import todoRoutes from "./routes/todoRoutes.js"; + + +dotenv.config(); + +const ___filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(___filename); + const app = express(); const PORT = process.env.PORT || 4000; +//middleware +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); +//app.use(express.static(path.join(__dirname, "public"))); + // Basic route app.get("/", (req, res) => { res.send("Hello from Express!"); }); +//todo API route +app.use("/api/todos", todoRoutes); + +app.use((err, req, res, next) => { + console.error(err.stacks); + res.status(500).json({ + success: false, + message: "Something went wrong", + error: err.message + }); +}) + // Start server app.listen(PORT, () => { console.log(`Backend is running on http://localhost:${PORT}`); + console.log(`Todo API available at http://localhost:${PORT}/api/todos`) }); diff --git a/backend/models/todo.js b/backend/models/todo.js new file mode 100644 index 00000000..830f7daf --- /dev/null +++ b/backend/models/todo.js @@ -0,0 +1,116 @@ +import { create } from 'domain'; +import fs from 'fs/promises'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +class Todo { + constructor() { + this.filePath = path.join(__dirname, 'todos.json'); + this.init(); + } + + async init() { + try { + await fs.access(this.filePath); + } catch (error) { + await this.saveToFile([]); + } + + + } + + async readFromFile () { + try { + const data = await fs.readFile(this.filePath, 'utf-8'); + return JSON.parse(data); + } catch (error) { + console.error('Error reading file:', error); + return []; + + } + } + + + async saveToFile(todos) { + try { + await fs.writeFile(this.filePath, JSON.stringify(todos, null, 2)); + return true; + } catch (error) { + console.error('Error saving file:', error); + return false; + } + } + + async getAll() { + return await this.readFromFile(); + } + // Get next available ID + async getNextId() { + const todos = await this.readFromFile(); + if (todos.length === 0) return 1; + const maxId = Math.max(...todos.map(todo => todo.id)); + return maxId + 1; + } + + async getById(id) { + const todos =await this.readFromFile(); + return todos.find(todo => todo.id === parseInt (id)); + } + + async create(todoData) { + const todos = await this.readFromFile(); + const nextId = await this.getNextId(); + const newTodo = { + id: nextId, + title: todoData.title, + completed: false, + createdAt: new Date().toISOString() + + }; + todos.push(newTodo); + await this.saveToFile(todos); + return newTodo; + } + + async update(id, updatedData) { + const todos = await this.readFromFile(); + const index = todos.findIndex(todo => todo.id === parseInt(id)); + if (index === -1) { + return null; + } + todos[index] = { + ...todos[index], + ...updatedData, + updatedAt: new Date().toISOString(), + }; + await this.saveToFile(todos); + return todos[index]; + + + } + + async delete(id) { + const todos = await this.readFromFile(); + const filteredTodos = todos.filter(todo => todo.id !== parseInt(id)); + await this.saveToFile(filteredTodos); + return true; + } + + async getStats() { + const todos = await this.readFromFile(); + const completed = todos.filter(todo => todo.completed).length; + return { + total: todos.length, + completed, + pending: todos.length - completed, + + }; + } + +} + +const todoModel = new Todo(); +export default todoModel; \ No newline at end of file diff --git a/backend/models/todos.json b/backend/models/todos.json new file mode 100644 index 00000000..9ebf0122 --- /dev/null +++ b/backend/models/todos.json @@ -0,0 +1,15 @@ +[ + { + "id": 2, + "title": "Debug your code", + "completed": true, + "createdAt": "2026-04-10T14:04:53.215Z", + "updatedAt": "2026-04-10T14:49:34.898Z" + }, + { + "id": 3, + "title": "Remind your gf to drink water", + "completed": false, + "createdAt": "2026-04-10T14:05:23.463Z" + } +] \ No newline at end of file diff --git a/backend/package.json b/backend/package.json index c85981fa..3d2df231 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,6 +1,11 @@ { "name": "backend", "version": "1.0.0", + "description": "", + "license": "ISC", + "author": "", + "type": "module", + "main": "index.js", "scripts": { "start": "node index.js" }, diff --git a/backend/routes/todoRoutes.js b/backend/routes/todoRoutes.js new file mode 100644 index 00000000..d56b8bf0 --- /dev/null +++ b/backend/routes/todoRoutes.js @@ -0,0 +1,25 @@ +import express from "express"; +import { createTodo, listTodoById, listTodos, updateTodo, deleteTodo} from "../controllers/todoController.js"; + + + +const router = express.Router(); + +//get all +router.get("/", listTodos); + +//get by id +router.get("/:id", listTodoById); + +//create" +router.post("/", createTodo); + +//update +router.put("/:id", updateTodo); + +//delete +router.delete("/:id", deleteTodo); + + + +export default router; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index c61d591b..4280b1d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,10 +11,17 @@ "frontend", "backend", "mobile" - ] + ], + "dependencies": { + "cors": "^2.8.6" + }, + "devDependencies": { + "nodemon": "^3.1.14" + } }, "backend": { "version": "1.0.0", + "license": "ISC", "dependencies": { "express": "^4.18.2" } @@ -7480,6 +7487,19 @@ "node": ">=0.6" } }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/body-parser": { "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", @@ -7871,6 +7891,44 @@ "node": "*" } }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/chownr": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", @@ -8239,6 +8297,23 @@ "url": "https://opencollective.com/core-js" } }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cosmiconfig": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", @@ -10910,6 +10985,13 @@ "node": ">= 4" } }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, "node_modules/image-size": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", @@ -11110,6 +11192,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/is-boolean-object": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", @@ -13902,6 +13997,97 @@ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==" }, + "node_modules/nodemon": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz", + "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^10.2.1", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/nodemon/node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/nodemon/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nodemon/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -14756,6 +14942,13 @@ "url": "https://github.com/sponsors/lupomontero" } }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, "node_modules/pump": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", @@ -15467,6 +15660,19 @@ "node": ">=0.10.0" } }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/readline": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/readline/-/readline-1.3.0.tgz", @@ -16342,6 +16548,19 @@ "is-arrayish": "^0.3.1" } }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/sisteransi": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", @@ -17273,6 +17492,16 @@ "node": ">=0.6" } }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, "node_modules/tough-cookie": { "version": "4.1.4", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", @@ -17516,6 +17745,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, "node_modules/undici": { "version": "6.21.2", "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.2.tgz", diff --git a/package.json b/package.json index edc3a5d1..126950f8 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "codebility-assessment", "version": "1.0.0", "private": true, + "type": "module", "workspaces": [ "frontend", "backend", @@ -11,6 +12,13 @@ "start:frontend": "npm run dev --workspace frontend", "start:backend": "npm run start --workspace backend", "start:mobile": "npm run start --workspace mobile", - "install:all": "npm install --workspaces" + "install:all": "npm install --workspaces", + "nodemon:backend": "nodemon backend" + }, + "devDependencies": { + "nodemon": "^3.1.14" + }, + "dependencies": { + "cors": "^2.8.6" } } From b22114ad2812fc6e1d01ff217ecc35483bb6f160 Mon Sep 17 00:00:00 2001 From: oliverDev Date: Fri, 10 Apr 2026 22:58:43 +0800 Subject: [PATCH 2/2] Implement todo API --- backend/index.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/backend/index.js b/backend/index.js index aa64ba52..4fbe2f23 100644 --- a/backend/index.js +++ b/backend/index.js @@ -1,14 +1,11 @@ import express from "express"; import dotenv from "dotenv"; -import path from "path"; -import { fileURLToPath } from "url"; + import todoRoutes from "./routes/todoRoutes.js"; dotenv.config(); -const ___filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(___filename); const app = express(); const PORT = process.env.PORT || 4000; @@ -16,7 +13,6 @@ const PORT = process.env.PORT || 4000; //middleware app.use(express.json()); app.use(express.urlencoded({ extended: true })); -//app.use(express.static(path.join(__dirname, "public"))); // Basic route app.get("/", (req, res) => {