fix(bruno): convert drifted body keys to body.data — CLI scenario runs now carry bodies
Follow-up to the sweep route audit, correcting its diagnosis: the CLI
DOES read .yml bodies — via OpenCollection's `body.data` string form
(the NewDevDBSetup script always worked because it used it). The
failing scenario POSTs used `body.json` (structured) / `body.jsonBody`
(string) — keys the CLI silently ignores, sending Content-Length: 0
with form-urlencoded content-type, so every server bind failed.
- 81 bodies converted to data-form (json maps serialized; jsonBody
strings kept verbatim, {{vars}} intact).
- 6 pre-existing TRUNCATED payloads repaired: the original generator
cut bodies at the first {{var}} (every SHA-256-identified scenario
plus Sync Progress/Sync Bookmarks shipped mid-JSON and 400'd from
day one). Rewritten whole, faithful to each file's documented
purpose; book_sha256/book_uuid added to the environment.
- environments: opds_base_url correction from the sweep retained.
Verified live against the rebuilt dev container: the full koreader
scenario tree runs green through `bru run` — 14/14 (202s across
immediate/checkpoint/SHA-256 progress, 200s bookmarks, highlights,
notes; SHA-256 book identification exercised against a real
file_sha256).
This commit is contained in:
1 parent
63a20dcc95
commit
09b6693505
83 files changed
+386
-2769
No files matched your search
@@ -2,37 +2,36 @@ info:
|
||||
name: SetBaseUrl
|
||||
type: http
|
||||
seq: 3
|
||||
|
||||
http:
|
||||
method: PUT
|
||||
url: '{{base_url}}/api/system/config'
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: |-
|
||||
{
|
||||
"base_url": "http://localhost:8765"
|
||||
}
|
||||
data: "{\n \"base_url\": \"http://localhost:8765\"\n}"
|
||||
headers:
|
||||
- key: Authorization
|
||||
value: Bearer {{token}}
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
|
||||
settings:
|
||||
encodeUrl: true
|
||||
timeout: 0
|
||||
followRedirects: true
|
||||
maxRedirects: 5
|
||||
docs: '## Set Base URL
|
||||
|
||||
docs: |-
|
||||
## Set Base URL
|
||||
|
||||
Configures the server's base_url during initial dev database setup.
|
||||
Configures the server''s base_url during initial dev database setup.
|
||||
|
||||
|
||||
Must be run after RegisterUser (which provides the auth token) and before
|
||||
|
||||
any library/device creation (which require setup to be complete).
|
||||
|
||||
|
||||
**Method:** PUT
|
||||
|
||||
**Endpoint:** /api/system/config
|
||||
**Auth:** Bearer token (from RegisterUser)
|
||||
|
||||
**Auth:** Bearer token (from RegisterUser)'
|
||||
@@ -8,43 +8,8 @@ http:
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"media_item_ids\": [\n \"{{bookId1"
|
||||
data: "{\n \"media_item_ids\": [\n \"{{bookId1"
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
|
||||
docs: |-
|
||||
## Bulk Delete Media Items
|
||||
|
||||
Deletes multiple media items in a single request.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/media-items/bulk-delete
|
||||
|
||||
**Authentication:** Required (Bearer token)
|
||||
|
||||
**Request Body:**
|
||||
- `media_item_ids` (array of strings): Array of media item UUIDs to delete
|
||||
|
||||
**Response:**
|
||||
- `results` (array): Results for each deletion attempt
|
||||
- `total` (number): Total number of media items processed
|
||||
- `deleted` (number): Number of successfully deleted media items
|
||||
- `failed` (number): Number of failed deletions
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success (with partial results if some failed)
|
||||
- 400: Invalid request data
|
||||
- 401: Unauthorized
|
||||
- 403: Forbidden
|
||||
- 500: Internal server error
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"media_item_ids": [
|
||||
"uuid-1",
|
||||
"uuid-2",
|
||||
"uuid-3"
|
||||
]
|
||||
docs: "## Bulk Delete Media Items\n\n Deletes multiple media items in a single request.\n\n **Method:** POST\n\n **Endpoint:** /api/media-items/bulk-delete\n\n **Authentication:** Required (Bearer token)\n\n **Request Body:**\n - `media_item_ids` (array of strings): Array of media item UUIDs to delete\n\n **Response:**\n - `results` (array): Results for each deletion attempt\n - `total` (number): Total number of media items processed\n - `deleted` (number): Number of successfully deleted media items\n - `failed` (number): Number of failed deletions\n\n **Status Codes:**\n - 200: Success (with partial results if some failed)\n - 400: Invalid request data\n - 401: Unauthorized\n - 403: Forbidden\n - 500: Internal server error\n\n **Example:**\n ```json\n {\n \"media_item_ids\": [\n \"uuid-1\",\n \"uuid-2\",\n \"uuid-3\"\n ]"
|
||||
@@ -8,48 +8,9 @@ http:
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"media_item_updates\": [\n {\n \"media_item_id\"\
|
||||
: \"{{bookId1"
|
||||
data: "{\n \"media_item_updates\": [\n {\n \"media_item_id\": \"{{bookId1"
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
|
||||
docs: |-
|
||||
## Bulk Update Media Items
|
||||
|
||||
Updates multiple media items in a single request with different fields for each item.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/media-items/bulk-update
|
||||
|
||||
**Authentication:** Required (Bearer token)
|
||||
|
||||
**Request Body:**
|
||||
- `media_item_updates` (array): Array of update objects
|
||||
- `media_item_id` (string): Media item UUID to update
|
||||
- `updates` (object): Fields to update (can include title, author, genre, tags, contributors, etc.)
|
||||
|
||||
**Response:**
|
||||
- `results` (array): Results for each update attempt
|
||||
- `total` (number): Total number of media items processed
|
||||
- `updated` (number): Number of successfully updated media items
|
||||
- `failed` (number): Number of failed updates
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success (with partial results if some failed)
|
||||
- 400: Invalid request data
|
||||
- 401: Unauthorized
|
||||
- 403: Forbidden
|
||||
- 500: Internal server error
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"media_item_updates": [
|
||||
{
|
||||
"media_item_id": "uuid-1",
|
||||
"updates": {
|
||||
"title": "New Title",
|
||||
"genre": "Fiction",
|
||||
"tags": ["fiction", "adventure"]
|
||||
docs: "## Bulk Update Media Items\n\n Updates multiple media items in a single request with different fields for each item.\n\n **Method:** POST\n\n **Endpoint:** /api/media-items/bulk-update\n\n **Authentication:** Required (Bearer token)\n\n **Request Body:**\n - `media_item_updates` (array): Array of update objects\n - `media_item_id` (string): Media item UUID to update\n - `updates` (object): Fields to update (can include title, author, genre, tags, contributors, etc.)\n\n **Response:**\n - `results` (array): Results for each update attempt\n - `total` (number): Total number of media items processed\n - `updated` (number): Number of successfully updated media items\n - `failed` (number): Number of failed updates\n\n **Status Codes:**\n - 200: Success (with partial results if some failed)\n - 400: Invalid request data\n - 401: Unauthorized\n - 403: Forbidden\n - 500: Internal server error\n\n **Example:**\n ```json\n {\n \"media_item_updates\": [\n {\n\
|
||||
\ \"media_item_id\": \"uuid-1\",\n \"updates\": {\n \"title\": \"New Title\",\n \"genre\": \"Fiction\",\n \"tags\": [\"fiction\", \"adventure\"]"
|
||||
@@ -8,31 +8,5 @@ http:
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"book_ids\": [\n \"{{book_id_1}}"
|
||||
|
||||
docs: |-
|
||||
## Add Books to Collection
|
||||
|
||||
Adds multiple books to an existing collection.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/collections/{collection_id}/books
|
||||
|
||||
**Authentication:** Required (Bearer token)
|
||||
|
||||
**Path Parameters:**
|
||||
- `collection_id` (string): Collection ID
|
||||
|
||||
**Request Body:**
|
||||
- `book_ids` (array of strings, required): List of book IDs to add
|
||||
|
||||
**Response:**
|
||||
- `message` (string): Success message
|
||||
- `added_count` (integer): Number of books added
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 400: Invalid request
|
||||
- 401: Unauthorized
|
||||
- 404: Collection not found
|
||||
data: "{\n \"book_ids\": [\n \"{{book_id_1}}"
|
||||
docs: "## Add Books to Collection\n\n Adds multiple books to an existing collection.\n \n **Method:** POST\n \n **Endpoint:** /api/collections/{collection_id}/books\n \n **Authentication:** Required (Bearer token)\n \n **Path Parameters:**\n - `collection_id` (string): Collection ID\n \n **Request Body:**\n - `book_ids` (array of strings, required): List of book IDs to add\n \n **Response:**\n - `message` (string): Success message\n - `added_count` (integer): Number of books added\n \n **Status Codes:**\n - 200: Success\n - 400: Invalid request\n - 401: Unauthorized\n - 404: Collection not found"
|
||||
@@ -8,40 +8,6 @@ http:
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"name\": \"Science Fiction\",\n \"description\": \"My favorite\
|
||||
\ sci-fi books\",\n \"color\": \"#ff0000\",\n \"icon\": \"\U0001F680\"\
|
||||
,\n \"auto_assign_rules\": [\n {\n \"id\": \"rule-1\",\n \
|
||||
\ \"field\": \"genre\",\n \"operator\": \"equals\",\n \"value\"\
|
||||
: \"Science Fiction\",\n \"priority\": 8"
|
||||
|
||||
docs: |-
|
||||
## Create Collection
|
||||
|
||||
Creates a new collection for organizing books.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/collections
|
||||
|
||||
**Authentication:** Required (Bearer token)
|
||||
|
||||
**Request Body:**
|
||||
- `name` (string, required): Collection name (max 100 chars)
|
||||
- `description` (string, optional): Collection description (max 500 chars)
|
||||
- `color` (string, optional): Hex color code (default #3498db)
|
||||
- `icon` (string, optional): Emoji icon (default 📚)
|
||||
- `auto_assign_rules` (array, optional): Auto-assignment rules
|
||||
- `id` (string): Rule ID
|
||||
- `field` (string): Field to match (genre, author, series, etc.)
|
||||
- `operator` (string): Comparison operator (equals, contains, startsWith)
|
||||
- `value` (string): Value to match
|
||||
- `priority` (integer): Rule priority (1-10)
|
||||
|
||||
**Response:**
|
||||
- Collection object with generated ID
|
||||
|
||||
**Status Codes:**
|
||||
- 201: Created
|
||||
- 400: Invalid request
|
||||
- 401: Unauthorized
|
||||
- 409: Collection name already exists
|
||||
data: "{\n \"name\": \"Science Fiction\",\n \"description\": \"My favorite sci-fi books\",\n \"color\": \"#ff0000\",\n \"icon\": \"\U0001F680\",\n \"auto_assign_rules\": [\n {\n \"id\": \"rule-1\",\n \"field\": \"genre\",\n \"operator\": \"equals\",\n \"value\": \"Science Fiction\",\n \"priority\": 8"
|
||||
docs: "## Create Collection\n\n Creates a new collection for organizing books.\n \n **Method:** POST\n \n **Endpoint:** /api/collections\n \n **Authentication:** Required (Bearer token)\n \n **Request Body:**\n - `name` (string, required): Collection name (max 100 chars)\n - `description` (string, optional): Collection description (max 500 chars)\n - `color` (string, optional): Hex color code (default #3498db)\n - `icon` (string, optional): Emoji icon (default \U0001F4DA)\n - `auto_assign_rules` (array, optional): Auto-assignment rules\n - `id` (string): Rule ID\n - `field` (string): Field to match (genre, author, series, etc.)\n - `operator` (string): Comparison operator (equals, contains, startsWith)\n - `value` (string): Value to match\n - `priority` (integer): Rule priority (1-10)\n \n **Response:**\n - Collection object with generated ID\n \n **Status Codes:**\n - 201: Created\n - 400: Invalid request\n - 401: Unauthorized\n - 409: Collection name\
|
||||
\ already exists"
|
||||
@@ -8,32 +8,5 @@ http:
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"collection_id\": \"{{collection_id}}"
|
||||
|
||||
docs: |-
|
||||
## Create Device Mapping
|
||||
|
||||
Maps a collection to a device shelf for synchronization.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/devices/{device_id}/collections
|
||||
|
||||
**Authentication:** Required (Bearer token)
|
||||
|
||||
**Path Parameters:**
|
||||
- `device_id` (string): Device ID
|
||||
|
||||
**Request Body:**
|
||||
- `collection_id` (string, required): Collection ID to map
|
||||
- `device_shelf_name` (string, optional): Custom shelf name on device
|
||||
- `sync_direction` (string, optional): sync direction (book_to_device, device_to_book, bidirectional)
|
||||
|
||||
**Response:**
|
||||
- Mapping object with generated ID
|
||||
|
||||
**Status Codes:**
|
||||
- 201: Created
|
||||
- 400: Invalid request
|
||||
- 401: Unauthorized
|
||||
- 404: Device or collection not found
|
||||
data: "{\n \"collection_id\": \"{{collection_id}}"
|
||||
docs: "## Create Device Mapping\n\n Maps a collection to a device shelf for synchronization.\n \n **Method:** POST\n \n **Endpoint:** /api/devices/{device_id}/collections\n \n **Authentication:** Required (Bearer token)\n \n **Path Parameters:**\n - `device_id` (string): Device ID\n \n **Request Body:**\n - `collection_id` (string, required): Collection ID to map\n - `device_shelf_name` (string, optional): Custom shelf name on device\n - `sync_direction` (string, optional): sync direction (book_to_device, device_to_book, bidirectional)\n \n **Response:**\n - Mapping object with generated ID\n \n **Status Codes:**\n - 201: Created\n - 400: Invalid request\n - 401: Unauthorized\n - 404: Device or collection not found"
|
||||
@@ -8,39 +8,5 @@ http:
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"name\": \"Sci-Fi Favorites\",\n \"description\": \"Updated\
|
||||
\ description\",\n \"color\": \"#00ff00\",\n \"icon\": \"⭐\",\n \"\
|
||||
auto_assign_rules\": [\n {\n \"id\": \"rule-2\",\n \"field\"\
|
||||
: \"series\",\n \"operator\": \"equals\",\n \"value\": \"Foundation\"\
|
||||
,\n \"priority\": 9"
|
||||
|
||||
docs: |-
|
||||
## Update Collection
|
||||
|
||||
Updates an existing collection's properties.
|
||||
|
||||
**Method:** PUT
|
||||
|
||||
**Endpoint:** /api/collections/{collection_id}
|
||||
|
||||
**Authentication:** Required (Bearer token)
|
||||
|
||||
**Path Parameters:**
|
||||
- `collection_id` (string): Collection ID
|
||||
|
||||
**Request Body:**
|
||||
- `name` (string, optional): New collection name
|
||||
- `description` (string, optional): New description
|
||||
- `color` (string, optional): New hex color code
|
||||
- `icon` (string, optional): New emoji icon
|
||||
- `auto_assign_rules` (array, optional): Updated auto-assignment rules
|
||||
|
||||
**Response:**
|
||||
- Updated collection object
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 400: Invalid request
|
||||
- 401: Unauthorized
|
||||
- 404: Collection not found
|
||||
- 409: Collection name already exists
|
||||
data: "{\n \"name\": \"Sci-Fi Favorites\",\n \"description\": \"Updated description\",\n \"color\": \"#00ff00\",\n \"icon\": \"⭐\",\n \"auto_assign_rules\": [\n {\n \"id\": \"rule-2\",\n \"field\": \"series\",\n \"operator\": \"equals\",\n \"value\": \"Foundation\",\n \"priority\": 9"
|
||||
docs: "## Update Collection\n\n Updates an existing collection's properties.\n \n **Method:** PUT\n \n **Endpoint:** /api/collections/{collection_id}\n \n **Authentication:** Required (Bearer token)\n \n **Path Parameters:**\n - `collection_id` (string): Collection ID\n \n **Request Body:**\n - `name` (string, optional): New collection name\n - `description` (string, optional): New description\n - `color` (string, optional): New hex color code\n - `icon` (string, optional): New emoji icon\n - `auto_assign_rules` (array, optional): Updated auto-assignment rules\n \n **Response:**\n - Updated collection object\n \n **Status Codes:**\n - 200: Success\n - 400: Invalid request\n - 401: Unauthorized\n - 404: Collection not found\n - 409: Collection name already exists"
|
||||
@@ -8,33 +8,5 @@ http:
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"device_shelf_name\": \"Science Fiction\",\n \"sync_direction\"\
|
||||
: \"book_to_device\""
|
||||
|
||||
docs: |-
|
||||
## Update Device Mapping
|
||||
|
||||
Updates settings for a collection-to-device mapping.
|
||||
|
||||
**Method:** PUT
|
||||
|
||||
**Endpoint:** /api/devices/{device_id}/collections/{mapping_id}
|
||||
|
||||
**Authentication:** Required (Bearer token)
|
||||
|
||||
**Path Parameters:**
|
||||
- `device_id` (string): Device ID
|
||||
- `mapping_id` (string): Mapping ID
|
||||
|
||||
**Request Body:**
|
||||
- `device_shelf_name` (string, optional): Custom shelf name on device
|
||||
- `sync_direction` (string, optional): Sync direction (book_to_device, device_to_book, bidirectional)
|
||||
|
||||
**Response:**
|
||||
- Updated mapping object
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 400: Invalid request
|
||||
- 401: Unauthorized
|
||||
- 404: Device or mapping not found
|
||||
data: "{\n \"device_shelf_name\": \"Science Fiction\",\n \"sync_direction\": \"book_to_device\""
|
||||
docs: "## Update Device Mapping\n\n Updates settings for a collection-to-device mapping.\n \n **Method:** PUT\n \n **Endpoint:** /api/devices/{device_id}/collections/{mapping_id}\n \n **Authentication:** Required (Bearer token)\n \n **Path Parameters:**\n - `device_id` (string): Device ID\n - `mapping_id` (string): Mapping ID\n \n **Request Body:**\n - `device_shelf_name` (string, optional): Custom shelf name on device\n - `sync_direction` (string, optional): Sync direction (book_to_device, device_to_book, bidirectional)\n \n **Response:**\n - Updated mapping object\n \n **Status Codes:**\n - 200: Success\n - 400: Invalid request\n - 401: Unauthorized\n - 404: Device or mapping not found"
|
||||
@@ -8,7 +8,7 @@ http:
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"library_id\": \"{{library_id}}\",\n \"name\": \"My Favorites\",\n \"icon\": \"⭐\",\n \"description\": \"My favorite books\",\n \"show_on_dashboard\": true,\n \"auto_assign_rules\": \"\",\n \"manual_book_ids\": [\"{{media_item_id}}\"],\n \"match_type\": \"all\"\n}"
|
||||
data: "{\n \"library_id\": \"{{library_id}}\",\n \"name\": \"My Favorites\",\n \"icon\": \"⭐\",\n \"description\": \"My favorite books\",\n \"show_on_dashboard\": true,\n \"auto_assign_rules\": \"\",\n \"manual_book_ids\": [\"{{media_item_id}}\"],\n \"match_type\": \"all\"\n}"
|
||||
runtime:
|
||||
scripts:
|
||||
- type: after-response
|
||||
|
||||
@@ -8,7 +8,7 @@ http:
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"library_id\": \"{{library_id}}\",\n \"icon\": \"\U0001F4DA\"\n}"
|
||||
data: "{\n \"library_id\": \"{{library_id}}\",\n \"icon\": \"\U0001F4DA\"\n}"
|
||||
runtime:
|
||||
scripts:
|
||||
- type: after-response
|
||||
|
||||
@@ -8,7 +8,7 @@ http:
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"library_id\": \"{{library_id}}\",\n \"name\": \"Science Fiction Books\",\n \"icon\": \"\U0001F680\",\n \"description\": \"All my sci-fi books\",\n \"show_on_dashboard\": true,\n \"auto_assign_rules\": \"{\\\"rules\\\":[{\\\"id\\\":\\\"rule1\\\",\\\"field\\\":\\\"genre\\\",\\\"operator\\\":\\\"equals\\\",\\\"value\\\":\\\"Sci-Fi\\\",\\\"priority\\\":1}]}\",\n \"manual_book_ids\": [],\n \"match_type\": \"all\"\n}"
|
||||
data: "{\n \"library_id\": \"{{library_id}}\",\n \"name\": \"Science Fiction Books\",\n \"icon\": \"\U0001F680\",\n \"description\": \"All my sci-fi books\",\n \"show_on_dashboard\": true,\n \"auto_assign_rules\": \"{\\\"rules\\\":[{\\\"id\\\":\\\"rule1\\\",\\\"field\\\":\\\"genre\\\",\\\"operator\\\":\\\"equals\\\",\\\"value\\\":\\\"Sci-Fi\\\",\\\"priority\\\":1}]}\",\n \"manual_book_ids\": [],\n \"match_type\": \"all\"\n}"
|
||||
runtime:
|
||||
scripts:
|
||||
- type: after-response
|
||||
|
||||
@@ -8,7 +8,7 @@ http:
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"library_id\": \"{{library_id}}\",\n \"rules\": [\n{\n \"id\": \"rule1\",\n \"field\": \"genre\",\n\"operator\": \"equals\",\n \"value\": \"Fiction\",\n\"priority\": 1\n }\n ],\n \"manual_book_ids\": [],\n \"limit\": 20\n}"
|
||||
data: "{\n \"library_id\": \"{{library_id}}\",\n \"rules\": [\n{\n \"id\": \"rule1\",\n \"field\": \"genre\",\n\"operator\": \"equals\",\n \"value\": \"Fiction\",\n\"priority\": 1\n }\n ],\n \"manual_book_ids\": [],\n \"limit\": 20\n}"
|
||||
runtime:
|
||||
scripts:
|
||||
- type: after-response
|
||||
|
||||
@@ -8,46 +8,11 @@ http:
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"operations\": [\n {\n \"collection_id\": \"{{collectionId1"
|
||||
data: "{\n \"operations\": [\n {\n \"collection_id\": \"{{collectionId1"
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
- key: Authorization
|
||||
value: Bearer {{authToken
|
||||
|
||||
docs: |-
|
||||
## Bulk Add Books to Collections
|
||||
|
||||
Adds multiple books to multiple collections in a single request. Each operation specifies a collection and a list of books to add.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/collections/bulk-add-books
|
||||
|
||||
**Authentication:** Bearer token
|
||||
|
||||
**Request Body:**
|
||||
- `operations` (array): Array of collection-book operations
|
||||
- `collection_id` (string): Collection UUID
|
||||
- `book_ids` (array): Array of book UUIDs to add to the collection
|
||||
|
||||
**Response:**
|
||||
- `results` (array): Results for each operation
|
||||
- `total` (number): Total number of operations
|
||||
- `success` (number): Number of successful operations
|
||||
- `failed` (number): Number of failed operations
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success (with partial results if some failed)
|
||||
- 400: Invalid request data
|
||||
- 401: Unauthorized
|
||||
- 403: Forbidden
|
||||
- 500: Internal server error
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"operations": [
|
||||
{
|
||||
"collection_id": "collection-uuid-1",
|
||||
"book_ids": ["book-1", "book-2"]
|
||||
docs: "## Bulk Add Books to Collections\n\n Adds multiple books to multiple collections in a single request. Each operation specifies a collection and a list of books to add.\n\n **Method:** POST\n\n **Endpoint:** /api/collections/bulk-add-books\n\n **Authentication:** Bearer token\n\n **Request Body:**\n - `operations` (array): Array of collection-book operations\n - `collection_id` (string): Collection UUID\n - `book_ids` (array): Array of book UUIDs to add to the collection\n\n **Response:**\n - `results` (array): Results for each operation\n - `total` (number): Total number of operations\n - `success` (number): Number of successful operations\n - `failed` (number): Number of failed operations\n\n **Status Codes:**\n - 200: Success (with partial results if some failed)\n - 400: Invalid request data\n - 401: Unauthorized\n - 403: Forbidden\n - 500: Internal server error\n\n **Example:**\n ```json\n {\n \"operations\": [\n {\n \"collection_id\":\
|
||||
\ \"collection-uuid-1\",\n \"book_ids\": [\"book-1\", \"book-2\"]"
|
||||
@@ -8,16 +8,8 @@ http:
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"book_ids\": [\n \"{{bookId1"
|
||||
data: "{\n \"book_ids\": [\n \"{{bookId1"
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
|
||||
docs: |-
|
||||
## Bulk Remove Books from Collection
|
||||
|
||||
Removes multiple books from a collection in a single request.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/collections/{collection_id
|
||||
docs: "## Bulk Remove Books from Collection\n\n Removes multiple books from a collection in a single request.\n\n **Method:** POST\n\n **Endpoint:** /api/collections/{collection_id"
|
||||
@@ -8,20 +8,10 @@ http:
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"book_ids\": []"
|
||||
data: "{\n \"book_ids\": []"
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
- key: Authorization
|
||||
value: Bearer {{authToken
|
||||
|
||||
docs: |-
|
||||
## Bulk Remove Books - Empty List Validation
|
||||
|
||||
Tests validation behavior when providing an empty book_ids array.
|
||||
|
||||
**Expected Result:** 400 Bad Request
|
||||
|
||||
**Validation Rule:** book_ids array must contain at least one book UUID.
|
||||
|
||||
**Purpose:** Ensures the API properly validates input and rejects empty removal requests.
|
||||
docs: "## Bulk Remove Books - Empty List Validation\n\n Tests validation behavior when providing an empty book_ids array.\n\n **Expected Result:** 400 Bad Request\n\n **Validation Rule:** book_ids array must contain at least one book UUID.\n\n **Purpose:** Ensures the API properly validates input and rejects empty removal requests."
|
||||
@@ -8,24 +8,10 @@ http:
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"book_ids\": [\n \"{{bookId1"
|
||||
data: "{\n \"book_ids\": [\n \"{{bookId1"
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
- key: Authorization
|
||||
value: Bearer {{authToken
|
||||
|
||||
docs: |-
|
||||
## Bulk Remove Books - Invalid IDs
|
||||
|
||||
Tests behavior when the book_ids array contains invalid UUID formats or non-existent books.
|
||||
|
||||
**Expected Result:** 200 OK with partial success
|
||||
|
||||
**Purpose:** Verifies that:
|
||||
- Invalid UUID formats don't crash the endpoint
|
||||
- Non-existent book IDs are handled gracefully
|
||||
- Valid IDs in the same request are still processed
|
||||
- Response includes detailed results showing which succeeded/failed
|
||||
|
||||
**Note:** The endpoint should process all valid IDs and report failures for invalid ones, allowing clients to handle partial failures appropriately.
|
||||
docs: "## Bulk Remove Books - Invalid IDs\n\n Tests behavior when the book_ids array contains invalid UUID formats or non-existent books.\n\n **Expected Result:** 200 OK with partial success\n\n **Purpose:** Verifies that:\n - Invalid UUID formats don't crash the endpoint\n - Non-existent book IDs are handled gracefully\n - Valid IDs in the same request are still processed\n - Response includes detailed results showing which succeeded/failed\n\n **Note:** The endpoint should process all valid IDs and report failures for invalid ones, allowing clients to handle partial failures appropriately."
|
||||
@@ -8,20 +8,10 @@ http:
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"book_ids\": [\n \"{{bookId1"
|
||||
data: "{\n \"book_ids\": [\n \"{{bookId1"
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
- key: Authorization
|
||||
value: Bearer {{authToken
|
||||
|
||||
docs: |-
|
||||
## Bulk Remove Books - Single Book
|
||||
|
||||
Tests that bulk remove endpoint works correctly with a single book.
|
||||
|
||||
**Expected Result:** 200 OK with removed: 1, total: 1
|
||||
|
||||
**Purpose:** Verifies the bulk remove endpoint handles single-item arrays correctly, providing flexibility for clients to use the same endpoint for both single and multiple removals.
|
||||
|
||||
**Note:** Using bulk remove for a single book is functionally equivalent to the single remove endpoint but allows for consistent error handling and response format.
|
||||
docs: "## Bulk Remove Books - Single Book\n\n Tests that bulk remove endpoint works correctly with a single book.\n\n **Expected Result:** 200 OK with removed: 1, total: 1\n\n **Purpose:** Verifies the bulk remove endpoint handles single-item arrays correctly, providing flexibility for clients to use the same endpoint for both single and multiple removals.\n\n **Note:** Using bulk remove for a single book is functionally equivalent to the single remove endpoint but allows for consistent error handling and response format."
|
||||
@@ -8,23 +8,10 @@ http:
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"rules\": [\n {\n \"field\": \"author\",\n \
|
||||
\ \"operator\": \"contains\",\n \"value\": \"Asimov\""
|
||||
data: "{\n \"rules\": [\n {\n \"field\": \"author\",\n \"operator\": \"contains\",\n \"value\": \"Asimov\""
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
- key: Authorization
|
||||
value: Bearer {{authToken
|
||||
|
||||
docs: |-
|
||||
## Test Collection Rules - Author Contains
|
||||
|
||||
Tests the "contains" operator on the author field to find books by a specific author (partial match).
|
||||
|
||||
**Example Use Case:** Finding all books by an author whose name contains "Asimov" (e.g., "Isaac Asimov").
|
||||
|
||||
**Operator:** `contains` - Matches if the field contains the specified value as a substring (case-insensitive typically).
|
||||
|
||||
**Expected Result:** Returns all books where the author field contains "Asimov".
|
||||
|
||||
**Purpose:** Demonstrates text-based partial matching for author searches, useful when you don't need the exact author name or want to find books by authors with similar names.
|
||||
docs: "## Test Collection Rules - Author Contains\n\n Tests the \"contains\" operator on the author field to find books by a specific author (partial match).\n\n **Example Use Case:** Finding all books by an author whose name contains \"Asimov\" (e.g., \"Isaac Asimov\").\n\n **Operator:** `contains` - Matches if the field contains the specified value as a substring (case-insensitive typically).\n\n **Expected Result:** Returns all books where the author field contains \"Asimov\".\n\n **Purpose:** Demonstrates text-based partial matching for author searches, useful when you don't need the exact author name or want to find books by authors with similar names."
|
||||
+2
-17
@@ -8,25 +8,10 @@ http:
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"rules\": [\n {\n \"field\": \"copyright_year\"\
|
||||
,\n \"operator\": \"greater_than\",\n \"value\": \"2000\""
|
||||
data: "{\n \"rules\": [\n {\n \"field\": \"copyright_year\",\n \"operator\": \"greater_than\",\n \"value\": \"2000\""
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
- key: Authorization
|
||||
value: Bearer {{authToken
|
||||
|
||||
docs: |-
|
||||
## Test Collection Rules - Copyright Year Greater Than
|
||||
|
||||
Tests the "greater_than" operator on the copyright_year field to find books published after a specific year.
|
||||
|
||||
**Example Use Case:** Creating a "Modern Books" collection with books published after 2000.
|
||||
|
||||
**Operator:** `greater_than` - Matches if the field value is greater than the specified value (numeric comparison).
|
||||
|
||||
**Field:** `copyright_year` - The year the book was copyrighted/published.
|
||||
|
||||
**Expected Result:** Returns all books with copyright_year greater than 2000 (i.e., published in 2001 or later).
|
||||
|
||||
**Purpose:** Demonstrates numeric comparison operators for creating date-based collections, useful for organizing books by publication era.
|
||||
docs: "## Test Collection Rules - Copyright Year Greater Than\n\n Tests the \"greater_than\" operator on the copyright_year field to find books published after a specific year.\n\n **Example Use Case:** Creating a \"Modern Books\" collection with books published after 2000.\n\n **Operator:** `greater_than` - Matches if the field value is greater than the specified value (numeric comparison).\n\n **Field:** `copyright_year` - The year the book was copyrighted/published.\n\n **Expected Result:** Returns all books with copyright_year greater than 2000 (i.e., published in 2001 or later).\n\n **Purpose:** Demonstrates numeric comparison operators for creating date-based collections, useful for organizing books by publication era."
|
||||
@@ -8,22 +8,10 @@ http:
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"rules\": []"
|
||||
data: "{\n \"rules\": []"
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
- key: Authorization
|
||||
value: Bearer {{authToken
|
||||
|
||||
docs: |-
|
||||
## Test Collection Rules - Empty Rules Array
|
||||
|
||||
Tests validation behavior when providing an empty rules array.
|
||||
|
||||
**Expected Result:** 400 Bad Request
|
||||
|
||||
**Validation Rule:** rules array must contain at least one rule object.
|
||||
|
||||
**Purpose:** Ensures the API properly validates input and rejects empty rule sets, preventing accidental queries that would return all books or cause performance issues.
|
||||
|
||||
**Use Case:** Client-side validation should prevent sending empty rules, but the API should also validate to catch malformed requests.
|
||||
docs: "## Test Collection Rules - Empty Rules Array\n\n Tests validation behavior when providing an empty rules array.\n\n **Expected Result:** 400 Bad Request\n\n **Validation Rule:** rules array must contain at least one rule object.\n\n **Purpose:** Ensures the API properly validates input and rejects empty rule sets, preventing accidental queries that would return all books or cause performance issues.\n\n **Use Case:** Client-side validation should prevent sending empty rules, but the API should also validate to catch malformed requests."
|
||||
@@ -8,27 +8,10 @@ http:
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"rules\": [\n {\n \"field\": \"genre\",\n \
|
||||
\ \"operator\": \"equals\",\n \"value\": \"NonExistentGenre123456\""
|
||||
data: "{\n \"rules\": [\n {\n \"field\": \"genre\",\n \"operator\": \"equals\",\n \"value\": \"NonExistentGenre123456\""
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
- key: Authorization
|
||||
value: Bearer {{authToken
|
||||
|
||||
docs: |-
|
||||
## Test Collection Rules - No Matches
|
||||
|
||||
Tests behavior when collection rules don't match any books in the library.
|
||||
|
||||
**Example Use Case:** Validating that a new genre name doesn't exist before creating a collection for it, or testing edge cases.
|
||||
|
||||
**Expected Result:** 200 OK with empty matches array and total: 0
|
||||
|
||||
**Purpose:** Verifies that the API handles zero-match scenarios gracefully:
|
||||
- Returns 200 (success) not 404
|
||||
- Returns empty array, not null
|
||||
- Returns total: 0 for clarity
|
||||
- No errors thrown for no results
|
||||
|
||||
**Note:** An empty result set is a valid response and doesn't indicate an error. This allows users to test rules confidently before creating collections.
|
||||
docs: "## Test Collection Rules - No Matches\n\n Tests behavior when collection rules don't match any books in the library.\n\n **Example Use Case:** Validating that a new genre name doesn't exist before creating a collection for it, or testing edge cases.\n\n **Expected Result:** 200 OK with empty matches array and total: 0\n\n **Purpose:** Verifies that the API handles zero-match scenarios gracefully:\n - Returns 200 (success) not 404\n - Returns empty array, not null\n - Returns total: 0 for clarity\n - No errors thrown for no results\n\n **Note:** An empty result set is a valid response and doesn't indicate an error. This allows users to test rules confidently before creating collections."
|
||||
@@ -8,60 +8,11 @@ http:
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"rules\": [\n {\n \"field\": \"genre\",\n \
|
||||
\ \"operator\": \"equals\",\n \"value\": \"Science Fiction\""
|
||||
data: "{\n \"rules\": [\n {\n \"field\": \"genre\",\n \"operator\": \"equals\",\n \"value\": \"Science Fiction\""
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
- key: Authorization
|
||||
value: Bearer {{authToken
|
||||
|
||||
docs: |-
|
||||
## Test Collection Rules
|
||||
|
||||
Tests collection rules against the library to see which books match, without creating a collection. Useful for previewing what books would be included in a collection with specific rules.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/collections/test-rules
|
||||
|
||||
**Authentication:** Bearer token
|
||||
|
||||
**Request Body:**
|
||||
- `rules` (array): Array of rule objects to test
|
||||
- `field` (string): Field to test (genre, author, copyright_year, tags, etc.)
|
||||
- `operator` (string): Comparison operator
|
||||
- `equals`: Exact match
|
||||
- `contains`: Contains substring (for text fields)
|
||||
- `greater_than`: Greater than (for numeric fields)
|
||||
- `less_than`: Less than (for numeric fields)
|
||||
- `not_equals`: Not equal to
|
||||
- `starts_with`: Starts with
|
||||
- `ends_with`: Ends with
|
||||
- `is_empty`: Field is empty or null
|
||||
- `is_not_empty`: Field is not empty and not null
|
||||
- `value` (string): Value to compare against (not required for is_empty/is_not_empty)
|
||||
|
||||
**Response:**
|
||||
- `matches` (array): Array of matching books
|
||||
- `id` (string): Book UUID
|
||||
- `title` (string): Book title
|
||||
- `author` (string): Book author
|
||||
- `genre` (string): Book genre
|
||||
- Additional book metadata
|
||||
- `total` (number): Total number of matching books
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success - returns matching books
|
||||
- 400: Invalid request (empty rules array, invalid field/operator)
|
||||
- 401: Unauthorized
|
||||
- 500: Internal server error
|
||||
|
||||
**Example Request:**
|
||||
```json
|
||||
{
|
||||
"rules": [
|
||||
{
|
||||
"field": "genre",
|
||||
"operator": "equals",
|
||||
"value": "Science Fiction"
|
||||
docs: "## Test Collection Rules\n\n Tests collection rules against the library to see which books match, without creating a collection. Useful for previewing what books would be included in a collection with specific rules.\n\n **Method:** POST\n\n **Endpoint:** /api/collections/test-rules\n\n **Authentication:** Bearer token\n\n **Request Body:**\n - `rules` (array): Array of rule objects to test\n - `field` (string): Field to test (genre, author, copyright_year, tags, etc.)\n - `operator` (string): Comparison operator\n - `equals`: Exact match\n - `contains`: Contains substring (for text fields)\n - `greater_than`: Greater than (for numeric fields)\n - `less_than`: Less than (for numeric fields)\n - `not_equals`: Not equal to\n - `starts_with`: Starts with\n - `ends_with`: Ends with\n - `is_empty`: Field is empty or null\n - `is_not_empty`: Field is not empty and not null\n - `value` (string): Value to compare against (not required\
|
||||
\ for is_empty/is_not_empty)\n\n **Response:**\n - `matches` (array): Array of matching books\n - `id` (string): Book UUID\n - `title` (string): Book title\n - `author` (string): Book author\n - `genre` (string): Book genre\n - Additional book metadata\n - `total` (number): Total number of matching books\n\n **Status Codes:**\n - 200: Success - returns matching books\n - 400: Invalid request (empty rules array, invalid field/operator)\n - 401: Unauthorized\n - 500: Internal server error\n\n **Example Request:**\n ```json\n {\n \"rules\": [\n {\n \"field\": \"genre\",\n \"operator\": \"equals\",\n \"value\": \"Science Fiction\""
|
||||
@@ -8,18 +8,8 @@ http:
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"winner\": \"koreader\",\n \"manual_data\": null,\n \"\
|
||||
apply_to_all_future_conflicts\": false,\n \"reason\": \"User chose more recent\
|
||||
\ progress\""
|
||||
data: "{\n \"winner\": \"koreader\",\n \"manual_data\": null,\n \"apply_to_all_future_conflicts\": false,\n \"reason\": \"User chose more recent progress\""
|
||||
headers:
|
||||
- key: Authorization
|
||||
value: Bearer {{token
|
||||
|
||||
docs: |-
|
||||
## Resolve Conflict
|
||||
|
||||
Resolves a sync conflict by choosing which source to use for the conflicting data.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/conflicts/{conflict_id
|
||||
docs: "## Resolve Conflict\n\n Resolves a sync conflict by choosing which source to use for the conflicting data.\n\n **Method:** POST\n\n **Endpoint:** /api/conflicts/{conflict_id"
|
||||
@@ -8,42 +8,10 @@ http:
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"conflict_ids\": [\n \"{{conflictId1"
|
||||
data: "{\n \"conflict_ids\": [\n \"{{conflictId1"
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
- key: Authorization
|
||||
value: Bearer {{authToken
|
||||
|
||||
docs: |-
|
||||
## Bulk Dismiss Conflicts
|
||||
|
||||
Dismisses multiple sync conflicts without resolving them. This removes them from the conflict list while leaving the data unchanged.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/conflicts/bulk-dismiss
|
||||
|
||||
**Authentication:** Bearer token
|
||||
|
||||
**Request Body:**
|
||||
- `conflict_ids` (array): Array of conflict UUIDs to dismiss
|
||||
|
||||
**Response:**
|
||||
- `results` (array): Results for each dismissal
|
||||
- `total` (number): Total number of conflicts processed
|
||||
- `success` (number): Number of successfully dismissed conflicts
|
||||
- `failed` (number): Number of failed dismissals
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 400: Invalid request data
|
||||
- 401: Unauthorized
|
||||
- 403: Forbidden
|
||||
- 404: One or more conflicts not found
|
||||
- 500: Internal server error
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"conflict_ids": ["uuid-1", "uuid-2"]
|
||||
docs: "## Bulk Dismiss Conflicts\n\n Dismisses multiple sync conflicts without resolving them. This removes them from the conflict list while leaving the data unchanged.\n\n **Method:** POST\n\n **Endpoint:** /api/conflicts/bulk-dismiss\n\n **Authentication:** Bearer token\n\n **Request Body:**\n - `conflict_ids` (array): Array of conflict UUIDs to dismiss\n\n **Response:**\n - `results` (array): Results for each dismissal\n - `total` (number): Total number of conflicts processed\n - `success` (number): Number of successfully dismissed conflicts\n - `failed` (number): Number of failed dismissals\n\n **Status Codes:**\n - 200: Success\n - 400: Invalid request data\n - 401: Unauthorized\n - 403: Forbidden\n - 404: One or more conflicts not found\n - 500: Internal server error\n\n **Example:**\n ```json\n {\n \"conflict_ids\": [\"uuid-1\", \"uuid-2\"]"
|
||||
@@ -8,48 +8,11 @@ http:
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"conflict_ids\": [\n \"{{conflictId1"
|
||||
data: "{\n \"conflict_ids\": [\n \"{{conflictId1"
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
- key: Authorization
|
||||
value: Bearer {{authToken
|
||||
|
||||
docs: |-
|
||||
## Bulk Resolve Conflicts
|
||||
|
||||
Resolves multiple sync conflicts in a single request using a specified resolution strategy.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/conflicts/bulk-resolve
|
||||
|
||||
**Authentication:** Bearer token
|
||||
|
||||
**Request Body:**
|
||||
- `conflict_ids` (array): Array of conflict UUIDs to resolve
|
||||
- `strategy` (string): Resolution strategy
|
||||
- `most_recent`: Use the most recently updated progress
|
||||
- `highest_progress`: Use the reading progress with the highest percent read
|
||||
- `server`: Always prefer server-side data
|
||||
- `device`: Always prefer device-side data
|
||||
|
||||
**Response:**
|
||||
- `results` (array): Results for each conflict resolution
|
||||
- `total` (number): Total number of conflicts processed
|
||||
- `success` (number): Number of successfully resolved conflicts
|
||||
- `failed` (number): Number of failed resolutions
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success (with partial results if some failed)
|
||||
- 400: Invalid request data
|
||||
- 401: Unauthorized
|
||||
- 403: Forbidden
|
||||
- 404: One or more conflicts not found
|
||||
- 500: Internal server error
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"conflict_ids": ["uuid-1", "uuid-2", "uuid-3"],
|
||||
"strategy": "most_recent"
|
||||
docs: "## Bulk Resolve Conflicts\n\n Resolves multiple sync conflicts in a single request using a specified resolution strategy.\n\n **Method:** POST\n\n **Endpoint:** /api/conflicts/bulk-resolve\n\n **Authentication:** Bearer token\n\n **Request Body:**\n - `conflict_ids` (array): Array of conflict UUIDs to resolve\n - `strategy` (string): Resolution strategy\n - `most_recent`: Use the most recently updated progress\n - `highest_progress`: Use the reading progress with the highest percent read\n - `server`: Always prefer server-side data\n - `device`: Always prefer device-side data\n\n **Response:**\n - `results` (array): Results for each conflict resolution\n - `total` (number): Total number of conflicts processed\n - `success` (number): Number of successfully resolved conflicts\n - `failed` (number): Number of failed resolutions\n\n **Status Codes:**\n - 200: Success (with partial results if some failed)\n - 400: Invalid request data\n - 401: Unauthorized\n\
|
||||
\ - 403: Forbidden\n - 404: One or more conflicts not found\n - 500: Internal server error\n\n **Example:**\n ```json\n {\n \"conflict_ids\": [\"uuid-1\", \"uuid-2\", \"uuid-3\"],\n \"strategy\": \"most_recent\""
|
||||
@@ -8,39 +8,11 @@ http:
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"conflict_ids\": [\n \"{{conflictId1"
|
||||
data: "{\n \"conflict_ids\": [\n \"{{conflictId1"
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
- key: Authorization
|
||||
value: Bearer {{authToken
|
||||
|
||||
docs: |-
|
||||
## Bulk Resolve with Highest Progress Strategy
|
||||
|
||||
Resolves multiple sync conflicts using the "highest_progress" strategy, which keeps the reading progress with the highest percentage read.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/conflicts/bulk-resolve
|
||||
|
||||
**Authentication:** Bearer token
|
||||
|
||||
**Request Body:**
|
||||
- `conflict_ids` (array): Array of conflict UUIDs to resolve
|
||||
- `strategy` (string): Must be "highest_progress"
|
||||
|
||||
**Response:**
|
||||
- `results` (array): Results for each conflict resolution
|
||||
- `total` (number): Total number of conflicts processed
|
||||
- `success` (number): Number of successfully resolved conflicts
|
||||
- `failed` (number): Number of failed resolutions
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 400: Invalid request data
|
||||
- 401: Unauthorized
|
||||
- 403: Forbidden
|
||||
- 500: Internal server error
|
||||
|
||||
**Note:** The highest progress strategy is ideal when you want to preserve the most reading progress across devices. Use this when you've been reading on multiple devices and want to keep the furthest position.
|
||||
docs: "## Bulk Resolve with Highest Progress Strategy\n\n Resolves multiple sync conflicts using the \"highest_progress\" strategy, which keeps the reading progress with the highest percentage read.\n\n **Method:** POST\n\n **Endpoint:** /api/conflicts/bulk-resolve\n\n **Authentication:** Bearer token\n\n **Request Body:**\n - `conflict_ids` (array): Array of conflict UUIDs to resolve\n - `strategy` (string): Must be \"highest_progress\"\n\n **Response:**\n - `results` (array): Results for each conflict resolution\n - `total` (number): Total number of conflicts processed\n - `success` (number): Number of successfully resolved conflicts\n - `failed` (number): Number of failed resolutions\n\n **Status Codes:**\n - 200: Success\n - 400: Invalid request data\n - 401: Unauthorized\n - 403: Forbidden\n - 500: Internal server error\n\n **Note:** The highest progress strategy is ideal when you want to preserve the most reading progress across devices. Use this when you've been\
|
||||
\ reading on multiple devices and want to keep the furthest position."
|
||||
@@ -8,40 +8,9 @@ http:
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"collection_name\": \"continue-reading\"\n}"
|
||||
data: "{\n \"collection_name\": \"continue-reading\"\n}"
|
||||
runtime:
|
||||
scripts:
|
||||
- type: tests
|
||||
code: "test(\"status must be 200 with valid collection\", function() {\n expect(res.status).to.eql(200);\n\
|
||||
});\n\ntest(\"response has success message\", function() {\n expect(res.body.message).to.exist;\n\
|
||||
});"
|
||||
|
||||
docs: |-
|
||||
Restore a system collection to its default state.
|
||||
|
||||
**Endpoint**: POST /api/dashboard/restore-system-collection
|
||||
**Auth**: Required (Bearer token)
|
||||
|
||||
## Request Body
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| collection_name | string | Yes | Name of system collection to restore |
|
||||
|
||||
Valid collection names:
|
||||
- `continue-reading`: Books you're currently reading (0 < progress < 1)
|
||||
- `recently-added`: Newly added items to this library
|
||||
- `recently-read`: Books you've finished (progress >= 1)
|
||||
- `not-started`: Books you haven't read yet (progress = 0 or no record)
|
||||
|
||||
## Example Request
|
||||
|
||||
```json
|
||||
{
|
||||
"collection_name": "continue-reading"
|
||||
}
|
||||
```
|
||||
|
||||
## Response
|
||||
|
||||
Returns success message on restore.
|
||||
code: "test(\"status must be 200 with valid collection\", function() {\n expect(res.status).to.eql(200);\n});\n\ntest(\"response has success message\", function() {\n expect(res.body.message).to.exist;\n});"
|
||||
docs: "Restore a system collection to its default state.\n\n**Endpoint**: POST /api/dashboard/restore-system-collection\n**Auth**: Required (Bearer token)\n\n## Request Body\n\n| Field | Type | Required | Description |\n|-------|------|----------|-------------|\n| collection_name | string | Yes | Name of system collection to restore |\n\nValid collection names:\n- `continue-reading`: Books you're currently reading (0 < progress < 1)\n- `recently-added`: Newly added items to this library\n- `recently-read`: Books you've finished (progress >= 1)\n- `not-started`: Books you haven't read yet (progress = 0 or no record)\n\n## Example Request\n\n```json\n{\n \"collection_name\": \"continue-reading\"\n}\n```\n\n## Response\n\nReturns success message on restore."
|
||||
@@ -8,41 +8,9 @@ http:
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"library_id\": \"{{library_id}}\",\n \"hidden_collections\"\
|
||||
: [\"not-started\"],\n \"collection_order\": [\"recently-added\", \"continue-reading\"\
|
||||
, \"recently-read\"],\n \"items_per_section\": 20\n}"
|
||||
data: "{\n \"library_id\": \"{{library_id}}\",\n \"hidden_collections\": [\"not-started\"],\n \"collection_order\": [\"recently-added\", \"continue-reading\", \"recently-read\"],\n \"items_per_section\": 20\n}"
|
||||
runtime:
|
||||
scripts:
|
||||
- type: tests
|
||||
code: "test(\"status must be 200 with valid request\", function() {\n expect(res.status).to.eql(200);\n\
|
||||
});"
|
||||
|
||||
docs: |-
|
||||
Update dashboard preferences for the authenticated user.
|
||||
|
||||
**Endpoint**: PUT /api/dashboard/preferences
|
||||
**Auth**: Required (Bearer token)
|
||||
|
||||
## Request Body
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| library_id | string | Yes | Library UUID |
|
||||
| hidden_collections | array | No | Collection names to hide from dashboard |
|
||||
| collection_order | array | No | Collection names in custom display order |
|
||||
| items_per_section | int | No | Items to show per section (default: 20) |
|
||||
|
||||
## Example Request
|
||||
|
||||
```json
|
||||
{
|
||||
"library_id": "cc23c3a7-f8fb-451a-a78d-2a16df1b725a",
|
||||
"hidden_collections": ["not-started"],
|
||||
"collection_order": ["recently-added", "continue-reading", "recently-read"],
|
||||
"items_per_section": 20
|
||||
}
|
||||
```
|
||||
|
||||
## Response
|
||||
|
||||
Returns updated preferences object on success.
|
||||
code: "test(\"status must be 200 with valid request\", function() {\n expect(res.status).to.eql(200);\n});"
|
||||
docs: "Update dashboard preferences for the authenticated user.\n\n**Endpoint**: PUT /api/dashboard/preferences\n**Auth**: Required (Bearer token)\n\n## Request Body\n\n| Field | Type | Required | Description |\n|-------|------|----------|-------------|\n| library_id | string | Yes | Library UUID |\n| hidden_collections | array | No | Collection names to hide from dashboard |\n| collection_order | array | No | Collection names in custom display order |\n| items_per_section | int | No | Items to show per section (default: 20) |\n\n## Example Request\n\n```json\n{\n \"library_id\": \"cc23c3a7-f8fb-451a-a78d-2a16df1b725a\",\n \"hidden_collections\": [\"not-started\"],\n \"collection_order\": [\"recently-added\", \"continue-reading\", \"recently-read\"],\n \"items_per_section\": 20\n}\n```\n\n## Response\n\nReturns updated preferences object on success."
|
||||
@@ -2,95 +2,16 @@ info:
|
||||
name: Get Analytics Tests
|
||||
type: http
|
||||
seq: 7
|
||||
|
||||
http:
|
||||
method: POST
|
||||
url: '{{base_url}}/api/sync/kobo/{{kobo_device_token}}/v1/analytics/gettests'
|
||||
auth: inherit
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
body:
|
||||
type: json
|
||||
json:
|
||||
platform: android
|
||||
firmware_version: 4.30.19023
|
||||
|
||||
docs: |-
|
||||
## Get Kobo Analytics Tests
|
||||
|
||||
Retrieves A/B testing configuration and feature flags for Kobo device.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/sync/kobo/v1/analytics/gettests
|
||||
|
||||
**Authentication:** Bearer token
|
||||
|
||||
**Request Body:**
|
||||
- `platform` (string): Device platform
|
||||
- `android`: Kobo Android app
|
||||
- `kobo`: Native Kobo firmware
|
||||
- `firmware_version` (string): Firmware version (e.g., "4.30.19023")
|
||||
- `device_model` (string, optional): Device model identifier
|
||||
- `locale` (string, optional): Device locale (e.g., "en_US")
|
||||
|
||||
**Response:**
|
||||
- `tests` (array): Active A/B tests
|
||||
- `test_name` (string): Test identifier
|
||||
- `variant` (string): Assigned variant (A, B, C, etc.)
|
||||
- `enabled` (boolean): Whether test is active
|
||||
- `parameters` (object): Test-specific parameters
|
||||
- `features` (object): Feature flags
|
||||
- `feature_name` (boolean/string): Feature state
|
||||
- `configuration` (object): Device configuration
|
||||
- `sync_interval_minutes` (integer): Recommended sync frequency
|
||||
- `batch_size` (integer): Max items per batch sync
|
||||
- `timeout_seconds` (integer): Request timeout
|
||||
- `version` (string): Configuration version
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 401: Unauthorized
|
||||
- 400: Invalid request parameters
|
||||
|
||||
**Analytics Testing Purpose:**
|
||||
- Kobo uses A/B testing for UX features
|
||||
- Feature flags for gradual rollout
|
||||
- Performance monitoring configuration
|
||||
- Sync behavior optimization
|
||||
- Device-specific tuning
|
||||
|
||||
**Common Tests:**
|
||||
- Sync frequency optimization
|
||||
- UI/UX variations
|
||||
- Network usage patterns
|
||||
- Battery life improvements
|
||||
- Feature set variations by model
|
||||
|
||||
**Feature Flags:**
|
||||
- New sync features
|
||||
- Beta functionality
|
||||
- Platform-specific capabilities
|
||||
- Experimental features
|
||||
|
||||
**Configuration Parameters:**
|
||||
- Optimal sync intervals for device
|
||||
- Batch size limits based on device capabilities
|
||||
- Timeout values for network conditions
|
||||
- Retry logic configuration
|
||||
- Cache policy settings
|
||||
|
||||
**Usage:**
|
||||
- Called during device initialization
|
||||
- Refreshed daily or weekly
|
||||
- Cached on device
|
||||
- Affects sync behavior
|
||||
- Can be overridden by server
|
||||
|
||||
**Use Cases:**
|
||||
- Device initialization
|
||||
- Feature rollout testing
|
||||
- Performance optimization
|
||||
- UX experiment participation
|
||||
- Configuration management
|
||||
data: "{\n \"platform\": \"android\",\n \"firmware_version\": \"4.30.19023\"\n}"
|
||||
docs: "## Get Kobo Analytics Tests\n\nRetrieves A/B testing configuration and feature flags for Kobo device.\n\n**Method:** POST\n\n**Endpoint:** /api/sync/kobo/v1/analytics/gettests\n\n**Authentication:** Bearer token\n\n**Request Body:**\n- `platform` (string): Device platform\n - `android`: Kobo Android app\n - `kobo`: Native Kobo firmware\n- `firmware_version` (string): Firmware version (e.g., \"4.30.19023\")\n- `device_model` (string, optional): Device model identifier\n- `locale` (string, optional): Device locale (e.g., \"en_US\")\n\n**Response:**\n- `tests` (array): Active A/B tests\n - `test_name` (string): Test identifier\n - `variant` (string): Assigned variant (A, B, C, etc.)\n - `enabled` (boolean): Whether test is active\n - `parameters` (object): Test-specific parameters\n- `features` (object): Feature flags\n - `feature_name` (boolean/string): Feature state\n- `configuration` (object): Device configuration\n - `sync_interval_minutes` (integer): Recommended sync frequency\n\
|
||||
\ - `batch_size` (integer): Max items per batch sync\n - `timeout_seconds` (integer): Request timeout\n- `version` (string): Configuration version\n\n**Status Codes:**\n- 200: Success\n- 401: Unauthorized\n- 400: Invalid request parameters\n\n**Analytics Testing Purpose:**\n- Kobo uses A/B testing for UX features\n- Feature flags for gradual rollout\n- Performance monitoring configuration\n- Sync behavior optimization\n- Device-specific tuning\n\n**Common Tests:**\n- Sync frequency optimization\n- UI/UX variations\n- Network usage patterns\n- Battery life improvements\n- Feature set variations by model\n\n**Feature Flags:**\n- New sync features\n- Beta functionality\n- Platform-specific capabilities\n- Experimental features\n\n**Configuration Parameters:**\n- Optimal sync intervals for device\n- Batch size limits based on device capabilities\n- Timeout values for network conditions\n- Retry logic configuration\n- Cache policy settings\n\n**Usage:**\n- Called during device initialization\n\
|
||||
- Refreshed daily or weekly\n- Cached on device\n- Affects sync behavior\n- Can be overridden by server\n\n**Use Cases:**\n- Device initialization\n- Feature rollout testing\n- Performance optimization\n- UX experiment participation\n- Configuration management"
|
||||
@@ -8,38 +8,10 @@ http:
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"confidence_threshold\": 0.8,\n \"limit\": 50"
|
||||
data: "{\n \"confidence_threshold\": 0.8,\n \"limit\": 50"
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
- key: Authorization
|
||||
value: Bearer {{authToken
|
||||
|
||||
docs: |-
|
||||
## Auto-Link Unlinked Books
|
||||
|
||||
Automatically links unlinked books to media items based on title and author matching with a configurable confidence threshold.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /sync/auto-link-books
|
||||
|
||||
**Authentication:** Bearer token
|
||||
|
||||
**Request Body:**
|
||||
- `confidence_threshold` (number, optional): Minimum confidence score for auto-linking (0-1, default: 0.8)
|
||||
- `limit` (number, optional): Maximum number of books to auto-link (default: 50)
|
||||
|
||||
**Response:**
|
||||
- `results` (array): Results for each auto-link attempt
|
||||
- `total` (number): Total number of books processed
|
||||
- `success` (number): Number of successful links
|
||||
- `failed` (number): Number of failed links
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 400: Invalid request data
|
||||
- 401: Unauthorized
|
||||
- 500: Internal server error
|
||||
|
||||
**Note:** Higher confidence thresholds produce fewer but more accurate matches. Consider the tradeoff between automation and accuracy.
|
||||
docs: "## Auto-Link Unlinked Books\n\n Automatically links unlinked books to media items based on title and author matching with a configurable confidence threshold.\n\n **Method:** POST\n\n **Endpoint:** /sync/auto-link-books\n\n **Authentication:** Bearer token\n\n **Request Body:**\n - `confidence_threshold` (number, optional): Minimum confidence score for auto-linking (0-1, default: 0.8)\n - `limit` (number, optional): Maximum number of books to auto-link (default: 50)\n\n **Response:**\n - `results` (array): Results for each auto-link attempt\n - `total` (number): Total number of books processed\n - `success` (number): Number of successful links\n - `failed` (number): Number of failed links\n\n **Status Codes:**\n - 200: Success\n - 400: Invalid request data\n - 401: Unauthorized\n - 500: Internal server error\n\n **Note:** Higher confidence thresholds produce fewer but more accurate matches. Consider the tradeoff between automation and accuracy."
|
||||
@@ -8,40 +8,10 @@ http:
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"links\": [\n {\n \"unlinked_book_id\": \"{{unlinkedBookId1"
|
||||
data: "{\n \"links\": [\n {\n \"unlinked_book_id\": \"{{unlinkedBookId1"
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
- key: Authorization
|
||||
value: Bearer {{authToken
|
||||
|
||||
docs: |-
|
||||
## Bulk Link Unlinked Books
|
||||
|
||||
Links multiple unlinked books to media items in a single request.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /sync/bulk-link-books
|
||||
|
||||
**Authentication:** Bearer token
|
||||
|
||||
**Request Body:**
|
||||
- `links` (array): Array of link objects
|
||||
- `unlinked_book_id` (string): Unlinked book UUID
|
||||
- `media_item_id` (string): Media item UUID to link to
|
||||
- `confidence_score` (number): Match confidence (0-1)
|
||||
|
||||
**Response:**
|
||||
- `results` (array): Results for each link attempt
|
||||
- `total` (number): Total number of links processed
|
||||
- `success` (number): Number of successful links
|
||||
- `failed` (number): Number of failed links
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 400: Invalid request data
|
||||
- 401: Unauthorized
|
||||
- 500: Internal server error
|
||||
|
||||
**Note:** Use this endpoint after reviewing suggestions from the Get Unlinked Book Suggestions endpoint.
|
||||
docs: "## Bulk Link Unlinked Books\n\n Links multiple unlinked books to media items in a single request.\n\n **Method:** POST\n\n **Endpoint:** /sync/bulk-link-books\n\n **Authentication:** Bearer token\n\n **Request Body:**\n - `links` (array): Array of link objects\n - `unlinked_book_id` (string): Unlinked book UUID\n - `media_item_id` (string): Media item UUID to link to\n - `confidence_score` (number): Match confidence (0-1)\n\n **Response:**\n - `results` (array): Results for each link attempt\n - `total` (number): Total number of links processed\n - `success` (number): Number of successful links\n - `failed` (number): Number of failed links\n\n **Status Codes:**\n - 200: Success\n - 400: Invalid request data\n - 401: Unauthorized\n - 500: Internal server error\n\n **Note:** Use this endpoint after reviewing suggestions from the Get Unlinked Book Suggestions endpoint."
|
||||
@@ -8,46 +8,9 @@ http:
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "[\n {\n \"ContentId\": \"{{bookUUID"
|
||||
data: "[\n {\n \"ContentId\": \"{{bookUUID"
|
||||
headers:
|
||||
- key: Authorization
|
||||
value: Bearer {{koboToken
|
||||
|
||||
docs: |-
|
||||
## Sync from Bookhoard to Kobo
|
||||
|
||||
Server-initiated sync pushing progress, bookmarks, and highlights from Bookhoard to Kobo device. Two-way sync endpoint.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/sync/kobo/sync-from-server
|
||||
|
||||
**Authentication:** Bearer token with Kobo device identification
|
||||
|
||||
**Headers:**
|
||||
- `x-kobo-device` (string): JSON string containing Kobo device info
|
||||
- `DeviceId`: Kobo device ID
|
||||
- `Model`: Kobo device model
|
||||
- `SerialNumber`: Kobo device serial number
|
||||
|
||||
**Request Body:** Array of sync data objects
|
||||
- `ContentId` (string): Book UUID
|
||||
- `PercentRead` (number): Reading progress percentage (0-100)
|
||||
- `LastModified` (string): ISO 8601 timestamp
|
||||
- `Bookmarks` (array, optional): Array of bookmark objects
|
||||
- `BookmarkId`: Unique bookmark ID
|
||||
- `ContentId`: Book UUID
|
||||
- `BookmarkText`: Bookmark text/note
|
||||
- `BookmarkType`: Type (bookmark, annotation, etc.)
|
||||
- `BookmarkTitle`: Bookmark title
|
||||
- `Highlights` (array, optional): Array of highlight objects (same structure as bookmarks)
|
||||
|
||||
**Response:**
|
||||
- Sync result confirmation
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 401: Unauthorized
|
||||
- 500: Internal server error
|
||||
|
||||
**Note:** Allows Bookhoard server to push updates to Kobo device, including reading progress, bookmarks, and highlights.
|
||||
docs: "## Sync from Bookhoard to Kobo\n\n Server-initiated sync pushing progress, bookmarks, and highlights from Bookhoard to Kobo device. Two-way sync endpoint.\n\n **Method:** POST\n\n **Endpoint:** /api/sync/kobo/sync-from-server\n\n **Authentication:** Bearer token with Kobo device identification\n\n **Headers:**\n - `x-kobo-device` (string): JSON string containing Kobo device info\n - `DeviceId`: Kobo device ID\n - `Model`: Kobo device model\n - `SerialNumber`: Kobo device serial number\n\n **Request Body:** Array of sync data objects\n - `ContentId` (string): Book UUID\n - `PercentRead` (number): Reading progress percentage (0-100)\n - `LastModified` (string): ISO 8601 timestamp\n - `Bookmarks` (array, optional): Array of bookmark objects\n - `BookmarkId`: Unique bookmark ID\n - `ContentId`: Book UUID\n - `BookmarkText`: Bookmark text/note\n - `BookmarkType`: Type (bookmark, annotation, etc.)\n - `BookmarkTitle`: Bookmark title\n - `Highlights` (array,\
|
||||
\ optional): Array of highlight objects (same structure as bookmarks)\n\n **Response:**\n - Sync result confirmation\n\n **Status Codes:**\n - 200: Success\n - 401: Unauthorized\n - 500: Internal server error\n\n **Note:** Allows Bookhoard server to push updates to Kobo device, including reading progress, bookmarks, and highlights."
|
||||
@@ -2,105 +2,16 @@ info:
|
||||
name: Sync Books from Server to Kobo
|
||||
type: http
|
||||
seq: 9
|
||||
|
||||
http:
|
||||
method: POST
|
||||
url: '{{base_url}}/api/sync/kobo/{{kobo_device_token}}/sync-from-server'
|
||||
auth: inherit
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
body:
|
||||
type: json
|
||||
json:
|
||||
force_sync: true
|
||||
books:
|
||||
- book-uuid-1
|
||||
- book-uuid-2
|
||||
|
||||
docs: |-
|
||||
## Sync Books from Server to Kobo
|
||||
|
||||
Pulls reading progress, annotations, and bookmarks from server to Kobo device.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/sync/kobo/sync-from-server
|
||||
|
||||
**Authentication:** Bearer token
|
||||
|
||||
**Request Body:**
|
||||
- `force_sync` (boolean): Force full sync (ignore last sync time)
|
||||
- `books` (array, optional): List of book UUIDs to sync
|
||||
- If empty, syncs all books with server data
|
||||
- If specified, only syncs listed books
|
||||
- `sync_options` (object, optional):
|
||||
- `since_date` (string): ISO 8601 timestamp for incremental sync
|
||||
- `include_annotations` (boolean): Include highlights/notes
|
||||
- `include_progress` (boolean): Include reading progress
|
||||
- `conflict_resolution` (string): `most_recent`, `server_wins`, `client_wins`
|
||||
|
||||
**Response:**
|
||||
- `synced` (integer): Number of books synced
|
||||
- `skipped` (integer): Books with no server changes
|
||||
- `failed` (integer): Books that failed to sync
|
||||
- `results` (array): Per-book sync results
|
||||
- `book_id` (string): Book UUID
|
||||
- `status` (string): `synced`, `skipped`, `failed`
|
||||
- `progress_pulled` (boolean): Whether progress was downloaded
|
||||
- `annotations_pulled` (integer): Number of annotations downloaded
|
||||
- `error` (string, optional): Error message if failed
|
||||
- `server_timestamp` (string): Server timestamp of sync
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Sync completed
|
||||
- 207: Multi-status (some succeeded, some failed)
|
||||
- 401: Unauthorized
|
||||
- 400: Invalid request
|
||||
|
||||
**Sync Direction:**
|
||||
- **Server → Device** (this endpoint)
|
||||
- Device → Server: Use `/sync/kobo/markup` endpoint
|
||||
- Bidirectional sync achieved by calling both
|
||||
|
||||
**Pull Sync Use Cases:**
|
||||
- New device setup (download all progress)
|
||||
- Device replacement (restore from server)
|
||||
- Multi-device sync (pull changes from other devices)
|
||||
- Conflict resolution (server wins)
|
||||
- Manual "Download from server" operation
|
||||
|
||||
**Conflict Resolution:**
|
||||
- `most_recent`: Latest timestamp wins (default)
|
||||
- `server_wins`: Server data always used
|
||||
- `client_wins`: Device data preserved
|
||||
- Applied per-book, per-item
|
||||
|
||||
**Sync Optimization:**
|
||||
- Incremental sync by default (since last sync)
|
||||
- Force sync does full comparison
|
||||
- Book-level batching (10 books per batch)
|
||||
- Delta transfer (only changed items)
|
||||
- Compression for large annotation sets
|
||||
|
||||
**Kobo Device Behavior:**
|
||||
- Device updates local database
|
||||
- Progress reflected in reading view
|
||||
- Annotations appear in Notebook
|
||||
- Bookmarks updated in navigation
|
||||
- Conflict warnings shown to user
|
||||
- Sync progress displayed on screen
|
||||
|
||||
**Performance:**
|
||||
- Small sync (1-10 books): 2-5 seconds
|
||||
- Medium sync (10-50 books): 5-15 seconds
|
||||
- Large sync (50-200 books): 15-45 seconds
|
||||
- Timeout: 120 seconds
|
||||
|
||||
**Use Cases:**
|
||||
- Initial device sync
|
||||
- After firmware update
|
||||
- From another device's changes
|
||||
- Manual sync request
|
||||
- Conflict recovery
|
||||
- Data restoration
|
||||
data: "{\n \"force_sync\": true,\n \"books\": [\n \"book-uuid-1\",\n \"book-uuid-2\"\n ]\n}"
|
||||
docs: "## Sync Books from Server to Kobo\n\nPulls reading progress, annotations, and bookmarks from server to Kobo device.\n\n**Method:** POST\n\n**Endpoint:** /api/sync/kobo/sync-from-server\n\n**Authentication:** Bearer token\n\n**Request Body:**\n- `force_sync` (boolean): Force full sync (ignore last sync time)\n- `books` (array, optional): List of book UUIDs to sync\n - If empty, syncs all books with server data\n - If specified, only syncs listed books\n- `sync_options` (object, optional):\n - `since_date` (string): ISO 8601 timestamp for incremental sync\n - `include_annotations` (boolean): Include highlights/notes\n - `include_progress` (boolean): Include reading progress\n - `conflict_resolution` (string): `most_recent`, `server_wins`, `client_wins`\n\n**Response:**\n- `synced` (integer): Number of books synced\n- `skipped` (integer): Books with no server changes\n- `failed` (integer): Books that failed to sync\n- `results` (array): Per-book sync results\n - `book_id` (string):\
|
||||
\ Book UUID\n - `status` (string): `synced`, `skipped`, `failed`\n - `progress_pulled` (boolean): Whether progress was downloaded\n - `annotations_pulled` (integer): Number of annotations downloaded\n - `error` (string, optional): Error message if failed\n- `server_timestamp` (string): Server timestamp of sync\n\n**Status Codes:**\n- 200: Sync completed\n- 207: Multi-status (some succeeded, some failed)\n- 401: Unauthorized\n- 400: Invalid request\n\n**Sync Direction:**\n- **Server → Device** (this endpoint)\n- Device → Server: Use `/sync/kobo/markup` endpoint\n- Bidirectional sync achieved by calling both\n\n**Pull Sync Use Cases:**\n- New device setup (download all progress)\n- Device replacement (restore from server)\n- Multi-device sync (pull changes from other devices)\n- Conflict resolution (server wins)\n- Manual \"Download from server\" operation\n\n**Conflict Resolution:**\n- `most_recent`: Latest timestamp wins (default)\n- `server_wins`: Server data always used\n- `client_wins`:\
|
||||
\ Device data preserved\n- Applied per-book, per-item\n\n**Sync Optimization:**\n- Incremental sync by default (since last sync)\n- Force sync does full comparison\n- Book-level batching (10 books per batch)\n- Delta transfer (only changed items)\n- Compression for large annotation sets\n\n**Kobo Device Behavior:**\n- Device updates local database\n- Progress reflected in reading view\n- Annotations appear in Notebook\n- Bookmarks updated in navigation\n- Conflict warnings shown to user\n- Sync progress displayed on screen\n\n**Performance:**\n- Small sync (1-10 books): 2-5 seconds\n- Medium sync (10-50 books): 5-15 seconds\n- Large sync (50-200 books): 15-45 seconds\n- Timeout: 120 seconds\n\n**Use Cases:**\n- Initial device sync\n- After firmware update\n- From another device's changes\n- Manual sync request\n- Conflict recovery\n- Data restoration"
|
||||
@@ -2,90 +2,17 @@ info:
|
||||
name: Sync Multiple Books Progress
|
||||
type: http
|
||||
seq: 3
|
||||
|
||||
http:
|
||||
method: POST
|
||||
url: '{{base_url}}/api/sync/kobo/{{kobo_device_token}}/markup'
|
||||
auth: inherit
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
- key: x-kobo-device
|
||||
value: '{"DeviceId":"{{kobo_device_id}}","Model":"Kobo Aura"}'
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
- key: x-kobo-device
|
||||
value: '{"DeviceId":"{{kobo_device_id}}","Model":"Kobo Aura"}'
|
||||
body:
|
||||
type: json
|
||||
json:
|
||||
ReadingSync:
|
||||
- ContentId: book-1-uuid
|
||||
PercentRead: 25.0
|
||||
EntitlementId: entitlement-1
|
||||
RemainingTimeMinutes: 240
|
||||
FirstReadTime: '2026-01-25T10:00:00Z'
|
||||
LastModified: '2026-01-30T18:00:00Z'
|
||||
- ContentId: book-2-uuid
|
||||
PercentRead: 78.5
|
||||
EntitlementId: entitlement-2
|
||||
RemainingTimeMinutes: 45
|
||||
FirstReadTime: '2026-01-25T14:00:00Z'
|
||||
LastModified: '2026-01-30T20:00:00Z'
|
||||
BookmarkSync: []
|
||||
|
||||
docs: |-
|
||||
## Sync Multiple Books Progress
|
||||
|
||||
Synchronizes reading progress for multiple books in a single request.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/sync/kobo/markup
|
||||
|
||||
**Authentication:** Bearer token
|
||||
|
||||
**Headers:**
|
||||
- `Authorization`: Bearer {{kobo_device_token}}
|
||||
- `x-kobo-device`: Device information (Model: "Kobo Aura")
|
||||
|
||||
**Request Body:**
|
||||
- `ReadingSync` (array): Multiple progress items
|
||||
- Each item contains: ContentId, PercentRead, EntitlementId, etc.
|
||||
- `BookmarkSync` (array): Empty for progress-only batch
|
||||
|
||||
**Response:**
|
||||
- `total` (integer): Total items in request
|
||||
- `synced` (integer): Successfully synced
|
||||
- `failed` (integer): Failed items
|
||||
- `results` (array): Per-item results
|
||||
- `ContentId` (string): Book UUID
|
||||
- `status` (string): `synced`, `failed`, `skipped`
|
||||
- `error` (string, optional): Error message if failed
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Batch sync completed
|
||||
- 207: Multi-status (some succeeded, some failed)
|
||||
- 401: Unauthorized
|
||||
- 413: Payload too large (>1MB)
|
||||
|
||||
**Batch Sync Advantages:**
|
||||
- Efficient sync of entire library
|
||||
- Reduces HTTP overhead
|
||||
- Faster for devices with many books
|
||||
- Atomic operation (all or nothing by default)
|
||||
|
||||
**Kobo Batch Sync Behavior:**
|
||||
- Triggered when device connects after being offline
|
||||
- Occurs during manual "Sync now" operation
|
||||
- Limited to 100 books per request
|
||||
- Progress updates shown on device
|
||||
- Failed items retried individually
|
||||
|
||||
**Performance:**
|
||||
- Typical batch: 10-50 books in 1-3 seconds
|
||||
- Large batch: 50-100 books in 3-8 seconds
|
||||
- Timeout: 30 seconds
|
||||
- Rate limit: 10 batches per minute per device
|
||||
|
||||
**Use Cases:**
|
||||
- Initial device sync after registration
|
||||
- Catch-up sync after extended offline period
|
||||
- Library-wide progress update
|
||||
- Pre-sync before device firmware update
|
||||
data: "{\n \"ReadingSync\": [\n {\n \"ContentId\": \"book-1-uuid\",\n \"PercentRead\": 25.0,\n \"EntitlementId\": \"entitlement-1\",\n \"RemainingTimeMinutes\": 240,\n \"FirstReadTime\": \"2026-01-25T10:00:00Z\",\n \"LastModified\": \"2026-01-30T18:00:00Z\"\n },\n {\n \"ContentId\": \"book-2-uuid\",\n \"PercentRead\": 78.5,\n \"EntitlementId\": \"entitlement-2\",\n \"RemainingTimeMinutes\": 45,\n \"FirstReadTime\": \"2026-01-25T14:00:00Z\",\n \"LastModified\": \"2026-01-30T20:00:00Z\"\n }\n ],\n \"BookmarkSync\": []\n}"
|
||||
docs: "## Sync Multiple Books Progress\n\nSynchronizes reading progress for multiple books in a single request.\n\n**Method:** POST\n\n**Endpoint:** /api/sync/kobo/markup\n\n**Authentication:** Bearer token\n\n**Headers:**\n- `Authorization`: Bearer {{kobo_device_token}}\n- `x-kobo-device`: Device information (Model: \"Kobo Aura\")\n\n**Request Body:**\n- `ReadingSync` (array): Multiple progress items\n - Each item contains: ContentId, PercentRead, EntitlementId, etc.\n- `BookmarkSync` (array): Empty for progress-only batch\n\n**Response:**\n- `total` (integer): Total items in request\n- `synced` (integer): Successfully synced\n- `failed` (integer): Failed items\n- `results` (array): Per-item results\n - `ContentId` (string): Book UUID\n - `status` (string): `synced`, `failed`, `skipped`\n - `error` (string, optional): Error message if failed\n\n**Status Codes:**\n- 200: Batch sync completed\n- 207: Multi-status (some succeeded, some failed)\n- 401: Unauthorized\n- 413: Payload too large\
|
||||
\ (>1MB)\n\n**Batch Sync Advantages:**\n- Efficient sync of entire library\n- Reduces HTTP overhead\n- Faster for devices with many books\n- Atomic operation (all or nothing by default)\n\n**Kobo Batch Sync Behavior:**\n- Triggered when device connects after being offline\n- Occurs during manual \"Sync now\" operation\n- Limited to 100 books per request\n- Progress updates shown on device\n- Failed items retried individually\n\n**Performance:**\n- Typical batch: 10-50 books in 1-3 seconds\n- Large batch: 50-100 books in 3-8 seconds\n- Timeout: 30 seconds\n- Rate limit: 10 batches per minute per device\n\n**Use Cases:**\n- Initial device sync after registration\n- Catch-up sync after extended offline period\n- Library-wide progress update\n- Pre-sync before device firmware update"
|
||||
@@ -2,83 +2,17 @@ info:
|
||||
name: Sync Progress with Bookmarks
|
||||
type: http
|
||||
seq: 2
|
||||
|
||||
http:
|
||||
method: POST
|
||||
url: '{{base_url}}/api/sync/kobo/{{kobo_device_token}}/markup'
|
||||
auth: inherit
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
- key: x-kobo-device
|
||||
value: '{"DeviceId":"{{kobo_device_id}}","Model":"Kobo Clara"}'
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
- key: x-kobo-device
|
||||
value: '{"DeviceId":"{{kobo_device_id}}","Model":"Kobo Clara"}'
|
||||
body:
|
||||
type: json
|
||||
json:
|
||||
ReadingSync:
|
||||
- ContentId: book-uuid
|
||||
PercentRead: 42.3
|
||||
EntitlementId: entitlement-id
|
||||
RemainingTimeMinutes: 138
|
||||
FirstReadTime: '2026-01-25T10:00:00Z'
|
||||
LastModified: '2026-01-30T20:00:00Z'
|
||||
BookmarkSync:
|
||||
- ContentId: book-uuid
|
||||
BookmarkText: highlighted text passage
|
||||
BookmarkType: annotation
|
||||
BookmarkTitle: Chapter 3
|
||||
|
||||
docs: |-
|
||||
## Sync Progress with Bookmarks
|
||||
|
||||
Synchronizes reading progress and highlights/annotations from Kobo device.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/sync/kobo/markup
|
||||
|
||||
**Authentication:** Bearer token
|
||||
|
||||
**Headers:**
|
||||
- `Authorization`: Bearer {{kobo_device_token}}
|
||||
- `x-kobo-device`: Device information JSON
|
||||
|
||||
**Request Body:**
|
||||
- `ReadingSync` (array): Progress items (see Sync Reading Progress)
|
||||
- `BookmarkSync` (array): Highlights and annotations
|
||||
- `ContentId` (string): Book UUID
|
||||
- `BookmarkText` (string): Highlighted text or note content
|
||||
- `BookmarkType` (string): Type of bookmark
|
||||
- `annotation`: Highlighted text
|
||||
- `note`: Personal note
|
||||
- `bookmark`: Location bookmark
|
||||
- `BookmarkTitle` (string): Reference (e.g., chapter name)
|
||||
- `ChapterID` (string, optional): Chapter identifier
|
||||
- `DateCreated` (string, optional): Creation timestamp
|
||||
|
||||
**Response:**
|
||||
- `progress_synced` (integer): Progress items synced
|
||||
- `bookmarks_synced` (integer): Bookmark items synced
|
||||
- `conflicts_resolved` (integer): Number of conflicts auto-resolved
|
||||
- `timestamp` (string): Sync timestamp
|
||||
- `details` (object): Sync breakdown by type
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Successful sync
|
||||
- 401: Unauthorized
|
||||
- 400: Invalid data
|
||||
|
||||
**Kobo Highlight Features:**
|
||||
- 5 highlight colors (yellow, green, blue, pink, orange)
|
||||
- Chapter-based organization
|
||||
- Linked to reading progress
|
||||
- Appears in Kobo "Notebook" view
|
||||
- Can be exported from device
|
||||
- Syncs across all user devices
|
||||
|
||||
**Annotation Sync:**
|
||||
- Highlight text preserved exactly
|
||||
- Color mapped to system colors
|
||||
- Chapter reference maintained
|
||||
- Location data converted to standard format
|
||||
- Notes attached to highlights synced separately
|
||||
data: "{\n \"ReadingSync\": [\n {\n \"ContentId\": \"book-uuid\",\n \"PercentRead\": 42.3,\n \"EntitlementId\": \"entitlement-id\",\n \"RemainingTimeMinutes\": 138,\n \"FirstReadTime\": \"2026-01-25T10:00:00Z\",\n \"LastModified\": \"2026-01-30T20:00:00Z\"\n }\n ],\n \"BookmarkSync\": [\n {\n \"ContentId\": \"book-uuid\",\n \"BookmarkText\": \"highlighted text passage\",\n \"BookmarkType\": \"annotation\",\n \"BookmarkTitle\": \"Chapter 3\"\n }\n ]\n}"
|
||||
docs: "## Sync Progress with Bookmarks\n\nSynchronizes reading progress and highlights/annotations from Kobo device.\n\n**Method:** POST\n\n**Endpoint:** /api/sync/kobo/markup\n\n**Authentication:** Bearer token\n\n**Headers:**\n- `Authorization`: Bearer {{kobo_device_token}}\n- `x-kobo-device`: Device information JSON\n\n**Request Body:**\n- `ReadingSync` (array): Progress items (see Sync Reading Progress)\n- `BookmarkSync` (array): Highlights and annotations\n - `ContentId` (string): Book UUID\n - `BookmarkText` (string): Highlighted text or note content\n - `BookmarkType` (string): Type of bookmark\n - `annotation`: Highlighted text\n - `note`: Personal note\n - `bookmark`: Location bookmark\n - `BookmarkTitle` (string): Reference (e.g., chapter name)\n - `ChapterID` (string, optional): Chapter identifier\n - `DateCreated` (string, optional): Creation timestamp\n\n**Response:**\n- `progress_synced` (integer): Progress items synced\n- `bookmarks_synced` (integer): Bookmark\
|
||||
\ items synced\n- `conflicts_resolved` (integer): Number of conflicts auto-resolved\n- `timestamp` (string): Sync timestamp\n- `details` (object): Sync breakdown by type\n\n**Status Codes:**\n- 200: Successful sync\n- 401: Unauthorized\n- 400: Invalid data\n\n**Kobo Highlight Features:**\n- 5 highlight colors (yellow, green, blue, pink, orange)\n- Chapter-based organization\n- Linked to reading progress\n- Appears in Kobo \"Notebook\" view\n- Can be exported from device\n- Syncs across all user devices\n\n**Annotation Sync:**\n- Highlight text preserved exactly\n- Color mapped to system colors\n- Chapter reference maintained\n- Location data converted to standard format\n- Notes attached to highlights synced separately"
|
||||
@@ -2,83 +2,18 @@ info:
|
||||
name: Sync Reading Progress
|
||||
type: http
|
||||
seq: 1
|
||||
|
||||
http:
|
||||
method: POST
|
||||
url: '{{base_url}}/api/sync/kobo/{{kobo_device_token}}/markup'
|
||||
auth: inherit
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
- key: x-kobo-device
|
||||
value: '{"DeviceId":"{{kobo_device_id}}","Model":"Kobo Clara"}'
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
- key: x-kobo-device
|
||||
value: '{"DeviceId":"{{kobo_device_id}}","Model":"Kobo Clara"}'
|
||||
body:
|
||||
type: json
|
||||
json:
|
||||
ReadingSync:
|
||||
- ContentId: book-uuid-here
|
||||
PercentRead: 45.6
|
||||
EntitlementId: entitlement-id-here
|
||||
RemainingTimeMinutes: 120
|
||||
FirstReadTime: '2026-01-25T10:00:00Z'
|
||||
LastModified: '2026-01-30T20:00:00Z'
|
||||
BookmarkSync: []
|
||||
|
||||
docs: |-
|
||||
## Sync Kobo Reading Progress
|
||||
|
||||
Synchronizes reading progress from a Kobo device to the server using Bearer token authentication.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/sync/kobo/markup
|
||||
|
||||
**Authentication:** Bearer token (in Authorization header)
|
||||
|
||||
**Headers:**
|
||||
- `Authorization`: Bearer {{kobo_device_token}}
|
||||
- `x-kobo-device`: JSON-encoded device info
|
||||
- `DeviceId`: Kobo device identifier
|
||||
- `Model`: Device model (e.g., "Kobo Clara", "Kobo Libra", "Kobo Aura")
|
||||
|
||||
**Request Body:**
|
||||
- `ReadingSync` (array): Reading progress items
|
||||
- `ContentId` (string): Book/Content UUID
|
||||
- `PercentRead` (number): Reading progress 0-100
|
||||
- `EntitlementId` (string): Kobo entitlement ID
|
||||
- `RemainingTimeMinutes` (integer): Estimated reading time remaining
|
||||
- `FirstReadTime` (string): ISO 8601 timestamp when first opened
|
||||
- `LastModified` (string): ISO 8601 timestamp of last progress update
|
||||
- `BookmarkSync` (array): Empty array for progress-only sync
|
||||
|
||||
**Response:**
|
||||
- `synced` (integer): Number of items synced
|
||||
- `failed` (integer): Number of items that failed to sync
|
||||
- `timestamp` (string): Server timestamp of sync
|
||||
- `books` (array): Synced book data
|
||||
- `ContentId` (string): Book UUID
|
||||
- `status` (string): `synced`, `failed`, `skipped`
|
||||
- `server_percent` (number): Server-side progress (for conflict detection)
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Sync successful
|
||||
- 401: Invalid device token
|
||||
- 403: Device not approved
|
||||
- 400: Invalid request format
|
||||
|
||||
**Authentication Methods:**
|
||||
This endpoint uses **Bearer token authentication** (token in Authorization header).
|
||||
Alternative: Use `/sync/kobo/{token}/markup` with token in URL path.
|
||||
|
||||
**Kobo Sync Features:**
|
||||
- Native Kobo sync protocol
|
||||
- Supports Kobo Clara, Libra, Aura, Forma, Sage, Elipsa
|
||||
- Progress percentage tracking
|
||||
- Reading time estimation
|
||||
- Cross-device synchronization
|
||||
- Automatic conflict resolution (most recent wins)
|
||||
|
||||
**Sync Frequency:**
|
||||
- Kobo devices auto-sync every 15-30 minutes when connected to WiFi
|
||||
- Manual sync available from device settings
|
||||
- Sync triggers on: device wake, book close, WiFi connection
|
||||
data: "{\n \"ReadingSync\": [\n {\n \"ContentId\": \"book-uuid-here\",\n \"PercentRead\": 45.6,\n \"EntitlementId\": \"entitlement-id-here\",\n \"RemainingTimeMinutes\": 120,\n \"FirstReadTime\": \"2026-01-25T10:00:00Z\",\n \"LastModified\": \"2026-01-30T20:00:00Z\"\n }\n ],\n \"BookmarkSync\": []\n}"
|
||||
docs: "## Sync Kobo Reading Progress\n\nSynchronizes reading progress from a Kobo device to the server using Bearer token authentication.\n\n**Method:** POST\n\n**Endpoint:** /api/sync/kobo/markup\n\n**Authentication:** Bearer token (in Authorization header)\n\n**Headers:**\n- `Authorization`: Bearer {{kobo_device_token}}\n- `x-kobo-device`: JSON-encoded device info\n - `DeviceId`: Kobo device identifier\n - `Model`: Device model (e.g., \"Kobo Clara\", \"Kobo Libra\", \"Kobo Aura\")\n\n**Request Body:**\n- `ReadingSync` (array): Reading progress items\n - `ContentId` (string): Book/Content UUID\n - `PercentRead` (number): Reading progress 0-100\n - `EntitlementId` (string): Kobo entitlement ID\n - `RemainingTimeMinutes` (integer): Estimated reading time remaining\n - `FirstReadTime` (string): ISO 8601 timestamp when first opened\n - `LastModified` (string): ISO 8601 timestamp of last progress update\n- `BookmarkSync` (array): Empty array for progress-only sync\n\n**Response:**\n\
|
||||
- `synced` (integer): Number of items synced\n- `failed` (integer): Number of items that failed to sync\n- `timestamp` (string): Server timestamp of sync\n- `books` (array): Synced book data\n - `ContentId` (string): Book UUID\n - `status` (string): `synced`, `failed`, `skipped`\n - `server_percent` (number): Server-side progress (for conflict detection)\n\n**Status Codes:**\n- 200: Sync successful\n- 401: Invalid device token\n- 403: Device not approved\n- 400: Invalid request format\n\n**Authentication Methods:**\nThis endpoint uses **Bearer token authentication** (token in Authorization header).\nAlternative: Use `/sync/kobo/{token}/markup` with token in URL path.\n\n**Kobo Sync Features:**\n- Native Kobo sync protocol\n- Supports Kobo Clara, Libra, Aura, Forma, Sage, Elipsa\n- Progress percentage tracking\n- Reading time estimation\n- Cross-device synchronization\n- Automatic conflict resolution (most recent wins)\n\n**Sync Frequency:**\n- Kobo devices auto-sync every 15-30 minutes\
|
||||
\ when connected to WiFi\n- Manual sync available from device settings\n- Sync triggers on: device wake, book close, WiFi connection"
|
||||
@@ -2,91 +2,18 @@ info:
|
||||
name: Sync Single Bookmark
|
||||
type: http
|
||||
seq: 5
|
||||
|
||||
http:
|
||||
method: POST
|
||||
url: '{{base_url}}/api/sync/kobo/{{kobo_device_token}}/bookmark'
|
||||
auth: inherit
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
- key: x-kobo-device
|
||||
value: '{"DeviceId":"{{kobo_device_id}}","Model":"Kobo Clara"}'
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
- key: x-kobo-device
|
||||
value: '{"DeviceId":"{{kobo_device_id}}","Model":"Kobo Clara"}'
|
||||
body:
|
||||
type: json
|
||||
json:
|
||||
ContentId: book-uuid
|
||||
BookmarkText: Bookmarked passage
|
||||
BookmarkType: annotation
|
||||
BookmarkTitle: Chapter 3
|
||||
|
||||
docs: |-
|
||||
## Sync Single Bookmark
|
||||
|
||||
Synchronizes an individual bookmark/highlight from Kobo device.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/sync/kobo/bookmark
|
||||
|
||||
**Authentication:** Bearer token
|
||||
|
||||
**Headers:**
|
||||
- `Authorization`: Bearer {{kobo_device_token}}
|
||||
- `x-kobo-device`: Device information JSON
|
||||
|
||||
**Request Body:**
|
||||
- `ContentId` (string): Book UUID
|
||||
- `BookmarkText` (string): Highlighted text or bookmark description
|
||||
- `BookmarkType` (string): Type of bookmark
|
||||
- `annotation`: Highlighted text passage
|
||||
- `note`: Personal note
|
||||
- `bookmark`: Location marker
|
||||
- `BookmarkTitle` (string): Reference title (e.g., chapter name)
|
||||
- `ChapterID` (string, optional): Chapter identifier
|
||||
- `DateCreated` (string, optional): ISO 8601 timestamp
|
||||
- `highlight_color` (string, optional): Color name (yellow, green, blue, pink, orange)
|
||||
|
||||
**Response:**
|
||||
- `id` (string): Server bookmark ID
|
||||
- `ContentId` (string): Associated book UUID
|
||||
- `status` (string): `created`, `updated`, `skipped` (duplicate)
|
||||
- `timestamp` (string): Server timestamp
|
||||
- `url` (string): API URL to retrieve bookmark
|
||||
|
||||
**Status Codes:**
|
||||
- 201: Bookmark created
|
||||
- 200: Bookmark updated (duplicate found)
|
||||
- 409: Duplicate bookmark (unchanged)
|
||||
- 401: Unauthorized
|
||||
- 400: Invalid bookmark data
|
||||
|
||||
**Single Bookmark Sync vs Batch:**
|
||||
- **Single bookmark endpoint:** Real-time, immediate sync
|
||||
- **Batch markup endpoint:** Deferred, periodic sync
|
||||
- Use single when user explicitly creates highlight
|
||||
- Use batch for periodic background sync
|
||||
|
||||
**Kobo Trigger:**
|
||||
- User highlights text → immediate single sync
|
||||
- User adds note → immediate single sync
|
||||
- Device goes online → batch sync of all changes
|
||||
|
||||
**Duplicate Detection:**
|
||||
- Same ContentId + similar BookmarkText + same location
|
||||
- Updates existing if text modified
|
||||
- Skips if identical bookmark exists
|
||||
- Preserves creation date of original
|
||||
|
||||
**Use Cases:**
|
||||
- Real-time highlight sync
|
||||
- Instant note backup
|
||||
- Immediate annotation sharing
|
||||
- Quick single annotation update
|
||||
- Testing annotation sync
|
||||
|
||||
**Notes:**
|
||||
- Much faster than full markup sync
|
||||
- Lower bandwidth usage
|
||||
- Ideal for intermittent connectivity
|
||||
- Can be called multiple times safely
|
||||
data: "{\n \"ContentId\": \"book-uuid\",\n \"BookmarkText\": \"Bookmarked passage\",\n \"BookmarkType\": \"annotation\",\n \"BookmarkTitle\": \"Chapter 3\"\n}"
|
||||
docs: "## Sync Single Bookmark\n\nSynchronizes an individual bookmark/highlight from Kobo device.\n\n**Method:** POST\n\n**Endpoint:** /api/sync/kobo/bookmark\n\n**Authentication:** Bearer token\n\n**Headers:**\n- `Authorization`: Bearer {{kobo_device_token}}\n- `x-kobo-device`: Device information JSON\n\n**Request Body:**\n- `ContentId` (string): Book UUID\n- `BookmarkText` (string): Highlighted text or bookmark description\n- `BookmarkType` (string): Type of bookmark\n - `annotation`: Highlighted text passage\n - `note`: Personal note\n - `bookmark`: Location marker\n- `BookmarkTitle` (string): Reference title (e.g., chapter name)\n- `ChapterID` (string, optional): Chapter identifier\n- `DateCreated` (string, optional): ISO 8601 timestamp\n- `highlight_color` (string, optional): Color name (yellow, green, blue, pink, orange)\n\n**Response:**\n- `id` (string): Server bookmark ID\n- `ContentId` (string): Associated book UUID\n- `status` (string): `created`, `updated`, `skipped` (duplicate)\n\
|
||||
- `timestamp` (string): Server timestamp\n- `url` (string): API URL to retrieve bookmark\n\n**Status Codes:**\n- 201: Bookmark created\n- 200: Bookmark updated (duplicate found)\n- 409: Duplicate bookmark (unchanged)\n- 401: Unauthorized\n- 400: Invalid bookmark data\n\n**Single Bookmark Sync vs Batch:**\n- **Single bookmark endpoint:** Real-time, immediate sync\n- **Batch markup endpoint:** Deferred, periodic sync\n- Use single when user explicitly creates highlight\n- Use batch for periodic background sync\n\n**Kobo Trigger:**\n- User highlights text → immediate single sync\n- User adds note → immediate single sync\n- Device goes online → batch sync of all changes\n\n**Duplicate Detection:**\n- Same ContentId + similar BookmarkText + same location\n- Updates existing if text modified\n- Skips if identical bookmark exists\n- Preserves creation date of original\n\n**Use Cases:**\n- Real-time highlight sync\n- Instant note backup\n- Immediate annotation sharing\n- Quick single annotation\
|
||||
\ update\n- Testing annotation sync\n\n**Notes:**\n- Much faster than full markup sync\n- Lower bandwidth usage\n- Ideal for intermittent connectivity\n- Can be called multiple times safely"
|
||||
@@ -2,105 +2,17 @@ info:
|
||||
name: Sync with Annotations
|
||||
type: http
|
||||
seq: 4
|
||||
|
||||
http:
|
||||
method: POST
|
||||
url: '{{base_url}}/api/sync/kobo/{{kobo_device_token}}/markup'
|
||||
auth: inherit
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
- key: x-kobo-device
|
||||
value: '{"DeviceId":"{{kobo_device_id}}","Model":"Kobo Libra"}'
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
- key: x-kobo-device
|
||||
value: '{"DeviceId":"{{kobo_device_id}}","Model":"Kobo Libra"}'
|
||||
body:
|
||||
type: json
|
||||
json:
|
||||
ReadingSync:
|
||||
- ContentId: book-uuid
|
||||
PercentRead: 55.0
|
||||
EntitlementId: entitlement-id
|
||||
RemainingTimeMinutes: 120
|
||||
FirstReadTime: '2026-01-25T10:00:00Z'
|
||||
LastModified: '2026-01-30T20:00:00Z'
|
||||
BookmarkSync:
|
||||
- ContentId: book-uuid
|
||||
BookmarkText: Important quote
|
||||
BookmarkType: annotation
|
||||
BookmarkTitle: Chapter 4 - The Truth
|
||||
- ContentId: book-uuid
|
||||
BookmarkText: Another quote
|
||||
BookmarkType: annotation
|
||||
BookmarkTitle: Chapter 5
|
||||
- ContentId: book-uuid
|
||||
BookmarkText: Note to myself
|
||||
BookmarkType: note
|
||||
BookmarkTitle: Personal note
|
||||
|
||||
docs: |-
|
||||
## Sync with Multiple Annotations
|
||||
|
||||
Synchronizes reading progress with multiple highlights and notes.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/sync/kobo/markup
|
||||
|
||||
**Authentication:** Bearer token
|
||||
|
||||
**Headers:**
|
||||
- `Authorization`: Bearer {{kobo_device_token}}
|
||||
- `x-kobo-device`: Device info (Model: "Kobo Libra")
|
||||
|
||||
**Request Body:**
|
||||
- `ReadingSync` (array): Single book progress
|
||||
- `BookmarkSync` (array): Multiple annotations
|
||||
- Can include highlights (annotation type)
|
||||
- Can include notes (note type)
|
||||
- Each has: ContentId, BookmarkText, BookmarkType, BookmarkTitle
|
||||
|
||||
**Response:**
|
||||
- `progress_synced` (boolean): Progress update status
|
||||
- `annotations_synced` (integer): Number of annotations synced
|
||||
- `highlights_count` (integer): Highlights synced
|
||||
- `notes_count` (integer): Notes synced
|
||||
- `conflicts` (array): Any annotation conflicts resolved
|
||||
- `timestamp` (string): Sync completion time
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Successful sync
|
||||
- 401: Unauthorized
|
||||
- 400: Invalid annotation data
|
||||
|
||||
**Kobo Annotation Types:**
|
||||
- **Highlights:** Selected text passages
|
||||
- 5 preset colors available
|
||||
- Can have chapter titles
|
||||
- Exportable to PDF/Mobile
|
||||
- **Notes:** Personal annotations
|
||||
- Free-form text
|
||||
- Can be attached to highlights
|
||||
- Separate from highlights
|
||||
- **Bookmarks:** Location markers
|
||||
- Chapter positions
|
||||
- Quick navigation
|
||||
|
||||
**Sync Behavior:**
|
||||
- Duplicates detected by content matching
|
||||
- Most recent edit wins conflicts
|
||||
- Annotations linked to book content
|
||||
- Chapter references preserved
|
||||
- Order maintained from device
|
||||
|
||||
**Kobo Notebook Export:**
|
||||
- All annotations appear in Kobo "Notebook"
|
||||
- Can be exported to PDF
|
||||
- Can be exported to Mobile (text)
|
||||
- Organized by book
|
||||
- Shows highlight context
|
||||
|
||||
**Use Cases:**
|
||||
- Study and research
|
||||
- Book club discussion prep
|
||||
- Content review
|
||||
- Sharing insights
|
||||
- Personal learning archive
|
||||
data: "{\n \"ReadingSync\": [\n {\n \"ContentId\": \"book-uuid\",\n \"PercentRead\": 55.0,\n \"EntitlementId\": \"entitlement-id\",\n \"RemainingTimeMinutes\": 120,\n \"FirstReadTime\": \"2026-01-25T10:00:00Z\",\n \"LastModified\": \"2026-01-30T20:00:00Z\"\n }\n ],\n \"BookmarkSync\": [\n {\n \"ContentId\": \"book-uuid\",\n \"BookmarkText\": \"Important quote\",\n \"BookmarkType\": \"annotation\",\n \"BookmarkTitle\": \"Chapter 4 - The Truth\"\n },\n {\n \"ContentId\": \"book-uuid\",\n \"BookmarkText\": \"Another quote\",\n \"BookmarkType\": \"annotation\",\n \"BookmarkTitle\": \"Chapter 5\"\n },\n {\n \"ContentId\": \"book-uuid\",\n \"BookmarkText\": \"Note to myself\",\n \"BookmarkType\": \"note\",\n \"BookmarkTitle\": \"Personal note\"\n }\n ]\n}"
|
||||
docs: "## Sync with Multiple Annotations\n\nSynchronizes reading progress with multiple highlights and notes.\n\n**Method:** POST\n\n**Endpoint:** /api/sync/kobo/markup\n\n**Authentication:** Bearer token\n\n**Headers:**\n- `Authorization`: Bearer {{kobo_device_token}}\n- `x-kobo-device`: Device info (Model: \"Kobo Libra\")\n\n**Request Body:**\n- `ReadingSync` (array): Single book progress\n- `BookmarkSync` (array): Multiple annotations\n - Can include highlights (annotation type)\n - Can include notes (note type)\n - Each has: ContentId, BookmarkText, BookmarkType, BookmarkTitle\n\n**Response:**\n- `progress_synced` (boolean): Progress update status\n- `annotations_synced` (integer): Number of annotations synced\n- `highlights_count` (integer): Highlights synced\n- `notes_count` (integer): Notes synced\n- `conflicts` (array): Any annotation conflicts resolved\n- `timestamp` (string): Sync completion time\n\n**Status Codes:**\n- 200: Successful sync\n- 401: Unauthorized\n- 400: Invalid\
|
||||
\ annotation data\n\n**Kobo Annotation Types:**\n- **Highlights:** Selected text passages\n - 5 preset colors available\n - Can have chapter titles\n - Exportable to PDF/Mobile\n- **Notes:** Personal annotations\n - Free-form text\n - Can be attached to highlights\n - Separate from highlights\n- **Bookmarks:** Location markers\n - Chapter positions\n - Quick navigation\n\n**Sync Behavior:**\n- Duplicates detected by content matching\n- Most recent edit wins conflicts\n- Annotations linked to book content\n- Chapter references preserved\n- Order maintained from device\n\n**Kobo Notebook Export:**\n- All annotations appear in Kobo \"Notebook\"\n- Can be exported to PDF\n- Can be exported to Mobile (text)\n- Organized by book\n- Shows highlight context\n\n**Use Cases:**\n- Study and research\n- Book club discussion prep\n- Content review\n- Sharing insights\n- Personal learning archive"
|
||||
@@ -8,38 +8,8 @@ http:
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"BookmarkSync\": [\n {\n \"BookmarkId\": \"bookmark_2\"\
|
||||
,\n \"ContentId\": \"kobo_xyz789\",\n \"BookmarkText\": \"Important\
|
||||
\ note\",\n \"BookmarkType\": \"bookmark\",\n \"DateCreated\"\
|
||||
: \"2026-01-31T12:00:00Z\""
|
||||
data: "{\n \"BookmarkSync\": [\n {\n \"BookmarkId\": \"bookmark_2\",\n \"ContentId\": \"kobo_xyz789\",\n \"BookmarkText\": \"Important note\",\n \"BookmarkType\": \"bookmark\",\n \"DateCreated\": \"2026-01-31T12:00:00Z\""
|
||||
headers:
|
||||
- key: Authorization
|
||||
value: Bearer {{device_token
|
||||
|
||||
docs: |-
|
||||
## Kobo Bookmark Sync
|
||||
|
||||
Synchronizes bookmarks from a Kobo device to the Bookhoard server.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/v1/kobo/bookmark
|
||||
|
||||
**Authentication:** Bearer token (device token)
|
||||
|
||||
**Request Body:**
|
||||
- `BookmarkSync` (array): Array of bookmark objects
|
||||
- `BookmarkId` (string): Unique bookmark ID
|
||||
- `ContentId` (string): Book/content ID
|
||||
- `BookmarkText` (string): Bookmark text or note
|
||||
- `BookmarkType` (string): Type (bookmark, highlight, note)
|
||||
- `DateCreated` (string): ISO 8601 timestamp
|
||||
|
||||
**Response:**
|
||||
- `Status` (string): Sync status (Success, Partial)
|
||||
- `BookmarksSynced` (number): Number of bookmarks synced
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 401: Unauthorized
|
||||
- 500: Internal server error
|
||||
docs: "## Kobo Bookmark Sync\n\n Synchronizes bookmarks from a Kobo device to the Bookhoard server.\n\n **Method:** POST\n\n **Endpoint:** /api/v1/kobo/bookmark\n\n **Authentication:** Bearer token (device token)\n\n **Request Body:**\n - `BookmarkSync` (array): Array of bookmark objects\n - `BookmarkId` (string): Unique bookmark ID\n - `ContentId` (string): Book/content ID\n - `BookmarkText` (string): Bookmark text or note\n - `BookmarkType` (string): Type (bookmark, highlight, note)\n - `DateCreated` (string): ISO 8601 timestamp\n\n **Response:**\n - `Status` (string): Sync status (Success, Partial)\n - `BookmarksSynced` (number): Number of bookmarks synced\n\n **Status Codes:**\n - 200: Success\n - 401: Unauthorized\n - 500: Internal server error"
|
||||
@@ -8,36 +8,8 @@ http:
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"unlinked_book_id\": \"{{unlinked_book_id"
|
||||
data: "{\n \"unlinked_book_id\": \"{{unlinked_book_id"
|
||||
headers:
|
||||
- key: Authorization
|
||||
value: Bearer {{user_token
|
||||
|
||||
docs: |-
|
||||
## Link Unlinked Book - Manual Resolution
|
||||
|
||||
Manually links an unlinked book to a media item in the library.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/sync/link-book
|
||||
|
||||
**Authentication:** Bearer token
|
||||
|
||||
**Request Body:**
|
||||
- `unlinked_book_id` (string): Unlinked book UUID
|
||||
- `media_item_id` (string): Media item UUID to link to
|
||||
- `confidence_score` (number): Match confidence (0-1, 1.0 for manual)
|
||||
|
||||
**Response:**
|
||||
- `status` (string): Link status (linked)
|
||||
- `unlinked_book_id` (string): Unlinked book UUID
|
||||
- `media_item_id` (string): Media item UUID
|
||||
- `message` (string): Success message
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success - book linked
|
||||
- 400: Invalid request
|
||||
- 401: Unauthorized
|
||||
- 404: Book or media item not found
|
||||
- 500: Internal server error
|
||||
docs: "## Link Unlinked Book - Manual Resolution\n\n Manually links an unlinked book to a media item in the library.\n\n **Method:** POST\n\n **Endpoint:** /api/sync/link-book\n\n **Authentication:** Bearer token\n\n **Request Body:**\n - `unlinked_book_id` (string): Unlinked book UUID\n - `media_item_id` (string): Media item UUID to link to\n - `confidence_score` (number): Match confidence (0-1, 1.0 for manual)\n\n **Response:**\n - `status` (string): Link status (linked)\n - `unlinked_book_id` (string): Unlinked book UUID\n - `media_item_id` (string): Media item UUID\n - `message` (string): Success message\n\n **Status Codes:**\n - 200: Success - book linked\n - 400: Invalid request\n - 401: Unauthorized\n - 404: Book or media item not found\n - 500: Internal server error"
|
||||
@@ -8,18 +8,8 @@ http:
|
||||
auth: none
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"ContentId\": \"book-uuid\",\n \"BookmarkText\": \"Highlighted\
|
||||
\ text\",\n \"BookmarkType\": \"annotation\",\n \"BookmarkTitle\": \"\
|
||||
Chapter 3\""
|
||||
data: "{\n \"ContentId\": \"book-uuid\",\n \"BookmarkText\": \"Highlighted text\",\n \"BookmarkType\": \"annotation\",\n \"BookmarkTitle\": \"Chapter 3\""
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
|
||||
docs: |-
|
||||
## Sync Bookmark - URL Path Token
|
||||
|
||||
Synchronizes bookmarks and annotations from Kobo device using token in URL path.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /sync/kobo/{kobo_device_token
|
||||
docs: "## Sync Bookmark - URL Path Token\n\n Synchronizes bookmarks and annotations from Kobo device using token in URL path.\n\n **Method:** POST\n\n **Endpoint:** /sync/kobo/{kobo_device_token"
|
||||
@@ -8,19 +8,8 @@ http:
|
||||
auth: none
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"ReadingSync\": [\n {\n \"ContentId\": \"book-uuid\"\
|
||||
,\n \"PercentRead\": 45.6,\n \"EntitlementId\": \"entitlement-id\"\
|
||||
,\n \"RemainingTimeMinutes\": 120,\n \"FirstReadTime\": \"2026-01-25T10:00:00Z\"\
|
||||
,\n \"LastModified\": \"2026-01-30T20:00:00Z\""
|
||||
data: "{\n \"ReadingSync\": [\n {\n \"ContentId\": \"book-uuid\",\n \"PercentRead\": 45.6,\n \"EntitlementId\": \"entitlement-id\",\n \"RemainingTimeMinutes\": 120,\n \"FirstReadTime\": \"2026-01-25T10:00:00Z\",\n \"LastModified\": \"2026-01-30T20:00:00Z\""
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
|
||||
docs: |-
|
||||
## Sync Reading Progress - URL Path Token
|
||||
|
||||
Synchronizes reading progress from Kobo device using token in URL path.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /sync/kobo/{kobo_device_token
|
||||
docs: "## Sync Reading Progress - URL Path Token\n\n Synchronizes reading progress from Kobo device using token in URL path.\n\n **Method:** POST\n\n **Endpoint:** /sync/kobo/{kobo_device_token"
|
||||
@@ -3,18 +3,8 @@ docs: "## Checkpoint Sync\n\nIncremental sync using checkpoint-based change trac
|
||||
\ Repeat from step 3\n\n**Sync Optimization:**\n- Only books with progress changes\n- Skips unmodified books\n- Delta transfer\n- Compression for large payloads\n- Batch processing\n\n**Failure Handling:**\n- Checkpoint ID preserved on failure\n- Retry with same checkpoint\n- Full sync if checkpoint expired\n- Checkpoint validity: 24 hours\n- Auto-fallback to full sync\n\n**Performance:**\n- Small changes (1-10 books): 100-300ms\n- Medium changes (10-50 books): 300ms-1s\n- Large changes (50-100 books): 1-3s\n- Typical: 5-10x faster than full sync\n\n**When to Use:**\n- Default sync mode for most users\n- Periodic background sync\n- Large library management\n- Mobile/network-constrained environments\n- Battery-powered devices\n\n**Configuration:**\n- Checkpoint expiration: 24 hours\n- Max history: 100 checkpoints\n- Auto-cleanup of old checkpoints\n- Configurable sync interval"
|
||||
http:
|
||||
body:
|
||||
json:
|
||||
books:
|
||||
- chapter: 3
|
||||
percentage: 0.45
|
||||
uuid: '{{media_item_id}}'
|
||||
- chapter: 8
|
||||
percentage: 0.75
|
||||
uuid: '{{media_item_id}}'
|
||||
checkpoint_id: checkpoint-uuid
|
||||
since_timestamp: '2026-01-30T19:00:00Z'
|
||||
sync_mode: checkpoint
|
||||
type: json
|
||||
data: "{\n \"books\": [\n {\n \"chapter\": 3,\n \"percentage\": 0.45,\n \"uuid\": \"{{media_item_id}}\"\n },\n {\n \"chapter\": 8,\n \"percentage\": 0.75,\n \"uuid\": \"{{media_item_id}}\"\n }\n ],\n \"checkpoint_id\": \"checkpoint-uuid\",\n \"since_timestamp\": \"2026-01-30T19:00:00Z\",\n \"sync_mode\": \"checkpoint\"\n}"
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
|
||||
@@ -3,14 +3,8 @@ docs: "## Immediate Sync - Page Turn\n\nReal-time progress sync triggered immedi
|
||||
- Research and study\n- Testing sync functionality\n- When power source available\n\n**When NOT to Use:**\n- Battery conservation needed\n- Unstable network\n- Extended reading sessions\n- Background sync preferred"
|
||||
http:
|
||||
body:
|
||||
json:
|
||||
books:
|
||||
- chapter: 3
|
||||
percentage: 0.45678
|
||||
timestamp: '2026-01-30T20:00:00Z'
|
||||
uuid: '{{media_item_id}}'
|
||||
sync_mode: immediate
|
||||
type: json
|
||||
data: "{\n \"books\": [\n {\n \"chapter\": 3,\n \"percentage\": 0.45678,\n \"timestamp\": \"2026-01-30T20:00:00Z\",\n \"uuid\": \"{{media_item_id}}\"\n }\n ],\n \"sync_mode\": \"immediate\"\n}"
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
docs: "## KOReader Sync Annotations - Per-Book SHA-256\n\n Synchronizes annotations (highlights) from KOReader with per-annotation SHA-256 hashes for multi-book sync.\n\n **Method:** POST\n\n **Endpoint:** /api/v1/koreader/sync/bookmarks\n\n **Authentication:** Bearer token (KOReader device token)\n\n **Request Body:**\n - `book_uuid` (string): Primary book UUID\n - `highlights` (array): Array of highlight objects\n - `text` (string): Highlighted text\n - `pos0`, `pos1` (string): EPUB CFI positions\n - `color` (string): Highlight color (hex)\n - `page` (number): Page number\n - `book_sha256` (string): SHA-256 hash for this specific book\n\n **Response:**\n - `highlights_synced` (number): Number of highlights synced\n\n **Status Codes:**\n - 200: Success\n - 401: Unauthorized\n - 500: Internal server error\n\n **Note:** Each highlight can include its own book_sha256, allowing annotations from multiple books in a single request."
|
||||
http:
|
||||
body:
|
||||
jsonBody: "{\n \"book_uuid\": \"{{book_uuid"
|
||||
type: json
|
||||
data: "{\n \"book_sha256\": \"{{book_sha256}}\",\n \"bookmarks\": [\n {\n \"chapter\": 3,\n \"datetime\": \"2026-01-30T19:55:00Z\",\n \"page\": 45,\n \"pos0\": \"p1\",\n \"text\": \"Per-book annotation\",\n \"type\": \"bookmark\"\n }\n ]\n}"
|
||||
headers: []
|
||||
method: POST
|
||||
url: '{{base_url}}/api/sync/koreader/bookmarks'
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
docs: "## KOReader Sync Bookmarks - SHA-256\n\n Synchronizes bookmarks, notes, and highlights from KOReader using SHA-256 hash for book identification.\n\n **Method:** POST\n\n **Endpoint:** /api/v1/koreader/sync/bookmarks\n\n **Authentication:** Bearer token (KOReader device token)\n\n **Request Body:**\n - `book_sha256` (string): SHA-256 hash of book file\n - `bookmarks` (array): Array of bookmarks\n - `notes` (array): Array of notes\n - `highlights` (array): Array of highlights\n\n **Response:**\n - `sync_status` (string): Sync status\n - `total_synced` (number): Total items synced\n\n **Status Codes:**\n - 200: Success\n - 401: Unauthorized\n - 500: Internal server error"
|
||||
http:
|
||||
body:
|
||||
jsonBody: "{\n \"book_sha256\": \"{{book_sha256"
|
||||
type: json
|
||||
data: "{\n \"book_sha256\": \"{{book_sha256}}\",\n \"bookmarks\": [\n {\n \"chapter\": 3,\n \"datetime\": \"2026-01-30T19:55:00Z\",\n \"page\": 45,\n \"pos0\": \"p1\",\n \"text\": \"SHA-256 bookmark\",\n \"type\": \"bookmark\"\n }\n ]\n}"
|
||||
headers: []
|
||||
method: POST
|
||||
url: '{{base_url}}/api/sync/koreader/bookmarks'
|
||||
|
||||
@@ -3,20 +3,8 @@ docs: "## Sync KOReader Bookmarks\n\nSynchronizes bookmarks separately from prog
|
||||
\ bookmark\" menu\n- Shown in \"Bookmarks\" panel\n- Quick access via \"Go to bookmark\"\n- Exportable to JSON\n- Can be edited/deleted\n\n**Use Cases:**\n- Quick navigation aids\n- Chapter markers\n- Important passages\n- Reading progress points\n- Study session markers"
|
||||
http:
|
||||
body:
|
||||
json:
|
||||
books:
|
||||
- bookmarks:
|
||||
- chapter: 3
|
||||
datetime: '2026-01-30T19:55:00Z'
|
||||
notes: Marked this chapter as important
|
||||
page: 45
|
||||
pos0: epubcfi(/6/4/2:15)
|
||||
pos1: epubcfi(/6/4/2:20)
|
||||
text: Important passage
|
||||
type: bookmark
|
||||
uuid: '{{media_item_id}}'
|
||||
library_id: optional-library-uuid
|
||||
type: json
|
||||
data: "{\n \"book_uuid\": \"{{media_item_id}}\",\n \"bookmarks\": [\n {\n \"chapter\": 3,\n \"datetime\": \"2026-01-30T19:55:00Z\",\n \"page\": 45,\n \"pos0\": \"epubcfi(/6/4/2:15)\",\n \"pos1\": \"epubcfi(/6/4/2:20)\",\n \"text\": \"Important passage\",\n \"type\": \"bookmark\"\n }\n ]\n}"
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
|
||||
@@ -4,10 +4,7 @@ docs: "## Sync KOReader Highlights\n\nSynchronizes text highlights separately fr
|
||||
http:
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"book_uuid\": \"786b9c39-e8c3-47ab-a48a-c7f6cd769930\",\n \"highlights\": [\n {\n \"datetime\": \"2026-01-30T19:50:00Z\",\n \"text\": \"bruno sweep highlight\",\n \"chapter\": 1,\n \"page\": 1,\n \"pos0\": \"1\",\n \"pos1\": \"9\",\n \"color\": \"#ffff00\"\n }\n ]\n}"
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
data: "{\n \"book_uuid\": \"786b9c39-e8c3-47ab-a48a-c7f6cd769930\",\n \"highlights\": [\n {\n \"datetime\": \"2026-01-30T19:50:00Z\",\n \"text\": \"bruno sweep highlight\",\n \"chapter\": 1,\n \"page\": 1,\n \"pos0\": \"1\",\n \"pos1\": \"9\",\n \"color\": \"#ffff00\"\n }\n ]\n}"
|
||||
method: POST
|
||||
url: '{{base_url}}/api/sync/koreader/bookmarks'
|
||||
auth:
|
||||
|
||||
@@ -3,8 +3,8 @@ docs: "## Sync KOReader Notes\n\nSynchronizes user notes separately from highlig
|
||||
\ via \"Add note\" option\n- Edited in note editor\n- Shown in \"Notes\" panel\n- Can be organized by chapter\n- Export functionality available\n- Search support\n\n**Advanced Features:**\n- **Markdown**: Bold, italic, lists (some versions)\n- **Tags**: User-defined tags (via plugins)\n- **Links**: Internal/external links\n- **Formatting**: Rich text in newer versions\n\n**Use Cases:**\n- Study notes\n- Personal reflections\n- Academic annotations\n- Research insights\n- Book club discussion prep\n- Knowledge management"
|
||||
http:
|
||||
body:
|
||||
jsonBody: "{\n \"book_uuid\": \"{{media_item_id}}\",\n \"notes\": [\n {\n \"datetime\": \"2026-01-30T19:52:00Z\",\n \"notes\": \"A personal note about this chapter\",\n \"chapter\": 4,\n \"page\": 57,\n \"pos0\": \"3\"\n }\n ]\n}"
|
||||
type: json
|
||||
data: "{\n \"book_uuid\": \"{{media_item_id}}\",\n \"notes\": [\n {\n \"datetime\": \"2026-01-30T19:52:00Z\",\n \"notes\": \"A personal note about this chapter\",\n \"chapter\": 4,\n \"page\": 57,\n \"pos0\": \"3\"\n }\n ]\n}"
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
docs: "## KOReader Sync Progress - SHA-256 Only\n\n Synchronizes reading progress using only SHA-256 hash for book identification (when UUID is not available).\n\n **Method:** POST\n\n **Endpoint:** /api/v1/koreader/sync/progress\n\n **Authentication:** Bearer token (KOReader device token)\n\n **Request Body:**\n - `sync_mode` (string): Sync mode (immediate, deferred)\n - `books` (array): Array of book progress objects\n - `sha256` (string): SHA-256 hash of book file\n - `file_path` (string): Path to book file\n - `percentage` (number): Progress percentage\n - `page` (number): Current page\n - `total_pages` (number): Total pages\n\n **Response:**\n - `sync_status` (string): Sync status\n - `books_synced` (number): Number of books synced\n\n **Status Codes:**\n - 200: Success\n - 202: Accepted\n - 401: Unauthorized\n - 500: Internal server error\n\n **Note:** Use this when book UUID is not available, falling back to SHA-256 hash for identification."
|
||||
http:
|
||||
body:
|
||||
jsonBody: "{\n \"sync_mode\": \"immediate\",\n \"books\": [\n {\n \"sha256\": \"{{book_sha256"
|
||||
type: json
|
||||
data: "{\n \"sync_mode\": \"immediate\",\n \"books\": [\n {\n \"sha256\": \"{{book_sha256}}\",\n \"percentage\": 0.5\n }\n ]\n}"
|
||||
headers: []
|
||||
method: POST
|
||||
url: '{{base_url}}/api/sync/koreader/progress'
|
||||
|
||||
@@ -2,8 +2,8 @@ docs: "## KOReader Sync Progress - SHA-256\n\n Synchronizes reading progress fr
|
||||
\ (number): Number of books synced\n\n **Status Codes:**\n - 200: Success\n - 202: Accepted - processing\n - 401: Unauthorized\n - 500: Internal server error"
|
||||
http:
|
||||
body:
|
||||
jsonBody: "{\n \"sync_mode\": \"immediate\",\n \"books\": [\n {\n \"uuid\": \"{{book_uuid"
|
||||
type: json
|
||||
data: "{\n \"sync_mode\": \"immediate\",\n \"books\": [\n {\n \"sha256\": \"{{book_sha256}}\",\n \"percentage\": 0.5\n }\n ]\n}"
|
||||
headers: []
|
||||
method: POST
|
||||
url: '{{base_url}}/api/sync/koreader/progress'
|
||||
|
||||
@@ -2,28 +2,8 @@ docs: "## Sync KOReader Progress - Multiple Books\n\nSynchronizes reading progre
|
||||
\ Batch Behavior:**\n- Triggered on device wake from sleep\n- Occurs during \"Sync now\" operation\n- Runs on WiFi connection\n- Limited to 100 books per request\n- Automatic retry on failed books\n\n**Performance:**\n- Small batch (2-10 books): 200-500ms\n- Medium batch (10-50 books): 500ms-2s\n- Large batch (50-100 books): 2-5s\n- Timeout: 30 seconds\n\n**Use Cases:**\n- Device initialization sync\n- Periodic background sync\n- Post-offline catch-up sync\n- Library-wide progress update\n- Before firmware update"
|
||||
http:
|
||||
body:
|
||||
json:
|
||||
books:
|
||||
- authors:
|
||||
- Author One
|
||||
chapter: 1
|
||||
epubcfi: epubcfi(/6/4/2:10)
|
||||
last_read: '2026-01-30T19:00:00Z'
|
||||
percentage: 0.25
|
||||
progress: 0.25
|
||||
title: Book One
|
||||
uuid: '{{media_item_id}}'
|
||||
- authors:
|
||||
- Author Two
|
||||
chapter: 8
|
||||
epubcfi: epubcfi(/6/4/2:50)
|
||||
last_read: '2026-01-30T20:00:00Z'
|
||||
percentage: 0.75
|
||||
progress: 0.75
|
||||
title: Book Two
|
||||
uuid: '{{media_item_id}}'
|
||||
library_id: optional-library-uuid
|
||||
type: json
|
||||
data: "{\n \"books\": [\n {\n \"authors\": [\n \"Author One\"\n ],\n \"chapter\": 1,\n \"epubcfi\": \"epubcfi(/6/4/2:10)\",\n \"last_read\": \"2026-01-30T19:00:00Z\",\n \"percentage\": 0.25,\n \"progress\": 0.25,\n \"title\": \"Book One\",\n \"uuid\": \"{{media_item_id}}\"\n },\n {\n \"authors\": [\n \"Author Two\"\n ],\n \"chapter\": 8,\n \"epubcfi\": \"epubcfi(/6/4/2:50)\",\n \"last_read\": \"2026-01-30T20:00:00Z\",\n \"percentage\": 0.75,\n \"progress\": 0.75,\n \"title\": \"Book Two\",\n \"uuid\": \"{{media_item_id}}\"\n }\n ],\n \"library_id\": \"optional-library-uuid\"\n}"
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
|
||||
@@ -3,20 +3,8 @@ docs: "## Sync KOReader Progress - Single Book\n\nSynchronizes reading progress
|
||||
- Real-time page turn sync\n- Progress backup\n- Cross-device continuity\n- Reading time tracking\n- Chapter completion detection"
|
||||
http:
|
||||
body:
|
||||
json:
|
||||
books:
|
||||
- authors:
|
||||
- Author Name
|
||||
chapter: 3
|
||||
character: 15432
|
||||
epubcfi: epubcfi(/6/4/2:15)
|
||||
last_read: '2026-01-30T20:00:00Z'
|
||||
percentage: 0.45
|
||||
progress: 0.45
|
||||
title: Book Title
|
||||
uuid: '{{media_item_id}}'
|
||||
library_id: optional-library-uuid
|
||||
type: json
|
||||
data: "{\n \"books\": [\n {\n \"authors\": [\n \"Author Name\"\n ],\n \"chapter\": 3,\n \"character\": 15432,\n \"epubcfi\": \"epubcfi(/6/4/2:15)\",\n \"last_read\": \"2026-01-30T20:00:00Z\",\n \"percentage\": 0.45,\n \"progress\": 0.45,\n \"title\": \"Book Title\",\n \"uuid\": \"{{media_item_id}}\"\n }\n ],\n \"library_id\": \"optional-library-uuid\"\n}"
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
|
||||
@@ -3,26 +3,8 @@ docs: "## Sync Progress with Bookmarks\n\nSynchronizes reading progress with boo
|
||||
- Searchable by content\n\n**Use Cases:**\n- Study and research\n- Content review\n- Passage tracking\n- Quick navigation\n- Cross-device bookmark access"
|
||||
http:
|
||||
body:
|
||||
json:
|
||||
books:
|
||||
- authors:
|
||||
- Author Name
|
||||
bookmarks:
|
||||
- chapter: 3
|
||||
datetime: '2026-01-30T19:55:00Z'
|
||||
notes: highlighted text
|
||||
page: 45
|
||||
pos0: epubcfi(/6/4/2:15)
|
||||
pos1: epubcfi(/6/4/2:20)
|
||||
text: highlighted text excerpt
|
||||
type: highlight
|
||||
last_read: '2026-01-30T20:00:00Z'
|
||||
percentage: 0.45
|
||||
progress: 0.45
|
||||
title: Book Title
|
||||
uuid: '{{media_item_id}}'
|
||||
library_id: optional-library-uuid
|
||||
type: json
|
||||
data: "{\n \"books\": [\n {\n \"authors\": [\n \"Author Name\"\n ],\n \"bookmarks\": [\n {\n \"chapter\": 3,\n \"datetime\": \"2026-01-30T19:55:00Z\",\n \"notes\": \"highlighted text\",\n \"page\": 45,\n \"pos0\": \"epubcfi(/6/4/2:15)\",\n \"pos1\": \"epubcfi(/6/4/2:20)\",\n \"text\": \"highlighted text excerpt\",\n \"type\": \"highlight\"\n }\n ],\n \"last_read\": \"2026-01-30T20:00:00Z\",\n \"percentage\": 0.45,\n \"progress\": 0.45,\n \"title\": \"Book Title\",\n \"uuid\": \"{{media_item_id}}\"\n }\n ],\n \"library_id\": \"optional-library-uuid\"\n}"
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
|
||||
@@ -3,29 +3,8 @@ docs: "## Sync Progress with Highlights and Notes\n\nSynchronizes reading progre
|
||||
\ passages\n- Blue: Key concepts\n- Red: Critical information\n- Orange: Interesting quotes\n- Custom colors available\n\n**Sync Advantages:**\n- Separation allows granular control\n- Highlights sync without notes\n- Notes sync independently\n- Better conflict resolution\n- Efficient for large annotation sets\n\n**Use Cases:**\n- Academic research\n- Study groups\n- Content analysis\n- Personal knowledge management\n- Sharing insights"
|
||||
http:
|
||||
body:
|
||||
json:
|
||||
books:
|
||||
- authors:
|
||||
- Author Name
|
||||
highlights:
|
||||
- chapter: 4
|
||||
datetime: '2026-01-30T19:50:00Z'
|
||||
page_end: 79
|
||||
page_start: 78
|
||||
pos0: epubcfi(/6/4/2:20)
|
||||
pos1: epubcfi(/6/4/2:30)
|
||||
text: Important passage
|
||||
last_read: '2026-01-30T20:00:00Z'
|
||||
notes:
|
||||
- chapter: 4
|
||||
datetime: '2026-01-30T19:52:00Z'
|
||||
text: My note about this chapter
|
||||
percentage: 0.6
|
||||
progress: 0.6
|
||||
title: Book Title
|
||||
uuid: '{{media_item_id}}'
|
||||
library_id: optional-library-uuid
|
||||
type: json
|
||||
data: "{\n \"books\": [\n {\n \"authors\": [\n \"Author Name\"\n ],\n \"highlights\": [\n {\n \"chapter\": 4,\n \"datetime\": \"2026-01-30T19:50:00Z\",\n \"page_end\": 79,\n \"page_start\": 78,\n \"pos0\": \"epubcfi(/6/4/2:20)\",\n \"pos1\": \"epubcfi(/6/4/2:30)\",\n \"text\": \"Important passage\"\n }\n ],\n \"last_read\": \"2026-01-30T20:00:00Z\",\n \"notes\": [\n {\n \"chapter\": 4,\n \"datetime\": \"2026-01-30T19:52:00Z\",\n \"text\": \"My note about this chapter\"\n }\n ],\n \"percentage\": 0.6,\n \"progress\": 0.6,\n \"title\": \"Book Title\",\n \"uuid\": \"{{media_item_id}}\"\n }\n ],\n \"library_id\": \"optional-library-uuid\"\n}"
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
|
||||
@@ -2,8 +2,8 @@ docs: "## KOReader Sync Progress\n\n Synchronizes reading progress from a KORea
|
||||
\ model identifier\n\n **Response:**\n - `sync_status` (string): Sync status (accepted, processing)\n - `books_synced` (number): Number of books synced\n\n **Status Codes:**\n - 202: Accepted - sync queued\n - 401: Unauthorized\n - 500: Internal server error"
|
||||
http:
|
||||
body:
|
||||
jsonBody: "{\n \"library_id\": null,\n \"books\": [\n {\n \"uuid\": \"{{book_uuid"
|
||||
type: json
|
||||
data: "{\n \"sync_mode\": \"immediate\",\n \"books\": [\n {\n \"uuid\": \"{{media_item_id}}\",\n \"percentage\": 0.45,\n \"chapter\": 3\n }\n ]\n}"
|
||||
headers: []
|
||||
method: POST
|
||||
url: '{{base_url}}/api/sync/koreader/progress'
|
||||
|
||||
@@ -8,16 +8,8 @@ http:
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"media_item_ids\": [\n \"{{bookUUID1"
|
||||
data: "{\n \"media_item_ids\": [\n \"{{bookUUID1"
|
||||
headers:
|
||||
- key: Authorization
|
||||
value: Bearer {{userToken
|
||||
|
||||
docs: |-
|
||||
## Add Books to Kobo Shelf
|
||||
|
||||
Add one or more books to a Kobo device shelf. Manages which books should be synced to a specific Kobo device. Supports multiple shelves for organization.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/devices/{deviceID
|
||||
docs: "## Add Books to Kobo Shelf\n\n Add one or more books to a Kobo device shelf. Manages which books should be synced to a specific Kobo device. Supports multiple shelves for organization.\n\n **Method:** POST\n\n **Endpoint:** /api/devices/{deviceID"
|
||||
@@ -2,51 +2,15 @@ info:
|
||||
name: Check Pending Registration
|
||||
type: http
|
||||
seq: 4
|
||||
|
||||
http:
|
||||
method: POST
|
||||
url: '{{base_url}}/api/devices/register/status'
|
||||
auth: none
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
body:
|
||||
type: json
|
||||
json:
|
||||
registration_id: registration-uuid-here
|
||||
|
||||
docs: |-
|
||||
## Check Registration Status
|
||||
|
||||
Checks the current status of a device registration request.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/devices/register/status
|
||||
|
||||
**Authentication:** None
|
||||
|
||||
**Request Body:**
|
||||
- `registration_id` (string): UUID received from registration request
|
||||
|
||||
**Response:**
|
||||
- `registration_id` (string): The registration UUID
|
||||
- `status` (string): Current status
|
||||
- `pending`: Awaiting user approval
|
||||
- `approved`: Registration approved, device ready
|
||||
- `rejected`: Registration rejected by user
|
||||
- `expired`: Registration expired (not approved in time)
|
||||
- `device_name` (string): Name of the device
|
||||
- `device_type` (string): Type of device
|
||||
- `created_at` (string): Registration timestamp
|
||||
- `updated_at` (string): Last status update timestamp
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Status retrieved successfully
|
||||
- 404: Registration ID not found
|
||||
- 400: Invalid registration ID format
|
||||
|
||||
**Polling Recommendations:**
|
||||
- Poll every 5-10 seconds while status is `pending`
|
||||
- Stop polling when status changes to `approved`, `rejected`, or `expired`
|
||||
- Use exponential backoff for mobile devices to save battery
|
||||
data: "{\n \"registration_id\": \"registration-uuid-here\"\n}"
|
||||
docs: "## Check Registration Status\n\nChecks the current status of a device registration request.\n\n**Method:** POST\n\n**Endpoint:** /api/devices/register/status\n\n**Authentication:** None\n\n**Request Body:**\n- `registration_id` (string): UUID received from registration request\n\n**Response:**\n- `registration_id` (string): The registration UUID\n- `status` (string): Current status\n - `pending`: Awaiting user approval\n - `approved`: Registration approved, device ready\n - `rejected`: Registration rejected by user\n - `expired`: Registration expired (not approved in time)\n- `device_name` (string): Name of the device\n- `device_type` (string): Type of device\n- `created_at` (string): Registration timestamp\n- `updated_at` (string): Last status update timestamp\n\n**Status Codes:**\n- 200: Status retrieved successfully\n- 404: Registration ID not found\n- 400: Invalid registration ID format\n\n**Polling Recommendations:**\n- Poll every 5-10 seconds while status is `pending`\n\
|
||||
- Stop polling when status changes to `approved`, `rejected`, or `expired`\n- Use exponential backoff for mobile devices to save battery"
|
||||
@@ -8,30 +8,5 @@ http:
|
||||
auth: none
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"registration_id\": \"{{registrationId}}"
|
||||
|
||||
docs: |-
|
||||
## Check Registration Status
|
||||
|
||||
Checks the current status of a device registration request.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/devices/register/status
|
||||
|
||||
**Authentication:** None
|
||||
|
||||
**Request Body:**
|
||||
- `registration_id` (string, required): Registration request ID
|
||||
|
||||
**Response:**
|
||||
- `status` (string): Registration status (pending, approved, rejected, expired)
|
||||
- `device_name` (string): Device name
|
||||
- `device_type` (string): Device type
|
||||
- `created_at` (string): Request timestamp
|
||||
- `expires_at` (string): Expiration timestamp
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 404: Registration ID not found
|
||||
- 410: Registration expired
|
||||
data: "{\n \"registration_id\": \"{{registrationId}}"
|
||||
docs: "## Check Registration Status\n\n Checks the current status of a device registration request.\n\n **Method:** POST\n\n **Endpoint:** /api/devices/register/status\n\n **Authentication:** None\n\n **Request Body:**\n - `registration_id` (string, required): Registration request ID\n\n **Response:**\n - `status` (string): Registration status (pending, approved, rejected, expired)\n - `device_name` (string): Device name\n - `device_type` (string): Device type\n - `created_at` (string): Request timestamp\n - `expires_at` (string): Expiration timestamp\n\n **Status Codes:**\n - 200: Success\n - 404: Registration ID not found\n - 410: Registration expired"
|
||||
@@ -8,48 +8,12 @@ http:
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"file_path\": \"/mnt/sd/books/my-book.kepub.epub\",\n \"\
|
||||
media_item_id\": \"{{media_item_id"
|
||||
data: "{\n \"file_path\": \"/mnt/sd/books/my-book.kepub.epub\",\n \"media_item_id\": \"{{media_item_id"
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
runtime:
|
||||
scripts:
|
||||
- type: tests
|
||||
code: "test_create_device_file_alias_success(status, headers, body) {\n if\
|
||||
\ (status !== 201 && status !== 200) {\n throw new Error(\"Expected status\
|
||||
\ 201 or 200, got \" + status);"
|
||||
|
||||
docs: |-
|
||||
## Create Device File Alias
|
||||
|
||||
Creates a new file alias for a device. File aliases map device-specific file paths to media items.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/devices/:id/file-aliases
|
||||
|
||||
**Authentication:** Required (Bearer token)
|
||||
|
||||
**Path Parameters:**
|
||||
- `id` (string, required): Device UUID
|
||||
|
||||
**Request Body:**
|
||||
- `file_path` (string, required): Device-specific file path
|
||||
- `media_item_id` (string, required): Media item UUID to link to
|
||||
|
||||
**Response:** Created file alias object
|
||||
- `id` (string): Alias UUID
|
||||
- `device_id` (string): Device UUID
|
||||
- `file_path` (string): Device-specific file path
|
||||
- `media_item_id` (string): Associated media item UUID
|
||||
- `created_at` (string): Creation timestamp
|
||||
- `updated_at` (string): Last update timestamp
|
||||
|
||||
**Status Codes:**
|
||||
- 201: Created
|
||||
- 200: Success
|
||||
- 400: Invalid request body
|
||||
- 401: Unauthorized
|
||||
- 404: Device not found
|
||||
- 500: Internal server error
|
||||
code: "test_create_device_file_alias_success(status, headers, body) {\n if (status !== 201 && status !== 200) {\n throw new Error(\"Expected status 201 or 200, got \" + status);"
|
||||
docs: "## Create Device File Alias\n\n Creates a new file alias for a device. File aliases map device-specific file paths to media items.\n\n **Method:** POST\n\n **Endpoint:** /api/devices/:id/file-aliases\n\n **Authentication:** Required (Bearer token)\n\n **Path Parameters:**\n - `id` (string, required): Device UUID\n\n **Request Body:**\n - `file_path` (string, required): Device-specific file path\n - `media_item_id` (string, required): Media item UUID to link to\n\n **Response:** Created file alias object\n - `id` (string): Alias UUID\n - `device_id` (string): Device UUID\n - `file_path` (string): Device-specific file path\n - `media_item_id` (string): Associated media item UUID\n - `created_at` (string): Creation timestamp\n - `updated_at` (string): Last update timestamp\n\n **Status Codes:**\n - 201: Created\n - 200: Success\n - 400: Invalid request body\n - 401: Unauthorized\n - 404: Device not found\n - 500: Internal server error"
|
||||
@@ -2,57 +2,77 @@ info:
|
||||
name: Disable Device Sync
|
||||
type: http
|
||||
seq: 8
|
||||
|
||||
http:
|
||||
method: PUT
|
||||
url: '{{base_url}}/api/devices/{{device_id}}'
|
||||
auth: inherit
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
body:
|
||||
type: json
|
||||
json:
|
||||
device_name: My Kobo Clara
|
||||
sync_enabled: false
|
||||
auto_sync: false
|
||||
sync_frequency_minutes: 30
|
||||
data: "{\n \"device_name\": \"My Kobo Clara\",\n \"sync_enabled\": false,\n \"auto_sync\": false,\n \"sync_frequency_minutes\": 30\n}"
|
||||
docs: '## Disable Device Sync
|
||||
|
||||
docs: |-
|
||||
## Disable Device Sync
|
||||
|
||||
Disables synchronization for a specific device.
|
||||
|
||||
|
||||
**Method:** PUT
|
||||
|
||||
|
||||
**Endpoint:** /api/devices/:device_id
|
||||
|
||||
|
||||
**Authentication:** Required (Bearer token)
|
||||
|
||||
|
||||
**Path Parameters:**
|
||||
|
||||
- `device_id` (string): UUID of the device
|
||||
|
||||
|
||||
**Request Body:**
|
||||
|
||||
- `device_name` (string, optional): Device name
|
||||
|
||||
- `sync_enabled` (boolean): Must be `false`
|
||||
|
||||
- `auto_sync` (boolean): Should be `false`
|
||||
|
||||
- `sync_frequency_minutes` (integer, optional): Any value (sync disabled)
|
||||
|
||||
|
||||
**Response:**
|
||||
|
||||
- `id` (string): Device UUID
|
||||
|
||||
- `device_name` (string): Device name
|
||||
|
||||
- `sync_enabled` (boolean): `false`
|
||||
|
||||
- `auto_sync` (boolean): `false`
|
||||
|
||||
- `message` (string): Confirmation message
|
||||
|
||||
|
||||
**Status Codes:**
|
||||
|
||||
- 200: Sync disabled successfully
|
||||
|
||||
- 401: Unauthorized
|
||||
|
||||
- 403: Forbidden
|
||||
|
||||
- 404: Device not found
|
||||
|
||||
|
||||
**Use Cases:**
|
||||
|
||||
- Temporarily disable sync for troubleshooting
|
||||
|
||||
- Stop sync for a lost or stolen device
|
||||
|
||||
- Disable sync before selling or giving away device
|
||||
- Prevent data usage on limited connections
|
||||
|
||||
- Prevent data usage on limited connections'
|
||||
@@ -8,32 +8,5 @@ http:
|
||||
auth: none
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"device_name\": \"My Kindle Paperwhite\",\n \"device_type\"\
|
||||
: \"koreader\",\n \"device_identifier\": \"kindle-pw5-hardware-id-12345\""
|
||||
|
||||
docs: |-
|
||||
## Initiate Device Registration
|
||||
|
||||
Initiates a new device registration request that requires admin approval.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/devices/register
|
||||
|
||||
**Authentication:** None (open endpoint)
|
||||
|
||||
**Request Body:**
|
||||
- `device_name` (string, required): Human-readable device name (max 100 chars)
|
||||
- `device_type` (string, required): Device type (kobo, koreader, kindle, etc.)
|
||||
- `device_identifier` (string, required): Unique hardware ID (max 200 chars)
|
||||
|
||||
**Response:**
|
||||
- `registration_id` (string): Unique registration request ID
|
||||
- `status` (string): Initial status (pending)
|
||||
- `expires_at` (string): Expiration timestamp (usually 24 hours)
|
||||
- `message` (string): Informational message
|
||||
|
||||
**Status Codes:**
|
||||
- 201: Registration initiated
|
||||
- 400: Invalid request
|
||||
- 409: Device already registered
|
||||
data: "{\n \"device_name\": \"My Kindle Paperwhite\",\n \"device_type\": \"koreader\",\n \"device_identifier\": \"kindle-pw5-hardware-id-12345\""
|
||||
docs: "## Initiate Device Registration\n\n Initiates a new device registration request that requires admin approval.\n\n **Method:** POST\n\n **Endpoint:** /api/devices/register\n\n **Authentication:** None (open endpoint)\n\n **Request Body:**\n - `device_name` (string, required): Human-readable device name (max 100 chars)\n - `device_type` (string, required): Device type (kobo, koreader, kindle, etc.)\n - `device_identifier` (string, required): Unique hardware ID (max 200 chars)\n\n **Response:**\n - `registration_id` (string): Unique registration request ID\n - `status` (string): Initial status (pending)\n - `expires_at` (string): Expiration timestamp (usually 24 hours)\n - `message` (string): Informational message\n\n **Status Codes:**\n - 201: Registration initiated\n - 400: Invalid request\n - 409: Device already registered"
|
||||
@@ -2,48 +2,62 @@ info:
|
||||
name: Register Device
|
||||
type: http
|
||||
seq: 1
|
||||
|
||||
http:
|
||||
method: POST
|
||||
url: '{{base_url}}/api/devices/register'
|
||||
auth: none
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
body:
|
||||
type: json
|
||||
json:
|
||||
device_name: My Kobo Clara
|
||||
device_type: kobo
|
||||
device_identifier: N1234567890123
|
||||
data: "{\n \"device_name\": \"My Kobo Clara\",\n \"device_type\": \"kobo\",\n \"device_identifier\": \"N1234567890123\"\n}"
|
||||
docs: '## Register Device
|
||||
|
||||
docs: |-
|
||||
## Register Device
|
||||
|
||||
Initiates device registration by sending device information to the server.
|
||||
|
||||
|
||||
**Method:** POST
|
||||
|
||||
|
||||
**Endpoint:** /api/devices/register
|
||||
|
||||
|
||||
**Authentication:** None (public endpoint)
|
||||
|
||||
|
||||
**Request Body:**
|
||||
|
||||
- `device_name` (string): Human-readable name for the device
|
||||
|
||||
- `device_type` (string): Type of device - `kobo`, `koreader`, or `web`
|
||||
|
||||
- `device_identifier` (string): Unique device identifier (serial number or other ID)
|
||||
|
||||
|
||||
**Response:**
|
||||
|
||||
- `registration_id` (string): UUID for tracking registration status
|
||||
|
||||
- `status` (string): Registration status - `pending`, `approved`, or `rejected`
|
||||
|
||||
- `message` (string): Status message
|
||||
|
||||
|
||||
**Status Codes:**
|
||||
|
||||
- 201: Registration initiated successfully
|
||||
|
||||
- 400: Invalid request data
|
||||
|
||||
- 409: Device already registered
|
||||
|
||||
|
||||
**Notes:**
|
||||
|
||||
- Device registration requires user approval before activation
|
||||
|
||||
- Registration ID should be stored for status checking
|
||||
- Device identifier must be unique per device
|
||||
|
||||
- Device identifier must be unique per device'
|
||||
@@ -2,47 +2,60 @@ info:
|
||||
name: Register KOReader Device
|
||||
type: http
|
||||
seq: 2
|
||||
|
||||
http:
|
||||
method: POST
|
||||
url: '{{base_url}}/api/devices/register'
|
||||
auth: none
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
body:
|
||||
type: json
|
||||
json:
|
||||
device_name: My Kindle Paperwhite
|
||||
device_type: koreader
|
||||
device_identifier: G090GP123456789
|
||||
data: "{\n \"device_name\": \"My Kindle Paperwhite\",\n \"device_type\": \"koreader\",\n \"device_identifier\": \"G090GP123456789\"\n}"
|
||||
docs: '## Register KOReader Device
|
||||
|
||||
docs: |-
|
||||
## Register KOReader Device
|
||||
|
||||
Registers a KOReader device (typically Kindle devices running KOReader software).
|
||||
|
||||
|
||||
**Method:** POST
|
||||
|
||||
|
||||
**Endpoint:** /api/devices/register
|
||||
|
||||
|
||||
**Authentication:** None (public endpoint)
|
||||
|
||||
|
||||
**Request Body:**
|
||||
|
||||
- `device_name` (string): Human-readable name for the device
|
||||
|
||||
- `device_type` (string): Must be `koreader`
|
||||
|
||||
- `device_identifier` (string): Kindle serial number or unique device ID
|
||||
|
||||
|
||||
**Response:**
|
||||
|
||||
- `registration_id` (string): UUID for tracking registration status
|
||||
|
||||
- `status` (string): Registration status - `pending`
|
||||
|
||||
- `message` (string): Confirmation message
|
||||
|
||||
|
||||
**Status Codes:**
|
||||
|
||||
- 201: Registration initiated
|
||||
|
||||
- 400: Invalid device type or data
|
||||
|
||||
|
||||
**KOReader Notes:**
|
||||
|
||||
- KOReader runs on Kindle, Kobo, PocketBook, and Android devices
|
||||
|
||||
- Device identifier is typically the device serial number
|
||||
- Supports bookmarks, progress, highlights, and annotations sync
|
||||
|
||||
- Supports bookmarks, progress, highlights, and annotations sync'
|
||||
@@ -2,48 +2,62 @@ info:
|
||||
name: Register Web Device
|
||||
type: http
|
||||
seq: 3
|
||||
|
||||
http:
|
||||
method: POST
|
||||
url: '{{base_url}}/api/devices/register'
|
||||
auth: none
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
body:
|
||||
type: json
|
||||
json:
|
||||
device_name: Chrome Browser
|
||||
device_type: web
|
||||
device_identifier: web-client-abc123
|
||||
data: "{\n \"device_name\": \"Chrome Browser\",\n \"device_type\": \"web\",\n \"device_identifier\": \"web-client-abc123\"\n}"
|
||||
docs: '## Register Web Device
|
||||
|
||||
docs: |-
|
||||
## Register Web Device
|
||||
|
||||
Registers a web browser client for reading progress sync.
|
||||
|
||||
|
||||
**Method:** POST
|
||||
|
||||
|
||||
**Endpoint:** /api/devices/register
|
||||
|
||||
|
||||
**Authentication:** None (public endpoint)
|
||||
|
||||
|
||||
**Request Body:**
|
||||
|
||||
- `device_name` (string): Browser identification (e.g., "Chrome Browser")
|
||||
|
||||
- `device_type` (string): Must be `web`
|
||||
|
||||
- `device_identifier` (string): Unique browser/client identifier
|
||||
|
||||
|
||||
**Response:**
|
||||
|
||||
- `registration_id` (string): UUID for tracking
|
||||
|
||||
- `status` (string): `pending` or `auto-approved`
|
||||
|
||||
- `message` (string): Status message
|
||||
|
||||
|
||||
**Status Codes:**
|
||||
|
||||
- 201: Registration successful
|
||||
|
||||
- 400: Invalid request
|
||||
|
||||
|
||||
**Web Device Notes:**
|
||||
|
||||
- Web devices use localStorage for device identification
|
||||
|
||||
- May be auto-approved without admin intervention
|
||||
|
||||
- Used for browser-based reading progress tracking
|
||||
- Supports manual progress updates, highlights, and notes
|
||||
|
||||
- Supports manual progress updates, highlights, and notes'
|
||||
@@ -2,60 +2,83 @@ info:
|
||||
name: Update Device Settings
|
||||
type: http
|
||||
seq: 7
|
||||
|
||||
http:
|
||||
method: PUT
|
||||
url: '{{base_url}}/api/devices/{{device_id}}'
|
||||
auth: inherit
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
body:
|
||||
type: json
|
||||
json:
|
||||
device_name: Updated Device Name
|
||||
sync_enabled: true
|
||||
auto_sync: true
|
||||
sync_frequency_minutes: 10
|
||||
data: "{\n \"device_name\": \"Updated Device Name\",\n \"sync_enabled\": true,\n \"auto_sync\": true,\n \"sync_frequency_minutes\": 10\n}"
|
||||
docs: '## Update Device Settings
|
||||
|
||||
docs: |-
|
||||
## Update Device Settings
|
||||
|
||||
Updates device configuration and sync settings.
|
||||
|
||||
|
||||
**Method:** PUT
|
||||
|
||||
|
||||
**Endpoint:** /api/devices/:device_id
|
||||
|
||||
|
||||
**Authentication:** Required (Bearer token)
|
||||
|
||||
|
||||
**Path Parameters:**
|
||||
|
||||
- `device_id` (string): UUID of the device
|
||||
|
||||
|
||||
**Request Body:**
|
||||
|
||||
- `device_name` (string, optional): New device name
|
||||
|
||||
- `sync_enabled` (boolean, optional): Enable/disable sync
|
||||
|
||||
- `auto_sync` (boolean, optional): Enable automatic sync
|
||||
|
||||
- `sync_frequency_minutes` (integer, optional): Sync interval (5-1440 minutes)
|
||||
|
||||
|
||||
**Response:**
|
||||
|
||||
- `id` (string): Device UUID
|
||||
|
||||
- `device_name` (string): Updated device name
|
||||
|
||||
- `sync_enabled` (boolean): Updated sync status
|
||||
|
||||
- `auto_sync` (boolean): Updated auto-sync setting
|
||||
|
||||
- `sync_frequency_minutes` (integer): Updated sync interval
|
||||
|
||||
- `updated_at` (string): Update timestamp
|
||||
|
||||
|
||||
**Status Codes:**
|
||||
|
||||
- 200: Settings updated successfully
|
||||
|
||||
- 400: Invalid sync frequency or data
|
||||
|
||||
- 401: Unauthorized
|
||||
|
||||
- 403: Forbidden (device belongs to different user)
|
||||
|
||||
- 404: Device not found
|
||||
|
||||
|
||||
**Sync Frequency Guidelines:**
|
||||
|
||||
- Mobile devices: 10-30 minutes
|
||||
|
||||
- E-readers on WiFi: 15-60 minutes
|
||||
|
||||
- Web clients: 5-15 minutes
|
||||
|
||||
- Minimum: 5 minutes
|
||||
- Maximum: 1440 minutes (24 hours)
|
||||
|
||||
- Maximum: 1440 minutes (24 hours)'
|
||||
@@ -8,35 +8,5 @@ http:
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"device_name\": \"My Updated Kindle\",\n \"sync_enabled\"\
|
||||
: true,\n \"auto_sync\": true,\n \"sync_frequency_minutes\": 10"
|
||||
|
||||
docs: |-
|
||||
## Update Device
|
||||
|
||||
Updates device settings and synchronization preferences.
|
||||
|
||||
**Method:** PUT
|
||||
|
||||
**Endpoint:** /api/devices/{device_id}
|
||||
|
||||
**Authentication:** Required (Bearer token, admin or device owner)
|
||||
|
||||
**Path Parameters:**
|
||||
- `device_id` (string): Device ID
|
||||
|
||||
**Request Body:**
|
||||
- `device_name` (string, optional): New device name (max 100 chars)
|
||||
- `sync_enabled` (boolean, optional): Enable/disable synchronization
|
||||
- `auto_sync` (boolean, optional): Enable automatic synchronization
|
||||
- `sync_frequency_minutes` (integer, optional): Sync frequency in minutes (5-1440)
|
||||
|
||||
**Response:**
|
||||
- Updated device object
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 400: Invalid request
|
||||
- 401: Unauthorized
|
||||
- 403: Forbidden (not device owner)
|
||||
- 404: Device not found
|
||||
data: "{\n \"device_name\": \"My Updated Kindle\",\n \"sync_enabled\": true,\n \"auto_sync\": true,\n \"sync_frequency_minutes\": 10"
|
||||
docs: "## Update Device\n\n Updates device settings and synchronization preferences.\n\n **Method:** PUT\n\n **Endpoint:** /api/devices/{device_id}\n\n **Authentication:** Required (Bearer token, admin or device owner)\n\n **Path Parameters:**\n - `device_id` (string): Device ID\n\n **Request Body:**\n - `device_name` (string, optional): New device name (max 100 chars)\n - `sync_enabled` (boolean, optional): Enable/disable synchronization\n - `auto_sync` (boolean, optional): Enable automatic synchronization\n - `sync_frequency_minutes` (integer, optional): Sync frequency in minutes (5-1440)\n\n **Response:**\n - Updated device object\n\n **Status Codes:**\n - 200: Success\n - 400: Invalid request\n - 401: Unauthorized\n - 403: Forbidden (not device owner)\n - 404: Device not found"
|
||||
@@ -2,65 +2,92 @@ info:
|
||||
name: Update Sync Frequency
|
||||
type: http
|
||||
seq: 9
|
||||
|
||||
http:
|
||||
method: PUT
|
||||
url: '{{base_url}}/api/devices/{{device_id}}'
|
||||
auth: inherit
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
body:
|
||||
type: json
|
||||
json:
|
||||
device_name: My Kobo Clara
|
||||
sync_enabled: true
|
||||
auto_sync: true
|
||||
sync_frequency_minutes: 15
|
||||
data: "{\n \"device_name\": \"My Kobo Clara\",\n \"sync_enabled\": true,\n \"auto_sync\": true,\n \"sync_frequency_minutes\": 15\n}"
|
||||
docs: '## Update Sync Frequency
|
||||
|
||||
docs: |-
|
||||
## Update Sync Frequency
|
||||
|
||||
Changes how often the device automatically syncs with the server.
|
||||
|
||||
|
||||
**Method:** PUT
|
||||
|
||||
|
||||
**Endpoint:** /api/devices/:device_id
|
||||
|
||||
|
||||
**Authentication:** Required (Bearer token)
|
||||
|
||||
|
||||
**Path Parameters:**
|
||||
|
||||
- `device_id` (string): UUID of the device
|
||||
|
||||
|
||||
**Request Body:**
|
||||
|
||||
- `device_name` (string, optional): Device name
|
||||
|
||||
- `sync_enabled` (boolean): Must be `true` for auto-sync
|
||||
|
||||
- `auto_sync` (boolean): Must be `true`
|
||||
|
||||
- `sync_frequency_minutes` (integer): New sync interval (5-1440)
|
||||
|
||||
|
||||
**Response:**
|
||||
|
||||
- `id` (string): Device UUID
|
||||
|
||||
- `sync_enabled` (boolean): `true`
|
||||
|
||||
- `auto_sync` (boolean): `true`
|
||||
|
||||
- `sync_frequency_minutes` (integer): Updated interval
|
||||
|
||||
- `next_sync_at` (string): Estimated next sync time
|
||||
|
||||
- `updated_at` (string): Update timestamp
|
||||
|
||||
|
||||
**Status Codes:**
|
||||
|
||||
- 200: Frequency updated successfully
|
||||
|
||||
- 400: Invalid frequency value
|
||||
|
||||
- 401: Unauthorized
|
||||
|
||||
- 403: Forbidden
|
||||
|
||||
- 404: Device not found
|
||||
|
||||
|
||||
**Recommended Frequencies:**
|
||||
|
||||
- **Active reading:** 5-10 minutes
|
||||
|
||||
- **Normal usage:** 15-30 minutes
|
||||
|
||||
- **Occasional reading:** 30-60 minutes
|
||||
|
||||
- **Battery saving:** 60+ minutes
|
||||
|
||||
- **WiFi only (3G):** 30-60 minutes
|
||||
|
||||
|
||||
**Notes:**
|
||||
|
||||
- More frequent sync = more battery usage
|
||||
|
||||
- Sync only occurs when device is online
|
||||
- Manual sync can be triggered anytime regardless of frequency
|
||||
|
||||
- Manual sync can be triggered anytime regardless of frequency'
|
||||
@@ -40,3 +40,7 @@ variables:
|
||||
name: comic_library_id
|
||||
- secret: true
|
||||
name: manga_library_id
|
||||
- secret: true
|
||||
name: book_sha256
|
||||
- secret: true
|
||||
name: book_uuid
|
||||
@@ -8,107 +8,7 @@ http:
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"name\": \"My Sci-Fi Books\",\n \"resource_type\":\
|
||||
\ \"media-items\",\n \"filters\": {\n \"genre_filter\": \"Science Fiction\"\
|
||||
,\n \"sort\": \"title ASC\",\n \"author_filter\": \"\"\n }\n\
|
||||
}"
|
||||
|
||||
docs: |-
|
||||
## Create Saved Filter
|
||||
|
||||
Creates a new saved filter for the authenticated user.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/saved-filters
|
||||
|
||||
**Authentication:** Required (Bearer token)
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"name": "My Sci-Fi Books",
|
||||
"resource_type": "media-items",
|
||||
"filters": {
|
||||
"genre_filter": "Science Fiction",
|
||||
"sort": "title ASC",
|
||||
"author_filter": "",
|
||||
"series_filter": ""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Required Fields:**
|
||||
- `name` (string, required): Filter name
|
||||
- Must be unique per user + resource type combination
|
||||
- Maximum 100 characters
|
||||
- Cannot be empty
|
||||
- `resource_type` (string, required): Type of resource to filter
|
||||
- Examples: "media-items", "collections", "devices"
|
||||
- Must match valid resource types
|
||||
- `filters` (object, required): Key-value pairs of filter criteria
|
||||
- Flexible structure - any valid filter fields
|
||||
- Serialized as JSONB in database
|
||||
|
||||
**Common Filter Fields for media-items:**
|
||||
- `search`: General search term
|
||||
- `author_filter`: Filter by author name
|
||||
- `genre_filter`: Filter by genre
|
||||
- `series_filter`: Filter by series
|
||||
- `language_filter`: Filter by language
|
||||
- `year_min`: Minimum copyright year
|
||||
- `year_max`: Maximum copyright year
|
||||
- `has_cover`: Boolean for cover image
|
||||
- `sort`: Sort order (e.g., "title ASC", "created_at DESC")
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "My Sci-Fi Books",
|
||||
"resource_type": "media-items",
|
||||
"filters": {
|
||||
"genre_filter": "Science Fiction",
|
||||
"sort": "title ASC"
|
||||
},
|
||||
"created_at": "2024-03-20T12:00:00Z",
|
||||
"updated_at": "2024-03-20T12:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**Status Codes:**
|
||||
- 201: Created - Filter successfully created
|
||||
- 400: Bad Request - Invalid request body or missing required fields
|
||||
- 401: Unauthorized - Invalid or missing authentication token
|
||||
- 409: Conflict - Filter with this name already exists for this resource type
|
||||
|
||||
**Error Response Examples:**
|
||||
|
||||
Duplicate name error:
|
||||
```json
|
||||
{
|
||||
"error": "filter with name 'My Sci-Fi Books' already exists for this resource type"
|
||||
}
|
||||
```
|
||||
|
||||
**Example Usage:**
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "My Sci-Fi Books",
|
||||
"resource_type": "media-items",
|
||||
"filters": {
|
||||
"genre_filter": "Science Fiction",
|
||||
"sort": "title ASC"
|
||||
}
|
||||
}' \
|
||||
"{{base_url}}/api/saved-filters"
|
||||
```
|
||||
|
||||
**Validation Rules:**
|
||||
- Name must be unique per user + resource type
|
||||
- Name cannot be empty or whitespace only
|
||||
- Resource type must be valid
|
||||
- Filters object must be provided (can be empty object {})
|
||||
data: "{\n \"name\": \"My Sci-Fi Books\",\n \"resource_type\": \"media-items\",\n \"filters\": {\n \"genre_filter\": \"Science Fiction\",\n \"sort\": \"title ASC\",\n \"author_filter\": \"\"\n }\n}"
|
||||
docs: "## Create Saved Filter\n\nCreates a new saved filter for the authenticated user.\n\n**Method:** POST\n\n**Endpoint:** /api/saved-filters\n\n**Authentication:** Required (Bearer token)\n\n**Request Body:**\n```json\n{\n \"name\": \"My Sci-Fi Books\",\n \"resource_type\": \"media-items\",\n \"filters\": {\n \"genre_filter\": \"Science Fiction\",\n \"sort\": \"title ASC\",\n \"author_filter\": \"\",\n \"series_filter\": \"\"\n }\n}\n```\n\n**Required Fields:**\n- `name` (string, required): Filter name\n - Must be unique per user + resource type combination\n - Maximum 100 characters\n - Cannot be empty\n- `resource_type` (string, required): Type of resource to filter\n - Examples: \"media-items\", \"collections\", \"devices\"\n - Must match valid resource types\n- `filters` (object, required): Key-value pairs of filter criteria\n - Flexible structure - any valid filter fields\n - Serialized as JSONB in database\n\n**Common Filter Fields for media-items:**\n- `search`:\
|
||||
\ General search term\n- `author_filter`: Filter by author name\n- `genre_filter`: Filter by genre\n- `series_filter`: Filter by series\n- `language_filter`: Filter by language\n- `year_min`: Minimum copyright year\n- `year_max`: Maximum copyright year\n- `has_cover`: Boolean for cover image\n- `sort`: Sort order (e.g., \"title ASC\", \"created_at DESC\")\n\n**Response:**\n```json\n{\n \"id\": \"550e8400-e29b-41d4-a716-446655440000\",\n \"name\": \"My Sci-Fi Books\",\n \"resource_type\": \"media-items\",\n \"filters\": {\n \"genre_filter\": \"Science Fiction\",\n \"sort\": \"title ASC\"\n },\n \"created_at\": \"2024-03-20T12:00:00Z\",\n \"updated_at\": \"2024-03-20T12:00:00Z\"\n}\n```\n\n**Status Codes:**\n- 201: Created - Filter successfully created\n- 400: Bad Request - Invalid request body or missing required fields\n- 401: Unauthorized - Invalid or missing authentication token\n- 409: Conflict - Filter with this name already exists for this resource type\n\n**Error Response\
|
||||
\ Examples:**\n\nDuplicate name error:\n```json\n{\n \"error\": \"filter with name 'My Sci-Fi Books' already exists for this resource type\"\n}\n```\n\n**Example Usage:**\n```bash\ncurl -X POST \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"My Sci-Fi Books\",\n \"resource_type\": \"media-items\",\n \"filters\": {\n \"genre_filter\": \"Science Fiction\",\n \"sort\": \"title ASC\"\n }\n }' \\\n \"{{base_url}}/api/saved-filters\"\n```\n\n**Validation Rules:**\n- Name must be unique per user + resource type\n- Name cannot be empty or whitespace only\n- Resource type must be valid\n- Filters object must be provided (can be empty object {})"
|
||||
@@ -8,110 +8,7 @@ http:
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"name\": \"Updated Sci-Fi Books\",\n \"resource_type\"\
|
||||
: \"media-items\",\n \"filters\": {\n \"genre_filter\": \"Science Fiction\
|
||||
\ Fantasy\",\n \"sort\": \"author ASC\",\n \"year_min\": \"2000\"\
|
||||
\n }\n }"
|
||||
|
||||
docs: |-
|
||||
## Update Saved Filter
|
||||
|
||||
Updates an existing saved filter's name and/or filter criteria.
|
||||
|
||||
**Method:** PUT
|
||||
|
||||
**Endpoint:** /api/saved-filters/{filter_id}
|
||||
|
||||
**Authentication:** Required (Bearer token)
|
||||
|
||||
**Path Parameters:**
|
||||
- `filter_id` (string, required): UUID of the filter to update
|
||||
- Must be a valid UUID
|
||||
- Must belong to the authenticated user
|
||||
|
||||
**Request Body:**
|
||||
```json
|
||||
{
|
||||
"name": "Updated Sci-Fi Books",
|
||||
"resource_type": "media-items",
|
||||
"filters": {
|
||||
"genre_filter": "Science Fiction Fantasy",
|
||||
"sort": "author ASC",
|
||||
"year_min": "2000",
|
||||
"year_max": "2024"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Required Fields:**
|
||||
- `name` (string, required): New filter name
|
||||
- Must be unique per user + resource type (excluding current filter)
|
||||
- Maximum 100 characters
|
||||
- `resource_type` (string, required): Resource type (must match existing)
|
||||
- Cannot change resource type after creation
|
||||
- `filters` (object, required): Updated filter criteria
|
||||
- Replaces existing filters entirely
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "Updated Sci-Fi Books",
|
||||
"resource_type": "media-items",
|
||||
"filters": {
|
||||
"genre_filter": "Science Fiction Fantasy",
|
||||
"sort": "author ASC",
|
||||
"year_min": "2000",
|
||||
"year_max": "2024"
|
||||
},
|
||||
"created_at": "2024-03-20T12:00:00Z",
|
||||
"updated_at": "2024-03-20T15:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success - Filter updated successfully
|
||||
- 400: Bad Request - Invalid request body or filter ID
|
||||
- 401: Unauthorized - Invalid or missing authentication token
|
||||
- 404: Not Found - Filter doesn't exist or doesn't belong to user
|
||||
- 409: Conflict - New name conflicts with existing filter
|
||||
|
||||
**Error Response Examples:**
|
||||
|
||||
Filter not found:
|
||||
```json
|
||||
{
|
||||
"error": "filter not found or access denied"
|
||||
}
|
||||
```
|
||||
|
||||
Duplicate name:
|
||||
```json
|
||||
{
|
||||
"error": "filter with name 'Updated Sci-Fi Books' already exists for this resource type"
|
||||
}
|
||||
```
|
||||
|
||||
**Example Usage:**
|
||||
```bash
|
||||
curl -X PUT \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Updated Sci-Fi Books",
|
||||
"resource_type": "media-items",
|
||||
"filters": {
|
||||
"genre_filter": "Science Fiction Fantasy",
|
||||
"sort": "author ASC",
|
||||
"year_min": "2000"
|
||||
}
|
||||
}' \
|
||||
"{{base_url}}/api/saved-filters/{{filter_id}}"
|
||||
```
|
||||
|
||||
**Important Notes:**
|
||||
- You can only update filters that belong to you
|
||||
- The `updated_at` timestamp is automatically updated
|
||||
- Cannot change the resource_type (it's immutable)
|
||||
- New name must not conflict with other filters you own for the same resource type
|
||||
- All filter fields are replaced entirely (partial updates not supported)
|
||||
data: "{\n \"name\": \"Updated Sci-Fi Books\",\n \"resource_type\": \"media-items\",\n \"filters\": {\n \"genre_filter\": \"Science Fiction Fantasy\",\n \"sort\": \"author ASC\",\n \"year_min\": \"2000\"\n }\n }"
|
||||
docs: "## Update Saved Filter\n\nUpdates an existing saved filter's name and/or filter criteria.\n\n**Method:** PUT\n\n**Endpoint:** /api/saved-filters/{filter_id}\n\n**Authentication:** Required (Bearer token)\n\n**Path Parameters:**\n- `filter_id` (string, required): UUID of the filter to update\n - Must be a valid UUID\n - Must belong to the authenticated user\n\n**Request Body:**\n```json\n{\n \"name\": \"Updated Sci-Fi Books\",\n \"resource_type\": \"media-items\",\n \"filters\": {\n \"genre_filter\": \"Science Fiction Fantasy\",\n \"sort\": \"author ASC\",\n \"year_min\": \"2000\",\n \"year_max\": \"2024\"\n }\n}\n```\n\n**Required Fields:**\n- `name` (string, required): New filter name\n - Must be unique per user + resource type (excluding current filter)\n - Maximum 100 characters\n- `resource_type` (string, required): Resource type (must match existing)\n - Cannot change resource type after creation\n- `filters` (object, required): Updated filter criteria\n\
|
||||
\ - Replaces existing filters entirely\n\n**Response:**\n```json\n{\n \"id\": \"550e8400-e29b-41d4-a716-446655440000\",\n \"name\": \"Updated Sci-Fi Books\",\n \"resource_type\": \"media-items\",\n \"filters\": {\n \"genre_filter\": \"Science Fiction Fantasy\",\n \"sort\": \"author ASC\",\n \"year_min\": \"2000\",\n \"year_max\": \"2024\"\n },\n \"created_at\": \"2024-03-20T12:00:00Z\",\n \"updated_at\": \"2024-03-20T15:30:00Z\"\n}\n```\n\n**Status Codes:**\n- 200: Success - Filter updated successfully\n- 400: Bad Request - Invalid request body or filter ID\n- 401: Unauthorized - Invalid or missing authentication token\n- 404: Not Found - Filter doesn't exist or doesn't belong to user\n- 409: Conflict - New name conflicts with existing filter\n\n**Error Response Examples:**\n\nFilter not found:\n```json\n{\n \"error\": \"filter not found or access denied\"\n}\n```\n\nDuplicate name:\n```json\n{\n \"error\": \"filter with name 'Updated Sci-Fi Books' already exists\
|
||||
\ for this resource type\"\n}\n```\n\n**Example Usage:**\n```bash\ncurl -X PUT \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Updated Sci-Fi Books\",\n \"resource_type\": \"media-items\",\n \"filters\": {\n \"genre_filter\": \"Science Fiction Fantasy\",\n \"sort\": \"author ASC\",\n \"year_min\": \"2000\"\n }\n }' \\\n \"{{base_url}}/api/saved-filters/{{filter_id}}\"\n```\n\n**Important Notes:**\n- You can only update filters that belong to you\n- The `updated_at` timestamp is automatically updated\n- Cannot change the resource_type (it's immutable)\n- New name must not conflict with other filters you own for the same resource type\n- All filter fields are replaced entirely (partial updates not supported)"
|
||||
@@ -8,238 +8,11 @@ http:
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"name\": \"Complete Workflow Test\",\n \"resource_type\"\
|
||||
: \"media-items\",\n \"filters\": {\n \"genre_filter\": \"Science Fiction\"\
|
||||
,\n \"sort\": \"title ASC\"\n }\n }"
|
||||
|
||||
docs: |-
|
||||
## Complete CRUD Workflow
|
||||
|
||||
End-to-end test demonstrating the complete lifecycle of a saved filter: Create → Read → Update → Delete.
|
||||
|
||||
**Workflow Steps:**
|
||||
|
||||
**Step 1: CREATE a new filter**
|
||||
```bash
|
||||
CREATE_RESPONSE=$(curl -s -X POST \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Complete Workflow Test",
|
||||
"resource_type": "media-items",
|
||||
"filters": {
|
||||
"genre_filter": "Science Fiction",
|
||||
"sort": "title ASC"
|
||||
}
|
||||
}' \
|
||||
"{{base_url}}/api/saved-filters")
|
||||
|
||||
echo "Create Response:"
|
||||
echo $CREATE_RESPONSE | jq '.'
|
||||
|
||||
# Extract filter ID for subsequent requests
|
||||
FILTER_ID=$(echo $CREATE_RESPONSE | jq -r '.id')
|
||||
echo "Created filter ID: $FILTER_ID"
|
||||
|
||||
# Expected Status: 201 Created
|
||||
# Expected Response:
|
||||
# {
|
||||
# "id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
# "name": "Complete Workflow Test",
|
||||
# "resource_type": "media-items",
|
||||
# "filters": {
|
||||
# "genre_filter": "Science Fiction",
|
||||
# "sort": "title ASC"
|
||||
# },
|
||||
# "created_at": "2024-03-20T12:00:00Z",
|
||||
# "updated_at": "2024-03-20T12:00:00Z"
|
||||
# }
|
||||
```
|
||||
|
||||
**Step 2: READ the created filter (list all)**
|
||||
```bash
|
||||
# List all filters for media-items
|
||||
LIST_RESPONSE=$(curl -s \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
"{{base_url}}/api/saved-filters?resource_type=media-items")
|
||||
|
||||
echo "List Response:"
|
||||
echo $LIST_RESPONSE | jq '.'
|
||||
|
||||
# Verify our filter is in the list
|
||||
FILTER_EXISTS=$(echo $LIST_RESPONSE | jq --arg id "$FILTER_ID" \
|
||||
'.[] | select(.id == $id) | .id')
|
||||
|
||||
if [ "$FILTER_EXISTS" == "$FILTER_ID" ]; then
|
||||
echo "✓ Filter found in list"
|
||||
else
|
||||
echo "✗ Filter NOT found in list"
|
||||
fi
|
||||
|
||||
# Expected Status: 200 OK
|
||||
# Expected Response: Array containing our newly created filter
|
||||
```
|
||||
|
||||
**Step 3: UPDATE the filter**
|
||||
```bash
|
||||
# Update filter name and criteria
|
||||
UPDATE_RESPONSE=$(curl -s -X PUT \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Complete Workflow Test - Updated",
|
||||
"resource_type": "media-items",
|
||||
"filters": {
|
||||
"genre_filter": "Science Fiction Fantasy",
|
||||
"sort": "author ASC",
|
||||
"year_min": "2000"
|
||||
}
|
||||
}' \
|
||||
"{{base_url}}/api/saved-filters/$FILTER_ID")
|
||||
|
||||
echo "Update Response:"
|
||||
echo $UPDATE_RESPONSE | jq '.'
|
||||
|
||||
# Verify updated_at timestamp changed
|
||||
UPDATED_AT=$(echo $UPDATE_RESPONSE | jq -r '.updated_at')
|
||||
echo "Filter updated at: $UPDATED_AT"
|
||||
|
||||
# Expected Status: 200 OK
|
||||
# Expected Response:
|
||||
# {
|
||||
# "id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
# "name": "Complete Workflow Test - Updated",
|
||||
# "resource_type": "media-items",
|
||||
# "filters": {
|
||||
# "genre_filter": "Science Fiction Fantasy",
|
||||
# "sort": "author ASC",
|
||||
# "year_min": "2000"
|
||||
# },
|
||||
# "created_at": "2024-03-20T12:00:00Z",
|
||||
# "updated_at": "2024-03-20T12:05:00Z" # NEW TIMESTAMP
|
||||
# }
|
||||
```
|
||||
|
||||
**Step 4: DELETE the filter**
|
||||
```bash
|
||||
# Delete the filter
|
||||
DELETE_RESPONSE=$(curl -s -X DELETE \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
"{{base_url}}/api/saved-filters/$FILTER_ID")
|
||||
|
||||
echo "Delete Response:"
|
||||
echo $DELETE_RESPONSE
|
||||
|
||||
# Expected Status: 204 No Content
|
||||
# Expected Response: (empty body)
|
||||
if [ -z "$DELETE_RESPONSE" ]; then
|
||||
echo "✓ Filter deleted successfully (empty response)"
|
||||
fi
|
||||
```
|
||||
|
||||
**Step 5: VERIFY deletion**
|
||||
```bash
|
||||
# Try to get the deleted filter
|
||||
GET_RESPONSE=$(curl -s \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
"{{base_url}}/api/saved-filters?resource_type=media-items")
|
||||
|
||||
echo "List After Delete:"
|
||||
echo $GET_RESPONSE | jq '.'
|
||||
|
||||
# Verify filter is NOT in the list
|
||||
FILTER_EXISTS=$(echo $GET_RESPONSE | jq --arg id "$FILTER_ID" \
|
||||
'.[] | select(.id == $id) | .id')
|
||||
|
||||
if [ -z "$FILTER_EXISTS" ]; then
|
||||
echo "✓ Filter successfully deleted (not found in list)"
|
||||
else
|
||||
echo "✗ Filter still exists (deletion failed)"
|
||||
fi
|
||||
|
||||
# Expected: Filter no longer appears in list
|
||||
```
|
||||
|
||||
**Shell Script Complete Example:**
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
BASE_URL="{{base_url}}"
|
||||
TOKEN="YOUR_TOKEN"
|
||||
|
||||
echo "=== Complete CRUD Workflow Test ==="
|
||||
echo ""
|
||||
|
||||
# 1. CREATE
|
||||
echo "Step 1: Creating filter..."
|
||||
FILTER_ID=$(curl -s -X POST \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Workflow Test",
|
||||
"resource_type": "media-items",
|
||||
"filters": {"genre_filter": "Science Fiction"}
|
||||
}' \
|
||||
"$BASE_URL/api/saved-filters" | jq -r '.id')
|
||||
|
||||
echo "✓ Created filter: $FILTER_ID"
|
||||
echo ""
|
||||
|
||||
# 2. READ
|
||||
echo "Step 2: Reading filters..."
|
||||
curl -s -H "Authorization: Bearer $TOKEN" \
|
||||
"$BASE_URL/api/saved-filters?resource_type=media-items" | \
|
||||
jq --arg id "$FILTER_ID" '.[] | select(.id == $id)'
|
||||
|
||||
echo "✓ Filter retrieved"
|
||||
echo ""
|
||||
|
||||
# 3. UPDATE
|
||||
echo "Step 3: Updating filter..."
|
||||
curl -s -X PUT \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Workflow Test - Updated",
|
||||
"resource_type": "media-items",
|
||||
"filters": {"genre_filter": "Fantasy"}
|
||||
}' \
|
||||
"$BASE_URL/api/saved-filters/$FILTER_ID" | jq '.'
|
||||
|
||||
echo "✓ Filter updated"
|
||||
echo ""
|
||||
|
||||
# 4. DELETE
|
||||
echo "Step 4: Deleting filter..."
|
||||
curl -s -X DELETE \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
"$BASE_URL/api/saved-filters/$FILTER_ID" -w "\nStatus: %{http_code}\n"
|
||||
|
||||
echo "✓ Filter deleted"
|
||||
echo ""
|
||||
|
||||
# 5. VERIFY
|
||||
echo "Step 5: Verifying deletion..."
|
||||
COUNT=$(curl -s -H "Authorization: Bearer $TOKEN" \
|
||||
"$BASE_URL/api/saved-filters?resource_type=media-items" | \
|
||||
jq --arg id "$FILTER_ID" '[.[] | select(.id == $id)] | length')
|
||||
|
||||
if [ "$COUNT" -eq 0 ]; then
|
||||
echo "✓ Verification successful - filter deleted"
|
||||
else
|
||||
echo "✗ Verification failed - filter still exists"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== All CRUD operations completed successfully ==="
|
||||
```
|
||||
|
||||
**Success Criteria:**
|
||||
- ✅ Create returns 201 with generated ID
|
||||
- ✅ Read returns filter in list
|
||||
- ✅ Update modifies filter and updates timestamp
|
||||
- ✅ Delete returns 204 No Content
|
||||
- ✅ Filter no longer appears in list after deletion
|
||||
data: "{\n \"name\": \"Complete Workflow Test\",\n \"resource_type\": \"media-items\",\n \"filters\": {\n \"genre_filter\": \"Science Fiction\",\n \"sort\": \"title ASC\"\n }\n }"
|
||||
docs: "## Complete CRUD Workflow\n\nEnd-to-end test demonstrating the complete lifecycle of a saved filter: Create → Read → Update → Delete.\n\n**Workflow Steps:**\n\n**Step 1: CREATE a new filter**\n```bash\nCREATE_RESPONSE=$(curl -s -X POST \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Complete Workflow Test\",\n \"resource_type\": \"media-items\",\n \"filters\": {\n \"genre_filter\": \"Science Fiction\",\n \"sort\": \"title ASC\"\n }\n }' \\\n \"{{base_url}}/api/saved-filters\")\n\necho \"Create Response:\"\necho $CREATE_RESPONSE | jq '.'\n\n# Extract filter ID for subsequent requests\nFILTER_ID=$(echo $CREATE_RESPONSE | jq -r '.id')\necho \"Created filter ID: $FILTER_ID\"\n\n# Expected Status: 201 Created\n# Expected Response:\n# {\n# \"id\": \"550e8400-e29b-41d4-a716-446655440000\",\n# \"name\": \"Complete Workflow Test\",\n# \"resource_type\": \"media-items\",\n# \"filters\": {\n#\
|
||||
\ \"genre_filter\": \"Science Fiction\",\n# \"sort\": \"title ASC\"\n# },\n# \"created_at\": \"2024-03-20T12:00:00Z\",\n# \"updated_at\": \"2024-03-20T12:00:00Z\"\n# }\n```\n\n**Step 2: READ the created filter (list all)**\n```bash\n# List all filters for media-items\nLIST_RESPONSE=$(curl -s \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n \"{{base_url}}/api/saved-filters?resource_type=media-items\")\n\necho \"List Response:\"\necho $LIST_RESPONSE | jq '.'\n\n# Verify our filter is in the list\nFILTER_EXISTS=$(echo $LIST_RESPONSE | jq --arg id \"$FILTER_ID\" \\\n '.[] | select(.id == $id) | .id')\n\nif [ \"$FILTER_EXISTS\" == \"$FILTER_ID\" ]; then\n echo \"✓ Filter found in list\"\nelse\n echo \"✗ Filter NOT found in list\"\nfi\n\n# Expected Status: 200 OK\n# Expected Response: Array containing our newly created filter\n```\n\n**Step 3: UPDATE the filter**\n```bash\n# Update filter name and criteria\nUPDATE_RESPONSE=$(curl -s -X PUT \\\n -H \"Authorization: Bearer\
|
||||
\ YOUR_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Complete Workflow Test - Updated\",\n \"resource_type\": \"media-items\",\n \"filters\": {\n \"genre_filter\": \"Science Fiction Fantasy\",\n \"sort\": \"author ASC\",\n \"year_min\": \"2000\"\n }\n }' \\\n \"{{base_url}}/api/saved-filters/$FILTER_ID\")\n\necho \"Update Response:\"\necho $UPDATE_RESPONSE | jq '.'\n\n# Verify updated_at timestamp changed\nUPDATED_AT=$(echo $UPDATE_RESPONSE | jq -r '.updated_at')\necho \"Filter updated at: $UPDATED_AT\"\n\n# Expected Status: 200 OK\n# Expected Response:\n# {\n# \"id\": \"550e8400-e29b-41d4-a716-446655440000\",\n# \"name\": \"Complete Workflow Test - Updated\",\n# \"resource_type\": \"media-items\",\n# \"filters\": {\n# \"genre_filter\": \"Science Fiction Fantasy\",\n# \"sort\": \"author ASC\",\n# \"year_min\": \"2000\"\n# },\n# \"created_at\": \"2024-03-20T12:00:00Z\",\n# \"updated_at\": \"2024-03-20T12:05:00Z\"\
|
||||
\ # NEW TIMESTAMP\n# }\n```\n\n**Step 4: DELETE the filter**\n```bash\n# Delete the filter\nDELETE_RESPONSE=$(curl -s -X DELETE \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n \"{{base_url}}/api/saved-filters/$FILTER_ID\")\n\necho \"Delete Response:\"\necho $DELETE_RESPONSE\n\n# Expected Status: 204 No Content\n# Expected Response: (empty body)\nif [ -z \"$DELETE_RESPONSE\" ]; then\n echo \"✓ Filter deleted successfully (empty response)\"\nfi\n```\n\n**Step 5: VERIFY deletion**\n```bash\n# Try to get the deleted filter\nGET_RESPONSE=$(curl -s \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n \"{{base_url}}/api/saved-filters?resource_type=media-items\")\n\necho \"List After Delete:\"\necho $GET_RESPONSE | jq '.'\n\n# Verify filter is NOT in the list\nFILTER_EXISTS=$(echo $GET_RESPONSE | jq --arg id \"$FILTER_ID\" \\\n '.[] | select(.id == $id) | .id')\n\nif [ -z \"$FILTER_EXISTS\" ]; then\n echo \"✓ Filter successfully deleted (not found in list)\"\nelse\n echo \"✗ Filter still\
|
||||
\ exists (deletion failed)\"\nfi\n\n# Expected: Filter no longer appears in list\n```\n\n**Shell Script Complete Example:**\n```bash\n#!/bin/bash\n\nset -e # Exit on error\n\nBASE_URL=\"{{base_url}}\"\nTOKEN=\"YOUR_TOKEN\"\n\necho \"=== Complete CRUD Workflow Test ===\"\necho \"\"\n\n# 1. CREATE\necho \"Step 1: Creating filter...\"\nFILTER_ID=$(curl -s -X POST \\\n -H \"Authorization: Bearer $TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Workflow Test\",\n \"resource_type\": \"media-items\",\n \"filters\": {\"genre_filter\": \"Science Fiction\"}\n }' \\\n \"$BASE_URL/api/saved-filters\" | jq -r '.id')\n\necho \"✓ Created filter: $FILTER_ID\"\necho \"\"\n\n# 2. READ\necho \"Step 2: Reading filters...\"\ncurl -s -H \"Authorization: Bearer $TOKEN\" \\\n \"$BASE_URL/api/saved-filters?resource_type=media-items\" | \\\n jq --arg id \"$FILTER_ID\" '.[] | select(.id == $id)'\n\necho \"✓ Filter retrieved\"\necho \"\"\n\n# 3. UPDATE\necho \"Step 3:\
|
||||
\ Updating filter...\"\ncurl -s -X PUT \\\n -H \"Authorization: Bearer $TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Workflow Test - Updated\",\n \"resource_type\": \"media-items\",\n \"filters\": {\"genre_filter\": \"Fantasy\"}\n }' \\\n \"$BASE_URL/api/saved-filters/$FILTER_ID\" | jq '.'\n\necho \"✓ Filter updated\"\necho \"\"\n\n# 4. DELETE\necho \"Step 4: Deleting filter...\"\ncurl -s -X DELETE \\\n -H \"Authorization: Bearer $TOKEN\" \\\n \"$BASE_URL/api/saved-filters/$FILTER_ID\" -w \"\\nStatus: %{http_code}\\n\"\n\necho \"✓ Filter deleted\"\necho \"\"\n\n# 5. VERIFY\necho \"Step 5: Verifying deletion...\"\nCOUNT=$(curl -s -H \"Authorization: Bearer $TOKEN\" \\\n \"$BASE_URL/api/saved-filters?resource_type=media-items\" | \\\n jq --arg id \"$FILTER_ID\" '[.[] | select(.id == $id)] | length')\n\nif [ \"$COUNT\" -eq 0 ]; then\n echo \"✓ Verification successful - filter deleted\"\nelse\n echo \"✗ Verification failed - filter still exists\"\
|
||||
\n exit 1\nfi\n\necho \"\"\necho \"=== All CRUD operations completed successfully ===\"\n```\n\n**Success Criteria:**\n- ✅ Create returns 201 with generated ID\n- ✅ Read returns filter in list\n- ✅ Update modifies filter and updates timestamp\n- ✅ Delete returns 204 No Content\n- ✅ Filter no longer appears in list after deletion"
|
||||
@@ -8,69 +8,6 @@ http:
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"name\": \"Duplicate Test\",\n \"resource_type\":\
|
||||
\ \"media-items\",\n \"filters\": {\n \"search\": \"test\",\n \"genre_filter\"\
|
||||
: \"Science Fiction\"\n }\n }"
|
||||
|
||||
docs: |-
|
||||
## Duplicate Name Validation
|
||||
|
||||
Tests that the API correctly prevents creating duplicate filter names for the same user + resource type combination.
|
||||
|
||||
**Scenario:**
|
||||
1. Create first filter with name "Duplicate Test"
|
||||
2. Attempt to create second filter with same name
|
||||
3. Expect 409 Conflict error
|
||||
|
||||
**Expected Behavior:**
|
||||
- First request: 201 Created
|
||||
- Second request: 409 Conflict with error message
|
||||
|
||||
**Error Response:**
|
||||
```json
|
||||
{
|
||||
"error": "filter with name 'Duplicate Test' already exists for this resource type"
|
||||
}
|
||||
```
|
||||
|
||||
**Test Steps:**
|
||||
```bash
|
||||
# First request - should succeed
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Duplicate Test",
|
||||
"resource_type": "media-items",
|
||||
"filters": {"search": "test"}
|
||||
}' \
|
||||
"{{base_url}}/api/saved-filters"
|
||||
|
||||
# Response: 201 Created
|
||||
|
||||
# Second request - should fail with 409
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Duplicate Test",
|
||||
"resource_type": "media-items",
|
||||
"filters": {"search": "test"}
|
||||
}' \
|
||||
"{{base_url}}/api/saved-filters"
|
||||
|
||||
# Response: 409 Conflict
|
||||
# {
|
||||
# "error": "filter with name 'Duplicate Test' already exists for this resource type"
|
||||
# }
|
||||
```
|
||||
|
||||
**Important Notes:**
|
||||
- Duplicate check is per user + resource type
|
||||
- Same filter name can be used for different resource types
|
||||
- Different users can have filters with the same name
|
||||
- Validation happens in service layer before database insertion
|
||||
|
||||
**Related Tests:**
|
||||
- Update filter with name that conflicts with another filter (should also return 409)
|
||||
- Create filter with different name for different resource type (should succeed)
|
||||
data: "{\n \"name\": \"Duplicate Test\",\n \"resource_type\": \"media-items\",\n \"filters\": {\n \"search\": \"test\",\n \"genre_filter\": \"Science Fiction\"\n }\n }"
|
||||
docs: "## Duplicate Name Validation\n\nTests that the API correctly prevents creating duplicate filter names for the same user + resource type combination.\n\n**Scenario:**\n1. Create first filter with name \"Duplicate Test\"\n2. Attempt to create second filter with same name\n3. Expect 409 Conflict error\n\n**Expected Behavior:**\n- First request: 201 Created\n- Second request: 409 Conflict with error message\n\n**Error Response:**\n```json\n{\n \"error\": \"filter with name 'Duplicate Test' already exists for this resource type\"\n}\n```\n\n**Test Steps:**\n```bash\n# First request - should succeed\ncurl -X POST \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Duplicate Test\",\n \"resource_type\": \"media-items\",\n \"filters\": {\"search\": \"test\"}\n }' \\\n \"{{base_url}}/api/saved-filters\"\n\n# Response: 201 Created\n\n# Second request - should fail with 409\ncurl -X POST \\\n -H \"Authorization: Bearer\
|
||||
\ YOUR_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Duplicate Test\",\n \"resource_type\": \"media-items\",\n \"filters\": {\"search\": \"test\"}\n }' \\\n \"{{base_url}}/api/saved-filters\"\n\n# Response: 409 Conflict\n# {\n# \"error\": \"filter with name 'Duplicate Test' already exists for this resource type\"\n# }\n```\n\n**Important Notes:**\n- Duplicate check is per user + resource type\n- Same filter name can be used for different resource types\n- Different users can have filters with the same name\n- Validation happens in service layer before database insertion\n\n**Related Tests:**\n- Update filter with name that conflicts with another filter (should also return 409)\n- Create filter with different name for different resource type (should succeed)"
|
||||
@@ -8,254 +8,10 @@ http:
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"name\": \"\",\n \"resource_type\": \"media-items\"\
|
||||
,\n \"filters\": {}\n }"
|
||||
|
||||
docs: |-
|
||||
## Filter Validation - Edge Cases
|
||||
|
||||
Tests various validation edge cases including empty names, invalid resource types, malformed JSON, etc.
|
||||
|
||||
**Test Cases:**
|
||||
|
||||
**Test 1: Empty Filter Name**
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "",
|
||||
"resource_type": "media-items",
|
||||
"filters": {}
|
||||
}' \
|
||||
"{{base_url}}/api/saved-filters"
|
||||
|
||||
# Expected: 400 Bad Request
|
||||
# {
|
||||
# "error": "name and resource_type are required"
|
||||
# }
|
||||
```
|
||||
|
||||
**Test 2: Missing Name Field**
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"resource_type": "media-items",
|
||||
"filters": {}
|
||||
}' \
|
||||
"{{base_url}}/api/saved-filters"
|
||||
|
||||
# Expected: 400 Bad Request
|
||||
# Missing required field validation
|
||||
```
|
||||
|
||||
**Test 3: Missing Resource Type**
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Test Filter",
|
||||
"filters": {}
|
||||
}' \
|
||||
"{{base_url}}/api/saved-filters"
|
||||
|
||||
# Expected: 400 Bad Request
|
||||
# {
|
||||
# "error": "name and resource_type are required"
|
||||
# }
|
||||
```
|
||||
|
||||
**Test 4: Invalid Resource Type**
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Test Filter",
|
||||
"resource_type": "invalid-type",
|
||||
"filters": {}
|
||||
}' \
|
||||
"{{base_url}}/api/saved-filters"
|
||||
|
||||
# Expected: 400 Bad Request or success
|
||||
# Note: Current validation allows any string (extensible design)
|
||||
# Future: May validate against allowed resource types
|
||||
```
|
||||
|
||||
**Test 5: Empty Filters Object**
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Empty Filter",
|
||||
"resource_type": "media-items",
|
||||
"filters": {}
|
||||
}' \
|
||||
"{{base_url}}/api/saved-filters"
|
||||
|
||||
# Expected: 201 Created
|
||||
# Empty filters object is valid (matches no criteria)
|
||||
```
|
||||
|
||||
**Test 6: Very Long Filter Name**
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "This is a very long filter name that exceeds one hundred characters and should be rejected by the validation logic in the handler",
|
||||
"resource_type": "media-items",
|
||||
"filters": {}
|
||||
}' \
|
||||
"{{base_url}}/api/saved-filters"
|
||||
|
||||
# Expected: 400 Bad Request or 201 Created
|
||||
# Note: Plan specifies max 100 chars, but validation may not be enforced
|
||||
```
|
||||
|
||||
**Test 7: Special Characters in Name**
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Filter with <script>alert(\"xss\")</script>",
|
||||
"resource_type": "media-items",
|
||||
"filters": {}
|
||||
}' \
|
||||
"{{base_url}}/api/saved-filters"
|
||||
|
||||
# Expected: 201 Created (input sanitized)
|
||||
# Note: Names are stored as-is, no HTML escaping needed
|
||||
```
|
||||
|
||||
**Test 8: Unicode Characters in Name**
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "🚀 Sci-Fi Books 📚",
|
||||
"resource_type": "media-items",
|
||||
"filters": {
|
||||
"genre_filter": "Science Fiction",
|
||||
"search": "科幻"
|
||||
}
|
||||
}' \
|
||||
"{{base_url}}/api/saved-filters"
|
||||
|
||||
# Expected: 201 Created
|
||||
# Unicode (emoji, CJK characters) should be supported
|
||||
```
|
||||
|
||||
**Test 9: Malformed JSON**
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Test",
|
||||
"resource_type": "media-items",
|
||||
"filters": {
|
||||
"missing closing brace": "broken"
|
||||
}' \
|
||||
"{{base_url}}/api/saved-filters"
|
||||
|
||||
# Expected: 400 Bad Request
|
||||
# {
|
||||
# "error": "invalid request body"
|
||||
# }
|
||||
```
|
||||
|
||||
**Test 10: Invalid Filter ID Format**
|
||||
```bash
|
||||
curl -X PUT \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Updated",
|
||||
"resource_type": "media-items",
|
||||
"filters": {}
|
||||
}' \
|
||||
"{{base_url}}/api/saved-filters/not-a-uuid"
|
||||
|
||||
# Expected: 400 Bad Request
|
||||
# {
|
||||
# "error": "invalid filter ID"
|
||||
# }
|
||||
```
|
||||
|
||||
**Test 11: Missing Filters Field**
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "Test Filter",
|
||||
"resource_type": "media-items"
|
||||
}' \
|
||||
"{{base_url}}/api/saved-filters"
|
||||
|
||||
# Expected: 400 Bad Request
|
||||
# Missing required field
|
||||
```
|
||||
|
||||
**Test 12: Whitespace-Only Name**
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": " ",
|
||||
"resource_type": "media-items",
|
||||
"filters": {}
|
||||
}' \
|
||||
"{{base_url}}/api/saved-filters"
|
||||
|
||||
# Expected: 400 Bad Request
|
||||
# Name validation should reject whitespace-only names
|
||||
```
|
||||
|
||||
**Validation Rules Summary:**
|
||||
|
||||
**Required Fields:**
|
||||
- `name`: Non-empty string (max 100 chars per plan)
|
||||
- `resource_type`: Non-empty string
|
||||
- `filters`: Object (can be empty {})
|
||||
|
||||
**Optional Fields:**
|
||||
- None (all 3 fields are required)
|
||||
|
||||
**Field Validation:**
|
||||
- Name uniqueness checked per user + resource type
|
||||
- Filter ID must be valid UUID for PUT/DELETE
|
||||
- Filters must be valid JSON object
|
||||
|
||||
**Common Validation Errors:**
|
||||
```json
|
||||
{
|
||||
"error": "name and resource_type are required"
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "invalid request body"
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "invalid filter ID"
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "filter with name 'X' already exists for this resource type"
|
||||
}
|
||||
```
|
||||
data: "{\n \"name\": \"\",\n \"resource_type\": \"media-items\",\n \"filters\": {}\n }"
|
||||
docs: "## Filter Validation - Edge Cases\n\nTests various validation edge cases including empty names, invalid resource types, malformed JSON, etc.\n\n**Test Cases:**\n\n**Test 1: Empty Filter Name**\n```bash\ncurl -X POST \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"\",\n \"resource_type\": \"media-items\",\n \"filters\": {}\n }' \\\n \"{{base_url}}/api/saved-filters\"\n\n# Expected: 400 Bad Request\n# {\n# \"error\": \"name and resource_type are required\"\n# }\n```\n\n**Test 2: Missing Name Field**\n```bash\ncurl -X POST \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"resource_type\": \"media-items\",\n \"filters\": {}\n }' \\\n \"{{base_url}}/api/saved-filters\"\n\n# Expected: 400 Bad Request\n# Missing required field validation\n```\n\n**Test 3: Missing Resource Type**\n```bash\ncurl -X POST \\\n -H \"Authorization: Bearer YOUR_TOKEN\"\
|
||||
\ \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Test Filter\",\n \"filters\": {}\n }' \\\n \"{{base_url}}/api/saved-filters\"\n\n# Expected: 400 Bad Request\n# {\n# \"error\": \"name and resource_type are required\"\n# }\n```\n\n**Test 4: Invalid Resource Type**\n```bash\ncurl -X POST \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Test Filter\",\n \"resource_type\": \"invalid-type\",\n \"filters\": {}\n }' \\\n \"{{base_url}}/api/saved-filters\"\n\n# Expected: 400 Bad Request or success\n# Note: Current validation allows any string (extensible design)\n# Future: May validate against allowed resource types\n```\n\n**Test 5: Empty Filters Object**\n```bash\ncurl -X POST \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Empty Filter\",\n \"resource_type\": \"media-items\",\n \"filters\": {}\n }' \\\n \
|
||||
\ \"{{base_url}}/api/saved-filters\"\n\n# Expected: 201 Created\n# Empty filters object is valid (matches no criteria)\n```\n\n**Test 6: Very Long Filter Name**\n```bash\ncurl -X POST \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"This is a very long filter name that exceeds one hundred characters and should be rejected by the validation logic in the handler\",\n \"resource_type\": \"media-items\",\n \"filters\": {}\n }' \\\n \"{{base_url}}/api/saved-filters\"\n\n# Expected: 400 Bad Request or 201 Created\n# Note: Plan specifies max 100 chars, but validation may not be enforced\n```\n\n**Test 7: Special Characters in Name**\n```bash\ncurl -X POST \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Filter with <script>alert(\\\"xss\\\")</script>\",\n \"resource_type\": \"media-items\",\n \"filters\": {}\n }' \\\n \"{{base_url}}/api/saved-filters\"\
|
||||
\n\n# Expected: 201 Created (input sanitized)\n# Note: Names are stored as-is, no HTML escaping needed\n```\n\n**Test 8: Unicode Characters in Name**\n```bash\ncurl -X POST \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"\U0001F680 Sci-Fi Books \U0001F4DA\",\n \"resource_type\": \"media-items\",\n \"filters\": {\n \"genre_filter\": \"Science Fiction\",\n \"search\": \"科幻\"\n }\n }' \\\n \"{{base_url}}/api/saved-filters\"\n\n# Expected: 201 Created\n# Unicode (emoji, CJK characters) should be supported\n```\n\n**Test 9: Malformed JSON**\n```bash\ncurl -X POST \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Test\",\n \"resource_type\": \"media-items\",\n \"filters\": {\n\"missing closing brace\": \"broken\"\n}' \\\n \"{{base_url}}/api/saved-filters\"\n\n# Expected: 400 Bad Request\n# {\n# \"error\": \"invalid request body\"\
|
||||
\n# }\n```\n\n**Test 10: Invalid Filter ID Format**\n```bash\ncurl -X PUT \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Updated\",\n \"resource_type\": \"media-items\",\n \"filters\": {}\n }' \\\n \"{{base_url}}/api/saved-filters/not-a-uuid\"\n\n# Expected: 400 Bad Request\n# {\n# \"error\": \"invalid filter ID\"\n# }\n```\n\n**Test 11: Missing Filters Field**\n```bash\ncurl -X POST \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \"Test Filter\",\n \"resource_type\": \"media-items\"\n }' \\\n \"{{base_url}}/api/saved-filters\"\n\n# Expected: 400 Bad Request\n# Missing required field\n```\n\n**Test 12: Whitespace-Only Name**\n```bash\ncurl -X POST \\\n -H \"Authorization: Bearer YOUR_TOKEN\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"name\": \" \",\n \"resource_type\": \"media-items\",\n \"filters\": {}\n\
|
||||
\ }' \\\n \"{{base_url}}/api/saved-filters\"\n\n# Expected: 400 Bad Request\n# Name validation should reject whitespace-only names\n```\n\n**Validation Rules Summary:**\n\n**Required Fields:**\n- `name`: Non-empty string (max 100 chars per plan)\n- `resource_type`: Non-empty string\n- `filters`: Object (can be empty {})\n\n**Optional Fields:**\n- None (all 3 fields are required)\n\n**Field Validation:**\n- Name uniqueness checked per user + resource type\n- Filter ID must be valid UUID for PUT/DELETE\n- Filters must be valid JSON object\n\n**Common Validation Errors:**\n```json\n{\n \"error\": \"name and resource_type are required\"\n}\n```\n\n```json\n{\n \"error\": \"invalid request body\"\n}\n```\n\n```json\n{\n \"error\": \"invalid filter ID\"\n}\n```\n\n```json\n{\n \"error\": \"filter with name 'X' already exists for this resource type\"\n}\n```"
|
||||
@@ -8,25 +8,5 @@ http:
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"library_id\": \"{{library_id"
|
||||
|
||||
docs: |-
|
||||
## Start Watch Mode
|
||||
|
||||
Starts real-time file system monitoring for a specific library. New media items will be detected and processed almost instantly.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/scanner/watch/start
|
||||
|
||||
**Authentication:** Required (Bearer token, Admin only)
|
||||
|
||||
**Request Body:**
|
||||
- `library_id` (string, required): UUID of the library to watch
|
||||
- Example: `"550e8400-e29b-41d4-a716-446655440000"`
|
||||
|
||||
**Response:** HTTP 200 (OK)
|
||||
```json
|
||||
{
|
||||
"message": "watch mode started for library",
|
||||
"library_id": "550e8400-e29b-41d4-a716-446655440000"
|
||||
data: "{\n \"library_id\": \"{{library_id"
|
||||
docs: "## Start Watch Mode\n \n Starts real-time file system monitoring for a specific library. New media items will be detected and processed almost instantly.\n \n **Method:** POST\n \n **Endpoint:** /api/scanner/watch/start\n \n **Authentication:** Required (Bearer token, Admin only)\n \n **Request Body:**\n - `library_id` (string, required): UUID of the library to watch\n - Example: `\"550e8400-e29b-41d4-a716-446655440000\"`\n \n **Response:** HTTP 200 (OK)\n ```json\n {\n \"message\": \"watch mode started for library\",\n \"library_id\": \"550e8400-e29b-41d4-a716-446655440000\""
|
||||
@@ -8,25 +8,5 @@ http:
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"library_id\": \"{{library_id"
|
||||
|
||||
docs: |-
|
||||
## Stop Watch Mode
|
||||
|
||||
Stops real-time file system monitoring for a specific library.
|
||||
|
||||
**Method:** POST
|
||||
|
||||
**Endpoint:** /api/scanner/watch/stop
|
||||
|
||||
**Authentication:** Required (Bearer token, Admin only)
|
||||
|
||||
**Request Body:**
|
||||
- `library_id` (string, required): UUID of the library to stop watching
|
||||
- Example: `"550e8400-e29b-41d4-a716-446655440000"`
|
||||
|
||||
**Response:** HTTP 200 (OK)
|
||||
```json
|
||||
{
|
||||
"message": "watch mode stopped for library",
|
||||
"library_id": "550e8400-e29b-41d4-a716-446655440000"
|
||||
data: "{\n \"library_id\": \"{{library_id"
|
||||
docs: "## Stop Watch Mode\n \n Stops real-time file system monitoring for a specific library.\n \n **Method:** POST\n \n **Endpoint:** /api/scanner/watch/stop\n \n **Authentication:** Required (Bearer token, Admin only)\n \n **Request Body:**\n - `library_id` (string, required): UUID of the library to stop watching\n - Example: `\"550e8400-e29b-41d4-a716-446655440000\"`\n \n **Response:** HTTP 200 (OK)\n ```json\n {\n \"message\": \"watch mode stopped for library\",\n \"library_id\": \"550e8400-e29b-41d4-a716-446655440000\""
|
||||
@@ -8,35 +8,8 @@ http:
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"base_url\": \"https://bookhoard.example.com\",\n \"opds_base_url\"\
|
||||
: \"https://bookhoard.example.com/opds\",\n \"api_base_url\": \"https://bookhoard.example.com/api\""
|
||||
data: "{\n \"base_url\": \"https://bookhoard.example.com\",\n \"opds_base_url\": \"https://bookhoard.example.com/opds\",\n \"api_base_url\": \"https://bookhoard.example.com/api\""
|
||||
headers:
|
||||
- key: Authorization
|
||||
value: Bearer {{admin_token
|
||||
|
||||
docs: |-
|
||||
## Update System Configuration
|
||||
|
||||
Updates system-wide configuration settings for the Bookhoard instance.
|
||||
|
||||
**Method:** PUT
|
||||
|
||||
**Endpoint:** /api/system/config
|
||||
|
||||
**Authentication:** Bearer token (admin only)
|
||||
|
||||
**Request Body:**
|
||||
- `base_url` (string): Base URL for the instance
|
||||
- `opds_base_url` (string): OPDS endpoint base URL
|
||||
- `api_base_url` (string): API endpoint base URL
|
||||
|
||||
**Response:**
|
||||
- `status` (string): Update status
|
||||
- `config` (object): Updated configuration
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 400: Invalid configuration
|
||||
- 401: Unauthorized
|
||||
- 403: Forbidden (admin only)
|
||||
- 500: Internal server error
|
||||
docs: "## Update System Configuration\n\n Updates system-wide configuration settings for the Bookhoard instance.\n\n **Method:** PUT\n\n **Endpoint:** /api/system/config\n\n **Authentication:** Bearer token (admin only)\n\n **Request Body:**\n - `base_url` (string): Base URL for the instance\n - `opds_base_url` (string): OPDS endpoint base URL\n - `api_base_url` (string): API endpoint base URL\n\n **Response:**\n - `status` (string): Update status\n - `config` (object): Updated configuration\n\n **Status Codes:**\n - 200: Success\n - 400: Invalid configuration\n - 401: Unauthorized\n - 403: Forbidden (admin only)\n - 500: Internal server error"
|
||||
@@ -4,22 +4,12 @@ info:
|
||||
seq: 2
|
||||
http:
|
||||
method: PUT
|
||||
url: "{{base_url}}/api/libraries/scan-settings"
|
||||
url: '{{base_url}}/api/libraries/scan-settings'
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
json:
|
||||
scan_poll_interval_seconds: 30
|
||||
auto_scan_enabled: true
|
||||
data: "{\n \"scan_poll_interval_seconds\": 30,\n \"auto_scan_enabled\": true\n}"
|
||||
headers:
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
|
||||
docs: |-
|
||||
## Update System Scan Settings
|
||||
|
||||
Updates system-wide scan settings that apply to all libraries.
|
||||
|
||||
**Authentication**: Admin token required
|
||||
**Request**: Scan frequency (15-1440 minutes) and enabled status
|
||||
**Response**: Success message
|
||||
- key: Content-Type
|
||||
value: application/json
|
||||
docs: "## Update System Scan Settings\n\n Updates system-wide scan settings that apply to all libraries.\n\n **Authentication**: Admin token required\n **Request**: Scan frequency (15-1440 minutes) and enabled status\n **Response**: Success message"
|
||||
@@ -8,29 +8,5 @@ http:
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: |-
|
||||
{
|
||||
"new_password": "NewSecurePassword123!",
|
||||
"confirm_password": "NewSecurePassword123!"
|
||||
}
|
||||
|
||||
docs: |-
|
||||
## Admin Reset User Password
|
||||
|
||||
Admin-only endpoint to reset another user's password.
|
||||
|
||||
**Method:** PUT
|
||||
|
||||
**Endpoint:** /api/auth/password/:id
|
||||
|
||||
**Authentication:** Required (Admin Bearer token)
|
||||
|
||||
**Request Body:**
|
||||
- `new_password` (string, required): New password
|
||||
- `confirm_password` (string, required): Must match new_password
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 400: Passwords do not match or invalid format
|
||||
- 403: Not an admin
|
||||
- 404: User not found
|
||||
data: "{\n \"new_password\": \"NewSecurePassword123!\",\n \"confirm_password\": \"NewSecurePassword123!\"\n}"
|
||||
docs: "## Admin Reset User Password\n \n Admin-only endpoint to reset another user's password.\n \n **Method:** PUT\n \n **Endpoint:** /api/auth/password/:id\n \n **Authentication:** Required (Admin Bearer token)\n \n **Request Body:**\n - `new_password` (string, required): New password\n - `confirm_password` (string, required): Must match new_password\n \n **Status Codes:**\n - 200: Success\n - 400: Passwords do not match or invalid format\n - 403: Not an admin\n - 404: User not found"
|
||||
@@ -8,35 +8,5 @@ http:
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: |-
|
||||
{
|
||||
"username": "newusername",
|
||||
"email": "newemail@example.com",
|
||||
"role": "admin"
|
||||
}
|
||||
|
||||
docs: |-
|
||||
## Admin Update User
|
||||
|
||||
Admin-only endpoint to update another user's profile.
|
||||
|
||||
**Method:** PUT
|
||||
|
||||
**Endpoint:** /api/auth/profile/:id
|
||||
|
||||
**Authentication:** Required (Admin Bearer token)
|
||||
|
||||
**Request Body:**
|
||||
- `username` (string, optional): New username
|
||||
- `email` (string, optional): New email
|
||||
- `first_name` (string, optional): First name
|
||||
- `last_name` (string, optional): Last name
|
||||
- `theme` (string, optional): Theme preference
|
||||
- `role` (string, optional): Role ("user" or "admin") - admin only
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 400: Invalid role or cannot demote last admin
|
||||
- 403: Not an admin
|
||||
- 404: User not found
|
||||
- 409: Username or email already taken
|
||||
data: "{\n \"username\": \"newusername\",\n \"email\": \"newemail@example.com\",\n \"role\": \"admin\"\n}"
|
||||
docs: "## Admin Update User\n \n Admin-only endpoint to update another user's profile.\n \n **Method:** PUT\n \n **Endpoint:** /api/auth/profile/:id\n \n **Authentication:** Required (Admin Bearer token)\n \n **Request Body:**\n - `username` (string, optional): New username\n - `email` (string, optional): New email\n - `first_name` (string, optional): First name\n - `last_name` (string, optional): Last name\n - `theme` (string, optional): Theme preference\n - `role` (string, optional): Role (\"user\" or \"admin\") - admin only\n \n **Status Codes:**\n - 200: Success\n - 400: Invalid role or cannot demote last admin\n - 403: Not an admin\n - 404: User not found\n - 409: Username or email already taken"
|
||||
@@ -8,30 +8,5 @@ http:
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: "{\n \"current_password\": \"password123\",\n \"new_password\"\
|
||||
: \"newpassword123\",\n \"confirm_password\": \"newpassword123\""
|
||||
|
||||
docs: |-
|
||||
## Update Password
|
||||
|
||||
Updates the authenticated user's password.
|
||||
|
||||
**Method:** PUT
|
||||
|
||||
**Endpoint:** /api/auth/password
|
||||
|
||||
**Authentication:** Required
|
||||
|
||||
**Request Body:**
|
||||
- `current_password` (string, required): Current password for verification
|
||||
- `new_password` (string, required): New password (minimum 6 characters)
|
||||
- `confirm_password` (string, required): Confirmation of new password
|
||||
|
||||
**Response:**
|
||||
- `message` (string): Success message
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 400: Password validation failed
|
||||
- 401: Current password incorrect
|
||||
- 401: Unauthorized
|
||||
data: "{\n \"current_password\": \"password123\",\n \"new_password\": \"newpassword123\",\n \"confirm_password\": \"newpassword123\""
|
||||
docs: "## Update Password\n \n Updates the authenticated user's password.\n \n **Method:** PUT\n \n **Endpoint:** /api/auth/password\n \n **Authentication:** Required\n \n **Request Body:**\n - `current_password` (string, required): Current password for verification\n - `new_password` (string, required): New password (minimum 6 characters)\n - `confirm_password` (string, required): Confirmation of new password\n \n **Response:**\n - `message` (string): Success message\n \n **Status Codes:**\n - 200: Success\n - 400: Password validation failed\n - 401: Current password incorrect\n - 401: Unauthorized"
|
||||
@@ -8,38 +8,5 @@ http:
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: |-
|
||||
{
|
||||
"username": "updateduser",
|
||||
"email": "updated@tests.bookhoard.internal",
|
||||
"first_name": "Updated",
|
||||
"last_name": "User",
|
||||
"theme": "dracula"
|
||||
}
|
||||
|
||||
docs: |-
|
||||
## Update User Profile
|
||||
|
||||
Updates the authenticated user's profile information.
|
||||
|
||||
**Method:** PUT
|
||||
|
||||
**Endpoint:** /api/auth/profile
|
||||
|
||||
**Authentication:** Required (Bearer token)
|
||||
|
||||
**Request Body:**
|
||||
- `username` (string, optional): New username (must be unique)
|
||||
- `email` (string, optional): New email address (must be unique)
|
||||
- `first_name` (string, optional): First name
|
||||
- `last_name` (string, optional): Last name
|
||||
- `theme` (string, optional): Theme preference
|
||||
|
||||
**Response:**
|
||||
- `message` (string): Success message
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 400: Invalid request
|
||||
- 401: Unauthorized
|
||||
- 409: Username or email already taken
|
||||
data: "{\n \"username\": \"updateduser\",\n \"email\": \"updated@tests.bookhoard.internal\",\n \"first_name\": \"Updated\",\n \"last_name\": \"User\",\n \"theme\": \"dracula\"\n}"
|
||||
docs: "## Update User Profile\n \n Updates the authenticated user's profile information.\n \n **Method:** PUT\n \n **Endpoint:** /api/auth/profile\n \n **Authentication:** Required (Bearer token)\n \n **Request Body:**\n - `username` (string, optional): New username (must be unique)\n - `email` (string, optional): New email address (must be unique)\n - `first_name` (string, optional): First name\n - `last_name` (string, optional): Last name\n - `theme` (string, optional): Theme preference\n \n **Response:**\n - `message` (string): Success message\n \n **Status Codes:**\n - 200: Success\n - 400: Invalid request\n - 401: Unauthorized\n - 409: Username or email already taken"
|
||||
@@ -8,29 +8,5 @@ http:
|
||||
auth: inherit
|
||||
body:
|
||||
type: json
|
||||
jsonBody: |-
|
||||
{
|
||||
"theme": "dracula"
|
||||
}
|
||||
|
||||
docs: |-
|
||||
## Update Theme
|
||||
|
||||
Fast theme switching for the application UI.
|
||||
|
||||
**Method:** PUT
|
||||
|
||||
**Endpoint:** /api/auth/theme
|
||||
|
||||
**Authentication:** Required (Bearer token)
|
||||
|
||||
**Request Body:**
|
||||
- `theme` (string, required): Theme name
|
||||
|
||||
**Response:**
|
||||
- `message` (string): Success message
|
||||
|
||||
**Status Codes:**
|
||||
- 200: Success
|
||||
- 400: Invalid theme
|
||||
- 401: Unauthorized
|
||||
data: "{\n \"theme\": \"dracula\"\n}"
|
||||
docs: "## Update Theme\n \n Fast theme switching for the application UI.\n \n **Method:** PUT\n \n **Endpoint:** /api/auth/theme\n \n **Authentication:** Required (Bearer token)\n \n **Request Body:**\n - `theme` (string, required): Theme name\n \n **Response:**\n - `message` (string): Success message\n \n **Status Codes:**\n - 200: Success\n - 400: Invalid theme\n - 401: Unauthorized"
|
||||
Reference in new issue
Block a user