diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..3114d67f3d --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +# Problem 4 (Node.js/TypeScript) +node_modules/ +dist/ +.env +.env.* +!.env.example + +# Logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Optional TypeScript incremental cache +*.tsbuildinfo + +migrations/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000000..edb6fb55b5 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +All answers and implementations for Problems 4, 5, and 6 related to the Backend Engineer (Remote) role are documented here. \ No newline at end of file diff --git a/problem_4/sum_to_n.js b/problem_4/sum_to_n.js new file mode 100644 index 0000000000..352310e3d1 --- /dev/null +++ b/problem_4/sum_to_n.js @@ -0,0 +1,29 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.sum_to_n_a = sum_to_n_a; +exports.sum_to_n_b = sum_to_n_b; +exports.sum_to_n_c = sum_to_n_c; +function sum_to_n_a(n) { + //time: O(n), space: O(1) + if (n <= 0) + return 0; + var sum = 0; + for (var i = 1; i <= n; i++) { + sum += i; + } + return sum; +} +function sum_to_n_b(n) { + //time: O(n), space: O(n) + if (n <= 0) + return 0; + if (n === 1) + return 1; + return n + sum_to_n_b(n - 1); +} +function sum_to_n_c(n) { + //time: O(1), space: O(1) + if (n <= 0) + return 0; + return (n * (n + 1)) / 2; +} diff --git a/problem_4/sum_to_n.ts b/problem_4/sum_to_n.ts new file mode 100644 index 0000000000..e23d7f27f7 --- /dev/null +++ b/problem_4/sum_to_n.ts @@ -0,0 +1,23 @@ +export function sum_to_n_a(n: number): number { + //time: O(n), space: O(1) + if (n <= 0) return 0; + let sum = 0; + for (let i = 1; i <= n; i++) { + sum += i; + } + return sum; +} + +export function sum_to_n_b(n: number): number { + //time: O(n), space: O(n) + if (n <= 0) return 0; + if (n === 1) return 1; + return n + sum_to_n_b(n - 1); +} + +export function sum_to_n_c(n: number): number { + //time: O(1), space: O(1) + if (n <= 0) return 0; + return (n * (n + 1)) / 2; +} + diff --git a/problem_4/task.md b/problem_4/task.md new file mode 100644 index 0000000000..9a2139b09e --- /dev/null +++ b/problem_4/task.md @@ -0,0 +1,25 @@ +# Task + +Provide 3 unique implementations of the following function in TypeScript. + +- Comment on the complexity or efficiency of each function. + +**Input**: `n` - any integer + +*Assuming this input will always produce a result lesser than `Number.MAX_SAFE_INTEGER`*. + +**Output**: `return` - summation to `n`, i.e. `sum_to_n(5) === 1 + 2 + 3 + 4 + 5 === 15`. + +```go +func sum_to_n_a(n: number): number { + // your code here +} + +func sum_to_n_b(n: number): number { + // your code here +} + +func sum_to_n_c(n: number): number { + // your code here +} +``` \ No newline at end of file diff --git a/problem_4/test.js b/problem_4/test.js new file mode 100644 index 0000000000..d1c322536f --- /dev/null +++ b/problem_4/test.js @@ -0,0 +1,25 @@ +const { sum_to_n_a, sum_to_n_b, sum_to_n_c } = require('./sum_to_n'); + +function expected(n) { + if (n <= 0) return 0; + return (n * (n + 1)) / 2; +} + +const cases = [-3, 0, 1, 2, 5, 10, 100]; +let allPass = true; +for (const n of cases) { + const a = sum_to_n_a(n); + const b = sum_to_n_b(n); + const c = sum_to_n_c(n); + const exp = expected(n); + const pass = (a === exp) && (b === exp) && (c === exp); + console.log(`n=${n}: a=${a}, b=${b}, c=${c}, expected=${exp} -> ${pass ? 'PASS' : 'FAIL'}`); + if (!pass) allPass = false; +} + +if (!allPass) { + console.error('Some tests failed'); + process.exit(1); +} else { + console.log('All tests passed'); +} diff --git a/problem_5/.env.example b/problem_5/.env.example new file mode 100644 index 0000000000..b5de318cdd --- /dev/null +++ b/problem_5/.env.example @@ -0,0 +1,2 @@ +PORT=3000 +DATABASE_URL="postgresql://postgres:postgres@localhost:5432/resources_db?schema=public" \ No newline at end of file diff --git a/problem_5/README.md b/problem_5/README.md new file mode 100644 index 0000000000..5814f2ed12 --- /dev/null +++ b/problem_5/README.md @@ -0,0 +1,375 @@ +# Problem 5 - Resource CRUD Service + +Resource CRUD service built with TypeScript, Express, Prisma, and PostgreSQL. + +The service follows a controller/service/repository structure and includes: + +- Create resource +- List resources with filters and pagination +- Fetch resource detail +- Update resource +- Soft delete by default +- Hard delete as an explicit opt-in +- Batch update +- Batch delete +- Prisma migrations and seed script + +## Tech Stack + +- Node.js 18+ +- Express 4 +- TypeScript +- Prisma ORM 5 +- PostgreSQL 16 +- Docker Compose + +## Repository Layout + +- `src/server.ts`: application bootstrap, Prisma connect/disconnect, graceful shutdown +- `src/app.ts`: Express app factory, routes, health check, and error middleware +- `src/routes/`: route registration +- `src/controllers/`: HTTP layer and request/response mapping +- `src/services/`: validation and business rules +- `src/repositories/`: Prisma data access and transactions +- `src/dto/`: request/response DTOs +- `src/models/`: domain models +- `src/enums/`: shared enum definitions +- `src/exceptions/`: typed HTTP errors +- `src/prisma.ts`: Prisma client singleton +- `prisma/schema.prisma`: Prisma schema and mappings +- `prisma/migrations/`: database migrations +- `prisma/seed.js`: seed script +- `docker-compose.yml`: local PostgreSQL container + +## Configuration + +Create your environment file from the template: + +```bash +cp .env.example .env +``` + +Default values: + +```bash +PORT=3000 +DATABASE_URL="postgresql://postgres:postgres@localhost:5432/resources_db?schema=public" +``` + +## Setup and Run + +### 1. Start PostgreSQL + +```bash +docker compose up -d +``` + +This starts PostgreSQL on `localhost:5432` with database `resources_db`. + +### 2. Install Dependencies + +```bash +npm install +``` + +### 3. Apply Prisma Migrations + +Generate the Prisma client and apply the local migration history: + +```bash +npm run prisma:generate +npx prisma migrate dev +``` + +For environments where the migrations already exist and should only be applied: + +```bash +npx prisma migrate deploy +``` + +This repository includes migrations for: + +- the initial `Resource` table and enum +- soft-delete columns: `is_archived` and `deleted_at` + +### 4. Seed the Database + +Seed sample data directly with the provided script: + +```bash +node prisma/seed.js +``` + +The seed script inserts a curated sample dataset into the `resources` table. + +### 5. Run the API + +Development: + +```bash +npm run dev +``` + +Production-like: + +```bash +npm run build +npm run start +``` + +Default URL: + +```bash +http://localhost:3000 +``` + +## API Contract + +### Health + +- `GET /health` + +Returns: + +```json +{ "ok": true } +``` + +This endpoint also performs a simple Prisma query to verify the database connection. + +### Root + +- `GET /` + +Returns a lightweight service summary: + +```json +{ + "ok": true, + "service": "resource-service", + "routes": ["/health", "/resources"] +} +``` + +### Create Resource + +- `POST /resources` + +Request body: + +```json +{ + "name": "My Resource", + "description": "Optional description", + "status": "active" +} +``` + +Rules: + +- `name` is required and trimmed in the service layer +- `status` defaults to `active` +- allowed values are `active` and `inactive` + +Response shape: + +```json +{ + "id": 1, + "name": "My Resource", + "description": "Optional description", + "status": "active", + "is_archived": false, + "deleted_at": null, + "created_at": "2026-05-21T00:00:00.000Z", + "updated_at": "2026-05-21T00:00:00.000Z" +} +``` + +### List Resources + +- `GET /resources` + +Query params: + +- `status=active|inactive` +- `name=` +- `limit=` default `20`, capped at `100` +- `offset=` default `0` + +Implementation notes: + +- archived rows are excluded from the list +- name filtering uses case-insensitive substring matching +- pagination is offset-based + +Example: + +```bash +curl "http://localhost:3000/resources?status=active&name=my&limit=10&offset=0" +``` + +Response shape: + +```json +{ + "data": [], + "count": 0 +} +``` + +### Get Resource Detail + +- `GET /resources/:id` + +Rules: + +- `id` must be a positive integer +- archived resources are not returned + +### Update Resource + +- `PATCH /resources/:id` + +Request body can include any of: + +```json +{ + "name": "Updated Name", + "description": "Updated desc", + "status": "inactive" +} +``` + +Rules: + +- at least one field must be provided +- empty names are rejected +- updates are executed atomically + +### Delete Resource + +- `DELETE /resources/:id` + +Default behavior is soft delete. + +Soft delete: + +- sets `is_archived` to `true` +- sets `deleted_at` to the current timestamp +- keeps the row in the database for audit/history + +Hard delete: + +```bash +curl -X DELETE "http://localhost:3000/resources/1?hard=true" +``` + +Hard delete permanently removes the row. + +### Batch Update + +- `PATCH /resources/batch` + +Request body: + +```json +{ + "ids": [1, 2, 3], + "patch": { + "status": "inactive" + } +} +``` + +Rules: + +- `ids` must be a non-empty array of positive integers +- `patch` must contain at least one valid field +- the operation is transactional + +Response shape: + +```json +{ + "updated": 3, + "data": [] +} +``` + +### Batch Delete + +- `DELETE /resources/batch` + +Request body: + +```json +{ + "ids": [4, 5, 6] +} +``` + +Soft delete is the default batch behavior. + +Hard batch delete: + +```json +{ + "ids": [4, 5, 6], + "hard": true +} +``` + +Response shape: + +```json +{ + "deleted": 3, + "ids": [4, 5, 6] +} +``` + +## Error Handling + +The app uses typed HTTP exceptions: + +- `400` for validation errors +- `404` when a resource is not found +- `500` for unexpected server errors + +In production, unexpected errors return a generic message. In non-production environments, the error details are included in the response for debugging. + +## Implementation Notes for Reviewers + +- Repository operations use Prisma transactions for atomic updates and batch operations. +- Prisma enum values are mapped into a local domain enum so the service layer stays independent from Prisma types. +- Soft delete is implemented in the repository and exposed through the API as the default delete mode. +- Hard delete is opt-in via `?hard=true` on single delete and `"hard": true` on batch delete. +- The response DTO uses snake_case audit fields: `is_archived`, `deleted_at`, `created_at`, and `updated_at`. +- The health check confirms both the app and database are reachable. + +## Example cURL Flow + +```bash +curl -X POST http://localhost:3000/resources \ + -H "Content-Type: application/json" \ + -d '{"name":"Alpha","description":"first","status":"active"}' + +curl http://localhost:3000/resources + +curl http://localhost:3000/resources/1 + +curl -X PATCH http://localhost:3000/resources/1 \ + -H "Content-Type: application/json" \ + -d '{"name":"Alpha Updated"}' + +curl -X DELETE http://localhost:3000/resources/1 + +curl -X DELETE "http://localhost:3000/resources/1?hard=true" +``` + +## Stop PostgreSQL + +```bash +docker compose down +``` diff --git a/problem_5/docker-compose.yml b/problem_5/docker-compose.yml new file mode 100644 index 0000000000..982df2ef9b --- /dev/null +++ b/problem_5/docker-compose.yml @@ -0,0 +1,18 @@ +version: "3.9" + +services: + postgres: + image: postgres:16 + container_name: problem4_postgres + restart: unless-stopped + environment: + POSTGRES_DB: resources_db + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + +volumes: + postgres_data: diff --git a/problem_5/package-lock.json b/problem_5/package-lock.json new file mode 100644 index 0000000000..6565baa2d1 --- /dev/null +++ b/problem_5/package-lock.json @@ -0,0 +1,1593 @@ +{ + "name": "problem-4-resource-service", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "problem-4-resource-service", + "version": "1.0.0", + "dependencies": { + "@prisma/client": "^5.22.0", + "dotenv": "^16.4.5", + "express": "^4.21.2" + }, + "devDependencies": { + "@types/express": "^4.17.21", + "@types/node": "^22.10.1", + "prisma": "^5.22.0", + "tsx": "^4.19.2", + "typescript": "^5.7.2" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@prisma/client": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.22.0.tgz", + "integrity": "sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.13" + }, + "peerDependencies": { + "prisma": "*" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + } + } + }, + "node_modules/@prisma/debug": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.22.0.tgz", + "integrity": "sha512-AUt44v3YJeggO2ZU5BkXI7M4hu9BF2zzH2iF2V5pyXT/lRTyWiElZ7It+bRH1EshoMRxHgpYg4VB6rCM+mG5jQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.22.0.tgz", + "integrity": "sha512-UNjfslWhAt06kVL3CjkuYpHAWSO6L4kDCVPegV6itt7nD1kSJavd3vhgAEhjglLJJKEdJ7oIqDJ+yHk6qO8gPA==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/fetch-engine": "5.22.0", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/engines-version": { + "version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2.tgz", + "integrity": "sha512-2PTmxFR2yHW/eB3uqWtcgRcgAbG1rwG9ZriSvQw+nnb7c4uCr3RAcGMb6/zfE88SKlC1Nj2ziUvc96Z379mHgQ==", + "devOptional": true, + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.22.0.tgz", + "integrity": "sha512-bkrD/Mc2fSvkQBV5EpoFcZ87AvOgDxbG99488a5cexp5Ccny+UM6MAe/UFkUC0wLYD9+9befNOqGiIJhhq+HbA==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0", + "@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2", + "@prisma/get-platform": "5.22.0" + } + }, + "node_modules/@prisma/get-platform": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.22.0.tgz", + "integrity": "sha512-pHhpQdr1UPFpt+zFfnPazhulaZYCUqeIcPpJViYoq9R+D/yw4fjE+CtnsnKzPYm0ddUbeXUzjGVGIRVgPDCk4Q==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "5.22.0" + } + }, + "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/express": { + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", + "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "^1" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.8", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", + "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", + "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/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "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": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/body-parser": { + "version": "1.20.5", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", + "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.15.1", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "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/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/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "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/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/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "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/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/esbuild": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" + } + }, + "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": "4.22.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", + "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.5", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.15.1", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "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": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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/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-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.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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/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": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "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/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "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/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/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": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/prisma": { + "version": "5.22.0", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-5.22.0.tgz", + "integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==", + "devOptional": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/engines": "5.22.0" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=16.13" + }, + "optionalDependencies": { + "fsevents": "2.3.3" + } + }, + "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/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": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "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/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/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/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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/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/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/tsx": { + "version": "4.22.3", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.3.tgz", + "integrity": "sha512-mdoNxBC/cSQObGGVQ5Bpn5i+yv7j68gk3Nfm3wFjcJg3Z0Mix9jzAFfP12prmm5eVGmDKtp0yyArrs0Q+8gZHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "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/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "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" + } + } + } +} diff --git a/problem_5/package.json b/problem_5/package.json new file mode 100644 index 0000000000..1781beb657 --- /dev/null +++ b/problem_5/package.json @@ -0,0 +1,25 @@ +{ + "name": "problem-4-resource-service", + "version": "1.0.0", + "private": true, + "type": "commonjs", + "scripts": { + "dev": "tsx watch src/server.ts", + "build": "tsc -p tsconfig.json", + "start": "node dist/server.js", + "prisma:generate": "prisma generate", + "prisma:push": "prisma db push" + }, + "dependencies": { + "@prisma/client": "^5.22.0", + "dotenv": "^16.4.5", + "express": "^4.21.2" + }, + "devDependencies": { + "@types/express": "^4.17.21", + "@types/node": "^22.10.1", + "prisma": "^5.22.0", + "tsx": "^4.19.2", + "typescript": "^5.7.2" + } +} diff --git a/problem_5/prisma/schema.prisma b/problem_5/prisma/schema.prisma new file mode 100644 index 0000000000..082139398c --- /dev/null +++ b/problem_5/prisma/schema.prisma @@ -0,0 +1,28 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "postgresql" + url = env("DATABASE_URL") +} + +enum ResourceStatus { + active + inactive +} + +model Resource { + id Int @id @default(autoincrement()) + name String + description String? + status ResourceStatus @default(active) + isArchived Boolean @default(false) @map("is_archived") + deletedAt DateTime? @map("deleted_at") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") + + @@map("resources") + @@index([status]) + @@index([isArchived]) +} diff --git a/problem_5/prisma/seed.js b/problem_5/prisma/seed.js new file mode 100644 index 0000000000..9d239f75dc --- /dev/null +++ b/problem_5/prisma/seed.js @@ -0,0 +1,48 @@ +const { PrismaClient } = require('@prisma/client'); + +const prisma = new PrismaClient(); + +async function main() { + const resources = [ + { name: 'Alpha', description: 'Seeded resource Alpha', status: 'active' }, + { name: 'Beta', description: 'Seeded resource Beta', status: 'inactive' }, + { name: 'Gamma', description: 'Seeded resource Gamma', status: 'active' }, + { name: 'Delta', description: 'Seeded resource Delta', status: 'active' }, + { name: 'Epsilon', description: 'Seeded resource Epsilon', status: 'inactive' }, + { name: 'Zeta', description: 'Seeded resource Zeta', status: 'active' }, + { name: 'Eta', description: 'Seeded resource Eta', status: 'inactive' }, + { name: 'Theta', description: 'Seeded resource Theta', status: 'active' }, + { name: 'Iota', description: 'Seeded resource Iota', status: 'active' }, + { name: 'Kappa', description: 'Seeded resource Kappa', status: 'inactive' }, + { name: 'Lambda', description: 'Seeded resource Lambda', status: 'active' }, + { name: 'Mu', description: 'Seeded resource Mu', status: 'inactive' }, + { name: 'Nu', description: 'Seeded resource Nu', status: 'active' }, + { name: 'Xi', description: 'Seeded resource Xi', status: 'active' }, + { name: 'Omicron', description: 'Seeded resource Omicron', status: 'inactive' }, + { name: 'Pi', description: 'Seeded resource Pi', status: 'active' }, + { name: 'Rho', description: 'Seeded resource Rho', status: 'inactive' }, + { name: 'Sigma', description: 'Seeded resource Sigma', status: 'active' }, + { name: 'Tau', description: 'Seeded resource Tau', status: 'active' }, + { name: 'Upsilon', description: 'Seeded resource Upsilon', status: 'inactive' }, + { name: 'Phi', description: 'Seeded resource Phi', status: 'active' }, + { name: 'Chi', description: 'Seeded resource Chi', status: 'inactive' }, + { name: 'Psi', description: 'Seeded resource Psi', status: 'active' }, + { name: 'Omega', description: 'Seeded resource Omega', status: 'active' }, + ]; + + const result = await prisma.resource.createMany({ + data: resources, + skipDuplicates: true + }); + + console.log(`Inserted ${result.count} resources (duplicates skipped).`); +} + +main() + .catch((e) => { + console.error('Seed failed:', e); + process.exitCode = 1; + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/problem_5/src/app.ts b/problem_5/src/app.ts new file mode 100644 index 0000000000..bc153ef085 --- /dev/null +++ b/problem_5/src/app.ts @@ -0,0 +1,42 @@ +import express, { NextFunction, Request, Response } from 'express'; +import { HttpException } from './exceptions/http.exception'; +import { prisma } from './prisma'; +import { createResourceRouter } from './routes/resource.routes'; + +export function createApp() { + const app = express(); + app.use(express.json()); + + app.get('/health', async (_req: Request, res: Response) => { + await prisma.$queryRaw`SELECT 1`; + res.json({ ok: true }); + }); + + // Root route: simple service summary + app.get('/', (_req: Request, res: Response) => { + return res.json({ ok: true, service: 'resource-service', routes: ['/health', '/resources'] }); + }); + + app.use('/resources', createResourceRouter()); + + app.use((error: unknown, _req: Request, res: Response, _next: NextFunction) => { + if (error instanceof HttpException) { + return res.status(error.statusCode).json({ + error: error.message, + ...(error.details ? { details: error.details } : {}) + }); + } + // log full error server-side but only return generic message to clients + // in production NODE_ENV should be 'production' + // show details only in non-production environments + // eslint-disable-next-line no-console + console.error('Unexpected error:', error); + const showDetails = process.env.NODE_ENV !== 'production'; + return res.status(500).json({ + error: 'internal server error', + ...(showDetails ? { details: String(error) } : {}) + }); + }); + + return app; +} diff --git a/problem_5/src/config.ts b/problem_5/src/config.ts new file mode 100644 index 0000000000..e09aaa48ed --- /dev/null +++ b/problem_5/src/config.ts @@ -0,0 +1,7 @@ +import dotenv from 'dotenv'; + +dotenv.config(); + +export const config = { + port: Number(process.env.PORT ?? 3000) +}; diff --git a/problem_5/src/controllers/resource.controller.ts b/problem_5/src/controllers/resource.controller.ts new file mode 100644 index 0000000000..10c472cdf9 --- /dev/null +++ b/problem_5/src/controllers/resource.controller.ts @@ -0,0 +1,91 @@ +import { NextFunction, Request, Response } from 'express'; +import { + BatchDeleteResourceDto, + BatchUpdateResourceDto, + CreateResourceDto, + ListResourcesQueryDto, + UpdateResourceDto, + toResourceResponseDto +} from '../dto/resource.dto'; +import { HttpException, InternalServerException } from '../exceptions/http.exception'; +import { ResourceService } from '../services/resource.service'; + +export class ResourceController { + constructor(private readonly service: ResourceService = new ResourceService()) {} + + create = async (req: Request<{}, {}, CreateResourceDto>, res: Response, next: NextFunction) => { + try { + const created = await this.service.create(req.body); + return res.status(201).json(toResourceResponseDto(created)); + } catch (error) { + return next(this.normalizeError(error, 'failed to create resource')); + } + }; + + list = async (req: Request<{}, {}, {}, ListResourcesQueryDto>, res: Response, next: NextFunction) => { + try { + const result = await this.service.list(req.query); + return res.json({ data: result.data.map(toResourceResponseDto), count: result.count }); + } catch (error) { + return next(this.normalizeError(error, 'failed to list resources')); + } + }; + + getById = async (req: Request<{ id: string }>, res: Response, next: NextFunction) => { + try { + const item = await this.service.getById(req.params.id); + return res.json(toResourceResponseDto(item)); + } catch (error) { + return next(this.normalizeError(error, 'failed to fetch resource')); + } + }; + + updateById = async (req: Request<{ id: string }, {}, UpdateResourceDto>, res: Response, next: NextFunction) => { + try { + const updated = await this.service.updateById(req.params.id, req.body); + return res.json(toResourceResponseDto(updated)); + } catch (error) { + return next(this.normalizeError(error, 'failed to update resource')); + } + }; + + deleteById = async (req: Request<{ id: string }>, res: Response, next: NextFunction) => { + try { + const hard = req.query.hard === 'true'; + if (hard) { + await this.service.hardDeleteById?.(req.params.id); + } else { + await this.service.deleteById(req.params.id); + } + return res.status(204).send(); + } catch (error) { + return next(this.normalizeError(error, 'failed to delete resource')); + } + }; + + batchUpdate = async (req: Request<{}, {}, BatchUpdateResourceDto>, res: Response, next: NextFunction) => { + try { + const updated = await this.service.batchUpdate(req.body); + return res.json({ updated: updated.length, data: updated.map(toResourceResponseDto) }); + } catch (error) { + return next(this.normalizeError(error, 'failed to batch update resources')); + } + }; + + batchDelete = async (req: Request<{}, {}, BatchDeleteResourceDto>, res: Response, next: NextFunction) => { + try { + const hard = (req.body as any).hard === true; + const deletedIds = hard ? await this.service.hardBatchDelete(req.body.ids) : await this.service.batchDelete(req.body.ids); + return res.json({ deleted: deletedIds.length, ids: deletedIds }); + } catch (error) { + return next(this.normalizeError(error, 'failed to batch delete resources')); + } + }; + + private normalizeError(error: unknown, fallbackMessage: string): HttpException { + if (error instanceof HttpException) { + return error; + } + return new InternalServerException(fallbackMessage, String(error)); + } +} diff --git a/problem_5/src/dto/resource.dto.ts b/problem_5/src/dto/resource.dto.ts new file mode 100644 index 0000000000..1bc56e0ce8 --- /dev/null +++ b/problem_5/src/dto/resource.dto.ts @@ -0,0 +1,59 @@ +import { ResourceStatusEnum } from '../enums/resource-status.enum'; +import { ResourceModel } from '../models/resource.model'; + +export interface CreateResourceDto { + name?: string; + description?: string | null; + status?: ResourceStatusEnum; +} + +export interface UpdateResourceDto { + name?: string; + description?: string | null; + status?: ResourceStatusEnum; +} + +export interface BatchUpdateResourceDto { + ids?: number[]; + patch?: UpdateResourceDto; +} + +export interface BatchDeleteResourceDto { + ids?: number[]; +} + +export interface ListResourcesQueryDto { + status?: string; + name?: string; + limit?: string; + offset?: string; +} + +export interface ResourceResponseDto { + id: number; + name: string; + description: string | null; + status: ResourceStatusEnum; + is_archived: boolean; + deleted_at: string | null; + created_at: string; + updated_at: string; +} + +export interface ListResourcesResponseDto { + data: ResourceResponseDto[]; + count: number; +} + +export function toResourceResponseDto(model: ResourceModel): ResourceResponseDto { + return { + id: model.id, + name: model.name, + description: model.description, + status: model.status, + is_archived: model.isArchived, + deleted_at: model.deletedAt ? model.deletedAt.toISOString() : null, + created_at: model.createdAt.toISOString(), + updated_at: model.updatedAt.toISOString() + }; +} diff --git a/problem_5/src/enums/resource-status.enum.ts b/problem_5/src/enums/resource-status.enum.ts new file mode 100644 index 0000000000..fd07d98e7c --- /dev/null +++ b/problem_5/src/enums/resource-status.enum.ts @@ -0,0 +1,4 @@ +export enum ResourceStatusEnum { + ACTIVE = 'active', + INACTIVE = 'inactive' +} diff --git a/problem_5/src/exceptions/http.exception.ts b/problem_5/src/exceptions/http.exception.ts new file mode 100644 index 0000000000..6d5defb025 --- /dev/null +++ b/problem_5/src/exceptions/http.exception.ts @@ -0,0 +1,28 @@ +export class HttpException extends Error { + public readonly statusCode: number; + public readonly details?: string; + + constructor(statusCode: number, message: string, details?: string) { + super(message); + this.statusCode = statusCode; + this.details = details; + } +} + +export class BadRequestException extends HttpException { + constructor(message: string) { + super(400, message); + } +} + +export class NotFoundException extends HttpException { + constructor(message: string) { + super(404, message); + } +} + +export class InternalServerException extends HttpException { + constructor(message: string, details?: string) { + super(500, message, details); + } +} diff --git a/problem_5/src/models/resource.model.ts b/problem_5/src/models/resource.model.ts new file mode 100644 index 0000000000..4bb1d70f7c --- /dev/null +++ b/problem_5/src/models/resource.model.ts @@ -0,0 +1,19 @@ +import { ResourceStatusEnum } from '../enums/resource-status.enum'; + +export interface ResourceModel { + id: number; + name: string; + description: string | null; + status: ResourceStatusEnum; + isArchived: boolean; + deletedAt: Date | null; + createdAt: Date; + updatedAt: Date; +} + +export interface ListResourceFiltersModel { + status?: ResourceStatusEnum; + name?: string; + limit: number; + offset: number; +} diff --git a/problem_5/src/prisma.ts b/problem_5/src/prisma.ts new file mode 100644 index 0000000000..9b6c4ce30b --- /dev/null +++ b/problem_5/src/prisma.ts @@ -0,0 +1,3 @@ +import { PrismaClient } from '@prisma/client'; + +export const prisma = new PrismaClient(); diff --git a/problem_5/src/repositories/resource.repository.ts b/problem_5/src/repositories/resource.repository.ts new file mode 100644 index 0000000000..ff70e50dc2 --- /dev/null +++ b/problem_5/src/repositories/resource.repository.ts @@ -0,0 +1,189 @@ +import { prisma } from '../prisma'; +import { ResourceStatusEnum } from '../enums/resource-status.enum'; +import { ListResourceFiltersModel, ResourceModel } from '../models/resource.model'; + +function toDomainStatus(status: string): ResourceStatusEnum { + return status === ResourceStatusEnum.INACTIVE ? ResourceStatusEnum.INACTIVE : ResourceStatusEnum.ACTIVE; +} + +function toDomainModel(input: { + id: number; + name: string; + description: string | null; + status: string; + isArchived: boolean; + deletedAt: Date | null; + createdAt: Date; + updatedAt: Date; +}): ResourceModel { + return { + id: input.id, + name: input.name, + description: input.description, + status: toDomainStatus(input.status), + isArchived: input.isArchived, + deletedAt: input.deletedAt ?? null, + createdAt: input.createdAt, + updatedAt: input.updatedAt + }; +} + +function toPersistenceStatus(status: ResourceStatusEnum): 'active' | 'inactive' { + return status === ResourceStatusEnum.INACTIVE ? 'inactive' : 'active'; +} + +export class ResourceRepository { + async create(input: { name: string; description: string | null; status: 'active' | 'inactive' }): Promise { + const created = await prisma.resource.create({ + data: { + name: input.name, + description: input.description, + status: input.status + } + }); + + return toDomainModel({ + ...created, + isArchived: created.isArchived ?? false, + deletedAt: created.deletedAt ?? null + }); + } + + async list(filters: ListResourceFiltersModel): Promise<{ data: ResourceModel[]; count: number }> { + const where: any = { + isArchived: false, + ...(filters.status ? { status: filters.status } : {}), + ...(filters.name ? { name: { contains: filters.name, mode: 'insensitive' as const } } : {}) + }; + + const [data, count] = await Promise.all([ + prisma.resource.findMany({ + where, + orderBy: { id: 'desc' }, + take: filters.limit, + skip: filters.offset + }), + prisma.resource.count({ where }) + ]); + + return { data: data.map((d: any) => toDomainModel({ + id: d.id, + name: d.name, + description: d.description, + status: d.status, + isArchived: d.isArchived ?? false, + deletedAt: d.deletedAt ?? null, + createdAt: d.createdAt, + updatedAt: d.updatedAt + })), count }; + } + + async findById(id: number): Promise { + const found: any = await prisma.resource.findFirst({ where: { id, isArchived: false } }); + return found ? toDomainModel({ + id: found.id, + name: found.name, + description: found.description, + status: found.status, + isArchived: found.isArchived ?? false, + deletedAt: found.deletedAt ?? null, + createdAt: found.createdAt, + updatedAt: found.updatedAt + }) : null; + } + + async updateById( + id: number, + patch: Partial> + ): Promise { + try { + const updated = await prisma.$transaction(async (tx) => { + const row = await tx.resource.update({ + where: { id }, + data: { + ...(patch.name !== undefined ? { name: patch.name } : {}), + ...(patch.description !== undefined ? { description: patch.description } : {}), + ...(patch.status !== undefined ? { status: toPersistenceStatus(patch.status) } : {}) + } + }); + return row; + }); + + return toDomainModel({ + id: updated.id, + name: updated.name, + description: updated.description, + status: updated.status, + isArchived: updated.isArchived ?? false, + deletedAt: updated.deletedAt ?? null, + createdAt: updated.createdAt, + updatedAt: updated.updatedAt + }); + } catch (err: any) { + if (err?.code === 'P2025') return null; + throw err; + } + } + + async deleteById(id: number): Promise { + // soft-delete by default: set is_archived and deleted_at + const res = await prisma.resource.updateMany({ where: { id, isArchived: false }, data: { isArchived: true, deletedAt: new Date() } }); + return res.count > 0; + } + + // permanent hard delete + async hardDeleteById(id: number): Promise { + const rows: Array<{ id: number }> = await prisma.resource.findMany({ where: { id: id }, select: { id: true } }); + if (rows.length === 0) return []; + await prisma.resource.deleteMany({ where: { id } }); + return rows.map((r) => r.id); + } + + // hard delete many + async hardDeleteMany(ids: number[]): Promise { + const rows: Array<{ id: number }> = await prisma.resource.findMany({ where: { id: { in: ids } }, select: { id: true } }); + if (rows.length === 0) return []; + await prisma.resource.deleteMany({ where: { id: { in: ids } } }); + return rows.map((r) => r.id); + } + + async batchUpdate( + ids: number[], + patch: Partial> + ): Promise { + // make atomic: update and return rows in a transaction + await prisma.$transaction(async (tx) => { + await tx.resource.updateMany({ + where: { id: { in: ids } }, + data: { + ...(patch.name !== undefined ? { name: patch.name } : {}), + ...(patch.description !== undefined ? { description: patch.description } : {}), + ...(patch.status !== undefined ? { status: toPersistenceStatus(patch.status) } : {}) + } + }); + }); + + const rows: any[] = await prisma.resource.findMany({ where: { id: { in: ids } } }); + return rows.map((d) => toDomainModel({ + id: d.id, + name: d.name, + description: d.description, + status: d.status, + isArchived: d.isArchived ?? false, + deletedAt: d.deletedAt ?? null, + createdAt: d.createdAt, + updatedAt: d.updatedAt + })); + } + + async batchDelete(ids: number[]): Promise { + // soft-delete in transaction and return ids that were archived + const result = await prisma.$transaction(async (tx) => { + const toReturn = await tx.resource.findMany({ where: { id: { in: ids }, isArchived: false }, select: { id: true } }); + await tx.resource.updateMany({ where: { id: { in: ids } }, data: { isArchived: true, deletedAt: new Date() } }); + return toReturn; + }); + + return result.map((r: any) => r.id); + } +} diff --git a/problem_5/src/routes/resource.routes.ts b/problem_5/src/routes/resource.routes.ts new file mode 100644 index 0000000000..c38694083b --- /dev/null +++ b/problem_5/src/routes/resource.routes.ts @@ -0,0 +1,17 @@ +import { Router } from 'express'; +import { ResourceController } from '../controllers/resource.controller'; + +export function createResourceRouter(): Router { + const router = Router(); + const controller = new ResourceController(); + + router.post('/', controller.create); + router.get('/', controller.list); + router.get('/:id', controller.getById); + router.patch('/batch', controller.batchUpdate); + router.delete('/batch', controller.batchDelete); + router.patch('/:id', controller.updateById); + router.delete('/:id', controller.deleteById); + + return router; +} diff --git a/problem_5/src/server.ts b/problem_5/src/server.ts new file mode 100644 index 0000000000..4c7622e5dd --- /dev/null +++ b/problem_5/src/server.ts @@ -0,0 +1,28 @@ +import { createApp } from './app'; +import { config } from './config'; +import { prisma } from './prisma'; + +const app = createApp(); + +async function start(): Promise { + await prisma.$connect(); + app.listen(config.port, () => { + // Simple startup log for local development. + console.log(`Resource service listening on port ${config.port}`); + }); +} + +start().catch((error) => { + console.error('Failed to start server:', error); + process.exit(1); +}); + +process.on('SIGINT', async () => { + await prisma.$disconnect(); + process.exit(0); +}); + +process.on('SIGTERM', async () => { + await prisma.$disconnect(); + process.exit(0); +}); diff --git a/problem_5/src/services/resource.service.ts b/problem_5/src/services/resource.service.ts new file mode 100644 index 0000000000..4ca0e56a86 --- /dev/null +++ b/problem_5/src/services/resource.service.ts @@ -0,0 +1,151 @@ +import { + BatchUpdateResourceDto, + CreateResourceDto, + ListResourcesQueryDto, + UpdateResourceDto +} from '../dto/resource.dto'; +import { ResourceStatusEnum } from '../enums/resource-status.enum'; +import { BadRequestException, NotFoundException } from '../exceptions/http.exception'; +import { ListResourceFiltersModel, ResourceModel } from '../models/resource.model'; +import { ResourceRepository } from '../repositories/resource.repository'; + +function parsePositiveInt(value: string): number { + const num = Number(value); + if (!Number.isInteger(num) || num <= 0) { + throw new BadRequestException('id must be a positive integer'); + } + return num; +} + +function parseIds(ids?: number[]): number[] { + if (!Array.isArray(ids) || ids.length === 0) { + throw new BadRequestException('ids must be a non-empty array'); + } + const clean = ids.filter((id) => Number.isInteger(id) && id > 0); + if (clean.length !== ids.length) { + throw new BadRequestException('all ids must be positive integers'); + } + return clean; +} + +function parseStatus(status?: string): ResourceStatusEnum | undefined { + if (status === undefined) return undefined; + if (status !== ResourceStatusEnum.ACTIVE && status !== ResourceStatusEnum.INACTIVE) { + throw new BadRequestException('status must be active or inactive'); + } + return status; +} + +export class ResourceService { + constructor(private readonly repository: ResourceRepository = new ResourceRepository()) {} + + async create(dto: CreateResourceDto): Promise { + const name = dto.name?.trim(); + if (!name) { + throw new BadRequestException('name is required'); + } + + const status = parseStatus(dto.status) ?? ResourceStatusEnum.ACTIVE; + return this.repository.create({ + name, + description: dto.description ?? null, + status + }); + } + + async list(query: ListResourcesQueryDto): Promise<{ data: ResourceModel[]; count: number }> { + const status = parseStatus(query.status); + const filters: ListResourceFiltersModel = { + status, + name: query.name, + limit: Math.min(Math.max(Number(query.limit ?? 20), 1), 100), + offset: Math.max(Number(query.offset ?? 0), 0) + }; + + return this.repository.list(filters); + } + + async getById(rawId: string): Promise { + const id = parsePositiveInt(rawId); + const item = await this.repository.findById(id); + if (!item) { + throw new NotFoundException('resource not found'); + } + return item; + } + + async updateById(rawId: string, dto: UpdateResourceDto): Promise { + const id = parsePositiveInt(rawId); + const patch = { + ...(dto.name !== undefined ? { name: dto.name.trim() } : {}), + ...(dto.description !== undefined ? { description: dto.description } : {}), + ...(dto.status !== undefined ? { status: parseStatus(dto.status) } : {}) + }; + + if (dto.name !== undefined && !dto.name.trim()) { + throw new BadRequestException('name cannot be empty'); + } + + const hasAnyPatch = Object.keys(patch).length > 0; + if (!hasAnyPatch) { + throw new BadRequestException('provide at least one field to update'); + } + + const updated = await this.repository.updateById(id, patch); + if (!updated) { + throw new NotFoundException('resource not found'); + } + return updated; + } + + async deleteById(rawId: string): Promise { + const id = parsePositiveInt(rawId); + const deleted = await this.repository.deleteById(id); + if (!deleted) { + throw new NotFoundException('resource not found'); + } + } + + // hard delete (permanent) + async hardDeleteById(rawId: string): Promise { + const id = parsePositiveInt(rawId); + const deletedIds = await this.repository.hardDeleteById?.(id as any as number) ?? []; + if (!deletedIds || deletedIds.length === 0) { + throw new NotFoundException('resource not found'); + } + } + + async batchUpdate(dto: BatchUpdateResourceDto): Promise { + const ids = parseIds(dto.ids); + const patch = dto.patch; + if (!patch || Object.keys(patch).length === 0) { + throw new BadRequestException('patch is required'); + } + + if (patch.name !== undefined && !patch.name.trim()) { + throw new BadRequestException('name cannot be empty'); + } + + const sanitizedPatch = { + ...(patch.name !== undefined ? { name: patch.name.trim() } : {}), + ...(patch.description !== undefined ? { description: patch.description } : {}), + ...(patch.status !== undefined ? { status: parseStatus(patch.status) } : {}) + }; + + if (Object.keys(sanitizedPatch).length === 0) { + throw new BadRequestException('patch has no valid fields to update'); + } + + return this.repository.batchUpdate(ids, sanitizedPatch); + } + + async batchDelete(ids?: number[]): Promise { + const cleanIds = parseIds(ids); + return this.repository.batchDelete(cleanIds); + } + + async hardBatchDelete(ids?: number[]): Promise { + const cleanIds = parseIds(ids); + return this.repository.hardDeleteMany(cleanIds); + } +} diff --git a/problem_5/task.md b/problem_5/task.md new file mode 100644 index 0000000000..e5a273e8f0 --- /dev/null +++ b/problem_5/task.md @@ -0,0 +1,12 @@ +# Task + +Develop a backend server with ExpressJS. You are required to build a set of CRUD interface that allow a user to interact with the service. You are required to use TypeScript for this task. + +1. Interface functionalities: + 1. Create a resource. + 2. List resources with basic filters. + 3. Get details of a resource. + 4. Update resource details. + 5. Delete a resource. +2. You should connect your backend service with a simple database for data persistence. +3. Provide [`README.md`](http://README.md) for the configuration and the way to run application. \ No newline at end of file diff --git a/problem_5/tsconfig.json b/problem_5/tsconfig.json new file mode 100644 index 0000000000..ef726d26a7 --- /dev/null +++ b/problem_5/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "CommonJS", + "lib": ["ES2020"], + "rootDir": "src", + "outDir": "dist", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src"] +} diff --git a/problem_6/README.md b/problem_6/README.md new file mode 100644 index 0000000000..a0d1de41a1 --- /dev/null +++ b/problem_6/README.md @@ -0,0 +1,82 @@ +# Technical Specification: Real-Time Scoreboard Module + +--- + +## 1. System Overview & Core Requirements + +This specification outlines the architecture, execution plan, and data contracts for the Real-Time Scoreboard Module. The system is designed to ingest high-frequency user actions, process score increments securely, and broadcast updated rankings to connected clients with sub-second latency. + +### System Requirements: + +* **Top 10 Global Scoreboard:** Maintain an in-memory sorted cache to serve the top 10 user scores instantly. +* **Real-Time Fan-Out:** Push leaderboard modifications to active clients via a persistent, low-overhead streaming mechanism. +* **Action-Driven Ingestion:** Expose an API endpoint to ingest generic user action events and map them to point values on the server side. +* **High Ingestion Responsiveness:** Decouple the synchronous request ingestion path from downstream cache projection and real-time streaming components. +* **Security & Anti-Cheat Controls:** Enforce token authentication, eliminate client-side score valuation, and implement replay attack mitigations. + +--- + +## 2. Architectural Topology & Execution Flow + +The module implements an **Event-Driven Architecture leveraging the Transactional Outbox Pattern**. This decouples the transactional system of record (PostgreSQL) from the volatile read model (Redis), ensuring data integrity without creating write bottlenecks. + +### 2.1 System Component Diagram + +* Component diagram (PNG): [architectural_diagram.png](architectural_diagram.png) + +### 2.2 Sequence of Execution + +* Sequence diagram (PNG): [sequence_diagram.png](sequence_diagram.png) + +--- + +## 3. Interface Contracts (OpenAPI 3.0.1) + +The following schema defines the strict API surface area for the scoreboard module. All incoming controllers, request interceptors, and error handlers must adhere directly to this specification. + +* OpenAPI spec (YAML): [openapi.yaml](openapi.yaml) + +--- + +## 4. Implementation Details & Execution Plan + +This section outlines the actionable workflows the development team will follow to build, test, and integrate the system, ensuring seamless execution from local development to production. + +* **Development Workflow:** Work will be executed in agile sprints, isolating feature development. Backend API development, database schema migrations, and frontend integration will run concurrently, strictly adhering to the API contracts defined above. +* **Local Containerization:** Utilize Docker to spin up local environments containing the application, PostgreSQL, and Redis. This ensures environment consistency across the team and eliminates integration discrepancies. +* **Version Control & CI/CD Pipelines:** The team will enforce a strict Git branching strategy for all new features. All pull requests must pass peer review and trigger the CI/CD pipelines to streamline backend delivery. Automated unit and integration tests must pass before any code is merged into the main branch. + +--- + +## 5. Deployment Plan & Rollout Strategy + +This section defines the infrastructure strategy and the step-by-step process for safely transitioning the application into live environments. + +* **Cloud Infrastructure:** The system will utilize a distributed design with AWS, provisioned via Infrastructure as Code (IaC) to guarantee repeatable and identical environment setups across Development, Staging, and Production. +* **Container Orchestration:** The backend module will utilize Kubernetes deployments. This allows for automated scaling of the application pods to handle high-frequency ingestion spikes and maintains high availability for the real-time SSE connections. +* **Deployment Methodology:** The transition to production will execute via a Rolling Update strategy within the Kubernetes cluster, guaranteeing zero downtime. The load balancer will only route live traffic to newly deployed pods once they pass all automated readiness and liveness health checks. +* **Monitoring & Automated Rollback:** System metrics (CPU usage, memory consumption, Redis latency, and API error rates) will be actively monitored. If error thresholds are exceeded during a rollout, the CI/CD pipeline will automatically trigger a rollback to the last stable release. + +--- + +## 6. Architectural Rationale & Technical Trade-offs + +### Transactional Outbox vs. Immediate Cache Writes + +Writing directly to a database and a cache sequentially within an identical application thread presents a critical failure mode: if the cache layer experiences a transient network drop after the database commit, the read and write models diverge indefinitely. By executing the business state mutation and appending an event log entry to an `outbox_events` table within a single atomic ACID transaction, eventual consistency between PostgreSQL and Redis is structurally guaranteed. + +### Server-Sent Events (SSE) vs. WebSockets + +WebSockets introduce a full-duplex communication channel over a persistent TCP connection, requiring complex state management, custom heartbeat framing, and high memory overhead per node. Because the leaderboard interface only requires unidirectional server-to-client streaming, Server-Sent Events (SSE) was selected. SSE operates natively over standard HTTP/1.1 or HTTP/2, utilizes standard text streaming protocols, and delegates reconnection logic entirely to the browser's native capabilities. + +### Redis Pub/Sub for Horizontal Scalability + +In a horizontally scaled application cluster, clients are distributed across multiple independent application nodes. When a node processes a score modification, it must notify clients connected to all other nodes. Utilizing a central Redis Pub/Sub topology allows application nodes to subscribe to a unified event backbone, executing message fan-out to locally held client connection pools seamlessly. + +--- + +## 7. System Benefits & Quality Attributes + +* **Zero-Trust Ingestion:** The client payload contains zero information regarding numerical point adjustments. The client submits a verifiable action identifier; the server maps the token against an internal config dictionary to dictate score deltas, nullifying payload tampering vectors. +* **System Resilience:** If the Redis speed layer faces an outage, the PostgreSQL transaction log remains unaffected. Once the cache recovers, the outbox worker automatically drains the pending transaction backlog, establishing a self-healing operational boundary. +* **Predictable Database I/O:** Client ranking lookups and Top 10 lists bypass relational database indexing entirely, executing against an in-memory structure bounded at $O(\log N)$ complexity. \ No newline at end of file diff --git a/problem_6/architectural_diagram_v2.png b/problem_6/architectural_diagram_v2.png new file mode 100644 index 0000000000..eb785f33ed Binary files /dev/null and b/problem_6/architectural_diagram_v2.png differ diff --git a/problem_6/openapi.yaml b/problem_6/openapi.yaml new file mode 100644 index 0000000000..970eb3c638 --- /dev/null +++ b/problem_6/openapi.yaml @@ -0,0 +1,160 @@ +openapi: 3.0.1 +info: + title: Real-Time Scoreboard API + version: 1.0.0 +paths: + /api/v1/scores/increment: + post: + summary: Submit a completed action to calculate and increment score + security: + - cognitoJwt: [] + parameters: + - in: header + name: Idempotency-Key + required: true + schema: + type: string + format: uuid + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + actionType: + type: string + example: COMPLETED_LEVEL_5 + actionReferenceId: + type: string + description: Client-side unique transaction/action tracking reference. + clientTimestamp: + type: string + format: date-time + required: + - actionType + responses: + '202': + description: Action accepted. Leaderboard updates will stream asynchronously via SSE. + content: + application/json: + schema: + type: object + properties: + userId: + type: string + eventId: + type: string + format: uuid + projectionStatus: + type: string + example: pending + '400': + description: Invalid request payload or unrecognized actionType + '401': + description: Unauthorized or missing JWT access token + '409': + description: Duplicate request detected via Idempotency-Key + '503': + description: Database or system unavailable + + /api/v1/leaderboard: + get: + summary: Retrieve leaderboard entries with query-driven pagination + parameters: + - in: query + name: limit + schema: + type: integer + default: 10 + minimum: 1 + maximum: 100 + - in: query + name: offset + schema: + type: integer + default: 0 + minimum: 0 + - in: query + name: scope + schema: + type: string + default: global + responses: + '200': + description: Success + content: + application/json: + schema: + type: object + properties: + updatedAt: + type: string + format: date-time + items: + type: array + items: + type: object + properties: + userId: + type: string + score: + type: integer + rank: + type: integer + + /api/v1/leaderboard/me: + get: + summary: Get authenticated user's individual score, rank, and relative neighbors + security: + - cognitoJwt: [] + responses: + '200': + description: Success + content: + application/json: + schema: + type: object + properties: + userId: + type: string + score: + type: integer + rank: + type: integer + neighbors: + type: array + items: + type: object + properties: + userId: + type: string + score: + type: integer + rank: + type: integer + '401': + description: Unauthorized + + /api/v1/leaderboard/stream: + get: + summary: Open a persistent Server-Sent Events stream for real-time leaderboard updates + security: + - cognitoJwt: [] + responses: + '200': + description: Event stream established. Pushes 'leaderboard.initial' and 'leaderboard.updated' events. + content: + text/event-stream: + schema: + type: string + '401': + description: Unauthorized + + +components: + securitySchemes: + cognitoJwt: + type: http + scheme: bearer + bearerFormat: JWT \ No newline at end of file diff --git a/problem_6/sequence_diagram.png b/problem_6/sequence_diagram.png new file mode 100644 index 0000000000..5166779c0f Binary files /dev/null and b/problem_6/sequence_diagram.png differ diff --git a/problem_6/task.md b/problem_6/task.md new file mode 100644 index 0000000000..106a039a66 --- /dev/null +++ b/problem_6/task.md @@ -0,0 +1,16 @@ +# Task + +Write the specification for a software module on the API service (backend application server). + +1. Create a documentation for this module on a `README.md` file. +2. Create a diagram to illustrate the flow of execution. +3. Add additional comments for improvement you may have in the documentation. +4. Your specification will be given to a backend engineering team to implement. + +### Software Requirements + +1. We have a website with a score board, which shows the top 10 user’s scores. +2. We want live update of the score board. +3. User can do an action (which we do not need to care what the action is), completing this action will increase the user’s score. +4. Upon completion the action will dispatch an API call to the application server to update the score. +5. We want to prevent malicious users from increasing scores without authorisation. \ No newline at end of file diff --git a/readme.md b/readme.md deleted file mode 100644 index 1ff4bc95b4..0000000000 --- a/readme.md +++ /dev/null @@ -1,10 +0,0 @@ -# 99Tech Code Challenge #1 # - -Note that if you fork this repository, your responses may be publicly linked to this repo. -Please submit your application along with the solutions attached or linked. - -It is important that you minimally attempt the problems, even if you do not arrive at a working solution. - -## Submission ## -You can either provide a link to an online repository, attach the solution in your application, or whichever method you prefer. -We're cool as long as we can view your solution without any pain. diff --git a/src/problem1/.keep b/src/problem1/.keep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/problem2/index.html b/src/problem2/index.html deleted file mode 100644 index 4058a68bff..0000000000 --- a/src/problem2/index.html +++ /dev/null @@ -1,27 +0,0 @@ - - - - - Fancy Form - - - - - - - - -
-
Swap
- - - - - - - -
- - - - diff --git a/src/problem2/script.js b/src/problem2/script.js deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/problem2/style.css b/src/problem2/style.css deleted file mode 100644 index 915af91c72..0000000000 --- a/src/problem2/style.css +++ /dev/null @@ -1,8 +0,0 @@ -body { - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; - min-width: 360px; - font-family: Arial, Helvetica, sans-serif; -} diff --git a/src/problem3/.keep b/src/problem3/.keep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/problem4/.keep b/src/problem4/.keep deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/problem5/.keep b/src/problem5/.keep deleted file mode 100644 index e69de29bb2..0000000000