2026-10-25 16:35:47 +01:00
|
|
|
import {
|
|
|
|
|
Collection,
|
|
|
|
|
type CollectionDefinition,
|
|
|
|
|
ENTITY_RETENTION_CLASS,
|
|
|
|
|
type IndexDefinition,
|
|
|
|
|
NODE_RETENTION_MS,
|
|
|
|
|
ParticipantStatus,
|
|
|
|
|
RetentionClass,
|
|
|
|
|
RoomStatus
|
|
|
|
|
} from '../src/index.js';
|
|
|
|
|
|
|
|
|
|
type Key = IndexDefinition['key'];
|
|
|
|
|
|
|
|
|
|
function index(
|
|
|
|
|
collection: Collection,
|
|
|
|
|
name: string,
|
|
|
|
|
key: Key,
|
|
|
|
|
extra: Partial<Omit<IndexDefinition, 'collection' | 'name' | 'key'>> = {}
|
|
|
|
|
): IndexDefinition {
|
|
|
|
|
return { collection, name, key, options: extra.options ?? {}, ...omitOptions(extra) };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function omitOptions(extra: Partial<IndexDefinition>): Partial<IndexDefinition> {
|
|
|
|
|
const { options: _options, ...rest } = extra;
|
|
|
|
|
|
|
|
|
|
return rest;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function entityTtl(collection: Collection): IndexDefinition {
|
|
|
|
|
return index(collection, 'expiresAt_ttl', [['expiresAt', 1]], { options: { expireAfterSeconds: 0 } });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ROOM_FEATURES = ['egress', 'ingress', 'agent', 'sip'] as const;
|
|
|
|
|
|
|
|
|
|
/** Every index the writer creates. Names are stable: the writer compares them to update TTL values with `collMod`. */
|
|
|
|
|
export const INDEXES: readonly IndexDefinition[] = [
|
|
|
|
|
index(Collection.ROOMS, 'status_createdAt_id', [
|
|
|
|
|
['status', 1],
|
|
|
|
|
['createdAt', -1],
|
|
|
|
|
['_id', -1]
|
|
|
|
|
]),
|
|
|
|
|
index(Collection.ROOMS, 'createdAt_id', [
|
|
|
|
|
['createdAt', -1],
|
|
|
|
|
['_id', -1]
|
|
|
|
|
]),
|
|
|
|
|
index(Collection.ROOMS, 'status_endedAt_id', [
|
|
|
|
|
['status', 1],
|
|
|
|
|
['endedAt', -1],
|
|
|
|
|
['_id', -1]
|
|
|
|
|
]),
|
|
|
|
|
index(Collection.ROOMS, 'nameLower_createdAt', [
|
|
|
|
|
['nameLower', 1],
|
|
|
|
|
['createdAt', -1]
|
|
|
|
|
]),
|
openvidu-dashboard: media node labels, and nothing about nodes in CE
The contract gains mediaNode on nodes, rooms and participants, stoppedAt on nodes and the
partial mediaNode index that replaces the nodeId one; the scenario's two servers are
labelled. The status reports the writer without node counts and with stoppedAt when the
server stopped, the rooms list filters by mediaNode, list items carry it, the overview
groups by it for editions and a node scope's media statistics coverage follows it.
CE renders nothing about nodes without per-node-breakdown: no nodes line, By node card,
Node select, scope selector or Node columns, not even locked; the Overview reads "Server
stopped at" for a stopped server. The generator gains --media-nodes, which node rollups
now require.
2026-11-04 12:13:58 +01:00
|
|
|
index(
|
|
|
|
|
Collection.ROOMS,
|
|
|
|
|
'mediaNode_status_createdAt_id',
|
|
|
|
|
[
|
|
|
|
|
['mediaNode', 1],
|
|
|
|
|
['status', 1],
|
|
|
|
|
['createdAt', -1],
|
|
|
|
|
['_id', -1]
|
|
|
|
|
],
|
|
|
|
|
{ options: { partialFilterExpression: { mediaNode: { $exists: true } } } }
|
|
|
|
|
),
|
2026-10-25 16:35:47 +01:00
|
|
|
index(
|
|
|
|
|
Collection.ROOMS,
|
|
|
|
|
'status_lastEventAt_active',
|
|
|
|
|
[
|
|
|
|
|
['status', 1],
|
|
|
|
|
['lastEventAt', 1]
|
|
|
|
|
],
|
|
|
|
|
{ options: { partialFilterExpression: { status: RoomStatus.ACTIVE } } }
|
|
|
|
|
),
|
|
|
|
|
...ROOM_FEATURES.map((feature) =>
|
|
|
|
|
index(
|
|
|
|
|
Collection.ROOMS,
|
|
|
|
|
`status_createdAt_id_${feature}`,
|
|
|
|
|
[
|
|
|
|
|
['status', 1],
|
|
|
|
|
['createdAt', -1],
|
|
|
|
|
['_id', -1]
|
|
|
|
|
],
|
|
|
|
|
{ options: { partialFilterExpression: { [`features.${feature}`]: true } } }
|
|
|
|
|
)
|
|
|
|
|
),
|
|
|
|
|
entityTtl(Collection.ROOMS),
|
|
|
|
|
|
|
|
|
|
index(Collection.PARTICIPANTS, 'roomSid_joinedAt_id', [
|
|
|
|
|
['roomSid', 1],
|
|
|
|
|
['joinedAt', -1],
|
|
|
|
|
['_id', -1]
|
|
|
|
|
]),
|
|
|
|
|
index(Collection.PARTICIPANTS, 'roomSid_identityLower_joinedAt', [
|
|
|
|
|
['roomSid', 1],
|
|
|
|
|
['identityLower', 1],
|
|
|
|
|
['joinedAt', -1]
|
|
|
|
|
]),
|
|
|
|
|
index(Collection.PARTICIPANTS, 'identityLower_joinedAt', [
|
|
|
|
|
['identityLower', 1],
|
|
|
|
|
['joinedAt', -1]
|
|
|
|
|
]),
|
|
|
|
|
index(Collection.PARTICIPANTS, 'status_kind', [
|
|
|
|
|
['status', 1],
|
|
|
|
|
['kind', 1]
|
|
|
|
|
]),
|
|
|
|
|
index(Collection.PARTICIPANTS, 'kind_status_joinedAt_id', [
|
|
|
|
|
['kind', 1],
|
|
|
|
|
['status', 1],
|
|
|
|
|
['joinedAt', -1],
|
|
|
|
|
['_id', -1]
|
|
|
|
|
]),
|
|
|
|
|
index(Collection.PARTICIPANTS, 'joinedAt_id', [
|
|
|
|
|
['joinedAt', -1],
|
|
|
|
|
['_id', -1]
|
|
|
|
|
]),
|
|
|
|
|
index(
|
|
|
|
|
Collection.PARTICIPANTS,
|
|
|
|
|
'status_lastStatsAt_open',
|
|
|
|
|
[
|
|
|
|
|
['status', 1],
|
|
|
|
|
['lastStatsAt', 1]
|
|
|
|
|
],
|
|
|
|
|
{
|
|
|
|
|
options: {
|
|
|
|
|
partialFilterExpression: { status: { $in: [ParticipantStatus.ACTIVE, ParticipantStatus.JOINING] } }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
),
|
|
|
|
|
entityTtl(Collection.PARTICIPANTS),
|
|
|
|
|
|
|
|
|
|
index(Collection.TRACKS, 'participantSid_publishedAt', [
|
|
|
|
|
['participantSid', 1],
|
|
|
|
|
['publishedAt', 1]
|
|
|
|
|
]),
|
|
|
|
|
index(Collection.TRACKS, 'roomSid_publishedAt', [
|
|
|
|
|
['roomSid', 1],
|
|
|
|
|
['publishedAt', 1]
|
|
|
|
|
]),
|
|
|
|
|
entityTtl(Collection.TRACKS),
|
|
|
|
|
|
|
|
|
|
index(Collection.TRACK_SUBSCRIPTIONS, 'subscriberSid_subscribedAt', [
|
|
|
|
|
['subscriberSid', 1],
|
|
|
|
|
['subscribedAt', 1]
|
|
|
|
|
]),
|
|
|
|
|
index(Collection.TRACK_SUBSCRIPTIONS, 'trackSid_subscribedAt', [
|
|
|
|
|
['trackSid', 1],
|
|
|
|
|
['subscribedAt', 1]
|
|
|
|
|
]),
|
|
|
|
|
entityTtl(Collection.TRACK_SUBSCRIPTIONS),
|
|
|
|
|
|
|
|
|
|
index(Collection.EGRESSES, 'startedAt_id', [
|
|
|
|
|
['startedAt', -1],
|
|
|
|
|
['_id', -1]
|
|
|
|
|
]),
|
|
|
|
|
index(Collection.EGRESSES, 'roomSid_startedAt', [
|
|
|
|
|
['roomSid', 1],
|
|
|
|
|
['startedAt', -1]
|
|
|
|
|
]),
|
|
|
|
|
index(Collection.EGRESSES, 'status_startedAt', [
|
|
|
|
|
['status', 1],
|
|
|
|
|
['startedAt', -1]
|
|
|
|
|
]),
|
|
|
|
|
index(
|
|
|
|
|
Collection.EGRESSES,
|
|
|
|
|
'roomNameLower_startedAt',
|
|
|
|
|
[
|
|
|
|
|
['roomNameLower', 1],
|
|
|
|
|
['startedAt', -1]
|
|
|
|
|
],
|
|
|
|
|
{ options: { partialFilterExpression: { roomNameLower: { $exists: true } } } }
|
|
|
|
|
),
|
|
|
|
|
entityTtl(Collection.EGRESSES),
|
|
|
|
|
|
|
|
|
|
index(Collection.INGRESSES, 'createdAt_id', [
|
|
|
|
|
['createdAt', -1],
|
|
|
|
|
['_id', -1]
|
|
|
|
|
]),
|
|
|
|
|
index(Collection.INGRESSES, 'status_createdAt', [
|
|
|
|
|
['status', 1],
|
|
|
|
|
['createdAt', -1]
|
|
|
|
|
]),
|
|
|
|
|
index(Collection.INGRESSES, 'nameLower_createdAt', [
|
|
|
|
|
['nameLower', 1],
|
|
|
|
|
['createdAt', -1]
|
|
|
|
|
]),
|
|
|
|
|
index(
|
|
|
|
|
Collection.INGRESSES,
|
|
|
|
|
'lastSessionAt_id',
|
|
|
|
|
[
|
|
|
|
|
['lastSessionAt', -1],
|
|
|
|
|
['_id', -1]
|
|
|
|
|
],
|
|
|
|
|
{ options: { partialFilterExpression: { lastSessionAt: { $exists: true } } } }
|
|
|
|
|
),
|
|
|
|
|
entityTtl(Collection.INGRESSES),
|
|
|
|
|
|
|
|
|
|
index(Collection.INGRESS_SESSIONS, 'ingressId_startedAt', [
|
|
|
|
|
['ingressId', 1],
|
|
|
|
|
['startedAt', -1]
|
|
|
|
|
]),
|
|
|
|
|
index(Collection.INGRESS_SESSIONS, 'roomSid_startedAt', [
|
|
|
|
|
['roomSid', 1],
|
|
|
|
|
['startedAt', -1]
|
|
|
|
|
]),
|
|
|
|
|
index(Collection.INGRESS_SESSIONS, 'status', [['status', 1]]),
|
|
|
|
|
index(Collection.INGRESS_SESSIONS, 'startedAt_id', [
|
|
|
|
|
['startedAt', -1],
|
|
|
|
|
['_id', -1]
|
|
|
|
|
]),
|
|
|
|
|
entityTtl(Collection.INGRESS_SESSIONS),
|
|
|
|
|
|
|
|
|
|
index(Collection.EVENTS, 'roomSid_ts', [
|
|
|
|
|
['roomSid', 1],
|
|
|
|
|
['ts', 1]
|
|
|
|
|
]),
|
|
|
|
|
index(Collection.EVENTS, 'participantSid_ts', [
|
|
|
|
|
['participantSid', 1],
|
|
|
|
|
['ts', 1]
|
|
|
|
|
]),
|
|
|
|
|
index(
|
|
|
|
|
Collection.EVENTS,
|
|
|
|
|
'egressId_ts',
|
|
|
|
|
[
|
|
|
|
|
['egressId', 1],
|
|
|
|
|
['ts', 1]
|
|
|
|
|
],
|
|
|
|
|
{ options: { partialFilterExpression: { egressId: { $exists: true } } } }
|
|
|
|
|
),
|
|
|
|
|
index(
|
|
|
|
|
Collection.EVENTS,
|
|
|
|
|
'ingressId_ts',
|
|
|
|
|
[
|
|
|
|
|
['ingressId', 1],
|
|
|
|
|
['ts', 1]
|
|
|
|
|
],
|
|
|
|
|
{ options: { partialFilterExpression: { ingressId: { $exists: true } } } }
|
|
|
|
|
),
|
|
|
|
|
index(Collection.EVENTS, 'ts_ttl', [['ts', 1]], { retentionClass: RetentionClass.EVENTS }),
|
|
|
|
|
|
|
|
|
|
index(Collection.TRACK_STATS, 'meta.participantSid_ts', [
|
|
|
|
|
['meta.participantSid', 1],
|
|
|
|
|
['ts', 1]
|
|
|
|
|
]),
|
|
|
|
|
index(Collection.TRACK_STATS, 'meta.roomSid_ts', [
|
|
|
|
|
['meta.roomSid', 1],
|
|
|
|
|
['ts', 1]
|
|
|
|
|
]),
|
|
|
|
|
index(Collection.TRACK_STATS, 'ts_ttl', [['ts', 1]], {
|
|
|
|
|
retentionClass: RetentionClass.STATS,
|
|
|
|
|
timeseriesFallbackOnly: true
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
index(
|
|
|
|
|
Collection.ANALYTICS_5MIN,
|
|
|
|
|
'scope_bucket',
|
|
|
|
|
[
|
|
|
|
|
['scope', 1],
|
|
|
|
|
['bucket', 1]
|
|
|
|
|
],
|
|
|
|
|
{ options: { unique: true } }
|
|
|
|
|
),
|
|
|
|
|
index(Collection.ANALYTICS_5MIN, 'bucket_ttl', [['bucket', 1]], { retentionClass: RetentionClass.ROLLUPS_5MIN }),
|
|
|
|
|
index(
|
|
|
|
|
Collection.ANALYTICS_HOURLY,
|
|
|
|
|
'scope_bucket',
|
|
|
|
|
[
|
|
|
|
|
['scope', 1],
|
|
|
|
|
['bucket', 1]
|
|
|
|
|
],
|
|
|
|
|
{ options: { unique: true } }
|
|
|
|
|
),
|
|
|
|
|
index(Collection.ANALYTICS_HOURLY, 'bucket_ttl', [['bucket', 1]], {
|
|
|
|
|
retentionClass: RetentionClass.ROLLUPS_HOURLY
|
|
|
|
|
}),
|
|
|
|
|
index(
|
|
|
|
|
Collection.ANALYTICS_DAILY,
|
|
|
|
|
'scope_bucket',
|
|
|
|
|
[
|
|
|
|
|
['scope', 1],
|
|
|
|
|
['bucket', 1]
|
|
|
|
|
],
|
|
|
|
|
{ options: { unique: true } }
|
|
|
|
|
),
|
|
|
|
|
index(Collection.ANALYTICS_DAILY, 'bucket_ttl', [['bucket', 1]], {
|
|
|
|
|
retentionClass: RetentionClass.ROLLUPS_DAILY
|
|
|
|
|
}),
|
|
|
|
|
|
|
|
|
|
index(Collection.NODES, 'lastSeenAt_ttl', [['lastSeenAt', 1]], {
|
|
|
|
|
options: { expireAfterSeconds: NODE_RETENTION_MS / 1000 }
|
|
|
|
|
})
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
/** Every collection of the model in creation order, with its validator file and retention. */
|
|
|
|
|
export const COLLECTION_DEFINITIONS: readonly CollectionDefinition[] = Object.values(Collection).map((name) => {
|
|
|
|
|
const definition: CollectionDefinition = { name, schema: `${name}.schema.json` };
|
|
|
|
|
const retentionClass = (ENTITY_RETENTION_CLASS as Partial<Record<Collection, RetentionClass>>)[name];
|
|
|
|
|
|
|
|
|
|
if (retentionClass !== undefined) {
|
|
|
|
|
definition.retentionClass = retentionClass;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (name === Collection.TRACK_STATS) {
|
|
|
|
|
definition.timeseries = {
|
|
|
|
|
timeField: 'ts',
|
|
|
|
|
metaField: 'meta',
|
|
|
|
|
granularity: 'seconds',
|
|
|
|
|
retentionClass: RetentionClass.STATS
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return definition;
|
|
|
|
|
});
|