Contest Schema
MongoDB schema for multi-platform competitive programming contest data.
Overview
The Contest schema stores contest information from multiple platforms (Codeforces, LeetCode, CodeChef, AtCoder) in a unified format with platform-specific metadata.
Location
Server/src/contests/schemas/contest.schema.tsSchema Definition
TypeScript Interface
interface ContestDocument extends Document {
_id: Types.ObjectId;
platformId: string;
name: string;
platform: ContestPlatform;
phase: ContestPhase;
type: ContestType;
startTime: Date;
endTime: Date;
durationMinutes: number;
description?: string;
websiteUrl?: string;
registrationUrl?: string;
preparedBy?: string;
difficulty?: DifficultyLevel;
participantCount?: number;
problemCount?: number;
country?: string;
city?: string;
platformMetadata: PlatformMetadata;
isActive: boolean;
isNotified: boolean;
lastSyncedAt?: Date;
createdAt: Date;
updatedAt: Date;
// Virtual fields
isUpcoming: boolean;
isRunning: boolean;
isFinished: boolean;
timeUntilStart: number;
timeUntilEnd: number;
}Mongoose Schema
@Schema({ timestamps: true })
export class Contest {
@Prop({ required: true, index: true })
platformId: string;
@Prop({ required: true, index: true })
name: string;
@Prop({ required: true, enum: ContestPlatform, index: true })
platform: ContestPlatform;
@Prop({ required: true, enum: ContestPhase, index: true })
phase: ContestPhase;
@Prop({ required: true, enum: ContestType })
type: ContestType;
@Prop({ required: true, index: true })
startTime: Date;
@Prop({ required: true })
endTime: Date;
@Prop({ required: true })
durationMinutes: number;
// ... other fields
}Enums
ContestPlatform
enum ContestPlatform {
CODEFORCES = 'codeforces',
LEETCODE = 'leetcode',
CODECHEF = 'codechef',
ATCODER = 'atcoder',
}Supported competitive programming platforms.
ContestPhase
enum ContestPhase {
// Universal phases
BEFORE = 'BEFORE',
CODING = 'CODING',
FINISHED = 'FINISHED',
// Codeforces specific
PENDING_SYSTEM_TEST = 'PENDING_SYSTEM_TEST',
SYSTEM_TEST = 'SYSTEM_TEST',
// LeetCode specific
UPCOMING = 'UPCOMING',
RUNNING = 'RUNNING',
ENDED = 'ENDED',
// CodeChef specific
NOT_STARTED = 'NOT_STARTED',
STARTED = 'STARTED',
COMPLETED = 'COMPLETED',
}Contest lifecycle phases across different platforms.
ContestType
enum ContestType {
// Codeforces
CF = 'CF',
IOI = 'IOI',
ICPC = 'ICPC',
// LeetCode
WEEKLY = 'WEEKLY',
BIWEEKLY = 'BIWEEKLY',
// CodeChef
LONG = 'LONG',
COOK_OFF = 'COOK_OFF',
LUNCH_TIME = 'LUNCH_TIME',
STARTERS = 'STARTERS',
// AtCoder
ABC = 'ABC',
ARC = 'ARC',
AGC = 'AGC',
AHC = 'AHC',
}Platform-specific contest types.
DifficultyLevel
enum DifficultyLevel {
BEGINNER = 'BEGINNER',
EASY = 'EASY',
MEDIUM = 'MEDIUM',
HARD = 'HARD',
EXPERT = 'EXPERT',
}Contest difficulty levels.
Fields
Required Fields
platformId
- Type:
String - Required: Yes
- Indexed: Yes
- Description: Platform-specific contest identifier
- Examples:
- Codeforces:
"1900"(numeric ID) - LeetCode:
"weekly-contest-380"(slug) - CodeChef:
"START120"(contest code) - AtCoder:
"abc340"(contest ID)
- Codeforces:
name
- Type:
String - Required: Yes
- Indexed: Yes (text index)
- Description: Contest name/title
- Examples:
"Codeforces Round #900 (Div. 2)""Weekly Contest 380""Starters 120""AtCoder Beginner Contest 340"
platform
- Type:
ContestPlatform(enum) - Required: Yes
- Indexed: Yes
- Description: Source platform
- Values:
codeforces,leetcode,codechef,atcoder
phase
- Type:
ContestPhase(enum) - Required: Yes
- Indexed: Yes
- Description: Current contest phase/status
- Common Values:
BEFORE,CODING,FINISHED
type
- Type:
ContestType(enum) - Required: Yes
- Description: Platform-specific contest type
- Examples:
CF,WEEKLY,STARTERS,ABC
startTime
- Type:
Date - Required: Yes
- Indexed: Yes
- Description: Contest start timestamp
- Format: ISO 8601 date
endTime
- Type:
Date - Required: Yes
- Description: Contest end timestamp
- Format: ISO 8601 date
durationMinutes
- Type:
Number - Required: Yes
- Description: Contest duration in minutes
- Calculation:
(endTime - startTime) / 60000
Optional Fields
description
- Type:
String - Required: No
- Indexed: Yes (text index)
- Description: Contest description or details
websiteUrl
- Type:
String - Required: No
- Description: Link to contest page
- Examples:
"https://codeforces.com/contest/1900""https://leetcode.com/contest/weekly-contest-380"
registrationUrl
- Type:
String - Required: No
- Description: Link to contest registration
preparedBy
- Type:
String - Required: No
- Description: Contest author/organizer
difficulty
- Type:
DifficultyLevel(enum) - Required: No
- Description: Contest difficulty level
- Values:
BEGINNER,EASY,MEDIUM,HARD,EXPERT
participantCount
- Type:
Number - Default:
0 - Description: Number of registered participants
problemCount
- Type:
Number - Default:
0 - Description: Number of problems in contest
country
- Type:
String - Required: No
- Description: Country (for regional contests)
city
- Type:
String - Required: No
- Description: City (for onsite contests)
platformMetadata
- Type:
Object(PlatformMetadata) - Default:
{} - Description: Platform-specific additional data
- See: Platform Metadata
System Fields
isActive
- Type:
Boolean - Default:
true - Indexed: Yes
- Description: Contest active status
- Usage: Soft delete mechanism
isNotified
- Type:
Boolean - Default:
false - Indexed: Yes
- Description: Whether users have been notified
- Usage: Prevent duplicate notifications
lastSyncedAt
- Type:
Date - Required: No
- Indexed: Yes
- Description: Last sync timestamp from platform API
createdAt
- Type:
Date - Auto-generated: Yes
- Description: Document creation timestamp
updatedAt
- Type:
Date - Auto-generated: Yes
- Description: Last update timestamp
Platform Metadata
Flexible object for storing platform-specific data.
interface PlatformMetadata {
// Codeforces specific
frozen?: boolean;
relativeTimeSeconds?: number;
icpcRegion?: string;
season?: string;
// LeetCode specific
titleSlug?: string;
premiumOnly?: boolean;
// CodeChef specific
division?: string;
rated?: boolean;
// AtCoder specific
ratedRange?: string;
penalty?: number;
}Codeforces Metadata
frozen: Scoreboard frozen statusrelativeTimeSeconds: Time relative to nowicpcRegion: ICPC region (if applicable)season: Contest season
LeetCode Metadata
titleSlug: URL-friendly contest slugpremiumOnly: Premium-only contest flag
CodeChef Metadata
division: Contest division (Div 1, Div 2, etc.)rated: Whether contest is rated
AtCoder Metadata
ratedRange: Rating range affected (e.g., "1200-1999")penalty: Penalty time in minutes
Virtual Fields
isUpcoming
- Type:
Boolean - Virtual: Yes
- Description: Contest hasn't started yet
- Calculation:
startTime > now
isRunning
- Type:
Boolean - Virtual: Yes
- Description: Contest is currently running
- Calculation:
startTime <= now && endTime >= now
isFinished
- Type:
Boolean - Virtual: Yes
- Description: Contest has ended
- Calculation:
endTime < now
timeUntilStart
- Type:
Number - Virtual: Yes
- Unit: Milliseconds
- Description: Time until contest starts
- Calculation:
max(0, startTime - now)
timeUntilEnd
- Type:
Number - Virtual: Yes
- Unit: Milliseconds
- Description: Time until contest ends
- Calculation:
max(0, endTime - now)
Indexes
Single Field Indexes
{ platformId: 1 } // Platform-specific ID lookup
{ name: 1 } // Name search
{ platform: 1 } // Filter by platform
{ phase: 1 } // Filter by phase
{ startTime: 1 } // Sort by start time
{ isActive: 1 } // Filter active contests
{ isNotified: 1 } // Notification tracking
{ lastSyncedAt: 1 } // Sync trackingCompound Indexes
{ platform: 1, startTime: 1 } // Platform contests sorted by time
{ platform: 1, phase: 1 } // Platform contests by phase
{ startTime: 1, isActive: 1 } // Active upcoming contests
{ phase: 1, isActive: 1 } // Active contests by phase
{ platformId: 1, platform: 1 } // Unique constraint
{ isNotified: 1, startTime: 1 } // Notification queriesText Index
{ name: 'text', description: 'text' } // Full-text searchUnique Index
{ platformId: 1, platform: 1 } // Prevent duplicatesPurpose: Ensure each contest is unique per platform.
Example Documents
Codeforces Contest
{
"_id": "65c1234567890abcdef12345",
"platformId": "1900",
"name": "Codeforces Round #900 (Div. 2)",
"platform": "codeforces",
"phase": "BEFORE",
"type": "CF",
"startTime": "2024-02-20T14:35:00.000Z",
"endTime": "2024-02-20T16:35:00.000Z",
"durationMinutes": 120,
"websiteUrl": "https://codeforces.com/contest/1900",
"difficulty": "MEDIUM",
"participantCount": 15000,
"problemCount": 6,
"platformMetadata": {
"frozen": false,
"relativeTimeSeconds": -86400
},
"isActive": true,
"isNotified": false,
"lastSyncedAt": "2024-02-19T12:00:00.000Z",
"createdAt": "2024-02-15T10:00:00.000Z",
"updatedAt": "2024-02-19T12:00:00.000Z"
}LeetCode Contest
{
"_id": "65c1234567890abcdef12346",
"platformId": "weekly-contest-380",
"name": "Weekly Contest 380",
"platform": "leetcode",
"phase": "UPCOMING",
"type": "WEEKLY",
"startTime": "2024-02-18T02:30:00.000Z",
"endTime": "2024-02-18T04:00:00.000Z",
"durationMinutes": 90,
"description": "Weekly Contest 380",
"websiteUrl": "https://leetcode.com/contest/weekly-contest-380",
"difficulty": "MEDIUM",
"problemCount": 4,
"platformMetadata": {
"titleSlug": "weekly-contest-380",
"premiumOnly": false
},
"isActive": true,
"isNotified": false,
"lastSyncedAt": "2024-02-17T12:00:00.000Z",
"createdAt": "2024-02-10T08:00:00.000Z",
"updatedAt": "2024-02-17T12:00:00.000Z"
}CodeChef Contest
{
"_id": "65c1234567890abcdef12347",
"platformId": "START120",
"name": "Starters 120",
"platform": "codechef",
"phase": "NOT_STARTED",
"type": "STARTERS",
"startTime": "2024-02-21T14:30:00.000Z",
"endTime": "2024-02-21T16:30:00.000Z",
"durationMinutes": 120,
"websiteUrl": "https://www.codechef.com/START120",
"difficulty": "BEGINNER",
"participantCount": 5000,
"problemCount": 8,
"platformMetadata": {
"division": "All",
"rated": true
},
"isActive": true,
"isNotified": false,
"lastSyncedAt": "2024-02-19T12:00:00.000Z",
"createdAt": "2024-02-15T10:00:00.000Z",
"updatedAt": "2024-02-19T12:00:00.000Z"
}AtCoder Contest
{
"_id": "65c1234567890abcdef12348",
"platformId": "abc340",
"name": "AtCoder Beginner Contest 340",
"platform": "atcoder",
"phase": "BEFORE",
"type": "ABC",
"startTime": "2024-02-17T12:00:00.000Z",
"endTime": "2024-02-17T13:40:00.000Z",
"durationMinutes": 100,
"websiteUrl": "https://atcoder.jp/contests/abc340",
"difficulty": "BEGINNER",
"problemCount": 6,
"platformMetadata": {
"ratedRange": "All",
"penalty": 5
},
"isActive": true,
"isNotified": false,
"lastSyncedAt": "2024-02-16T12:00:00.000Z",
"createdAt": "2024-02-10T08:00:00.000Z",
"updatedAt": "2024-02-16T12:00:00.000Z"
}Common Queries
Find Upcoming Contests
const upcomingContests = await this.contestModel.find({
startTime: { $gt: new Date() },
isActive: true
}).sort({ startTime: 1 });Find Running Contests
const now = new Date();
const runningContests = await this.contestModel.find({
startTime: { $lte: now },
endTime: { $gte: now },
isActive: true
});Find by Platform
const codeforcesContests = await this.contestModel.find({
platform: ContestPlatform.CODEFORCES,
isActive: true
}).sort({ startTime: -1 });Find by Type
const weeklyContests = await this.contestModel.find({
type: ContestType.WEEKLY,
isActive: true
});Search by Name
const contests = await this.contestModel.find({
$text: { $search: 'beginner' }
});Find Contests Needing Notification
const contestsToNotify = await this.contestModel.find({
isNotified: false,
isActive: true,
startTime: {
$gte: new Date(),
$lte: new Date(Date.now() + 24 * 60 * 60 * 1000) // Next 24 hours
}
});Upsert Pattern
Used during platform synchronization to avoid duplicates:
await this.contestModel.updateOne(
{ platformId: contest.platformId, platform: contest.platform },
{ $set: contest },
{ upsert: true }
);Relationships
One-to-Many with Notifications
// Contest has many notifications
Notification.contestId -> Contest._idReferenced by Users (indirectly)
Users receive notifications about contests based on their platform preferences.
Data Lifecycle
1. Creation (Sync)
// Adapter fetches contest from platform API
const contests = await adapter.fetchContests();
// Transform to internal format
const contestData = adapter.transformToInternalFormat(rawContest);
// Upsert to database
await contestModel.updateOne(
{ platformId: contestData.platformId, platform: contestData.platform },
{ $set: contestData },
{ upsert: true }
);2. Update (Re-sync)
Contests are re-synced every 6 hours to update phase, participant count, etc.
3. Notification
// Mark as notified after sending notifications
await contestModel.updateOne(
{ _id: contestId },
{ $set: { isNotified: true } }
);4. Cleanup (Optional)
Old finished contests can be archived or deleted:
// Archive contests older than 90 days
await contestModel.updateMany(
{
endTime: { $lt: new Date(Date.now() - 90 * 24 * 60 * 60 * 1000) },
phase: ContestPhase.FINISHED
},
{ $set: { isActive: false } }
);Performance Optimization
Query Optimization
- Use indexes for frequently queried fields
- Project only needed fields to reduce data transfer
- Use lean() for read-only queries
- Limit results with pagination
Example Optimized Query
const contests = await this.contestModel
.find({
platform: ContestPlatform.CODEFORCES,
startTime: { $gt: new Date() }
})
.select('name startTime endTime platform type')
.sort({ startTime: 1 })
.limit(20)
.lean();Best Practices
✅ Do
- Use upsert to prevent duplicates during sync
- Index frequently queried fields (platform, startTime, phase)
- Use compound indexes for multi-field queries
- Mark contests as notified to prevent duplicate notifications
- Store platform metadata for platform-specific features
- Use virtual fields for computed properties
- Validate enum values before saving
❌ Don't
- Don't create duplicate contests (use unique index)
- Don't skip lastSyncedAt updates
- Don't hard-delete contests (use isActive flag)
- Don't ignore phase updates during re-sync
- Don't query without indexes on large collections
- Don't store redundant data (use virtual fields)
Migration Notes
Adding New Platform
- Add platform to
ContestPlatformenum - Add platform-specific types to
ContestTypeenum - Add platform-specific phases to
ContestPhaseenum - Update
PlatformMetadatainterface - Create platform adapter
- Test sync and queries
Adding New Field
// Add field to schema
@Prop()
newField?: string;
// Migrate existing documents (optional)
await contestModel.updateMany(
{ newField: { $exists: false } },
{ $set: { newField: 'default-value' } }
);
