diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 00000000..97cbbad4 --- /dev/null +++ b/backend/README.md @@ -0,0 +1,208 @@ +# Todo List REST API (Backend Assessment) + +A lightweight, clean, and type-safe REST API for managing a Todo list, built with Node.js, Express, and TypeScript. The application uses an in-memory data store and follows a layered design to demonstrate structured, maintainable backend architecture. + +--- + +## Tech Stack + +- **Runtime & Language**: Node.js, TypeScript (ESM, `NodeNext` module/resolution) +- **Web Framework**: Express.js +- **Development Tooling**: `tsx` (for hot reloading / dev runner), `tsc` (for build compilation) +- **Database/Persistence**: In-memory storage (plain array, no DB/ORM) + +--- + +## Folder Structure + +```text +src/ +├── common/ # Shared utilities, errors, and middlewares +│ ├── error/ # Custom Domain Errors +│ │ └── domain.error.ts +│ └── middleware/ # Global request middlewares +│ └── error.middleware.ts +├── controller/ # API routing and HTTP request handling +│ ├── app.router.ts # Mounts resource routes to the Express app +│ └── todo.controller.ts # Express Router containing HTTP routes & handlers +├── model/ # Data representations and Transfer Objects +│ └── todo.model.ts # Todo interfaces and DTOs (CreateTodoDTO, UpdateTodoDTO) +├── repository/ # Storage interface and in-memory engine +│ └── todo.repository.ts # ITodoRepository interface & InMemoryTodoRepository implementation +├── usecase/ # Core business logic and validation rules +│ └── todo.usecase.ts # TodoUsecase containing operations orchestration +└── index.ts # Express application entrypoint +``` + +--- + +## Architecture & Request Flow + +The application is structured using a **Layered Architecture**, enforcing a clean separation of concerns and a unidirectional dependency flow: + +$$\text{Client Request} \longrightarrow \text{Routes / Controller} \longrightarrow \text{Usecase} \longrightarrow \text{Repository} \longrightarrow \text{In-Memory Store}$$ + +### Architectural Roles: +1. **Controller Layer (`todo.controller.ts`)**: Handles HTTP routing, request parsing, and response formatting. It is merged with routes in a single file per resource, directly exporting an Express `Router` to reduce boilerplate while keeping route configuration close to its handlers. It delegates all business decisions to the Usecase layer. +2. **Usecase Layer (`todo.usecase.ts`)**: Orchestrates business rules, handles input schema/type validations, and manages workflow execution. It depends exclusively on the `ITodoRepository` interface, remaining completely decoupled from the data storage engine. +3. **Repository Layer (`todo.repository.ts`)**: Encapsulates raw data persistence mechanisms (in-memory storage array). + +### Storage Decoupling (Interface Design): +The repository is defined using an interface (`ITodoRepository`) to allow the storage backend to be easily swapped (e.g., to SQL, MongoDB, or an external API) without modifying any business logic in the Usecase layer. Conversely, the Usecase and Controller do not use interfaces since there is no architectural need to swap their implementations within this project's scope. + +--- + +## Setup & Running Instructions + +### 1. Install Dependencies +Install the required development and runtime packages: +```bash +npm install +``` + +### 2. Start the Development Server +Runs the application with live reload enabled using `tsx watch`: +```bash +npm run dev +``` +The server will start running on port `4000` (`http://localhost:4000`). + +### 3. Build & Run in Production +Compile the TypeScript code to JavaScript: +```bash +npm run build +``` + +Start the compiled application: +```bash +npm start +``` + +--- + +## 📋 API Endpoints & Contract Documentation + +### Todo Entity Shape +Every Todo object conforms to the following schema: +```json +{ + "id": "03d96607-5294-411b-9eaa-2dd470114396", + "title": "Cleaned Title", + "completed": true, + "createdAt": "2026-07-03T13:16:32.352Z" +} +``` + +--- + +### 1. List All Todos +- **Method / Path**: `GET /api/todos` +- **Response (200 OK)**: + ```json + [ + { + "id": "5b496db7-e383-498e-9570-1e2121b796e9", + "title": "Buy groceries", + "completed": false, + "createdAt": "2026-07-03T13:05:54.456Z" + } + ] + ``` + +### 2. Get Single Todo +- **Method / Path**: `GET /api/todos/:id` +- **Response (200 OK)**: + ```json + { + "id": "5b496db7-e383-498e-9570-1e2121b796e9", + "title": "Buy groceries", + "completed": false, + "createdAt": "2026-07-03T13:05:54.456Z" + } + ``` +- **Response (404 Not Found)**: + ```json + { + "message": "Todo not found" + } + ``` + +### 3. Create Todo +- **Method / Path**: `POST /api/todos` +- **Request Body**: + ```json + { + "title": "Buy milk" + } + ``` +- **Response (201 Created)**: + ```json + { + "id": "97e411b0-9eaa-411b-9eaa-2dd470114396", + "title": "Buy milk", + "completed": false, + "createdAt": "2026-07-03T13:18:22.102Z" + } + ``` +- **Response (400 Bad Request)**: + ```json + { + "message": "Title is required" + } + ``` + +### 4. Update Todo (Partial Update) +- **Method / Path**: `PUT /api/todos/:id` +- **Request Body** *(all fields optional)*: + ```json + { + "title": "Buy organic milk", + "completed": true + } + ``` +- **Response (200 OK)**: + ```json + { + "id": "97e411b0-9eaa-411b-9eaa-2dd470114396", + "title": "Buy organic milk", + "completed": true, + "createdAt": "2026-07-03T13:18:22.102Z" + } + ``` +- **Response (400 Bad Request - Invalid Title Type)**: + ```json + { + "message": "Title must be a non-empty string" + } + ``` +- **Response (400 Bad Request - Invalid Completed Type)**: + ```json + { + "message": "Completed must be a boolean" + } + ``` +- **Response (404 Not Found)**: + ```json + { + "message": "Todo not found" + } + ``` + +### 5. Delete Todo +- **Method / Path**: `DELETE /api/todos/:id` +- **Response (204 No Content)**: *(Empty body)* +- **Response (404 Not Found)**: + ```json + { + "message": "Todo not found" + } + ``` + +--- + +## Design Decisions & Key Callouts + +1. **Partial-Update Semantics for PUT**: The `PUT` endpoint is implemented with partial-update (PATCH-like) semantics instead of a strict full replacement. Clients can update only a subset of the fields (e.g., toggling `completed` without re-sending the unchanged `title`) without accidentally resetting unspecified properties. +2. **In-Memory Storage Lifetime**: The data layer uses an in-memory array (`private todos: Todo[] = []`). Therefore, the state is temporary and resets whenever the Node.js application process restarts. +3. **Custom Global Error Handling**: Operational/business exceptions are modeled as a custom `DomainError` carrying HTTP status codes. Express interceptors handle these errors gracefully, returning structured JSON responses. Uncaught syntax errors (such as malformed JSON payloads) are also intercepted to prevent raw stack traces from leaking, failing safe with clean `400` or `500` status codes. +4. **Input Sanitization**: Text inputs like the todo `title` are automatically trimmed of leading and trailing whitespaces during both creation and update logic. diff --git a/backend/index.js b/backend/index.js deleted file mode 100644 index ff6a6a3b..00000000 --- a/backend/index.js +++ /dev/null @@ -1,13 +0,0 @@ -const express = require("express"); -const app = express(); -const PORT = process.env.PORT || 4000; - -// Basic route -app.get("/", (req, res) => { - res.send("Hello from Express!"); -}); - -// Start server -app.listen(PORT, () => { - console.log(`Backend is running on http://localhost:${PORT}`); -}); diff --git a/backend/package.json b/backend/package.json index c85981fa..7b8a03f5 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,10 +1,19 @@ { "name": "backend", "version": "1.0.0", + "type": "module", "scripts": { - "start": "node index.js" + "dev": "tsx watch src/index.ts", + "build": "tsc", + "start": "node dist/index.js" }, "dependencies": { "express": "^4.18.2" + }, + "devDependencies": { + "@types/express": "^5.0.6", + "@types/node": "^20.17.30", + "tsx": "^4.22.5", + "typescript": "^5.8.3" } } diff --git a/backend/src/common/error/domain.error.ts b/backend/src/common/error/domain.error.ts new file mode 100644 index 00000000..dbda27a9 --- /dev/null +++ b/backend/src/common/error/domain.error.ts @@ -0,0 +1,9 @@ +export class DomainError extends Error { + public readonly statusCode: number; + + constructor(statusCode: number, message: string) { + super(message); + this.statusCode = statusCode; + Error.captureStackTrace(this, this.constructor); + } +} diff --git a/backend/src/common/middleware/error.middleware.ts b/backend/src/common/middleware/error.middleware.ts new file mode 100644 index 00000000..086f7602 --- /dev/null +++ b/backend/src/common/middleware/error.middleware.ts @@ -0,0 +1,14 @@ +import type { Request, Response, NextFunction } from 'express'; +import { DomainError } from '../error/domain.error.js'; + +export const errorHandler = (error: unknown, req: Request, res: Response, next: NextFunction) => { + if (error instanceof DomainError) { + return res.status(error.statusCode).json({ message: error.message }); + } + if (error instanceof SyntaxError && 'status' in error && error.status === 400) { + return res.status(400).json({ message: 'Invalid JSON payload' }); + } + console.error(error); + res.status(500).json({ message: 'Internal server error' }); +} + diff --git a/backend/src/controller/app.router.ts b/backend/src/controller/app.router.ts new file mode 100644 index 00000000..180fcb70 --- /dev/null +++ b/backend/src/controller/app.router.ts @@ -0,0 +1,7 @@ +import type { Express } from "express"; +import todoRouter from './todo.controller.js'; + + +export const appController = (app: Express) => { + app.use('/api/todos', todoRouter); +} diff --git a/backend/src/controller/todo.controller.ts b/backend/src/controller/todo.controller.ts new file mode 100644 index 00000000..48048023 --- /dev/null +++ b/backend/src/controller/todo.controller.ts @@ -0,0 +1,60 @@ +import { Router } from "express"; +import type { Request, Response, NextFunction } from 'express'; +import { InMemoryTodoRepository } from "../repository/todo.repository.js"; +import { TodoUsecase } from "../usecase/todo.usecase.js"; + + +const router = Router(); + +const repo = new InMemoryTodoRepository(); //changing this into adding a param if we r going to use a db +const uc = new TodoUsecase(repo); + +router.get('/', (req: Request, res: Response, next: NextFunction) => { + try { + res.status(200).json(uc.getAllTodos()); + } catch (error) { + next(error); + } +}); + +router.get('/:id', (req: Request, res: Response, next: NextFunction) => { + try { + const id = req.params.id as string; + + res.status(200).json(uc.getTodoById(id)); + } catch (error) { + next(error); + } +}); + +router.post('/', (req: Request, res: Response, next: NextFunction) => { + try { + res.status(201).json(uc.createTodo(req.body)); + } catch (error) { + next(error); + } +}); + +router.put('/:id', (req: Request, res: Response, next: NextFunction) => { + try { + const id = req.params.id as string; + + res.status(200).json(uc.updateTodo(id, req.body)); + } catch (error) { + next(error); + } +}); + +router.delete('/:id', (req: Request, res: Response, next: NextFunction) => { + try { + const id = req.params.id as string; + uc.deleteTodo(id); + + res.status(204).send(); + } catch (error) { + next(error); + } +}); + + +export default router; diff --git a/backend/src/index.ts b/backend/src/index.ts new file mode 100644 index 00000000..e135a920 --- /dev/null +++ b/backend/src/index.ts @@ -0,0 +1,18 @@ +import express from 'express'; +import { errorHandler } from './common/middleware/error.middleware.js'; +import { appController } from './controller/app.router.js'; + + +const PORT = process.env.PORT || 4000; + +const app = express(); + +app.use(express.json()); +appController(app); +app.use(errorHandler); + + +app.listen(PORT, () => { + console.log(`Server running at ${PORT}`); +}); + diff --git a/backend/src/model/todo.model.ts b/backend/src/model/todo.model.ts new file mode 100644 index 00000000..c54449e9 --- /dev/null +++ b/backend/src/model/todo.model.ts @@ -0,0 +1,10 @@ +export interface Todo { + id: string; + title: string; + completed: boolean; + createdAt: string; +} + +export type CreateTodoDTO = Pick; + +export type UpdateTodoDTO = Partial>; diff --git a/backend/src/repository/todo.repository.ts b/backend/src/repository/todo.repository.ts new file mode 100644 index 00000000..7e66506d --- /dev/null +++ b/backend/src/repository/todo.repository.ts @@ -0,0 +1,51 @@ +import crypto from 'node:crypto'; +import { Todo, CreateTodoDTO, UpdateTodoDTO } from '../model/todo.model.js'; + +export interface ITodoRepository { + findAll(): Todo[]; + findById(id: string): Todo | undefined; + create(data: CreateTodoDTO): Todo; + update(id: string, data: UpdateTodoDTO): Todo | undefined; + delete(id: string): boolean; +} + +export class InMemoryTodoRepository implements ITodoRepository { + private todos: Todo[] = []; // change this into a constructor if id go and implement it to a db in the controller + + findAll(): Todo[] { + return this.todos; + } + + findById(id: string): Todo | undefined { + return this.todos.find((t) => t.id === id); + } + + create(data: CreateTodoDTO): Todo { + const todo: Todo = { + id: crypto.randomUUID(), + title: data.title, + completed: false, + createdAt: new Date().toISOString(), + }; + this.todos.push(todo); + return todo; + } + + update(id: string, data: UpdateTodoDTO): Todo | undefined { + const todo = this.findById(id); + if (!todo) return undefined; + + if (data.title !== undefined) todo.title = data.title; + if (data.completed !== undefined) todo.completed = data.completed; + + return todo; + } + + delete(id: string): boolean { + const index = this.todos.findIndex((t) => t.id === id); + if (index === -1) return false; + + this.todos.splice(index, 1); + return true; + } +} diff --git a/backend/src/usecase/todo.usecase.ts b/backend/src/usecase/todo.usecase.ts new file mode 100644 index 00000000..1576fed5 --- /dev/null +++ b/backend/src/usecase/todo.usecase.ts @@ -0,0 +1,55 @@ +import { ITodoRepository } from '../repository/todo.repository.js'; +import { Todo, CreateTodoDTO, UpdateTodoDTO } from '../model/todo.model.js'; +import { DomainError } from '../common/error/domain.error.js'; + +export class TodoUsecase { + constructor(private repo: ITodoRepository) { } + + getAllTodos(): Todo[] { + return this.repo.findAll(); + } + + getTodoById(id: string): Todo { + const todo = this.repo.findById(id); + if (!todo) throw new DomainError(404, 'Todo not found'); + return todo; + } + + createTodo(data: CreateTodoDTO): Todo { + if (!data || typeof data.title !== 'string' || !data.title.trim()) { + throw new DomainError(400, 'Title is required'); + } + return this.repo.create({ title: data.title.trim() }); + } + + updateTodo(id: string, data: UpdateTodoDTO): Todo { + if (!data || typeof data !== 'object') { + throw new DomainError(400, 'Invalid request body'); + } + + const updateData: UpdateTodoDTO = {}; + + if (data.title !== undefined) { + if (typeof data.title !== 'string' || !data.title.trim()) { + throw new DomainError(400, 'Title must be a non-empty string'); + } + updateData.title = data.title.trim(); + } + + if (data.completed !== undefined) { + if (typeof data.completed !== 'boolean') { + throw new DomainError(400, 'Completed must be a boolean'); + } + updateData.completed = data.completed; + } + + const updated = this.repo.update(id, updateData); + if (!updated) throw new DomainError(404, 'Todo not found'); + return updated; + } + + deleteTodo(id: string): void { + const deleted = this.repo.delete(id); + if (!deleted) throw new DomainError(404, 'Todo not found'); + } +} diff --git a/backend/tsconfig.json b/backend/tsconfig.json new file mode 100644 index 00000000..ec0dbe3e --- /dev/null +++ b/backend/tsconfig.json @@ -0,0 +1,105 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + /* Projects */ + // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ + // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ + // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ + // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ + // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + /* Language and Environment */ + "target": "ES2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + // "jsx": "preserve", /* Specify what JSX code is generated. */ + // "libReplacement": true, /* Enable lib replacement. */ + // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ + // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ + // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ + // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ + // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ + // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ + // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ + // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ + // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ + // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ + /* Modules */ + "module": "nodenext", /* Specify what module code is generated. */ + "rootDir": "src", /* Specify the root folder within your source files. */ + "moduleResolution": "nodenext", /* Specify how TypeScript looks up a file from a given module specifier. */ + // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ + // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ + // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ + // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ + // "types": [], /* Specify type package names to be included without being referenced in a source file. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ + // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ + // "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */ + // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ + // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ + // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ + // "noUncheckedSideEffectImports": true, /* Check side effect imports. */ + // "resolveJsonModule": true, /* Enable importing .json files. */ + // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ + // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ + /* JavaScript Support */ + // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ + // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ + // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ + /* Emit */ + // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ + // "declarationMap": true, /* Create sourcemaps for d.ts files. */ + // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ + // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ + // "noEmit": true, /* Disable emitting files from a compilation. */ + // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ + "outDir": "dist", /* Specify an output folder for all emitted files. */ + // "removeComments": true, /* Disable emitting comments. */ + // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ + // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ + // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ + // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ + // "newLine": "crlf", /* Set the newline character for emitting files. */ + // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ + // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ + // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ + // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ + // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ + /* Interop Constraints */ + // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ + // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ + // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */ + // "erasableSyntaxOnly": true, /* Do not allow runtime constructs that are not part of ECMAScript. */ + // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ + // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ + // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ + // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ + // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */ + // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ + // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ + // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ + // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ + // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ + // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ + // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ + // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ + // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ + // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ + // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ + // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +} diff --git a/package-lock.json b/package-lock.json index c61d591b..4bd70b7b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,12 @@ "version": "1.0.0", "dependencies": { "express": "^4.18.2" + }, + "devDependencies": { + "@types/express": "^5.0.6", + "@types/node": "^20.17.30", + "tsx": "^4.22.5", + "typescript": "^5.8.3" } }, "frontend": { @@ -2825,6 +2831,448 @@ "tslib": "^2.4.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.5.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.5.1.tgz", @@ -5936,6 +6384,27 @@ "@babel/types": "^7.20.7" } }, + "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/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", @@ -5964,6 +6433,31 @@ "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", "dev": true }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -5977,6 +6471,13 @@ "resolved": "https://registry.npmjs.org/@types/hammerjs/-/hammerjs-2.0.46.tgz", "integrity": "sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==" }, + "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/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -6052,6 +6553,20 @@ "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", "devOptional": true }, + "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/react": { "version": "19.1.0", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.0.tgz", @@ -6089,6 +6604,27 @@ "csstype": "^3.0.2" } }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -9134,6 +9670,48 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -17343,6 +17921,25 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, + "node_modules/tsx": { + "version": "4.22.5", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.5.tgz", + "integrity": "sha512-F7JnSfPl5ASt6LqwWyUQ3T8BwN3q0eQEbFMYa2iRWaVQmmudo0d7fRmwM4O002gsvW1bs0yBYioutsAjqLJMvQ==", + "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-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",