openvidu/openvidu-dashboard/openspec/specs/rooms-api/spec.md

252 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# rooms-api Specification
## Purpose
Read access to room sessions: the rooms list and its summary numbers, every query backed by an index.
## Requirements
### Requirement: Rooms list endpoint
Edition: CE. `GET /api/v1/rooms` SHALL list the room sessions created in the range with the common query contract, the
filters `status`, `q`, `features` and `nodeId`, the sort keys `createdAt` (default, index-backed) and the ranked
`endedAt`, `duration`, `uniqueIdentities` and `active`, `order` (`desc` default, `asc`), and the list envelope
`{ items, nextCursor, prevCursor, total? }` with `total` under the `DASHBOARD_LIST_TOTAL_MAX_RANGE` rule.
#### Scenario: Default list
- **GIVEN** 40 rooms created in the last 24 hours and 5 older ones
- **WHEN** the client requests `GET /api/v1/rooms` with a valid token
- **THEN** the response is `200` with the 25 most recently created rooms of the last 24 hours, `nextCursor` set,
`prevCursor: null` and `total: 40`
#### Scenario: Cursor walk
- **GIVEN** 60 rooms in range and `limit=25`
- **WHEN** the client follows `nextCursor` twice and then `prevCursor` once
- **THEN** the pages hold rooms 1-25, 26-50, 51-60 and 26-50 with no gaps or duplicates, each page ordered by
`createdAt desc` with `_id desc` as tie-breaker
#### Scenario: Ranked cursor walk
- **GIVEN** 60 rooms created in range, two of them active, and `sort=duration&limit=25`
- **WHEN** the client follows `nextCursor` twice
- **THEN** the pages hold rooms 1-25, 26-50 and 51-60 of the duration ranking at the first page's `asOf`, with `total:
60` on each page and no gaps or duplicates
#### Scenario: Common query contract
- **WHEN** the client requests `GET /api/v1/rooms`
- **THEN** the common query contract it accepts is `from`, `to`, `tz`, `limit` (1-100, default 25) and an opaque
`cursor`
### Requirement: Room list item
Edition: CE. Each rooms list item SHALL be the room document's public projection: identity, lifecycle, node,
participant, egress and ingress counters, bandwidth, feature flags, the end cause behind the "estimated" cue, the last
stats flush, the media statistics mark and the `stale` flag of `api-conventions`, never internal id arrays,
`schemaVersion` or `expiresAt`.
Participant counters count STANDARD sessions only; `participants.unique` counts distinct STANDARD identities.
#### Scenario: Fields of an item
- **WHEN** an item is read
- **THEN** it has `sid`, `name`, `status` (`active` | `closed`), `createdAt`, `endedAt?`, `durationMs?`, `closedBy?`
(`event` | `reconciler`), `nodeId?`, `participants { sessions, unique, active, peakActive }`,
`egress { total, active }`, `ingress { sessions, active }`, `bytes { up, down }`,
`features { egress, ingress, agent, sip }`, `lastStatsAt?`, `stale?`, `mediaStats?` and no `participantSids`,
`identities` or similar array
#### Scenario: Active room
- **GIVEN** a room still open with three active STANDARD participants and one active EGRESS participant
- **WHEN** it is returned
- **THEN** `status` is `active`, `endedAt`, `durationMs` and `closedBy` are absent and `participants.active` is 3
#### Scenario: Unique identities
- **GIVEN** a closed room where STANDARD identity `alice` had three sessions, `bob` one, and an AGENT session joined
- **WHEN** it is returned
- **THEN** `participants.sessions` is 4 and `participants.unique` is 2
#### Scenario: Stale active room
- **GIVEN** the default writer batch interval of 10 s and an active room whose `lastStatsAt` is 3 minutes old at the
server's clock
- **WHEN** it is returned
- **THEN** it has `stale: true`, and an active room whose `lastStatsAt` is 20 s old has no `stale` property
#### Scenario: Room of a node without media statistics
- **GIVEN** a room document with `mediaStats: false` and `bytes { up: 0, down: 0 }`
- **WHEN** it is returned
- **THEN** it has `mediaStats: false` and `bytes { up: 0, down: 0 }`, and an item of a room without the field has no
`mediaStats` property
### Requirement: Status filter
Edition: CE. The rooms list SHALL accept `status` as `active`, `closed` or `all` (default `all`) and reject any other
value with `400` `VALIDATION_ERROR`.
#### Scenario: Active rooms only
- **WHEN** the client sends `?status=active`
- **THEN** every item has `status: "active"` and the time range still applies to `createdAt`
#### Scenario: Unknown status
- **WHEN** the client sends `?status=open`
- **THEN** the response is `400` with code `VALIDATION_ERROR` and `details.issues[0].path` equal to `["status"]`
### Requirement: Time range and sort keys
Edition: CE. The rooms list SHALL apply `from`/`to` to `createdAt` for every sort key. `createdAt` SHALL be served in
index order; `endedAt`, `duration`, `uniqueIdentities` and `active` SHALL be ranked sorts as `api-conventions` defines,
accepted with any `status` when the range is within `DASHBOARD_LIST_TOTAL_MAX_RANGE` and answered `400`
`SORT_RANGE_TOO_WIDE` beyond it. Any other sort key SHALL be rejected with `400` `VALIDATION_ERROR`.
#### Scenario: Closed rooms by end time
- **GIVEN** a room created 3 days ago that ended 2 hours ago, and rooms created 5 and 8 hours ago that ended 3 hours
and 1 hour ago
- **WHEN** the client sends `?status=closed&sort=endedAt&from=now-24h&to=now`
- **THEN** `items` holds the room ended 1 hour ago and the room ended 3 hours ago, in that order, and not the room
created 3 days ago, whose `createdAt` is outside the range
#### Scenario: End-time sort without the closed filter
- **GIVEN** created in the last 24 hours: a room ended 1 hour ago, a room ended 10 minutes ago and an active room
- **WHEN** the client sends `?sort=endedAt&status=all`
- **THEN** the response is `200` with the active room (ranked at `asOf`), the room ended 10 minutes ago and the room
ended 1 hour ago, in that order
#### Scenario: Rooms by unique identities
- **GIVEN** three rooms created in range with `participants.unique` 12, 3 and 7
- **WHEN** the client sends `?sort=uniqueIdentities`
- **THEN** `items` holds the rooms with 12, 7 and 3 unique identities, in that order
#### Scenario: Rooms by active participants
- **GIVEN** an active room with 5 active participants, an active room with 2 and a closed room, all created in range
- **WHEN** the client sends `?sort=active&order=desc`
- **THEN** `items` holds the room with 5, the room with 2 and the closed room (0), in that order
#### Scenario: Ranked sort beyond the limit
- **GIVEN** the default `DASHBOARD_LIST_TOTAL_MAX_RANGE` of 7 days
- **WHEN** the client sends `?sort=uniqueIdentities&from=now-30d&to=now`
- **THEN** the response is `400` with code `SORT_RANGE_TOO_WIDE`
#### Scenario: Unsupported sort key
- **WHEN** the client sends `?sort=name`
- **THEN** the response is `400` with code `VALIDATION_ERROR` and `details.issues[0].path` equal to `["sort"]`
#### Scenario: Ranked values
- **WHEN** the client requests a ranked sort
- **THEN** `endedAt` ranks `endedAt` (`asOf` while active), `duration` ranks `durationMs` (`asOf − createdAt` while
active), `uniqueIdentities` ranks `participants.unique` and `active` ranks `participants.active`
### Requirement: Name prefix search and SID lookup
Edition: CE. The rooms list SHALL treat `q` as a case-insensitive prefix over the room name when it has at least two
characters, combined with `status`, `features` and `nodeId`, SHALL reject a one-character `q` with `400`, and SHALL
treat a value matching `^RM_[A-Za-z0-9]+$` as an exact SID lookup that ignores `from`, `to`, `status`, `features`,
`nodeId` and `sort` and returns at most one item.
#### Scenario: Prefix search
- **GIVEN** rooms named `Demo-Room`, `demo-2` and `my-demo` created in range
- **WHEN** the client sends `?q=DE`
- **THEN** `items` holds `Demo-Room` and `demo-2` ordered by `createdAt desc`, and not `my-demo`
#### Scenario: One character
- **WHEN** the client sends `?q=d`
- **THEN** the response is `400` with code `VALIDATION_ERROR`, `details.issues[0].path` equal to `["q"]` and a message
stating the two-character minimum
#### Scenario: Exact SID outside the range
- **GIVEN** a closed room `RM_ab12cd` created 40 days ago
- **WHEN** the client sends `?q=RM_ab12cd&from=now-24h&status=active`
- **THEN** the response is `200` with that single room in `items`, `total: 1` and both cursors `null`
#### Scenario: Prefix within one node
- **GIVEN** rooms named `demo-a` on `node-a` and `demo-b` on `node-b`, both created in range
- **WHEN** the client sends `?q=demo&nodeId=node-a`
- **THEN** `items` holds `demo-a` only
### Requirement: Feature and node filters
Edition: CE. The rooms list SHALL accept `features` as a comma-separated subset of `egress`, `ingress`, `agent`, `sip`
and return rooms whose flag is set for any of the listed values, SHALL accept `nodeId` as an exact match on the room's
node, and SHALL reject an unknown feature value with `400`.
#### Scenario: Rooms with egress or ingress
- **GIVEN** a room with an egress, a room with an ingress session and a plain room
- **WHEN** the client sends `?features=egress,ingress`
- **THEN** the first two rooms are returned and the plain room is not
#### Scenario: Rooms of one node
- **WHEN** the client sends `?nodeId=node-a&status=active`
- **THEN** every item has `nodeId: "node-a"` and `status: "active"`
#### Scenario: Unknown feature
- **WHEN** the client sends `?features=egress,webhook`
- **THEN** the response is `400` with code `VALIDATION_ERROR` and `details.issues[0].path` equal to `["features"]`
### Requirement: Rooms summary endpoint
Edition: CE. `GET /api/v1/rooms/summary` SHALL accept `from`, `to` and `tz` and answer `200` with
`{ activeRooms, roomsInRange, participantSessions, activeParticipants, approximate }`, where the participant counts
are STANDARD sessions and the two range counts are exact when the range is within `DASHBOARD_LIST_TOTAL_MAX_RANGE`
and are hour-aligned sums of the hourly rollups with `approximate: true` beyond it.
#### Scenario: Short range
- **GIVEN** 7 active rooms, 120 rooms created, 900 STANDARD and 40 EGRESS participant sessions joined in the last 24
hours, and 31 active STANDARD participants
- **WHEN** the client requests `GET /api/v1/rooms/summary?from=now-24h&to=now`
- **THEN** the response is `{ activeRooms: 7, roomsInRange: 120, participantSessions: 900, activeParticipants: 31,
approximate: false }`
#### Scenario: Long range
- **GIVEN** the default threshold of 7 days
- **WHEN** the client requests `GET /api/v1/rooms/summary?from=now-30d&to=now`
- **THEN** `roomsInRange` and `participantSessions` equal the sums of `rooms.created` and `participants.joined` of the
global hourly rollups whose bucket starts inside the range, and `approximate` is `true`
### Requirement: Index-backed room queries
Edition: CE. Every query of this capability SHALL run on an index of the shared contract
(`dashboard-ce/typings/contract/indexes.ts`), or on `_id` for the hourly rollups the long-range summary reads: no
winning plan contains a `COLLSCAN`, and `totalDocsExamined` is bounded
by the page size plus the documented residual filters for the index-ordered pages and by the rooms created in the range
for the ranked sorts.
#### Scenario: Explain plans on the generated database
- **GIVEN** a database generated by the data generator at scenario M
- **WHEN** the list (every filter combination and sort) and summary queries are explained
- **THEN** each winning plan uses an index of the shared contract and none contains `COLLSCAN`
#### Scenario: Ranked sorts over 7 days
- **GIVEN** a database generated at scenario M for 7 days
- **WHEN** the list is explained with each ranked sort key, with and without `status`, `q`, `nodeId` and `features`
- **THEN** each plan scans the range index of its filters (`createdAt_id`, `status_createdAt_id`,
`nameLower_createdAt`, `nodeId_status_createdAt_id` or the feature indexes), `totalDocsExamined` is at most the rooms
created in the range, and the p95 of a page is within 300 ms
#### Scenario: Name prefix within one node
- **WHEN** the list with `q` and `nodeId` is explained
- **THEN** the plan uses `nameLower_createdAt` with `nodeId` as a residual filter