import { createCollections } from '@openvidu-dashboard/data-generator/schema'; import type { Db, Document } from 'mongodb'; import { Collection, type EgressDocument, type EventDocument, type HourlyRollupDocument, type IngressSessionDocument, type ParticipantDocument, type RoomDocument, type TrackDocument, type TrackStatsDocument, type TrackSubscriptionDocument } from '@openvidu-dashboard/typings'; import { loadFixture } from './fixtures.js'; const YEAR_SECONDS = 365 * 86_400; const MINUTE = 60_000; /** Contract collections and indexes; TTLs far enough that the TTL monitor never removes seeded documents. */ export async function createContractCollections(db: Db): Promise { await createCollections(db, { timeseries: true, retention: { events: YEAR_SECONDS, stats: YEAR_SECONDS, tracks: YEAR_SECONDS, entities: YEAR_SECONDS, rollups5Min: YEAR_SECONDS, rollupsHourly: YEAR_SECONDS } }); } const SEEDED: readonly Collection[] = [ Collection.ROOMS, Collection.PARTICIPANTS, Collection.TRACKS, Collection.TRACK_SUBSCRIPTIONS, Collection.EGRESSES, Collection.INGRESS_SESSIONS, Collection.EVENTS, Collection.TRACK_STATS, Collection.ANALYTICS_HOURLY ]; export interface Seed { rooms?: RoomDocument[]; participants?: ParticipantDocument[]; tracks?: TrackDocument[]; subscriptions?: TrackSubscriptionDocument[]; egresses?: EgressDocument[]; ingressSessions?: IngressSessionDocument[]; events?: EventDocument[]; trackStats?: TrackStatsDocument[]; hourly?: HourlyRollupDocument[]; } /** Empties the seeded collections and inserts `seed`. */ export async function reseed(db: Db, seed: Seed): Promise { for (const collection of SEEDED) { await db.collection(collection).deleteMany({}); } const inserts: [Collection, Document[] | undefined][] = [ [Collection.ROOMS, seed.rooms], [Collection.PARTICIPANTS, seed.participants], [Collection.TRACKS, seed.tracks], [Collection.TRACK_SUBSCRIPTIONS, seed.subscriptions], [Collection.EGRESSES, seed.egresses], [Collection.INGRESS_SESSIONS, seed.ingressSessions], [Collection.EVENTS, seed.events], [Collection.TRACK_STATS, seed.trackStats], [Collection.ANALYTICS_HOURLY, seed.hourly] ]; for (const [collection, documents] of inserts) { if (documents !== undefined && documents.length > 0) { await db.collection(collection).insertMany(documents); } } } function template(collection: Collection, pick: (doc: T) => boolean): T { const found = loadFixture(collection).find(pick); if (found === undefined) { throw new Error(`no ${collection} fixture matches`); } return structuredClone(found); } export function minutesAgo(minutes: number, now = Date.now()): Date { return new Date(now - minutes * MINUTE); } type RoomInput = Partial & { _id: string; createdAt: Date }; /** A closed room shaped like the contract fixture; ends 30 minutes after creation unless `endedAt` is given. */ export function closedRoom(input: RoomInput): RoomDocument { const base = template(Collection.ROOMS, (room) => room.status === 'closed'); const name = input.name ?? input._id; const endedAt = input.endedAt ?? new Date(input.createdAt.getTime() + 30 * MINUTE); return { ...base, name, nameLower: name.toLowerCase(), endedAt, durationMs: endedAt.getTime() - input.createdAt.getTime(), lastEventAt: endedAt, lastStatsAt: endedAt, expiresAt: new Date(endedAt.getTime() + YEAR_SECONDS * 1000), ...input }; } /** An active room shaped like the contract fixture. */ export function activeRoom(input: RoomInput): RoomDocument { const { endedAt: _endedAt, durationMs: _durationMs, endReason: _endReason, closedBy: _closedBy, ...base } = template(Collection.ROOMS, (room) => room.status === 'active'); const name = input.name ?? input._id; return { ...base, name, nameLower: name.toLowerCase(), lastEventAt: input.createdAt, expiresAt: null, ...input }; } type ParticipantInput = Partial & { _id: string; identity: string; roomSid: string; joinedAt: Date; }; /** A participant session shaped like the contract fixture; active unless `leftAt` is given. */ export function participant(input: ParticipantInput): ParticipantDocument { const { leftAt: _leftAt, durationMs: _durationMs, disconnectReason: _reason, ...base } = template(Collection.PARTICIPANTS, (doc) => doc.identity === 'alice'); const left = input.leftAt !== undefined; return { ...base, identityLower: input.identity.toLowerCase(), kind: 'STANDARD', status: left ? 'left' : 'active', ...(left && input.leftAt ? { durationMs: input.leftAt.getTime() - input.joinedAt.getTime() } : {}), expiresAt: left ? new Date(Date.now() + YEAR_SECONDS * 1000) : null, ...input }; } type TrackInput = Partial & Pick; /** A published track shaped like the contract fixture (the camera of alice); override the source for audio. */ export function track(input: TrackInput): TrackDocument { return { ...template(Collection.TRACKS, (doc) => doc.source === 'CAMERA'), status: 'published', expiresAt: null, ...input }; } type SubscriptionInput = Partial & Pick; /** A subscription shaped like the contract fixture; `_id` is `:`. */ export function subscription(input: SubscriptionInput): TrackSubscriptionDocument { return { ...template(Collection.TRACK_SUBSCRIPTIONS, (doc) => doc.status === 'unsubscribed'), _id: `${input.trackSid}:${input.subscriberSid}`, ...input }; } type EgressInput = Partial & { _id: string; roomSid: string; startedAt: Date }; export function egress(input: EgressInput): EgressDocument { return { ...template(Collection.EGRESSES, (doc) => doc.type === 'room_composite'), ...input }; } type IngressSessionInput = Partial & { _id: string; roomSid: string; startedAt: Date }; export function ingressSession(input: IngressSessionInput): IngressSessionDocument { return { ...template(Collection.INGRESS_SESSIONS, () => true), ...input }; } type EventInput = Partial & Pick; export function event(input: EventInput): EventDocument { return { nodeId: 'ND_test', synthetic: false, payload: { note: input._id }, ...input }; } export function trackStat( input: Pick & Partial ): TrackStatsDocument { return { score: 4, minScore: 3.5, medianScore: 4, bytes: 1_000, packets: 10, packetsLost: 0, rttMs: 20, jitterMs: 5, nacks: 0, plis: 0, firs: 0, frames: 0, layers: [], ...input }; } /** A global hourly rollup with the room and participant counters the rooms summary sums. */ export function globalHourly( bucket: Date, rooms: number, participants: number, scope = 'global' ): HourlyRollupDocument { return { _id: `${scope}:${bucket.toISOString().replace('.000Z', 'Z')}`, scope, bucket, rooms: { created: rooms }, participants: { joined: participants } }; }