Przeglądaj źródła

feat: initial project setup with complete market data service architecture

- Add comprehensive Node.js/Express.js backend structure
- Implement PostgreSQL database schema with Sequelize ORM
- Create RESTful API endpoints for symbols, candles, and live prices
- Add data validation using Joi and error handling middleware
- Configure Winston logging and security with Helmet.js
- Set up modular architecture with controllers, routes, and models
- Update .gitignore with standard Node.js exclusions
- Enhance README with detailed documentation, installation, and API guides

This commit establishes the foundational architecture for a high-performance
financial data API supporting multiple asset classes including cryptocurrencies,
stocks, forex, and commodities with both RESTful endpoints and real-time
WebSocket capabilities.
Hussain Afzal 9 miesięcy temu
rodzic
commit
7ece7a970d

+ 84 - 15
.gitignore

@@ -1,30 +1,99 @@
-# ---> Node
+# Dependencies
+node_modules/
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Environment variables
+.env
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
 # Logs
-logs
+logs/
 *.log
 npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
 
 # Runtime data
 pids
 *.pid
 *.seed
-
-# Directory for instrumented libs generated by jscoverage/JSCover
-lib-cov
+*.pid.lock
 
 # Coverage directory used by tools like istanbul
-coverage
+coverage/
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Microbundle cache
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+.parcel-cache
+
+# Next.js build output
+.next
+
+# Nuxt.js build / generate output
+.nuxt
+dist
+
+# Gatsby files
+.cache/
+public
 
-# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
-.grunt
+# Storybook build outputs
+.out
+.storybook-out
 
-# node-waf configuration
-.lock-wscript
+# Temporary folders
+tmp/
+temp/
 
-# Compiled binary addons (http://nodejs.org/api/addons.html)
-build/Release
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
 
-# Dependency directory
-# https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git
-node_modules
+# Database
+*.sqlite
+*.db
 
+# OS generated files
+Thumbs.db

+ 204 - 2
README.md

@@ -1,3 +1,205 @@
-# market-data-service
+# Market Data Service
 
-Market Data Service is a high-performance financial data API that provides comprehensive Symbol prices of different markets  through both RESTful endpoints and real-time WebSocket connections.
+A high-performance financial data API that provides comprehensive market data for various financial instruments including cryptocurrencies, stocks, forex, and commodities through both RESTful endpoints and real-time WebSocket connections.
+
+## Features
+
+- **Multi-Asset Support**: Handles cryptocurrencies, stocks, forex, and commodities
+- **Real-time Data**: Live price feeds with bid/ask spreads
+- **Historical Data**: OHLCV candle data with flexible timeframes
+- **RESTful API**: Well-structured endpoints for all operations
+- **Data Validation**: Comprehensive input validation using Joi
+- **Error Handling**: Robust error handling with detailed responses
+- **Security**: Helmet.js for security headers, CORS support
+- **Logging**: Winston-based logging with multiple transports
+- **Database**: PostgreSQL with Sequelize ORM
+- **Scalable Architecture**: Modular design with controllers, routes, and middleware
+
+## Tech Stack
+
+- **Backend**: Node.js, Express.js
+- **Database**: PostgreSQL
+- **ORM**: Sequelize
+- **Validation**: Joi
+- **Security**: Helmet, CORS
+- **Logging**: Winston, Morgan
+- **Testing**: Jest
+- **Development**: Nodemon, ESLint
+
+## Project Structure
+
+```
+market-data-service/
+├── src/
+│   ├── config/
+│   │   └── database.js          # Database configuration
+│   ├── controllers/
+│   │   ├── symbolController.js      # Symbol CRUD operations
+│   │   ├── candleController.js      # Candle data operations
+│   │   └── livePriceController.js   # Live price operations
+│   ├── middleware/
+│   │   ├── errorHandler.js          # Global error handling
+│   │   └── validation.js            # Request validation
+│   ├── models/
+│   │   ├── Symbol.js                # Symbol model
+│   │   ├── Candle1h.js              # 1-hour candle model
+│   │   ├── LivePrice.js             # Live price model
+│   │   └── index.js                 # Model associations
+│   ├── routes/
+│   │   ├── symbols.js               # Symbol routes
+│   │   ├── candles.js               # Candle routes
+│   │   └── livePrices.js            # Live price routes
+│   ├── utils/
+│   │   └── logger.js                # Logging utility
+│   ├── app.js                       # Express app configuration
+│   └── server.js                    # Server startup
+├── tests/                           # Test files
+├── schema.sql                       # Database schema
+├── .env                             # Environment variables
+├── .gitignore                       # Git ignore rules
+├── package.json                     # Dependencies and scripts
+└── README.md                        # This file
+```
+
+## Installation
+
+1. **Clone the repository**
+   ```bash
+   git clone https://git.mqldevelopment.com/muhammad.uzair/market-data-service.git
+   cd market-data-service
+   ```
+
+2. **Install dependencies**
+   ```bash
+   npm install
+   ```
+
+3. **Set up environment variables**
+   ```bash
+   cp .env.example .env
+   ```
+   Edit `.env` with your database credentials and other configuration.
+
+4. **Set up the database**
+   ```bash
+   # Create PostgreSQL database
+   createdb market_data
+
+   # Run the schema
+   psql -d market_data -f schema.sql
+   ```
+
+5. **Start the development server**
+   ```bash
+   npm run dev
+   ```
+
+The server will start on `http://localhost:3000`
+
+## Environment Variables
+
+Create a `.env` file in the root directory with the following variables:
+
+```env
+# Database Configuration
+DB_HOST=localhost
+DB_PORT=5432
+DB_NAME=market_data
+DB_USER=your_username
+DB_PASSWORD=your_password
+
+# Server Configuration
+PORT=3000
+NODE_ENV=development
+
+# JWT Configuration (if needed for authentication)
+JWT_SECRET=your_jwt_secret_key
+
+# API Keys (if needed for external services)
+# BINANCE_API_KEY=your_api_key
+# BINANCE_API_SECRET=your_api_secret
+
+# CORS Configuration
+CORS_ORIGIN=*
+```
+
+## API Endpoints
+
+### Health Check
+- `GET /health` - Check service health
+
+### Symbols
+- `GET /api/symbols` - Get all symbols (with filtering)
+- `GET /api/symbols/search` - Search symbols by name
+- `GET /api/symbols/:id` - Get symbol by ID
+- `POST /api/symbols` - Create new symbol
+- `PUT /api/symbols/:id` - Update symbol
+- `DELETE /api/symbols/:id` - Delete symbol (soft delete)
+
+### Candles
+- `GET /api/candles` - Get candles with filtering
+- `GET /api/candles/ohlc` - Get OHLC data
+- `GET /api/candles/:symbolId/latest` - Get latest candle for symbol
+- `POST /api/candles` - Create new candle
+- `POST /api/candles/bulk` - Bulk create candles
+
+### Live Prices
+- `GET /api/live-prices` - Get all live prices
+- `GET /api/live-prices/exchange/:exchange` - Get live prices by exchange
+- `GET /api/live-prices/type/:type` - Get live prices by instrument type
+- `GET /api/live-prices/:symbolId` - Get live price for symbol
+- `POST /api/live-prices` - Create/update live price
+- `POST /api/live-prices/bulk` - Bulk update live prices
+- `DELETE /api/live-prices/:symbolId` - Delete live price
+
+## Database Schema
+
+The service uses three main tables:
+
+1. **symbols** - Trading symbols metadata
+2. **candles_1h** - Hourly OHLCV data
+3. **live_prices** - Current market prices
+
+See `schema.sql` for complete table definitions.
+
+## Development
+
+### Available Scripts
+
+- `npm start` - Start production server
+- `npm run dev` - Start development server with auto-reload
+- `npm test` - Run tests
+- `npm run test:watch` - Run tests in watch mode
+- `npm run lint` - Run ESLint
+- `npm run lint:fix` - Fix ESLint issues
+
+### Code Style
+
+This project uses ESLint for code linting. Run `npm run lint` to check for issues and `npm run lint:fix` to automatically fix them.
+
+### Testing
+
+Tests are written using Jest. Add your test files in the `tests/` directory.
+
+## Deployment
+
+1. Set `NODE_ENV=production` in your environment
+2. Run `npm start` to start the production server
+3. Consider using a process manager like PM2 for production deployments
+
+## Contributing
+
+1. Fork the repository
+2. Create a feature branch
+3. Make your changes
+4. Add tests for new features
+5. Ensure all tests pass
+6. Submit a pull request
+
+## License
+
+ISC License - see LICENSE file for details.
+
+## Support
+
+For support or questions, please contact the development team.

+ 1629 - 0
package-lock.json

@@ -0,0 +1,1629 @@
+{
+  "name": "market-data-service",
+  "version": "1.0.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "market-data-service",
+      "version": "1.0.0",
+      "license": "ISC",
+      "dependencies": {
+        "cors": "^2.8.5",
+        "dotenv": "^17.2.3",
+        "express": "^5.1.0",
+        "helmet": "^8.1.0",
+        "joi": "^18.0.1",
+        "morgan": "^1.10.1",
+        "pg": "^8.16.3",
+        "sequelize": "^6.37.7",
+        "winston": "^3.18.3"
+      }
+    },
+    "node_modules/@colors/colors": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz",
+      "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.1.90"
+      }
+    },
+    "node_modules/@dabh/diagnostics": {
+      "version": "2.0.8",
+      "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz",
+      "integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@so-ric/colorspace": "^1.1.6",
+        "enabled": "2.0.x",
+        "kuler": "^2.0.0"
+      }
+    },
+    "node_modules/@hapi/address": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz",
+      "integrity": "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "@hapi/hoek": "^11.0.2"
+      },
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
+    "node_modules/@hapi/formula": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-3.0.2.tgz",
+      "integrity": "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@hapi/hoek": {
+      "version": "11.0.7",
+      "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz",
+      "integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@hapi/pinpoint": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-2.0.1.tgz",
+      "integrity": "sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==",
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/@hapi/tlds": {
+      "version": "1.1.3",
+      "resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.3.tgz",
+      "integrity": "sha512-QIvUMB5VZ8HMLZF9A2oWr3AFM430QC8oGd0L35y2jHpuW6bIIca6x/xL7zUf4J7L9WJ3qjz+iJII8ncaeMbpSg==",
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
+    "node_modules/@hapi/topo": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz",
+      "integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "@hapi/hoek": "^11.0.2"
+      }
+    },
+    "node_modules/@so-ric/colorspace": {
+      "version": "1.1.6",
+      "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz",
+      "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==",
+      "license": "MIT",
+      "dependencies": {
+        "color": "^5.0.2",
+        "text-hex": "1.0.x"
+      }
+    },
+    "node_modules/@standard-schema/spec": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
+      "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
+      "license": "MIT"
+    },
+    "node_modules/@types/debug": {
+      "version": "4.1.12",
+      "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
+      "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/ms": "*"
+      }
+    },
+    "node_modules/@types/ms": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
+      "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
+      "license": "MIT"
+    },
+    "node_modules/@types/node": {
+      "version": "24.8.0",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-24.8.0.tgz",
+      "integrity": "sha512-5x08bUtU8hfboMTrJ7mEO4CpepS9yBwAqcL52y86SWNmbPX8LVbNs3EP4cNrIZgdjk2NAlP2ahNihozpoZIxSg==",
+      "license": "MIT",
+      "dependencies": {
+        "undici-types": "~7.14.0"
+      }
+    },
+    "node_modules/@types/triple-beam": {
+      "version": "1.3.5",
+      "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz",
+      "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==",
+      "license": "MIT"
+    },
+    "node_modules/@types/validator": {
+      "version": "13.15.3",
+      "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.3.tgz",
+      "integrity": "sha512-7bcUmDyS6PN3EuD9SlGGOxM77F8WLVsrwkxyWxKnxzmXoequ6c7741QBrANq6htVRGOITJ7z72mTP6Z4XyuG+Q==",
+      "license": "MIT"
+    },
+    "node_modules/accepts": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
+      "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-types": "^3.0.0",
+        "negotiator": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/async": {
+      "version": "3.2.6",
+      "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
+      "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
+      "license": "MIT"
+    },
+    "node_modules/basic-auth": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
+      "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
+      "license": "MIT",
+      "dependencies": {
+        "safe-buffer": "5.1.2"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/basic-auth/node_modules/safe-buffer": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+      "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+      "license": "MIT"
+    },
+    "node_modules/body-parser": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
+      "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
+      "license": "MIT",
+      "dependencies": {
+        "bytes": "^3.1.2",
+        "content-type": "^1.0.5",
+        "debug": "^4.4.0",
+        "http-errors": "^2.0.0",
+        "iconv-lite": "^0.6.3",
+        "on-finished": "^2.4.1",
+        "qs": "^6.14.0",
+        "raw-body": "^3.0.0",
+        "type-is": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "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/color": {
+      "version": "5.0.2",
+      "resolved": "https://registry.npmjs.org/color/-/color-5.0.2.tgz",
+      "integrity": "sha512-e2hz5BzbUPcYlIRHo8ieAhYgoajrJr+hWoceg6E345TPsATMUKqDgzt8fSXZJJbxfpiPzkWyphz8yn8At7q3fA==",
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^3.0.1",
+        "color-string": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/color-convert": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.2.tgz",
+      "integrity": "sha512-UNqkvCDXstVck3kdowtOTWROIJQwafjOfXSmddoDrXo4cewMKmusCeF22Q24zvjR8nwWib/3S/dfyzPItPEiJg==",
+      "license": "MIT",
+      "dependencies": {
+        "color-name": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=14.6"
+      }
+    },
+    "node_modules/color-name": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.0.2.tgz",
+      "integrity": "sha512-9vEt7gE16EW7Eu7pvZnR0abW9z6ufzhXxGXZEVU9IqPdlsUiMwJeJfRtq0zePUmnbHGT9zajca7mX8zgoayo4A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.20"
+      }
+    },
+    "node_modules/color-string": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.2.tgz",
+      "integrity": "sha512-RxmjYxbWemV9gKu4zPgiZagUxbH3RQpEIO77XoSSX0ivgABDZ+h8Zuash/EMFLTI4N9QgFPOJ6JQpPZKFxa+dA==",
+      "license": "MIT",
+      "dependencies": {
+        "color-name": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/content-disposition": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
+      "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==",
+      "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.2.2",
+      "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
+      "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.6.0"
+      }
+    },
+    "node_modules/cors": {
+      "version": "2.8.5",
+      "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
+      "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
+      "license": "MIT",
+      "dependencies": {
+        "object-assign": "^4",
+        "vary": "^1"
+      },
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/debug": {
+      "version": "4.4.3",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+      "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/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/dotenv": {
+      "version": "17.2.3",
+      "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
+      "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://dotenvx.com"
+      }
+    },
+    "node_modules/dottie": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz",
+      "integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==",
+      "license": "MIT"
+    },
+    "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/enabled": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz",
+      "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==",
+      "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/escape-html": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+      "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+      "license": "MIT"
+    },
+    "node_modules/etag": {
+      "version": "1.8.1",
+      "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+      "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/express": {
+      "version": "5.1.0",
+      "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
+      "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
+      "license": "MIT",
+      "dependencies": {
+        "accepts": "^2.0.0",
+        "body-parser": "^2.2.0",
+        "content-disposition": "^1.0.0",
+        "content-type": "^1.0.5",
+        "cookie": "^0.7.1",
+        "cookie-signature": "^1.2.1",
+        "debug": "^4.4.0",
+        "encodeurl": "^2.0.0",
+        "escape-html": "^1.0.3",
+        "etag": "^1.8.1",
+        "finalhandler": "^2.1.0",
+        "fresh": "^2.0.0",
+        "http-errors": "^2.0.0",
+        "merge-descriptors": "^2.0.0",
+        "mime-types": "^3.0.0",
+        "on-finished": "^2.4.1",
+        "once": "^1.4.0",
+        "parseurl": "^1.3.3",
+        "proxy-addr": "^2.0.7",
+        "qs": "^6.14.0",
+        "range-parser": "^1.2.1",
+        "router": "^2.2.0",
+        "send": "^1.1.0",
+        "serve-static": "^2.2.0",
+        "statuses": "^2.0.1",
+        "type-is": "^2.0.1",
+        "vary": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 18"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/fecha": {
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
+      "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==",
+      "license": "MIT"
+    },
+    "node_modules/finalhandler": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
+      "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.4.0",
+        "encodeurl": "^2.0.0",
+        "escape-html": "^1.0.3",
+        "on-finished": "^2.4.1",
+        "parseurl": "^1.3.3",
+        "statuses": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/fn.name": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz",
+      "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==",
+      "license": "MIT"
+    },
+    "node_modules/forwarded": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
+      "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/fresh": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
+      "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/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.2",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/helmet": {
+      "version": "8.1.0",
+      "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz",
+      "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18.0.0"
+      }
+    },
+    "node_modules/http-errors": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
+      "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
+      "license": "MIT",
+      "dependencies": {
+        "depd": "2.0.0",
+        "inherits": "2.0.4",
+        "setprototypeof": "1.2.0",
+        "statuses": "2.0.1",
+        "toidentifier": "1.0.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/http-errors/node_modules/statuses": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+      "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/iconv-lite": {
+      "version": "0.6.3",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+      "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+      "license": "MIT",
+      "dependencies": {
+        "safer-buffer": ">= 2.1.2 < 3.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/inflection": {
+      "version": "1.13.4",
+      "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz",
+      "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==",
+      "engines": [
+        "node >= 0.4.0"
+      ],
+      "license": "MIT"
+    },
+    "node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+      "license": "ISC"
+    },
+    "node_modules/ipaddr.js": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
+      "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/is-promise": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
+      "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
+      "license": "MIT"
+    },
+    "node_modules/is-stream": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
+      "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/joi": {
+      "version": "18.0.1",
+      "resolved": "https://registry.npmjs.org/joi/-/joi-18.0.1.tgz",
+      "integrity": "sha512-IiQpRyypSnLisQf3PwuN2eIHAsAIGZIrLZkd4zdvIar2bDyhM91ubRjy8a3eYablXsh9BeI/c7dmPYHca5qtoA==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "@hapi/address": "^5.1.1",
+        "@hapi/formula": "^3.0.2",
+        "@hapi/hoek": "^11.0.7",
+        "@hapi/pinpoint": "^2.0.1",
+        "@hapi/tlds": "^1.1.1",
+        "@hapi/topo": "^6.0.2",
+        "@standard-schema/spec": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 20"
+      }
+    },
+    "node_modules/kuler": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz",
+      "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==",
+      "license": "MIT"
+    },
+    "node_modules/lodash": {
+      "version": "4.17.21",
+      "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+      "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+      "license": "MIT"
+    },
+    "node_modules/logform": {
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz",
+      "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@colors/colors": "1.6.0",
+        "@types/triple-beam": "^1.3.2",
+        "fecha": "^4.2.0",
+        "ms": "^2.1.1",
+        "safe-stable-stringify": "^2.3.1",
+        "triple-beam": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 12.0.0"
+      }
+    },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/media-typer": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
+      "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/merge-descriptors": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
+      "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.54.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
+      "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
+      "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "^1.54.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/moment": {
+      "version": "2.30.1",
+      "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
+      "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
+      "license": "MIT",
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/moment-timezone": {
+      "version": "0.5.48",
+      "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz",
+      "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==",
+      "license": "MIT",
+      "dependencies": {
+        "moment": "^2.29.4"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/morgan": {
+      "version": "1.10.1",
+      "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.1.tgz",
+      "integrity": "sha512-223dMRJtI/l25dJKWpgij2cMtywuG/WiUKXdvwfbhGKBhy1puASqXwFzmWZ7+K73vUPoR7SS2Qz2cI/g9MKw0A==",
+      "license": "MIT",
+      "dependencies": {
+        "basic-auth": "~2.0.1",
+        "debug": "2.6.9",
+        "depd": "~2.0.0",
+        "on-finished": "~2.3.0",
+        "on-headers": "~1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/morgan/node_modules/debug": {
+      "version": "2.6.9",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+      "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "2.0.0"
+      }
+    },
+    "node_modules/morgan/node_modules/ms": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+      "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+      "license": "MIT"
+    },
+    "node_modules/morgan/node_modules/on-finished": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
+      "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==",
+      "license": "MIT",
+      "dependencies": {
+        "ee-first": "1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/negotiator": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
+      "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/object-assign": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+      "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/object-inspect": {
+      "version": "1.13.4",
+      "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+      "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/on-finished": {
+      "version": "2.4.1",
+      "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
+      "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
+      "license": "MIT",
+      "dependencies": {
+        "ee-first": "1.1.1"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/on-headers": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz",
+      "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+      "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+      "license": "ISC",
+      "dependencies": {
+        "wrappy": "1"
+      }
+    },
+    "node_modules/one-time": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz",
+      "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==",
+      "license": "MIT",
+      "dependencies": {
+        "fn.name": "1.x.x"
+      }
+    },
+    "node_modules/parseurl": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+      "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/path-to-regexp": {
+      "version": "8.3.0",
+      "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
+      "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
+      "license": "MIT",
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/pg": {
+      "version": "8.16.3",
+      "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz",
+      "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==",
+      "license": "MIT",
+      "dependencies": {
+        "pg-connection-string": "^2.9.1",
+        "pg-pool": "^3.10.1",
+        "pg-protocol": "^1.10.3",
+        "pg-types": "2.2.0",
+        "pgpass": "1.0.5"
+      },
+      "engines": {
+        "node": ">= 16.0.0"
+      },
+      "optionalDependencies": {
+        "pg-cloudflare": "^1.2.7"
+      },
+      "peerDependencies": {
+        "pg-native": ">=3.0.1"
+      },
+      "peerDependenciesMeta": {
+        "pg-native": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/pg-cloudflare": {
+      "version": "1.2.7",
+      "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz",
+      "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==",
+      "license": "MIT",
+      "optional": true
+    },
+    "node_modules/pg-connection-string": {
+      "version": "2.9.1",
+      "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz",
+      "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==",
+      "license": "MIT"
+    },
+    "node_modules/pg-int8": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
+      "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
+      "license": "ISC",
+      "engines": {
+        "node": ">=4.0.0"
+      }
+    },
+    "node_modules/pg-pool": {
+      "version": "3.10.1",
+      "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz",
+      "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==",
+      "license": "MIT",
+      "peerDependencies": {
+        "pg": ">=8.0"
+      }
+    },
+    "node_modules/pg-protocol": {
+      "version": "1.10.3",
+      "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz",
+      "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==",
+      "license": "MIT"
+    },
+    "node_modules/pg-types": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
+      "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
+      "license": "MIT",
+      "dependencies": {
+        "pg-int8": "1.0.1",
+        "postgres-array": "~2.0.0",
+        "postgres-bytea": "~1.0.0",
+        "postgres-date": "~1.0.4",
+        "postgres-interval": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/pgpass": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
+      "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
+      "license": "MIT",
+      "dependencies": {
+        "split2": "^4.1.0"
+      }
+    },
+    "node_modules/postgres-array": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
+      "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/postgres-bytea": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
+      "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/postgres-date": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
+      "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/postgres-interval": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
+      "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
+      "license": "MIT",
+      "dependencies": {
+        "xtend": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "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.14.0",
+      "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
+      "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "side-channel": "^1.1.0"
+      },
+      "engines": {
+        "node": ">=0.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/range-parser": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+      "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/raw-body": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.1.tgz",
+      "integrity": "sha512-9G8cA+tuMS75+6G/TzW8OtLzmBDMo8p1JRxN5AZ+LAp8uxGA8V8GZm4GQ4/N5QNQEnLmg6SS7wyuSmbKepiKqA==",
+      "license": "MIT",
+      "dependencies": {
+        "bytes": "3.1.2",
+        "http-errors": "2.0.0",
+        "iconv-lite": "0.7.0",
+        "unpipe": "1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/raw-body/node_modules/iconv-lite": {
+      "version": "0.7.0",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz",
+      "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==",
+      "license": "MIT",
+      "dependencies": {
+        "safer-buffer": ">= 2.1.2 < 3.0.0"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/express"
+      }
+    },
+    "node_modules/readable-stream": {
+      "version": "3.6.2",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+      "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+      "license": "MIT",
+      "dependencies": {
+        "inherits": "^2.0.3",
+        "string_decoder": "^1.1.1",
+        "util-deprecate": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/retry-as-promised": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.1.1.tgz",
+      "integrity": "sha512-hMD7odLOt3LkTjcif8aRZqi/hybjpLNgSk5oF5FCowfCjok6LukpN2bDX7R5wDmbgBQFn7YoBxSagmtXHaJYJw==",
+      "license": "MIT"
+    },
+    "node_modules/router": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
+      "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.4.0",
+        "depd": "^2.0.0",
+        "is-promise": "^4.0.0",
+        "parseurl": "^1.3.3",
+        "path-to-regexp": "^8.0.0"
+      },
+      "engines": {
+        "node": ">= 18"
+      }
+    },
+    "node_modules/safe-buffer": {
+      "version": "5.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/safe-stable-stringify": {
+      "version": "2.5.0",
+      "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
+      "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+      "license": "MIT"
+    },
+    "node_modules/semver": {
+      "version": "7.7.3",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
+      "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/send": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
+      "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==",
+      "license": "MIT",
+      "dependencies": {
+        "debug": "^4.3.5",
+        "encodeurl": "^2.0.0",
+        "escape-html": "^1.0.3",
+        "etag": "^1.8.1",
+        "fresh": "^2.0.0",
+        "http-errors": "^2.0.0",
+        "mime-types": "^3.0.1",
+        "ms": "^2.1.3",
+        "on-finished": "^2.4.1",
+        "range-parser": "^1.2.1",
+        "statuses": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 18"
+      }
+    },
+    "node_modules/sequelize": {
+      "version": "6.37.7",
+      "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.7.tgz",
+      "integrity": "sha512-mCnh83zuz7kQxxJirtFD7q6Huy6liPanI67BSlbzSYgVNl5eXVdE2CN1FuAeZwG1SNpGsNRCV+bJAVVnykZAFA==",
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/sequelize"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@types/debug": "^4.1.8",
+        "@types/validator": "^13.7.17",
+        "debug": "^4.3.4",
+        "dottie": "^2.0.6",
+        "inflection": "^1.13.4",
+        "lodash": "^4.17.21",
+        "moment": "^2.29.4",
+        "moment-timezone": "^0.5.43",
+        "pg-connection-string": "^2.6.1",
+        "retry-as-promised": "^7.0.4",
+        "semver": "^7.5.4",
+        "sequelize-pool": "^7.1.0",
+        "toposort-class": "^1.0.1",
+        "uuid": "^8.3.2",
+        "validator": "^13.9.0",
+        "wkx": "^0.5.0"
+      },
+      "engines": {
+        "node": ">=10.0.0"
+      },
+      "peerDependenciesMeta": {
+        "ibm_db": {
+          "optional": true
+        },
+        "mariadb": {
+          "optional": true
+        },
+        "mysql2": {
+          "optional": true
+        },
+        "oracledb": {
+          "optional": true
+        },
+        "pg": {
+          "optional": true
+        },
+        "pg-hstore": {
+          "optional": true
+        },
+        "snowflake-sdk": {
+          "optional": true
+        },
+        "sqlite3": {
+          "optional": true
+        },
+        "tedious": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/sequelize-pool": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-7.1.0.tgz",
+      "integrity": "sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 10.0.0"
+      }
+    },
+    "node_modules/serve-static": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
+      "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==",
+      "license": "MIT",
+      "dependencies": {
+        "encodeurl": "^2.0.0",
+        "escape-html": "^1.0.3",
+        "parseurl": "^1.3.3",
+        "send": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 18"
+      }
+    },
+    "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.0",
+      "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
+      "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "object-inspect": "^1.13.3"
+      },
+      "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/split2": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
+      "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
+      "license": "ISC",
+      "engines": {
+        "node": ">= 10.x"
+      }
+    },
+    "node_modules/stack-trace": {
+      "version": "0.0.10",
+      "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz",
+      "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==",
+      "license": "MIT",
+      "engines": {
+        "node": "*"
+      }
+    },
+    "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/string_decoder": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+      "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+      "license": "MIT",
+      "dependencies": {
+        "safe-buffer": "~5.2.0"
+      }
+    },
+    "node_modules/text-hex": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
+      "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==",
+      "license": "MIT"
+    },
+    "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/toposort-class": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz",
+      "integrity": "sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==",
+      "license": "MIT"
+    },
+    "node_modules/triple-beam": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz",
+      "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 14.0.0"
+      }
+    },
+    "node_modules/type-is": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
+      "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
+      "license": "MIT",
+      "dependencies": {
+        "content-type": "^1.0.5",
+        "media-typer": "^1.1.0",
+        "mime-types": "^3.0.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/undici-types": {
+      "version": "7.14.0",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz",
+      "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==",
+      "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/util-deprecate": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+      "license": "MIT"
+    },
+    "node_modules/uuid": {
+      "version": "8.3.2",
+      "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+      "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+      "license": "MIT",
+      "bin": {
+        "uuid": "dist/bin/uuid"
+      }
+    },
+    "node_modules/validator": {
+      "version": "13.15.15",
+      "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz",
+      "integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.10"
+      }
+    },
+    "node_modules/vary": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+      "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/winston": {
+      "version": "3.18.3",
+      "resolved": "https://registry.npmjs.org/winston/-/winston-3.18.3.tgz",
+      "integrity": "sha512-NoBZauFNNWENgsnC9YpgyYwOVrl2m58PpQ8lNHjV3kosGs7KJ7Npk9pCUE+WJlawVSe8mykWDKWFSVfs3QO9ww==",
+      "license": "MIT",
+      "dependencies": {
+        "@colors/colors": "^1.6.0",
+        "@dabh/diagnostics": "^2.0.8",
+        "async": "^3.2.3",
+        "is-stream": "^2.0.0",
+        "logform": "^2.7.0",
+        "one-time": "^1.0.0",
+        "readable-stream": "^3.4.0",
+        "safe-stable-stringify": "^2.3.1",
+        "stack-trace": "0.0.x",
+        "triple-beam": "^1.3.0",
+        "winston-transport": "^4.9.0"
+      },
+      "engines": {
+        "node": ">= 12.0.0"
+      }
+    },
+    "node_modules/winston-transport": {
+      "version": "4.9.0",
+      "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz",
+      "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==",
+      "license": "MIT",
+      "dependencies": {
+        "logform": "^2.7.0",
+        "readable-stream": "^3.6.2",
+        "triple-beam": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 12.0.0"
+      }
+    },
+    "node_modules/wkx": {
+      "version": "0.5.0",
+      "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz",
+      "integrity": "sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
+    "node_modules/wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+      "license": "ISC"
+    },
+    "node_modules/xtend": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+      "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4"
+      }
+    }
+  }
+}

+ 35 - 0
package.json

@@ -0,0 +1,35 @@
+{
+  "name": "market-data-service",
+  "version": "1.0.0",
+  "description": "Market Data Service is a high-performance financial data API that provides comprehensive Symbol prices of different markets  through both RESTful endpoints and real-time WebSocket connections.",
+  "main": "index.js",
+  "scripts": {
+    "start": "node src/server.js",
+    "dev": "nodemon src/server.js",
+    "test": "jest",
+    "test:watch": "jest --watch",
+    "lint": "eslint src/**/*.js",
+    "lint:fix": "eslint src/**/*.js --fix",
+    "db:migrate": "sequelize-cli db:migrate",
+    "db:seed": "sequelize-cli db:seed:all"
+  },
+  "repository": {
+    "type": "git",
+    "url": "https://git.mqldevelopment.com/muhammad.uzair/market-data-service.git"
+  },
+  "keywords": [],
+  "author": "",
+  "license": "ISC",
+  "type": "commonjs",
+  "dependencies": {
+    "cors": "^2.8.5",
+    "dotenv": "^17.2.3",
+    "express": "^5.1.0",
+    "helmet": "^8.1.0",
+    "joi": "^18.0.1",
+    "morgan": "^1.10.1",
+    "pg": "^8.16.3",
+    "sequelize": "^6.37.7",
+    "winston": "^3.18.3"
+  }
+}

+ 50 - 0
schema.sql

@@ -0,0 +1,50 @@
+-- symbols table
+-- Stores metadata for each trading symbol
+CREATE TABLE symbols (
+    id SERIAL PRIMARY KEY,
+    symbol VARCHAR(50) NOT NULL UNIQUE,
+    base_asset VARCHAR(50),
+    quote_asset VARCHAR(50),
+    exchange VARCHAR(50),
+    instrument_type VARCHAR(20) CHECK (instrument_type IN ('crypto', 'stock', 'forex', 'commodity')),
+    is_active BOOLEAN DEFAULT TRUE,
+    created_at TIMESTAMPTZ DEFAULT NOW(),
+    updated_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+CREATE INDEX idx_symbols_exchange ON symbols(exchange);
+CREATE INDEX idx_symbols_type ON symbols(instrument_type);
+
+-- candles_1h table
+-- Stores hourly OHLCV data for each symbol
+CREATE TABLE candles_1h (
+    id BIGSERIAL PRIMARY KEY,
+    symbol_id INT NOT NULL REFERENCES symbols(id) ON DELETE CASCADE,
+    open_time TIMESTAMPTZ NOT NULL,
+    close_time TIMESTAMPTZ NOT NULL,
+    open NUMERIC(18,8) NOT NULL,
+    high NUMERIC(18,8) NOT NULL,
+    low NUMERIC(18,8) NOT NULL,
+    close NUMERIC(18,8) NOT NULL,
+    volume NUMERIC(20,8),
+    trades_count INT,
+    quote_volume NUMERIC(20,8),
+    created_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+CREATE UNIQUE INDEX idx_candles_symbol_time ON candles_1h(symbol_id, open_time);
+CREATE INDEX idx_candles_open_time ON candles_1h(open_time);
+
+-- live_prices table
+-- Stores the latest live market prices per symbol
+CREATE TABLE live_prices (
+    symbol_id INT PRIMARY KEY REFERENCES symbols(id) ON DELETE CASCADE,
+    price NUMERIC(18,8) NOT NULL,
+    bid NUMERIC(18,8),
+    ask NUMERIC(18,8),
+    bid_size NUMERIC(18,8),
+    ask_size NUMERIC(18,8),
+    last_updated TIMESTAMPTZ DEFAULT NOW()
+);
+
+CREATE INDEX idx_live_prices_price ON live_prices(price);

+ 70 - 0
src/app.js

@@ -0,0 +1,70 @@
+const express = require('express');
+const cors = require('cors');
+const helmet = require('helmet');
+const morgan = require('morgan');
+require('dotenv').config();
+
+// Import routes
+const symbolRoutes = require('./routes/symbols');
+const candleRoutes = require('./routes/candles');
+const livePriceRoutes = require('./routes/livePrices');
+
+// Import middleware
+const errorHandler = require('./middleware/errorHandler');
+
+// Import utilities
+const logger = require('./utils/logger');
+
+const app = express();
+
+// Security middleware
+app.use(helmet());
+
+// CORS configuration
+app.use(cors({
+  origin: process.env.CORS_ORIGIN || '*',
+  credentials: true
+}));
+
+// Body parsing middleware
+app.use(express.json({ limit: '10mb' }));
+app.use(express.urlencoded({ extended: true, limit: '10mb' }));
+
+// Logging middleware
+if (process.env.NODE_ENV === 'development') {
+  app.use(morgan('combined'));
+} else {
+  app.use(morgan('combined', {
+    stream: {
+      write: (message) => logger.http(message.trim())
+    }
+  }));
+}
+
+// Health check endpoint
+app.get('/health', (req, res) => {
+  res.json({
+    success: true,
+    message: 'Market Data Service is running',
+    timestamp: new Date().toISOString(),
+    environment: process.env.NODE_ENV
+  });
+});
+
+// API routes
+app.use('/api/symbols', symbolRoutes);
+app.use('/api/candles', candleRoutes);
+app.use('/api/live-prices', livePriceRoutes);
+
+// 404 handler
+app.use('*', (req, res) => {
+  res.status(404).json({
+    success: false,
+    message: 'Route not found'
+  });
+});
+
+// Error handling middleware (must be last)
+app.use(errorHandler);
+
+module.exports = app;

+ 37 - 0
src/config/database.js

@@ -0,0 +1,37 @@
+const { Sequelize } = require('sequelize');
+require('dotenv').config();
+
+const sequelize = new Sequelize(
+  process.env.DB_NAME,
+  process.env.DB_USER,
+  process.env.DB_PASSWORD,
+  {
+    host: process.env.DB_HOST,
+    port: process.env.DB_PORT,
+    dialect: 'postgres',
+    logging: process.env.NODE_ENV === 'development' ? console.log : false,
+    pool: {
+      max: 5,
+      min: 0,
+      acquire: 30000,
+      idle: 10000
+    },
+    define: {
+      timestamps: true,
+      underscored: true,
+      freezeTableName: true
+    }
+  }
+);
+
+// Test the connection
+const testConnection = async () => {
+  try {
+    await sequelize.authenticate();
+    console.log('Database connection has been established successfully.');
+  } catch (error) {
+    console.error('Unable to connect to the database:', error);
+  }
+};
+
+module.exports = { sequelize, testConnection };

+ 211 - 0
src/controllers/candleController.js

@@ -0,0 +1,211 @@
+const { Candle1h, Symbol } = require('../models');
+const { Op } = require('sequelize');
+
+class CandleController {
+  // Get candles for a symbol
+  async getCandles(req, res, next) {
+    try {
+      const {
+        symbolId,
+        startTime,
+        endTime,
+        limit = 100,
+        offset = 0
+      } = req.query;
+
+      // Verify symbol exists
+      const symbol = await Symbol.findByPk(symbolId);
+      if (!symbol) {
+        const error = new Error('Symbol not found');
+        error.statusCode = 404;
+        return next(error);
+      }
+
+      const where = { symbolId: parseInt(symbolId) };
+
+      if (startTime) {
+        where.openTime = {
+          ...where.openTime,
+          [Op.gte]: new Date(startTime)
+        };
+      }
+
+      if (endTime) {
+        where.openTime = {
+          ...where.openTime,
+          [Op.lte]: new Date(endTime)
+        };
+      }
+
+      const candles = await Candle1h.findAndCountAll({
+        where,
+        limit: parseInt(limit),
+        offset: parseInt(offset),
+        order: [['openTime', 'DESC']],
+        include: [{
+          model: Symbol,
+          as: 'symbol',
+          attributes: ['symbol', 'exchange', 'instrumentType']
+        }]
+      });
+
+      res.json({
+        success: true,
+        data: candles.rows,
+        pagination: {
+          total: candles.count,
+          limit: parseInt(limit),
+          offset: parseInt(offset),
+          hasMore: offset + candles.rows.length < candles.count
+        }
+      });
+    } catch (error) {
+      next(error);
+    }
+  }
+
+  // Get latest candle for a symbol
+  async getLatestCandle(req, res, next) {
+    try {
+      const { symbolId } = req.params;
+
+      // Verify symbol exists
+      const symbol = await Symbol.findByPk(symbolId);
+      if (!symbol) {
+        const error = new Error('Symbol not found');
+        error.statusCode = 404;
+        return next(error);
+      }
+
+      const candle = await Candle1h.findOne({
+        where: { symbolId: parseInt(symbolId) },
+        order: [['openTime', 'DESC']],
+        include: [{
+          model: Symbol,
+          as: 'symbol',
+          attributes: ['symbol', 'exchange', 'instrumentType']
+        }]
+      });
+
+      if (!candle) {
+        const error = new Error('No candle data found for this symbol');
+        error.statusCode = 404;
+        return next(error);
+      }
+
+      res.json({
+        success: true,
+        data: candle
+      });
+    } catch (error) {
+      next(error);
+    }
+  }
+
+  // Create new candle
+  async createCandle(req, res, next) {
+    try {
+      const candleData = {
+        ...req.body,
+        symbolId: parseInt(req.body.symbolId)
+      };
+
+      // Verify symbol exists
+      const symbol = await Symbol.findByPk(candleData.symbolId);
+      if (!symbol) {
+        const error = new Error('Symbol not found');
+        error.statusCode = 404;
+        return next(error);
+      }
+
+      const candle = await Candle1h.create(candleData);
+
+      res.status(201).json({
+        success: true,
+        data: candle,
+        message: 'Candle created successfully'
+      });
+    } catch (error) {
+      next(error);
+    }
+  }
+
+  // Bulk create candles
+  async bulkCreateCandles(req, res, next) {
+    try {
+      const { candles } = req.body;
+
+      if (!Array.isArray(candles) || candles.length === 0) {
+        const error = new Error('Candles array is required');
+        error.statusCode = 400;
+        return next(error);
+      }
+
+      // Verify all symbols exist
+      const symbolIds = [...new Set(candles.map(c => c.symbolId))];
+      const existingSymbols = await Symbol.findAll({
+        where: { id: symbolIds },
+        attributes: ['id']
+      });
+
+      const existingSymbolIds = existingSymbols.map(s => s.id);
+      const invalidSymbolIds = symbolIds.filter(id => !existingSymbolIds.includes(id));
+
+      if (invalidSymbolIds.length > 0) {
+        const error = new Error(`Invalid symbol IDs: ${invalidSymbolIds.join(', ')}`);
+        error.statusCode = 400;
+        return next(error);
+      }
+
+      const createdCandles = await Candle1h.bulkCreate(candles);
+
+      res.status(201).json({
+        success: true,
+        data: createdCandles,
+        message: `${createdCandles.length} candles created successfully`
+      });
+    } catch (error) {
+      next(error);
+    }
+  }
+
+  // Get OHLC data aggregated by time period
+  async getOHLC(req, res, next) {
+    try {
+      const { symbolId, period = '1h', limit = 100 } = req.query;
+
+      // Verify symbol exists
+      const symbol = await Symbol.findByPk(symbolId);
+      if (!symbol) {
+        const error = new Error('Symbol not found');
+        error.statusCode = 404;
+        return next(error);
+      }
+
+      // For now, only support 1h period since we only have candles_1h table
+      if (period !== '1h') {
+        const error = new Error('Only 1h period is currently supported');
+        error.statusCode = 400;
+        return next(error);
+      }
+
+      const candles = await Candle1h.findAll({
+        where: { symbolId: parseInt(symbolId) },
+        limit: parseInt(limit),
+        order: [['openTime', 'DESC']],
+        attributes: ['openTime', 'open', 'high', 'low', 'close', 'volume']
+      });
+
+      res.json({
+        success: true,
+        data: candles,
+        period,
+        symbol: symbol.symbol
+      });
+    } catch (error) {
+      next(error);
+    }
+  }
+}
+
+module.exports = new CandleController();

+ 251 - 0
src/controllers/livePriceController.js

@@ -0,0 +1,251 @@
+const { LivePrice, Symbol } = require('../models');
+
+class LivePriceController {
+  // Get all live prices
+  async getAllLivePrices(req, res, next) {
+    try {
+      const { limit = 100, offset = 0 } = req.query;
+
+      const livePrices = await LivePrice.findAndCountAll({
+        limit: parseInt(limit),
+        offset: parseInt(offset),
+        order: [['lastUpdated', 'DESC']],
+        include: [{
+          model: Symbol,
+          as: 'symbol',
+          attributes: ['symbol', 'exchange', 'instrumentType', 'isActive']
+        }]
+      });
+
+      res.json({
+        success: true,
+        data: livePrices.rows,
+        pagination: {
+          total: livePrices.count,
+          limit: parseInt(limit),
+          offset: parseInt(offset),
+          hasMore: offset + livePrices.rows.length < livePrices.count
+        }
+      });
+    } catch (error) {
+      next(error);
+    }
+  }
+
+  // Get live price for a specific symbol
+  async getLivePrice(req, res, next) {
+    try {
+      const { symbolId } = req.params;
+
+      const livePrice = await LivePrice.findOne({
+        where: { symbolId: parseInt(symbolId) },
+        include: [{
+          model: Symbol,
+          as: 'symbol',
+          attributes: ['symbol', 'exchange', 'instrumentType', 'isActive']
+        }]
+      });
+
+      if (!livePrice) {
+        const error = new Error('Live price not found for this symbol');
+        error.statusCode = 404;
+        return next(error);
+      }
+
+      res.json({
+        success: true,
+        data: livePrice
+      });
+    } catch (error) {
+      next(error);
+    }
+  }
+
+  // Update or create live price
+  async upsertLivePrice(req, res, next) {
+    try {
+      const { symbolId, price, bid, ask, bidSize, askSize } = req.body;
+
+      // Verify symbol exists and is active
+      const symbol = await Symbol.findByPk(symbolId);
+      if (!symbol) {
+        const error = new Error('Symbol not found');
+        error.statusCode = 404;
+        return next(error);
+      }
+
+      if (!symbol.isActive) {
+        const error = new Error('Cannot update live price for inactive symbol');
+        error.statusCode = 400;
+        return next(error);
+      }
+
+      const [livePrice, created] = await LivePrice.upsert({
+        symbolId: parseInt(symbolId),
+        price,
+        bid,
+        ask,
+        bidSize,
+        askSize,
+        lastUpdated: new Date()
+      });
+
+      res.status(created ? 201 : 200).json({
+        success: true,
+        data: livePrice,
+        message: created ? 'Live price created successfully' : 'Live price updated successfully'
+      });
+    } catch (error) {
+      next(error);
+    }
+  }
+
+  // Bulk update live prices
+  async bulkUpdateLivePrices(req, res, next) {
+    try {
+      const { prices } = req.body;
+
+      if (!Array.isArray(prices) || prices.length === 0) {
+        const error = new Error('Prices array is required');
+        error.statusCode = 400;
+        return next(error);
+      }
+
+      // Verify all symbols exist and are active
+      const symbolIds = prices.map(p => p.symbolId);
+      const existingSymbols = await Symbol.findAll({
+        where: {
+          id: symbolIds,
+          isActive: true
+        },
+        attributes: ['id']
+      });
+
+      const existingSymbolIds = existingSymbols.map(s => s.id);
+      const invalidSymbolIds = symbolIds.filter(id => !existingSymbolIds.includes(id));
+
+      if (invalidSymbolIds.length > 0) {
+        const error = new Error(`Invalid or inactive symbol IDs: ${invalidSymbolIds.join(', ')}`);
+        error.statusCode = 400;
+        return next(error);
+      }
+
+      // Prepare data for bulk upsert
+      const upsertData = prices.map(price => ({
+        symbolId: parseInt(price.symbolId),
+        price: price.price,
+        bid: price.bid,
+        ask: price.ask,
+        bidSize: price.bidSize,
+        askSize: price.askSize,
+        lastUpdated: new Date()
+      }));
+
+      const updatedPrices = [];
+      for (const data of upsertData) {
+        const [livePrice] = await LivePrice.upsert(data);
+        updatedPrices.push(livePrice);
+      }
+
+      res.json({
+        success: true,
+        data: updatedPrices,
+        message: `${updatedPrices.length} live prices updated successfully`
+      });
+    } catch (error) {
+      next(error);
+    }
+  }
+
+  // Get live prices by exchange
+  async getLivePricesByExchange(req, res, next) {
+    try {
+      const { exchange } = req.params;
+      const { limit = 100, offset = 0 } = req.query;
+
+      const livePrices = await LivePrice.findAndCountAll({
+        limit: parseInt(limit),
+        offset: parseInt(offset),
+        order: [['lastUpdated', 'DESC']],
+        include: [{
+          model: Symbol,
+          as: 'symbol',
+          where: { exchange, isActive: true },
+          attributes: ['symbol', 'exchange', 'instrumentType']
+        }]
+      });
+
+      res.json({
+        success: true,
+        data: livePrices.rows,
+        pagination: {
+          total: livePrices.count,
+          limit: parseInt(limit),
+          offset: parseInt(offset),
+          hasMore: offset + livePrices.rows.length < livePrices.count
+        }
+      });
+    } catch (error) {
+      next(error);
+    }
+  }
+
+  // Get live prices by instrument type
+  async getLivePricesByType(req, res, next) {
+    try {
+      const { type } = req.params;
+      const { limit = 100, offset = 0 } = req.query;
+
+      const livePrices = await LivePrice.findAndCountAll({
+        limit: parseInt(limit),
+        offset: parseInt(offset),
+        order: [['lastUpdated', 'DESC']],
+        include: [{
+          model: Symbol,
+          as: 'symbol',
+          where: { instrumentType: type, isActive: true },
+          attributes: ['symbol', 'exchange', 'instrumentType']
+        }]
+      });
+
+      res.json({
+        success: true,
+        data: livePrices.rows,
+        pagination: {
+          total: livePrices.count,
+          limit: parseInt(limit),
+          offset: parseInt(offset),
+          hasMore: offset + livePrices.rows.length < livePrices.count
+        }
+      });
+    } catch (error) {
+      next(error);
+    }
+  }
+
+  // Delete live price for a symbol
+  async deleteLivePrice(req, res, next) {
+    try {
+      const { symbolId } = req.params;
+
+      const deletedRowsCount = await LivePrice.destroy({
+        where: { symbolId: parseInt(symbolId) }
+      });
+
+      if (deletedRowsCount === 0) {
+        const error = new Error('Live price not found for this symbol');
+        error.statusCode = 404;
+        return next(error);
+      }
+
+      res.json({
+        success: true,
+        message: 'Live price deleted successfully'
+      });
+    } catch (error) {
+      next(error);
+    }
+  }
+}
+
+module.exports = new LivePriceController();

+ 164 - 0
src/controllers/symbolController.js

@@ -0,0 +1,164 @@
+const { Symbol } = require('../models');
+const { Op } = require('sequelize');
+
+class SymbolController {
+  // Get all symbols with optional filtering
+  async getAllSymbols(req, res, next) {
+    try {
+      const {
+        exchange,
+        instrumentType,
+        isActive = true,
+        limit = 100,
+        offset = 0
+      } = req.query;
+
+      const where = {};
+      if (exchange) where.exchange = exchange;
+      if (instrumentType) where.instrumentType = instrumentType;
+      if (isActive !== undefined) where.isActive = isActive === 'true';
+
+      const symbols = await Symbol.findAndCountAll({
+        where,
+        limit: parseInt(limit),
+        offset: parseInt(offset),
+        order: [['symbol', 'ASC']]
+      });
+
+      res.json({
+        success: true,
+        data: symbols.rows,
+        pagination: {
+          total: symbols.count,
+          limit: parseInt(limit),
+          offset: parseInt(offset),
+          hasMore: offset + symbols.rows.length < symbols.count
+        }
+      });
+    } catch (error) {
+      next(error);
+    }
+  }
+
+  // Get symbol by ID
+  async getSymbolById(req, res, next) {
+    try {
+      const { id } = req.params;
+
+      const symbol = await Symbol.findByPk(id);
+
+      if (!symbol) {
+        const error = new Error('Symbol not found');
+        error.statusCode = 404;
+        return next(error);
+      }
+
+      res.json({
+        success: true,
+        data: symbol
+      });
+    } catch (error) {
+      next(error);
+    }
+  }
+
+  // Create new symbol
+  async createSymbol(req, res, next) {
+    try {
+      const symbol = await Symbol.create(req.body);
+
+      res.status(201).json({
+        success: true,
+        data: symbol,
+        message: 'Symbol created successfully'
+      });
+    } catch (error) {
+      next(error);
+    }
+  }
+
+  // Update symbol
+  async updateSymbol(req, res, next) {
+    try {
+      const { id } = req.params;
+
+      const [updatedRowsCount] = await Symbol.update(req.body, {
+        where: { id }
+      });
+
+      if (updatedRowsCount === 0) {
+        const error = new Error('Symbol not found');
+        error.statusCode = 404;
+        return next(error);
+      }
+
+      const updatedSymbol = await Symbol.findByPk(id);
+
+      res.json({
+        success: true,
+        data: updatedSymbol,
+        message: 'Symbol updated successfully'
+      });
+    } catch (error) {
+      next(error);
+    }
+  }
+
+  // Delete symbol (soft delete by setting isActive to false)
+  async deleteSymbol(req, res, next) {
+    try {
+      const { id } = req.params;
+
+      const [updatedRowsCount] = await Symbol.update(
+        { isActive: false },
+        { where: { id } }
+      );
+
+      if (updatedRowsCount === 0) {
+        const error = new Error('Symbol not found');
+        error.statusCode = 404;
+        return next(error);
+      }
+
+      res.json({
+        success: true,
+        message: 'Symbol deactivated successfully'
+      });
+    } catch (error) {
+      next(error);
+    }
+  }
+
+  // Search symbols by symbol name
+  async searchSymbols(req, res, next) {
+    try {
+      const { q, limit = 20 } = req.query;
+
+      if (!q) {
+        const error = new Error('Search query is required');
+        error.statusCode = 400;
+        return next(error);
+      }
+
+      const symbols = await Symbol.findAll({
+        where: {
+          symbol: {
+            [Op.iLike]: `%${q}%`
+          },
+          isActive: true
+        },
+        limit: parseInt(limit),
+        order: [['symbol', 'ASC']]
+      });
+
+      res.json({
+        success: true,
+        data: symbols
+      });
+    } catch (error) {
+      next(error);
+    }
+  }
+}
+
+module.exports = new SymbolController();

+ 60 - 0
src/middleware/errorHandler.js

@@ -0,0 +1,60 @@
+const errorHandler = (err, req, res, next) => {
+  console.error(err.stack);
+
+  // Sequelize validation error
+  if (err.name === 'SequelizeValidationError') {
+    const errors = err.errors.map(error => ({
+      field: error.path,
+      message: error.message,
+      value: error.value
+    }));
+
+    return res.status(400).json({
+      success: false,
+      message: 'Validation error',
+      errors
+    });
+  }
+
+  // Sequelize unique constraint error
+  if (err.name === 'SequelizeUniqueConstraintError') {
+    return res.status(409).json({
+      success: false,
+      message: 'Duplicate entry',
+      error: err.errors[0].message
+    });
+  }
+
+  // Sequelize foreign key constraint error
+  if (err.name === 'SequelizeForeignKeyConstraintError') {
+    return res.status(400).json({
+      success: false,
+      message: 'Foreign key constraint error',
+      error: err.message
+    });
+  }
+
+  // Joi validation error
+  if (err.isJoi) {
+    return res.status(400).json({
+      success: false,
+      message: 'Validation error',
+      details: err.details.map(detail => ({
+        field: detail.path.join('.'),
+        message: detail.message
+      }))
+    });
+  }
+
+  // Default error
+  const statusCode = err.statusCode || 500;
+  const message = err.message || 'Internal server error';
+
+  res.status(statusCode).json({
+    success: false,
+    message,
+    ...(process.env.NODE_ENV === 'development' && { stack: err.stack })
+  });
+};
+
+module.exports = errorHandler;

+ 90 - 0
src/middleware/validation.js

@@ -0,0 +1,90 @@
+const Joi = require('joi');
+
+// Symbol validation schemas
+const symbolSchema = Joi.object({
+  symbol: Joi.string().max(50).required(),
+  baseAsset: Joi.string().max(50),
+  quoteAsset: Joi.string().max(50),
+  exchange: Joi.string().max(50),
+  instrumentType: Joi.string().valid('crypto', 'stock', 'forex', 'commodity').required(),
+  isActive: Joi.boolean().default(true)
+});
+
+const symbolIdSchema = Joi.object({
+  id: Joi.number().integer().positive().required()
+});
+
+// Candle validation schemas
+const candleQuerySchema = Joi.object({
+  symbolId: Joi.number().integer().positive().required(),
+  startTime: Joi.date().iso(),
+  endTime: Joi.date().iso().when('startTime', {
+    is: Joi.exist(),
+    then: Joi.date().iso().greater(Joi.ref('startTime'))
+  }),
+  limit: Joi.number().integer().min(1).max(1000).default(100),
+  offset: Joi.number().integer().min(0).default(0)
+});
+
+// Live price validation schemas
+const livePriceSchema = Joi.object({
+  symbolId: Joi.number().integer().positive().required(),
+  price: Joi.number().precision(8).positive().required(),
+  bid: Joi.number().precision(8).positive(),
+  ask: Joi.number().precision(8).positive(),
+  bidSize: Joi.number().precision(8).positive(),
+  askSize: Joi.number().precision(8).positive()
+});
+
+// Middleware functions
+const validate = (schema) => {
+  return (req, res, next) => {
+    const { error, value } = schema.validate(req.body, { abortEarly: false });
+
+    if (error) {
+      error.isJoi = true;
+      return next(error);
+    }
+
+    req.body = value;
+    next();
+  };
+};
+
+const validateQuery = (schema) => {
+  return (req, res, next) => {
+    const { error, value } = schema.validate(req.query, { abortEarly: false });
+
+    if (error) {
+      error.isJoi = true;
+      return next(error);
+    }
+
+    req.query = value;
+    next();
+  };
+};
+
+const validateParams = (schema) => {
+  return (req, res, next) => {
+    const { error, value } = schema.validate(req.params, { abortEarly: false });
+
+    if (error) {
+      error.isJoi = true;
+      return next(error);
+    }
+
+    req.params = value;
+    next();
+  };
+};
+
+module.exports = {
+  symbolSchema,
+  symbolIdSchema,
+  candleQuerySchema,
+  livePriceSchema,
+  validate,
+  validateQuery,
+  validateParams
+};

+ 73 - 0
src/models/Candle1h.js

@@ -0,0 +1,73 @@
+const { DataTypes } = require('sequelize');
+const { sequelize } = require('../config/database');
+const Symbol = require('./Symbol');
+
+const Candle1h = sequelize.define('Candle1h', {
+  id: {
+    type: DataTypes.BIGINT,
+    primaryKey: true,
+    autoIncrement: true
+  },
+  symbolId: {
+    type: DataTypes.INTEGER,
+    field: 'symbol_id',
+    allowNull: false,
+    references: {
+      model: Symbol,
+      key: 'id'
+    }
+  },
+  openTime: {
+    type: DataTypes.DATE,
+    field: 'open_time',
+    allowNull: false
+  },
+  closeTime: {
+    type: DataTypes.DATE,
+    field: 'close_time',
+    allowNull: false
+  },
+  open: {
+    type: DataTypes.DECIMAL(18, 8),
+    allowNull: false
+  },
+  high: {
+    type: DataTypes.DECIMAL(18, 8),
+    allowNull: false
+  },
+  low: {
+    type: DataTypes.DECIMAL(18, 8),
+    allowNull: false
+  },
+  close: {
+    type: DataTypes.DECIMAL(18, 8),
+    allowNull: false
+  },
+  volume: {
+    type: DataTypes.DECIMAL(20, 8)
+  },
+  tradesCount: {
+    type: DataTypes.INTEGER,
+    field: 'trades_count'
+  },
+  quoteVolume: {
+    type: DataTypes.DECIMAL(20, 8),
+    field: 'quote_volume'
+  },
+  createdAt: {
+    type: DataTypes.DATE,
+    field: 'created_at'
+  }
+}, {
+  tableName: 'candles_1h',
+  indexes: [
+    { unique: true, fields: ['symbol_id', 'open_time'] },
+    { fields: ['open_time'] }
+  ]
+});
+
+// Define associations
+Candle1h.belongsTo(Symbol, { foreignKey: 'symbolId', as: 'symbol' });
+Symbol.hasMany(Candle1h, { foreignKey: 'symbolId', as: 'candles1h' });
+
+module.exports = Candle1h;

+ 49 - 0
src/models/LivePrice.js

@@ -0,0 +1,49 @@
+const { DataTypes } = require('sequelize');
+const { sequelize } = require('../config/database');
+const Symbol = require('./Symbol');
+
+const LivePrice = sequelize.define('LivePrice', {
+  symbolId: {
+    type: DataTypes.INTEGER,
+    primaryKey: true,
+    field: 'symbol_id',
+    references: {
+      model: Symbol,
+      key: 'id'
+    }
+  },
+  price: {
+    type: DataTypes.DECIMAL(18, 8),
+    allowNull: false
+  },
+  bid: {
+    type: DataTypes.DECIMAL(18, 8)
+  },
+  ask: {
+    type: DataTypes.DECIMAL(18, 8)
+  },
+  bidSize: {
+    type: DataTypes.DECIMAL(18, 8),
+    field: 'bid_size'
+  },
+  askSize: {
+    type: DataTypes.DECIMAL(18, 8),
+    field: 'ask_size'
+  },
+  lastUpdated: {
+    type: DataTypes.DATE,
+    field: 'last_updated',
+    defaultValue: DataTypes.NOW
+  }
+}, {
+  tableName: 'live_prices',
+  indexes: [
+    { fields: ['price'] }
+  ]
+});
+
+// Define associations
+LivePrice.belongsTo(Symbol, { foreignKey: 'symbolId', as: 'symbol' });
+Symbol.hasOne(LivePrice, { foreignKey: 'symbolId', as: 'livePrice' });
+
+module.exports = LivePrice;

+ 52 - 0
src/models/Symbol.js

@@ -0,0 +1,52 @@
+const { DataTypes } = require('sequelize');
+const { sequelize } = require('../config/database');
+
+const Symbol = sequelize.define('Symbol', {
+  id: {
+    type: DataTypes.INTEGER,
+    primaryKey: true,
+    autoIncrement: true
+  },
+  symbol: {
+    type: DataTypes.STRING(50),
+    allowNull: false,
+    unique: true
+  },
+  baseAsset: {
+    type: DataTypes.STRING(50),
+    field: 'base_asset'
+  },
+  quoteAsset: {
+    type: DataTypes.STRING(50),
+    field: 'quote_asset'
+  },
+  exchange: {
+    type: DataTypes.STRING(50)
+  },
+  instrumentType: {
+    type: DataTypes.ENUM('crypto', 'stock', 'forex', 'commodity'),
+    field: 'instrument_type',
+    allowNull: false
+  },
+  isActive: {
+    type: DataTypes.BOOLEAN,
+    field: 'is_active',
+    defaultValue: true
+  },
+  createdAt: {
+    type: DataTypes.DATE,
+    field: 'created_at'
+  },
+  updatedAt: {
+    type: DataTypes.DATE,
+    field: 'updated_at'
+  }
+}, {
+  tableName: 'symbols',
+  indexes: [
+    { fields: ['exchange'] },
+    { fields: ['instrument_type'] }
+  ]
+});
+
+module.exports = Symbol;

+ 29 - 0
src/models/index.js

@@ -0,0 +1,29 @@
+const { sequelize } = require('../config/database');
+const Symbol = require('./Symbol');
+const Candle1h = require('./Candle1h');
+const LivePrice = require('./LivePrice');
+
+// Define associations
+Symbol.hasMany(Candle1h, { foreignKey: 'symbolId', as: 'candles1h' });
+Candle1h.belongsTo(Symbol, { foreignKey: 'symbolId', as: 'symbol' });
+
+Symbol.hasOne(LivePrice, { foreignKey: 'symbolId', as: 'livePrice' });
+LivePrice.belongsTo(Symbol, { foreignKey: 'symbolId', as: 'symbol' });
+
+// Sync database (only in development)
+if (process.env.NODE_ENV === 'development') {
+  sequelize.sync({ alter: true })
+    .then(() => {
+      console.log('Database synchronized successfully.');
+    })
+    .catch((error) => {
+      console.error('Error synchronizing database:', error);
+    });
+}
+
+module.exports = {
+  sequelize,
+  Symbol,
+  Candle1h,
+  LivePrice
+};

+ 61 - 0
src/routes/candles.js

@@ -0,0 +1,61 @@
+const express = require('express');
+const router = express.Router();
+const candleController = require('../controllers/candleController');
+const { validate, validateQuery, validateParams } = require('../middleware/validation');
+const Joi = require('joi');
+
+// GET /api/candles - Get candles with filtering
+router.get('/', validateQuery(Joi.object({
+  symbolId: Joi.number().integer().positive().required(),
+  startTime: Joi.date().iso(),
+  endTime: Joi.date().iso().when('startTime', {
+    is: Joi.exist(),
+    then: Joi.date().iso().greater(Joi.ref('startTime'))
+  }),
+  limit: Joi.number().integer().min(1).max(1000).default(100),
+  offset: Joi.number().integer().min(0).default(0)
+})), candleController.getCandles);
+
+// GET /api/candles/ohlc - Get OHLC data
+router.get('/ohlc', validateQuery(Joi.object({
+  symbolId: Joi.number().integer().positive().required(),
+  period: Joi.string().valid('1h').default('1h'),
+  limit: Joi.number().integer().min(1).max(1000).default(100)
+})), candleController.getOHLC);
+
+// GET /api/candles/:symbolId/latest - Get latest candle for a symbol
+router.get('/:symbolId/latest', validateParams(Joi.object({
+  symbolId: Joi.number().integer().positive().required()
+})), candleController.getLatestCandle);
+
+// POST /api/candles - Create new candle
+router.post('/', validate(Joi.object({
+  symbolId: Joi.number().integer().positive().required(),
+  openTime: Joi.date().iso().required(),
+  closeTime: Joi.date().iso().required(),
+  open: Joi.number().precision(8).positive().required(),
+  high: Joi.number().precision(8).positive().required(),
+  low: Joi.number().precision(8).positive().required(),
+  close: Joi.number().precision(8).positive().required(),
+  volume: Joi.number().precision(8).positive(),
+  tradesCount: Joi.number().integer().min(0),
+  quoteVolume: Joi.number().precision(8).positive()
+})), candleController.createCandle);
+
+// POST /api/candles/bulk - Bulk create candles
+router.post('/bulk', validate(Joi.object({
+  candles: Joi.array().items(Joi.object({
+    symbolId: Joi.number().integer().positive().required(),
+    openTime: Joi.date().iso().required(),
+    closeTime: Joi.date().iso().required(),
+    open: Joi.number().precision(8).positive().required(),
+    high: Joi.number().precision(8).positive().required(),
+    low: Joi.number().precision(8).positive().required(),
+    close: Joi.number().precision(8).positive().required(),
+    volume: Joi.number().precision(8).positive(),
+    tradesCount: Joi.number().integer().min(0),
+    quoteVolume: Joi.number().precision(8).positive()
+  })).min(1).required()
+})), candleController.bulkCreateCandles);
+
+module.exports = router;

+ 47 - 0
src/routes/livePrices.js

@@ -0,0 +1,47 @@
+const express = require('express');
+const router = express.Router();
+const livePriceController = require('../controllers/livePriceController');
+const { validate, validateQuery, validateParams, livePriceSchema } = require('../middleware/validation');
+const Joi = require('joi');
+
+// GET /api/live-prices - Get all live prices
+router.get('/', validateQuery(Joi.object({
+  limit: Joi.number().integer().min(1).max(1000).default(100),
+  offset: Joi.number().integer().min(0).default(0)
+})), livePriceController.getAllLivePrices);
+
+// GET /api/live-prices/exchange/:exchange - Get live prices by exchange
+router.get('/exchange/:exchange', validateParams(Joi.object({
+  exchange: Joi.string().max(50).required()
+})), validateQuery(Joi.object({
+  limit: Joi.number().integer().min(1).max(1000).default(100),
+  offset: Joi.number().integer().min(0).default(0)
+})), livePriceController.getLivePricesByExchange);
+
+// GET /api/live-prices/type/:type - Get live prices by instrument type
+router.get('/type/:type', validateParams(Joi.object({
+  type: Joi.string().valid('crypto', 'stock', 'forex', 'commodity').required()
+})), validateQuery(Joi.object({
+  limit: Joi.number().integer().min(1).max(1000).default(100),
+  offset: Joi.number().integer().min(0).default(0)
+})), livePriceController.getLivePricesByType);
+
+// GET /api/live-prices/:symbolId - Get live price for a specific symbol
+router.get('/:symbolId', validateParams(Joi.object({
+  symbolId: Joi.number().integer().positive().required()
+})), livePriceController.getLivePrice);
+
+// POST /api/live-prices - Create or update live price
+router.post('/', validate(livePriceSchema), livePriceController.upsertLivePrice);
+
+// POST /api/live-prices/bulk - Bulk update live prices
+router.post('/bulk', validate(Joi.object({
+  prices: Joi.array().items(livePriceSchema).min(1).required()
+})), livePriceController.bulkUpdateLivePrices);
+
+// DELETE /api/live-prices/:symbolId - Delete live price for a symbol
+router.delete('/:symbolId', validateParams(Joi.object({
+  symbolId: Joi.number().integer().positive().required()
+})), livePriceController.deleteLivePrice);
+
+module.exports = router;

+ 33 - 0
src/routes/symbols.js

@@ -0,0 +1,33 @@
+const express = require('express');
+const router = express.Router();
+const symbolController = require('../controllers/symbolController');
+const { validate, validateQuery, validateParams, symbolSchema, symbolIdSchema } = require('../middleware/validation');
+
+// GET /api/symbols - Get all symbols with optional filtering
+router.get('/', validateQuery(require('joi').object({
+  exchange: require('joi').string().max(50),
+  instrumentType: require('joi').string().valid('crypto', 'stock', 'forex', 'commodity'),
+  isActive: require('joi').string().valid('true', 'false'),
+  limit: require('joi').number().integer().min(1).max(1000).default(100),
+  offset: require('joi').number().integer().min(0).default(0)
+})), symbolController.getAllSymbols);
+
+// GET /api/symbols/search - Search symbols by name
+router.get('/search', validateQuery(require('joi').object({
+  q: require('joi').string().required(),
+  limit: require('joi').number().integer().min(1).max(100).default(20)
+})), symbolController.searchSymbols);
+
+// GET /api/symbols/:id - Get symbol by ID
+router.get('/:id', validateParams(symbolIdSchema), symbolController.getSymbolById);
+
+// POST /api/symbols - Create new symbol
+router.post('/', validate(symbolSchema), symbolController.createSymbol);
+
+// PUT /api/symbols/:id - Update symbol
+router.put('/:id', validateParams(symbolIdSchema), validate(symbolSchema), symbolController.updateSymbol);
+
+// DELETE /api/symbols/:id - Delete symbol (soft delete)
+router.delete('/:id', validateParams(symbolIdSchema), symbolController.deleteSymbol);
+
+module.exports = router;

+ 55 - 0
src/server.js

@@ -0,0 +1,55 @@
+const app = require('./app');
+const { testConnection } = require('./config/database');
+const logger = require('./utils/logger');
+
+const PORT = process.env.PORT || 3000;
+
+// Test database connection and start server
+const startServer = async () => {
+  try {
+    // Test database connection
+    await testConnection();
+
+    // Start the server
+    const server = app.listen(PORT, () => {
+      logger.info(`Market Data Service is running on port ${PORT}`);
+      logger.info(`Environment: ${process.env.NODE_ENV || 'development'}`);
+      logger.info(`Health check available at: http://localhost:${PORT}/health`);
+    });
+
+    // Graceful shutdown
+    process.on('SIGTERM', () => {
+      logger.info('SIGTERM received, shutting down gracefully');
+      server.close(() => {
+        logger.info('Process terminated');
+        process.exit(0);
+      });
+    });
+
+    process.on('SIGINT', () => {
+      logger.info('SIGINT received, shutting down gracefully');
+      server.close(() => {
+        logger.info('Process terminated');
+        process.exit(0);
+      });
+    });
+
+  } catch (error) {
+    logger.error('Failed to start server:', error);
+    process.exit(1);
+  }
+};
+
+// Handle uncaught exceptions
+process.on('uncaughtException', (error) => {
+  logger.error('Uncaught Exception:', error);
+  process.exit(1);
+});
+
+// Handle unhandled promise rejections
+process.on('unhandledRejection', (reason, promise) => {
+  logger.error('Unhandled Rejection at:', promise, 'reason:', reason);
+  process.exit(1);
+});
+
+startServer();

+ 54 - 0
src/utils/logger.js

@@ -0,0 +1,54 @@
+const winston = require('winston');
+
+// Define log levels
+const levels = {
+  error: 0,
+  warn: 1,
+  info: 2,
+  http: 3,
+  debug: 4,
+};
+
+// Define colors for each level
+const colors = {
+  error: 'red',
+  warn: 'yellow',
+  info: 'green',
+  http: 'magenta',
+  debug: 'white',
+};
+
+// Tell winston that we want to link the colors
+winston.addColors(colors);
+
+// Define the format for logs
+const format = winston.format.combine(
+  winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss:ms' }),
+  winston.format.colorize({ all: true }),
+  winston.format.printf(
+    (info) => `${info.timestamp} ${info.level}: ${info.message}`,
+  ),
+);
+
+// Define which transports the logger must use
+const transports = [
+  // Allow the use the console to print the messages
+  new winston.transports.Console(),
+  // Allow to print all the error level messages inside the error.log file
+  new winston.transports.File({
+    filename: 'logs/error.log',
+    level: 'error',
+  }),
+  // Allow to print all the error message inside the all.log file
+  new winston.transports.File({ filename: 'logs/all.log' }),
+];
+
+// Create the logger instance
+const logger = winston.createLogger({
+  level: process.env.NODE_ENV === 'development' ? 'debug' : 'warn',
+  levels,
+  format,
+  transports,
+});
+
+module.exports = logger;