From 9fc61a47442da2691c3fba2993f4de353a49c3d8 Mon Sep 17 00:00:00 2001 From: Lin Htut Kyaw Date: Wed, 1 Jul 2026 14:34:42 +0630 Subject: [PATCH 01/13] chore: golang implementation of problem4 three ways to sum to n --- src/problem4/README.md | 124 ++++++++++++++++++++++++++++++++++++++ src/problem4/go.mod | 3 + src/problem4/main.go | 97 +++++++++++++++++++++++++++++ src/problem4/main_test.go | 21 +++++++ 4 files changed, 245 insertions(+) create mode 100644 src/problem4/README.md create mode 100644 src/problem4/go.mod create mode 100644 src/problem4/main.go create mode 100644 src/problem4/main_test.go 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) + } +} From 1529b170b79979ac1c76bc89aba0ba755979d409 Mon Sep 17 00:00:00 2001 From: Lin Htut Kyaw Date: Wed, 1 Jul 2026 14:41:13 +0630 Subject: [PATCH 02/13] chore: bun init with typescript --- src/problem5/.gitignore | 34 ++++++++++++++++++++++++++++++++++ src/problem5/README.md | 15 +++++++++++++++ src/problem5/bun.lock | 26 ++++++++++++++++++++++++++ src/problem5/index.ts | 1 + src/problem5/package.json | 12 ++++++++++++ src/problem5/tsconfig.json | 30 ++++++++++++++++++++++++++++++ 6 files changed, 118 insertions(+) create mode 100644 src/problem5/.gitignore create mode 100644 src/problem5/README.md create mode 100644 src/problem5/bun.lock create mode 100644 src/problem5/index.ts create mode 100644 src/problem5/package.json create mode 100644 src/problem5/tsconfig.json 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..2c25a4dd75 --- /dev/null +++ b/src/problem5/README.md @@ -0,0 +1,15 @@ +# problem5 + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +This project was created using `bun init` in bun v1.3.14. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. diff --git a/src/problem5/bun.lock b/src/problem5/bun.lock new file mode 100644 index 0000000000..7fa8376e18 --- /dev/null +++ b/src/problem5/bun.lock @@ -0,0 +1,26 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "problem5", + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], + + "@types/node": ["@types/node@26.0.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-fc3KiUoBt6kie0N9bIW3E47vZsuaMf0PM2AaUpLCLT0s/LvX1nxAim6Fc049cNxODPpGm6qRAuUOB86SkRuPQw=="], + + "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], + } +} diff --git a/src/problem5/index.ts b/src/problem5/index.ts new file mode 100644 index 0000000000..f67b2c6454 --- /dev/null +++ b/src/problem5/index.ts @@ -0,0 +1 @@ +console.log("Hello via Bun!"); \ No newline at end of file diff --git a/src/problem5/package.json b/src/problem5/package.json new file mode 100644 index 0000000000..bd1b04d27c --- /dev/null +++ b/src/problem5/package.json @@ -0,0 +1,12 @@ +{ + "name": "problem5", + "module": "index.ts", + "type": "module", + "private": true, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5" + } +} diff --git a/src/problem5/tsconfig.json b/src/problem5/tsconfig.json new file mode 100644 index 0000000000..b2e7497d89 --- /dev/null +++ b/src/problem5/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + "types": ["bun"], + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} From e0cfe7067d3fa26e6866e7d879c86d43cfe83549 Mon Sep 17 00:00:00 2001 From: Lin Htut Kyaw Date: Wed, 1 Jul 2026 16:28:48 +0630 Subject: [PATCH 03/13] chore: initial express typescript setup --- src/problem5/bun.lock | 306 +++++++++++++++++- src/problem5/index.ts | 1 - src/problem5/package.json | 21 +- src/problem5/src/app.ts | 12 + .../src/controllers/hello.controller.ts | 7 + src/problem5/src/routes/hello.routes.ts | 10 + src/problem5/src/server.ts | 7 + src/problem5/src/services/hello.service.ts | 1 + src/problem5/tsconfig.json | 61 ++-- 9 files changed, 397 insertions(+), 29 deletions(-) delete mode 100644 src/problem5/index.ts create mode 100644 src/problem5/src/app.ts create mode 100644 src/problem5/src/controllers/hello.controller.ts create mode 100644 src/problem5/src/routes/hello.routes.ts create mode 100644 src/problem5/src/server.ts create mode 100644 src/problem5/src/services/hello.service.ts diff --git a/src/problem5/bun.lock b/src/problem5/bun.lock index 7fa8376e18..48a95b15d5 100644 --- a/src/problem5/bun.lock +++ b/src/problem5/bun.lock @@ -4,23 +4,325 @@ "workspaces": { "": { "name": "problem5", + "dependencies": { + "cors": "^2.8.6", + "dotenv": "^17.4.2", + "express": "^5.2.1", + }, "devDependencies": { "@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": "^5", + "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/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=="], + + "binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="], + + "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=="], + "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "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=="], + + "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=="], + + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + + "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=="], + + "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=="], + + "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=="], + + "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=="], + + "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=="], + + "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=="], + + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "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=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "mylas": ["mylas@2.1.14", "", {}, "sha512-BzQguy9W9NJgoVn2mRWzbFrFWWztGCcng2QI9+41frfk+Athwgx3qhqhvStz7ExeUUu7Kzw427sNzHpEZNINog=="], + + "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], + + "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=="], + + "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=="], + + "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=="], + + "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=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "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=="], + + "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], + + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + + "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=="], + + "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=="], + + "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/index.ts b/src/problem5/index.ts deleted file mode 100644 index f67b2c6454..0000000000 --- a/src/problem5/index.ts +++ /dev/null @@ -1 +0,0 @@ -console.log("Hello via Bun!"); \ No newline at end of file diff --git a/src/problem5/package.json b/src/problem5/package.json index bd1b04d27c..67f87f5ab0 100644 --- a/src/problem5/package.json +++ b/src/problem5/package.json @@ -1,12 +1,27 @@ { "name": "problem5", - "module": "index.ts", + "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" + }, "devDependencies": { - "@types/bun": "latest" + "@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": "^5" + "typescript": "^6.0.3" + }, + "dependencies": { + "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..740ccccc40 --- /dev/null +++ b/src/problem5/src/app.ts @@ -0,0 +1,12 @@ +import express from "express"; +import cors from "cors"; +import helloRoutes from "@/routes/hello.routes.js"; + +const app = express(); + +app.use(cors()); +app.use(express.json()); + +app.use("/", helloRoutes); + +export default app; \ No newline at end of file diff --git a/src/problem5/src/controllers/hello.controller.ts b/src/problem5/src/controllers/hello.controller.ts new file mode 100644 index 0000000000..97a8ea0149 --- /dev/null +++ b/src/problem5/src/controllers/hello.controller.ts @@ -0,0 +1,7 @@ +import type { Request, Response } from "express"; +import * as service from "@/services/hello.service.js"; + +export const sayHello = async (req: Request, res: Response) => { + const data = service.getHelloData() + res.json(data); +}; diff --git a/src/problem5/src/routes/hello.routes.ts b/src/problem5/src/routes/hello.routes.ts new file mode 100644 index 0000000000..0600784840 --- /dev/null +++ b/src/problem5/src/routes/hello.routes.ts @@ -0,0 +1,10 @@ +import { Router } from "express"; +import { + sayHello +} from "@/controllers/hello.controller.js"; + +const router = Router(); + +router.get("/", sayHello) + +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/hello.service.ts b/src/problem5/src/services/hello.service.ts new file mode 100644 index 0000000000..c631b34431 --- /dev/null +++ b/src/problem5/src/services/hello.service.ts @@ -0,0 +1 @@ +export const getHelloData = () => ({"message": "Hello"}) \ No newline at end of file diff --git a/src/problem5/tsconfig.json b/src/problem5/tsconfig.json index b2e7497d89..aa3eb44633 100644 --- a/src/problem5/tsconfig.json +++ b/src/problem5/tsconfig.json @@ -1,30 +1,45 @@ { + // Visit https://aka.ms/tsconfig to read more about this file "compilerOptions": { - // Environment setup & latest features - "lib": ["ESNext"], - "target": "ESNext", - "module": "Preserve", - "moduleDetection": "force", - "jsx": "react-jsx", - "allowJs": true, - "types": ["bun"], + // File Layout + "rootDir": "./src", + "outDir": "./dist", + "paths": { + "@/*": ["./src/*"] + }, - // Bundler mode - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "noEmit": true, + // Environment Settings + // See also https://aka.ms/tsconfig/module + "module": "nodenext", + "target": "esnext", + "lib": ["esnext"], + "types": ["node"], + // and npm install -D @types/node - // Best practices - "strict": true, - "skipLibCheck": true, - "noFallthroughCasesInSwitch": true, + // Other Outputs + "sourceMap": true, + "declaration": true, + "declarationMap": true, + + // Stricter Typechecking Options "noUncheckedIndexedAccess": true, - "noImplicitOverride": true, + "exactOptionalPropertyTypes": true, - // Some stricter flags (disabled by default) - "noUnusedLocals": false, - "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false - } + // 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"], } From 6df30510539ca652c8bb7748c7ddb1a9f0e31766 Mon Sep 17 00:00:00 2001 From: Lin Htut Kyaw Date: Wed, 1 Jul 2026 19:37:19 +0630 Subject: [PATCH 04/13] chore: task crud with urgency sorting --- src/problem5/bun.lock | 74 ++++++++++++++ src/problem5/code-challenge.db | Bin 0 -> 32768 bytes src/problem5/package.json | 5 +- src/problem5/src/app.ts | 4 +- .../src/controllers/hello.controller.ts | 7 -- .../src/controllers/task.controller.ts | 36 +++++++ src/problem5/src/db.ts | 33 +++++++ src/problem5/src/db_utils.ts | 19 ++++ src/problem5/src/interfaces/task.interface.ts | 12 +++ src/problem5/src/routes/hello.routes.ts | 10 -- src/problem5/src/routes/tesk.route.ts | 21 ++++ src/problem5/src/services/hello.service.ts | 1 - src/problem5/src/services/task.service.ts | 90 ++++++++++++++++++ 13 files changed, 291 insertions(+), 21 deletions(-) create mode 100644 src/problem5/code-challenge.db delete mode 100644 src/problem5/src/controllers/hello.controller.ts create mode 100644 src/problem5/src/controllers/task.controller.ts create mode 100644 src/problem5/src/db.ts create mode 100644 src/problem5/src/db_utils.ts create mode 100644 src/problem5/src/interfaces/task.interface.ts delete mode 100644 src/problem5/src/routes/hello.routes.ts create mode 100644 src/problem5/src/routes/tesk.route.ts delete mode 100644 src/problem5/src/services/hello.service.ts create mode 100644 src/problem5/src/services/task.service.ts diff --git a/src/problem5/bun.lock b/src/problem5/bun.lock index 48a95b15d5..bda5b875f2 100644 --- a/src/problem5/bun.lock +++ b/src/problem5/bun.lock @@ -5,11 +5,13 @@ "": { "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", @@ -81,6 +83,8 @@ "@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=="], @@ -111,12 +115,22 @@ "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=="], @@ -127,6 +141,8 @@ "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=="], @@ -141,8 +157,14 @@ "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=="], @@ -153,6 +175,8 @@ "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=="], @@ -165,12 +189,16 @@ "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=="], @@ -179,6 +207,8 @@ "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=="], @@ -189,6 +219,8 @@ "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=="], @@ -203,10 +235,14 @@ "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=="], @@ -233,12 +269,22 @@ "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=="], @@ -259,8 +305,12 @@ "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=="], @@ -271,6 +321,10 @@ "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=="], @@ -281,8 +335,12 @@ "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=="], @@ -297,10 +355,22 @@ "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=="], @@ -309,6 +379,8 @@ "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=="], @@ -317,6 +389,8 @@ "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=="], diff --git a/src/problem5/code-challenge.db b/src/problem5/code-challenge.db new file mode 100644 index 0000000000000000000000000000000000000000..be282f0ff0c190d9e2627a7b23fc2b4f8b8a1012 GIT binary patch literal 32768 zcmeI*(Qn#D90%|-Bv4R+s#0W4)0Pua1t>{}aamUOu(8JLG6+e7*_tQjf=jJ|O|hM= zsy2yg51X{tJ?*d9{(-&hY5zjs_qu;!cg6-oAni7-s-mwIf_*;!?suQN7Xvys@6`=Q zrw^^sKy#=}t`R{XA5uyP+a=@*dj;DX+u^TZ6M~%!-mj6HKmCzP-zV{_GD-iM{v!2T z`kmC()Q{{C8w4N#0SG_<0uX=z1R(JG3yeRD$HlEJ;hF7d5Bj>J*P9ruF`y$op+3Z zp3j-qSGhtVS@?E6E^clLitohE_lXyKJzA0}JJ+7iJ>1v4f5i^#B(KNX zmtrwd*%XLjcJ;6A$9;BV*4A9d@{ZeHecoPMn(oHUXk6Ub5XK+)Df4W5_S!<_Sfxe*DY8>2aDak@^EhfIdu{y;`c0GHYO=Iug@i-C_x3`5;FF(Iew*J`F&5pis z7@f;{;bfjCwZpC~JgW(DQ51#m-03iRLu5zhuIEORp?TSEJBHI==r^B9UES`CjG<## z=5%%qM}{>roF~&+98AUO^pg3dEHB5k7bt@Xm>cS5*D!lIuMw9G^<=94yV=XHa)*3Q z|Fb!t%KSp2>7PmFdgcLp!v+BeKmY;|fB*y_009U<00Izr)dFrbLCQ7Z%`I7OTKzMf zcC0S@_eksaHM6HLE2F#gcx15;r3U4se|ue&c8hzvrDC!4Zi&74K~bVosd8(tQk0ey zHG~9tLloX(&8qwp0coFo03ao`QRk6yrgwQf>3lq?Qhty9&BrU2D@FM*;>F|L@Zyzt zywZA*UTIah+ZE|{I2>z%7qL+|V(Cr8`RN7WUOGEJ;Z){N63IL#ndh0mUiAQ@JqSPm z0uX=z1Rwwb2tWV=5P$##{C*uGl(dQl}Pa2OeDJHmHEHeiKTcm{FnaNAOHaf zKmY;|fB*y_009U<00I!Wv;s-qGu;1Q+VMrlAOHafKmY;|fB*y_009U<00LY9-~VGC zKmY;|fB*y_009U<00Izz00b_-0KWgf{9}wBLI45~fB*y_009U<00Izz00j8=|FP8f zB=bY&diwXYn=Y^eY!H9|1Rwwb2tWV=5P$##An?BljNgvMiEv~5RF10EX01JWmfil- ztoKy9Y$dyFx%=!+HuQA56_rA3;3WPL z3Qf7GR@GLsedm*Em&Ol5g`+PfC$lB|-H1031v#>OASR3WbGq4s0a=H=RsC6M2)N8E rO?_tz{m=XQ>-xJwoFs0Hx8)03m6o>3OGB-mA6EZy1?QUO4=nf_{mV+$ literal 0 HcmV?d00001 diff --git a/src/problem5/package.json b/src/problem5/package.json index 67f87f5ab0..722ee3a519 100644 --- a/src/problem5/package.json +++ b/src/problem5/package.json @@ -6,9 +6,11 @@ "scripts": { "dev": "tsx watch src/server.ts", "build": "tsc && tsc-alias", - "start": "rm -rf ./dist && tsc && tsc-alias && node dist/server.js" + "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", @@ -20,6 +22,7 @@ "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 index 740ccccc40..cb32b08470 100644 --- a/src/problem5/src/app.ts +++ b/src/problem5/src/app.ts @@ -1,12 +1,12 @@ import express from "express"; import cors from "cors"; -import helloRoutes from "@/routes/hello.routes.js"; +import taskRoutes from "@/routes/tesk.route.js"; const app = express(); app.use(cors()); app.use(express.json()); -app.use("/", helloRoutes); +app.use("/tasks", taskRoutes); export default app; \ No newline at end of file diff --git a/src/problem5/src/controllers/hello.controller.ts b/src/problem5/src/controllers/hello.controller.ts deleted file mode 100644 index 97a8ea0149..0000000000 --- a/src/problem5/src/controllers/hello.controller.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { Request, Response } from "express"; -import * as service from "@/services/hello.service.js"; - -export const sayHello = async (req: Request, res: Response) => { - const data = service.getHelloData() - res.json(data); -}; diff --git a/src/problem5/src/controllers/task.controller.ts b/src/problem5/src/controllers/task.controller.ts new file mode 100644 index 0000000000..e8e3ea1502 --- /dev/null +++ b/src/problem5/src/controllers/task.controller.ts @@ -0,0 +1,36 @@ +import * as service from "@/services/task.service.js"; +import type { Request, Response } from "express"; + +export function createTask(req: Request, res: Response) { + const result = service.createTask(req.body); + res.json(result); +} + +export function listTasks(req: Request, res: Response) { + const result = service.listTasks(req.query); + res.json(result); +} + +export function getTaskById(req: Request, res: Response) { + const task = service.getTaskById(Number(req.params.id)); + + if (!task) { + return res.status(404).json({ error: "Not found" }); + } + + res.json(task); +} + +export function updateStatus(req: Request, res: Response) { + const task = service.updateStatus( + Number(req.params.id), + req.body.status + ); + + res.json(task); +} + +export function archiveTask(req: Request, res: Response) { + const result = service.archiveTask(Number(req.params.id)); + 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..43bbaf9c16 --- /dev/null +++ b/src/problem5/src/db.ts @@ -0,0 +1,33 @@ +import { exec } from "@/db_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 3, + 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')) +); +`); \ No newline at end of file diff --git a/src/problem5/src/db_utils.ts b/src/problem5/src/db_utils.ts new file mode 100644 index 0000000000..0405a2197e --- /dev/null +++ b/src/problem5/src/db_utils.ts @@ -0,0 +1,19 @@ +import Database from "better-sqlite3"; + +const instance = new Database("code-challenge.db"); + +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); +} \ 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..2168ab6553 --- /dev/null +++ b/src/problem5/src/interfaces/task.interface.ts @@ -0,0 +1,12 @@ +export type TaskStatus = "pending" | "in_progress" | "done" | "archived"; + +export interface Task { + id: number; + title: string; + description?: string; + priority: number; + status: TaskStatus; + due_at?: string; + created_at: string; + updated_at: string; +} \ No newline at end of file diff --git a/src/problem5/src/routes/hello.routes.ts b/src/problem5/src/routes/hello.routes.ts deleted file mode 100644 index 0600784840..0000000000 --- a/src/problem5/src/routes/hello.routes.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Router } from "express"; -import { - sayHello -} from "@/controllers/hello.controller.js"; - -const router = Router(); - -router.get("/", sayHello) - -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..030de5fd82 --- /dev/null +++ b/src/problem5/src/routes/tesk.route.ts @@ -0,0 +1,21 @@ +import { Router } from "express"; +import * as controller from "@/controllers/task.controller.js"; + +const router = Router(); + +// create task +router.post("/", controller.createTask); + +// list tasks +router.get("/", controller.listTasks); + +// single task +router.get("/:id", controller.getTaskById); + +// status change +router.patch("/:id/status", controller.updateStatus); + +// archive +router.delete("/:id", controller.archiveTask); + +export default router; \ No newline at end of file diff --git a/src/problem5/src/services/hello.service.ts b/src/problem5/src/services/hello.service.ts deleted file mode 100644 index c631b34431..0000000000 --- a/src/problem5/src/services/hello.service.ts +++ /dev/null @@ -1 +0,0 @@ -export const getHelloData = () => ({"message": "Hello"}) \ 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..791635bbb1 --- /dev/null +++ b/src/problem5/src/services/task.service.ts @@ -0,0 +1,90 @@ +import { execute, query, queryOne } from "@/db_utils.js"; +import type { Task, TaskStatus } from "@/interfaces/task.interface.js"; + +export function createTask(input: { + title: string; + description?: string; + priority?: number; + due_at?: string; +}) { + const result = execute(` + INSERT INTO tasks (title, description, priority, due_at, updated_at) + VALUES (?, ?, ?, ?, datetime('now')) + `, [ + input.title, + input.description ?? null, + input.priority ?? 3, + 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 urgencyScore = task.priority * 10 + (task.due_at ? Math.max(0, 10 - daysUntil(task.due_at)) : 0); + + return { + ...task, + urgencyScore + }; +} + +function daysUntil(date: string) { + const diff = new Date(date).getTime() - Date.now(); + return Math.ceil(diff / (1000 * 60 * 60 * 24)); +} + +export function listTasks(filter?: { + status?: string; +}) { + let query_string = "SELECT * FROM tasks WHERE 1=1"; + const params: any[] = []; + + if (filter?.status) { + query_string += " AND status = ?"; + params.push(filter.status); + } + + const tasks = query(query_string, params); + + return tasks + .map((t) => ({ + ...t, + urgencyScore: + t && + t.priority * 10 + (t.due_at ? 5 : 0) + })) + .sort((a, b) => b.urgencyScore - a.urgencyScore); +} + +export function updateStatus(id: number, status: TaskStatus) { + const current = getTaskById(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 archiveTask(id: number) { + return updateStatus(id, "archived"); +} \ No newline at end of file From 8e75fced765207cd98ef4e9bc463fe475a76a3da Mon Sep 17 00:00:00 2001 From: Lin Htut Kyaw Date: Wed, 1 Jul 2026 19:37:42 +0630 Subject: [PATCH 05/13] chore: added postman collection for trying out the api --- ...challenge_Problem5.postman_collection.json | 506 ++++++++++++++++++ 1 file changed, 506 insertions(+) create mode 100644 src/problem5/code-challenge_Problem5.postman_collection.json 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..3beff3c39f --- /dev/null +++ b/src/problem5/code-challenge_Problem5.postman_collection.json @@ -0,0 +1,506 @@ +{ + "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\": \"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": "Create Task", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"title\": \"Solve code challange\",\n \"description\": \"Solve code challange - Problem5\",\n \"priority\": \"10\",\n \"due_at\": \"01-07-20026\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseURL}}/tasks", + "host": [ + "{{baseURL}}" + ], + "path": [ + "tasks" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "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": "232" + }, + { + "key": "ETag", + "value": "W/\"e8-dk8FqsnOfqH1O4i5qUpijSujNjI\"" + }, + { + "key": "Date", + "value": "Wed, 01 Jul 2026 12:47:01 GMT" + }, + { + "key": "Connection", + "value": "keep-alive" + }, + { + "key": "Keep-Alive", + "value": "timeout=5" + } + ], + "cookie": [ + { + "expires": "Invalid Date", + "domain": "", + "path": "" + } + ], + "body": "{\n \"id\": 3,\n \"title\": \"Solve code challange\",\n \"description\": \"Solve code challange - Problem5\",\n \"priority\": 10,\n \"status\": \"pending\",\n \"due_at\": \"01-07-20026\",\n \"created_at\": \"2026-07-01 12:47:01\",\n \"updated_at\": \"2026-07-01 12:47:01\",\n \"urgencyScore\": 100\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/3", + "host": [ + "{{baseURL}}" + ], + "path": [ + "tasks", + "3" + ] + } + }, + "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": "232" + }, + { + "key": "ETag", + "value": "W/\"e8-dk8FqsnOfqH1O4i5qUpijSujNjI\"" + }, + { + "key": "Date", + "value": "Wed, 01 Jul 2026 12:48:29 GMT" + }, + { + "key": "Connection", + "value": "keep-alive" + }, + { + "key": "Keep-Alive", + "value": "timeout=5" + } + ], + "cookie": [], + "body": "{\n \"id\": 3,\n \"title\": \"Solve code challange\",\n \"description\": \"Solve code challange - Problem5\",\n \"priority\": 10,\n \"status\": \"pending\",\n \"due_at\": \"01-07-20026\",\n \"created_at\": \"2026-07-01 12:47:01\",\n \"updated_at\": \"2026-07-01 12:47:01\",\n \"urgencyScore\": 100\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", + "host": [ + "{{baseURL}}" + ], + "path": [ + "tasks" + ] + } + }, + "response": [ + { + "name": "Get Tasks", + "originalRequest": { + "method": "GET", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseURL}}/tasks?status=pending", + "host": [ + "{{baseURL}}" + ], + "path": [ + "tasks" + ], + "query": [ + { + "key": "status", + "value": "pending" + } + ] + } + }, + "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": "425" + }, + { + "key": "ETag", + "value": "W/\"1a9-Jljelzjo11tfhZ2jM3eEKnoSBGQ\"" + }, + { + "key": "Date", + "value": "Wed, 01 Jul 2026 12:56:54 GMT" + }, + { + "key": "Connection", + "value": "keep-alive" + }, + { + "key": "Keep-Alive", + "value": "timeout=5" + } + ], + "cookie": [], + "body": "[\n {\n \"id\": 3,\n \"title\": \"Solve code challange\",\n \"description\": \"Solve code challange - Problem5\",\n \"priority\": 10,\n \"status\": \"pending\",\n \"due_at\": \"01-07-20026\",\n \"created_at\": \"2026-07-01 12:47:01\",\n \"updated_at\": \"2026-07-01 12:47:01\",\n \"urgencyScore\": 105\n },\n {\n \"id\": 2,\n \"title\": \"Title2\",\n \"description\": \"Desc2\",\n \"priority\": 5,\n \"status\": \"pending\",\n \"due_at\": \"02-01-20026\",\n \"created_at\": \"2026-07-01 11:59:19\",\n \"updated_at\": \"2026-07-01 11:59:19\",\n \"urgencyScore\": 55\n }\n]" + } + ] + }, + { + "name": "Archive Task", + "request": { + "auth": { + "type": "noauth" + }, + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseURL}}/tasks/1", + "host": [ + "{{baseURL}}" + ], + "path": [ + "tasks", + "1" + ] + } + }, + "response": [ + { + "name": "Archive Task", + "originalRequest": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "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": "193" + }, + { + "key": "ETag", + "value": "W/\"c1-pEOUnCcTVLx0hHV3+pRjJuLtFLc\"" + }, + { + "key": "Date", + "value": "Wed, 01 Jul 2026 12:05:03 GMT" + }, + { + "key": "Connection", + "value": "keep-alive" + }, + { + "key": "Keep-Alive", + "value": "timeout=5" + } + ], + "cookie": [], + "body": "{\n \"id\": 1,\n \"title\": \"Title1\",\n \"description\": \"Desc1\",\n \"priority\": 10,\n \"status\": \"archived\",\n \"due_at\": \"01-01-20026\",\n \"created_at\": \"2026-07-01 11:58:58\",\n \"updated_at\": \"2026-07-01 12:05:03\",\n \"urgencyScore\": 100\n}" + } + ] + }, + { + "name": "Update Task Status", + "request": { + "auth": { + "type": "noauth" + }, + "method": "PATCH", + "header": [], + "body": { + "mode": "raw", + "raw": "// \"pending\" | \"in_progress\" | \"done\" | \"archived\"\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\" | \"archived\"\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": "196" + }, + { + "key": "ETag", + "value": "W/\"c4-Voz6aI0618UfFeI+TG1QcytBl6k\"" + }, + { + "key": "Date", + "value": "Wed, 01 Jul 2026 12:03:58 GMT" + }, + { + "key": "Connection", + "value": "keep-alive" + }, + { + "key": "Keep-Alive", + "value": "timeout=5" + } + ], + "cookie": [], + "body": "{\n \"id\": 1,\n \"title\": \"Title1\",\n \"description\": \"Desc1\",\n \"priority\": 10,\n \"status\": \"in_progress\",\n \"due_at\": \"01-01-20026\",\n \"created_at\": \"2026-07-01 11:58:58\",\n \"updated_at\": \"2026-07-01 12:03:58\",\n \"urgencyScore\": 100\n}" + } + ] + } + ] +} \ No newline at end of file From daaaebe3ed5460835724051a5e76b00a37635cb8 Mon Sep 17 00:00:00 2001 From: Lin Htut Kyaw Date: Wed, 1 Jul 2026 19:57:41 +0630 Subject: [PATCH 06/13] chore: added actual delete insted of archive and added patch archive api --- src/problem5/code-challenge.db | Bin 32768 -> 32768 bytes ...challenge_Problem5.postman_collection.json | 148 +++++++++++++++--- .../src/controllers/task.controller.ts | 11 ++ src/problem5/src/routes/tesk.route.ts | 5 +- src/problem5/src/services/task.service.ts | 14 ++ 5 files changed, 155 insertions(+), 23 deletions(-) diff --git a/src/problem5/code-challenge.db b/src/problem5/code-challenge.db index be282f0ff0c190d9e2627a7b23fc2b4f8b8a1012..bceafee51b32e906f9caeb565ebc365cb38c9422 100644 GIT binary patch delta 305 zcmZo@U}|V!njp<6GEv5vQDkGnB7Pnr1}47Q4E#xav-xIk7F6ivn><^toP~cQL%?J) zc~^F0D?>{wW0TEE@^b|kIW`%vIEZlY1TpY;@(b}D;Vb3Sp@EK4aYpz)%;6j0_Bn%z(nlMX8A;sVNXCBcPbBfjLlK!O+mk)B=crN=ge- yuq%N&9i*fvJvA@6GB`P3wkQ?o6a#}=WV4K|j0~+zEWOw#Yu7o$ecVx7!2|$0bWd9V delta 96 zcmV-m0H6PWfC7Mk0+1U45Rn{10T8iZq7MwQ00R!201pxmVh)?L5g?5YlbarQ1Wo}7 z0FxFUMF%oEFf}?bGqYkJoe%*AvM>Zd5C#p101t@|5)Q-;bq*;Fh_ew8bPcnJeRu+$ Cix^@6 diff --git a/src/problem5/code-challenge_Problem5.postman_collection.json b/src/problem5/code-challenge_Problem5.postman_collection.json index 3beff3c39f..248edafe6e 100644 --- a/src/problem5/code-challenge_Problem5.postman_collection.json +++ b/src/problem5/code-challenge_Problem5.postman_collection.json @@ -309,16 +309,16 @@ ] }, { - "name": "Archive Task", + "name": "Update Task Status", "request": { "auth": { "type": "noauth" }, - "method": "DELETE", + "method": "PATCH", "header": [], "body": { "mode": "raw", - "raw": "", + "raw": "// \"pending\" | \"in_progress\" | \"done\"\n\n{\n \"status\": \"in_progress\"\n}", "options": { "raw": { "language": "json" @@ -326,25 +326,26 @@ } }, "url": { - "raw": "{{baseURL}}/tasks/1", + "raw": "{{baseURL}}/tasks/1/status", "host": [ "{{baseURL}}" ], "path": [ "tasks", - "1" + "1", + "status" ] } }, "response": [ { - "name": "Archive Task", + "name": "Update Task Status", "originalRequest": { - "method": "DELETE", + "method": "PATCH", "header": [], "body": { "mode": "raw", - "raw": "", + "raw": "// \"pending\" | \"in_progress\" | \"done\"\n\n{\n \"status\": \"in_progress\"\n}", "options": { "raw": { "language": "json" @@ -352,19 +353,20 @@ } }, "url": { - "raw": "{{baseURL}}/tasks/1", + "raw": "{{baseURL}}/tasks/1/status", "host": [ "{{baseURL}}" ], "path": [ "tasks", - "1" + "1", + "status" ] } }, "status": "OK", "code": 200, - "_postman_previewlanguage": null, + "_postman_previewlanguage": "", "header": [ { "key": "X-Powered-By", @@ -380,15 +382,15 @@ }, { "key": "Content-Length", - "value": "193" + "value": "196" }, { "key": "ETag", - "value": "W/\"c1-pEOUnCcTVLx0hHV3+pRjJuLtFLc\"" + "value": "W/\"c4-Voz6aI0618UfFeI+TG1QcytBl6k\"" }, { "key": "Date", - "value": "Wed, 01 Jul 2026 12:05:03 GMT" + "value": "Wed, 01 Jul 2026 12:03:58 GMT" }, { "key": "Connection", @@ -399,13 +401,19 @@ "value": "timeout=5" } ], - "cookie": [], - "body": "{\n \"id\": 1,\n \"title\": \"Title1\",\n \"description\": \"Desc1\",\n \"priority\": 10,\n \"status\": \"archived\",\n \"due_at\": \"01-01-20026\",\n \"created_at\": \"2026-07-01 11:58:58\",\n \"updated_at\": \"2026-07-01 12:05:03\",\n \"urgencyScore\": 100\n}" + "cookie": [ + { + "expires": "Invalid Date", + "domain": "", + "path": "" + } + ], + "body": "{\n \"id\": 1,\n \"title\": \"Title1\",\n \"description\": \"Desc1\",\n \"priority\": 10,\n \"status\": \"in_progress\",\n \"due_at\": \"01-01-20026\",\n \"created_at\": \"2026-07-01 11:58:58\",\n \"updated_at\": \"2026-07-01 12:03:58\",\n \"urgencyScore\": 100\n}" } ] }, { - "name": "Update Task Status", + "name": "Archive Task", "request": { "auth": { "type": "noauth" @@ -414,7 +422,7 @@ "header": [], "body": { "mode": "raw", - "raw": "// \"pending\" | \"in_progress\" | \"done\" | \"archived\"\n\n{\n \"status\": \"in_progress\"\n}", + "raw": "", "options": { "raw": { "language": "json" @@ -422,26 +430,26 @@ } }, "url": { - "raw": "{{baseURL}}/tasks/1/status", + "raw": "{{baseURL}}/tasks/1/archive", "host": [ "{{baseURL}}" ], "path": [ "tasks", "1", - "status" + "archive" ] } }, "response": [ { - "name": "Update Task Status", + "name": "Archive Task", "originalRequest": { "method": "PATCH", "header": [], "body": { "mode": "raw", - "raw": "// \"pending\" | \"in_progress\" | \"done\" | \"archived\"\n\n{\n \"status\": \"in_progress\"\n}", + "raw": "", "options": { "raw": { "language": "json" @@ -501,6 +509,102 @@ "body": "{\n \"id\": 1,\n \"title\": \"Title1\",\n \"description\": \"Desc1\",\n \"priority\": 10,\n \"status\": \"in_progress\",\n \"due_at\": \"01-01-20026\",\n \"created_at\": \"2026-07-01 11:58:58\",\n \"updated_at\": \"2026-07-01 12:03:58\",\n \"urgencyScore\": 100\n}" } ] + }, + { + "name": "Delete Task", + "request": { + "auth": { + "type": "noauth" + }, + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{baseURL}}/tasks/1", + "host": [ + "{{baseURL}}" + ], + "path": [ + "tasks", + "1" + ] + } + }, + "response": [ + { + "name": "Archive Task", + "originalRequest": { + "method": "DELETE", + "header": [], + "body": { + "mode": "raw", + "raw": "", + "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": "193" + }, + { + "key": "ETag", + "value": "W/\"c1-pEOUnCcTVLx0hHV3+pRjJuLtFLc\"" + }, + { + "key": "Date", + "value": "Wed, 01 Jul 2026 12:05:03 GMT" + }, + { + "key": "Connection", + "value": "keep-alive" + }, + { + "key": "Keep-Alive", + "value": "timeout=5" + } + ], + "cookie": [], + "body": "{\n \"id\": 1,\n \"title\": \"Title1\",\n \"description\": \"Desc1\",\n \"priority\": 10,\n \"status\": \"archived\",\n \"due_at\": \"01-01-20026\",\n \"created_at\": \"2026-07-01 11:58:58\",\n \"updated_at\": \"2026-07-01 12:05:03\",\n \"urgencyScore\": 100\n}" + } + ] } ] } \ No newline at end of file diff --git a/src/problem5/src/controllers/task.controller.ts b/src/problem5/src/controllers/task.controller.ts index e8e3ea1502..71e310610c 100644 --- a/src/problem5/src/controllers/task.controller.ts +++ b/src/problem5/src/controllers/task.controller.ts @@ -22,6 +22,12 @@ export function getTaskById(req: Request, res: Response) { } export function updateStatus(req: Request, res: Response) { + if (!req.body.status) { + return res.status(400).json({ error: "Missing status" }); + } + else if (!["pending", "in_progress", "done"].includes(req.body.status)) { + return res.status(400).json({ error: "Invalid status! Must be one of: pending, in_progress, done" }); + } const task = service.updateStatus( Number(req.params.id), req.body.status @@ -33,4 +39,9 @@ export function updateStatus(req: Request, res: Response) { export function archiveTask(req: Request, res: Response) { const result = service.archiveTask(Number(req.params.id)); res.json(result); +} + +export function deleteTask(req: Request, res: Response) { + const result = service.deleteTask(Number(req.params.id)); + res.json(result); } \ No newline at end of file diff --git a/src/problem5/src/routes/tesk.route.ts b/src/problem5/src/routes/tesk.route.ts index 030de5fd82..6bbf454fd7 100644 --- a/src/problem5/src/routes/tesk.route.ts +++ b/src/problem5/src/routes/tesk.route.ts @@ -16,6 +16,9 @@ router.get("/:id", controller.getTaskById); router.patch("/:id/status", controller.updateStatus); // archive -router.delete("/:id", controller.archiveTask); +router.patch("/:id/archive", controller.archiveTask); + +// delete +router.delete("/:id", controller.deleteTask); export default router; \ No newline at end of file diff --git a/src/problem5/src/services/task.service.ts b/src/problem5/src/services/task.service.ts index 791635bbb1..e682e26eda 100644 --- a/src/problem5/src/services/task.service.ts +++ b/src/problem5/src/services/task.service.ts @@ -87,4 +87,18 @@ export function updateStatus(id: number, status: TaskStatus) { export function archiveTask(id: number) { return updateStatus(id, "archived"); +} + +export function deleteTask(id: number) { + const current = getTaskById(id); + if (!current) return null; + + execute(`DELETE FROM tasks WHERE id = ?`, [id]); + + execute(` + INSERT INTO task_events (task_id, event, payload) + VALUES (?, 'TASK_DELETED', ?) + `, [id, JSON.stringify(current)]); + + return current; } \ No newline at end of file From 5232c818c31f146dcf13e50bb982f4e95b1d5029 Mon Sep 17 00:00:00 2001 From: Lin Htut Kyaw Date: Thu, 2 Jul 2026 09:51:13 +0630 Subject: [PATCH 07/13] chore: removed archive, imporved controller validations, improved strict typing --- .../src/controllers/task.controller.ts | 98 +++++++--- src/problem5/src/db.ts | 2 +- src/problem5/src/db_utils.ts | 19 -- src/problem5/src/interfaces/task.interface.ts | 25 ++- src/problem5/src/routes/tesk.route.ts | 4 +- src/problem5/src/services/task.service.ts | 185 ++++++++++++------ src/problem5/src/utils.ts | 64 ++++++ 7 files changed, 291 insertions(+), 106 deletions(-) delete mode 100644 src/problem5/src/db_utils.ts create mode 100644 src/problem5/src/utils.ts diff --git a/src/problem5/src/controllers/task.controller.ts b/src/problem5/src/controllers/task.controller.ts index 71e310610c..845e80848d 100644 --- a/src/problem5/src/controllers/task.controller.ts +++ b/src/problem5/src/controllers/task.controller.ts @@ -1,47 +1,99 @@ +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 type { Request, Response } from "express"; export function createTask(req: Request, res: Response) { - const result = service.createTask(req.body); - res.json(result); + const body = req.body as unknown; + if ( + typeof body !== "object" || + body === null || + !("title" in body) || + typeof body.title !== "string" || + body.title.trim() === "" + ) { + return res.status(400).json({ error: "title is required" }); + } + const input = body as CreateTaskInput; + const task = service.createTask(input); + return res.status(201).json(task); } export function listTasks(req: Request, res: Response) { - const result = service.listTasks(req.query); - res.json(result); + const statusRaw = getSingleQueryValue(req.query.status); + const priorityRaw = getSingleQueryValue(req.query.priority); + if (statusRaw !== undefined && !isTaskStatus(statusRaw)) { + return res.status(400).json({ error: "Invalid status" }); + } + let priority: number | undefined; + if (priorityRaw !== undefined) { + if (!/^[1-5]$/.test(priorityRaw)) { + return res.status(400).json({ error: "priority must be between 1 and 5" }); + } + priority = Number(priorityRaw); + } + const filter: ListTasksFilter = {}; + if (statusRaw !== undefined) { + filter.status = statusRaw; + } + if (priority !== undefined) { + filter.priority = priority; + } + const tasks = service.listTasks(filter); + return res.json(tasks); } export function getTaskById(req: Request, res: Response) { - const task = service.getTaskById(Number(req.params.id)); - + 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" }); } - - res.json(task); + return res.json(task); } export function updateStatus(req: Request, res: Response) { - if (!req.body.status) { - return res.status(400).json({ error: "Missing status" }); + const id = getTaskId(req); + if (id === null) { + return res.status(400).json({ error: "Invalid task id" }); } - else if (!["pending", "in_progress", "done"].includes(req.body.status)) { - return res.status(400).json({ error: "Invalid status! Must be one of: pending, in_progress, done" }); + const body = req.body as { status?: unknown }; + if (!isTaskStatus(body.status)) { + return res.status(400).json({ + error: "status must be pending, in_progress, or done", + }); } - const task = service.updateStatus( - Number(req.params.id), - req.body.status - ); - - res.json(task); + const task = service.updateStatus(id, body.status); + if (!task) { + return res.status(404).json({ error: "Not found" }); + } + return res.json(task); } -export function archiveTask(req: Request, res: Response) { - const result = service.archiveTask(Number(req.params.id)); - res.json(result); +export function updateTask(req: Request, res: Response) { + const id = getTaskId(req); + if (id === null) { + return res.status(400).json({ error: "Invalid task id" }); + } + const input = req.body as UpdateTaskInput; + const task = service.updateTask(id, input); + if (!task) { + return res.status(404).json({ error: "Not found" }); + } + return res.json(task); } export function deleteTask(req: Request, res: Response) { - const result = service.deleteTask(Number(req.params.id)); - res.json(result); + 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/db.ts b/src/problem5/src/db.ts index 43bbaf9c16..f152ef1295 100644 --- a/src/problem5/src/db.ts +++ b/src/problem5/src/db.ts @@ -1,4 +1,4 @@ -import { exec } from "@/db_utils.js"; +import { exec } from "@/utils.js"; exec(` CREATE TABLE IF NOT EXISTS tasks ( diff --git a/src/problem5/src/db_utils.ts b/src/problem5/src/db_utils.ts deleted file mode 100644 index 0405a2197e..0000000000 --- a/src/problem5/src/db_utils.ts +++ /dev/null @@ -1,19 +0,0 @@ -import Database from "better-sqlite3"; - -const instance = new Database("code-challenge.db"); - -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); -} \ No newline at end of file diff --git a/src/problem5/src/interfaces/task.interface.ts b/src/problem5/src/interfaces/task.interface.ts index 2168ab6553..811778e02e 100644 --- a/src/problem5/src/interfaces/task.interface.ts +++ b/src/problem5/src/interfaces/task.interface.ts @@ -1,4 +1,4 @@ -export type TaskStatus = "pending" | "in_progress" | "done" | "archived"; +export type TaskStatus = "pending" | "in_progress" | "done"; export interface Task { id: number; @@ -9,4 +9,25 @@ export interface Task { due_at?: string; created_at: string; updated_at: string; -} \ No newline at end of file +} + +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; +}; + +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/routes/tesk.route.ts b/src/problem5/src/routes/tesk.route.ts index 6bbf454fd7..7e91448d3a 100644 --- a/src/problem5/src/routes/tesk.route.ts +++ b/src/problem5/src/routes/tesk.route.ts @@ -15,8 +15,8 @@ router.get("/:id", controller.getTaskById); // status change router.patch("/:id/status", controller.updateStatus); -// archive -router.patch("/:id/archive", controller.archiveTask); +// update single task +router.patch("/:id", controller.updateTask); // delete router.delete("/:id", controller.deleteTask); diff --git a/src/problem5/src/services/task.service.ts b/src/problem5/src/services/task.service.ts index e682e26eda..0af3516627 100644 --- a/src/problem5/src/services/task.service.ts +++ b/src/problem5/src/services/task.service.ts @@ -1,104 +1,171 @@ -import { execute, query, queryOne } from "@/db_utils.js"; -import type { Task, TaskStatus } from "@/interfaces/task.interface.js"; - -export function createTask(input: { - title: string; - description?: string; - priority?: number; - due_at?: string; -}) { - const result = execute(` +import { calculateUrgency, execute, query, queryOne } from "@/utils.js"; +import type { CreateTaskInput, ListTasksFilter, SqlValue, Task, TaskStatus, UpdateTaskInput } from "@/interfaces/task.interface.js"; + +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, - input.priority ?? 3, - input.due_at ?? null]); + `, + [ + input.title, + input.description ?? null, + input.priority ?? 3, + input.due_at ?? null, + ] + ); const id = Number(result.lastInsertRowid); - execute(` + execute( + ` INSERT INTO task_events (task_id, event, payload) VALUES (?, 'TASK_CREATED', ?) - `, [id, JSON.stringify(input)]); + `, + [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 task = queryOne( + "SELECT * FROM tasks WHERE id = ?", + [id] + ); - const urgencyScore = task.priority * 10 + (task.due_at ? Math.max(0, 10 - daysUntil(task.due_at)) : 0); + if (!task) { + return null; + } return { ...task, - urgencyScore + urgencyScore: calculateUrgency(task), }; } -function daysUntil(date: string) { - const diff = new Date(date).getTime() - Date.now(); - return Math.ceil(diff / (1000 * 60 * 60 * 24)); -} - -export function listTasks(filter?: { - status?: string; -}) { - let query_string = "SELECT * FROM tasks WHERE 1=1"; - const params: any[] = []; +export function listTasks(filter: ListTasksFilter = {}) { + let sql = "SELECT * FROM tasks WHERE 1 = 1"; + const params: SqlValue[] = []; - if (filter?.status) { - query_string += " AND status = ?"; + if (filter.status !== undefined) { + sql += " AND status = ?"; params.push(filter.status); } - const tasks = query(query_string, params); + if (filter.priority !== undefined) { + sql += " AND priority = ?"; + params.push(filter.priority); + } - return tasks - .map((t) => ({ - ...t, - urgencyScore: - t && - t.priority * 10 + (t.due_at ? 5 : 0) + return query(sql, params) + .map((task) => ({ + ...task, + urgencyScore: calculateUrgency(task), })) .sort((a, b) => b.urgencyScore - a.urgencyScore); } export function updateStatus(id: number, status: TaskStatus) { const current = getTaskById(id); - if (!current) return null; - execute(` - UPDATE tasks - SET status = ?, updated_at = datetime('now') - WHERE id = ? - `, [status, id]); + if (!current) { + return null; + } - execute(` - INSERT INTO task_events (task_id, event, payload) - VALUES (?, 'STATUS_CHANGED', ?) - `, [id, JSON.stringify({ from: current.status, to: status })]); + 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 archiveTask(id: number) { - return updateStatus(id, "archived"); +export function updateTask(id: number, input: UpdateTaskInput) { + const current = getTaskById(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(input.priority); + } + + if (input.due_at !== undefined) { + updates.push("due_at = ?"); + params.push(input.due_at); + } + + if (updates.length === 0) { + return current; + } + + 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 = getTaskById(id); - if (!current) return null; - execute(`DELETE 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(` - 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/utils.ts b/src/problem5/src/utils.ts new file mode 100644 index 0000000000..78aa7801ae --- /dev/null +++ b/src/problem5/src/utils.ts @@ -0,0 +1,64 @@ +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"); + +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; +} + + +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; +} From 85cf134ad597ab1452705299067aa42de9096946 Mon Sep 17 00:00:00 2001 From: Lin Htut Kyaw Date: Thu, 2 Jul 2026 10:02:15 +0630 Subject: [PATCH 08/13] chore: added task_event apis --- src/problem5/src/app.ts | 2 + .../src/controllers/task_event.controller.ts | 60 +++++++++++++++++++ .../src/interfaces/task_event.interface.ts | 12 ++++ src/problem5/src/routes/task_event.route.ts | 15 +++++ src/problem5/src/routes/tesk.route.ts | 2 +- .../src/services/task_event.service.ts | 35 +++++++++++ 6 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 src/problem5/src/controllers/task_event.controller.ts create mode 100644 src/problem5/src/interfaces/task_event.interface.ts create mode 100644 src/problem5/src/routes/task_event.route.ts create mode 100644 src/problem5/src/services/task_event.service.ts diff --git a/src/problem5/src/app.ts b/src/problem5/src/app.ts index cb32b08470..4ee6325441 100644 --- a/src/problem5/src/app.ts +++ b/src/problem5/src/app.ts @@ -1,12 +1,14 @@ import express from "express"; import cors from "cors"; import taskRoutes from "@/routes/tesk.route.js"; +import taskEventRoutes from "@/routes/task_even.route.js" const app = express(); app.use(cors()); app.use(express.json()); +app.use("/tasks", taskRoutes); app.use("/tasks", taskRoutes); export default app; \ 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/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..97cc4242a3 --- /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("/", controller.listTaskEvents); + +// GET /events/:id +router.get("/:id", controller.getTaskEventById); + +// GET /tasks/:taskId/events +router.get("/task/:taskId", controller.getTaskEventsByTaskId); + +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 index 7e91448d3a..4d171b7dda 100644 --- a/src/problem5/src/routes/tesk.route.ts +++ b/src/problem5/src/routes/tesk.route.ts @@ -2,7 +2,7 @@ import { Router } from "express"; import * as controller from "@/controllers/task.controller.js"; const router = Router(); - +// BASE URL /tasks // create task router.post("/", controller.createTask); 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 From 0e13a16a1f73d72b27480cbfebe27c34eef54fac Mon Sep 17 00:00:00 2001 From: Lin Htut Kyaw Date: Thu, 2 Jul 2026 13:33:12 +0630 Subject: [PATCH 09/13] chore: added error handling, better structure and improvements --- src/problem5/code-challenge.db | Bin 32768 -> 40960 bytes src/problem5/src/app.ts | 10 +- .../src/controllers/task.controller.ts | 141 ++++++++++-------- .../src/controllers/task_tag.controller.ts | 55 +++++++ src/problem5/src/db.ts | 4 +- src/problem5/src/errors/http.error.ts | 33 ++++ src/problem5/src/interfaces/tag.interface.ts | 4 + src/problem5/src/interfaces/task.interface.ts | 1 + src/problem5/src/routes/task_event.route.ts | 6 +- src/problem5/src/routes/task_tag.route.ts | 15 ++ src/problem5/src/routes/tesk.route.ts | 12 +- src/problem5/src/services/tag.service.ts | 33 ++++ src/problem5/src/services/task.service.ts | 118 +++++++++++---- src/problem5/src/services/task_tag.service.ts | 92 ++++++++++++ src/problem5/src/utils.ts | 5 +- .../src/validations/task.validation.ts | 20 +++ 16 files changed, 443 insertions(+), 106 deletions(-) create mode 100644 src/problem5/src/controllers/task_tag.controller.ts create mode 100644 src/problem5/src/errors/http.error.ts create mode 100644 src/problem5/src/interfaces/tag.interface.ts create mode 100644 src/problem5/src/routes/task_tag.route.ts create mode 100644 src/problem5/src/services/tag.service.ts create mode 100644 src/problem5/src/services/task_tag.service.ts create mode 100644 src/problem5/src/validations/task.validation.ts diff --git a/src/problem5/code-challenge.db b/src/problem5/code-challenge.db index bceafee51b32e906f9caeb565ebc365cb38c9422..735b245511e92653bb925fb0f2d00bdfe8e7ffab 100644 GIT binary patch delta 1389 zcma)*OK1~87{_OJHjhc09YZNwsM0l3X`qEown=b7rH_cEC5kQh010igwA)7#l1*rl zvdO`FOV9P>!HWmU)q~(g5R~FUL3`+-MOvy9d>r)P%;eFujp9OPCttqr`~PRYUy4Pc z_(E9i@_NA0<6P_2J0bwLi~(s?y5ilCysdlQxmI7x)0VMkr8gcsft_-=3oe_fq_Jcs zmlD=wZk{ePIkqIsWc|Bm=vHiCBBqSQhhuk?W~s`JxKdB=sgwt%+)g>#1y`C1Cl&%QAd;Y5lph;a7&P$A(6go=nzEXH9Ea8H`Qbj>Y0i zP#K61D?v4|ewRG)(#t~99w3j2F71_l$|uQ=BMjtou=Bj84d!Q*AX}0~ZrYPEQb{v4 zqlWv`XkSPTMc9Y!)sUh_^l(HE1)GFs1q`$(X8lGMGYiI|k+PJV+4L0MDv^h-A9U&F2ukQ;b=V2{H*9fD<9mmNW`30fBatx^_bZm+?2PbBd6aI7Ljuf+%8)EA$iqv84m4Q1+dfmE3vc00vrS+R&WqG0QLFv>O;!qKA-w8bK-5){(eI9V2N(#9IEOG8K+%+{Qk3of-rY-*cq zQME~QJ8aUfJN2*F{()U~+P~2IuKO4Ed&YnvB+Ztp%HfMbVn4t4^Y4A$;{ZhORUed1 z&k!El&av(ZGIxXHdF~TI;5hCUJ?H2-c+AiNdk!A_;NXJ4x41jM{*_8UjQU)PNj!}92S zcsO_WP%UYypp`x?t3v34u#yx6vnlLUH1(c(C>$K_?3E6WgkAMWDAlz6oeH(wQ!845 z>iO+Pm06L0CqYvmYC>h7p0#qB6?gR0wyih)3+%k%7`kUP>-45(tgf)FepafLHDRSm z*FE#tSXs90Z{?{uh>ECY@b-M-=is+(zW_{?%}?!1T6~G zNmh^UFU4Y6b&=;xt7&}eK5f$*v%22(Z2!FO*Jt;uOT)vs6^&;X7Wm%BLCP%KmOC?% zXprM5nType{n(gLt5tR$)YN2tc_xSccnEWAfBV+BEQGaMI+ilPE+7ta|GpT8~ z4ae+wrfm&pSI04J$MjBzvv@EShto@DO(`$0>rPM(Cg5_%u$rdTTJ~?!rJ?Rm)&Dnp zS*vb`&FTL(=Tn)RTqOO1%REZIpg(v(00Izz00bZa0SG_<0uX=z1m3hjH=5u!cKNrL zWVvd$PYj`9H|f_Cz1`NWmNBhNSQ8E$n?95}-be;-uSIE1+*~V)V)6YVJ=uvU2~ts6 z-&90tN>S!F?%=H~{|?<&W1k2}+w=i|l+>NZ6Z6DqvUt+jc#^X50sVyI$%?e4$m@HN z1V`~$H~e@-7OyxvNUwNaxGicP4o7!jMKlWg;c!8EgK(G5E=V|)`J0PmUUHe2nSb8& z0ONiTfB*y_009U<00Izz00bZa0SH`KAi+noy#H?yQHqsFu)|Cwy5yGy|FJWdVz;?* z<$ECj1Rwwb2tWV=5P$##AOHafKmY>QSRl!A!~6ehJVHnf0uX=z1Rwwb2tWV=5P$## zAaHd7eE)xSLqvc81Rwwb2tWV=5P$##AOHafTz3I{|9{;_2>C$(0uX=z1Rwwb2tWV= z5P$##t}ej7|DT&b)UEs)zs~0IkTBlq(aW$Wi6-V`fID_3c1k!YE1>5|sxsOqZK6uBVmqtiv}lLA(`v(Z43>iU zT{x7i6eVSS>wY}OCGu6ds+F`_wSM>WQso}&>0`$}9?#XY!{-yR{8i$6@5bUBf4lcg zj{0o32zY~48v2eF`XBcV*7fi5>{ZiS?Z0Zy4F9EHE>ngz*skEr IZQ0A=U!!WFTL1t6 diff --git a/src/problem5/src/app.ts b/src/problem5/src/app.ts index 4ee6325441..f23a3430b1 100644 --- a/src/problem5/src/app.ts +++ b/src/problem5/src/app.ts @@ -1,14 +1,18 @@ import express from "express"; import cors from "cors"; import taskRoutes from "@/routes/tesk.route.js"; -import taskEventRoutes from "@/routes/task_even.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("/tasks", taskRoutes); -app.use("/tasks", taskRoutes); +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 index 845e80848d..8657f890e8 100644 --- a/src/problem5/src/controllers/task.controller.ts +++ b/src/problem5/src/controllers/task.controller.ts @@ -1,99 +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 unknown; - if ( - typeof body !== "object" || - body === null || - !("title" in body) || - typeof body.title !== "string" || - body.title.trim() === "" - ) { + const body = req.body as Partial; + + if (!body || typeof body.title !== "string" || body.title.trim() === "") { return res.status(400).json({ error: "title is required" }); } - const input = body as CreateTaskInput; - const task = service.createTask(input); - return res.status(201).json(task); + + 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); - if (statusRaw !== undefined && !isTaskStatus(statusRaw)) { - return res.status(400).json({ error: "Invalid status" }); + 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; - if (priorityRaw !== undefined) { - if (!/^[1-5]$/.test(priorityRaw)) { - return res.status(400).json({ error: "priority must be between 1 and 5" }); + + try { + if (priorityRaw !== undefined) { + priority = validatePriority(Number(priorityRaw)); } - priority = Number(priorityRaw); + } catch (e) { + return res.status(400).json({ error: (e as Error).message }); } - const filter: ListTasksFilter = {}; - if (statusRaw !== undefined) { - filter.status = statusRaw; - } - if (priority !== undefined) { - filter.priority = priority; - } - const tasks = service.listTasks(filter); - return res.json(tasks); + + 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" }); - } + 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" }); - } + 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" }); - } - const body = req.body as { status?: unknown }; - if (!isTaskStatus(body.status)) { - return res.status(400).json({ - error: "status must be pending, in_progress, or done", - }); - } - const task = service.updateStatus(id, body.status); - if (!task) { - return res.status(404).json({ error: "Not found" }); + 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 }); } - return res.json(task); } export function updateTask(req: Request, res: Response) { const id = getTaskId(req); - if (id === null) { - return res.status(400).json({ error: "Invalid task id" }); - } - const input = req.body as UpdateTaskInput; - const task = service.updateTask(id, input); - if (!task) { - return res.status(404).json({ error: "Not found" }); + 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 }); } - return res.json(task); } export function deleteTask(req: Request, res: Response) { const id = getTaskId(req); - if (id === null) { - return res.status(400).json({ error: "Invalid task id" }); - } + 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" }); - } + 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_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 index f152ef1295..383fd8e809 100644 --- a/src/problem5/src/db.ts +++ b/src/problem5/src/db.ts @@ -5,7 +5,7 @@ CREATE TABLE IF NOT EXISTS tasks ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, description TEXT, - priority INTEGER NOT NULL DEFAULT 3, + 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')), @@ -30,4 +30,6 @@ CREATE TABLE IF NOT EXISTS task_events ( 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 index 811778e02e..f43b717042 100644 --- a/src/problem5/src/interfaces/task.interface.ts +++ b/src/problem5/src/interfaces/task.interface.ts @@ -23,6 +23,7 @@ export interface CreateTaskInput { export interface ListTasksFilter { status?: TaskStatus; priority?: number; + tags?: string[]; }; export interface UpdateTaskInput { diff --git a/src/problem5/src/routes/task_event.route.ts b/src/problem5/src/routes/task_event.route.ts index 97cc4242a3..b351396bf5 100644 --- a/src/problem5/src/routes/task_event.route.ts +++ b/src/problem5/src/routes/task_event.route.ts @@ -4,12 +4,12 @@ import * as controller from "@/controllers/task_event.controller.js"; const router = Router(); // GET /events -router.get("/", controller.listTaskEvents); +router.get("/events", controller.listTaskEvents); // GET /events/:id -router.get("/:id", controller.getTaskEventById); +router.get("/events/:id", controller.getTaskEventById); // GET /tasks/:taskId/events -router.get("/task/:taskId", controller.getTaskEventsByTaskId); +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 index 4d171b7dda..0c6aecc869 100644 --- a/src/problem5/src/routes/tesk.route.ts +++ b/src/problem5/src/routes/tesk.route.ts @@ -4,21 +4,21 @@ import * as controller from "@/controllers/task.controller.js"; const router = Router(); // BASE URL /tasks // create task -router.post("/", controller.createTask); +router.post("/tasks", controller.createTask); // list tasks -router.get("/", controller.listTasks); +router.get("/tasks", controller.listTasks); // single task -router.get("/:id", controller.getTaskById); +router.get("/tasks/:id", controller.getTaskById); // status change -router.patch("/:id/status", controller.updateStatus); +router.patch("/tasks/:id/status", controller.updateStatus); // update single task -router.patch("/:id", controller.updateTask); +router.patch("/tasks/:id", controller.updateTask); // delete -router.delete("/:id", controller.deleteTask); +router.delete("/tasks/:id", controller.deleteTask); export default router; \ 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 index 0af3516627..5ecfe7437b 100644 --- a/src/problem5/src/services/task.service.ts +++ b/src/problem5/src/services/task.service.ts @@ -1,5 +1,40 @@ import { calculateUrgency, execute, query, queryOne } from "@/utils.js"; -import type { CreateTaskInput, ListTasksFilter, SqlValue, Task, TaskStatus, UpdateTaskInput } from "@/interfaces/task.interface.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( @@ -10,8 +45,8 @@ export function createTask(input: CreateTaskInput) { [ input.title, input.description ?? null, - input.priority ?? 3, - input.due_at ?? null, + normalizePriority(input.priority), + input.due_at ?? null ] ); @@ -34,44 +69,65 @@ export function getTaskById(id: number) { [id] ); - if (!task) { - return null; - } + if (!task) return null; + + const tagsMap = getTaskTagsBulk([id]); return { ...task, urgencyScore: calculateUrgency(task), + tags: tagsMap.get(id) ?? [] }; } export function listTasks(filter: ListTasksFilter = {}) { - let sql = "SELECT * FROM tasks WHERE 1 = 1"; const params: SqlValue[] = []; - if (filter.status !== undefined) { - sql += " AND status = ?"; + 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 priority = ?"; + sql += ` AND t.priority = ?`; params.push(filter.priority); } - return query(sql, params) - .map((task) => ({ - ...task, - urgencyScore: calculateUrgency(task), - })) + 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 = getTaskById(id); + const current = queryOne( + "SELECT * FROM tasks WHERE id = ?", + [id] + ); - if (!current) { - return null; - } + if (!current) return null; execute( ` @@ -94,11 +150,12 @@ export function updateStatus(id: number, status: TaskStatus) { } export function updateTask(id: number, input: UpdateTaskInput) { - const current = getTaskById(id); + const current = queryOne( + "SELECT * FROM tasks WHERE id = ?", + [id] + ); - if (!current) { - return null; - } + if (!current) return null; const updates: string[] = []; const params: SqlValue[] = []; @@ -115,7 +172,7 @@ export function updateTask(id: number, input: UpdateTaskInput) { if (input.priority !== undefined) { updates.push("priority = ?"); - params.push(input.priority); + params.push(normalizePriority(input.priority)); } if (input.due_at !== undefined) { @@ -123,9 +180,7 @@ export function updateTask(id: number, input: UpdateTaskInput) { params.push(input.due_at); } - if (updates.length === 0) { - return current; - } + if (!updates.length) return getTaskById(id); updates.push("updated_at = datetime('now')"); params.push(id); @@ -151,11 +206,12 @@ export function updateTask(id: number, input: UpdateTaskInput) { } export function deleteTask(id: number) { - const current = getTaskById(id); + const current = queryOne( + "SELECT * FROM tasks WHERE id = ?", + [id] + ); - if (!current) { - return null; - } + if (!current) return null; execute( ` 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 index 78aa7801ae..069409fd22 100644 --- a/src/problem5/src/utils.ts +++ b/src/problem5/src/utils.ts @@ -34,8 +34,7 @@ export function calculateUrgency(task: Task) { return task.priority * 10 + dueWeight; } - -const taskStatuses = ["pending", "in_progress", "done"] as const; +export const taskStatuses = ["pending", "in_progress", "done"] as const; export function isTaskStatus(value: unknown): value is TaskStatus { return ( @@ -53,7 +52,7 @@ export function getTaskId(req: Request): number | null { if (typeof rawId !== "string") { return null; } - if (isValidId(rawId)) { + if (!isValidId(rawId)) { return null; } return Number(rawId); 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 From 07cd03478f3ee2c96752a3f88b6d030b511d79b0 Mon Sep 17 00:00:00 2001 From: Lin Htut Kyaw Date: Thu, 2 Jul 2026 14:04:20 +0630 Subject: [PATCH 10/13] chore: added readme, updated postman --- src/problem5/code-challenge.db | Bin 40960 -> 40960 bytes ...challenge_Problem5.postman_collection.json | 354 +++++++++++++++--- src/problem5/src/utils.ts | 2 + 3 files changed, 296 insertions(+), 60 deletions(-) diff --git a/src/problem5/code-challenge.db b/src/problem5/code-challenge.db index 735b245511e92653bb925fb0f2d00bdfe8e7ffab..10987355f808eb881ba39a5a9b87b6c9529dcca7 100644 GIT binary patch delta 1000 zcmZ`%O=#0#82-K{ZJN>L-54^q>DpzCIcXtHY%_Bibt?*vF<6TSg;@X3>CIf#Cdhq+Qmi?fIeEELf=Y3wD=UG|vt*rT; z4B_z8)6+Qo>c(>b;VWVU6$vV6!m6;^=E1By z#Yn|F8bPa|m&(O_*|cp*bw7p2uc=yG))Mj@%cK4?c&O8$gXBwQ!LSN>DG`&BF8r-tZe1A#}a*GA#EbSspX}*vZ+qZFoSmAtz>6%MhmfnWoE!DrYb&mSSlF@2c4 zvh;75RVK)s!dAg@d_kt4UtB7d&2IRO{Ln=31HQo**n%cB3HBXu{s4n9%eq_o-~i$F z^Icx)I)d+Hf)<%!9o9+dTL^M2rlkS5RU97jYU#9GD2rfO*cGY*>V6$+hO)$ysZg}; z<2b~R)zeBQwQxnx&d;PW>6!bYV>x$BQ4^`e9i!Z5wE?~3k{A_BWvf`WoO_}+85NC+ zsplMmpsK5$3F3p}ntLABvjGm_v4@tzc#JdY*|f)K8KR~{-N&_a%ZH8QQdG2^oKvw$ z=eEm)`H+k_-X@ERi)E9<$v7C}D0NaxBsDosfbEF=zl4swbTm4uro?Cc0mM&SRTeU- zOm;z^zMPu7L`Aw)F5W#hms2FiBk?_{0L`M}uvCEhxWc+CwLvTsStrgO#P*n|7f#4W zQ#{$I)*j(L_(O8CiAcKkiS;|M37f?H_mJ?@m;~tR^V5}Ky*rIQ-!-%nu+j>y3L;J}3hv_UQi}##gceN5sNG#G`2+?)zfbMBejv{;g zRT@VqAe>g8j1wjxdZ_Iz@1kJnFu&nldZ-8?{Kkbx95?i>?gnn#_fS8h6zGd4lt%W! z3-ykzFx{_NNJF4+8qzdBFK#YiiJiLv7IB5O+AW_K(Gh3O&Xt?}F)0E1p-+0JH%c_b z0m7ud>L5cQEUk$%&4+S}Gr#R;3gb?-WEet-|7HQujhx6pHEYT!xV~k!235#km1jmN ojq^l>EfX2zW;W5#I~uAu$GM%RW~$zJUawHm`kc!2s8LP*0c$~8a{vGU diff --git a/src/problem5/code-challenge_Problem5.postman_collection.json b/src/problem5/code-challenge_Problem5.postman_collection.json index 248edafe6e..5183ca56cd 100644 --- a/src/problem5/code-challenge_Problem5.postman_collection.json +++ b/src/problem5/code-challenge_Problem5.postman_collection.json @@ -16,7 +16,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\n \"title\": \"Title2\",\n \"description\": \"Desc2\",\n \"priority\": \"5\",\n \"due_at\": \"02-01-20026\"\n}", + "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" @@ -41,7 +41,7 @@ "header": [], "body": { "mode": "raw", - "raw": "{\n \"title\": \"Solve code challange\",\n \"description\": \"Solve code challange - Problem5\",\n \"priority\": \"10\",\n \"due_at\": \"01-07-20026\"\n}", + "raw": "{\n \"title\": \"Bug 2\",\n \"description\": \"Bug desc\",\n \"priority\": 9,\n \"due_at\": \"05-06-2026\"\n}", "options": { "raw": { "language": "json" @@ -58,9 +58,9 @@ ] } }, - "status": "OK", - "code": 200, - "_postman_previewlanguage": "json", + "status": "Created", + "code": 201, + "_postman_previewlanguage": null, "header": [ { "key": "X-Powered-By", @@ -76,15 +76,15 @@ }, { "key": "Content-Length", - "value": "232" + "value": "202" }, { "key": "ETag", - "value": "W/\"e8-dk8FqsnOfqH1O4i5qUpijSujNjI\"" + "value": "W/\"ca-gx4RjGXkmU5ysy2EpgdWE/Poe4A\"" }, { "key": "Date", - "value": "Wed, 01 Jul 2026 12:47:01 GMT" + "value": "Thu, 02 Jul 2026 07:04:28 GMT" }, { "key": "Connection", @@ -95,14 +95,104 @@ "value": "timeout=5" } ], - "cookie": [ + "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" + }, { - "expires": "Invalid Date", - "domain": "", - "path": "" + "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" } ], - "body": "{\n \"id\": 3,\n \"title\": \"Solve code challange\",\n \"description\": \"Solve code challange - Problem5\",\n \"priority\": 10,\n \"status\": \"pending\",\n \"due_at\": \"01-07-20026\",\n \"created_at\": \"2026-07-01 12:47:01\",\n \"updated_at\": \"2026-07-01 12:47:01\",\n \"urgencyScore\": 100\n}" + "cookie": [], + "body": "[\n {\n \"id\": 4,\n \"name\": \"bug\"\n }\n]" } ] }, @@ -153,13 +243,13 @@ } }, "url": { - "raw": "{{baseURL}}/tasks/3", + "raw": "{{baseURL}}/tasks/4", "host": [ "{{baseURL}}" ], "path": [ "tasks", - "3" + "4" ] } }, @@ -181,15 +271,15 @@ }, { "key": "Content-Length", - "value": "232" + "value": "223" }, { "key": "ETag", - "value": "W/\"e8-dk8FqsnOfqH1O4i5qUpijSujNjI\"" + "value": "W/\"df-AvDffL2gXyuuIlf7LHMqjSywEkE\"" }, { "key": "Date", - "value": "Wed, 01 Jul 2026 12:48:29 GMT" + "value": "Thu, 02 Jul 2026 07:05:38 GMT" }, { "key": "Connection", @@ -201,7 +291,7 @@ } ], "cookie": [], - "body": "{\n \"id\": 3,\n \"title\": \"Solve code challange\",\n \"description\": \"Solve code challange - Problem5\",\n \"priority\": 10,\n \"status\": \"pending\",\n \"due_at\": \"01-07-20026\",\n \"created_at\": \"2026-07-01 12:47:01\",\n \"updated_at\": \"2026-07-01 12:47:01\",\n \"urgencyScore\": 100\n}" + "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}" } ] }, @@ -226,12 +316,22 @@ } }, "url": { - "raw": "{{baseURL}}/tasks", + "raw": "{{baseURL}}/tasks?tags=Tag1&tags=Tag3", "host": [ "{{baseURL}}" ], "path": [ "tasks" + ], + "query": [ + { + "key": "tags", + "value": "Tag1" + }, + { + "key": "tags", + "value": "Tag3" + } ] } }, @@ -251,7 +351,7 @@ } }, "url": { - "raw": "{{baseURL}}/tasks?status=pending", + "raw": "{{baseURL}}/tasks?tags=bug,chore", "host": [ "{{baseURL}}" ], @@ -260,8 +360,8 @@ ], "query": [ { - "key": "status", - "value": "pending" + "key": "tags", + "value": "bug,chore" } ] } @@ -284,15 +384,15 @@ }, { "key": "Content-Length", - "value": "425" + "value": "761" }, { "key": "ETag", - "value": "W/\"1a9-Jljelzjo11tfhZ2jM3eEKnoSBGQ\"" + "value": "W/\"2f9-O4WXUriEJKnF2sXXrbZFpJzwJbE\"" }, { "key": "Date", - "value": "Wed, 01 Jul 2026 12:56:54 GMT" + "value": "Thu, 02 Jul 2026 07:06:18 GMT" }, { "key": "Connection", @@ -304,7 +404,7 @@ } ], "cookie": [], - "body": "[\n {\n \"id\": 3,\n \"title\": \"Solve code challange\",\n \"description\": \"Solve code challange - Problem5\",\n \"priority\": 10,\n \"status\": \"pending\",\n \"due_at\": \"01-07-20026\",\n \"created_at\": \"2026-07-01 12:47:01\",\n \"updated_at\": \"2026-07-01 12:47:01\",\n \"urgencyScore\": 105\n },\n {\n \"id\": 2,\n \"title\": \"Title2\",\n \"description\": \"Desc2\",\n \"priority\": 5,\n \"status\": \"pending\",\n \"due_at\": \"02-01-20026\",\n \"created_at\": \"2026-07-01 11:59:19\",\n \"updated_at\": \"2026-07-01 11:59:19\",\n \"urgencyScore\": 55\n }\n]" + "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]" } ] }, @@ -366,7 +466,7 @@ }, "status": "OK", "code": 200, - "_postman_previewlanguage": "", + "_postman_previewlanguage": null, "header": [ { "key": "X-Powered-By", @@ -382,15 +482,15 @@ }, { "key": "Content-Length", - "value": "196" + "value": "264" }, { "key": "ETag", - "value": "W/\"c4-Voz6aI0618UfFeI+TG1QcytBl6k\"" + "value": "W/\"108-S085vqV6D/0k5Lm3IoZPIL/m9ic\"" }, { "key": "Date", - "value": "Wed, 01 Jul 2026 12:03:58 GMT" + "value": "Thu, 02 Jul 2026 07:07:03 GMT" }, { "key": "Connection", @@ -401,19 +501,13 @@ "value": "timeout=5" } ], - "cookie": [ - { - "expires": "Invalid Date", - "domain": "", - "path": "" - } - ], - "body": "{\n \"id\": 1,\n \"title\": \"Title1\",\n \"description\": \"Desc1\",\n \"priority\": 10,\n \"status\": \"in_progress\",\n \"due_at\": \"01-01-20026\",\n \"created_at\": \"2026-07-01 11:58:58\",\n \"updated_at\": \"2026-07-01 12:03:58\",\n \"urgencyScore\": 100\n}" + "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": "Archive Task", + "name": "Patch Task", "request": { "auth": { "type": "noauth" @@ -422,7 +516,7 @@ "header": [], "body": { "mode": "raw", - "raw": "", + "raw": "// \"pending\" | \"in_progress\" | \"done\"\n\n{\n \"status\": \"in_progress\"\n}", "options": { "raw": { "language": "json" @@ -430,26 +524,26 @@ } }, "url": { - "raw": "{{baseURL}}/tasks/1/archive", + "raw": "{{baseURL}}/tasks/1/status", "host": [ "{{baseURL}}" ], "path": [ "tasks", "1", - "archive" + "status" ] } }, "response": [ { - "name": "Archive Task", + "name": "Patch Task", "originalRequest": { "method": "PATCH", "header": [], "body": { "mode": "raw", - "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" @@ -457,14 +551,13 @@ } }, "url": { - "raw": "{{baseURL}}/tasks/1/status", + "raw": "{{baseURL}}/tasks/1", "host": [ "{{baseURL}}" ], "path": [ "tasks", - "1", - "status" + "1" ] } }, @@ -486,15 +579,15 @@ }, { "key": "Content-Length", - "value": "196" + "value": "242" }, { "key": "ETag", - "value": "W/\"c4-Voz6aI0618UfFeI+TG1QcytBl6k\"" + "value": "W/\"f2-PPfHKZEC1TYY/OoWvPWCYXMfD+k\"" }, { "key": "Date", - "value": "Wed, 01 Jul 2026 12:03:58 GMT" + "value": "Thu, 02 Jul 2026 07:12:52 GMT" }, { "key": "Connection", @@ -506,7 +599,7 @@ } ], "cookie": [], - "body": "{\n \"id\": 1,\n \"title\": \"Title1\",\n \"description\": \"Desc1\",\n \"priority\": 10,\n \"status\": \"in_progress\",\n \"due_at\": \"01-01-20026\",\n \"created_at\": \"2026-07-01 11:58:58\",\n \"updated_at\": \"2026-07-01 12:03:58\",\n \"urgencyScore\": 100\n}" + "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}" } ] }, @@ -528,19 +621,158 @@ } }, "url": { - "raw": "{{baseURL}}/tasks/1", + "raw": "{{baseURL}}/tasks/3", "host": [ "{{baseURL}}" ], "path": [ "tasks", - "1" + "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": "Archive Task", + "name": "Remove Task Tag", "originalRequest": { "method": "DELETE", "header": [], @@ -554,12 +786,14 @@ } }, "url": { - "raw": "{{baseURL}}/tasks/1", + "raw": "{{baseURL}}/tasks/1/tags/1", "host": [ "{{baseURL}}" ], "path": [ "tasks", + "1", + "tags", "1" ] } @@ -582,15 +816,15 @@ }, { "key": "Content-Length", - "value": "193" + "value": "24" }, { "key": "ETag", - "value": "W/\"c1-pEOUnCcTVLx0hHV3+pRjJuLtFLc\"" + "value": "W/\"18-iomQfn6l/AlQ4fQB5fY7bKB9btw\"" }, { "key": "Date", - "value": "Wed, 01 Jul 2026 12:05:03 GMT" + "value": "Thu, 02 Jul 2026 07:09:23 GMT" }, { "key": "Connection", @@ -602,7 +836,7 @@ } ], "cookie": [], - "body": "{\n \"id\": 1,\n \"title\": \"Title1\",\n \"description\": \"Desc1\",\n \"priority\": 10,\n \"status\": \"archived\",\n \"due_at\": \"01-01-20026\",\n \"created_at\": \"2026-07-01 11:58:58\",\n \"updated_at\": \"2026-07-01 12:05:03\",\n \"urgencyScore\": 100\n}" + "body": "[\n {\n \"id\": 2,\n \"name\": \"init\"\n }\n]" } ] } diff --git a/src/problem5/src/utils.ts b/src/problem5/src/utils.ts index 069409fd22..c3ef7b074a 100644 --- a/src/problem5/src/utils.ts +++ b/src/problem5/src/utils.ts @@ -4,6 +4,8 @@ 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[]; } From 11245a94e2664c2d0028d57bbab3c3a90f137870 Mon Sep 17 00:00:00 2001 From: Lin Htut Kyaw Date: Thu, 2 Jul 2026 14:05:29 +0630 Subject: [PATCH 11/13] bugfix: updated full readme,md --- src/problem5/README.md | 274 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 268 insertions(+), 6 deletions(-) diff --git a/src/problem5/README.md b/src/problem5/README.md index 2c25a4dd75..574391bb08 100644 --- a/src/problem5/README.md +++ b/src/problem5/README.md @@ -1,15 +1,277 @@ -# problem5 +# Task Management API -To install dependencies: +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 -bun install +npm run dev ``` -To run: +Build and start the production version: ```bash -bun run index.ts +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 ``` -This project was created using `bun init` in bun v1.3.14. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. +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. From 07d9d5c8312ae461327ad6bc4fac064e039b5d8d Mon Sep 17 00:00:00 2001 From: Lin Htut Kyaw Date: Thu, 2 Jul 2026 14:31:56 +0630 Subject: [PATCH 12/13] chore: project documentations --- problem6/README.md | 318 +++++++++++++++++++++++++++++ problem6/docs/API_SPECIFICATION.md | 0 problem6/docs/PROJECT_PLAN.md | 162 +++++++++++++++ 3 files changed, 480 insertions(+) create mode 100644 problem6/README.md create mode 100644 problem6/docs/API_SPECIFICATION.md create mode 100644 problem6/docs/PROJECT_PLAN.md 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..e69de29bb2 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 From 7e1adfbfecad218a96db714b3c0e50134f41cc7a Mon Sep 17 00:00:00 2001 From: Lin Htut Kyaw Date: Thu, 2 Jul 2026 14:53:30 +0630 Subject: [PATCH 13/13] hotfix: wrong project dir --- src/problem5/README.md | 2 +- src/problem6/README.md | 318 +++++++++++++++++++++++++ src/problem6/docs/API_SPECIFICATION.md | 169 +++++++++++++ src/problem6/docs/PROJECT_PLAN.md | 162 +++++++++++++ 4 files changed, 650 insertions(+), 1 deletion(-) create mode 100644 src/problem6/README.md create mode 100644 src/problem6/docs/API_SPECIFICATION.md create mode 100644 src/problem6/docs/PROJECT_PLAN.md diff --git a/src/problem5/README.md b/src/problem5/README.md index 574391bb08..7b612395d4 100644 --- a/src/problem5/README.md +++ b/src/problem5/README.md @@ -1,4 +1,4 @@ -# Task Management API +# 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. 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