API Reference
Nodes
Nodes represent folders in a materialized-paths tree within a project. Files (Video, Audio, etc.) reference their parent folder via folderId but are not nodes themselves.
Path convention:
- Root-level:
path = ",",name = "camera" - Depth 1:
path = ",camera,",name = "front" - Depth 2:
path = ",camera,front,",name = "rgb"
GET /nodes/children
List 1-level children of a node. If parentId is omitted, returns project root nodes.
| Property | Value |
|---|---|
| Auth | JWT |
| DB roundtrips (by parentId) | 3 (2 effective) |
| DB roundtrips (root) | 4 (3 effective) |
By parentId:
| # | Query | Condition | Parallel |
|---|---|---|---|
| 1 | node.findUnique({ id: parentId }) | Always | — |
| 2 | node.findMany({ projectId, path }) | Always | ✓ |
| 3 | node.count({ projectId, path }) | Always | ✓ |
Root (no parentId):
| # | Query | Condition | Parallel |
|---|---|---|---|
| 1 | namespace.findUnique({ slug }) | Always | — |
| 2 | space.findFirst({ nameprojectId, slug }) | Always | — |
| 3 | node.findMany({ projectId, path: "," }) | Always | ✓ |
| 4 | node.count({ projectId, path: "," }) | Always | ✓ |
Query
| Param | Type | Required | Default | Description |
|---|---|---|---|---|
parentId | string | No | — | Parent node ID. Omit for root. |
namespace | string | If no parentId | — | Namespace slug |
project | string | If no parentId | — | Project slug |
page | number | No | 1 | Page number (1-based) |
pageSize | number | No | 50 | Items per page (max 200) |
Response 200
{
"nodes": [
{
"id": "node-id",
"name": "camera",
"kind": "folder",
"leaf": 0,
"path": ",",
"createdAt": "2026-04-14T...",
"updatedAt": "2026-04-14T..."
}
],
"page": 1,
"pageSize": 50,
"total": 2,
"totalPages": 1
}Errors
| Status | Condition |
|---|---|
| 400 | namespace and space missing when parentId omitted |
| 404 | Parent node not found or deleted |
| 404 | Namespace / project not found |
GET /nodes/:nodeId/episodes
List all episodes under a node, including episodes in its descendants. Uses materialized path prefix matching.
| Property | Value |
|---|---|
| Auth | JWT |
| DB roundtrips | 3 (2 effective) |
| # | Query | Condition | Parallel |
|---|---|---|---|
| 1 | node.findUnique({ id: nodeId }) | Always | — |
| 2 | node.findMany({ projectId, kind: "episode", path startsWith ... }) | Always | ✓ |
| 3 | node.count(...) | Always | ✓ |
Params
| Param | Type | Required | Description |
|---|---|---|---|
nodeId | string | Yes | Parent node ID |
page | number | No | Page number (default 1) |
pageSize | number | No | Items per page (default 50, max 200) |
Response 200
{
"episodes": [
{
"id": "node-id",
"name": "ep-alpha",
"path": ",project-a,",
"kind": "episode",
"episodeId": "episode-id",
"nodePath": "/project-a/ep-alpha",
"createdAt": "2026-04-24T...",
"updatedAt": "2026-04-24T..."
}
],
"page": 1,
"pageSize": 50,
"total": 3,
"totalPages": 1
}If the node itself is an episode, it is included in the results. Includes episodes at any depth below the node.
Errors
| Status | Condition |
|---|---|
| 404 | Node not found or deleted |
POST /nodes
Create a folder. All ancestor folders are auto-created.
| Property | Value |
|---|---|
| Auth | JWT |
| DB roundtrips | 2 + N |
| # | Query | Condition |
|---|---|---|
| 1 | namespace.findUnique({ slug }) | Always |
| 2 | space.findFirst({ nameprojectId, slug }) | Always |
| 3–N | node.upsert({ projectId, path, name }) | One per path segment |
| N+1 | node.findUnique({ id }) | Always |
Creating
/camera/front/new= 3 upserts.
Body
| Field | Type | Required | Description |
|---|---|---|---|
namespace | string | Yes | Namespace slug |
project | string | Yes | Project slug |
path | string | Yes | Folder path (e.g. /camera/front/new) |
Response 201
{
"id": "node-id",
"name": "new",
"kind": "folder",
"leaf": 0,
"path": ",camera,front,",
"createdAt": "2026-04-14T...",
"updatedAt": "2026-04-14T..."
}Errors
| Status | Condition |
|---|---|
| 400 | Missing fields or invalid path |
| 400 | Cannot create root node (/) |
| 404 | Namespace / project not found |
PATCH /nodes/:id/rename
Rename a folder. Updates descendant folders' materialized paths in one updateMany.
| Property | Value |
|---|---|
| Auth | JWT |
| DB roundtrips | 4 |
| Descendant writes | O(F) where F = descendant folder count |
| # | Query | Condition |
|---|---|---|
| 1 | node.findUnique({ id }) | Always |
| 2 | node.findFirst({ projectId, path, name }) | Collision check |
| 3 | $runCommandRaw updateMany | Update descendants |
| 4 | node.update({ id }) | Rename self |
Body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | New folder name |
Response 200
{
"id": "node-id",
"name": "renamed",
"kind": "folder",
"leaf": 0,
"path": ",camera,",
"createdAt": "2026-04-14T...",
"updatedAt": "2026-04-14T..."
}Errors
| Status | Condition |
|---|---|
| 400 | name missing |
| 404 | Node not found or deleted |
| 409 | Name collision in same parent |
PATCH /nodes/:id/move
Move a folder to a new parent. Updates descendant folders' materialized paths in one updateMany.
| Property | Value |
|---|---|
| Auth | JWT |
| DB roundtrips | 4–5 |
| Descendant writes | O(F) where F = descendant folder count |
| # | Query | Condition |
|---|---|---|
| 1 | node.findUnique({ id }) | Always |
| 2 | node.findUnique({ id: parentId }) | If parentId not null |
| 3 | node.findFirst({ projectId, path, name }) | Collision check |
| 4 | $runCommandRaw updateMany | Update descendants |
| 5 | node.update({ id }) | Update self |
Body
| Field | Type | Required | Description |
|---|---|---|---|
parentId | string | null | Yes | Target parent node ID, or null for root |
Response 200
{
"id": "node-id",
"name": "front",
"kind": "folder",
"leaf": 0,
"path": ",lidar,",
"createdAt": "2026-04-14T...",
"updatedAt": "2026-04-14T..."
}Errors
| Status | Condition |
|---|---|
| 400 | Cannot move across projects |
| 400 | Cannot move into own subtree |
| 404 | Node / target parent not found or deleted |
| 409 | Name collision in target folder |