Initial Commit
This commit is contained in:
426
docs/changes/vehicles-dropdown-v1/phase-03-api-migration.md
Normal file
426
docs/changes/vehicles-dropdown-v1/phase-03-api-migration.md
Normal file
@@ -0,0 +1,426 @@
|
||||
# Phase 3: API Migration
|
||||
|
||||
## Overview
|
||||
|
||||
This phase updates the vehicles API controller to use the new MVP Platform database for all dropdown endpoints while maintaining exact API compatibility. All existing response formats and authentication patterns are preserved.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Phase 2 backend migration completed successfully
|
||||
- VIN decoder service functional
|
||||
- MVP Platform repository working correctly
|
||||
- Backend service can query MVP Platform database
|
||||
- All TypeScript compilation successful
|
||||
|
||||
## Current API Endpoints to Update
|
||||
|
||||
**Existing endpoints that will be updated**:
|
||||
- `GET /api/vehicles/dropdown/makes` (unauthenticated)
|
||||
- `GET /api/vehicles/dropdown/models/:make` (unauthenticated)
|
||||
- `GET /api/vehicles/dropdown/transmissions` (unauthenticated)
|
||||
- `GET /api/vehicles/dropdown/engines` (unauthenticated)
|
||||
- `GET /api/vehicles/dropdown/trims` (unauthenticated)
|
||||
|
||||
**Existing endpoints that remain unchanged**:
|
||||
- `POST /api/vehicles` (authenticated - uses VIN decoder)
|
||||
- `GET /api/vehicles` (authenticated)
|
||||
- `GET /api/vehicles/:id` (authenticated)
|
||||
- `PUT /api/vehicles/:id` (authenticated)
|
||||
- `DELETE /api/vehicles/:id` (authenticated)
|
||||
|
||||
## Tasks
|
||||
|
||||
### Task 3.1: Update Vehicles Controller
|
||||
|
||||
**Location**: `backend/src/features/vehicles/api/vehicles.controller.ts`
|
||||
|
||||
**Action**: Replace external API dropdown methods with MVP Platform database calls:
|
||||
|
||||
```typescript
|
||||
// UPDATE imports - REMOVE:
|
||||
// import { vpicClient } from '../external/vpic/vpic.client';
|
||||
|
||||
// ADD new imports:
|
||||
import { VehiclesService } from '../domain/vehicles.service';
|
||||
|
||||
export class VehiclesController {
|
||||
private vehiclesService: VehiclesService;
|
||||
|
||||
constructor() {
|
||||
this.vehiclesService = new VehiclesService();
|
||||
}
|
||||
|
||||
// UPDATE existing dropdown methods:
|
||||
|
||||
async getDropdownMakes(request: FastifyRequest, reply: FastifyReply) {
|
||||
try {
|
||||
logger.info('Getting dropdown makes from MVP Platform');
|
||||
const makes = await this.vehiclesService.getDropdownMakes();
|
||||
|
||||
// Maintain exact same response format
|
||||
const response = makes.map(make => ({
|
||||
Make_ID: make.id,
|
||||
Make_Name: make.name
|
||||
}));
|
||||
|
||||
reply.status(200).send(response);
|
||||
} catch (error) {
|
||||
logger.error('Get dropdown makes failed', { error });
|
||||
reply.status(500).send({ error: 'Failed to retrieve makes' });
|
||||
}
|
||||
}
|
||||
|
||||
async getDropdownModels(request: FastifyRequest<{ Params: { make: string } }>, reply: FastifyReply) {
|
||||
try {
|
||||
const { make } = request.params;
|
||||
logger.info('Getting dropdown models from MVP Platform', { make });
|
||||
|
||||
const models = await this.vehiclesService.getDropdownModels(make);
|
||||
|
||||
// Maintain exact same response format
|
||||
const response = models.map(model => ({
|
||||
Model_ID: model.id,
|
||||
Model_Name: model.name
|
||||
}));
|
||||
|
||||
reply.status(200).send(response);
|
||||
} catch (error) {
|
||||
logger.error('Get dropdown models failed', { error });
|
||||
reply.status(500).send({ error: 'Failed to retrieve models' });
|
||||
}
|
||||
}
|
||||
|
||||
async getDropdownTransmissions(request: FastifyRequest, reply: FastifyReply) {
|
||||
try {
|
||||
logger.info('Getting dropdown transmissions from MVP Platform');
|
||||
const transmissions = await this.vehiclesService.getDropdownTransmissions();
|
||||
|
||||
// Maintain exact same response format
|
||||
const response = transmissions.map(transmission => ({
|
||||
Name: transmission.name
|
||||
}));
|
||||
|
||||
reply.status(200).send(response);
|
||||
} catch (error) {
|
||||
logger.error('Get dropdown transmissions failed', { error });
|
||||
reply.status(500).send({ error: 'Failed to retrieve transmissions' });
|
||||
}
|
||||
}
|
||||
|
||||
async getDropdownEngines(request: FastifyRequest, reply: FastifyReply) {
|
||||
try {
|
||||
logger.info('Getting dropdown engines from MVP Platform');
|
||||
const engines = await this.vehiclesService.getDropdownEngines();
|
||||
|
||||
// Maintain exact same response format
|
||||
const response = engines.map(engine => ({
|
||||
Name: engine.name
|
||||
}));
|
||||
|
||||
reply.status(200).send(response);
|
||||
} catch (error) {
|
||||
logger.error('Get dropdown engines failed', { error });
|
||||
reply.status(500).send({ error: 'Failed to retrieve engines' });
|
||||
}
|
||||
}
|
||||
|
||||
async getDropdownTrims(request: FastifyRequest, reply: FastifyReply) {
|
||||
try {
|
||||
logger.info('Getting dropdown trims from MVP Platform');
|
||||
const trims = await this.vehiclesService.getDropdownTrims();
|
||||
|
||||
// Maintain exact same response format
|
||||
const response = trims.map(trim => ({
|
||||
Name: trim.name
|
||||
}));
|
||||
|
||||
reply.status(200).send(response);
|
||||
} catch (error) {
|
||||
logger.error('Get dropdown trims failed', { error });
|
||||
reply.status(500).send({ error: 'Failed to retrieve trims' });
|
||||
}
|
||||
}
|
||||
|
||||
// All other methods remain unchanged (createVehicle, getUserVehicles, etc.)
|
||||
}
|
||||
```
|
||||
|
||||
### Task 3.2: Verify Routes Configuration
|
||||
|
||||
**Location**: `backend/src/features/vehicles/api/vehicles.routes.ts`
|
||||
|
||||
**Action**: Ensure dropdown routes remain unauthenticated (no changes needed, just verification):
|
||||
|
||||
```typescript
|
||||
// VERIFY these routes remain unauthenticated:
|
||||
fastify.get('/vehicles/dropdown/makes', {
|
||||
handler: vehiclesController.getDropdownMakes.bind(vehiclesController)
|
||||
});
|
||||
|
||||
fastify.get<{ Params: { make: string } }>('/vehicles/dropdown/models/:make', {
|
||||
handler: vehiclesController.getDropdownModels.bind(vehiclesController)
|
||||
});
|
||||
|
||||
fastify.get('/vehicles/dropdown/transmissions', {
|
||||
handler: vehiclesController.getDropdownTransmissions.bind(vehiclesController)
|
||||
});
|
||||
|
||||
fastify.get('/vehicles/dropdown/engines', {
|
||||
handler: vehiclesController.getDropdownEngines.bind(vehiclesController)
|
||||
});
|
||||
|
||||
fastify.get('/vehicles/dropdown/trims', {
|
||||
handler: vehiclesController.getDropdownTrims.bind(vehiclesController)
|
||||
});
|
||||
```
|
||||
|
||||
**Note**: These routes should NOT have `preHandler: fastify.authenticate` to maintain unauthenticated access as required by security.md.
|
||||
|
||||
### Task 3.3: Update Response Error Handling
|
||||
|
||||
**Action**: Add specific error handling for database connectivity issues:
|
||||
|
||||
```typescript
|
||||
// Add to VehiclesController class:
|
||||
|
||||
private handleDatabaseError(error: any, operation: string, reply: FastifyReply) {
|
||||
logger.error(`${operation} database error`, { error });
|
||||
|
||||
// Check for specific database connection errors
|
||||
if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND') {
|
||||
reply.status(503).send({
|
||||
error: 'Service temporarily unavailable',
|
||||
message: 'Database connection issue'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Generic database error
|
||||
if (error.code && error.code.startsWith('P')) { // PostgreSQL error codes
|
||||
reply.status(500).send({
|
||||
error: 'Database query failed',
|
||||
message: 'Please try again later'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Generic error
|
||||
reply.status(500).send({
|
||||
error: `Failed to ${operation}`,
|
||||
message: 'Internal server error'
|
||||
});
|
||||
}
|
||||
|
||||
// Update all dropdown methods to use this error handler:
|
||||
// Replace each catch block with:
|
||||
} catch (error) {
|
||||
this.handleDatabaseError(error, 'retrieve makes', reply);
|
||||
}
|
||||
```
|
||||
|
||||
### Task 3.4: Add Performance Monitoring
|
||||
|
||||
**Action**: Add response time logging for performance monitoring:
|
||||
|
||||
```typescript
|
||||
// Add to VehiclesController class:
|
||||
|
||||
private async measurePerformance<T>(
|
||||
operation: string,
|
||||
fn: () => Promise<T>
|
||||
): Promise<T> {
|
||||
const startTime = Date.now();
|
||||
try {
|
||||
const result = await fn();
|
||||
const duration = Date.now() - startTime;
|
||||
logger.info(`MVP Platform ${operation} completed`, { duration });
|
||||
return result;
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
logger.error(`MVP Platform ${operation} failed`, { duration, error });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Update dropdown methods to use performance monitoring:
|
||||
async getDropdownMakes(request: FastifyRequest, reply: FastifyReply) {
|
||||
try {
|
||||
logger.info('Getting dropdown makes from MVP Platform');
|
||||
const makes = await this.measurePerformance('makes query', () =>
|
||||
this.vehiclesService.getDropdownMakes()
|
||||
);
|
||||
|
||||
// ... rest of method unchanged
|
||||
} catch (error) {
|
||||
this.handleDatabaseError(error, 'retrieve makes', reply);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Task 3.5: Update Health Check
|
||||
|
||||
**Location**: `backend/src/features/vehicles/api/vehicles.controller.ts`
|
||||
|
||||
**Action**: Add MVP Platform database health check method:
|
||||
|
||||
```typescript
|
||||
// Add new health check method:
|
||||
async healthCheck(request: FastifyRequest, reply: FastifyReply) {
|
||||
try {
|
||||
// Test MVP Platform database connection
|
||||
await this.measurePerformance('health check', async () => {
|
||||
const testResult = await this.vehiclesService.testMvpPlatformConnection();
|
||||
if (!testResult) {
|
||||
throw new Error('MVP Platform database connection failed');
|
||||
}
|
||||
});
|
||||
|
||||
reply.status(200).send({
|
||||
status: 'healthy',
|
||||
mvpPlatform: 'connected',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Health check failed', { error });
|
||||
reply.status(503).send({
|
||||
status: 'unhealthy',
|
||||
error: error.message,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Location**: `backend/src/features/vehicles/domain/vehicles.service.ts`
|
||||
|
||||
**Action**: Add health check method to service:
|
||||
|
||||
```typescript
|
||||
// Add to VehiclesService class:
|
||||
async testMvpPlatformConnection(): Promise<boolean> {
|
||||
try {
|
||||
await mvpPlatformRepository.getMakes();
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error('MVP Platform connection test failed', { error });
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Task 3.6: Update Route Registration for Health Check
|
||||
|
||||
**Location**: `backend/src/features/vehicles/api/vehicles.routes.ts`
|
||||
|
||||
**Action**: Add health check route:
|
||||
|
||||
```typescript
|
||||
// Add health check route (unauthenticated for monitoring):
|
||||
fastify.get('/vehicles/health', {
|
||||
handler: vehiclesController.healthCheck.bind(vehiclesController)
|
||||
});
|
||||
```
|
||||
|
||||
## Validation Steps
|
||||
|
||||
### Step 1: Test API Response Formats
|
||||
|
||||
```bash
|
||||
# Test makes endpoint
|
||||
curl -s http://localhost:3001/api/vehicles/dropdown/makes | jq '.[0]'
|
||||
# Should return: {"Make_ID": number, "Make_Name": "string"}
|
||||
|
||||
# Test models endpoint
|
||||
curl -s "http://localhost:3001/api/vehicles/dropdown/models/Honda" | jq '.[0]'
|
||||
# Should return: {"Model_ID": number, "Model_Name": "string"}
|
||||
|
||||
# Test transmissions endpoint
|
||||
curl -s http://localhost:3001/api/vehicles/dropdown/transmissions | jq '.[0]'
|
||||
# Should return: {"Name": "string"}
|
||||
```
|
||||
|
||||
### Step 2: Test Performance
|
||||
|
||||
```bash
|
||||
# Test response times (should be < 100ms)
|
||||
time curl -s http://localhost:3001/api/vehicles/dropdown/makes > /dev/null
|
||||
|
||||
# Load test with multiple concurrent requests
|
||||
for i in {1..10}; do
|
||||
curl -s http://localhost:3001/api/vehicles/dropdown/makes > /dev/null &
|
||||
done
|
||||
wait
|
||||
```
|
||||
|
||||
### Step 3: Test Error Handling
|
||||
|
||||
```bash
|
||||
# Test with invalid make name
|
||||
curl -s "http://localhost:3001/api/vehicles/dropdown/models/InvalidMake" | jq '.'
|
||||
# Should return empty array or appropriate error
|
||||
|
||||
# Test health check
|
||||
curl -s http://localhost:3001/api/vehicles/health | jq '.'
|
||||
# Should return: {"status": "healthy", "mvpPlatform": "connected", "timestamp": "..."}
|
||||
```
|
||||
|
||||
### Step 4: Verify Authentication Patterns
|
||||
|
||||
```bash
|
||||
# Test that dropdown endpoints are unauthenticated (should work without token)
|
||||
curl -s http://localhost:3001/api/vehicles/dropdown/makes | jq '. | length'
|
||||
# Should return number > 0
|
||||
|
||||
# Test that vehicle CRUD endpoints still require authentication
|
||||
curl -s http://localhost:3001/api/vehicles
|
||||
# Should return 401 Unauthorized
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
### Common Issues and Solutions
|
||||
|
||||
**Issue**: Empty response arrays
|
||||
**Solution**: Check MVP Platform database has data, verify SQL queries, check table names
|
||||
|
||||
**Issue**: Slow response times (> 100ms)
|
||||
**Solution**: Add database indexes, optimize queries, check connection pool settings
|
||||
|
||||
**Issue**: Authentication errors on dropdown endpoints
|
||||
**Solution**: Verify routes don't have authentication middleware, check security.md compliance
|
||||
|
||||
**Issue**: Wrong response format
|
||||
**Solution**: Compare with original vPIC API responses, adjust mapping in controller
|
||||
|
||||
### Rollback Procedure
|
||||
|
||||
1. Revert vehicles.controller.ts:
|
||||
```bash
|
||||
git checkout HEAD -- backend/src/features/vehicles/api/vehicles.controller.ts
|
||||
```
|
||||
|
||||
2. Revert vehicles.routes.ts if modified:
|
||||
```bash
|
||||
git checkout HEAD -- backend/src/features/vehicles/api/vehicles.routes.ts
|
||||
```
|
||||
|
||||
3. Restart backend service:
|
||||
```bash
|
||||
docker-compose restart backend
|
||||
```
|
||||
|
||||
## Next Steps
|
||||
|
||||
After successful completion of Phase 3:
|
||||
|
||||
1. Proceed to [Phase 4: Scheduled ETL](./phase-04-scheduled-etl.md)
|
||||
2. Monitor API response times in production
|
||||
3. Set up alerts for health check failures
|
||||
|
||||
## Dependencies for Next Phase
|
||||
|
||||
- All dropdown APIs returning correct data
|
||||
- Response times consistently under 100ms
|
||||
- Health check endpoint functional
|
||||
- No authentication issues with dropdown endpoints
|
||||
- Error handling working properly
|
||||
Reference in New Issue
Block a user