Skip to content

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.ts

Schema Definition

TypeScript Interface

typescript
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

typescript
@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

typescript
enum ContestPlatform {
  CODEFORCES = 'codeforces',
  LEETCODE = 'leetcode',
  CODECHEF = 'codechef',
  ATCODER = 'atcoder',
}

Supported competitive programming platforms.

ContestPhase

typescript
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

typescript
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

typescript
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)

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.

typescript
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 status
  • relativeTimeSeconds: Time relative to now
  • icpcRegion: ICPC region (if applicable)
  • season: Contest season

LeetCode Metadata

  • titleSlug: URL-friendly contest slug
  • premiumOnly: 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

typescript
{ 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 tracking

Compound Indexes

typescript
{ 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 queries

Text Index

typescript
{ name: 'text', description: 'text' }   // Full-text search

Unique Index

typescript
{ platformId: 1, platform: 1 }          // Prevent duplicates

Purpose: Ensure each contest is unique per platform.

Example Documents

Codeforces Contest

json
{
  "_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

json
{
  "_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

json
{
  "_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

json
{
  "_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

typescript
const upcomingContests = await this.contestModel.find({
  startTime: { $gt: new Date() },
  isActive: true
}).sort({ startTime: 1 });

Find Running Contests

typescript
const now = new Date();
const runningContests = await this.contestModel.find({
  startTime: { $lte: now },
  endTime: { $gte: now },
  isActive: true
});

Find by Platform

typescript
const codeforcesContests = await this.contestModel.find({
  platform: ContestPlatform.CODEFORCES,
  isActive: true
}).sort({ startTime: -1 });

Find by Type

typescript
const weeklyContests = await this.contestModel.find({
  type: ContestType.WEEKLY,
  isActive: true
});

Search by Name

typescript
const contests = await this.contestModel.find({
  $text: { $search: 'beginner' }
});

Find Contests Needing Notification

typescript
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:

typescript
await this.contestModel.updateOne(
  { platformId: contest.platformId, platform: contest.platform },
  { $set: contest },
  { upsert: true }
);

Relationships

One-to-Many with Notifications

typescript
// Contest has many notifications
Notification.contestId -> Contest._id

Referenced by Users (indirectly)

Users receive notifications about contests based on their platform preferences.

Data Lifecycle

1. Creation (Sync)

typescript
// 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

typescript
// 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:

typescript
// 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

  1. Use indexes for frequently queried fields
  2. Project only needed fields to reduce data transfer
  3. Use lean() for read-only queries
  4. Limit results with pagination

Example Optimized Query

typescript
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

  1. Use upsert to prevent duplicates during sync
  2. Index frequently queried fields (platform, startTime, phase)
  3. Use compound indexes for multi-field queries
  4. Mark contests as notified to prevent duplicate notifications
  5. Store platform metadata for platform-specific features
  6. Use virtual fields for computed properties
  7. Validate enum values before saving

❌ Don't

  1. Don't create duplicate contests (use unique index)
  2. Don't skip lastSyncedAt updates
  3. Don't hard-delete contests (use isActive flag)
  4. Don't ignore phase updates during re-sync
  5. Don't query without indexes on large collections
  6. Don't store redundant data (use virtual fields)

Migration Notes

Adding New Platform

  1. Add platform to ContestPlatform enum
  2. Add platform-specific types to ContestType enum
  3. Add platform-specific phases to ContestPhase enum
  4. Update PlatformMetadata interface
  5. Create platform adapter
  6. Test sync and queries

Adding New Field

typescript
// Add field to schema
@Prop()
newField?: string;

// Migrate existing documents (optional)
await contestModel.updateMany(
  { newField: { $exists: false } },
  { $set: { newField: 'default-value' } }
);

Built with ❤️ for competitive programmers worldwide.