diff --git a/problem6/README.md b/problem6/README.md new file mode 100644 index 0000000000..cf553c2c45 --- /dev/null +++ b/problem6/README.md @@ -0,0 +1,318 @@ +# Scoreboard Service (Problem 6) + +## What this service is + +This is a small backend service that keeps track of user scores and powers a **live Top 10 leaderboard**. + +The idea is simple: + +* A user does some action in the product +* The frontend sends a request to the backend saying “this user earned points” +* The backend verifies it (important) +* Score gets updated +* Everyone watching the leaderboard sees updates in real time + +The tricky part is not the scoring itself, but: + +* preventing fake score increments +* keeping leaderboard fast +* pushing updates live without hammering the database + +--- + +## High-level design + +At a high level, I’d split it like this: + +* **API Server** + + * receives score update requests + * validates them properly + * writes to DB + cache + +* **Postgres (source of truth)** + + * stores user scores permanently + +* **Redis (fast path)** + + * maintains leaderboard (sorted set) + * avoids hitting DB for every leaderboard request + +* **WebSocket / SSE layer** + + * pushes live leaderboard updates to clients + +--- + +## Score update flow (important part) + +This is the actual request lifecycle: + +1. User performs an action in frontend +2. Frontend sends a request to backend: + + * includes `userId` + * includes `actionId` + * includes signed payload (important) +3. Backend validates: + + * signature is correct + * request is not expired + * action wasn’t already used (idempotency check) +4. If valid: + + * increment score in DB + * update Redis leaderboard +5. Broadcast update event +6. Connected clients get new Top 10 immediately + +--- + +## API + +### Increment score + +``` +POST /api/v1/scores/increment +``` + +### Request + +```json +{ + "userId": "uuid", + "actionId": "string", + "timestamp": 1710000000, + "signature": "HMAC_HASH" +} +``` + +### Response + +```json +{ + "success": true, + "newScore": 120 +} +``` + +--- + +## Security (this is the important bit) + +If you skip this, the whole system is basically a cheat engine. + +### 1. Request signing (HMAC) + +Every request must be signed: + +``` +signature = HMAC(secret, userId + actionId + timestamp) +``` + +Server recomputes it and compares. + +Why: + +* prevents fake score injection from random clients +* ensures request came from trusted frontend/backend flow + +--- + +### 2. Replay protection + +Each `actionId` can only be used once per user. + +Store it in: + +* Redis (fast TTL-based) +* or DB unique constraint (safer long-term) + +--- + +### 3. Timestamp window + +Reject requests older than ~5 minutes. + +Prevents replaying old valid requests. + +--- + +### 4. Rate limiting + +Basic protection layer: + +* per user: limit score increments/sec +* per IP: global throttling + +Nothing fancy, just enough to stop abuse loops. + +--- + +## Data model + +### users + +```sql +id UUID PRIMARY KEY, +username TEXT, +score INT DEFAULT 0, +updated_at TIMESTAMP +``` + +### processed_actions + +Used for idempotency. + +```sql +user_id UUID, +action_id TEXT, +created_at TIMESTAMP, +PRIMARY KEY (user_id, action_id) +``` + +--- + +## Leaderboard strategy + +Instead of querying DB every time (bad idea under load), Redis handles it. + +We use a **Sorted Set**: + +### Update score + +``` +ZINCRBY leaderboard +``` + +### Get Top 10 + +``` +ZREVRANGE leaderboard 0 9 WITHSCORES +``` + +Why this works well: + +* O(log N) updates +* super fast reads +* perfect for “Top K” use cases + +DB stays the source of truth, Redis is just the fast view. + +--- + +## Real-time updates + +When score changes: + +* API updates Redis +* then emits an event: + +```json +{ + "type": "SCORE_UPDATED", + "userId": "uuid", + "score": 120 +} +``` + +WebSocket server listens and pushes updated leaderboard to clients. + +--- + +## Execution flow diagram + +```mermaid +sequenceDiagram + participant Client + participant API + participant Redis + participant DB + participant WS + + Client->>API: POST score increment (signed) + API->>API: validate signature + timestamp + API->>Redis: check actionId (dedupe) + API->>DB: update user score + API->>Redis: update leaderboard ZSET + API->>WS: publish update event + WS->>Client: push updated top 10 +``` + +--- + +## Failure handling (real-world stuff) + +A few practical things I’d expect in production: + +* DB is always the source of truth +* Redis can be rebuilt from DB if needed +* writes should be idempotent (safe retries) +* background retry queue for failed updates + +--- + +## Things I would improve if I had more time + +These are not required for the challenge, but good engineering additions: + +### 1. Move to event-driven design + +Instead of writing directly: + +* API → DB → Redis + +We could do: + +* API → Kafka event → workers update DB/Redis + +This improves scaling and decoupling. + +--- + +### 2. Add anomaly detection + +Even simple heuristics help: + +* sudden score spikes +* repeated action patterns +* abnormal request frequency + +Can be flagged for review. + +--- + +### 3. Separate read/write models (CQRS style) + +* Write path → Postgres +* Read path → Redis leaderboard + +Keeps reads extremely fast. + +--- + +### 4. Observability (often ignored but important) + +* structured logs per request +* metrics: + + * score update rate + * rejected requests (auth failures) + * Redis latency +* tracing if system grows + +--- + +## Summary + +The main idea of this design is: + +* DB = truth +* Redis = speed layer +* WebSockets = live updates +* HMAC + idempotency = abuse protection + +It’s not over-engineered, but it’s structured in a way that can scale without rewriting everything later. + +--- diff --git a/problem6/docs/API_SPECIFICATION.md b/problem6/docs/API_SPECIFICATION.md new file mode 100644 index 0000000000..9da3317acc --- /dev/null +++ b/problem6/docs/API_SPECIFICATION.md @@ -0,0 +1,169 @@ +# `API_SPECIFICATION.md` + +*(Detailed API + contracts + security expectations)* + +## Overview + +This file defines the exact API behavior for the Scoreboard Service. + +The goal is to make the backend implementation deterministic: + +* no guessing endpoints +* no ambiguity in auth +* no confusion about hashing or validation rules + +--- + +## Base URL + +``` +/api/v1 +``` + +--- + +## Authentication Model + +This service does NOT rely on classic session login for score updates. + +Instead, each request must be **cryptographically signed**. + +### Why + +Because score updates are: + +* client-triggered +* high abuse risk +* easy to spoof if left open + +So we enforce request integrity at the API level. + +--- + +## Signing Strategy (HMAC) + +### Signature formula + +``` +signature = HMAC_SHA256( + secret_key, + userId + ":" + actionId + ":" + timestamp +) +``` + +--- + +## Headers + +Every request must include: + +``` +X-Signature: +X-Timestamp: +Content-Type: application/json +``` + +Optional but recommended: + +``` +X-Client-Version: 1.0.0 +``` + +--- + +## Endpoint: Increment Score + +### POST `/scores/increment` + +This is the ONLY write endpoint for score changes. + +--- + +### Request Body + +```json +{ + "userId": "uuid", + "actionId": "string-unique-action-id", + "points": 10 +} +``` + +--- + +### Validation Rules + +Backend must enforce: + +* `userId` must exist +* `actionId` must be unique per user +* `points > 0` +* timestamp must be within ±300 seconds +* signature must match HMAC formula + +--- + +### Response + +```json +{ + "success": true, + "newScore": 150, + "rank": 4 +} +``` + +--- + +## Endpoint: Get Leaderboard + +### GET `/leaderboard/top` + +Returns top 10 users. + +--- + +### Response + +```json +{ + "top": [ + { "userId": "u1", "score": 200 }, + { "userId": "u2", "score": 180 } + ], + "generatedAt": 1710000000 +} +``` + +--- + +## Internal Behavior (important for reviewers) + +When a score update happens: + +1. validate request +2. check idempotency (`actionId`) +3. update Postgres +4. update Redis sorted set +5. emit event to pub/sub channel + +--- + +## Failure Modes + +| Case | Behavior | +| ----------------- | -------- | +| invalid signature | 401 | +| expired timestamp | 401 | +| reused actionId | 409 | +| invalid payload | 400 | + +--- + +## Notes / Tradeoffs + +* Redis is eventually consistent with DB (acceptable for leaderboard UX) +* DB is source of truth +* We prioritize write safety over instant consistency + +--- diff --git a/problem6/docs/PROJECT_PLAN.md b/problem6/docs/PROJECT_PLAN.md new file mode 100644 index 0000000000..03d448b9c9 --- /dev/null +++ b/problem6/docs/PROJECT_PLAN.md @@ -0,0 +1,162 @@ +# `PROJECT_PLAN.md` + +*(Timeline + engineering breakdown)* + +## Goal + +Build a backend service that supports: + +* secure score updates +* real-time leaderboard +* scalable read/write separation + +--- + +## Assumptions + +* small team (1–3 engineers) +* no existing infra constraints +* we can use Redis + Postgres +* WebSocket or SSE available + +--- + +## Phase 1 — Core Backend (Day 1–2) + +### Deliverables + +* Express/Node or Go API skeleton +* `/scores/increment` endpoint +* Postgres schema setup +* basic validation layer + +### Tasks + +* setup project structure +* implement DB models +* implement HMAC verification +* implement idempotency check + +--- + +## Phase 2 — Leaderboard System (Day 2–3) + +### Deliverables + +* Redis sorted set leaderboard +* `/leaderboard/top` endpoint + +### Tasks + +* integrate Redis client +* implement ZINCRBY logic +* implement top 10 retrieval +* sync DB → Redis updates + +--- + +## Phase 3 — Real-time Updates (Day 3–4) + +### Deliverables + +* WebSocket server OR SSE stream +* live leaderboard push + +### Tasks + +* pub/sub channel setup +* event emitter on score update +* client broadcast logic + +--- + +## Phase 4 — Security Hardening (Day 4–5) + +### Deliverables + +* abuse protection layer +* logging + audit trail + +### Tasks + +* rate limiting middleware +* replay protection (timestamp + nonce) +* structured logs for score changes + +--- + +## Phase 5 — Load & Stability (Day 5–6) + +### Deliverables + +* stress-tested endpoints +* basic monitoring hooks + +### Tasks + +* simulate high write load +* validate Redis behavior under pressure +* ensure DB remains stable +* add retry queue (optional) + +--- + +## Architecture Milestones + +| Stage | Focus | +| ----- | -------------------- | +| MVP | score updates + DB | +| v1 | Redis leaderboard | +| v2 | real-time updates | +| v3 | anti-cheat + scaling | + +--- + +## Risks + +### 1. Fake score injection + +Mitigated via: + +* HMAC signing +* timestamp validation + +--- + +### 2. Redis desync + +Mitigated via: + +* DB as source of truth +* periodic rebuild job (optional) + +--- + +### 3. High write traffic + +Mitigated via: + +* Redis-first leaderboard +* async event pipeline (future upgrade) + +--- + +## Optional Future Improvements + +* Kafka event bus +* multi-region leaderboard +* anomaly detection layer +* per-user score caps + +--- + +## Final Note + +If this were production, I’d expect: + +* DB is never bypassed +* Redis is rebuildable anytime +* all writes are idempotent +* security is enforced at request boundary, not business logic + +--- \ No newline at end of file diff --git a/src/problem4/README.md b/src/problem4/README.md new file mode 100644 index 0000000000..0655edda2d --- /dev/null +++ b/src/problem4/README.md @@ -0,0 +1,124 @@ +# Problem 4 – Sum to N (Golang) + +## Overview + +This repository contains three different implementations of the classic **sum-to-n** problem. + +Although the original challenge task mentioned **TypeScript**, the example code was in **Golang** so I figured the intended language to implement **problem4** is **Golang**. So, the implementations below follow the requirements while taking advantage of Go's language features. + +The functions also support **negative integers** by normalizing the input and restoring the sign before returning the result. + +--- + +## Solution A – Arithmetic Series (Gauss Formula) + +Uses the arithmetic series formula: + +```text +1 + 2 + ... + n = n(n + 1) / 2 +``` + +Since the result is computed mathematically, no iteration is required. + +**Complexity** + +* **Time:** O(1) +* **Space:** O(1) + +**When to use** + +This is the optimal solution for this problem and would be the preferred implementation in production whenever the mathematical formula is applicable. + +--- + +## Solution B – Iterative Accumulation + +Iterates from `1` to `n`, adding each value to a running total. + +**Complexity** + +* **Time:** O(n) +* **Space:** O(1) + +**When to use** + +While it isn't the fastest approach, it's simple, easy to understand, and serves as a good baseline implementation. + +--- + +## Solution C – Parallel Divide & Conquer + +This solution takes a different approach. + +Instead of processing the entire range sequentially, it recursively splits the range into smaller subranges and computes them in parallel using goroutines. To avoid creating an excessive number of goroutines, parallelism is only applied to the upper levels of recursion. Once a configurable depth or range threshold is reached, the remaining work is completed sequentially. + +**Complexity** + +* **Time:** O(n) +* **Space:** O(p + log n) + +Where: + +* `p` is the number of concurrently executing goroutines. +* `log n` is the recursion stack depth. + +Although the asymptotic complexity remains **O(n)**, this implementation demonstrates a practical optimization often used in real-world systems: limiting parallelism so that concurrency overhead does not outweigh the work being performed. + +For a problem like this, the mathematical solution is still unquestionably the fastest. The goal of this implementation is not to outperform Solution A, but to demonstrate how divide-and-conquer and controlled parallel execution can improve the runtime characteristics of computational workloads that cannot be reduced to a closed-form expression. + +--- + +## Benchmark (reference run) + +Benchmarks were executed locally on: + +* Go: `1.26.4` +* OS: Linux (Arch) +* CPU: Intel i5-1135G7 + +```text +Benchmark_sum_to_n_a ~0.62 ns/op 0 allocs +Benchmark_sum_to_n_c ~119 µs/op 60 allocs +Benchmark_sum_to_n_b ~171 µs/op 0 allocs +``` + +> Results will vary across machines and compiler versions. These are provided as a reference for relative performance only. + +--- + + +## Why Three Different Approaches? + +Each implementation focuses on a different way of thinking about the same problem. + +| Function | Demonstrates | +| ------------ | ----------------------------------------------------- | +| `sum_to_n_a` | Mathematical optimization | +| `sum_to_n_b` | Straightforward iterative solution | +| `sum_to_n_c` | Divide-and-conquer with controlled parallel execution | + +Rather than implementing the same algorithm three different ways, the intention was to showcase different techniques and the trade-offs between simplicity, mathematical optimization, and practical concurrency. + +--- + +## Running + +Run the program: + +```bash +go run . +``` + +Run the benchmarks: + +```bash +go test -bench=. -benchmem +``` + +--- + +## Notes + +Only **Solution A** achieves constant-time complexity and is the clear production choice for this specific problem. + +Solutions **B** and **C** are included to demonstrate alternative algorithmic approaches. In particular, Solution **C** explores how Go's concurrency model can be applied in a controlled way to recursive workloads, reflecting techniques that are useful in real-world computational problems where no closed-form solution exists. diff --git a/src/problem4/go.mod b/src/problem4/go.mod new file mode 100644 index 0000000000..8d888f5976 --- /dev/null +++ b/src/problem4/go.mod @@ -0,0 +1,3 @@ +module code_challange.problem4 + +go 1.26.4 diff --git a/src/problem4/main.go b/src/problem4/main.go new file mode 100644 index 0000000000..aaa8961a7a --- /dev/null +++ b/src/problem4/main.go @@ -0,0 +1,97 @@ +package main + +import ( + "fmt" +) + +// 1. Gauss Formula +// Time: O(1) +// Space: O(1) +// Uses arithmetic series formula: n(n+1)/2 +func sum_to_n_a(n int) int { + sign := 1 + if n < 0 { + sign = -1 + n = -n + } + + // prevent overflow during multiplication + return sign * int(int64(n)*int64(n+1)/2) +} + +// 2. Iterative Loop +// Time: O(n) +// Space: O(1) +// Baseline accumulation from 1 to n. +func sum_to_n_b(n int) int { + sign := 1 + if n < 0 { + sign = -1 + n = -n + } + + sum := 0 + for i := 1; i <= n; i++ { + sum += i + } + + return sign * sum +} + +// 3. Recursive Divide & Conquer +// Time: O(n) +// Space: O(p + log n) number of goroutines + recursion stack +// Splits range into two halves recursively to a depth limit then. +func sum_to_n_c(n int) int { + sign := 1 + if n < 0 { + sign = -1 + n = -n + } + + return sign * parallelSum(1, n, 4) // depth limit +} + +func parallelSum(l, r, depth int) int { + if l > r { + return 0 + } + if l == r { + return l + } + + // switch to sequential once depth is low + if depth == 0 || (r-l) < 1000 { + sum := 0 + for i := l; i <= r; i++ { + sum += i + } + return sum + } + + mid := (l + r) / 2 + + var left, right int + done := make(chan struct{}, 2) + + go func() { + left = parallelSum(l, mid, depth-1) + done <- struct{}{} + }() + + go func() { + right = parallelSum(mid+1, r, depth-1) + done <- struct{}{} + }() + + <-done + <-done + + return left + right +} + +func main() { + fmt.Println(sum_to_n_a(-5)) // 15 + fmt.Println(sum_to_n_b(-5)) // 15 + fmt.Println(sum_to_n_c(-5)) // 15 +} diff --git a/src/problem4/main_test.go b/src/problem4/main_test.go new file mode 100644 index 0000000000..55675b3a20 --- /dev/null +++ b/src/problem4/main_test.go @@ -0,0 +1,21 @@ +package main + +import "testing" + +func Benchmark_sum_to_n_a(b *testing.B) { + for i := 0; i < b.N; i++ { + sum_to_n_a(2560000) + } +} + +func Benchmark_sum_to_n_b(b *testing.B) { + for i := 0; i < b.N; i++ { + sum_to_n_b(256000) + } +} + +func Benchmark_sum_to_n_c(b *testing.B) { + for i := 0; i < b.N; i++ { + sum_to_n_c(256000) + } +} diff --git a/src/problem5/.gitignore b/src/problem5/.gitignore new file mode 100644 index 0000000000..a14702c409 --- /dev/null +++ b/src/problem5/.gitignore @@ -0,0 +1,34 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/src/problem5/README.md b/src/problem5/README.md new file mode 100644 index 0000000000..7b612395d4 --- /dev/null +++ b/src/problem5/README.md @@ -0,0 +1,277 @@ +# Problem 5 – A CRUD server (Task Management API) + +A small ExpressJS + TypeScript backend for managing tasks. It supports task CRUD operations, filtering, task tags, and task event logging with SQLite persistence. + +## Features + +* Create, list, retrieve, update, and delete tasks +* Filter tasks by status, priority, and tags +* Add, list, and remove tags for a task +* Record task lifecycle events +* SQLite database persistence +* Foreign key relationships and cascade cleanup +* Parameterized SQL queries +* Runtime urgency score calculation + +## Tech Stack + +* TypeScript +* ExpressJS +* SQLite via `better-sqlite3` +* `tsx` for development +* `tsc-alias` for resolving TypeScript path aliases after build + +## Requirements + +* Node.js 18 or later +* npm + +## Installation + +```bash +npm install +``` + +## Database + +The SQLite database file is intentionally tracked in this repository for assessment purposes. + +No environment variables are required. The project uses internal defaults. + +To initialize or rebuild the database schema: + +```bash +npm run db +``` + +This executes: + +```bash +tsx src/db.ts +``` + +> Running the database script may reset existing local data, depending on the implementation in `src/db.ts`. + +## Running the Application + +Start the development server with file watching: + +```bash +npm run dev +``` + +Build and start the production version: + +```bash +npm run build +npm start +``` + +The API is available at: + +```text +http://localhost:3000 +``` + +## Postman Collection + +A Postman collection is included and tracked in the repository: + +```text +/src/problem5/code-challenge_Problem5.postman_collection.json +``` + +To use it: + +1. Start the API with `npm run dev`. +2. Open Postman. +3. Select **Import**. +4. Choose `/src/problem5/code-challenge_Problem5.postman_collection.json`. +5. Set the collection variable `baseURL` to: + +```text +http://localhost:3000 +``` + +The collection covers task creation, retrieval, filtering, updates, deletion, and task tag operations. + +## API Endpoints + +### Tasks + +| Method | Endpoint | Description | +| -------- | ------------------- | -------------------------------- | +| `POST` | `/tasks` | Create a task | +| `GET` | `/tasks` | List tasks with optional filters | +| `GET` | `/tasks/:id` | Get one task | +| `PATCH` | `/tasks/:id` | Update task details | +| `PATCH` | `/tasks/:id/status` | Update task status | +| `DELETE` | `/tasks/:id` | Delete a task | + +### Task Tags + +| Method | Endpoint | Description | +| -------- | ---------------------------- | --------------------------- | +| `POST` | `/tasks/:taskId/tags` | Add a tag to a task | +| `GET` | `/tasks/:taskId/tags` | Get tags assigned to a task | +| `DELETE` | `/tasks/:taskId/tags/:tagId` | Remove a tag from a task | + +## Request Examples + +### Create a Task + +```http +POST /tasks +Content-Type: application/json +``` + +```json +{ + "title": "Implement task API", + "description": "Complete CRUD endpoints", + "priority": 8, + "due_at": "2026-07-10" +} +``` + +`title` is required. `priority` is optional and defaults to `5`. + +### Update a Task + +```http +PATCH /tasks/1 +Content-Type: application/json +``` + +```json +{ + "title": "Implement task API documentation", + "priority": 9, + "due_at": "2026-07-12" +} +``` + +Only supplied fields are updated. + +### Update Task Status + +```http +PATCH /tasks/1/status +Content-Type: application/json +``` + +```json +{ + "status": "in_progress" +} +``` + +Allowed status values: + +* `pending` +* `in_progress` +* `done` + +### Add a Tag + +```http +POST /tasks/1/tags +Content-Type: application/json +``` + +```json +{ + "name": "backend" +} +``` + +Tags are created only when they are assigned to an existing task. + +## Filtering Tasks + +`GET /tasks` supports the following optional query parameters: + +| Parameter | Example | Description | +| ---------- | ---------------- | ------------------------------- | +| `status` | `pending` | Filter by task status | +| `priority` | `8` | Filter by priority from 1 to 10 | +| `tags` | `backend,urgent` | Filter by one or more tag names | + +Examples: + +```http +GET /tasks?status=pending +GET /tasks?priority=8 +GET /tasks?tags=backend,urgent +GET /tasks?status=in_progress&priority=7 +``` + +## Validation Rules + +* Task title must be a non-empty string. +* Priority must be an integer from `1` to `10`. +* Status must be `pending`, `in_progress`, or `done`. +* Task IDs and tag IDs must be positive numeric values. +* A tag cannot be added to a task that does not exist. +* Duplicate task-tag relationships are ignored. +* Invalid input returns `400 Bad Request`. +* Missing resources return `404 Not Found`. + +## Database Structure + +The application uses four SQLite tables: + +| Table | Purpose | +| ------------- | -------------------------------- | +| `tasks` | Stores task details | +| `tags` | Stores reusable tag names | +| `task_tags` | Stores task-to-tag relationships | +| `task_events` | Stores task lifecycle events | + +Task events include: + +* `TASK_CREATED` +* `TASK_UPDATED` +* `STATUS_CHANGED` +* `TASK_DELETED` + +Foreign key constraints ensure task-tag relationships remain valid. Deleting a task removes related task-tag records and task events through cascading deletes. + +## Project Structure + +```text +src/ +├── controllers/ +│ ├── task.controller.ts +│ └── task_tag.controller.ts +├── services/ +│ ├── task.service.ts +│ ├── tag.service.ts +│ └── task_tag.service.ts +├── routes/ +│ ├── task.route.ts +│ └── task_tag.route.ts +├── validations/ +│ └── task.validation.ts +├── interfaces/ +├── db.ts +├── server.ts +└── utils.ts +``` + +## Available Scripts + +| Command | Description | +| --------------- | ----------------------------------------------- | +| `npm run dev` | Run the API in development mode with watch mode | +| `npm run build` | Compile TypeScript and resolve aliases | +| `npm start` | Rebuild and run the compiled application | +| `npm run db` | Initialize or rebuild the SQLite schema | + +## Notes + +* The API uses parameterized SQL statements to prevent SQL injection. +* The task list enriches tasks with their assigned tags and calculated urgency score. +* The database file is intentionally committed for the coding challenge. +* No external services or environment configuration are required. +* This project was created for a technical assessment. diff --git a/src/problem5/bun.lock b/src/problem5/bun.lock new file mode 100644 index 0000000000..bda5b875f2 --- /dev/null +++ b/src/problem5/bun.lock @@ -0,0 +1,402 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "problem5", + "dependencies": { + "better-sqlite3": "^12.11.1", + "cors": "^2.8.6", + "dotenv": "^17.4.2", + "express": "^5.2.1", + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.13", + "@types/bun": "latest", + "@types/cors": "^2.8.19", + "@types/express": "^5.0.6", + "@types/node": "^26.0.1", + "tsc-alias": "^1.8.17", + "tsx": "^4.22.4", + }, + "peerDependencies": { + "typescript": "^6.0.3", + }, + }, + }, + "packages": { + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@types/better-sqlite3": ["@types/better-sqlite3@7.6.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA=="], + + "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], + + "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], + + "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], + + "@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="], + + "@types/express": ["@types/express@5.0.6", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "^2" } }, "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA=="], + + "@types/express-serve-static-core": ["@types/express-serve-static-core@5.1.1", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A=="], + + "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="], + + "@types/node": ["@types/node@26.0.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw=="], + + "@types/qs": ["@types/qs@6.15.1", "", {}, "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw=="], + + "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], + + "@types/send": ["@types/send@1.2.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ=="], + + "@types/serve-static": ["@types/serve-static@2.2.0", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*" } }, "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ=="], + + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + + "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], + + "array-union": ["array-union@2.1.0", "", {}, "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "better-sqlite3": ["better-sqlite3@12.11.1", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-dq9AtApgg5PGFtBzPFSBl3HZQjHok5gaQCM6zh2Yk0aSmDCs1CbnVI8/HgASQkNKsWFpseIO9beg5xxpYhbIfA=="], + + "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], + + "bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="], + + "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], + + "body-parser": ["body-parser@2.3.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^2.0.0", "debug": "^4.4.3", "http-errors": "^2.0.1", "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", "qs": "^6.15.2", "raw-body": "^3.0.2", "type-is": "^2.1.0" } }, "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + + "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "chokidar": ["chokidar@3.6.0", "", { "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" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="], + + "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], + + "commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="], + + "content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="], + + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + + "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + + "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], + + "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], + + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "dir-glob": ["dir-glob@3.0.1", "", { "dependencies": { "path-type": "^4.0.0" } }, "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA=="], + + "dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + + "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="], + + "esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="], + + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + + "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], + + "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], + + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], + + "file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], + + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + + "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + + "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "get-tsconfig": ["get-tsconfig@4.14.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="], + + "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], + + "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "globby": ["globby@11.1.0", "", { "dependencies": { "array-union": "^2.1.0", "dir-glob": "^3.0.1", "fast-glob": "^3.2.9", "ignore": "^5.2.0", "merge2": "^1.4.1", "slash": "^3.0.0" } }, "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], + + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + + "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], + + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], + + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + + "is-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], + + "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + + "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + + "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "mylas": ["mylas@2.1.14", "", {}, "sha512-BzQguy9W9NJgoVn2mRWzbFrFWWztGCcng2QI9+41frfk+Athwgx3qhqhvStz7ExeUUu7Kzw427sNzHpEZNINog=="], + + "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="], + + "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + + "node-abi": ["node-abi@3.93.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-Cu6yUpX5Iavugm8BeX7c0wgU9CvOqfd1yM6A1d2q2ZMjym7GjpASv2GdRcTq3Fx+Sb5OgBkEEpw4VnAbY6Y5RA=="], + + "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + + "path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="], + + "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], + + "picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + + "plimit-lit": ["plimit-lit@1.6.1", "", { "dependencies": { "queue-lit": "^1.5.1" } }, "sha512-B7+VDyb8Tl6oMJT9oSO2CW8XC/T4UcJGrwOVoNGwOQsQYhlpfajmrMj5xeejqaASq3V/EqThyOeATEOMuSEXiA=="], + + "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], + + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + + "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="], + + "qs": ["qs@6.15.3", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A=="], + + "queue-lit": ["queue-lit@1.5.2", "", {}, "sha512-tLc36IOPeMAubu8BkW8YDBV+WyIgKlYU7zUNs0J5Vk9skSZ4JfGlPOqplP0aHdfv7HL0B2Pg6nwiq60Qc6M2Hw=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "range-parser": ["range-parser@1.3.0", "", {}, "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw=="], + + "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], + + "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], + + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="], + + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + + "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + + "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], + + "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], + + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + + "side-channel": ["side-channel@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4", "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ=="], + + "side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="], + + "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="], + + "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], + + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], + + "tar-fs": ["tar-fs@2.1.5", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw=="], + + "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + + "tsc-alias": ["tsc-alias@1.8.17", "", { "dependencies": { "chokidar": "^3.5.3", "commander": "^9.0.0", "get-tsconfig": "^4.10.0", "globby": "^11.0.4", "mylas": "^2.1.9", "normalize-path": "^3.0.0", "plimit-lit": "^1.2.6" }, "bin": { "tsc-alias": "dist/bin/index.js" } }, "sha512-EIduCZHqbNwPm8BZYfq1aD7BQ697A4h6uSGMOFQfYGoQwfrYFTKwYfy9Bv42YxHkduVBcn9Zx0DkX111DKskyg=="], + + "tsx": ["tsx@4.22.4", "", { "dependencies": { "esbuild": "~0.28.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg=="], + + "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], + + "type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="], + + "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], + + "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], + + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "body-parser/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], + + "type-is/content-type": ["content-type@2.0.0", "", {}, "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ=="], + } +} diff --git a/src/problem5/code-challenge.db b/src/problem5/code-challenge.db new file mode 100644 index 0000000000..10987355f8 Binary files /dev/null and b/src/problem5/code-challenge.db differ diff --git a/src/problem5/code-challenge_Problem5.postman_collection.json b/src/problem5/code-challenge_Problem5.postman_collection.json new file mode 100644 index 0000000000..5183ca56cd --- /dev/null +++ b/src/problem5/code-challenge_Problem5.postman_collection.json @@ -0,0 +1,844 @@ +{ + "info": { + "_postman_id": "f15dcb91-0a88-41b9-b58e-40d0dfb4cb75", + "name": "code-challenge_Problem5", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "30286622" + }, + "item": [ + { + "name": "Create Task", + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"title\": \"Implement Problem 6\",\n \"description\": \"Initial Project Setup\",\n \"priority\": \"10\",\n \"due_at\": \"03-07-2026\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseURL}}/tasks", + "host": [ + "{{baseURL}}" + ], + "path": [ + "tasks" + ] + } + }, + "response": [ + { + "name": "Create Task", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"title\": \"Bug 2\",\n \"description\": \"Bug desc\",\n \"priority\": 9,\n \"due_at\": \"05-06-2026\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseURL}}/tasks", + "host": [ + "{{baseURL}}" + ], + "path": [ + "tasks" + ] + } + }, + "status": "Created", + "code": 201, + "_postman_previewlanguage": null, + "header": [ + { + "key": "X-Powered-By", + "value": "Express" + }, + { + "key": "Access-Control-Allow-Origin", + "value": "*" + }, + { + "key": "Content-Type", + "value": "application/json; charset=utf-8" + }, + { + "key": "Content-Length", + "value": "202" + }, + { + "key": "ETag", + "value": "W/\"ca-gx4RjGXkmU5ysy2EpgdWE/Poe4A\"" + }, + { + "key": "Date", + "value": "Thu, 02 Jul 2026 07:04:28 GMT" + }, + { + "key": "Connection", + "value": "keep-alive" + }, + { + "key": "Keep-Alive", + "value": "timeout=5" + } + ], + "cookie": [], + "body": "{\n \"id\": 4,\n \"title\": \"Bug 2\",\n \"description\": \"Bug desc\",\n \"priority\": 9,\n \"status\": \"pending\",\n \"due_at\": \"05-06-2026\",\n \"created_at\": \"2026-07-02 07:04:27\",\n \"updated_at\": \"2026-07-02 07:04:27\",\n \"urgencyScore\": 157,\n \"tags\": []\n}" + } + ] + }, + { + "name": "Add Tags to Task", + "request": { + "auth": { + "type": "noauth" + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"title\": \"Title2\",\n \"description\": \"Desc2\",\n \"priority\": \"5\",\n \"due_at\": \"02-01-20026\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseURL}}/tasks", + "host": [ + "{{baseURL}}" + ], + "path": [ + "tasks" + ] + } + }, + "response": [ + { + "name": "Add Tags to Task", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"name\": \"bug\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseURL}}/tasks/4/tags", + "host": [ + "{{baseURL}}" + ], + "path": [ + "tasks", + "4", + "tags" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": null, + "header": [ + { + "key": "X-Powered-By", + "value": "Express" + }, + { + "key": "Access-Control-Allow-Origin", + "value": "*" + }, + { + "key": "Content-Type", + "value": "application/json; charset=utf-8" + }, + { + "key": "Content-Length", + "value": "23" + }, + { + "key": "ETag", + "value": "W/\"17-TPBdkRZpEmfaWYe0drA2+avE5u4\"" + }, + { + "key": "Date", + "value": "Thu, 02 Jul 2026 07:05:30 GMT" + }, + { + "key": "Connection", + "value": "keep-alive" + }, + { + "key": "Keep-Alive", + "value": "timeout=5" + } + ], + "cookie": [], + "body": "[\n {\n \"id\": 4,\n \"name\": \"bug\"\n }\n]" + } + ] + }, + { + "name": "Get Task by Id", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseURL}}/tasks/1", + "host": [ + "{{baseURL}}" + ], + "path": [ + "tasks", + "1" + ] + } + }, + "response": [ + { + "name": "Get Task by Id", + "originalRequest": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseURL}}/tasks/4", + "host": [ + "{{baseURL}}" + ], + "path": [ + "tasks", + "4" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": null, + "header": [ + { + "key": "X-Powered-By", + "value": "Express" + }, + { + "key": "Access-Control-Allow-Origin", + "value": "*" + }, + { + "key": "Content-Type", + "value": "application/json; charset=utf-8" + }, + { + "key": "Content-Length", + "value": "223" + }, + { + "key": "ETag", + "value": "W/\"df-AvDffL2gXyuuIlf7LHMqjSywEkE\"" + }, + { + "key": "Date", + "value": "Thu, 02 Jul 2026 07:05:38 GMT" + }, + { + "key": "Connection", + "value": "keep-alive" + }, + { + "key": "Keep-Alive", + "value": "timeout=5" + } + ], + "cookie": [], + "body": "{\n \"id\": 4,\n \"title\": \"Bug 2\",\n \"description\": \"Bug desc\",\n \"priority\": 9,\n \"status\": \"pending\",\n \"due_at\": \"05-06-2026\",\n \"created_at\": \"2026-07-02 07:04:27\",\n \"updated_at\": \"2026-07-02 07:04:27\",\n \"urgencyScore\": 157,\n \"tags\": [\n {\n \"id\": 4,\n \"name\": \"bug\"\n }\n ]\n}" + } + ] + }, + { + "name": "Get Tasks", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseURL}}/tasks?tags=Tag1&tags=Tag3", + "host": [ + "{{baseURL}}" + ], + "path": [ + "tasks" + ], + "query": [ + { + "key": "tags", + "value": "Tag1" + }, + { + "key": "tags", + "value": "Tag3" + } + ] + } + }, + "response": [ + { + "name": "Get Tasks", + "originalRequest": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseURL}}/tasks?tags=bug,chore", + "host": [ + "{{baseURL}}" + ], + "path": [ + "tasks" + ], + "query": [ + { + "key": "tags", + "value": "bug,chore" + } + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": null, + "header": [ + { + "key": "X-Powered-By", + "value": "Express" + }, + { + "key": "Access-Control-Allow-Origin", + "value": "*" + }, + { + "key": "Content-Type", + "value": "application/json; charset=utf-8" + }, + { + "key": "Content-Length", + "value": "761" + }, + { + "key": "ETag", + "value": "W/\"2f9-O4WXUriEJKnF2sXXrbZFpJzwJbE\"" + }, + { + "key": "Date", + "value": "Thu, 02 Jul 2026 07:06:18 GMT" + }, + { + "key": "Connection", + "value": "keep-alive" + }, + { + "key": "Keep-Alive", + "value": "timeout=5" + } + ], + "cookie": [], + "body": "[\n {\n \"id\": 1,\n \"title\": \"Init\",\n \"description\": \"Initial Project Setup\",\n \"priority\": 6,\n \"status\": \"pending\",\n \"due_at\": \"02-07-2026\",\n \"created_at\": \"2026-07-02 04:55:24\",\n \"updated_at\": \"2026-07-02 04:55:24\",\n \"urgencyScore\": 215,\n \"tags\": [\n {\n \"id\": 1,\n \"name\": \"chore\"\n },\n {\n \"id\": 2,\n \"name\": \"init\"\n }\n ]\n },\n {\n \"id\": 4,\n \"title\": \"Bug 2\",\n \"description\": \"Bug desc\",\n \"priority\": 9,\n \"status\": \"pending\",\n \"due_at\": \"05-06-2026\",\n \"created_at\": \"2026-07-02 07:04:27\",\n \"updated_at\": \"2026-07-02 07:04:27\",\n \"urgencyScore\": 157,\n \"tags\": [\n {\n \"id\": 4,\n \"name\": \"bug\"\n }\n ]\n },\n {\n \"id\": 2,\n \"title\": \"Implement Problem 6\",\n \"description\": \"Initial Project Setup\",\n \"priority\": 1,\n \"status\": \"pending\",\n \"due_at\": \"03-07-2026\",\n \"created_at\": \"2026-07-02 06:53:01\",\n \"updated_at\": \"2026-07-02 06:53:01\",\n \"urgencyScore\": 137,\n \"tags\": [\n {\n \"id\": 1,\n \"name\": \"chore\"\n },\n {\n \"id\": 3,\n \"name\": \"dev\"\n }\n ]\n }\n]" + } + ] + }, + { + "name": "Update Task Status", + "request": { + "auth": { + "type": "noauth" + }, + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "// \"pending\" | \"in_progress\" | \"done\"\n\n{\n \"status\": \"in_progress\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseURL}}/tasks/1/status", + "host": [ + "{{baseURL}}" + ], + "path": [ + "tasks", + "1", + "status" + ] + } + }, + "response": [ + { + "name": "Update Task Status", + "originalRequest": { + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "// \"pending\" | \"in_progress\" | \"done\"\n\n{\n \"status\": \"in_progress\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseURL}}/tasks/1/status", + "host": [ + "{{baseURL}}" + ], + "path": [ + "tasks", + "1", + "status" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": null, + "header": [ + { + "key": "X-Powered-By", + "value": "Express" + }, + { + "key": "Access-Control-Allow-Origin", + "value": "*" + }, + { + "key": "Content-Type", + "value": "application/json; charset=utf-8" + }, + { + "key": "Content-Length", + "value": "264" + }, + { + "key": "ETag", + "value": "W/\"108-S085vqV6D/0k5Lm3IoZPIL/m9ic\"" + }, + { + "key": "Date", + "value": "Thu, 02 Jul 2026 07:07:03 GMT" + }, + { + "key": "Connection", + "value": "keep-alive" + }, + { + "key": "Keep-Alive", + "value": "timeout=5" + } + ], + "cookie": [], + "body": "{\n \"id\": 1,\n \"title\": \"Init\",\n \"description\": \"Initial Project Setup\",\n \"priority\": 6,\n \"status\": \"in_progress\",\n \"due_at\": \"02-07-2026\",\n \"created_at\": \"2026-07-02 04:55:24\",\n \"updated_at\": \"2026-07-02 07:07:03\",\n \"urgencyScore\": 215,\n \"tags\": [\n {\n \"id\": 1,\n \"name\": \"chore\"\n },\n {\n \"id\": 2,\n \"name\": \"init\"\n }\n ]\n}" + } + ] + }, + { + "name": "Patch Task", + "request": { + "auth": { + "type": "noauth" + }, + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "// \"pending\" | \"in_progress\" | \"done\"\n\n{\n \"status\": \"in_progress\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseURL}}/tasks/1/status", + "host": [ + "{{baseURL}}" + ], + "path": [ + "tasks", + "1", + "status" + ] + } + }, + "response": [ + { + "name": "Patch Task", + "originalRequest": { + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"title\": \"Bug 2 Update\",\n \"description\": \"Bug desc updated\",\n \"priority\": 8,\n \"due_at\": \"07-07-2026\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseURL}}/tasks/1", + "host": [ + "{{baseURL}}" + ], + "path": [ + "tasks", + "1" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": null, + "header": [ + { + "key": "X-Powered-By", + "value": "Express" + }, + { + "key": "Access-Control-Allow-Origin", + "value": "*" + }, + { + "key": "Content-Type", + "value": "application/json; charset=utf-8" + }, + { + "key": "Content-Length", + "value": "242" + }, + { + "key": "ETag", + "value": "W/\"f2-PPfHKZEC1TYY/OoWvPWCYXMfD+k\"" + }, + { + "key": "Date", + "value": "Thu, 02 Jul 2026 07:12:52 GMT" + }, + { + "key": "Connection", + "value": "keep-alive" + }, + { + "key": "Keep-Alive", + "value": "timeout=5" + } + ], + "cookie": [], + "body": "{\n \"id\": 1,\n \"title\": \"Bug 2 Update\",\n \"description\": \"Bug desc updated\",\n \"priority\": 8,\n \"status\": \"in_progress\",\n \"due_at\": \"07-07-2026\",\n \"created_at\": \"2026-07-02 04:55:24\",\n \"updated_at\": \"2026-07-02 07:12:52\",\n \"urgencyScore\": 85,\n \"tags\": [\n {\n \"id\": 2,\n \"name\": \"init\"\n }\n ]\n}" + } + ] + }, + { + "name": "Delete Task", + "request": { + "auth": { + "type": "noauth" + }, + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseURL}}/tasks/3", + "host": [ + "{{baseURL}}" + ], + "path": [ + "tasks", + "3" + ] + } + }, + "response": [] + }, + { + "name": "Get Events", + "protocolProfileBehavior": { + "disableBodyPruning": true + }, + "request": { + "auth": { + "type": "noauth" + }, + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseURL}}/tasks?tags=Tag1&tags=Tag3", + "host": [ + "{{baseURL}}" + ], + "path": [ + "tasks" + ], + "query": [ + { + "key": "tags", + "value": "Tag1" + }, + { + "key": "tags", + "value": "Tag3" + } + ] + } + }, + "response": [ + { + "name": "Get Events", + "originalRequest": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseURL}}/events", + "host": [ + "{{baseURL}}" + ], + "path": [ + "events" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": null, + "header": [ + { + "key": "X-Powered-By", + "value": "Express" + }, + { + "key": "Access-Control-Allow-Origin", + "value": "*" + }, + { + "key": "Content-Type", + "value": "application/json; charset=utf-8" + }, + { + "key": "Content-Length", + "value": "1404" + }, + { + "key": "ETag", + "value": "W/\"57c-VQPbf5kff06z98Y9XL/X9t1Jo5I\"" + }, + { + "key": "Date", + "value": "Thu, 02 Jul 2026 07:13:10 GMT" + }, + { + "key": "Connection", + "value": "keep-alive" + }, + { + "key": "Keep-Alive", + "value": "timeout=5" + } + ], + "cookie": [], + "body": "[\n {\n \"id\": 7,\n \"task_id\": 1,\n \"event\": \"TASK_UPDATED\",\n \"payload\": \"{\\\"title\\\":\\\"Bug 2 Update\\\",\\\"description\\\":\\\"Bug desc updated\\\",\\\"priority\\\":8,\\\"due_at\\\":\\\"07-07-2026\\\"}\",\n \"created_at\": \"2026-07-02 07:12:52\"\n },\n {\n \"id\": 6,\n \"task_id\": 3,\n \"event\": \"TASK_DELETED\",\n \"payload\": \"{\\\"id\\\":3,\\\"title\\\":\\\"Bug 1\\\",\\\"description\\\":\\\"Bug desc\\\",\\\"priority\\\":10,\\\"status\\\":\\\"pending\\\",\\\"due_at\\\":\\\"05-07-2026\\\",\\\"created_at\\\":\\\"2026-07-02 06:56:21\\\",\\\"updated_at\\\":\\\"2026-07-02 06:56:21\\\"}\",\n \"created_at\": \"2026-07-02 07:07:31\"\n },\n {\n \"id\": 5,\n \"task_id\": 1,\n \"event\": \"STATUS_CHANGED\",\n \"payload\": \"{\\\"from\\\":\\\"pending\\\",\\\"to\\\":\\\"in_progress\\\"}\",\n \"created_at\": \"2026-07-02 07:07:03\"\n },\n {\n \"id\": 4,\n \"task_id\": 4,\n \"event\": \"TASK_CREATED\",\n \"payload\": \"{\\\"title\\\":\\\"Bug 2\\\",\\\"description\\\":\\\"Bug desc\\\",\\\"due_at\\\":\\\"05-06-2026\\\",\\\"priority\\\":9}\",\n \"created_at\": \"2026-07-02 07:04:28\"\n },\n {\n \"id\": 3,\n \"task_id\": 3,\n \"event\": \"TASK_CREATED\",\n \"payload\": \"{\\\"title\\\":\\\"Bug 1\\\",\\\"description\\\":\\\"Bug desc\\\",\\\"due_at\\\":\\\"05-07-2026\\\",\\\"priority\\\":10}\",\n \"created_at\": \"2026-07-02 06:56:21\"\n },\n {\n \"id\": 2,\n \"task_id\": 2,\n \"event\": \"TASK_CREATED\",\n \"payload\": \"{\\\"title\\\":\\\"Implement Problem 6\\\",\\\"description\\\":\\\"Initial Project Setup\\\",\\\"due_at\\\":\\\"03-07-2026\\\",\\\"priority\\\":1}\",\n \"created_at\": \"2026-07-02 06:53:01\"\n },\n {\n \"id\": 1,\n \"task_id\": 1,\n \"event\": \"TASK_CREATED\",\n \"payload\": \"{\\\"title\\\":\\\"Init\\\",\\\"description\\\":\\\"Initial Project Setup\\\",\\\"priority\\\":6,\\\"due_at\\\":\\\"02-07-2026\\\"}\",\n \"created_at\": \"2026-07-02 04:55:24\"\n }\n]" + } + ] + }, + { + "name": "Remove Task Tag", + "request": { + "auth": { + "type": "noauth" + }, + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseURL}}/tasks/3/tags/2", + "host": [ + "{{baseURL}}" + ], + "path": [ + "tasks", + "3", + "tags", + "2" + ] + } + }, + "response": [ + { + "name": "Remove Task Tag", + "originalRequest": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseURL}}/tasks/1/tags/1", + "host": [ + "{{baseURL}}" + ], + "path": [ + "tasks", + "1", + "tags", + "1" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": null, + "header": [ + { + "key": "X-Powered-By", + "value": "Express" + }, + { + "key": "Access-Control-Allow-Origin", + "value": "*" + }, + { + "key": "Content-Type", + "value": "application/json; charset=utf-8" + }, + { + "key": "Content-Length", + "value": "24" + }, + { + "key": "ETag", + "value": "W/\"18-iomQfn6l/AlQ4fQB5fY7bKB9btw\"" + }, + { + "key": "Date", + "value": "Thu, 02 Jul 2026 07:09:23 GMT" + }, + { + "key": "Connection", + "value": "keep-alive" + }, + { + "key": "Keep-Alive", + "value": "timeout=5" + } + ], + "cookie": [], + "body": "[\n {\n \"id\": 2,\n \"name\": \"init\"\n }\n]" + } + ] + } + ] +} \ No newline at end of file diff --git a/src/problem5/package.json b/src/problem5/package.json new file mode 100644 index 0000000000..722ee3a519 --- /dev/null +++ b/src/problem5/package.json @@ -0,0 +1,30 @@ +{ + "name": "problem5", + "module": "src/server.ts", + "type": "module", + "private": true, + "scripts": { + "dev": "tsx watch src/server.ts", + "build": "tsc && tsc-alias", + "start": "rm -rf ./dist && tsc && tsc-alias && node dist/server.js", + "db": "tsx src/db.ts" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.13", + "@types/bun": "latest", + "@types/cors": "^2.8.19", + "@types/express": "^5.0.6", + "@types/node": "^26.0.1", + "tsc-alias": "^1.8.17", + "tsx": "^4.22.4" + }, + "peerDependencies": { + "typescript": "^6.0.3" + }, + "dependencies": { + "better-sqlite3": "^12.11.1", + "cors": "^2.8.6", + "dotenv": "^17.4.2", + "express": "^5.2.1" + } +} diff --git a/src/problem5/src/app.ts b/src/problem5/src/app.ts new file mode 100644 index 0000000000..f23a3430b1 --- /dev/null +++ b/src/problem5/src/app.ts @@ -0,0 +1,18 @@ +import express from "express"; +import cors from "cors"; +import taskRoutes from "@/routes/tesk.route.js"; +import taskEventRoutes from "@/routes/task_event.route.js" +import taskTagRoutes from "@/routes/task_tag.route.js" +import { errorHandler } from "./errors/http.error.js"; + +const app = express(); + +app.use(cors()); +app.use(express.json()); + +app.use("/", taskRoutes); // tasks +app.use("/", taskEventRoutes); // events +app.use("/", taskTagRoutes); // tasks/:id/tags + +app.use(errorHandler); +export default app; \ No newline at end of file diff --git a/src/problem5/src/controllers/task.controller.ts b/src/problem5/src/controllers/task.controller.ts new file mode 100644 index 0000000000..8657f890e8 --- /dev/null +++ b/src/problem5/src/controllers/task.controller.ts @@ -0,0 +1,122 @@ +import type { CreateTaskInput, ListTasksFilter, UpdateTaskInput } from "@/interfaces/task.interface.js"; +import * as service from "@/services/task.service.js"; +import { getSingleQueryValue, getTaskId, isTaskStatus } from "@/utils.js"; +import { validatePriority, validateStatus } from "@/validations/task.validation.js"; +import type { Request, Response } from "express"; + +export function createTask(req: Request, res: Response) { + const body = req.body as Partial; + + if (!body || typeof body.title !== "string" || body.title.trim() === "") { + return res.status(400).json({ error: "title is required" }); + } + + try { + const input: CreateTaskInput = { + title: body.title + }; + if (body.description !== undefined) input.description = body.description; + if (body.due_at !== undefined) input.due_at = body.due_at; + const priority = validatePriority(body.priority) + if (priority !== undefined) input.priority = priority; + const task = service.createTask(input); + return res.status(201).json(task); + } catch (e) { + return res.status(400).json({ error: (e as Error).message }); + } +} + +export function listTasks(req: Request, res: Response) { + const statusRaw = getSingleQueryValue(req.query.status); + const priorityRaw = getSingleQueryValue(req.query.priority); + const tagsRaw = req.query.tags; + + let status: any; + + if (statusRaw !== undefined) { + if (!isTaskStatus(statusRaw)) { + return res.status(400).json({ error: "Invalid status" }); + } + status = statusRaw; + } + + let priority: number | undefined; + + try { + if (priorityRaw !== undefined) { + priority = validatePriority(Number(priorityRaw)); + } + } catch (e) { + return res.status(400).json({ error: (e as Error).message }); + } + + const tags = + typeof tagsRaw === "string" + ? tagsRaw.split(",").map(t => t.trim()).filter(Boolean) + : undefined; + + const filter: ListTasksFilter = { + ...(status && { status }), + ...(priority !== undefined && { priority }), + ...(tags && { tags }) + }; + + return res.json(service.listTasks(filter)); +} + +export function getTaskById(req: Request, res: Response) { + const id = getTaskId(req); + if (id === null) return res.status(400).json({ error: "Invalid task id" }); + + const task = service.getTaskById(id); + if (!task) return res.status(404).json({ error: "Not found" }); + + return res.json(task); +} + +export function updateStatus(req: Request, res: Response) { + const id = getTaskId(req); + if (id === null) return res.status(400).json({ error: "Invalid task id" }); + + try { + const status = validateStatus((req.body as any).status); + const task = service.updateStatus(id, status); + + if (!task) return res.status(404).json({ error: "Not found" }); + + return res.json(task); + } catch (e) { + return res.status(400).json({ error: (e as Error).message }); + } +} + +export function updateTask(req: Request, res: Response) { + const id = getTaskId(req); + if (id === null) return res.status(400).json({ error: "Invalid task id" }); + + try { + const input: UpdateTaskInput = { + ...req.body, + priority: req.body.priority !== undefined + ? validatePriority(req.body.priority) + : undefined + }; + + const task = service.updateTask(id, input); + if (!task) return res.status(404).json({ error: "Not found" }); + + return res.json(task); + } catch (e) { + return res.status(400).json({ error: (e as Error).message }); + } +} + +export function deleteTask(req: Request, res: Response) { + const id = getTaskId(req); + if (id === null) return res.status(400).json({ error: "Invalid task id" }); + + const task = service.deleteTask(id); + if (!task) return res.status(404).json({ error: "Not found" }); + + return res.status(204).send(); +} \ No newline at end of file diff --git a/src/problem5/src/controllers/task_event.controller.ts b/src/problem5/src/controllers/task_event.controller.ts new file mode 100644 index 0000000000..6bce6473af --- /dev/null +++ b/src/problem5/src/controllers/task_event.controller.ts @@ -0,0 +1,60 @@ +import type { ListTaskEventsFilter } from "@/interfaces/task_event.interface.js"; +import * as service from "@/services/task_event.service.js"; +import type { Request, Response } from "express"; + +function toNumber(value: unknown): number | null { + if (typeof value !== "string" || !/^\d+$/.test(value)) { + return null; + } + return Number(value); +} + +export function listTaskEvents(req: Request, res: Response) { + const taskIdRaw = toNumber(req.query.taskId); + const eventRaw = + typeof req.query.event === "string" + ? req.query.event + : undefined; + + const filter: ListTaskEventsFilter = {}; + + if (taskIdRaw !== null) { + filter.taskId = taskIdRaw; + } + + if (eventRaw !== undefined) { + filter.event = eventRaw; + } + + const result = service.listTaskEvents(filter); + + return res.json(result); +} + +export function getTaskEventById(req: Request, res: Response) { + const id = toNumber(req.params.id); + + if (id === null) { + return res.status(400).json({ error: "Invalid event id" }); + } + + const event = service.getTaskEventById(id); + + if (!event) { + return res.status(404).json({ error: "Not found" }); + } + + return res.json(event); +} + +export function getTaskEventsByTaskId(req: Request, res: Response) { + const taskId = toNumber(req.params.taskId); + + if (taskId === null) { + return res.status(400).json({ error: "Invalid task id" }); + } + + const events = service.getTaskEventsByTaskId(taskId); + + return res.json(events); +} \ No newline at end of file diff --git a/src/problem5/src/controllers/task_tag.controller.ts b/src/problem5/src/controllers/task_tag.controller.ts new file mode 100644 index 0000000000..39d08d859b --- /dev/null +++ b/src/problem5/src/controllers/task_tag.controller.ts @@ -0,0 +1,55 @@ +import * as service from "@/services/task_tag.service.js"; +import type { Request, Response } from "express"; + +function toNumber(value: unknown): number | null { + if (typeof value !== "string" || !/^\d+$/.test(value)) { + return null; + } + return Number(value); +} + +export function addTag(req: Request, res: Response) { + const taskId = toNumber(req.params.taskId); + if (taskId === null) { + return res.status(400).json({ error: "Invalid task id" }); + } + + const tagName = req.body?.name; + if (typeof tagName !== "string" || tagName.trim() === "") { + return res.status(400).json({ error: "Invalid tag name" }); + } + + try { + const result = service.addTagToTask(taskId, tagName.trim()); + return res.json(result); + } catch (e) { + return res.status(404).json({ error: (e as Error).message }); + } +} + +export function removeTag(req: Request, res: Response) { + const taskId = toNumber(req.params.taskId); + const tagId = toNumber(req.params.tagId); + + if (taskId === null || tagId === null) { + return res.status(400).json({ error: "Invalid ids" }); + } + + try { + const result = service.removeTagFromTask(taskId, tagId); + return res.json(result); + } catch (e) { + return res.status(404).json({ error: (e as Error).message }); + } +} + +export function getTaskTags(req: Request, res: Response) { + const taskId = toNumber(req.params.taskId); + + if (taskId === null) { + return res.status(400).json({ error: "Invalid task id" }); + } + + const result = service.getTaskTags(taskId); + return res.json(result); +} \ No newline at end of file diff --git a/src/problem5/src/db.ts b/src/problem5/src/db.ts new file mode 100644 index 0000000000..383fd8e809 --- /dev/null +++ b/src/problem5/src/db.ts @@ -0,0 +1,35 @@ +import { exec } from "@/utils.js"; + +exec(` +CREATE TABLE IF NOT EXISTS tasks ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL, + description TEXT, + priority INTEGER NOT NULL DEFAULT 5 CHECK (priority BETWEEN 1 AND 10), + status TEXT NOT NULL DEFAULT 'pending', + due_at TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) +); + +CREATE TABLE IF NOT EXISTS tags ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE +); + +CREATE TABLE IF NOT EXISTS task_tags ( + task_id INTEGER, + tag_id INTEGER, + PRIMARY KEY (task_id, tag_id) +); + +CREATE TABLE IF NOT EXISTS task_events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + task_id INTEGER, + event TEXT NOT NULL, + payload TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')) +); +CREATE INDEX IF NOT EXISTS idx_task_tags_task_id ON task_tags(task_id); +CREATE INDEX IF NOT EXISTS idx_task_tags_tag_id ON task_tags(tag_id); +`); \ No newline at end of file diff --git a/src/problem5/src/errors/http.error.ts b/src/problem5/src/errors/http.error.ts new file mode 100644 index 0000000000..5c61ce9790 --- /dev/null +++ b/src/problem5/src/errors/http.error.ts @@ -0,0 +1,33 @@ +import type { Request, Response, NextFunction } from "express"; + +export class HttpError extends Error { + statusCode: number; + + constructor(statusCode: number, message: string) { + super(message); + this.statusCode = statusCode; + } +} + +export function errorHandler( + err: unknown, + req: Request, + res: Response, + next: NextFunction +) { + if (res.headersSent) { + return next(err); + } + + if (err instanceof HttpError) { + return res.status(err.statusCode).json({ + error: err.message, + }); + } + + console.error(err); + + return res.status(500).json({ + error: "Internal Server Error", + }); +} \ No newline at end of file diff --git a/src/problem5/src/interfaces/tag.interface.ts b/src/problem5/src/interfaces/tag.interface.ts new file mode 100644 index 0000000000..32f82a12eb --- /dev/null +++ b/src/problem5/src/interfaces/tag.interface.ts @@ -0,0 +1,4 @@ +export type Tag = { + id: number; + name: string; +}; \ No newline at end of file diff --git a/src/problem5/src/interfaces/task.interface.ts b/src/problem5/src/interfaces/task.interface.ts new file mode 100644 index 0000000000..f43b717042 --- /dev/null +++ b/src/problem5/src/interfaces/task.interface.ts @@ -0,0 +1,34 @@ +export type TaskStatus = "pending" | "in_progress" | "done"; + +export interface Task { + id: number; + title: string; + description?: string; + priority: number; + status: TaskStatus; + due_at?: string; + created_at: string; + updated_at: string; +} + +export type SqlValue = string | number | null; + +export interface CreateTaskInput { + title: string; + description?: string; + priority?: number; + due_at?: string; +}; + +export interface ListTasksFilter { + status?: TaskStatus; + priority?: number; + tags?: string[]; +}; + +export interface UpdateTaskInput { + title?: string; + description?: string | null; + priority?: number; + due_at?: string | null; +}; \ No newline at end of file diff --git a/src/problem5/src/interfaces/task_event.interface.ts b/src/problem5/src/interfaces/task_event.interface.ts new file mode 100644 index 0000000000..3dbee616c6 --- /dev/null +++ b/src/problem5/src/interfaces/task_event.interface.ts @@ -0,0 +1,12 @@ +export interface TaskEvent { + id: number; + task_id: number; + event: string; + payload: string | null; + created_at: string; +}; + +export interface ListTaskEventsFilter { + taskId?: number; + event?: string; +}; diff --git a/src/problem5/src/routes/task_event.route.ts b/src/problem5/src/routes/task_event.route.ts new file mode 100644 index 0000000000..b351396bf5 --- /dev/null +++ b/src/problem5/src/routes/task_event.route.ts @@ -0,0 +1,15 @@ +import { Router } from "express"; +import * as controller from "@/controllers/task_event.controller.js"; + +const router = Router(); + +// GET /events +router.get("/events", controller.listTaskEvents); + +// GET /events/:id +router.get("/events/:id", controller.getTaskEventById); + +// GET /tasks/:taskId/events +router.get("/tasks/:taskId", controller.getTaskEventsByTaskId); + +export default router; \ No newline at end of file diff --git a/src/problem5/src/routes/task_tag.route.ts b/src/problem5/src/routes/task_tag.route.ts new file mode 100644 index 0000000000..2c174bc602 --- /dev/null +++ b/src/problem5/src/routes/task_tag.route.ts @@ -0,0 +1,15 @@ +import { Router } from "express"; +import * as controller from "@/controllers/task_tag.controller.js"; + +const router = Router(); + +// add tag to task +router.post("/tasks/:taskId/tags", controller.addTag); + +// remove tag from task +router.delete("/tasks/:taskId/tags/:tagId", controller.removeTag); + +// get tags for task +router.get("/tasks/:taskId/tags", controller.getTaskTags); + +export default router; \ No newline at end of file diff --git a/src/problem5/src/routes/tesk.route.ts b/src/problem5/src/routes/tesk.route.ts new file mode 100644 index 0000000000..0c6aecc869 --- /dev/null +++ b/src/problem5/src/routes/tesk.route.ts @@ -0,0 +1,24 @@ +import { Router } from "express"; +import * as controller from "@/controllers/task.controller.js"; + +const router = Router(); +// BASE URL /tasks +// create task +router.post("/tasks", controller.createTask); + +// list tasks +router.get("/tasks", controller.listTasks); + +// single task +router.get("/tasks/:id", controller.getTaskById); + +// status change +router.patch("/tasks/:id/status", controller.updateStatus); + +// update single task +router.patch("/tasks/:id", controller.updateTask); + +// delete +router.delete("/tasks/:id", controller.deleteTask); + +export default router; \ No newline at end of file diff --git a/src/problem5/src/server.ts b/src/problem5/src/server.ts new file mode 100644 index 0000000000..867ac1d85b --- /dev/null +++ b/src/problem5/src/server.ts @@ -0,0 +1,7 @@ +import app from "./app.js"; + +const PORT = process.env.PORT || 3000; + +app.listen(PORT, () => { + console.log(`Server running on http://localhost:${PORT}`); +}); \ No newline at end of file diff --git a/src/problem5/src/services/tag.service.ts b/src/problem5/src/services/tag.service.ts new file mode 100644 index 0000000000..8f461f7785 --- /dev/null +++ b/src/problem5/src/services/tag.service.ts @@ -0,0 +1,33 @@ +import type { Tag } from "@/interfaces/tag.interface.js"; +import { execute, query, queryOne } from "@/utils.js"; + +export function createTag(name: string) { + execute( + `INSERT OR IGNORE INTO tags (name) VALUES (?)`, + [name] + ); + + return getTagByName(name); +} + +export function getTagByName(name: string) { + return queryOne( + `SELECT id, name FROM tags WHERE name = ?`, + [name] + ); +} + +export function getOrCreateTag(name: string) { + const existing = getTagByName(name); + if (existing) return existing; + + execute(`INSERT INTO tags (name) VALUES (?)`, [name]); + return getTagByName(name); +} + +export function listTags() { + return query( + `SELECT id, name FROM tags ORDER BY name ASC`, + [] + ); +} \ No newline at end of file diff --git a/src/problem5/src/services/task.service.ts b/src/problem5/src/services/task.service.ts new file mode 100644 index 0000000000..5ecfe7437b --- /dev/null +++ b/src/problem5/src/services/task.service.ts @@ -0,0 +1,227 @@ +import { calculateUrgency, execute, query, queryOne } from "@/utils.js"; +import { getTaskTagsBulk } from "@/services/task_tag.service.js"; +import type { + CreateTaskInput, + ListTasksFilter, + SqlValue, + Task, + TaskStatus, + UpdateTaskInput +} from "@/interfaces/task.interface.js"; + +function normalizePriority(priority: unknown): number { + if (priority === undefined || priority === null) { + throw new Error("priority is required"); + } + + if (typeof priority !== "number" || !Number.isInteger(priority)) { + throw new Error("priority must be integer"); + } + + if (priority < 1 || priority > 10) { + throw new Error("priority must be between 1 and 10"); + } + + return priority; +} + +function enrichTasks(tasks: Task[]) { + const ids = tasks.map(t => t.id); + const tagMap = getTaskTagsBulk(ids); + + return tasks.map(task => ({ + ...task, + urgencyScore: calculateUrgency(task), + tags: tagMap.get(task.id) ?? [] + })); +} + +export function createTask(input: CreateTaskInput) { + const result = execute( + ` + INSERT INTO tasks (title, description, priority, due_at, updated_at) + VALUES (?, ?, ?, ?, datetime('now')) + `, + [ + input.title, + input.description ?? null, + normalizePriority(input.priority), + input.due_at ?? null + ] + ); + + const id = Number(result.lastInsertRowid); + + execute( + ` + INSERT INTO task_events (task_id, event, payload) + VALUES (?, 'TASK_CREATED', ?) + `, + [id, JSON.stringify(input)] + ); + + return getTaskById(id); +} + +export function getTaskById(id: number) { + const task = queryOne( + "SELECT * FROM tasks WHERE id = ?", + [id] + ); + + if (!task) return null; + + const tagsMap = getTaskTagsBulk([id]); + + return { + ...task, + urgencyScore: calculateUrgency(task), + tags: tagsMap.get(id) ?? [] + }; +} + +export function listTasks(filter: ListTasksFilter = {}) { + const params: SqlValue[] = []; + + let sql = ` + SELECT t.* + FROM tasks t + WHERE 1 = 1 + `; + + if (filter.status) { + sql += ` AND t.status = ?`; + params.push(filter.status); + } + + if (filter.priority !== undefined) { + sql += ` AND t.priority = ?`; + params.push(filter.priority); + } + + if (filter.tags?.length) { + const placeholders = filter.tags.map(() => "?").join(","); + + sql += ` + AND EXISTS ( + SELECT 1 + FROM task_tags tt + JOIN tags tg ON tg.id = tt.tag_id + WHERE tt.task_id = t.id + AND tg.name IN (${placeholders}) + ) + `; + + params.push(...filter.tags); + } + + const tasks = query(sql, params); + + return enrichTasks(tasks) + .sort((a, b) => b.urgencyScore - a.urgencyScore); +} + +export function updateStatus(id: number, status: TaskStatus) { + const current = queryOne( + "SELECT * FROM tasks WHERE id = ?", + [id] + ); + + if (!current) return null; + + execute( + ` + UPDATE tasks + SET status = ?, updated_at = datetime('now') + WHERE id = ? + `, + [status, id] + ); + + execute( + ` + INSERT INTO task_events (task_id, event, payload) + VALUES (?, 'STATUS_CHANGED', ?) + `, + [id, JSON.stringify({ from: current.status, to: status })] + ); + + return getTaskById(id); +} + +export function updateTask(id: number, input: UpdateTaskInput) { + const current = queryOne( + "SELECT * FROM tasks WHERE id = ?", + [id] + ); + + if (!current) return null; + + const updates: string[] = []; + const params: SqlValue[] = []; + + if (input.title !== undefined) { + updates.push("title = ?"); + params.push(input.title); + } + + if (input.description !== undefined) { + updates.push("description = ?"); + params.push(input.description); + } + + if (input.priority !== undefined) { + updates.push("priority = ?"); + params.push(normalizePriority(input.priority)); + } + + if (input.due_at !== undefined) { + updates.push("due_at = ?"); + params.push(input.due_at); + } + + if (!updates.length) return getTaskById(id); + + updates.push("updated_at = datetime('now')"); + params.push(id); + + execute( + ` + UPDATE tasks + SET ${updates.join(", ")} + WHERE id = ? + `, + params + ); + + execute( + ` + INSERT INTO task_events (task_id, event, payload) + VALUES (?, 'TASK_UPDATED', ?) + `, + [id, JSON.stringify(input)] + ); + + return getTaskById(id); +} + +export function deleteTask(id: number) { + const current = queryOne( + "SELECT * FROM tasks WHERE id = ?", + [id] + ); + + if (!current) return null; + + execute( + ` + INSERT INTO task_events (task_id, event, payload) + VALUES (?, 'TASK_DELETED', ?) + `, + [id, JSON.stringify(current)] + ); + + execute("DELETE FROM tasks WHERE id = ?", [id]); + + return current; +} \ No newline at end of file diff --git a/src/problem5/src/services/task_event.service.ts b/src/problem5/src/services/task_event.service.ts new file mode 100644 index 0000000000..441fcf92c6 --- /dev/null +++ b/src/problem5/src/services/task_event.service.ts @@ -0,0 +1,35 @@ +import type { ListTaskEventsFilter, TaskEvent } from "@/interfaces/task_event.interface.js"; +import { query, queryOne } from "@/utils.js"; + +export function listTaskEvents(filter: ListTaskEventsFilter = {}) { + let sql = "SELECT * FROM task_events WHERE 1 = 1"; + const params: (string | number)[] = []; + + if (filter.taskId !== undefined) { + sql += " AND task_id = ?"; + params.push(filter.taskId); + } + + if (filter.event !== undefined) { + sql += " AND event = ?"; + params.push(filter.event); + } + + sql += " ORDER BY created_at DESC"; + + return query(sql, params); +} + +export function getTaskEventById(id: number) { + return queryOne( + "SELECT * FROM task_events WHERE id = ?", + [id] + ); +} + +export function getTaskEventsByTaskId(taskId: number) { + return query( + "SELECT * FROM task_events WHERE task_id = ? ORDER BY created_at DESC", + [taskId] + ); +} \ No newline at end of file diff --git a/src/problem5/src/services/task_tag.service.ts b/src/problem5/src/services/task_tag.service.ts new file mode 100644 index 0000000000..e70a8949a1 --- /dev/null +++ b/src/problem5/src/services/task_tag.service.ts @@ -0,0 +1,92 @@ +import { execute, query, queryOne } from "@/utils.js"; +import { getOrCreateTag } from "./tag.service.js"; +import type { Tag } from "@/interfaces/tag.interface.js"; + +function assertTaskExists(taskId: number) { + const task = queryOne<{ id: number }>( + `SELECT id FROM tasks WHERE id = ?`, + [taskId] + ); + + if (!task) { + throw new Error("Task not found"); + } +} + +export function addTagToTask(taskId: number, tagName: string) { + assertTaskExists(taskId); + + const tag = getOrCreateTag(tagName); + + execute( + ` + INSERT OR IGNORE INTO task_tags (task_id, tag_id) + VALUES (?, ?) + `, + [taskId, tag.id] + ); + + return getTaskTags(taskId); +} + +export function removeTagFromTask(taskId: number, tagId: number) { + assertTaskExists(taskId); + + execute( + ` + DELETE FROM task_tags + WHERE task_id = ? AND tag_id = ? + `, + [taskId, tagId] + ); + + return getTaskTags(taskId); +} + +export function getTaskTags(taskId: number): Tag[] { + return query( + ` + SELECT t.id, t.name + FROM tags t + JOIN task_tags tt ON tt.tag_id = t.id + WHERE tt.task_id = ? + ORDER BY t.name ASC + `, + [taskId] + ); +} + +export function getTaskTagsBulk(taskIds: number[]): Map { + const map = new Map(); + + if (!taskIds.length) return map; + + const placeholders = taskIds.map(() => "?").join(","); + + const rows = query< + Tag & { task_id: number } + >( + ` + SELECT tt.task_id, t.id, t.name + FROM task_tags tt + JOIN tags t ON t.id = tt.tag_id + WHERE tt.task_id IN (${placeholders}) + `, + taskIds + ); + + for (const id of taskIds) { + map.set(id, []); + } + + for (const row of rows) { + const { task_id, id, name } = row; + + const list = map.get(task_id); + if (list) { + list.push({ id, name }); + } + } + + return map; +} \ No newline at end of file diff --git a/src/problem5/src/utils.ts b/src/problem5/src/utils.ts new file mode 100644 index 0000000000..c3ef7b074a --- /dev/null +++ b/src/problem5/src/utils.ts @@ -0,0 +1,65 @@ +import Database from "better-sqlite3"; +import type { Task, TaskStatus } from "./interfaces/task.interface.js"; +import type { Request } from "express"; + +const instance = new Database("code-challenge.db"); + +instance.pragma("foreign_keys = ON"); + +export function query(sql: string, params: unknown[] = []) { + return instance.prepare(sql).all(...params) as T[]; +} + +export function queryOne(sql: string, params: unknown[] = []) { + return instance.prepare(sql).get(...params) as T; +} + +export function execute(sql: string, params: unknown[] = []) { + return instance.prepare(sql).run(...params); +} + +export function exec(sql: string) { + instance.exec(sql); +} + + +export function daysUntil(date: string) { + const diff = new Date(date).getTime() - Date.now(); + return Math.ceil(diff / (1000 * 60 * 60 * 24)); +} + +export function calculateUrgency(task: Task) { + const dueWeight = task.due_at + ? Math.max(0, 10 - daysUntil(task.due_at)) + : 0; + + return task.priority * 10 + dueWeight; +} + +export const taskStatuses = ["pending", "in_progress", "done"] as const; + +export function isTaskStatus(value: unknown): value is TaskStatus { + return ( + typeof value === "string" && + taskStatuses.includes(value as TaskStatus) + ); +} + +export function isValidId(value: string): boolean { + return /^\d+$/.test(value) && Number(value) > 0; +} + +export function getTaskId(req: Request): number | null { + const rawId = req.params.id; + if (typeof rawId !== "string") { + return null; + } + if (!isValidId(rawId)) { + return null; + } + return Number(rawId); +} + +export function getSingleQueryValue(value: unknown): string | undefined { + return typeof value === "string" ? value : undefined; +} diff --git a/src/problem5/src/validations/task.validation.ts b/src/problem5/src/validations/task.validation.ts new file mode 100644 index 0000000000..ecbad1014c --- /dev/null +++ b/src/problem5/src/validations/task.validation.ts @@ -0,0 +1,20 @@ +import { HttpError } from "@/errors/http.error.js"; +import type { TaskStatus } from "@/interfaces/task.interface.js"; + +export function validatePriority(value: unknown): number | undefined { + if (value === undefined) return undefined; + if (typeof value !== "number" || !Number.isInteger(value)) { + throw new HttpError(400, "priority must be integer"); + } + if (value < 1 || value > 10) { + throw new HttpError(400, "priority must be between 1 and 10"); + } + return value; +} + +export function validateStatus(value: unknown): TaskStatus { + if (value === "pending" || value === "in_progress" || value === "done") { + return value; + } + throw new HttpError(400, "invalid status"); +} \ No newline at end of file diff --git a/src/problem5/tsconfig.json b/src/problem5/tsconfig.json new file mode 100644 index 0000000000..aa3eb44633 --- /dev/null +++ b/src/problem5/tsconfig.json @@ -0,0 +1,45 @@ +{ + // Visit https://aka.ms/tsconfig to read more about this file + "compilerOptions": { + // File Layout + "rootDir": "./src", + "outDir": "./dist", + "paths": { + "@/*": ["./src/*"] + }, + + // Environment Settings + // See also https://aka.ms/tsconfig/module + "module": "nodenext", + "target": "esnext", + "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, + "verbatimModuleSyntax": true, + "isolatedModules": true, + "noUncheckedSideEffectImports": true, + "moduleDetection": "force", + "skipLibCheck": true, + }, + "include": ["src"], +} diff --git a/src/problem6/README.md b/src/problem6/README.md new file mode 100644 index 0000000000..93287e7e7c --- /dev/null +++ b/src/problem6/README.md @@ -0,0 +1,318 @@ +# Problem 6 – Scoreboard Service Project Documentation + +## What this service is + +This is a small backend service that keeps track of user scores and powers a **live Top 10 leaderboard**. + +The idea is simple: + +* A user does some action in the product +* The frontend sends a request to the backend saying “this user earned points” +* The backend verifies it (important) +* Score gets updated +* Everyone watching the leaderboard sees updates in real time + +The tricky part is not the scoring itself, but: + +* preventing fake score increments +* keeping leaderboard fast +* pushing updates live without hammering the database + +--- + +## High-level design + +At a high level, I’d split it like this: + +* **API Server** + + * receives score update requests + * validates them properly + * writes to DB + cache + +* **Postgres (source of truth)** + + * stores user scores permanently + +* **Redis (fast path)** + + * maintains leaderboard (sorted set) + * avoids hitting DB for every leaderboard request + +* **WebSocket / SSE layer** + + * pushes live leaderboard updates to clients + +--- + +## Score update flow (important part) + +This is the actual request lifecycle: + +1. User performs an action in frontend +2. Frontend sends a request to backend: + + * includes `userId` + * includes `actionId` + * includes signed payload (important) +3. Backend validates: + + * signature is correct + * request is not expired + * action wasn’t already used (idempotency check) +4. If valid: + + * increment score in DB + * update Redis leaderboard +5. Broadcast update event +6. Connected clients get new Top 10 immediately + +--- + +## API + +### Increment score + +``` +POST /api/v1/scores/increment +``` + +### Request + +```json +{ + "userId": "uuid", + "actionId": "string", + "timestamp": 1710000000, + "signature": "HMAC_HASH" +} +``` + +### Response + +```json +{ + "success": true, + "newScore": 120 +} +``` + +--- + +## Security (this is the important bit) + +If you skip this, the whole system is basically a cheat engine. + +### 1. Request signing (HMAC) + +Every request must be signed: + +``` +signature = HMAC(secret, userId + actionId + timestamp) +``` + +Server recomputes it and compares. + +Why: + +* prevents fake score injection from random clients +* ensures request came from trusted frontend/backend flow + +--- + +### 2. Replay protection + +Each `actionId` can only be used once per user. + +Store it in: + +* Redis (fast TTL-based) +* or DB unique constraint (safer long-term) + +--- + +### 3. Timestamp window + +Reject requests older than ~5 minutes. + +Prevents replaying old valid requests. + +--- + +### 4. Rate limiting + +Basic protection layer: + +* per user: limit score increments/sec +* per IP: global throttling + +Nothing fancy, just enough to stop abuse loops. + +--- + +## Data model + +### users + +```sql +id UUID PRIMARY KEY, +username TEXT, +score INT DEFAULT 0, +updated_at TIMESTAMP +``` + +### processed_actions + +Used for idempotency. + +```sql +user_id UUID, +action_id TEXT, +created_at TIMESTAMP, +PRIMARY KEY (user_id, action_id) +``` + +--- + +## Leaderboard strategy + +Instead of querying DB every time (bad idea under load), Redis handles it. + +We use a **Sorted Set**: + +### Update score + +``` +ZINCRBY leaderboard +``` + +### Get Top 10 + +``` +ZREVRANGE leaderboard 0 9 WITHSCORES +``` + +Why this works well: + +* O(log N) updates +* super fast reads +* perfect for “Top K” use cases + +DB stays the source of truth, Redis is just the fast view. + +--- + +## Real-time updates + +When score changes: + +* API updates Redis +* then emits an event: + +```json +{ + "type": "SCORE_UPDATED", + "userId": "uuid", + "score": 120 +} +``` + +WebSocket server listens and pushes updated leaderboard to clients. + +--- + +## Execution flow diagram + +```mermaid +sequenceDiagram + participant Client + participant API + participant Redis + participant DB + participant WS + + Client->>API: POST score increment (signed) + API->>API: validate signature + timestamp + API->>Redis: check actionId (dedupe) + API->>DB: update user score + API->>Redis: update leaderboard ZSET + API->>WS: publish update event + WS->>Client: push updated top 10 +``` + +--- + +## Failure handling (real-world stuff) + +A few practical things I’d expect in production: + +* DB is always the source of truth +* Redis can be rebuilt from DB if needed +* writes should be idempotent (safe retries) +* background retry queue for failed updates + +--- + +## Things I would improve if I had more time + +These are not required for the challenge, but good engineering additions: + +### 1. Move to event-driven design + +Instead of writing directly: + +* API → DB → Redis + +We could do: + +* API → Kafka event → workers update DB/Redis + +This improves scaling and decoupling. + +--- + +### 2. Add anomaly detection + +Even simple heuristics help: + +* sudden score spikes +* repeated action patterns +* abnormal request frequency + +Can be flagged for review. + +--- + +### 3. Separate read/write models (CQRS style) + +* Write path → Postgres +* Read path → Redis leaderboard + +Keeps reads extremely fast. + +--- + +### 4. Observability (often ignored but important) + +* structured logs per request +* metrics: + + * score update rate + * rejected requests (auth failures) + * Redis latency +* tracing if system grows + +--- + +## Summary + +The main idea of this design is: + +* DB = truth +* Redis = speed layer +* WebSockets = live updates +* HMAC + idempotency = abuse protection + +It’s not over-engineered, but it’s structured in a way that can scale without rewriting everything later. + +--- diff --git a/src/problem6/docs/API_SPECIFICATION.md b/src/problem6/docs/API_SPECIFICATION.md new file mode 100644 index 0000000000..9da3317acc --- /dev/null +++ b/src/problem6/docs/API_SPECIFICATION.md @@ -0,0 +1,169 @@ +# `API_SPECIFICATION.md` + +*(Detailed API + contracts + security expectations)* + +## Overview + +This file defines the exact API behavior for the Scoreboard Service. + +The goal is to make the backend implementation deterministic: + +* no guessing endpoints +* no ambiguity in auth +* no confusion about hashing or validation rules + +--- + +## Base URL + +``` +/api/v1 +``` + +--- + +## Authentication Model + +This service does NOT rely on classic session login for score updates. + +Instead, each request must be **cryptographically signed**. + +### Why + +Because score updates are: + +* client-triggered +* high abuse risk +* easy to spoof if left open + +So we enforce request integrity at the API level. + +--- + +## Signing Strategy (HMAC) + +### Signature formula + +``` +signature = HMAC_SHA256( + secret_key, + userId + ":" + actionId + ":" + timestamp +) +``` + +--- + +## Headers + +Every request must include: + +``` +X-Signature: +X-Timestamp: +Content-Type: application/json +``` + +Optional but recommended: + +``` +X-Client-Version: 1.0.0 +``` + +--- + +## Endpoint: Increment Score + +### POST `/scores/increment` + +This is the ONLY write endpoint for score changes. + +--- + +### Request Body + +```json +{ + "userId": "uuid", + "actionId": "string-unique-action-id", + "points": 10 +} +``` + +--- + +### Validation Rules + +Backend must enforce: + +* `userId` must exist +* `actionId` must be unique per user +* `points > 0` +* timestamp must be within ±300 seconds +* signature must match HMAC formula + +--- + +### Response + +```json +{ + "success": true, + "newScore": 150, + "rank": 4 +} +``` + +--- + +## Endpoint: Get Leaderboard + +### GET `/leaderboard/top` + +Returns top 10 users. + +--- + +### Response + +```json +{ + "top": [ + { "userId": "u1", "score": 200 }, + { "userId": "u2", "score": 180 } + ], + "generatedAt": 1710000000 +} +``` + +--- + +## Internal Behavior (important for reviewers) + +When a score update happens: + +1. validate request +2. check idempotency (`actionId`) +3. update Postgres +4. update Redis sorted set +5. emit event to pub/sub channel + +--- + +## Failure Modes + +| Case | Behavior | +| ----------------- | -------- | +| invalid signature | 401 | +| expired timestamp | 401 | +| reused actionId | 409 | +| invalid payload | 400 | + +--- + +## Notes / Tradeoffs + +* Redis is eventually consistent with DB (acceptable for leaderboard UX) +* DB is source of truth +* We prioritize write safety over instant consistency + +--- diff --git a/src/problem6/docs/PROJECT_PLAN.md b/src/problem6/docs/PROJECT_PLAN.md new file mode 100644 index 0000000000..03d448b9c9 --- /dev/null +++ b/src/problem6/docs/PROJECT_PLAN.md @@ -0,0 +1,162 @@ +# `PROJECT_PLAN.md` + +*(Timeline + engineering breakdown)* + +## Goal + +Build a backend service that supports: + +* secure score updates +* real-time leaderboard +* scalable read/write separation + +--- + +## Assumptions + +* small team (1–3 engineers) +* no existing infra constraints +* we can use Redis + Postgres +* WebSocket or SSE available + +--- + +## Phase 1 — Core Backend (Day 1–2) + +### Deliverables + +* Express/Node or Go API skeleton +* `/scores/increment` endpoint +* Postgres schema setup +* basic validation layer + +### Tasks + +* setup project structure +* implement DB models +* implement HMAC verification +* implement idempotency check + +--- + +## Phase 2 — Leaderboard System (Day 2–3) + +### Deliverables + +* Redis sorted set leaderboard +* `/leaderboard/top` endpoint + +### Tasks + +* integrate Redis client +* implement ZINCRBY logic +* implement top 10 retrieval +* sync DB → Redis updates + +--- + +## Phase 3 — Real-time Updates (Day 3–4) + +### Deliverables + +* WebSocket server OR SSE stream +* live leaderboard push + +### Tasks + +* pub/sub channel setup +* event emitter on score update +* client broadcast logic + +--- + +## Phase 4 — Security Hardening (Day 4–5) + +### Deliverables + +* abuse protection layer +* logging + audit trail + +### Tasks + +* rate limiting middleware +* replay protection (timestamp + nonce) +* structured logs for score changes + +--- + +## Phase 5 — Load & Stability (Day 5–6) + +### Deliverables + +* stress-tested endpoints +* basic monitoring hooks + +### Tasks + +* simulate high write load +* validate Redis behavior under pressure +* ensure DB remains stable +* add retry queue (optional) + +--- + +## Architecture Milestones + +| Stage | Focus | +| ----- | -------------------- | +| MVP | score updates + DB | +| v1 | Redis leaderboard | +| v2 | real-time updates | +| v3 | anti-cheat + scaling | + +--- + +## Risks + +### 1. Fake score injection + +Mitigated via: + +* HMAC signing +* timestamp validation + +--- + +### 2. Redis desync + +Mitigated via: + +* DB as source of truth +* periodic rebuild job (optional) + +--- + +### 3. High write traffic + +Mitigated via: + +* Redis-first leaderboard +* async event pipeline (future upgrade) + +--- + +## Optional Future Improvements + +* Kafka event bus +* multi-region leaderboard +* anomaly detection layer +* per-user score caps + +--- + +## Final Note + +If this were production, I’d expect: + +* DB is never bypassed +* Redis is rebuildable anytime +* all writes are idempotent +* security is enforced at request boundary, not business logic + +--- \ No newline at end of file