diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..1d51586999 --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +# Node Dependencies +node_modules/ +npm-debug.log + +# Compiled TypeScript Output +dist/ +build/ + +# Local SQLite Database (from Problem 5) +app.db +*.db + +# Environment Variables +.env + +# OS/Editor Generated Files +.DS_Store +.vscode/ \ No newline at end of file diff --git a/src/problem4/README.md b/src/problem4/README.md new file mode 100644 index 0000000000..e38094cffb --- /dev/null +++ b/src/problem4/README.md @@ -0,0 +1,28 @@ +# Problem 4: Three ways to sum to n + +# Overview +This repository contains three unique implementations in TypeScript to calculate the summation of integers from 1 to `n` (e.g., `sum_to_n(5) === 1 + 2 + 3 + 4 + 5 === 15`). + +Each implementation explores a different algorithmic approach, varying in time and space complexity. + +# Implementation A: Iterative Approach +This approach uses a standard `for` loop to incrementally add each number from 1 to `n` to a running total. + +# Implementation B: Mathematical Formula (Optimal) +This approach utilizes the arithmetic progression sum formula: `n * (n + 1) / 2`. + +# Implementation C: Recursive Approach +This approach solves the problem by breaking it down into smaller sub-problems, calling itself with `n - 1` until it reaches the base case (`n <= 1`). + +## How to Run + +Ensure you have [Node.js](https://nodejs.org/) and TypeScript installed. + +1. Install dependencies (if testing libraries are configured): + npm install + +2. Compile the TypeScript file: + npx tsc src/sum.ts + +3. Run tests (assuming Jest is set up from your earlier configurations): + npm test \ No newline at end of file diff --git a/src/problem4/jest.config.js b/src/problem4/jest.config.js new file mode 100644 index 0000000000..86f88fb90c --- /dev/null +++ b/src/problem4/jest.config.js @@ -0,0 +1,11 @@ +const { createDefaultPreset } = require("ts-jest"); + +const tsJestTransformCfg = createDefaultPreset().transform; + +/** @type {import("jest").Config} **/ +module.exports = { + testEnvironment: "node", + transform: { + ...tsJestTransformCfg, + }, +}; \ No newline at end of file diff --git a/src/problem4/package.json b/src/problem4/package.json new file mode 100644 index 0000000000..59255061ee --- /dev/null +++ b/src/problem4/package.json @@ -0,0 +1,14 @@ +{ + "name": "code-challenge", + "version": "1.0.0", + "scripts": { + "build": "tsc", + "test": "jest" + }, + "devDependencies": { + "@types/jest": "^29.5.14", + "jest": "^29.7.0", + "ts-jest": "^29.4.9", + "typescript": "^5.0.0" + } +} diff --git a/src/problem4/src/index.ts b/src/problem4/src/index.ts new file mode 100644 index 0000000000..89e0ab5748 --- /dev/null +++ b/src/problem4/src/index.ts @@ -0,0 +1,7 @@ +import { sum_to_n_a, sum_to_n_b, sum_to_n_c } from "./sum"; + +const n = 5; + +console.log("Iterative:", sum_to_n_a(n)); +console.log("Formula:", sum_to_n_b(n)); +console.log("Recursive:", sum_to_n_c(n)); diff --git a/src/problem4/src/sum.ts b/src/problem4/src/sum.ts new file mode 100644 index 0000000000..c83af4b51a --- /dev/null +++ b/src/problem4/src/sum.ts @@ -0,0 +1,25 @@ +// Implementation A: Iterative +export function sum_to_n_a(n: number): number { + if (n < 0) throw new Error("n must be non-negative"); + + let sum = 0; + for (let i = 1; i <= n; i++) { + sum += i; + } + return sum; +} + +// Implementation B: Formula (Optimal) +export function sum_to_n_b(n: number): number { + if (n < 0) throw new Error("n must be non-negative"); + + return (n * (n + 1)) / 2; +} + +// Implementation C: Recursion +export function sum_to_n_c(n: number): number { + if (n < 0) throw new Error("n must be non-negative"); + if (n <= 1) return n; + + return n + sum_to_n_c(n - 1); +} diff --git a/src/problem4/tests/sum.test.ts b/src/problem4/tests/sum.test.ts new file mode 100644 index 0000000000..30cb93d6b7 --- /dev/null +++ b/src/problem4/tests/sum.test.ts @@ -0,0 +1,18 @@ +import { sum_to_n_a, sum_to_n_b, sum_to_n_c } from "../src/sum"; + +describe("Sum to n", () => { + const testCases = [ + { input: 0, expected: 0 }, + { input: 1, expected: 1 }, + { input: 5, expected: 15 }, + { input: 10, expected: 55 }, + ]; + + testCases.forEach(({ input, expected }) => { + test(`n = ${input}`, () => { + expect(sum_to_n_a(input)).toBe(expected); + expect(sum_to_n_b(input)).toBe(expected); + expect(sum_to_n_c(input)).toBe(expected); + }); + }); +}); diff --git a/src/problem4/tsconfig.json b/src/problem4/tsconfig.json new file mode 100644 index 0000000000..5a07012f1a --- /dev/null +++ b/src/problem4/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "ES6", + "module": "commonjs", + "strict": true, + "outDir": "dist", + "esModuleInterop": true, + "types": ["jest"] + }, + "include": ["src", "tests"] +} \ No newline at end of file diff --git a/src/problem5/README.md b/src/problem5/README.md new file mode 100644 index 0000000000..7aa18fec8d --- /dev/null +++ b/src/problem5/README.md @@ -0,0 +1,78 @@ +# Problem 5: A Crude Server + +This is a backend RESTful API built with ExpressJS and TypeScript. It implements a set of CRUD interfaces for a generic "Resource" entity and uses a local SQLite database for simple, reliable data persistence. + +## Prerequisites + +- [Node.js](https://nodejs.org/) (v14 or higher recommended) +- npm or yarn + +## Setup and Configuration + +1. **Install dependencies:** + Navigate to the project root and run: + ```bash + npm init -y + npm install express better-sqlite3 + npm install --save-dev typescript @types/express @types/node @types/better-sqlite3 ts-node nodemon + ``` + +2. **Database:** + This application uses `better-sqlite3`. Upon starting the server for the first time, an `app.db` SQLite file will be automatically generated in the root directory. The database tables will be created automatically if they do not exist. + +## How to Run the Application + +**Development Mode (with auto-reload):** +```bash +npm run dev +``` + +**Production Mode:** +```bash +npm run build +npm start +``` + +The server will start on **http://localhost:3000**. + +## Testing with Postman + +You can use API testing tools like [Postman](https://www.postman.com/) or the VS Code REST Client to interact with the server. Ensure the server is running locally on port 3000 before sending requests. + +### 1. Create a Resource +- **Method:** `POST` +- **URL:** `http://localhost:3000/resources` +- **Headers:** `Content-Type: application/json` +- **Body (raw JSON):** + ```json + { + "name": "Sample Resource", + "description": "This is a test", + "status": "active" + } + ``` + +### 2. List Resources +- **Method:** `GET` +- **URL:** `http://localhost:3000/resources` +- **Query Filters (Optional):** You can filter by status using `?status=active` or `?status=inactive` (e.g., `http://localhost:3000/resources?status=active`). + +### 3. Get Details of a Resource +- **Method:** `GET` +- **URL:** `http://localhost:3000/resources/1` *(Replace '1' with the ID of an existing resource)* + +### 4. Update Resource Details +- **Method:** `PUT` +- **URL:** `http://localhost:3000/resources/1` +- **Headers:** `Content-Type: application/json` +- **Body (raw JSON):** + ```json + { + "name": "Updated Name", + "status": "inactive" + } + ``` + +### 5. Delete a Resource +- **Method:** `DELETE` +- **URL:** `http://localhost:3000/resources/1` \ No newline at end of file diff --git a/src/problem5/package.json b/src/problem5/package.json new file mode 100644 index 0000000000..56dfa64fa5 --- /dev/null +++ b/src/problem5/package.json @@ -0,0 +1,28 @@ +{ + "name": "problem-5", + "version": "1.0.0", + "main": "index.js", + "type": "module", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "node --loader ts-node/esm src/index.ts", + "dev": "nodemon --exec node --loader ts-node/esm src/index.ts", + "build": "tsc" + }, + "keywords": [], + "author": "", + "license": "ISC", + "description": "This is a backend RESTful API built with ExpressJS and TypeScript. It implements a set of CRUD interfaces for a generic \"Resource\" entity and uses a local SQLite database for simple, reliable data persistence.", + "dependencies": { + "better-sqlite3": "^11.10.0", + "express": "^5.2.1" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.13", + "@types/express": "^5.0.6", + "@types/node": "^25.6.0", + "nodemon": "^3.1.14", + "ts-node": "^10.9.2", + "typescript": "^6.0.3" + } +} diff --git a/src/problem5/src/index.ts b/src/problem5/src/index.ts new file mode 100644 index 0000000000..f385420449 --- /dev/null +++ b/src/problem5/src/index.ts @@ -0,0 +1,119 @@ +// src/index.ts +import express, { type Request, type Response } from 'express'; +import Database from 'better-sqlite3'; + +const app = express(); +const port = process.env.PORT || 3000; + +// Middleware +app.use(express.json()); + +// Initialize SQLite Database (creates 'app.db' file in the root) +const db = new Database('app.db', { verbose: console.log }); + +// Create the table if it doesn't exist +db.exec(` + CREATE TABLE IF NOT EXISTS resources ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + description TEXT, + status TEXT DEFAULT 'active' + ) +`); + +// Prepared Statements for performance and security +const insertResource = db.prepare('INSERT INTO resources (name, description, status) VALUES (?, ?, ?)'); +const selectAllResources = db.prepare('SELECT * FROM resources'); +const selectResourcesByStatus = db.prepare('SELECT * FROM resources WHERE status = ?'); +const selectResourceById = db.prepare('SELECT * FROM resources WHERE id = ?'); +const updateResource = db.prepare('UPDATE resources SET name = ?, description = ?, status = ? WHERE id = ?'); +const deleteResource = db.prepare('DELETE FROM resources WHERE id = ?'); + +// 1. Create a resource +app.post('/resources', (req: Request, res: Response) => { + const { name, description, status } = req.body; + if (!name) { + return res.status(400).json({ error: 'Name is required' }); + } + + try { + const info = insertResource.run(name, description || null, status || 'active'); + res.status(201).json({ id: info.lastInsertRowid, name, description, status }); + } catch (error) { + res.status(500).json({ error: 'Failed to create resource' }); + } +}); + +// 2. List resources with basic filters (e.g., ?status=active) +app.get('/resources', (req: Request, res: Response) => { + const { status } = req.query; + + try { + let resources; + if (status && typeof status === 'string') { + resources = selectResourcesByStatus.all(status); + } else { + resources = selectAllResources.all(); + } + res.status(200).json(resources); + } catch (error) { + res.status(500).json({ error: 'Failed to fetch resources' }); + } +}); + +// 3. Get details of a resource +app.get('/resources/:id', (req: Request, res: Response) => { + const { id } = req.params; + + try { + const resource = selectResourceById.get(id); + if (!resource) { + return res.status(404).json({ error: 'Resource not found' }); + } + res.status(200).json(resource); + } catch (error) { + res.status(500).json({ error: 'Failed to fetch resource' }); + } +}); + +// 4. Update resource details +app.put('/resources/:id', (req: Request, res: Response) => { + const { id } = req.params; + const { name, description, status } = req.body; + + try { + const existing = selectResourceById.get(id) as any; + if (!existing) { + return res.status(404).json({ error: 'Resource not found' }); + } + + const updatedName = name !== undefined ? name : existing.name; + const updatedDesc = description !== undefined ? description : existing.description; + const updatedStatus = status !== undefined ? status : existing.status; + + updateResource.run(updatedName, updatedDesc, updatedStatus, id); + res.status(200).json({ id: Number(id), name: updatedName, description: updatedDesc, status: updatedStatus }); + } catch (error) { + res.status(500).json({ error: 'Failed to update resource' }); + } +}); + +// 5. Delete a resource +app.delete('/resources/:id', (req: Request, res: Response) => { + const { id } = req.params; + + try { + const info = deleteResource.run(id); + if (info.changes === 0) { + return res.status(404).json({ error: 'Resource not found' }); + } + res.status(204).send(); // 204 No Content + } catch (error) { + res.status(500).json({ error: 'Failed to delete resource' }); + } +}); + +// Start Server +app.listen(port, () => { + console.log(`Server is running on http://localhost:${port}`); +}); diff --git a/src/problem5/tsconfig.json b/src/problem5/tsconfig.json new file mode 100644 index 0000000000..bdbad8c9bc --- /dev/null +++ b/src/problem5/tsconfig.json @@ -0,0 +1,44 @@ +{ + // Visit https://aka.ms/tsconfig to read more about this file + "compilerOptions": { + // File Layout + "rootDir": "./src", + "outDir": "./dist", + + // Environment Settings + // See also https://aka.ms/tsconfig/module + "module": "nodenext", + "target": "esnext", + "types": [], + // For nodejs: + // "lib": ["esnext"], + // "types": ["node"], + // and npm install -D @types/node + + // Other Outputs + "sourceMap": true, + "declaration": true, + "declarationMap": true, + + // Stricter Typechecking Options + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + + // Style Options + // "noImplicitReturns": true, + // "noImplicitOverride": true, + // "noUnusedLocals": true, + // "noUnusedParameters": true, + // "noFallthroughCasesInSwitch": true, + // "noPropertyAccessFromIndexSignature": true, + + // Recommended Options + "strict": true, + "jsx": "react-jsx", + "verbatimModuleSyntax": true, + "isolatedModules": true, + "noUncheckedSideEffectImports": true, + "moduleDetection": "force", + "skipLibCheck": true, + } +} diff --git a/src/problem6/README.md b/src/problem6/README.md new file mode 100644 index 0000000000..1287881d71 --- /dev/null +++ b/src/problem6/README.md @@ -0,0 +1,98 @@ +# Score Board Module Specification + +## 1. Module Overview +The Score Board Module manages user scores and provides a real-time leaderboard of the top 10 users. Its primary goal is to ensure data integrity by preventing unauthorized score increments while maintaining high responsiveness for live updates. + +## 2. Execution Flow Diagram +The following sequence diagram illustrates the flow of execution when a user completes an action that updates their score. + +```mermaid +sequenceDiagram + participant Client + participant API as API Server + participant Auth as Security/Auth + participant DB as Database + participant Cache as Redis (Top 10) + participant WS as WebSocket Server + + Client->>API: 1. Complete Action (POST /api/v1/scores/update) + Note over Client,API: Includes JWT & Action Payload + + API->>Auth: 2. Validate JWT & Idempotency Key + alt Invalid Token or Duplicate Action + Auth-->>API: Unauthorized / Conflict + API-->>Client: 401 / 409 Error Response + else Valid Action + Auth-->>API: Validation Success + + API->>DB: 3. Update User Score (Transaction) + DB-->>API: Score Updated + + API->>Cache: 4. Update Leaderboard (ZINCRBY) + Cache-->>API: New Top 10 List + + API->>WS: 5. Dispatch 'LeaderboardUpdated' Event + WS-->>Client: 6. Broadcast New Top 10 to all connected clients + + API-->>Client: 7. 200 OK (Action Successful) + end +``` + +## 3. Software Requirements Fulfilled + +1. **Website with Top 10 Scoreboard:** Addressed via the `/api/v1/scores/top` endpoint and Redis caching. +2. **Live Updates:** Achieved using WebSockets to push changes to clients immediately upon a score change. +3. **User Action Increments Score:** Handled by the `/api/v1/scores/update` endpoint. +4. **API Dispatch on Action Completion:** The client is responsible for calling the API once the action completes on the frontend. +5. **Prevent Malicious Updates:** Implemented via JWT authentication, rate limiting, and server-side action validation. + +## 4. API Documentation + +### Update Score +* **Endpoint:** `POST /api/v1/scores/update` +* **Description:** Dispatched by the client upon completion of an action to increment the user's score. +* **Headers:** + * `Authorization`: `Bearer ` + * `X-Idempotency-Key`: `UUID` (To prevent duplicate requests/replay attacks) +* **Payload:** + ```json + { + "actionId": "string (identifier for the action performed)", + "timestamp": "ISO-8601 UTC timestamp" + } + ``` +* **Success Response (200 OK):** + ```json + { + "success": true, + "newScore": 1540 + } + ``` + +### Get Top 10 Leaderboard +* **Endpoint:** `GET /api/v1/scores/top` +* **Description:** Retrieves the current top 10 users. Typically used for initial page load before the WebSocket connection is established. +* **Success Response (200 OK):** + ```json + { + "leaderboard": [ + { "userId": "123", "username": "playerOne", "score": 5000 }, + { "userId": "456", "username": "playerTwo", "score": 4800 } + ] + } + ``` + +## 5. Security Strategy (Preventing Malicious Updates) +To meet the requirement of preventing unauthorized score increases, the backend implements the following defensive measures: + +1. **Authentication:** All update requests must be accompanied by a valid, server-signed JWT. Anonymous requests are strictly rejected. +2. **Abstracted Score Values:** The client **does not** send the amount of points to add. The client only sends the `actionId`. The server maps this `actionId` to a specific point value internally, preventing users from sending payloads like `{"scoreToAdd": 999999}`. +3. **Idempotency / Anti-Replay:** The client must generate and send an `X-Idempotency-Key` (or a unique transaction ID). The server caches this key. If a malicious user intercepts the request and tries to replay it, the server will reject the duplicate key. +4. **Rate Limiting:** A strict rate limiter prevents automated bots from spamming the update endpoint. + +## 6. Additional Comments for Improvement + +* **Redis Sorted Sets:** For highly efficient leaderboard management, we should utilize Redis Sorted Sets (`ZADD`, `ZINCRBY`, `ZREVRANGE`). This allows updating and querying the top 10 users in `O(log(N))` time, which is much faster than querying a relational database on every score change. +* **Event Sourcing / Audit Log:** Instead of destructively updating a single `score` column in the database, we should append each score change to an `audit_logs` table (e.g., `userId`, `actionId`, `pointsAdded`, `timestamp`). This allows the team to recalculate a user's true score if tampering is suspected, and provides analytics on which actions are performed most often. +* **Message Broker for Scale:** If the application scales to millions of concurrent users, the API server should push the score update task to a message broker (like RabbitMQ or Kafka) instead of processing it synchronously. A dedicated worker service would then process the queue, update the database/Redis, and trigger the WebSocket broadcast, ensuring the main API servers remain responsive. +```