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:
John O'Keefe committed 2026-10-24 15:03:36 -04:00
1 parent 85cdaf7196
commit ed0ce5d077
13 files changed
+545 -4

No files matched your search

+10 -4
View File
@@ -231,9 +231,15 @@ func validateRequestExample(t *testing.T, doc *APIDoc, schema Schema) {
}
// Staleness guard: required fields must be present in the example.
for _, field := range requiredJSONFields(reflect.TypeOf(schema.Request)) {
if !jsonHasKey(doc.ExampleRequest, field) {
t.Errorf("%s: example-request missing required field %q", doc.RelPath, field)
rt := reflect.TypeOf(schema.Request)
for rt != nil && rt.Kind() == reflect.Ptr {
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
// marks them required.
// marks them required. Callers must pass a struct kind.
func requiredJSONFields(rt reflect.Type) []string {
if rt.Kind() == reflect.Ptr {
rt = rt.Elem()
+44
View File
@@ -163,4 +163,48 @@ var Schemas = map[string]Schema{
"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}`),
},
}