소스 검색

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 4 달 전
부모
커밋
eda6f70d7e
10개의 변경된 파일6042개의 추가작업 그리고 918개의 파일을 삭제
  1. 48 13
      README.md
  2. 20 0
      config/config.json
  3. 43 0
      models/index.js
  4. 5820 897
      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
53 53
 - **Validation**: Joi
54 54
 - **Security**: Helmet, CORS
55 55
 - **Logging**: Winston, Morgan
56
-- **Testing**: Jest
57
-- **Development**: Nodemon, ESLint
56
+- **Testing**: Jest, Supertest
57
+- **Development**: Nodemon, ESLint, Sequelize CLI
58 58
 
59 59
 ## Project Structure
60 60
 
@@ -110,13 +110,13 @@ market-data-service/
110 110
    ```
111 111
    Edit `.env` with your database credentials and other configuration.
112 112
 
113
-4. **Set up the database**
113
+4. **Database setup**
114 114
    ```bash
115 115
    # Create PostgreSQL database
116 116
    createdb market_data
117 117
 
118
-   # Run the schema
119
-   psql -d market_data -f schema.sql
118
+   # Run migrations
119
+   npx sequelize-cli db:migrate
120 120
    ```
121 121
 
122 122
 5. **Start the development server**
@@ -182,15 +182,23 @@ CORS_ORIGIN=*
182 182
 - `POST /api/live-prices/bulk` - Bulk update live prices
183 183
 - `DELETE /api/live-prices/:symbolId` - Delete live price
184 184
 
185
-## Database Schema
185
+## Database Management
186 186
 
187
-The service uses three main tables:
187
+The database schema is managed through Sequelize migrations and models:
188 188
 
189
-1. **symbols** - Trading symbols metadata
190
-2. **candles_1h** - Hourly OHLCV data
191
-3. **live_prices** - Current market prices
189
+- Models are defined in `src/models/`
190
+- Migrations are stored in `migrations/`
191
+- Associations are configured in `src/models/index.js`
192 192
 
193
-See `schema.sql` for complete table definitions.
193
+Key entities:
194
+- **Symbol**: Financial instrument metadata
195
+- **Candle1h**: Hourly OHLCV data
196
+- **LivePrice**: Real-time market prices
197
+
198
+To update the database schema:
199
+1. Create a new migration: `npx sequelize-cli migration:generate --name description`
200
+2. Implement schema changes in the migration file
201
+3. Run migrations: `npx sequelize-cli db:migrate`
194 202
 
195 203
 ## Development
196 204
 
@@ -198,8 +206,10 @@ See `schema.sql` for complete table definitions.
198 206
 
199 207
 - `npm start` - Start production server
200 208
 - `npm run dev` - Start development server with auto-reload
201
-- `npm test` - Run tests
209
+- `npm test` - Run Jest test suite
202 210
 - `npm run test:watch` - Run tests in watch mode
211
+- `npm run migrate` - Run database migrations
212
+- `npm run migrate:undo` - Revert last migration
203 213
 - `npm run lint` - Run ESLint
204 214
 - `npm run lint:fix` - Fix ESLint issues
205 215
 
@@ -209,7 +219,32 @@ This project uses ESLint for code linting. Run `npm run lint` to check for issue
209 219
 
210 220
 ### Testing
211 221
 
212
-Tests are written using Jest. Add your test files in the `tests/` directory.
222
+The project uses Jest with Supertest for endpoint testing. Key features:
223
+
224
+- Integration tests with database cleanup
225
+- Test environment database configuration
226
+- Automatic test isolation with `{ force: true }` sync
227
+
228
+Run tests:
229
+```bash
230
+npm test         # Run full test suite
231
+npm run test:watch  # Run in watch mode
232
+```
233
+
234
+### Database Migrations
235
+
236
+We use Sequelize CLI for database migrations:
237
+
238
+```bash
239
+# Create new migration
240
+npx sequelize-cli migration:generate --name your-migration-name
241
+
242
+# Run pending migrations
243
+npx sequelize-cli db:migrate
244
+
245
+# Revert last migration
246
+npx sequelize-cli db:migrate:undo
247
+```
213 248
 
214 249
 ## Deployment
215 250
 

+ 20 - 0
config/config.json

@@ -0,0 +1,20 @@
1
+{
2
+  "development": {
3
+    "username": "market_user",
4
+    "password": "market_password",
5
+    "database": "market_data",
6
+    "host": "127.0.0.1",
7
+    "dialect": "postgres"
8
+  },
9
+  "test": {
10
+    "username": "market_user",
11
+    "password": "market_password",
12
+    "database": "market_data",
13
+    "host": "127.0.0.1",
14
+    "dialect": "postgres"
15
+  },
16
+  "production": {
17
+    "use_env_variable": "DATABASE_URL",
18
+    "dialect": "postgres"
19
+  }
20
+}

+ 43 - 0
models/index.js

@@ -0,0 +1,43 @@
1
+'use strict';
2
+
3
+const fs = require('fs');
4
+const path = require('path');
5
+const Sequelize = require('sequelize');
6
+const process = require('process');
7
+const basename = path.basename(__filename);
8
+const env = process.env.NODE_ENV || 'development';
9
+const config = require(__dirname + '/../config/config.json')[env];
10
+const db = {};
11
+
12
+let sequelize;
13
+if (config.use_env_variable) {
14
+  sequelize = new Sequelize(process.env[config.use_env_variable], config);
15
+} else {
16
+  sequelize = new Sequelize(config.database, config.username, config.password, config);
17
+}
18
+
19
+fs
20
+  .readdirSync(__dirname)
21
+  .filter(file => {
22
+    return (
23
+      file.indexOf('.') !== 0 &&
24
+      file !== basename &&
25
+      file.slice(-3) === '.js' &&
26
+      file.indexOf('.test.js') === -1
27
+    );
28
+  })
29
+  .forEach(file => {
30
+    const model = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes);
31
+    db[model.name] = model;
32
+  });
33
+
34
+Object.keys(db).forEach(modelName => {
35
+  if (db[modelName].associate) {
36
+    db[modelName].associate(db);
37
+  }
38
+});
39
+
40
+db.sequelize = sequelize;
41
+db.Sequelize = Sequelize;
42
+
43
+module.exports = db;

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 5820 - 897
package-lock.json


+ 6 - 0
package.json

@@ -31,5 +31,11 @@
31 31
     "pg": "^8.16.3",
32 32
     "sequelize": "^6.37.7",
33 33
     "winston": "^3.18.3"
34
+  },
35
+  "devDependencies": {
36
+    "@jest/globals": "^30.2.0",
37
+    "jest": "^30.2.0",
38
+    "sequelize-cli": "^6.6.3",
39
+    "supertest": "^7.1.4"
34 40
   }
35 41
 }

+ 1 - 1
src/app.js

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

+ 0 - 3
src/models/Candle1h.js

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

+ 0 - 3
src/models/LivePrice.js

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

+ 1 - 1
src/models/index.js

@@ -8,7 +8,7 @@ Symbol.hasMany(Candle1h, { foreignKey: 'symbolId', as: 'candles1h' });
8 8
 Candle1h.belongsTo(Symbol, { foreignKey: 'symbolId', as: 'symbol' });
9 9
 
10 10
 Symbol.hasOne(LivePrice, { foreignKey: 'symbolId', as: 'livePrice' });
11
-LivePrice.belongsTo(Symbol, { foreignKey: 'symbolId', as: 'symbol' });
11
+LivePrice.belongsTo(Symbol, { foreignKey: 'symbolId', as: 'livePriceSymbol' });
12 12
 
13 13
 // Sync database (only in development)
14 14
 if (process.env.NODE_ENV === 'development') {

+ 103 - 0
tests/candleController.test.js

@@ -0,0 +1,103 @@
1
+const request = require('supertest');
2
+const app = require('../src/app');
3
+const { Candle1h, Symbol, sequelize } = require('../src/models');
4
+
5
+describe('Candle Controller Integration Tests', () => {
6
+  let testSymbol;
7
+
8
+  beforeAll(async () => {
9
+    // Sync database and cleanup
10
+    await sequelize.sync({ force: true });
11
+    
12
+    // Create fresh test symbol
13
+    testSymbol = await Symbol.create({
14
+      symbol: 'TEST_SYMBOL',
15
+      exchange: 'TEST',
16
+      instrumentType: 'forex'
17
+    });
18
+  });
19
+
20
+  afterAll(async () => {
21
+    // Cleanup test data
22
+    await Candle1h.destroy({ where: {} });
23
+    await Symbol.destroy({ where: {} });
24
+    await sequelize.close();
25
+  });
26
+
27
+  describe('POST /api/candles/bulk', () => {
28
+    it('should create multiple candles from MT5 payload', async () => {
29
+      const mockCandles = [
30
+        {
31
+          symbolId: testSymbol.id,
32
+          openTime: '2025-10-17 00:00:00',
33
+          closeTime: '2025-10-17 01:00:00',
34
+          open: 1.1000,
35
+          high: 1.1050,
36
+          low: 1.0990,
37
+          close: 1.1025,
38
+          volume: 1000
39
+        },
40
+        {
41
+          symbolId: testSymbol.id,
42
+          openTime: '2025-10-17 01:00:00',
43
+          closeTime: '2025-10-17 02:00:00',
44
+          open: 1.1025,
45
+          high: 1.1075,
46
+          low: 1.1005,
47
+          close: 1.1060,
48
+          volume: 1200
49
+        }
50
+      ];
51
+
52
+      const response = await request(app)
53
+        .post('/api/candles/bulk')
54
+        .send({ candles: mockCandles })
55
+        .expect(201);
56
+
57
+      expect(response.body.success).toBe(true);
58
+      expect(response.body.message).toBe('2 candles created successfully');
59
+      expect(response.body.data.length).toBe(2);
60
+
61
+      // Verify database persistence
62
+      const dbCandles = await Candle1h.findAll({
63
+        where: { symbolId: testSymbol.id },
64
+        order: [['openTime', 'ASC']]
65
+      });
66
+
67
+      expect(dbCandles.length).toBe(2);
68
+      expect(Number(dbCandles[0].open)).toBeCloseTo(1.1000);
69
+      expect(Number(dbCandles[1].close)).toBeCloseTo(1.1060);
70
+    });
71
+
72
+    it('should reject invalid payload format', async () => {
73
+      const response = await request(app)
74
+        .post('/api/candles/bulk')
75
+        .send({ invalid: 'payload' })
76
+        .expect(400);
77
+
78
+      expect(response.body.success).toBe(false);
79
+      expect(response.body.message).toContain('Validation error');
80
+    });
81
+
82
+    it('should handle invalid symbol IDs', async () => {
83
+      const invalidCandles = [{
84
+        symbolId: 999,
85
+        openTime: '2025-10-17 00:00:00',
86
+        closeTime: '2025-10-17 01:00:00',
87
+        open: 1.1000,
88
+        high: 1.1050,
89
+        low: 1.0990,
90
+        close: 1.1025,
91
+        volume: 1000
92
+      }];
93
+
94
+      const response = await request(app)
95
+        .post('/api/candles/bulk')
96
+        .send({ candles: invalidCandles })
97
+        .expect(400);
98
+
99
+      expect(response.body.success).toBe(false);
100
+      expect(response.body.message).toContain('Invalid symbol IDs');
101
+    });
102
+  });
103
+});