From bc545d937fcdb0cb8332f1cb0103b04004e02859 Mon Sep 17 00:00:00 2001 From: yehtetsan Date: Sun, 17 May 2026 18:52:13 +0630 Subject: [PATCH 1/4] Implement: solution for the problem 4 of coding test --- src/problem4/.gitignore | 27 ++++ src/problem4/README.md | 201 ++++++++++++++++++++++++++++ src/problem4/package-lock.json | 236 +++++++++++++++++++++++++++++++++ src/problem4/package.json | 26 ++++ src/problem4/src/main.ts | 82 ++++++++++++ src/problem4/tsconfig.json | 10 ++ 6 files changed, 582 insertions(+) create mode 100644 src/problem4/.gitignore create mode 100644 src/problem4/README.md create mode 100644 src/problem4/package-lock.json create mode 100644 src/problem4/package.json create mode 100644 src/problem4/src/main.ts create mode 100644 src/problem4/tsconfig.json diff --git a/src/problem4/.gitignore b/src/problem4/.gitignore new file mode 100644 index 0000000000..ad316ca343 --- /dev/null +++ b/src/problem4/.gitignore @@ -0,0 +1,27 @@ +# Dependencies +node_modules/ + +# Build output +dist/ + +# Environment +.env +.env.* + +# Logs +logs/ +*.log +npm-debug.log* + +# OS +.DS_Store +Thumbs.db + +# Editor +.idea/ +.vscode/ +*.swp +*.swo + +# TypeScript +*.tsbuildinfo diff --git a/src/problem4/README.md b/src/problem4/README.md new file mode 100644 index 0000000000..645f01fc04 --- /dev/null +++ b/src/problem4/README.md @@ -0,0 +1,201 @@ +# Problem 4 — Sum from 1 to n + +Three TypeScript implementations of the summation \(1 + 2 + \cdots + n\), with correctness checks and a performance comparison suitable for technical review. + +## Project overview + +**Goal:** Given a non-negative integer `n`, return the sum of all integers from `1` to `n`. + +| Input | Output | +|-------|--------| +| `n = 5` | `15` | +| `n = 10` | `55` | + +Results are assumed to stay below `Number.MAX_SAFE_INTEGER`. + +## Getting started + +```bash +npm install +npm run dev # run tests + benchmarks (ts-node) +npm run build # compile to dist/ +npm start # run compiled output +``` + +Source entry point: `src/main.ts`. + +## Implementations + +### `sum_to_n_a` — iterative loop + +Accumulates a running total from `1` through `n` using a `for` loop. + +```ts +function sum_to_n_a(n: number): number { + let total = 0; + for (let i = 1; i <= n; i++) { + total += i; + } + return total; +} +``` + +Straightforward and easy to follow; performance scales linearly with `n`. + +### `sum_to_n_b` — recursion + +Defines the sum recursively: `sum(n) = n + sum(n - 1)`, with base case `n <= 0 → 0`. + +```ts +function sum_to_n_b(n: number): number { + if (n <= 0) { + return 0; + } + return n + sum_to_n_b(n - 1); +} +``` + +Mirrors the mathematical definition; each step consumes call-stack space. + +### `sum_to_n_c` — mathematical formula + +Uses Gauss’s formula: \(\frac{n(n + 1)}{2}\). + +```ts +function sum_to_n_c(n: number): number { + return (n * (n + 1)) / 2; +} +``` + +Constant-time arithmetic regardless of `n` (within safe integer limits). + +## Complexity analysis + +| Implementation | Time | Space | Notes | +|----------------|------|-------|-------| +| `sum_to_n_a` (loop) | O(n) | O(1) | One addition per integer | +| `sum_to_n_b` (recursion) | O(n) | O(n) | Call stack depth equals `n` | +| `sum_to_n_c` (formula) | O(1) | O(1) | Fixed number of operations | + +## Efficiency conclusion + +**`sum_to_n_c` is optimal** for this problem: + +- **Time:** O(1) — multiply, add, and divide once. +- **Space:** O(1) — no auxiliary structures or stack growth. + +The loop and recursion both perform O(n) work. Recursion adds function-call and stack-frame overhead on every step, so it is typically slower than the loop even though both are O(n) in big-O terms. + +For production use, prefer the formula unless there is a specific reason to demonstrate iteration or recursion (e.g. teaching or interviews). + +## Benchmarking + +The project includes a separate benchmark phase (after correctness tests) that times each function under load. + +### Configuration + +| Constant | Value | Rationale | +|----------|-------|-----------| +| `BENCHMARK_N` | `8_000` | Large enough to expose loop vs formula cost; below Node’s default recursion stack limit (~9.5k frames) | +| `BENCHMARK_ITERATIONS` | `100_000` | Enough repetitions for stable, readable timings | + +### Why repeated iterations? + +A single call—especially for the formula—often completes in microseconds. Running many iterations: + +- Averages out timer noise and system jitter +- Makes differences between approaches visible in console output + +### Why warm-up execution? + +The first call to a function can be slower due to JIT compilation and CPU caches. `benchmarkSumFunction` calls `fn(n)` once before timing so measurements reflect steady-state performance. + +### Why `void resultSink`? + +The benchmark loop accumulates return values into `resultSink`. Without using that value, a JavaScript engine might optimize away the loop as dead code. `void resultSink` keeps the work observable without changing the measured functions. + +### Why `performance.now()`? + +Node’s `performance.now()` (from `node:perf_hooks`) provides sub-millisecond resolution suitable for micro-benchmarks, unlike `Date.now()` which is coarser. Timings are reported as total milliseconds and average milliseconds per call. + +### Sample output (illustrative) + +``` +=== Performance benchmark === +n = 8000, iterations = 100000 + +sum_to_n_a (loop) + total time: ~800–1100 ms + average time: ~0.008–0.011 ms per call + +sum_to_n_b (recursion) + total time: ~9000–11000 ms + average time: ~0.09–0.11 ms per call + +sum_to_n_c (formula) + total time: ~1–3 ms + average time: ~0.00001–0.00002 ms per call +``` + +Exact numbers vary by machine and Node version; the formula should remain orders of magnitude faster than the loop, and the loop faster than recursion. + +## Design decisions + +### Recursion stack limitations + +`sum_to_n_b` creates one stack frame per integer down to the base case. In Node.js, depths around **10,000** commonly hit `Maximum call stack size exceeded`. That limit is independent of `Number.MAX_SAFE_INTEGER`—you can overflow the stack long before numeric overflow. + +Benchmarks use `n = 8_000` so recursion completes reliably without modifying the recursive implementation. + +### Why recursion is less efficient + +Recursion and the loop both do O(n) arithmetic, but recursion pays extra cost per step: + +- Function call / return overhead +- O(n) call-stack memory + +The loop runs in a single stack frame with O(1) extra space. + +### Why benchmark logic is isolated + +Benchmarking lives in `benchmarkSumFunction`, `runBenchmarks()`, and constants—not inside `sum_to_n_a`, `sum_to_n_b`, or `sum_to_n_c`. That keeps: + +- Core functions **pure** and easy to test +- Performance instrumentation **optional** and replaceable +- Reviewers able to judge algorithm correctness without measurement noise + +### Why functions are not modified for measurement + +Injecting timers or counters into the sum implementations would couple them to benchmarking concerns and change their structure. Passing function references into a generic benchmark harness preserves original behavior and matches how you would test production code in isolation. + +## AI-assisted development disclosure + +This submission was developed with AI tooling used deliberately and reviewed by the author. + +| Tool | Role | +|------|------| +| **ChatGPT** | Prompt engineering and conceptual explanations (complexity, benchmarking theory, documentation structure) | +| **Cursor** | Agentic-assisted implementation (scaffolding, refactors, README drafting) | + +**Author responsibility:** + +- All generated code was **manually reviewed** and understood before acceptance. +- Correctness was verified via test cases and by running the project locally. +- Outputs were **not** accepted blindly; implementations and docs were edited to match intent and standards. + +**Scope note:** The base assignment required three implementations and basic tests. The **benchmarking / time-comparison section** was the author’s own extension—added intentionally to demonstrate performance reasoning beyond the minimum spec. + +## Project structure + +``` +problem4/ +├── src/ +│ └── main.ts # implementations, tests, benchmarks +├── package.json +├── tsconfig.json +└── README.md +``` + +## License + +ISC — see `package.json`. diff --git a/src/problem4/package-lock.json b/src/problem4/package-lock.json new file mode 100644 index 0000000000..a2656b84aa --- /dev/null +++ b/src/problem4/package-lock.json @@ -0,0 +1,236 @@ +{ + "name": "problem4", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "problem4", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "@types/node": "^25.8.0", + "ts-node": "^10.9.2", + "typescript": "^6.0.3" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.8.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.8.0.tgz", + "integrity": "sha512-TCFSk8IZh+iLX1xtksoBVtdmgL+1IX0fC9BeU4QqFSuNdN/K+HUlhqOzEmSYYpZUVsLYcPqc9KX+60iDuninSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/src/problem4/package.json b/src/problem4/package.json new file mode 100644 index 0000000000..dc36ba575c --- /dev/null +++ b/src/problem4/package.json @@ -0,0 +1,26 @@ +{ + "name": "problem4", + "version": "1.0.0", + "description": "Solution to the problem 4 which is assigned for coding challenge.", + "main": "main.js", + "scripts": { + "start": "node dist/main.js", + "dev": "ts-node src/main.ts", + "build": "tsc" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Kikijake/code-challenge.git" + }, + "author": "Ye Htet San", + "license": "ISC", + "bugs": { + "url": "https://github.com/Kikijake/code-challenge/issues" + }, + "homepage": "https://github.com/Kikijake/code-challenge#readme", + "devDependencies": { + "@types/node": "^25.8.0", + "ts-node": "^10.9.2", + "typescript": "^6.0.3" + } +} diff --git a/src/problem4/src/main.ts b/src/problem4/src/main.ts new file mode 100644 index 0000000000..3e31e26b00 --- /dev/null +++ b/src/problem4/src/main.ts @@ -0,0 +1,82 @@ +import { performance } from "node:perf_hooks"; + +function sum_to_n_a(n: number): number { + let total = 0; + for (let i = 1; i <= n; i++) { + total += i; + } + return total; +} + +function sum_to_n_b(n: number): number { + if (n <= 0) { + return 0; + } + return n + sum_to_n_b(n - 1); +} + +function sum_to_n_c(n: number): number { + return (n * (n + 1)) / 2; +} + +const BENCHMARK_N = 8_000; +const BENCHMARK_ITERATIONS = 100_000; + +type SumFunction = (n: number) => number; + +function benchmarkSumFunction( + name: string, + fn: SumFunction, + n: number, + iterations: number +): void { + fn(n); + + let resultSink = 0; + const start = performance.now(); + + for (let i = 0; i < iterations; i++) { + resultSink += fn(n); + } + + const totalMs = performance.now() - start; + const averageMs = totalMs / iterations; + + console.log(name); + console.log(` total time: ${totalMs.toFixed(3)} ms`); + console.log(` average time: ${averageMs.toFixed(6)} ms per call`); + void resultSink; + console.log(""); +} + +function runTests(): void { + const testValues = [0, 1, 5, 10, 100]; + + console.log("=== sum_to_n test cases ===\n"); + + for (const n of testValues) { + const a = sum_to_n_a(n); + const b = sum_to_n_b(n); + const c = sum_to_n_c(n); + + console.log(`n = ${n}`); + console.log(` sum_to_n_a (loop): ${a}`); + console.log(` sum_to_n_b (recursion): ${b}`); + console.log(` sum_to_n_c (formula): ${c}`); + console.log(` all match: ${a === b && b === c}\n`); + } + + console.log("Assignment example: sum_to_n(5) =", sum_to_n_c(5)); +} + +function runBenchmarks(): void { + console.log("\n=== Performance benchmark ==="); + console.log(`n = ${BENCHMARK_N}, iterations = ${BENCHMARK_ITERATIONS}\n`); + + benchmarkSumFunction("sum_to_n_a (loop)", sum_to_n_a, BENCHMARK_N, BENCHMARK_ITERATIONS); + benchmarkSumFunction("sum_to_n_b (recursion)", sum_to_n_b, BENCHMARK_N, BENCHMARK_ITERATIONS); + benchmarkSumFunction("sum_to_n_c (formula)", sum_to_n_c, BENCHMARK_N, BENCHMARK_ITERATIONS); +} + +runTests(); +runBenchmarks(); diff --git a/src/problem4/tsconfig.json b/src/problem4/tsconfig.json new file mode 100644 index 0000000000..6adfa7d213 --- /dev/null +++ b/src/problem4/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "rootDir": "./src", + "outDir": "./dist", + "strict": true, + "types": ["node"] + } +} \ No newline at end of file From 1983b88f96b95c08ff222a7852ff25fd726dabfb Mon Sep 17 00:00:00 2001 From: yehtetsan Date: Sun, 17 May 2026 23:50:35 +0630 Subject: [PATCH 2/4] Implement: products CRUD API with typescript, expressjs, prisma, mysql for problem5 --- src/problem5/.gitignore | 4 + src/problem5/README.md | 420 ++++ src/problem5/package-lock.json | 2047 +++++++++++++++++ src/problem5/package.json | 42 + .../20260517151933_init/migration.sql | 11 + .../prisma/migrations/migration_lock.toml | 3 + src/problem5/prisma/schema.prisma | 19 + src/problem5/src/config/env.ts | 16 + src/problem5/src/config/prisma.ts | 5 + .../src/controllers/product.controller.ts | 47 + src/problem5/src/helpers/filter.helper.ts | 14 + src/problem5/src/helpers/response.helper.ts | 36 + src/problem5/src/main.ts | 36 + src/problem5/src/middlewares/asyncHandler.ts | 13 + .../src/middlewares/error.middleware.ts | 45 + .../src/middlewares/validate.middleware.ts | 31 + src/problem5/src/routes/index.ts | 8 + src/problem5/src/routes/product.routes.ts | 39 + src/problem5/src/services/product.service.ts | 56 + src/problem5/src/types/express.d.ts | 11 + .../src/validators/product.validators.ts | 82 + src/problem5/tsconfig.json | 28 + 22 files changed, 3013 insertions(+) create mode 100644 src/problem5/.gitignore create mode 100644 src/problem5/README.md create mode 100644 src/problem5/package-lock.json create mode 100644 src/problem5/package.json create mode 100644 src/problem5/prisma/migrations/20260517151933_init/migration.sql create mode 100644 src/problem5/prisma/migrations/migration_lock.toml create mode 100644 src/problem5/prisma/schema.prisma create mode 100644 src/problem5/src/config/env.ts create mode 100644 src/problem5/src/config/prisma.ts create mode 100644 src/problem5/src/controllers/product.controller.ts create mode 100644 src/problem5/src/helpers/filter.helper.ts create mode 100644 src/problem5/src/helpers/response.helper.ts create mode 100644 src/problem5/src/main.ts create mode 100644 src/problem5/src/middlewares/asyncHandler.ts create mode 100644 src/problem5/src/middlewares/error.middleware.ts create mode 100644 src/problem5/src/middlewares/validate.middleware.ts create mode 100644 src/problem5/src/routes/index.ts create mode 100644 src/problem5/src/routes/product.routes.ts create mode 100644 src/problem5/src/services/product.service.ts create mode 100644 src/problem5/src/types/express.d.ts create mode 100644 src/problem5/src/validators/product.validators.ts create mode 100644 src/problem5/tsconfig.json diff --git a/src/problem5/.gitignore b/src/problem5/.gitignore new file mode 100644 index 0000000000..1f65c61ae0 --- /dev/null +++ b/src/problem5/.gitignore @@ -0,0 +1,4 @@ +/node_modules +dist +.env +coverage diff --git a/src/problem5/README.md b/src/problem5/README.md new file mode 100644 index 0000000000..8170042313 --- /dev/null +++ b/src/problem5/README.md @@ -0,0 +1,420 @@ +# Product Management API + +A RESTful backend service for managing products, built as part of the 99Tech Code Challenge. The API provides full CRUD operations, query filtering, and paginated list responses with a consistent JSON envelope. + +--- + +## Project Overview + +This service exposes a **Product** resource over HTTP. Clients can create, read, update, and delete products, list products with pagination, and filter by name and price range. The codebase follows a layered architecture (routes → controllers → services → database) with centralized validation and error handling suitable for production-style review. + +--- + +## Tech Stack + +| Layer | Technology | +|-------|------------| +| Runtime | Node.js | +| Language | TypeScript | +| Framework | Express 5 | +| ORM | Prisma 6 | +| Database | MySQL | +| Validation | express-validator | +| Logging | Morgan | +| CORS | cors | +| Config | dotenv | + +--- + +## Features + +- **CRUD** — Create, read, update, and delete products +- **Pagination** — `page` and `limit` on list endpoints (default: page 1, limit 10, max 100) +- **Filtering** — Filter by name (partial match) and price range (`minPrice`, `maxPrice`) +- **Validation** — Request validation on body, query, and route params before handlers run +- **Consistent API responses** — Uniform success and error JSON shape +- **Health check** — `GET /health` for uptime checks +- **Type safety** — Strict TypeScript and Prisma-generated types + +--- + +## Project Structure + +``` +src/problem5/ +├── prisma/ +│ ├── schema.prisma # Data model +│ └── migrations/ # SQL migration history +├── src/ +│ ├── config/ +│ │ ├── env.ts # Environment variables +│ │ └── prisma.ts # Prisma client singleton +│ ├── controllers/ +│ │ └── product.controller.ts +│ ├── helpers/ +│ │ ├── filter.helper.ts # Merges query filters into Prisma where clauses +│ │ └── response.helper.ts # Standard JSON response helpers +│ ├── middlewares/ +│ │ ├── asyncHandler.ts # Wraps async route handlers +│ │ ├── error.middleware.ts +│ │ └── validate.middleware.ts +│ ├── routes/ +│ │ ├── index.ts # Mounts /products under /api +│ │ └── product.routes.ts +│ ├── services/ +│ │ └── product.service.ts # Business logic & database access +│ ├── types/ +│ │ └── express.d.ts # Extends Request with validated data +│ ├── validators/ +│ │ └── product.validators.ts +│ └── main.ts # App entry point +├── package.json +├── tsconfig.json +└── README.md +``` + +--- + +## Environment Setup + +Create a `.env` file in `src/problem5/` (this file is gitignored): + +```env +PORT=3000 +DATABASE_URL="mysql://USER:PASSWORD@localhost:3306/DATABASE_NAME" +``` + +| Variable | Required | Description | +|----------|----------|-------------| +| `DATABASE_URL` | Yes | MySQL connection string for Prisma | +| `PORT` | No | HTTP port (default: `3000`) | + +**Prerequisites** + +- Node.js 18+ (recommended) +- MySQL 8+ (or compatible server) +- npm + +--- + +## Installation + +From the repository root: + +```bash +cd src/problem5 +npm install +``` + +--- + +## Prisma Setup + +1. Ensure MySQL is running and the database in `DATABASE_URL` exists (or create it): + + ```sql + CREATE DATABASE your_database_name; + ``` + +2. Generate the Prisma Client: + + ```bash + npx prisma generate + ``` + +3. Apply migrations (see below). + +--- + +## Migration Commands + +| Command | Purpose | +|---------|---------| +| `npx prisma migrate dev` | Apply pending migrations in development (creates DB if needed) | +| `npx prisma migrate deploy` | Apply migrations in production/staging | +| `npx prisma migrate status` | Show migration state | +| `npx prisma db push` | Push schema without migration files (prototyping only) | +| `npx prisma studio` | Open Prisma Studio GUI for the database | + +Initial migration creates the `products` table with `id`, `name`, `description`, `price`, `createdAt`, and `updatedAt`. + +--- + +## Running the Application + +**Development** (hot reload via nodemon): + +```bash +npm run dev +``` + +**Production**: + +```bash +npm run build +npm start +``` + +The server listens at `http://localhost:3000` (or your configured `PORT`). + +--- + +## Development Commands + +| Script | Command | Description | +|--------|---------|-------------| +| `dev` | `npm run dev` | Start dev server with TypeScript via ts-node | +| `build` | `npm run build` | Generate Prisma client and compile TypeScript to `dist/` | +| `build:clean` | `npm run build:clean` | Remove `dist/` then run full build | +| `compile` | `npm run compile` | TypeScript compile only (`tsc`) | +| `prisma:generate` | `npm run prisma:generate` | Regenerate Prisma client | +| `start` | `npm start` | Run compiled app from `dist/main.js` | + +Additional useful commands: + +```bash +npx prisma generate # Regenerate client after schema changes +npx prisma studio # Inspect/edit data +``` + +--- + +## Build Commands + +```bash +npm run build +``` + +The build runs `prisma generate` then `tsc`. Output is written to `dist/` with source maps and declaration files per `tsconfig.json`. + +--- + +## API Base URL + +``` +http://localhost:3000 +``` + +| Scope | Base path | +|-------|-----------| +| Health | `http://localhost:3000/health` | +| API | `http://localhost:3000/api` | +| Products | `http://localhost:3000/api/products` | + +--- + +## Example API Endpoints + +### Health check + +```http +GET /health +``` + +**Response (200)** + +```json +{ + "success": true, + "message": "API is running", + "data": null +} +``` + +--- + +### Create product + +```http +POST /api/products +Content-Type: application/json + +{ + "name": "Wireless Mouse", + "description": "Ergonomic wireless mouse", + "price": 29.99 +} +``` + +**Response (201)** + +```json +{ + "success": true, + "message": "Product created successfully", + "data": { + "id": 1, + "name": "Wireless Mouse", + "description": "Ergonomic wireless mouse", + "price": "29.99", + "createdAt": "2026-05-17T15:30:00.000Z", + "updatedAt": "2026-05-17T15:30:00.000Z" + } +} +``` + +--- + +### List products (pagination & filters) + +```http +GET /api/products?page=1&limit=10 +GET /api/products?name=mouse&minPrice=10&maxPrice=50 +``` + +| Query param | Type | Description | +|-------------|------|-------------| +| `page` | integer | Page number (default: 1) | +| `limit` | integer | Items per page, 1–100 (default: 10) | +| `name` | string | Partial name match | +| `minPrice` | number | Minimum price (inclusive) | +| `maxPrice` | number | Maximum price (inclusive) | + +**Response (200)** + +```json +{ + "success": true, + "message": "Products retrieved successfully", + "pagination": { + "total": 25, + "page": 1, + "limit": 10, + "totalPage": 3 + }, + "data": [ /* array of products */ ] +} +``` + +--- + +### Get product by ID + +```http +GET /api/products/1 +``` + +**Response (200)** — single product in `data`. +**Response (404)** — if not found. + +--- + +### Update product + +```http +PUT /api/products/1 +Content-Type: application/json + +{ + "name": "Wireless Mouse Pro", + "price": 34.99 +} +``` + +All body fields are optional; at least one should be sent for a meaningful update. + +**Response (200)** — updated product in `data`. + +--- + +### Delete product + +```http +DELETE /api/products/1 +``` + +**Response (200)** + +```json +{ + "success": true, + "message": "Product deleted successfully", + "data": null +} +``` + +--- + +### Error response format + +```json +{ + "success": false, + "message": "Validation failed", + "errors": { + "name": "Name is required", + "price": "Price must be numeric" + } +} +``` + +| Status | Typical cause | +|--------|----------------| +| 400 | Validation failure | +| 404 | Product not found | +| 409 | Duplicate record (Prisma unique constraint) | +| 500 | Unexpected server error | + +--- + +## Validation Approach + +Validation uses **express-validator** chains defined in `src/validators/product.validators.ts` and applied via the `validate` middleware. + +1. **Route-level chains** — Each endpoint declares rules for `body`, `query`, or `params` (e.g. required `name` on create, positive integer `id` on `:id` routes). +2. **Middleware execution** — `validate()` runs all chains, collects failures, and returns `400` with a field → message map if invalid. +3. **Sanitized data** — On success, `matchedData()` is attached to `req.validated` so controllers receive typed, coerced values (e.g. `toInt()`, `toFloat()`). +4. **Query filters** — List filters use `customSanitizer` to map query strings into Prisma-compatible where fragments (e.g. `name` → `{ name: { contains: "..." } }`). + +Controllers read only from `req.validated`, keeping handlers free of manual parsing. + +--- + +## Architecture + +The application uses a **layered (N-tier) architecture**: + +``` +HTTP Request + ↓ +Routes → URL mapping, middleware stack + ↓ +validate → Input validation & coercion + ↓ +asyncHandler → Forwards rejected promises to error middleware + ↓ +Controller → HTTP status codes & response formatting + ↓ +Service → Business rules, Prisma queries, AppError for domain cases + ↓ +Prisma Client → MySQL persistence +``` + +**Design choices** + +- **Thin controllers** — Delegate logic to services; use `successResponse` for output. +- **Services own data access** — All Prisma calls live in `product.service.ts`. +- **Shared filter helper** — `buildWhere()` merges sanitized query fragments into a single Prisma `where` object. +- **Single Prisma instance** — Exported from `config/prisma.ts` for connection reuse. + +--- + +## Error Handling + +Errors are handled centrally in `error.middleware.ts`: + +| Error type | Behavior | +|------------|----------| +| `AppError` | Returns `message`, `statusCode`, and optional `errors` (e.g. 404 "Product not found") | +| `PrismaClientKnownRequestError` | Maps known codes: `P2025` → 404, `P2002` → 409 | +| Other errors | Logged to console; client receives generic 500 | + +**Async errors** — Route handlers are wrapped with `asyncHandler`, which catches promise rejections and passes them to `next(err)` so the error middleware always runs. + +**Operational vs programmer errors** — Expected cases (not found, validation) use `AppError` with explicit status codes; unexpected failures fall through to the 500 handler. + +--- + +## Author + +Ye Htet San — [99Tech Code Challenge](https://github.com/Kikijake/code-challenge) diff --git a/src/problem5/package-lock.json b/src/problem5/package-lock.json new file mode 100644 index 0000000000..cf86de91ea --- /dev/null +++ b/src/problem5/package-lock.json @@ -0,0 +1,2047 @@ +{ + "name": "problem5", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "problem5", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@prisma/client": "^6.8.2", + "cors": "^2.8.5", + "dotenv": "^16.5.0", + "express": "^5.1.0", + "express-validator": "^7.2.1", + "morgan": "^1.10.0", + "prisma": "^6.8.2" + }, + "devDependencies": { + "@types/cors": "^2.8.18", + "@types/express": "^5.0.2", + "@types/morgan": "^1.9.9", + "@types/node": "^22.15.21", + "nodemon": "^3.1.14", + "ts-node": "^10.9.2", + "typescript": "^5.8.3" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@prisma/client": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.19.3.tgz", + "integrity": "sha512-mKq3jQFhjvko5LTJFHGilsuQs+W+T3Gm451NzuTDGQxwCzwXHYnIu2zGkRoW+Exq3Rob7yp2MfzSrdIiZVhrBg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/config": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.19.3.tgz", + "integrity": "sha512-CBPT44BjlQxEt8kiMEauji2WHTDoVBOKl7UlewXmUgBPnr/oPRZC3psci5chJnYmH0ivEIog2OU9PGWoki3DLQ==", + "license": "Apache-2.0", + "dependencies": { + "c12": "3.1.0", + "deepmerge-ts": "7.1.5", + "effect": "3.21.0", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.3.tgz", + "integrity": "sha512-ljkJ+SgpXNktLG0Q/n4JGYCkKf0f8oYLyjImS2I8e2q2WCfdRRtWER062ZV/ixaNP2M2VKlWXVJiGzZaUgbKZw==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.19.3.tgz", + "integrity": "sha512-RSYxtlYFl5pJ8ZePgMv0lZ9IzVCOdTPOegrs2qcbAEFrBI1G33h6wyC9kjQvo0DnYEhEVY0X4LsuFHXLKQk88g==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.3", + "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "@prisma/fetch-engine": "6.19.3", + "@prisma/get-platform": "6.19.3" + } + }, + "node_modules/@prisma/engines-version": { + "version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7.tgz", + "integrity": "sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.19.3.tgz", + "integrity": "sha512-tKtl/qco9Nt7LU5iKhpultD8O4vMCZcU2CHjNTnRrL1QvSUr5W/GcyFPjNL87GtRrwBc7ubXXD9xy4EvLvt8JA==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.3", + "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "@prisma/get-platform": "6.19.3" + } + }, + "node_modules/@prisma/get-platform": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.19.3.tgz", + "integrity": "sha512-xFj1VcJ1N3MKooOQAGO0W5tsd0W2QzIvW7DD7c/8H14Zmp4jseeWAITm+w2LLoLrlhoHdPPh0NMZ8mfL6puoHA==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.3" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/morgan": { + "version": "1.9.10", + "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.10.tgz", + "integrity": "sha512-sS4A1zheMvsADRVfT0lYbJ4S9lmsey8Zo2F7cnbYjWHP67Q0AwMYuuzLlkIM2N8gAbb9cubhIVFwcIN2XyYCkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "22.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", + "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/c12": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", + "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.3", + "confbox": "^0.2.2", + "defu": "^6.1.4", + "dotenv": "^16.6.1", + "exsolve": "^1.0.7", + "giget": "^2.0.0", + "jiti": "^2.4.2", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^1.0.0", + "pkg-types": "^2.2.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.5" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, + "node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "license": "MIT" + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/effect": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.21.0.tgz", + "integrity": "sha512-PPN80qRokCd1f015IANNhrwOnLO7GrrMQfk4/lnZRE/8j7UPWrNNjPV0uBrZutI/nHzernbW+J0hdqQysHiSnQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, + "node_modules/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-validator": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/express-validator/-/express-validator-7.3.2.tgz", + "integrity": "sha512-ctLw1Vl6dXVH62dIQMDdTAQkrh480mkFuG6/SGXOaVlwPNukhRAe7EgJIMJ2TSAni8iwHBRp530zAZE5ZPF2IA==", + "license": "MIT", + "dependencies": { + "lodash": "^4.18.1", + "validator": "~13.15.23" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/exsolve": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", + "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "license": "MIT" + }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/giget": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.6.0", + "pathe": "^2.0.3" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/morgan": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz", + "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==", + "license": "MIT", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.1.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/morgan/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "license": "MIT" + }, + "node_modules/nodemon": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz", + "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^10.2.1", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/nodemon/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nypm": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.6.tgz", + "integrity": "sha512-vRyr0r4cbBapw07Xw8xrj9Teq3o7MUD35rSaTcanDbW+aK2XHDgJFiU6ZTj2GBw7Q12ysdsyFss+Vdz4hQ0Y6Q==", + "license": "MIT", + "dependencies": { + "citty": "^0.2.2", + "pathe": "^2.0.3", + "tinyexec": "^1.1.1" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/nypm/node_modules/citty": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.2.tgz", + "integrity": "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==", + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-types": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz", + "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==", + "license": "MIT", + "dependencies": { + "confbox": "^0.2.4", + "exsolve": "^1.0.8", + "pathe": "^2.0.3" + } + }, + "node_modules/prisma": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.3.tgz", + "integrity": "sha512-++ZJ0ijLrDJF6hNB4t4uxg2br3fC4H9Yc9tcbjr2fcNFP3rh/SBNrAgjhsqBU4Ght8JPrVofG/ZkXfnSfnYsFg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/config": "6.19.3", + "@prisma/engines": "6.19.3" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tinyexec": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz", + "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/validator": { + "version": "13.15.35", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz", + "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/src/problem5/package.json b/src/problem5/package.json new file mode 100644 index 0000000000..4e2502491f --- /dev/null +++ b/src/problem5/package.json @@ -0,0 +1,42 @@ +{ + "name": "problem5", + "version": "1.0.0", + "description": "Solution to the problem 5 which is assigned for coding challenge.", + "main": "main.js", + "scripts": { + "start": "node dist/main.js", + "dev": "nodemon --watch src --ext ts --exec \"node -r ts-node/register\" src/main.ts", + "prisma:generate": "prisma generate", + "compile": "tsc", + "build": "npm run prisma:generate && npm run compile", + "build:clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && npm run build" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/Kikijake/code-challenge.git" + }, + "author": "Ye Htet San", + "license": "ISC", + "bugs": { + "url": "https://github.com/Kikijake/code-challenge/issues" + }, + "homepage": "https://github.com/Kikijake/code-challenge#readme", + "dependencies": { + "@prisma/client": "^6.8.2", + "cors": "^2.8.5", + "dotenv": "^16.5.0", + "express": "^5.1.0", + "express-validator": "^7.2.1", + "morgan": "^1.10.0", + "prisma": "^6.8.2" + }, + "devDependencies": { + "@types/cors": "^2.8.18", + "@types/express": "^5.0.2", + "@types/morgan": "^1.9.9", + "@types/node": "^22.15.21", + "nodemon": "^3.1.14", + "ts-node": "^10.9.2", + "typescript": "^5.8.3" + } +} diff --git a/src/problem5/prisma/migrations/20260517151933_init/migration.sql b/src/problem5/prisma/migrations/20260517151933_init/migration.sql new file mode 100644 index 0000000000..e995e69971 --- /dev/null +++ b/src/problem5/prisma/migrations/20260517151933_init/migration.sql @@ -0,0 +1,11 @@ +-- CreateTable +CREATE TABLE `products` ( + `id` INTEGER NOT NULL AUTO_INCREMENT, + `name` VARCHAR(191) NOT NULL, + `description` VARCHAR(191) NULL, + `price` DECIMAL(10, 2) NOT NULL, + `createdAt` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updatedAt` DATETIME(3) NOT NULL, + + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; diff --git a/src/problem5/prisma/migrations/migration_lock.toml b/src/problem5/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000000..592fc0b32f --- /dev/null +++ b/src/problem5/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "mysql" diff --git a/src/problem5/prisma/schema.prisma b/src/problem5/prisma/schema.prisma new file mode 100644 index 0000000000..b8a228c36a --- /dev/null +++ b/src/problem5/prisma/schema.prisma @@ -0,0 +1,19 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "mysql" + url = env("DATABASE_URL") +} + +model Product { + id Int @id @default(autoincrement()) + name String + description String? + price Decimal @db.Decimal(10, 2) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@map("products") +} diff --git a/src/problem5/src/config/env.ts b/src/problem5/src/config/env.ts new file mode 100644 index 0000000000..583e713367 --- /dev/null +++ b/src/problem5/src/config/env.ts @@ -0,0 +1,16 @@ +import dotenv from "dotenv"; + +dotenv.config(); + +function requireEnv(key: string): string { + const value = process.env[key]; + if (!value) { + throw new Error(`Missing required environment variable: ${key}`); + } + return value; +} + +export const env = { + port: parseInt(process.env.PORT ?? "3000", 10), + databaseUrl: requireEnv("DATABASE_URL"), +}; diff --git a/src/problem5/src/config/prisma.ts b/src/problem5/src/config/prisma.ts new file mode 100644 index 0000000000..b5bf6ce859 --- /dev/null +++ b/src/problem5/src/config/prisma.ts @@ -0,0 +1,5 @@ +import { PrismaClient } from "@prisma/client"; + +const prisma = new PrismaClient(); + +export default prisma; diff --git a/src/problem5/src/controllers/product.controller.ts b/src/problem5/src/controllers/product.controller.ts new file mode 100644 index 0000000000..cab45689e9 --- /dev/null +++ b/src/problem5/src/controllers/product.controller.ts @@ -0,0 +1,47 @@ +import { Request, Response } from "express"; +import { Prisma } from "@prisma/client"; +import { productService } from "../services/product.service"; +import { successResponse } from "../helpers/response.helper"; + +export const productController = { + async create(req: Request, res: Response): Promise { + const data = req.validated; + const product = await productService.create( + data as Prisma.ProductCreateInput + ); + successResponse(res, product, "Product created successfully", 201); + }, + + async findAll(req: Request, res: Response): Promise { + const data = req.validated; + const result = await productService.findAll(data); + successResponse( + res, + result.products, + "Products retrieved successfully", + 200, + result.pagination + ); + }, + + async findById(req: Request, res: Response): Promise { + const { id } = req.validated; + const product = await productService.findById(id as number); + successResponse(res, product, "Product retrieved successfully", 200); + }, + + async update(req: Request, res: Response): Promise { + const { id, ...updateData } = req.validated; + const product = await productService.update( + id as number, + updateData as Prisma.ProductUpdateInput + ); + successResponse(res, product, "Product updated successfully", 200); + }, + + async delete(req: Request, res: Response): Promise { + const { id } = req.validated; + await productService.delete(id as number); + successResponse(res, null, "Product deleted successfully", 200); + }, +}; diff --git a/src/problem5/src/helpers/filter.helper.ts b/src/problem5/src/helpers/filter.helper.ts new file mode 100644 index 0000000000..0c61ecdbdb --- /dev/null +++ b/src/problem5/src/helpers/filter.helper.ts @@ -0,0 +1,14 @@ +export const buildWhere = >( + filters: Record +): TWhere => { + let where = {} as TWhere; + + Object.values(filters).forEach((fragment) => { + where = { + ...where, + ...fragment, + }; + }); + + return where; +}; diff --git a/src/problem5/src/helpers/response.helper.ts b/src/problem5/src/helpers/response.helper.ts new file mode 100644 index 0000000000..0db9ab2b6f --- /dev/null +++ b/src/problem5/src/helpers/response.helper.ts @@ -0,0 +1,36 @@ +import { Response } from "express"; + +export interface PaginationMeta { + total: number; + page: number; + limit: number; + totalPage: number; +} + +export const successResponse = ( + res: Response, + data: unknown, + message: string, + statusCode = 200, + pagination?: PaginationMeta +): void => { + res.status(statusCode).json({ + success: true, + message, + ...(pagination && { pagination }), + data, + }); +}; + +export const errorResponse = ( + res: Response, + message: string, + statusCode = 400, + errors?: Record +): void => { + res.status(statusCode).json({ + success: false, + message, + ...(errors && { errors }), + }); +}; diff --git a/src/problem5/src/main.ts b/src/problem5/src/main.ts new file mode 100644 index 0000000000..9e6167a317 --- /dev/null +++ b/src/problem5/src/main.ts @@ -0,0 +1,36 @@ +/// +import express from "express"; +import cors from "cors"; +import morgan from "morgan"; +import { env } from "./config/env"; +import routes from "./routes"; +import { errorMiddleware } from "./middlewares/error.middleware"; +import { successResponse } from "./helpers/response.helper"; + +const app = express(); + +app.use(cors()); +app.use(morgan("dev")); +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); + +app.get("/health", (_req, res) => { + successResponse(res, null, "API is running", 200); +}); + +app.use("/api", routes); + +app.use(errorMiddleware); + +const server = app.listen(env.port, () => { + console.log(`Server running on http://localhost:${env.port}`); +}); + +server.on("error", (err: NodeJS.ErrnoException) => { + if (err.code === "EADDRINUSE") { + console.error(`Port ${env.port} is already in use`); + } else { + console.error("Server error:", err); + } + process.exit(1); +}); diff --git a/src/problem5/src/middlewares/asyncHandler.ts b/src/problem5/src/middlewares/asyncHandler.ts new file mode 100644 index 0000000000..b8e6d511ee --- /dev/null +++ b/src/problem5/src/middlewares/asyncHandler.ts @@ -0,0 +1,13 @@ +import { Request, Response, NextFunction, RequestHandler } from "express"; + +type AsyncRequestHandler = ( + req: Request, + res: Response, + next: NextFunction +) => Promise; + +export const asyncHandler = + (fn: AsyncRequestHandler): RequestHandler => + (req, res, next) => { + Promise.resolve(fn(req, res, next)).catch(next); + }; diff --git a/src/problem5/src/middlewares/error.middleware.ts b/src/problem5/src/middlewares/error.middleware.ts new file mode 100644 index 0000000000..df27281f87 --- /dev/null +++ b/src/problem5/src/middlewares/error.middleware.ts @@ -0,0 +1,45 @@ +import { Request, Response, NextFunction } from "express"; +import { Prisma } from "@prisma/client"; +import { errorResponse } from "../helpers/response.helper"; + +export class AppError extends Error { + statusCode: number; + errors?: Record; + + constructor( + message: string, + statusCode = 500, + errors?: Record + ) { + super(message); + this.statusCode = statusCode; + this.name = "AppError"; + this.errors = errors; + } +} + +export const errorMiddleware = ( + err: Error, + _req: Request, + res: Response, + _next: NextFunction +): void => { + if (err instanceof AppError) { + errorResponse(res, err.message, err.statusCode, err.errors); + return; + } + + if (err instanceof Prisma.PrismaClientKnownRequestError) { + if (err.code === "P2025") { + errorResponse(res, "Record not found", 404); + return; + } + if (err.code === "P2002") { + errorResponse(res, "Duplicate record", 409); + return; + } + } + + console.error(err); + errorResponse(res, "Internal server error", 500); +}; diff --git a/src/problem5/src/middlewares/validate.middleware.ts b/src/problem5/src/middlewares/validate.middleware.ts new file mode 100644 index 0000000000..b634c02fbb --- /dev/null +++ b/src/problem5/src/middlewares/validate.middleware.ts @@ -0,0 +1,31 @@ +import { Request, Response, NextFunction } from "express"; +import { + validationResult, + matchedData, + ValidationChain, +} from "express-validator"; +import { errorResponse } from "../helpers/response.helper"; + +export const validate = + (validations: ValidationChain[]) => + async (req: Request, res: Response, next: NextFunction): Promise => { + await Promise.all(validations.map((validation) => validation.run(req))); + + const result = validationResult(req); + if (!result.isEmpty()) { + const errors: Record = {}; + result.array().forEach((err) => { + if (err.type === "field") { + errors[err.path] = err.msg; + } + }); + errorResponse(res, "Validation failed", 400, errors); + return; + } + + req.validated = matchedData(req, { + locations: ["body", "query", "params"], + }); + + next(); + }; diff --git a/src/problem5/src/routes/index.ts b/src/problem5/src/routes/index.ts new file mode 100644 index 0000000000..6ebc55b382 --- /dev/null +++ b/src/problem5/src/routes/index.ts @@ -0,0 +1,8 @@ +import { Router } from "express"; +import productRoutes from "./product.routes"; + +const router = Router(); + +router.use("/products", productRoutes); + +export default router; diff --git a/src/problem5/src/routes/product.routes.ts b/src/problem5/src/routes/product.routes.ts new file mode 100644 index 0000000000..8180c7a4b0 --- /dev/null +++ b/src/problem5/src/routes/product.routes.ts @@ -0,0 +1,39 @@ +import { Router } from "express"; +import { productController } from "../controllers/product.controller"; +import { asyncHandler } from "../middlewares/asyncHandler"; +import { validate } from "../middlewares/validate.middleware"; +import { productValidator } from "../validators/product.validators"; + +const router = Router(); + +router.post( + "/", + validate(productValidator.create), + asyncHandler(productController.create) +); + +router.get( + "/", + validate(productValidator.findAll), + asyncHandler(productController.findAll) +); + +router.get( + "/:id", + validate(productValidator.findById), + asyncHandler(productController.findById) +); + +router.put( + "/:id", + validate(productValidator.update), + asyncHandler(productController.update) +); + +router.delete( + "/:id", + validate(productValidator.delete), + asyncHandler(productController.delete) +); + +export default router; diff --git a/src/problem5/src/services/product.service.ts b/src/problem5/src/services/product.service.ts new file mode 100644 index 0000000000..92dcd4583f --- /dev/null +++ b/src/problem5/src/services/product.service.ts @@ -0,0 +1,56 @@ +import { Prisma } from "@prisma/client"; +import prisma from "../config/prisma"; +import { AppError } from "../middlewares/error.middleware"; +import { buildWhere } from "../helpers/filter.helper"; + +export const productService = { + async create(data: Prisma.ProductCreateInput) { + return prisma.product.create({ data }); + }, + + async findAll(validated: Record) { + const { page, limit, ...queryFields } = validated; + const where = buildWhere( + queryFields as Record + ); + const skip = ((page as number) - 1) * (limit as number); + + const [total, products] = await Promise.all([ + prisma.product.count({ where }), + prisma.product.findMany({ + where, + skip, + take: limit as number, + orderBy: { createdAt: "desc" }, + }), + ]); + + return { + products, + pagination: { + total, + page: page as number, + limit: limit as number, + totalPage: Math.ceil(total / (limit as number)) || 0, + }, + }; + }, + + async findById(id: number) { + const product = await prisma.product.findUnique({ where: { id } }); + if (!product) { + throw new AppError("Product not found", 404); + } + return product; + }, + + async update(id: number, data: Prisma.ProductUpdateInput) { + await this.findById(id); + return prisma.product.update({ where: { id }, data }); + }, + + async delete(id: number) { + await this.findById(id); + await prisma.product.delete({ where: { id } }); + }, +}; diff --git a/src/problem5/src/types/express.d.ts b/src/problem5/src/types/express.d.ts new file mode 100644 index 0000000000..365e7c9ad5 --- /dev/null +++ b/src/problem5/src/types/express.d.ts @@ -0,0 +1,11 @@ +import "express"; + +declare global { + namespace Express { + interface Request { + validated: Record; + } + } +} + +export {}; diff --git a/src/problem5/src/validators/product.validators.ts b/src/problem5/src/validators/product.validators.ts new file mode 100644 index 0000000000..7f35cc17a0 --- /dev/null +++ b/src/problem5/src/validators/product.validators.ts @@ -0,0 +1,82 @@ +import { body, param, query } from "express-validator"; + +const idParam = [ + param("id") + .isInt({ min: 1 }) + .withMessage("Invalid product id") + .toInt(), +]; + +export const productValidator = { + create: [ + body("name").trim().notEmpty().withMessage("Name is required"), + body("price") + .notEmpty() + .withMessage("Price is required") + .isFloat({ min: 0 }) + .withMessage("Price must be numeric") + .toFloat(), + body("description") + .optional({ values: "null" }) + .isString() + .withMessage("Description must be a string"), + ], + + findAll: [ + query("page") + .default(1) + .isInt({ min: 1 }) + .withMessage("Page must be a positive integer") + .toInt(), + query("limit") + .default(10) + .isInt({ min: 1, max: 100 }) + .withMessage("Limit must be between 1 and 100") + .toInt(), + query("name") + .optional() + .isString() + .withMessage("Name must be a string") + .customSanitizer((name) => ({ + name: { contains: name }, + })), + query("minPrice") + .optional() + .isFloat({ min: 0 }) + .withMessage("Minimum price must be a positive number") + .toFloat() + .customSanitizer((minPrice) => ({ + price: { gte: minPrice }, + })), + query("maxPrice") + .optional() + .isFloat({ min: 0 }) + .withMessage("Maximum price must be a positive number") + .toFloat() + .customSanitizer((maxPrice) => ({ + price: { lte: maxPrice }, + })), + ], + + findById: idParam, + + update: [ + ...idParam, + body("name") + .optional() + .trim() + .notEmpty() + .withMessage("Name cannot be empty"), + body("price") + .optional() + .isFloat({ min: 0 }) + .withMessage("Price must be numeric") + .toFloat(), + body("description") + .optional({ values: "null" }) + .isString() + .withMessage("Description must be a string"), + ], + + delete: idParam, +}; diff --git a/src/problem5/tsconfig.json b/src/problem5/tsconfig.json new file mode 100644 index 0000000000..ec2a63beef --- /dev/null +++ b/src/problem5/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["ES2020"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "moduleResolution": "node", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noUnusedLocals": true, + "noUnusedParameters": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"], + "ts-node": { + "files": true + } +} From fc43a85d36f560f6c6523d2f2f207dabb51d382f Mon Sep 17 00:00:00 2001 From: yehtetsan Date: Mon, 18 May 2026 00:30:52 +0630 Subject: [PATCH 3/4] Implement: README.md file for problem6 --- src/problem6/README.md | 187 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 src/problem6/README.md diff --git a/src/problem6/README.md b/src/problem6/README.md new file mode 100644 index 0000000000..ec17fe82d8 --- /dev/null +++ b/src/problem6/README.md @@ -0,0 +1,187 @@ +# Live Leaderboard API + +## 1. Overview + +This module is a small backend service for a website leaderboard. It stores user scores in MongoDB, returns the top 10 users, and pushes live updates with Socket.IO when someone earns points. + +**Why it exists:** When a user completes an action in the app (like finishing a quiz), the frontend tells the backend. The backend adds points to that user's score and everyone on the site sees the updated leaderboard without refreshing. + +**Tech stack:** + +- Node.js + Express.js — REST API +- MongoDB + Mongoose — store users and scores +- Socket.IO — live leaderboard updates +- JWT — protect score update routes + +--- + +## 2. Features + +- Update user score when an action is completed +- Get top 10 users on the leaderboard +- Broadcast leaderboard changes to all connected clients (Socket.IO) +- JWT authentication on protected routes +- Basic anti-cheat: server calculates points, blocks duplicate actions, simple rate limiting + +--- + +## 3. API Endpoints + +All score updates require a valid JWT in the header: + +```http +Authorization: Bearer +``` + +### POST /api/scores/update + +Called when the user finishes an action. The client sends the **action type only** — not a score number. + +**Request:** + +```json +{ + "actionType": "quiz_completed" +} +``` + +**Response:** + +```json +{ + "success": true, + "newScore": 150 +} +``` + +**Notes:** + +- JWT required — the backend gets `userId` from the token +- The backend decides how many points each `actionType` is worth +- The frontend must **not** send `score`, `points`, or `newScore` in the body + +**Errors (examples):** + +| Status | When | +|--------|------| +| 401 | Missing or invalid JWT | +| 400 | Unknown `actionType` | +| 409 | Same action already logged (duplicate) | +| 429 | Too many requests (rate limit) | + +--- + +### GET /api/leaderboard + +Returns the top 10 users by score. Can be public or protected depending on your app. + +**Response:** + +```json +{ + "topUsers": [ + { + "username": "alice", + "score": 500 + }, + { + "username": "bob", + "score": 420 + } + ] +} +``` + +--- + +## 4. MongoDB Collections + +### users + +| Field | Purpose | +|-------|---------| +| `username` | Display name on leaderboard | +| `score` | Current total score | +| `createdAt` | When the user was created | + +### actionLogs + +| Field | Purpose | +|-------|---------| +| `userId` | Who did the action | +| `actionType` | e.g. `quiz_completed` | +| `createdAt` | When it happened | + +**Why actionLogs?** Before updating a score, you can check if that user already got credit for the same action (for example, one `daily_login` per day, or store a unique `actionId` if the client sends one). That helps stop duplicate score bumps from double-clicks or replayed requests. + +--- + +## 5. Authentication and Security + +- **JWT authentication** — Only logged-in users can call `POST /api/scores/update`. Read `userId` from the token, not from the request body. +- **Backend validates actions** — Only allow known `actionType` values. Reject anything that does not match your list. +- **Backend calculates score** — Points come from server config (e.g. `quiz_completed` = +50). Never trust a score sent by the client. +- **Frontend cannot send score values** — Do not accept fields like `score`, `points`, or `amount` in the update body. +- **Simple rate limiting** — e.g. max 20 update requests per minute per user (use `express-rate-limit`). +- **Duplicate request checking** — Log each action in `actionLogs` and skip or reject if the same user + action was already processed (based on your rules). + +--- + +## 6. Real-Time Updates + +After a successful score update: + +1. Save the new score in MongoDB. +2. Query the top 10 users. +3. Use Socket.IO to emit an event (e.g. `leaderboard:update`) with the new list to all connected clients. + +Clients connect on page load, listen for `leaderboard:update`, and redraw the scoreboard. They can also call `GET /api/leaderboard` on first load or if the socket disconnects. + +**Example event payload:** + +```json +{ + "topUsers": [ + { "username": "alice", "score": 500 }, + { "username": "bob", "score": 420 } + ] +} +``` + +--- + +## 7. Simple Flow Diagram + +```mermaid +flowchart TD + A[User completes action] + B[Frontend sends POST /api/scores/update] + C[Backend validates JWT] + D[Backend calculates points and updates MongoDB] + E[Socket.IO emits leaderboard:update] + F[All clients refresh leaderboard UI] + + A --> B --> C --> D --> E --> F +``` + +--- + +## 8. Future Improvements + +- **Redis caching** — Cache the top 10 list so `GET /api/leaderboard` is faster under load +- **Daily leaderboard** — Reset or track scores per day with a separate collection or date field +- **Better anti-cheat checks** — Stricter rules per action, logging suspicious patterns, admin review + +--- + +## Action Types (Example) + +Define allowed types and points on the server: + +| actionType | Points | +|------------|--------| +| `quiz_completed` | 50 | +| `daily_login` | 10 | +| `level_cleared` | 100 | + +Add new rows here as the product grows — keep the logic in one place in the Express app. From db6be1411e05174bb7723810f824e202ffea3767 Mon Sep 17 00:00:00 2001 From: yehtetsan Date: Mon, 18 May 2026 00:37:50 +0630 Subject: [PATCH 4/4] Add: .env.example file --- src/problem5/.env.example | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 src/problem5/.env.example diff --git a/src/problem5/.env.example b/src/problem5/.env.example new file mode 100644 index 0000000000..c78981dc9d --- /dev/null +++ b/src/problem5/.env.example @@ -0,0 +1,4 @@ +# Copy this file to .env and fill in your values + +PORT=3000 +DATABASE_URL="mysql://USER:PASSWORD@localhost:3306/DATABASE_NAME"