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

243 lines
12 KiB
Markdown
Raw Normal View History

# 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 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?` 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
### 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`): 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