docs(media-items): Work Order 3 batch 3a — reader-adjacent CRUD (11 endpoints)
New frontmatter'd docs with real shapes (sqlc models pinned directly):
- bookmarks: get/create/update/delete (sync-aware rows: dedup_key,
last_modified_*, tombstones; delete is 204)
- review: upsert (empty string DELETES the review) / get (200 with
{review:null} when absent) / delete
- get single note + highlight (full sqlc row shapes)
- annotation history: restore (POST, kind via query or body) and purge
(DELETE, kind via required query; permanent)
Gate hardening forced by this batch:
- required-field walk now guards non-struct Request types (RawMessage pins)
- update_media_bookmark needed a full example block (no cross-reference
shortcuts — the gate checks every doc standalone)
This commit is contained in:
1 parent
85cdaf7196
commit
ed0ce5d077
13 files changed
+545
-4
No files matched your search
@@ -0,0 +1,76 @@
|
|||||||
|
---
|
||||||
|
method: POST
|
||||||
|
path: /api/media-items/:id/bookmarks
|
||||||
|
auth: jwt
|
||||||
|
area: media-items
|
||||||
|
---
|
||||||
|
<!-- source: internal/handlers/media.go:2206 MediaHandler.CreateMediaBookmark -->
|
||||||
|
|
||||||
|
# Create Media Bookmark
|
||||||
|
|
||||||
|
Creates a bookmark through the sync-aware path (dedup + last-write-wins +
|
||||||
|
tombstones). Returns **201** with the stored bookmark row.
|
||||||
|
|
||||||
|
## Example request
|
||||||
|
|
||||||
|
```json example-request
|
||||||
|
{
|
||||||
|
"title": "Chapter 3",
|
||||||
|
"position": "kindle:pos:1:2",
|
||||||
|
"notes": "revisit this",
|
||||||
|
"cfi_position": "epubcfi(/6/4)",
|
||||||
|
"page_number": 42,
|
||||||
|
"chapter_number": 3,
|
||||||
|
"percentage": 35.5,
|
||||||
|
"chapter_reference": 3,
|
||||||
|
"origin": "android"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example response
|
||||||
|
|
||||||
|
```json example-response
|
||||||
|
{
|
||||||
|
"id": "110e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"media_item_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"user_id": "92413c3a-e1b8-4fff-b3f9-95c74478d19a",
|
||||||
|
"page_number": 42,
|
||||||
|
"chapter_number": 3,
|
||||||
|
"cfi_position": "epubcfi(/6/4)",
|
||||||
|
"title": "Chapter 3",
|
||||||
|
"position": "kindle:pos:1:2",
|
||||||
|
"notes": "revisit this",
|
||||||
|
"created_at": "2026-10-24T13:57:01Z",
|
||||||
|
"dedup_key": "kindle:pos:1:2",
|
||||||
|
"last_modified_at": "2026-10-24T13:57:01Z",
|
||||||
|
"last_modified_source": "android",
|
||||||
|
"device_sync_data": null,
|
||||||
|
"percentage_location": 35.5,
|
||||||
|
"epubcfi_location": "epubcfi(/6/4)",
|
||||||
|
"chapter_reference": 3,
|
||||||
|
"deleted": false,
|
||||||
|
"deleted_at": null,
|
||||||
|
"origin_source": "android"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Request fields
|
||||||
|
|
||||||
|
| Field | Type | Required | Notes |
|
||||||
|
| ----- | ---- | -------- | ----- |
|
||||||
|
| title | string | yes | 1–255 chars |
|
||||||
|
| position | string | no | ≤100 chars |
|
||||||
|
| notes | string | no | ≤100000 chars |
|
||||||
|
| cfi_position | string | no | ≤255 chars |
|
||||||
|
| page_number | number | no | |
|
||||||
|
| chapter_number | number | no | |
|
||||||
|
| percentage | number | no | |
|
||||||
|
| chapter_reference | number | no | |
|
||||||
|
| origin | string | no | client label ("android", "web"); defaults to "web" |
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
| Status | Body | When |
|
||||||
|
| ------ | ---- | ---- |
|
||||||
|
| 400 | `{"error": "invalid request"}` / validation error | malformed body |
|
||||||
|
| 500 | `{"error": "..."}` | annotation service failure |
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
method: DELETE
|
||||||
|
path: /api/media-items/:id/bookmarks/:bookmarkId
|
||||||
|
auth: jwt
|
||||||
|
area: media-items
|
||||||
|
---
|
||||||
|
<!-- source: internal/handlers/media.go:2304 MediaHandler.DeleteMediaBookmark -->
|
||||||
|
|
||||||
|
# Delete Media Bookmark
|
||||||
|
|
||||||
|
Tombstones a bookmark (sync-aware delete, source "web"). Success is
|
||||||
|
**204 No Content** — no body.
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
| Status | Body | When |
|
||||||
|
| ------ | ---- | ---- |
|
||||||
|
| 400 | `{"error": "invalid bookmark id"}` / `{"error": "invalid user"}` | malformed |
|
||||||
|
| 404 | `{"error": "bookmark not found"}` | no such bookmark |
|
||||||
|
| 500 | `{"error": "..."}` | unexpected server error |
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
method: DELETE
|
||||||
|
path: /api/media-items/:id/review
|
||||||
|
auth: jwt
|
||||||
|
area: media-items
|
||||||
|
---
|
||||||
|
<!-- source: internal/handlers/media.go:1148 MediaHandler.DeleteMediaReview -->
|
||||||
|
|
||||||
|
# Delete Media Review
|
||||||
|
|
||||||
|
Deletes the signed-in user's review for a media item.
|
||||||
|
|
||||||
|
## Example response
|
||||||
|
|
||||||
|
```json example-response
|
||||||
|
{
|
||||||
|
"message": "review deleted"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
| Status | Body | When |
|
||||||
|
| ------ | ---- | ---- |
|
||||||
|
| 400 | `{"error": "invalid user"}` / `{"error": "invalid media item id"}` | malformed |
|
||||||
|
| 500 | `{"error": "..."}` | unexpected server error |
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
---
|
||||||
|
method: GET
|
||||||
|
path: /api/media-items/:id/bookmarks
|
||||||
|
auth: jwt
|
||||||
|
area: media-items
|
||||||
|
---
|
||||||
|
<!-- source: internal/handlers/media.go:2181 MediaHandler.GetMediaBookmarks -->
|
||||||
|
|
||||||
|
# Get Media Bookmarks
|
||||||
|
|
||||||
|
Lists the signed-in user's bookmarks for one media item (sync-aware rows:
|
||||||
|
dedup key and last-writer bookkeeping included).
|
||||||
|
|
||||||
|
## Example response
|
||||||
|
|
||||||
|
```json example-response
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "110e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"media_item_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"user_id": "92413c3a-e1b8-4fff-b3f9-95c74478d19a",
|
||||||
|
"page_number": null,
|
||||||
|
"chapter_number": null,
|
||||||
|
"cfi_position": null,
|
||||||
|
"title": "Chapter 3",
|
||||||
|
"position": "kindle:pos:1:2",
|
||||||
|
"notes": null,
|
||||||
|
"created_at": "2026-10-24T13:57:01Z",
|
||||||
|
"dedup_key": "kindle:pos:1:2",
|
||||||
|
"last_modified_at": "2026-10-24T13:57:01Z",
|
||||||
|
"last_modified_source": "web",
|
||||||
|
"device_sync_data": null,
|
||||||
|
"percentage_location": null,
|
||||||
|
"epubcfi_location": null,
|
||||||
|
"chapter_reference": null,
|
||||||
|
"deleted": false,
|
||||||
|
"deleted_at": null,
|
||||||
|
"origin_source": "web"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
`device_sync_data` is `[]byte` → base64 string when set, `null` otherwise.
|
||||||
|
pgtype fields are `null` when unset.
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
| Status | Body | When |
|
||||||
|
| ------ | ---- | ---- |
|
||||||
|
| 400 | `{"error": "invalid user"}` / `{"error": "invalid media item id"}` | malformed ids |
|
||||||
|
| 500 | `{"error": "..."}` | unexpected server error |
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
---
|
||||||
|
method: GET
|
||||||
|
path: /api/media-items/:id/highlights/:highlightId
|
||||||
|
auth: jwt
|
||||||
|
area: media-items
|
||||||
|
---
|
||||||
|
<!-- source: internal/handlers/media.go:2078 MediaHandler.GetMediaHighlight -->
|
||||||
|
|
||||||
|
# Get Media Highlight
|
||||||
|
|
||||||
|
Returns one highlight by id.
|
||||||
|
|
||||||
|
## Example response
|
||||||
|
|
||||||
|
```json example-response
|
||||||
|
{
|
||||||
|
"id": "440e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"media_item_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"user_id": "92413c3a-e1b8-4fff-b3f9-95c74478d19a",
|
||||||
|
"selection_text": "It was a bright cold day in April...",
|
||||||
|
"start_position": "kobo:3.1",
|
||||||
|
"end_position": "kobo:3.4",
|
||||||
|
"color": "yellow",
|
||||||
|
"note_id": null,
|
||||||
|
"created_at": "2026-10-24T13:57:01Z",
|
||||||
|
"updated_at": "2026-10-24T13:57:01Z",
|
||||||
|
"percentage_start": 41.2,
|
||||||
|
"percentage_end": 41.8,
|
||||||
|
"character_start": null,
|
||||||
|
"character_end": null,
|
||||||
|
"epubcfi_start": null,
|
||||||
|
"epubcfi_end": null,
|
||||||
|
"chapter_reference": 3,
|
||||||
|
"paragraph_start": null,
|
||||||
|
"paragraph_end": null,
|
||||||
|
"panel_number": null,
|
||||||
|
"device_sync_data": null,
|
||||||
|
"dedup_key": "kobo:3.1-3.4",
|
||||||
|
"last_modified_at": "2026-10-24T13:57:01Z",
|
||||||
|
"last_modified_source": "web",
|
||||||
|
"note_text": null,
|
||||||
|
"deleted": false,
|
||||||
|
"deleted_at": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
| Status | Body | When |
|
||||||
|
| ------ | ---- | ---- |
|
||||||
|
| 400 | `{"error": "invalid highlight id"}` | malformed id |
|
||||||
|
| 404 | `{"error": "highlight not found"}` | no such highlight |
|
||||||
|
| 500 | `{"error": "..."}` | unexpected server error |
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
---
|
||||||
|
method: GET
|
||||||
|
path: /api/media-items/:id/notes/:noteId
|
||||||
|
auth: jwt
|
||||||
|
area: media-items
|
||||||
|
---
|
||||||
|
<!-- source: internal/handlers/media.go:1913 MediaHandler.GetMediaNote -->
|
||||||
|
|
||||||
|
# Get Media Note
|
||||||
|
|
||||||
|
Returns one note by id.
|
||||||
|
|
||||||
|
## Example response
|
||||||
|
|
||||||
|
```json example-response
|
||||||
|
{
|
||||||
|
"id": "330e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"media_item_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"user_id": "92413c3a-e1b8-4fff-b3f9-95c74478d19a",
|
||||||
|
"content": "Key theme here.",
|
||||||
|
"position": "kobo:3.1",
|
||||||
|
"created_at": "2026-10-24T13:57:01Z",
|
||||||
|
"updated_at": "2026-10-24T13:57:01Z",
|
||||||
|
"percentage_location": 42.0,
|
||||||
|
"character_start": null,
|
||||||
|
"character_end": null,
|
||||||
|
"epubcfi_location": null,
|
||||||
|
"chapter_reference": 3,
|
||||||
|
"paragraph_reference": null,
|
||||||
|
"device_sync_data": null,
|
||||||
|
"dedup_key": "kobo:3.1",
|
||||||
|
"last_modified_at": "2026-10-24T13:57:01Z",
|
||||||
|
"last_modified_source": "web",
|
||||||
|
"deleted": false,
|
||||||
|
"deleted_at": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`device_sync_data` is `[]byte` → base64 string when set.
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
| Status | Body | When |
|
||||||
|
| ------ | ---- | ---- |
|
||||||
|
| 400 | `{"error": "invalid note id"}` | malformed id |
|
||||||
|
| 404 | `{"error": "note not found"}` | no such note |
|
||||||
|
| 500 | `{"error": "..."}` | unexpected server error |
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
method: GET
|
||||||
|
path: /api/media-items/:id/review
|
||||||
|
auth: jwt
|
||||||
|
area: media-items
|
||||||
|
---
|
||||||
|
<!-- source: internal/handlers/media.go:1120 MediaHandler.GetMediaReview -->
|
||||||
|
|
||||||
|
# Get Media Review
|
||||||
|
|
||||||
|
Returns the signed-in user's review for a media item, or `{"review": null}`
|
||||||
|
when none exists (200 in both cases).
|
||||||
|
|
||||||
|
## Example response
|
||||||
|
|
||||||
|
```json example-response
|
||||||
|
{
|
||||||
|
"id": "220e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"media_item_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"user_id": "92413c3a-e1b8-4fff-b3f9-95c74478d19a",
|
||||||
|
"review": "A slow start, but the second act earns it.",
|
||||||
|
"created_at": "2026-10-24T13:57:01Z",
|
||||||
|
"updated_at": "2026-10-24T13:57:01Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
| Status | Body | When |
|
||||||
|
| ------ | ---- | ---- |
|
||||||
|
| 400 | `{"error": "invalid user"}` / `{"error": "invalid media item id"}` | malformed |
|
||||||
|
| 500 | `{"error": "..."}` | unexpected server error |
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
method: DELETE
|
||||||
|
path: /api/media-items/:id/annotations/:annotationId
|
||||||
|
auth: jwt
|
||||||
|
area: media-items
|
||||||
|
---
|
||||||
|
<!-- source: internal/handlers/annotation_history.go:101 MediaHandler.PurgeDeletedAnnotation -->
|
||||||
|
|
||||||
|
# Purge Deleted Annotation
|
||||||
|
|
||||||
|
**Permanent** — removes a tombstoned annotation from history entirely. Kind
|
||||||
|
comes from the required `annotation_type` query parameter
|
||||||
|
(`highlight` | `note` | `bookmark`; no body fallback on DELETE).
|
||||||
|
|
||||||
|
## Example request
|
||||||
|
|
||||||
|
```text
|
||||||
|
DELETE /api/media-items/550e8400-.../annotations/440e8400-...?annotation_type=highlight
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example response
|
||||||
|
|
||||||
|
```json example-response
|
||||||
|
{
|
||||||
|
"purged": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
| Status | Body | When |
|
||||||
|
| ------ | ---- | ---- |
|
||||||
|
| 400 | `{"error": "annotation_type must be highlight, note, or bookmark"}` | missing/invalid kind |
|
||||||
|
| 404 | `{"error": "deleted annotation not found"}` | nothing tombstoned under that id |
|
||||||
|
| 500 | `{"error": "failed to purge annotation"}` | unexpected server error |
|
||||||
|
| 503 | `{"error": "annotation service unavailable"}` | service not constructed |
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
method: POST
|
||||||
|
path: /api/media-items/:id/annotations/:annotationId/restore
|
||||||
|
auth: jwt
|
||||||
|
area: media-items
|
||||||
|
---
|
||||||
|
<!-- source: internal/handlers/annotation_history.go:77 MediaHandler.RestoreDeletedAnnotation -->
|
||||||
|
|
||||||
|
# Restore Deleted Annotation
|
||||||
|
|
||||||
|
Restores a tombstoned annotation (highlight, note, or bookmark). The kind is
|
||||||
|
read from the `annotation_type` **query parameter** or, if absent, from a JSON
|
||||||
|
body `{"annotation_type": "..."}`.
|
||||||
|
|
||||||
|
## Example request
|
||||||
|
|
||||||
|
```text
|
||||||
|
POST /api/media-items/550e8400-.../annotations/440e8400-.../restore?annotation_type=highlight
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example response
|
||||||
|
|
||||||
|
```json example-response
|
||||||
|
{
|
||||||
|
"restored": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
| Status | Body | When |
|
||||||
|
| ------ | ---- | ---- |
|
||||||
|
| 400 | `{"error": "annotation_type must be highlight, note, or bookmark"}` | missing/invalid kind |
|
||||||
|
| 404 | `{"error": "deleted annotation not found"}` | nothing tombstoned under that id |
|
||||||
|
| 500 | `{"error": "failed to restore annotation"}` | unexpected server error |
|
||||||
|
| 503 | `{"error": "annotation service unavailable"}` | service not constructed |
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
---
|
||||||
|
method: PUT
|
||||||
|
path: /api/media-items/:id/bookmarks/:bookmarkId
|
||||||
|
auth: jwt
|
||||||
|
area: media-items
|
||||||
|
---
|
||||||
|
<!-- source: internal/handlers/media.go:2252 MediaHandler.UpdateMediaBookmark -->
|
||||||
|
|
||||||
|
# Update Media Bookmark
|
||||||
|
|
||||||
|
Edits a bookmark through the sync service: lands as a proper last-write-wins
|
||||||
|
edit with `source`/`modified_at` bookkeeping. Zero-valued locator fields
|
||||||
|
coalesce with the stored row (omitted fields keep their values).
|
||||||
|
|
||||||
|
## Example request
|
||||||
|
|
||||||
|
```json example-request
|
||||||
|
{
|
||||||
|
"title": "Chapter 3 (renamed)",
|
||||||
|
"notes": "updated note",
|
||||||
|
"position": "kindle:pos:1:2"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example response
|
||||||
|
|
||||||
|
```json example-response
|
||||||
|
{
|
||||||
|
"id": "110e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"media_item_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"user_id": "92413c3a-e1b8-4fff-b3f9-95c74478d19a",
|
||||||
|
"page_number": null,
|
||||||
|
"chapter_number": null,
|
||||||
|
"cfi_position": "epubcfi(/6/4)",
|
||||||
|
"title": "Chapter 3 (renamed)",
|
||||||
|
"position": "kindle:pos:1:2",
|
||||||
|
"notes": "updated note",
|
||||||
|
"created_at": "2026-10-24T13:57:01Z",
|
||||||
|
"dedup_key": "kindle:pos:1:2",
|
||||||
|
"last_modified_at": "2026-10-24T14:57:01Z",
|
||||||
|
"last_modified_source": "web",
|
||||||
|
"device_sync_data": null,
|
||||||
|
"percentage_location": 35.5,
|
||||||
|
"epubcfi_location": "epubcfi(/6/4)",
|
||||||
|
"chapter_reference": 3,
|
||||||
|
"deleted": false,
|
||||||
|
"deleted_at": null,
|
||||||
|
"origin_source": "web"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Request fields
|
||||||
|
|
||||||
|
| Field | Type | Required | Notes |
|
||||||
|
| ----- | ---- | -------- | ----- |
|
||||||
|
| title | string | yes | 1–255 chars |
|
||||||
|
| notes | string | no | |
|
||||||
|
| position | string | no | |
|
||||||
|
| cfi_position | string | no | zero value preserves stored |
|
||||||
|
| page_number | number | no | zero value preserves stored |
|
||||||
|
| chapter_number | number | no | zero value preserves stored |
|
||||||
|
| percentage | number | no | zero value preserves stored |
|
||||||
|
| chapter_reference | number | no | zero value preserves stored |
|
||||||
|
| origin | string | no | client label |
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
| Status | Body | When |
|
||||||
|
| ------ | ---- | ---- |
|
||||||
|
| 400 | `{"error": "invalid bookmark id"}` / `{"error": "invalid media item id"}` / `{"error": "invalid request"}` | malformed |
|
||||||
|
| 500 | `{"error": "..."}` | annotation service failure |
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
---
|
||||||
|
method: POST
|
||||||
|
path: /api/media-items/:id/review
|
||||||
|
auth: jwt
|
||||||
|
area: media-items
|
||||||
|
---
|
||||||
|
<!-- source: internal/handlers/media.go:1072 MediaHandler.UpsertMediaReview -->
|
||||||
|
|
||||||
|
# Upsert Media Review
|
||||||
|
|
||||||
|
Sets (or updates) the user's personal review for a media item. **An empty /
|
||||||
|
whitespace review deletes the stored review instead** and returns
|
||||||
|
`{"review": null}`.
|
||||||
|
|
||||||
|
`PUT /api/media-items/:id/review` is the same handler and behaves identically.
|
||||||
|
|
||||||
|
## Example request
|
||||||
|
|
||||||
|
```json example-request
|
||||||
|
{
|
||||||
|
"review": "A slow start, but the second act earns it."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example response
|
||||||
|
|
||||||
|
```json example-response
|
||||||
|
{
|
||||||
|
"id": "220e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"media_item_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"user_id": "92413c3a-e1b8-4fff-b3f9-95c74478d19a",
|
||||||
|
"review": "A slow start, but the second act earns it.",
|
||||||
|
"created_at": "2026-10-24T13:57:01Z",
|
||||||
|
"updated_at": "2026-10-24T13:57:01Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Errors
|
||||||
|
|
||||||
|
| Status | Body | When |
|
||||||
|
| ------ | ---- | ---- |
|
||||||
|
| 400 | `{"error": "invalid request"}` / `{"error": "invalid media item id"}` | malformed |
|
||||||
|
| 500 | `{"error": "..."}` | unexpected server error |
|
||||||
@@ -231,9 +231,15 @@ func validateRequestExample(t *testing.T, doc *APIDoc, schema Schema) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Staleness guard: required fields must be present in the example.
|
// Staleness guard: required fields must be present in the example.
|
||||||
for _, field := range requiredJSONFields(reflect.TypeOf(schema.Request)) {
|
rt := reflect.TypeOf(schema.Request)
|
||||||
if !jsonHasKey(doc.ExampleRequest, field) {
|
for rt != nil && rt.Kind() == reflect.Ptr {
|
||||||
t.Errorf("%s: example-request missing required field %q", doc.RelPath, field)
|
rt = rt.Elem()
|
||||||
|
}
|
||||||
|
if rt != nil && rt.Kind() == reflect.Struct {
|
||||||
|
for _, field := range requiredJSONFields(rt) {
|
||||||
|
if !jsonHasKey(doc.ExampleRequest, field) {
|
||||||
|
t.Errorf("%s: example-request missing required field %q", doc.RelPath, field)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -283,7 +289,7 @@ func validateResponseExample(t *testing.T, doc *APIDoc, schema Schema) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// requiredJSONFields lists json field names of struct type whose validate tag
|
// requiredJSONFields lists json field names of struct type whose validate tag
|
||||||
// marks them required.
|
// marks them required. Callers must pass a struct kind.
|
||||||
func requiredJSONFields(rt reflect.Type) []string {
|
func requiredJSONFields(rt reflect.Type) []string {
|
||||||
if rt.Kind() == reflect.Ptr {
|
if rt.Kind() == reflect.Ptr {
|
||||||
rt = rt.Elem()
|
rt = rt.Elem()
|
||||||
|
|||||||
@@ -163,4 +163,48 @@ var Schemas = map[string]Schema{
|
|||||||
"message": ""
|
"message": ""
|
||||||
}`),
|
}`),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// --- media-items batch 3a: reader-adjacent CRUD ---
|
||||||
|
"GET /api/media-items/:id/bookmarks": {
|
||||||
|
Response: []database.MediaBookmarks{},
|
||||||
|
},
|
||||||
|
"POST /api/media-items/:id/bookmarks": {
|
||||||
|
Request: handlers.CreateMediaBookmarkRequest{},
|
||||||
|
Response: database.MediaBookmarks{},
|
||||||
|
},
|
||||||
|
"PUT /api/media-items/:id/bookmarks/:bookmarkId": {
|
||||||
|
Request: handlers.UpdateMediaBookmarkRequest{},
|
||||||
|
Response: database.MediaBookmarks{},
|
||||||
|
},
|
||||||
|
"DELETE /api/media-items/:id/bookmarks/:bookmarkId": {},
|
||||||
|
// Review: the upsert binds an inline anonymous struct {review}; pinned
|
||||||
|
// by RawMessage. Empty review string = delete, returns {"review": null}.
|
||||||
|
"POST /api/media-items/:id/review": {
|
||||||
|
Request: json.RawMessage(`{"review": ""}`),
|
||||||
|
Response: database.MediaReviews{},
|
||||||
|
},
|
||||||
|
"PUT /api/media-items/:id/review": {
|
||||||
|
Request: json.RawMessage(`{"review": ""}`),
|
||||||
|
Response: database.MediaReviews{},
|
||||||
|
},
|
||||||
|
"GET /api/media-items/:id/review": {
|
||||||
|
Response: database.MediaReviews{},
|
||||||
|
},
|
||||||
|
"DELETE /api/media-items/:id/review": {
|
||||||
|
Response: json.RawMessage(`{"message": ""}`),
|
||||||
|
},
|
||||||
|
"GET /api/media-items/:id/notes/:noteId": {
|
||||||
|
Response: database.MediaNotes{},
|
||||||
|
},
|
||||||
|
"GET /api/media-items/:id/highlights/:highlightId": {
|
||||||
|
Response: database.MediaHighlights{},
|
||||||
|
},
|
||||||
|
"POST /api/media-items/:id/annotations/:annotationId/restore": {
|
||||||
|
Request: nil, // kind via ?annotation_type= or optional body
|
||||||
|
Response: json.RawMessage(`{"restored": false}`),
|
||||||
|
},
|
||||||
|
"DELETE /api/media-items/:id/annotations/:annotationId": {
|
||||||
|
Request: nil, // kind via required ?annotation_type=
|
||||||
|
Response: json.RawMessage(`{"purged": false}`),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user