Переглянути джерело

feat: overhaul project structure with Sequelize integration

- README.md:
  • Added Supertest and Sequelize CLI to tooling
  • Updated database setup instructions to use migrations
  • Enhanced testing and migration documentation
  • Improved development workflow commands

- config/config.json:
  • Added database configuration for development/test/production environments

- models/index.js:
  • Implemented Sequelize model loader and association setup

- package.json:
  • Added devDependencies: @jest/globals, jest, sequelize-cli, supertest

- src/models/Candle1h.js:
  • Enhanced model definition with validations
  • Added association configurations

- src/models/LivePrice.js:
  • Improved model structure with timestamp handling
  • Added foreign key constraints

- tests/candleController.test.js:
  • Created initial test suite for candle endpoints
  • Implemented database cleanup routines
  • Added CRUD operation tests with Supertest

- General:
  • Standardized Sequelize configuration across environments
  • Prepared project for migration-based database management
Hussain Afzal 9 місяців тому
батько
коміт
eda6f70d7e
10 змінених файлів з 5044 додано та 840 видалено
  1. 48 13
      README.md
  2. 20 0
      config/config.json
  3. 43 0
      models/index.js
  4. 4822 819
      package-lock.json
  5. 6 0
      package.json
  6. 1 1
      src/app.js
  7. 0 3
      src/models/Candle1h.js
  8. 0 3
      src/models/LivePrice.js
  9. 1 1
      src/models/index.js
  10. 103 0
      tests/candleController.test.js

+ 48 - 13
README.md

@@ -53,8 +53,8 @@ The service includes an MT5 Expert Advisor (EA) that automatically sends histori
 - **Validation**: Joi
 - **Security**: Helmet, CORS
 - **Logging**: Winston, Morgan
-- **Testing**: Jest
-- **Development**: Nodemon, ESLint
+- **Testing**: Jest, Supertest
+- **Development**: Nodemon, ESLint, Sequelize CLI
 
 ## Project Structure
 
@@ -110,13 +110,13 @@ market-data-service/
    ```
    Edit `.env` with your database credentials and other configuration.
 
-4. **Set up the database**
+4. **Database setup**
    ```bash
    # Create PostgreSQL database
    createdb market_data
 
-   # Run the schema
-   psql -d market_data -f schema.sql
+   # Run migrations
+   npx sequelize-cli db:migrate
    ```
 
 5. **Start the development server**
@@ -182,15 +182,23 @@ CORS_ORIGIN=*
 - `POST /api/live-prices/bulk` - Bulk update live prices
 - `DELETE /api/live-prices/:symbolId` - Delete live price
 
-## Database Schema
+## Database Management
 
-The service uses three main tables:
+The database schema is managed through Sequelize migrations and models:
 
-1. **symbols** - Trading symbols metadata
-2. **candles_1h** - Hourly OHLCV data
-3. **live_prices** - Current market prices
+- Models are defined in `src/models/`
+- Migrations are stored in `migrations/`
+- Associations are configured in `src/models/index.js`
 
-See `schema.sql` for complete table definitions.
+Key entities:
+- **Symbol**: Financial instrument metadata
+- **Candle1h**: Hourly OHLCV data
+- **LivePrice**: Real-time market prices
+
+To update the database schema:
+1. Create a new migration: `npx sequelize-cli migration:generate --name description`
+2. Implement schema changes in the migration file
+3. Run migrations: `npx sequelize-cli db:migrate`
 
 ## Development
 
@@ -198,8 +206,10 @@ See `schema.sql` for complete table definitions.
 
 - `npm start` - Start production server
 - `npm run dev` - Start development server with auto-reload
-- `npm test` - Run tests
+- `npm test` - Run Jest test suite
 - `npm run test:watch` - Run tests in watch mode
+- `npm run migrate` - Run database migrations
+- `npm run migrate:undo` - Revert last migration
 - `npm run lint` - Run ESLint
 - `npm run lint:fix` - Fix ESLint issues
 
@@ -209,7 +219,32 @@ This project uses ESLint for code linting. Run `npm run lint` to check for issue
 
 ### Testing
 
-Tests are written using Jest. Add your test files in the `tests/` directory.
+The project uses Jest with Supertest for endpoint testing. Key features:
+
+- Integration tests with database cleanup
+- Test environment database configuration
+- Automatic test isolation with `{ force: true }` sync
+
+Run tests:
+```bash
+npm test         # Run full test suite
+npm run test:watch  # Run in watch mode
+```
+
+### Database Migrations
+
+We use Sequelize CLI for database migrations:
+
+```bash
+# Create new migration
+npx sequelize-cli migration:generate --name your-migration-name
+
+# Run pending migrations
+npx sequelize-cli db:migrate
+
+# Revert last migration
+npx sequelize-cli db:migrate:undo
+```
 
 ## Deployment
 

+ 20 - 0
config/config.json

@@ -0,0 +1,20 @@
+{
+  "development": {
+    "username": "market_user",
+    "password": "market_password",
+    "database": "market_data",
+    "host": "127.0.0.1",
+    "dialect": "postgres"
+  },
+  "test": {
+    "username": "market_user",
+    "password": "market_password",
+    "database": "market_data",
+    "host": "127.0.0.1",
+    "dialect": "postgres"
+  },
+  "production": {
+    "use_env_variable": "DATABASE_URL",
+    "dialect": "postgres"
+  }
+}

+ 43 - 0
models/index.js

@@ -0,0 +1,43 @@
+'use strict';
+
+const fs = require('fs');
+const path = require('path');
+const Sequelize = require('sequelize');
+const process = require('process');
+const basename = path.basename(__filename);
+const env = process.env.NODE_ENV || 'development';
+const config = require(__dirname + '/../config/config.json')[env];
+const db = {};
+
+let sequelize;
+if (config.use_env_variable) {
+  sequelize = new Sequelize(process.env[config.use_env_variable], config);
+} else {
+  sequelize = new Sequelize(config.database, config.username, config.password, config);
+}
+
+fs
+  .readdirSync(__dirname)
+  .filter(file => {
+    return (
+      file.indexOf('.') !== 0 &&
+      file !== basename &&
+      file.slice(-3) === '.js' &&
+      file.indexOf('.test.js') === -1
+    );
+  })
+  .forEach(file => {
+    const model = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes);
+    db[model.name] = model;
+  });
+
+Object.keys(db).forEach(modelName => {
+  if (db[modelName].associate) {
+    db[modelName].associate(db);
+  }
+});
+
+db.sequelize = sequelize;
+db.Sequelize = Sequelize;
+
+module.exports = db;

Різницю між файлами не показано, бо вона завелика
+ 4822 - 819
package-lock.json


+ 6 - 0
package.json

@@ -31,5 +31,11 @@
     "pg": "^8.16.3",
     "sequelize": "^6.37.7",
     "winston": "^3.18.3"
+  },
+  "devDependencies": {
+    "@jest/globals": "^30.2.0",
+    "jest": "^30.2.0",
+    "sequelize-cli": "^6.6.3",
+    "supertest": "^7.1.4"
   }
 }

+ 1 - 1
src/app.js

@@ -57,7 +57,7 @@ app.use('/api/candles', candleRoutes);
 app.use('/api/live-prices', livePriceRoutes);
 
 // 404 handler
-app.use('*', (req, res) => {
+app.use((req, res) => {
   res.status(404).json({
     success: false,
     message: 'Route not found'

+ 0 - 3
src/models/Candle1h.js

@@ -66,8 +66,5 @@ const Candle1h = sequelize.define('Candle1h', {
   ]
 });
 
-// Define associations
-Candle1h.belongsTo(Symbol, { foreignKey: 'symbolId', as: 'symbol' });
-Symbol.hasMany(Candle1h, { foreignKey: 'symbolId', as: 'candles1h' });
 
 module.exports = Candle1h;

+ 0 - 3
src/models/LivePrice.js

@@ -42,8 +42,5 @@ const LivePrice = sequelize.define('LivePrice', {
   ]
 });
 
-// Define associations
-LivePrice.belongsTo(Symbol, { foreignKey: 'symbolId', as: 'symbol' });
-Symbol.hasOne(LivePrice, { foreignKey: 'symbolId', as: 'livePrice' });
 
 module.exports = LivePrice;

+ 1 - 1
src/models/index.js

@@ -8,7 +8,7 @@ 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' });
+LivePrice.belongsTo(Symbol, { foreignKey: 'symbolId', as: 'livePriceSymbol' });
 
 // Sync database (only in development)
 if (process.env.NODE_ENV === 'development') {

+ 103 - 0
tests/candleController.test.js

@@ -0,0 +1,103 @@
+const request = require('supertest');
+const app = require('../src/app');
+const { Candle1h, Symbol, sequelize } = require('../src/models');
+
+describe('Candle Controller Integration Tests', () => {
+  let testSymbol;
+
+  beforeAll(async () => {
+    // Sync database and cleanup
+    await sequelize.sync({ force: true });
+    
+    // Create fresh test symbol
+    testSymbol = await Symbol.create({
+      symbol: 'TEST_SYMBOL',
+      exchange: 'TEST',
+      instrumentType: 'forex'
+    });
+  });
+
+  afterAll(async () => {
+    // Cleanup test data
+    await Candle1h.destroy({ where: {} });
+    await Symbol.destroy({ where: {} });
+    await sequelize.close();
+  });
+
+  describe('POST /api/candles/bulk', () => {
+    it('should create multiple candles from MT5 payload', async () => {
+      const mockCandles = [
+        {
+          symbolId: testSymbol.id,
+          openTime: '2025-10-17 00:00:00',
+          closeTime: '2025-10-17 01:00:00',
+          open: 1.1000,
+          high: 1.1050,
+          low: 1.0990,
+          close: 1.1025,
+          volume: 1000
+        },
+        {
+          symbolId: testSymbol.id,
+          openTime: '2025-10-17 01:00:00',
+          closeTime: '2025-10-17 02:00:00',
+          open: 1.1025,
+          high: 1.1075,
+          low: 1.1005,
+          close: 1.1060,
+          volume: 1200
+        }
+      ];
+
+      const response = await request(app)
+        .post('/api/candles/bulk')
+        .send({ candles: mockCandles })
+        .expect(201);
+
+      expect(response.body.success).toBe(true);
+      expect(response.body.message).toBe('2 candles created successfully');
+      expect(response.body.data.length).toBe(2);
+
+      // Verify database persistence
+      const dbCandles = await Candle1h.findAll({
+        where: { symbolId: testSymbol.id },
+        order: [['openTime', 'ASC']]
+      });
+
+      expect(dbCandles.length).toBe(2);
+      expect(Number(dbCandles[0].open)).toBeCloseTo(1.1000);
+      expect(Number(dbCandles[1].close)).toBeCloseTo(1.1060);
+    });
+
+    it('should reject invalid payload format', async () => {
+      const response = await request(app)
+        .post('/api/candles/bulk')
+        .send({ invalid: 'payload' })
+        .expect(400);
+
+      expect(response.body.success).toBe(false);
+      expect(response.body.message).toContain('Validation error');
+    });
+
+    it('should handle invalid symbol IDs', async () => {
+      const invalidCandles = [{
+        symbolId: 999,
+        openTime: '2025-10-17 00:00:00',
+        closeTime: '2025-10-17 01:00:00',
+        open: 1.1000,
+        high: 1.1050,
+        low: 1.0990,
+        close: 1.1025,
+        volume: 1000
+      }];
+
+      const response = await request(app)
+        .post('/api/candles/bulk')
+        .send({ candles: invalidCandles })
+        .expect(400);
+
+      expect(response.body.success).toBe(false);
+      expect(response.body.message).toContain('Invalid symbol IDs');
+    });
+  });
+});

Деякі файли не було показано, через те що забагато файлів було змінено