agmission/Development/server/docs/archived/DATABASE_DESIGN.md

637 lines
16 KiB
Markdown

# Database Design and Migration Guide
## Current Database Schema Analysis
### Existing Collections
#### 1. JobAssign Collection (Current)
```javascript
{
_id: ObjectId,
job: Number, // Reference to Job._id
user: ObjectId, // Reference to User._id (aircraft/device)
status: Number, // 0: pending, 1: downloaded, 2: completed
date: Date
}
```
#### 2. Job Collection (Current)
```javascript
{
_id: Number,
name: String,
// ... existing fields
dlOp: {
type: Number,
mapOp: Mixed
}
// ... other fields
}
```
#### 3. Application Collection (Current)
```javascript
{
_id: ObjectId,
jobId: Number,
fileName: String,
fileSize: Number,
status: Number,
// ... other fields
}
```
## Enhanced Schema Design
### 1. Enhanced JobAssign Schema
```javascript
const JobAssignSchema = new Schema({
// Existing fields (unchanged for backward compatibility)
_id: ObjectId,
job: { type: Number, ref: 'Job', required: true },
user: { type: ObjectId, ref: 'User', required: true },
status: {
type: Number,
enum: [0, 1, 2], // 0: pending, 1: downloaded, 2: completed
default: 0
},
date: { type: Date, default: Date.now },
// New partner integration fields
partnerType: {
type: String,
enum: ['internal', 'satloc', 'dji', 'parrot', 'other'],
default: 'internal',
index: true
},
externalJobId: {
type: String,
sparse: true, // Only index non-null values
index: true
},
partnerMetadata: {
type: Schema.Types.Mixed,
default: null
},
// Sync state tracking for partner operations
syncState: {
jobUpload: {
status: {
type: String,
enum: ['pending', 'syncing', 'synced', 'failed'],
default: function() {
return this.partnerType === 'internal' ? 'synced' : 'pending';
}
},
attempts: { type: Number, default: 0, min: 0 },
lastAttempt: { type: Date },
lastSuccess: { type: Date },
error: { type: String },
errorCode: { type: String }, // For categorizing errors
nextRetry: { type: Date } // Scheduled next retry time
},
dataPolling: {
status: {
type: String,
enum: ['idle', 'polling', 'synced', 'failed'],
default: 'idle'
},
attempts: { type: Number, default: 0, min: 0 },
lastAttempt: { type: Date },
lastSuccess: { type: Date },
lastDataCheck: { type: Date },
error: { type: String },
errorCode: { type: String },
nextPoll: { type: Date }, // Scheduled next poll time
pollInterval: { type: Number, default: 60000 } // ms
}
},
// Partner-specific configuration
partnerConfig: {
aircraftId: { type: String }, // Partner's aircraft identifier
priority: {
type: String,
enum: ['low', 'normal', 'high'],
default: 'normal'
},
timeout: { type: Number, default: 30000 }, // Request timeout in ms
retryPolicy: {
maxAttempts: { type: Number, default: 5, min: 1, max: 10 },
baseDelay: { type: Number, default: 5000, min: 1000 },
maxDelay: { type: Number, default: 300000 },
backoffMultiplier: { type: Number, default: 2, min: 1, max: 5 },
jitter: { type: Boolean, default: true }
}
},
// Performance tracking
metrics: {
syncDuration: { type: Number }, // Total sync time in ms
dataSize: { type: Number }, // Amount of data processed
conversionTime: { type: Number }, // Time to convert data format
uploadTime: { type: Number }, // Time to upload to partner
downloadTime: { type: Number } // Time to download from partner
}
}, {
timestamps: true,
toJSON: { virtuals: true },
toObject: { virtuals: true }
});
// Compound indexes for efficient queries
JobAssignSchema.index({ user: 1, status: 1 });
JobAssignSchema.index({ job: 1, partnerType: 1 });
JobAssignSchema.index({ partnerType: 1, 'syncState.jobUpload.status': 1 });
JobAssignSchema.index({ partnerType: 1, 'syncState.dataPolling.status': 1 });
JobAssignSchema.index({ partnerType: 1, 'syncState.jobUpload.nextRetry': 1 });
JobAssignSchema.index({ partnerType: 1, 'syncState.dataPolling.nextPoll': 1 });
JobAssignSchema.index({ externalJobId: 1, partnerType: 1 }, { unique: true, sparse: true });
// Virtual fields
JobAssignSchema.virtual('isPartnerAssignment').get(function() {
return this.partnerType !== 'internal';
});
JobAssignSchema.virtual('needsSync').get(function() {
return this.isPartnerAssignment &&
this.syncState.jobUpload.status === 'pending';
});
JobAssignSchema.virtual('needsPolling').get(function() {
return this.isPartnerAssignment &&
this.syncState.jobUpload.status === 'synced' &&
this.syncState.dataPolling.status === 'idle' &&
this.status < 2;
});
```
### 2. Enhanced Application Schema
```javascript
const EnhancedApplicationSchema = new Schema({
// Existing fields (unchanged)
jobId: { type: Number, required: true },
fileName: { type: String, required: true },
fileSize: { type: Number, required: true },
status: { type: Number, default: 1 },
// Enhanced fields for partner integration
partnerType: {
type: String,
enum: ['internal', 'satloc', 'dji', 'parrot', 'other'],
default: 'internal',
index: true
},
externalJobId: {
type: String,
sparse: true,
index: true
},
assignmentId: {
type: ObjectId,
ref: 'JobAssign',
index: true
},
// Original data format and metadata
originalData: {
format: {
type: String,
enum: ['agnav', 'satloc', 'dji_log', 'parrot_log', 'csv', 'other']
},
version: { type: String },
encoding: { type: String, default: 'utf8' },
compression: { type: String },
checksum: { type: String }
},
// Partner-specific metadata
partnerMetadata: {
aircraftInfo: {
id: String,
model: String,
firmware: String,
sensors: [String]
},
flightInfo: {
startTime: Date,
endTime: Date,
duration: Number,
altitude: { min: Number, max: Number, avg: Number },
speed: { min: Number, max: Number, avg: Number },
weather: Schema.Types.Mixed
},
dataQuality: {
gpsAccuracy: Number,
signalStrength: Number,
dataCompleteness: Number, // percentage
anomalies: [String]
}
},
// Processing pipeline tracking
processingStage: {
type: String,
enum: [
'uploaded', // File uploaded/received
'validated', // Initial validation complete
'parsing', // Currently parsing file
'converting', // Converting to internal format
'processing', // Processing application details
'completed', // Successfully processed
'failed' // Processing failed
],
default: 'uploaded',
index: true
},
processingSteps: [{
step: {
type: String,
enum: ['upload', 'validate', 'parse', 'convert', 'process', 'finalize']
},
status: {
type: String,
enum: ['pending', 'running', 'completed', 'failed', 'skipped']
},
startTime: Date,
endTime: Date,
duration: Number, // milliseconds
error: String,
metadata: Schema.Types.Mixed
}],
// Conversion and processing metrics
conversionMetrics: {
recordsInput: { type: Number, min: 0 },
recordsOutput: { type: Number, min: 0 },
dataLossPercentage: { type: Number, min: 0, max: 100 },
conversionTime: { type: Number }, // milliseconds
conversionErrors: [String],
formatMappings: Schema.Types.Mixed
},
// Quality assurance
qualityChecks: [{
checkType: {
type: String,
enum: ['gps_continuity', 'spray_coverage', 'data_integrity', 'format_compliance']
},
status: {
type: String,
enum: ['passed', 'failed', 'warning', 'skipped']
},
score: { type: Number, min: 0, max: 100 },
issues: [String],
recommendations: [String]
}],
// File storage information
storage: {
originalPath: String,
processedPath: String,
backupPath: String,
compressionRatio: Number,
storageSize: Number
}
}, {
timestamps: true,
toJSON: { virtuals: true },
toObject: { virtuals: true }
});
// Indexes for performance
EnhancedApplicationSchema.index({ jobId: 1, partnerType: 1 });
EnhancedApplicationSchema.index({ processingStage: 1, createdAt: 1 });
EnhancedApplicationSchema.index({ assignmentId: 1 });
EnhancedApplicationSchema.index({ externalJobId: 1, partnerType: 1 });
// Virtual fields
EnhancedApplicationSchema.virtual('isPartnerData').get(function() {
return this.partnerType !== 'internal';
});
EnhancedApplicationSchema.virtual('processingDuration').get(function() {
if (this.processingSteps && this.processingSteps.length > 0) {
const firstStep = this.processingSteps.find(s => s.startTime);
const lastStep = [...this.processingSteps].reverse().find(s => s.endTime);
if (firstStep && lastStep) {
return lastStep.endTime - firstStep.startTime;
}
}
return null;
});
```
### 3. New Partner Configuration Schema
```javascript
const PartnerConfigSchema = new Schema({
code: {
type: String,
required: true,
unique: true,
lowercase: true,
match: /^[a-z0-9_]+$/
},
name: { type: String, required: true },
description: { type: String },
active: { type: Boolean, default: true },
// API Configuration
apiConfig: {
baseUrl: { type: String, required: true },
version: { type: String, default: 'v1' },
authentication: {
type: {
type: String,
enum: ['api_key', 'oauth2', 'basic_auth', 'bearer_token'],
required: true
},
credentials: Schema.Types.Mixed // Encrypted in production
},
rateLimit: {
requestsPerSecond: { type: Number, default: 10 },
burstLimit: { type: Number, default: 50 },
backoffStrategy: {
type: String,
enum: ['fixed', 'exponential', 'linear'],
default: 'exponential'
}
},
timeout: {
connect: { type: Number, default: 10000 },
request: { type: Number, default: 30000 },
response: { type: Number, default: 60000 }
}
},
// Capabilities and features
capabilities: [{
type: String,
enum: [
'job_upload',
'job_download',
'data_polling',
'real_time_sync',
'flight_planning',
'telemetry_streaming',
'weather_integration',
'obstacle_avoidance'
]
}],
// Data format specifications
dataFormats: {
input: [{
format: String,
version: String,
mimeType: String,
extensions: [String],
maxSize: Number,
compression: [String]
}],
output: [{
format: String,
version: String,
mimeType: String,
schema: Schema.Types.Mixed
}]
},
// Integration settings
integrationSettings: {
syncInterval: { type: Number, default: 60000 }, // ms
batchSize: { type: Number, default: 100 },
maxRetries: { type: Number, default: 3 },
healthCheckInterval: { type: Number, default: 300000 }, // 5 minutes
// Webhook configuration
webhooks: {
enabled: { type: Boolean, default: false },
endpoints: [{
event: {
type: String,
enum: ['job_completed', 'data_available', 'error_occurred', 'status_changed']
},
url: String,
secret: String,
retries: { type: Number, default: 3 }
}]
}
},
// Monitoring and alerting
monitoring: {
healthcheck: {
endpoint: String,
expectedResponse: Schema.Types.Mixed,
timeout: { type: Number, default: 10000 }
},
alerts: {
errorThreshold: { type: Number, default: 0.1 }, // 10% error rate
responseTimeThreshold: { type: Number, default: 5000 }, // 5 seconds
downtime: {
consecutiveFailures: { type: Number, default: 3 },
notificationChannels: [String]
}
}
}
}, {
timestamps: true
});
```
## Migration Strategy
### Phase 1: Backward Compatible Changes
1. **Add new fields to existing schemas**
```javascript
// Add to JobAssign collection
db.job_assigns.updateMany(
{ partnerType: { $exists: false } },
{
$set: {
partnerType: 'internal',
syncState: {
jobUpload: { status: 'synced', attempts: 0 },
dataPolling: { status: 'idle', attempts: 0 }
}
}
}
);
```
2. **Create indexes gradually**
```javascript
// Create indexes in background
db.job_assigns.createIndex(
{ partnerType: 1, "syncState.jobUpload.status": 1 },
{ background: true }
);
```
### Phase 2: Data Migration Scripts
```javascript
// Migration script for existing assignments
async function migrateExistingAssignments() {
const assignments = await JobAssign.find({
partnerType: { $exists: false }
});
for (const assignment of assignments) {
await JobAssign.findByIdAndUpdate(assignment._id, {
partnerType: 'internal',
syncState: {
jobUpload: {
status: 'synced',
attempts: 0,
lastSuccess: assignment.date
},
dataPolling: {
status: assignment.status > 1 ? 'synced' : 'idle',
attempts: 0
}
},
partnerConfig: {
retryPolicy: {
maxAttempts: 5,
baseDelay: 5000,
maxDelay: 300000,
backoffMultiplier: 2,
jitter: true
}
}
});
}
}
// Migration script for application records
async function migrateApplicationRecords() {
const apps = await Application.find({
partnerType: { $exists: false }
});
for (const app of apps) {
await Application.findByIdAndUpdate(app._id, {
partnerType: 'internal',
originalData: {
format: 'agnav',
version: '1.0',
encoding: 'utf8'
},
processingStage: app.status === 3 ? 'completed' : 'uploaded',
processingSteps: [{
step: 'upload',
status: 'completed',
startTime: app.createdAt,
endTime: app.createdAt,
duration: 0
}]
});
}
}
```
### Phase 3: Validation and Testing
```javascript
// Validation script to ensure data integrity
async function validateMigration() {
// Check all assignments have required fields
const invalidAssignments = await JobAssign.countDocuments({
$or: [
{ partnerType: { $exists: false } },
{ syncState: { $exists: false } }
]
});
if (invalidAssignments > 0) {
throw new Error(`${invalidAssignments} assignments missing required fields`);
}
// Check partner type consistency
const invalidPartnerTypes = await JobAssign.countDocuments({
partnerType: { $nin: ['internal', 'satloc', 'dji', 'parrot', 'other'] }
});
if (invalidPartnerTypes > 0) {
throw new Error(`${invalidPartnerTypes} assignments with invalid partner types`);
}
console.log('Migration validation passed');
}
```
## Performance Considerations
### Query Optimization
1. **Compound Indexes**
```javascript
// For partner sync operations
{ partnerType: 1, "syncState.jobUpload.status": 1, "syncState.jobUpload.nextRetry": 1 }
// For data polling
{ partnerType: 1, "syncState.dataPolling.nextPoll": 1, status: 1 }
// For user queries
{ user: 1, status: 1, partnerType: 1 }
```
2. **Sparse Indexes**
```javascript
// Only index documents with external job IDs
{ externalJobId: 1 }, { sparse: true }
```
### Data Archival Strategy
```javascript
// Archive completed assignments older than 90 days
const archiveOldAssignments = async () => {
const cutoffDate = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);
const oldAssignments = await JobAssign.find({
status: 2,
updatedAt: { $lt: cutoffDate }
});
// Move to archive collection
if (oldAssignments.length > 0) {
await JobAssignArchive.insertMany(oldAssignments);
await JobAssign.deleteMany({
_id: { $in: oldAssignments.map(a => a._id) }
});
}
console.log(`Archived ${oldAssignments.length} assignments`);
};
```
## Backup and Recovery
### Backup Strategy
1. **Full daily backups** of partner configuration
2. **Incremental hourly backups** of assignment data
3. **Point-in-time recovery** for critical operations
4. **Cross-region replication** for disaster recovery
### Recovery Procedures
1. **Partner outage recovery**: Automatic retry with exponential backoff
2. **Data corruption recovery**: Restore from backup and replay operations
3. **Migration rollback**: Automated rollback scripts for each phase
This enhanced database design provides a robust foundation for multi-partner integration while maintaining backward compatibility and performance.