Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
649f821850 | ||
|
|
4681e98766 | ||
|
|
56dbca37d0 | ||
|
|
27f42bf703 | ||
|
|
fb95b6d400 | ||
|
|
6a598786e9 | ||
|
|
3b66b981be | ||
|
|
6d01bd7971 | ||
|
|
09545ca5e1 | ||
|
|
eb57b7208b | ||
|
|
a730a62280 | ||
|
|
d41c3e4774 | ||
|
|
ab2a5568ae | ||
|
|
ecfd7276ab | ||
|
|
b6eba66801 | ||
|
|
4ab6a64bd8 | ||
|
|
dc0e3573a6 | ||
|
|
a716b386a1 | ||
|
|
8631aa00bf | ||
|
|
8733e83f1f | ||
|
|
dd2ab4ea96 | ||
|
|
da0bae94bb | ||
|
|
a1ac5b1541 | ||
|
|
73e8cdef4b | ||
|
|
a8f8c4def9 | ||
|
|
f1a6de7cd7 | ||
|
|
983980d07a | ||
|
|
0c45137f5b | ||
|
|
cfcd95b25e | ||
|
|
acd82fff0a | ||
|
|
6fc501b35a | ||
|
|
e21e1facde | ||
|
|
18084de7a6 | ||
|
|
7a7b2ab82a | ||
|
|
cc5677eb17 | ||
|
|
2c56ae4b9d | ||
|
|
25ef0d12ba | ||
|
|
b77eab7ffd | ||
|
|
6f13dc2b63 | ||
|
|
ae9588f1c6 | ||
|
|
eacc33bfac | ||
|
|
fa00d6d3cb | ||
|
|
7471ca99e8 | ||
|
|
4a6fb033cb | ||
|
|
ed0ce5d077 | ||
|
|
85cdaf7196 | ||
|
|
c4b62af213 | ||
|
|
50282895f5 | ||
|
|
cc893f758e | ||
|
|
994e0f86c4 | ||
|
|
11b179b91f | ||
|
|
84cd6328f4 | ||
|
|
9d0ae2bcc4 | ||
|
|
f8de17723a | ||
|
|
4769e832c4 |
No files matched your search
@@ -86,3 +86,4 @@ session-*.md
|
|||||||
# Binary media fixtures: never commit (generate with ffmpeg at test time,
|
# Binary media fixtures: never commit (generate with ffmpeg at test time,
|
||||||
# see docs/contributing/development.md → Media fixtures)
|
# see docs/contributing/development.md → Media fixtures)
|
||||||
internal/services/testdata/
|
internal/services/testdata/
|
||||||
|
test-results
|
||||||
@@ -41,6 +41,38 @@ with heavy interactivity. First-paint section markup is still templ
|
|||||||
deliberate; do not clone it to new pages, and do not "fix" the dashboard
|
deliberate; do not clone it to new pages, and do not "fix" the dashboard
|
||||||
without restoring a templ-partial fade path first.
|
without restoring a templ-partial fade path first.
|
||||||
|
|
||||||
|
## CSP discipline (hash-allow-listed script-src) — ALWAYS apply
|
||||||
|
|
||||||
|
script-src carries a sha256 hash, not `'unsafe-inline'` (and never
|
||||||
|
`'unsafe-eval'` — the Alpine build is `@alpinejs/csp` for exactly this
|
||||||
|
reason; keep it in lockstep with alpinejs). The browser therefore
|
||||||
|
refuses, **silently at build/test time**, two authoring patterns:
|
||||||
|
|
||||||
|
- **No inline `<script>` content outside `theme_flash_guard.templ`.**
|
||||||
|
The guard is the app's only inline script; its bytes are pinned by
|
||||||
|
`middleware.ScriptHashThemeFlashGuard` + the drift test. Editing the
|
||||||
|
guard means recomputing the hash (the failing test prints it); adding
|
||||||
|
a *second* inline script means hashing it too and extending
|
||||||
|
`cspSanctionedInlineScripts` in `templates/csp_source_test.go`.
|
||||||
|
Everything else goes in a bundled `web/src` module.
|
||||||
|
- **No native `on*=` handler attributes** (`onclick=`, `onerror=`,
|
||||||
|
`onsubmit=`, `onchange=`, … — quote AND templ brace forms). Inline
|
||||||
|
handlers never go through the hash path; hash-mode CSP refuses them
|
||||||
|
unconditionally. Use Alpine `@click`/`@change`/… bound to component
|
||||||
|
methods (see the 2026-10 CSP-build conversion commits for the
|
||||||
|
pattern), or delegated listeners for htmx-swapped content.
|
||||||
|
- **No htmx eval-dependent bindings** (`hx-on*`, `js:`-prefixed
|
||||||
|
`hx-vals`) — `'unsafe-eval'` is not granted.
|
||||||
|
|
||||||
|
`templates/csp_source_test.go` is the tripwire for all three (it
|
||||||
|
scans the `.templ` sources; it runs in seconds on host). `style-src
|
||||||
|
'unsafe-inline'` remains by design (htmx-injected styles, Alpine
|
||||||
|
`:style`); `'self'` still trusts the origin not to serve
|
||||||
|
attacker-supplied scripts — don't let upload paths become
|
||||||
|
script-executable. Remember: `templ` does not interpolate `{ expr }`
|
||||||
|
inside quoted attribute values (gotcha #1 below), so dynamic
|
||||||
|
`@click={ "fn('" + id + "')" }` attribute values must BE expressions.
|
||||||
|
|
||||||
## Library visibility (family-lens rule) — ALWAYS apply
|
## Library visibility (family-lens rule) — ALWAYS apply
|
||||||
|
|
||||||
Per-user library visibility (`library_visibility` table, default-ALLOW:
|
Per-user library visibility (`library_visibility` table, default-ALLOW:
|
||||||
|
|||||||
@@ -54,11 +54,13 @@ test: lint-rendering
|
|||||||
# UI markup, JS orchestrates. Fails when a web/src file builds more HTML
|
# UI markup, JS orchestrates. Fails when a web/src file builds more HTML
|
||||||
# template literals than its recorded baseline (web/src/rendering-allowlist.txt),
|
# template literals than its recorded baseline (web/src/rendering-allowlist.txt),
|
||||||
# or when a .templ file gains an inline <script> block (theme_flash_guard.templ
|
# or when a .templ file gains an inline <script> block (theme_flash_guard.templ
|
||||||
# is the only sanctioned one). Shrink baselines as fragments move to templ
|
# is the only sanctioned one). *.test.ts files are excluded: jsdom fixtures
|
||||||
|
# build DOM in tests, they don't render shipped UI. Shrink baselines as
|
||||||
|
# fragments move to templ
|
||||||
# partials; grow one only with a written reason.
|
# partials; grow one only with a written reason.
|
||||||
lint-rendering:
|
lint-rendering:
|
||||||
@violations=""; \
|
@violations=""; \
|
||||||
for f in $$(grep -rlE 'innerHTML[[:space:]]*=[[:space:]]*' web/src --include='*.ts' | grep -v 'web/src/reader/'); do \
|
for f in $$(grep -rlE 'innerHTML[[:space:]]*=[[:space:]]*' web/src --include='*.ts' | grep -v 'web/src/reader/' | grep -v '\.test\.ts$$'); do \
|
||||||
count=$$(grep -E 'innerHTML[[:space:]]*=[[:space:]]*' $$f | grep -cvE 'innerHTML[[:space:]]*=[[:space:]]*""[[:space:]]*$$'); \
|
count=$$(grep -E 'innerHTML[[:space:]]*=[[:space:]]*' $$f | grep -cvE 'innerHTML[[:space:]]*=[[:space:]]*""[[:space:]]*$$'); \
|
||||||
allowed=$$(grep -F "$$f:" web/src/rendering-allowlist.txt | cut -d: -f2); \
|
allowed=$$(grep -F "$$f:" web/src/rendering-allowlist.txt | cut -d: -f2); \
|
||||||
allowed=$${allowed:-0}; \
|
allowed=$${allowed:-0}; \
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
info:
|
|
||||||
name: Get Admin Library
|
|
||||||
type: http
|
|
||||||
seq: 5
|
|
||||||
http:
|
|
||||||
method: GET
|
|
||||||
url: '{{base_url}}/admin/library'
|
|
||||||
auth: inherit
|
|
||||||
body:
|
|
||||||
type: none
|
|
||||||
|
|
||||||
docs: |-
|
|
||||||
## Get Admin Library Page
|
|
||||||
|
|
||||||
Retrieves the admin library page for administrative access.
|
|
||||||
|
|
||||||
**Method:** GET
|
|
||||||
|
|
||||||
**Endpoint:** /admin/library
|
|
||||||
|
|
||||||
**Headers:**
|
|
||||||
- `Authorization` (string): Bearer token
|
|
||||||
|
|
||||||
**Response:**
|
|
||||||
- HTML content for the admin library page
|
|
||||||
|
|
||||||
**Status Codes:**
|
|
||||||
- 200: Success
|
|
||||||
- 401: Unauthorized
|
|
||||||
- 403: Forbidden
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
info:
|
||||||
|
name: Hash Conflicts (API)
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: GET
|
||||||
|
url: '{{base_url}}/api/admin/hash-conflicts'
|
||||||
|
auth: inherit
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
scripts:
|
||||||
|
- type: tests
|
||||||
|
code: |-
|
||||||
|
test("status is 200", function() {
|
||||||
|
expect(res.status).to.eql(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |-
|
||||||
|
# Hash Conflicts (API)
|
||||||
|
|
||||||
|
`GET /api/admin/hash-conflicts` — auth: jwt+admin.
|
||||||
|
|
||||||
|
Canonical reference: docs/developer/api/admin/hash_conflicts_api.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||||
|
type: text/markdown
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
info:
|
||||||
|
name: List Users
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: GET
|
||||||
|
url: '{{base_url}}/api/auth/users'
|
||||||
|
auth: inherit
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
scripts:
|
||||||
|
- type: tests
|
||||||
|
code: |-
|
||||||
|
test("status is 200", function() {
|
||||||
|
expect(res.status).to.eql(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |-
|
||||||
|
# List Users
|
||||||
|
|
||||||
|
`GET /api/auth/users` — auth: jwt+admin.
|
||||||
|
|
||||||
|
Canonical reference: docs/developer/api/admin/list_users.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||||
|
type: text/markdown
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
info:
|
||||||
|
name: Update User Max Devices
|
||||||
|
type: http
|
||||||
|
seq: 3
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: PUT
|
||||||
|
url: '{{base_url}}/api/auth/users/{{media_item_id}}/max-devices'
|
||||||
|
auth: inherit
|
||||||
|
body:
|
||||||
|
type: json
|
||||||
|
data: |-
|
||||||
|
{
|
||||||
|
"max_devices": 15
|
||||||
|
}
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
scripts:
|
||||||
|
- type: tests
|
||||||
|
code: |-
|
||||||
|
test("status is 200", function() {
|
||||||
|
expect(res.status).to.eql(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |-
|
||||||
|
# Update User Max Devices
|
||||||
|
|
||||||
|
`PUT /api/auth/users/:id/max-devices` — auth: jwt+admin.
|
||||||
|
|
||||||
|
Canonical reference: docs/developer/api/admin/update_user_max_devices.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||||
|
type: text/markdown
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
info:
|
|
||||||
name: Get Device Usage
|
|
||||||
type: http
|
|
||||||
seq: 2
|
|
||||||
http:
|
|
||||||
method: GET
|
|
||||||
url: '{{base_url}}/api/analytics/device-usage'
|
|
||||||
auth: inherit
|
|
||||||
body:
|
|
||||||
type: none
|
|
||||||
runtime:
|
|
||||||
scripts:
|
|
||||||
- type: tests
|
|
||||||
code: "test(\"status must be 200\", function() {\n expect(res.status).to.eql(200);"
|
|
||||||
|
|
||||||
docs: |-
|
|
||||||
Get usage statistics for all devices.
|
|
||||||
|
|
||||||
**Endpoint**: GET /api/analytics/device-usage
|
|
||||||
**Auth**: Required (Bearer token)
|
|
||||||
|
|
||||||
## Response Fields
|
|
||||||
|
|
||||||
| Field | Type | Description |
|
|
||||||
|-------|------|-------------|
|
|
||||||
| devices | array | List of device usage statistics |
|
|
||||||
| devices[].id | string | Device ID |
|
|
||||||
| devices[].device_name | string | Device name |
|
|
||||||
| devices[].device_type | string | Device type (kobo, kindle, koreader) |
|
|
||||||
| devices[].sync_count | int | Number of sync operations |
|
|
||||||
| devices[].last_sync | string | Last sync timestamp |
|
|
||||||
| devices[].total_reading_minutes | int | Total reading time on device |
|
|
||||||
| devices[].books_read | int | Number of books completed on device |
|
|
||||||
|
|
||||||
## Example Response
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"devices": [
|
|
||||||
{
|
|
||||||
"id": "789e4567-e89b-12d3-a456-426614174001",
|
|
||||||
"device_name": "My Kobo Clara",
|
|
||||||
"device_type": "kobo",
|
|
||||||
"sync_count": 45,
|
|
||||||
"last_sync": "2026-02-08T17:25:00Z",
|
|
||||||
"total_reading_minutes": 1250,
|
|
||||||
"books_read": 3
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
info:
|
|
||||||
name: Get Popular Books
|
|
||||||
type: http
|
|
||||||
seq: 1
|
|
||||||
http:
|
|
||||||
method: GET
|
|
||||||
url: '{{base_url}}/api/analytics/popular-books?limit=10'
|
|
||||||
auth: inherit
|
|
||||||
body:
|
|
||||||
type: none
|
|
||||||
runtime:
|
|
||||||
scripts:
|
|
||||||
- type: tests
|
|
||||||
code: "test(\"status must be 200\", function() {\n expect(res.status).to.eql(200);"
|
|
||||||
|
|
||||||
docs: |-
|
|
||||||
Get popular books sorted by read count.
|
|
||||||
|
|
||||||
**Endpoint**: GET /api/analytics/popular-books
|
|
||||||
**Auth**: Required (Bearer token)
|
|
||||||
|
|
||||||
## Query Parameters
|
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
|
||||||
|-----------|------|-----------|-------------|
|
|
||||||
| limit | int | No | Maximum number of books to return (default: 10) |
|
|
||||||
|
|
||||||
## Response Fields
|
|
||||||
|
|
||||||
| Field | Type | Description |
|
|
||||||
|-------|------|-------------|
|
|
||||||
| books | array | List of popular books |
|
|
||||||
| books[].media_item_id | string | Book ID |
|
|
||||||
| books[].title | string | Book title |
|
|
||||||
| books[].author | string | Book author |
|
|
||||||
| books[].read_count | int | Number of times read |
|
|
||||||
| books[].avg_completion | float | Average completion rate (0-1) |
|
|
||||||
| books[].cover_url | string | Cover image URL |
|
|
||||||
|
|
||||||
## Example Request
|
|
||||||
|
|
||||||
```
|
|
||||||
GET /api/analytics/popular-books?limit=10
|
|
||||||
```
|
|
||||||
|
|
||||||
## Example Response
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"books": [
|
|
||||||
{
|
|
||||||
"media_item_id": "323e4567-e89b-12d3-a456-426614174002",
|
|
||||||
"title": "The Great Gatsby",
|
|
||||||
"author": "F. Scott Fitzgerald",
|
|
||||||
"read_count": 5,
|
|
||||||
"avg_completion": 0.85,
|
|
||||||
"cover_url": "/api/books/323e4567-e89b-12d3-a456-426614174002/cover"
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
info:
|
|
||||||
name: Get Reading Stats Date Range
|
|
||||||
type: http
|
|
||||||
seq: 3
|
|
||||||
http:
|
|
||||||
method: GET
|
|
||||||
url: '{{base_url}}/api/analytics/reading-stats?start_date=2024-01-01&end_date=2024-01-31'
|
|
||||||
auth: inherit
|
|
||||||
body:
|
|
||||||
type: none
|
|
||||||
runtime:
|
|
||||||
scripts:
|
|
||||||
- type: tests
|
|
||||||
code: "test(\"status must be 200\", function() {\n expect(res.status).to.eql(200);"
|
|
||||||
|
|
||||||
docs: |-
|
|
||||||
Get reading statistics for a specific date range.
|
|
||||||
|
|
||||||
**Endpoint**: GET /api/analytics/reading-stats
|
|
||||||
**Auth**: Required (Bearer token)
|
|
||||||
|
|
||||||
## Query Parameters
|
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
|
||||||
|-----------|------|-----------|-------------|
|
|
||||||
| start_date | string | No | Start date (ISO 8601 format, default: 30 days ago) |
|
|
||||||
| end_date | string | No | End date (ISO 8601 format, default: today) |
|
|
||||||
|
|
||||||
## Response Fields
|
|
||||||
|
|
||||||
| Field | Type | Description |
|
|
||||||
|-------|------|-------------|
|
|
||||||
| total_books_read | int | Total books completed in range |
|
|
||||||
| total_pages_read | int | Total pages read in range |
|
|
||||||
| total_reading_time_minutes | int | Total reading time in minutes |
|
|
||||||
| completion_rate | float | Percentage of books completed (0-1) |
|
|
||||||
| daily_reading_minutes | array | Daily reading time per day |
|
|
||||||
| daily_reading_minutes[].date | string | Date (ISO 8601) |
|
|
||||||
| daily_reading_minutes[].minutes | int | Minutes read on that date |
|
|
||||||
|
|
||||||
## Example Request
|
|
||||||
|
|
||||||
```
|
|
||||||
GET /api/analytics/reading-stats?start_date=2024-01-01&end_date=2024-01-31
|
|
||||||
```
|
|
||||||
|
|
||||||
## Example Response
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"total_books_read": 2,
|
|
||||||
"total_pages_read": 450,
|
|
||||||
"total_reading_time_minutes": 720,
|
|
||||||
"completion_rate": 0.85,
|
|
||||||
"daily_reading_minutes": [
|
|
||||||
{
|
|
||||||
"date": "2024-01-01",
|
|
||||||
"minutes": 30
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
info:
|
|
||||||
name: Get Reading Stats
|
|
||||||
type: http
|
|
||||||
seq: 4
|
|
||||||
http:
|
|
||||||
method: GET
|
|
||||||
url: '{{base_url}}/api/analytics/reading-stats'
|
|
||||||
auth: inherit
|
|
||||||
body:
|
|
||||||
type: none
|
|
||||||
runtime:
|
|
||||||
scripts:
|
|
||||||
- type: tests
|
|
||||||
code: "test(\"status must be 200\", function() {\n expect(res.status).to.eql(200);"
|
|
||||||
|
|
||||||
docs: |-
|
|
||||||
Get overall reading statistics for the authenticated user.
|
|
||||||
|
|
||||||
**Endpoint**: GET /api/analytics/reading-stats
|
|
||||||
**Auth**: Required (Bearer token)
|
|
||||||
|
|
||||||
## Query Parameters
|
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
|
||||||
|-----------|------|-----------|-------------|
|
|
||||||
| start_date | string | No | Start date (ISO 8601 format) |
|
|
||||||
| end_date | string | No | End date (ISO 8601 format) |
|
|
||||||
|
|
||||||
## Response Fields
|
|
||||||
|
|
||||||
| Field | Type | Description |
|
|
||||||
|-------|------|-------------|
|
|
||||||
| total_books_read | int | Total books completed |
|
|
||||||
| total_pages_read | int | Total pages read |
|
|
||||||
| total_reading_time_minutes | int | Total reading time in minutes |
|
|
||||||
| completion_rate | float | Average book completion rate (0-1) |
|
|
||||||
| daily_reading_minutes | array | Daily reading time breakdown |
|
|
||||||
| daily_reading_minutes[].date | string | Date (ISO 8601) |
|
|
||||||
| daily_reading_minutes[].minutes | int | Minutes read on that date |
|
|
||||||
|
|
||||||
## Example Request
|
|
||||||
|
|
||||||
```
|
|
||||||
GET /api/analytics/reading-stats
|
|
||||||
```
|
|
||||||
|
|
||||||
## Example Response
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"total_books_read": 12,
|
|
||||||
"total_pages_read": 3450,
|
|
||||||
"total_reading_time_minutes": 5400,
|
|
||||||
"completion_rate": 0.78,
|
|
||||||
"daily_reading_minutes": [
|
|
||||||
{
|
|
||||||
"date": "2026-01-15",
|
|
||||||
"minutes": 45
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
info:
|
||||||
|
name: Get Reading Statistics
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: GET
|
||||||
|
url: '{{base_url}}/api/analytics/reading-stats'
|
||||||
|
auth: inherit
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
scripts:
|
||||||
|
- type: tests
|
||||||
|
code: |-
|
||||||
|
test("status is 200", function() {
|
||||||
|
expect(res.status).to.eql(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |-
|
||||||
|
# Get Reading Statistics
|
||||||
|
|
||||||
|
`GET /api/analytics/reading-stats` — auth: jwt.
|
||||||
|
|
||||||
|
Canonical reference: docs/developer/api/analytics/get_analytics.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||||
|
type: text/markdown
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
info:
|
||||||
|
name: Get Device Usage
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: GET
|
||||||
|
url: '{{base_url}}/api/analytics/device-usage'
|
||||||
|
auth: inherit
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
scripts:
|
||||||
|
- type: tests
|
||||||
|
code: |-
|
||||||
|
test("status is 200", function() {
|
||||||
|
expect(res.status).to.eql(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |-
|
||||||
|
# Get Device Usage
|
||||||
|
|
||||||
|
`GET /api/analytics/device-usage` — auth: jwt.
|
||||||
|
|
||||||
|
Canonical reference: docs/developer/api/analytics/get_device_usage.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||||
|
type: text/markdown
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
info:
|
||||||
|
name: Get Popular Books
|
||||||
|
type: http
|
||||||
|
seq: 3
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: GET
|
||||||
|
url: '{{base_url}}/api/analytics/popular-books'
|
||||||
|
auth: inherit
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
scripts:
|
||||||
|
- type: tests
|
||||||
|
code: |-
|
||||||
|
test("status is 200", function() {
|
||||||
|
expect(res.status).to.eql(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |-
|
||||||
|
# Get Popular Books
|
||||||
|
|
||||||
|
`GET /api/analytics/popular-books` — auth: jwt.
|
||||||
|
|
||||||
|
Canonical reference: docs/developer/api/analytics/get_popular_books.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||||
|
type: text/markdown
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
info:
|
||||||
|
name: Login User
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: POST
|
||||||
|
url: '{{base_url}}/api/auth/login'
|
||||||
|
auth: inherit
|
||||||
|
body:
|
||||||
|
type: json
|
||||||
|
data: |-
|
||||||
|
{
|
||||||
|
"login": "user@example.com",
|
||||||
|
"password": "SecureP@ss123!"
|
||||||
|
}
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
scripts:
|
||||||
|
- type: after-response
|
||||||
|
code: |-
|
||||||
|
function onResponse(res) {
|
||||||
|
try {
|
||||||
|
const responseBody = res.getBody();
|
||||||
|
const token = responseBody.access_token;
|
||||||
|
const refreshToken = responseBody.refresh_token;
|
||||||
|
if (token) {
|
||||||
|
bru.setEnvVar("token", token, { persist: true });
|
||||||
|
console.log("Access token saved");
|
||||||
|
}
|
||||||
|
if (refreshToken) {
|
||||||
|
bru.setEnvVar("refresh_token", refreshToken, { persist: true });
|
||||||
|
console.log("Refresh token saved");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Token capture failed:", error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onResponse();
|
||||||
|
|
||||||
|
- type: tests
|
||||||
|
code: |-
|
||||||
|
test("status is 200", function() {
|
||||||
|
expect(res.status).to.eql(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |-
|
||||||
|
# Login User
|
||||||
|
|
||||||
|
`POST /api/auth/login` — auth: none.
|
||||||
|
|
||||||
|
Canonical reference: docs/developer/api/authentication/login.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||||
|
type: text/markdown
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
info:
|
||||||
|
name: Logout User
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: POST
|
||||||
|
url: '{{base_url}}/api/auth/logout'
|
||||||
|
auth: inherit
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
scripts:
|
||||||
|
- type: tests
|
||||||
|
code: |-
|
||||||
|
test("status is 200", function() {
|
||||||
|
expect(res.status).to.eql(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |-
|
||||||
|
# Logout User
|
||||||
|
|
||||||
|
`POST /api/auth/logout` — auth: none.
|
||||||
|
|
||||||
|
Canonical reference: docs/developer/api/authentication/logout.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||||
|
type: text/markdown
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
info:
|
||||||
|
name: Password Rules
|
||||||
|
type: http
|
||||||
|
seq: 3
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: GET
|
||||||
|
url: '{{base_url}}/api/auth/password-rules'
|
||||||
|
auth: inherit
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
scripts:
|
||||||
|
- type: tests
|
||||||
|
code: |-
|
||||||
|
test("status is 200", function() {
|
||||||
|
expect(res.status).to.eql(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |-
|
||||||
|
# Password Rules
|
||||||
|
|
||||||
|
`GET /api/auth/password-rules` — auth: none.
|
||||||
|
|
||||||
|
Canonical reference: docs/developer/api/authentication/password_rules.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||||
|
type: text/markdown
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
info:
|
||||||
|
name: Refresh Token
|
||||||
|
type: http
|
||||||
|
seq: 4
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: POST
|
||||||
|
url: '{{base_url}}/api/auth/refresh'
|
||||||
|
auth: inherit
|
||||||
|
body:
|
||||||
|
type: json
|
||||||
|
data: |-
|
||||||
|
{
|
||||||
|
"refresh_token": "d4f5g6h7..."
|
||||||
|
}
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
scripts:
|
||||||
|
- type: after-response
|
||||||
|
code: |-
|
||||||
|
function onResponse(res) {
|
||||||
|
try {
|
||||||
|
const responseBody = res.getBody();
|
||||||
|
const token = responseBody.access_token;
|
||||||
|
const refreshToken = responseBody.refresh_token;
|
||||||
|
if (token) {
|
||||||
|
bru.setEnvVar("token", token, { persist: true });
|
||||||
|
console.log("Access token saved");
|
||||||
|
}
|
||||||
|
if (refreshToken) {
|
||||||
|
bru.setEnvVar("refresh_token", refreshToken, { persist: true });
|
||||||
|
console.log("Refresh token saved");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Token capture failed:", error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onResponse();
|
||||||
|
|
||||||
|
- type: tests
|
||||||
|
code: |-
|
||||||
|
test("status is 200", function() {
|
||||||
|
expect(res.status).to.eql(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |-
|
||||||
|
# Refresh Token
|
||||||
|
|
||||||
|
`POST /api/auth/refresh` — auth: none.
|
||||||
|
|
||||||
|
Canonical reference: docs/developer/api/authentication/refresh_token.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||||
|
type: text/markdown
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
info:
|
||||||
|
name: Register User
|
||||||
|
type: http
|
||||||
|
seq: 5
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: POST
|
||||||
|
url: '{{base_url}}/api/auth/register'
|
||||||
|
auth: inherit
|
||||||
|
body:
|
||||||
|
type: json
|
||||||
|
data: |-
|
||||||
|
{
|
||||||
|
"email": "user@example.com",
|
||||||
|
"username": "john",
|
||||||
|
"password": "SecureP@ss123!",
|
||||||
|
"first_name": "John",
|
||||||
|
"last_name": "Doe"
|
||||||
|
}
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
scripts:
|
||||||
|
- type: after-response
|
||||||
|
code: |-
|
||||||
|
function onResponse(res) {
|
||||||
|
try {
|
||||||
|
const responseBody = res.getBody();
|
||||||
|
const token = responseBody.access_token;
|
||||||
|
const refreshToken = responseBody.refresh_token;
|
||||||
|
if (token) {
|
||||||
|
bru.setEnvVar("token", token, { persist: true });
|
||||||
|
console.log("Access token saved");
|
||||||
|
}
|
||||||
|
if (refreshToken) {
|
||||||
|
bru.setEnvVar("refresh_token", refreshToken, { persist: true });
|
||||||
|
console.log("Refresh token saved");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Token capture failed:", error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onResponse();
|
||||||
|
|
||||||
|
- type: tests
|
||||||
|
code: |-
|
||||||
|
test("status is 201", function() {
|
||||||
|
expect(res.status).to.eql(201);
|
||||||
|
});
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |-
|
||||||
|
# Register User
|
||||||
|
|
||||||
|
`POST /api/auth/register` — auth: none.
|
||||||
|
|
||||||
|
Canonical reference: docs/developer/api/authentication/register.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||||
|
type: text/markdown
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
info:
|
||||||
|
name: Register - Admin Role Unauthenticated (403)
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: POST
|
||||||
|
url: '{{base_url}}/api/auth/register'
|
||||||
|
auth: inherit
|
||||||
|
body:
|
||||||
|
type: json
|
||||||
|
data: |-
|
||||||
|
{
|
||||||
|
"email": "smoke-admin-attempts@tests.bookhoard.internal",
|
||||||
|
"username": "smokeadminattempts",
|
||||||
|
"password": "Sm0ke@Pass123!",
|
||||||
|
"role": "admin"
|
||||||
|
}
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
scripts:
|
||||||
|
- type: tests
|
||||||
|
code: |-
|
||||||
|
test("status is 403", function() {
|
||||||
|
expect(res.status).to.eql(403);
|
||||||
|
});
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |-
|
||||||
|
# Register - Admin Role Unauthenticated (403)
|
||||||
|
|
||||||
|
With admins already present, an unauthenticated register attempt
|
||||||
|
carrying role=admin must be rejected with 403 and create nothing.
|
||||||
|
Repeats safely.
|
||||||
|
type: text/markdown
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
info:
|
||||||
|
name: Register - Duplicate Email (409)
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: POST
|
||||||
|
url: '{{base_url}}/api/auth/register'
|
||||||
|
auth: inherit
|
||||||
|
body:
|
||||||
|
type: json
|
||||||
|
data: |-
|
||||||
|
{
|
||||||
|
"email": "testuser@tests.bookhoard.internal",
|
||||||
|
"username": "testuser",
|
||||||
|
"password": "Wh4t3v3r@Pass!"
|
||||||
|
}
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
scripts:
|
||||||
|
- type: tests
|
||||||
|
code: |-
|
||||||
|
test("status is 409", function() {
|
||||||
|
expect(res.status).to.eql(409);
|
||||||
|
});
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |-
|
||||||
|
# Register - Duplicate Email (409)
|
||||||
|
|
||||||
|
Registering with an email that already exists must fail with 409 and
|
||||||
|
create nothing. Negative scenario — hand-authored (the generated set
|
||||||
|
covers only happy paths). Repeats safely.
|
||||||
|
type: text/markdown
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
info:
|
||||||
|
name: Register - Weak Password (400)
|
||||||
|
type: http
|
||||||
|
seq: 3
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: POST
|
||||||
|
url: '{{base_url}}/api/auth/register'
|
||||||
|
auth: inherit
|
||||||
|
body:
|
||||||
|
type: json
|
||||||
|
data: |-
|
||||||
|
{
|
||||||
|
"email": "smoke-weak-attempts@tests.bookhoard.internal",
|
||||||
|
"username": "smokeweakattempts",
|
||||||
|
"password": "weak"
|
||||||
|
}
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
scripts:
|
||||||
|
- type: tests
|
||||||
|
code: |-
|
||||||
|
test("status is 400", function() {
|
||||||
|
expect(res.status).to.eql(400);
|
||||||
|
});
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |-
|
||||||
|
# Register - Weak Password (400)
|
||||||
|
|
||||||
|
Passwords below the configured complexity rules (see
|
||||||
|
GET /api/auth/password-rules) must be rejected with 400. Repeats safely.
|
||||||
|
type: text/markdown
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
info:
|
||||||
|
name: Auto Link Books
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: POST
|
||||||
|
url: '{{base_url}}/api/sync/auto-link-books'
|
||||||
|
auth: inherit
|
||||||
|
body:
|
||||||
|
type: json
|
||||||
|
data: |-
|
||||||
|
{
|
||||||
|
"confidence_threshold": 0.8,
|
||||||
|
"limit": 100
|
||||||
|
}
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
scripts:
|
||||||
|
- type: tests
|
||||||
|
code: |-
|
||||||
|
test("status is 200", function() {
|
||||||
|
expect(res.status).to.eql(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |-
|
||||||
|
# Auto Link Books
|
||||||
|
|
||||||
|
`POST /api/sync/auto-link-books` — auth: jwt.
|
||||||
|
|
||||||
|
Canonical reference: docs/developer/api/book-matching/auto_link_books.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||||
|
type: text/markdown
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
info:
|
||||||
|
name: Bulk Link Books
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: POST
|
||||||
|
url: '{{base_url}}/api/sync/bulk-link-books'
|
||||||
|
auth: inherit
|
||||||
|
body:
|
||||||
|
type: json
|
||||||
|
data: |-
|
||||||
|
{
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"unlinked_book_id": "770e8400-e29b-41d4-a716-446655440002",
|
||||||
|
"media_item_id": "660e8400-e29b-41d4-a716-446655440001",
|
||||||
|
"confidence_score": 0.95
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"unlinked_book_id": "880e8400-e29b-41d4-a716-446655440003",
|
||||||
|
"media_item_id": "990e8400-e29b-41d4-a716-446655440004",
|
||||||
|
"confidence_score": 0.87
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
scripts:
|
||||||
|
- type: tests
|
||||||
|
code: |-
|
||||||
|
test("status is 200", function() {
|
||||||
|
expect(res.status).to.eql(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |-
|
||||||
|
# Bulk Link Books
|
||||||
|
|
||||||
|
`POST /api/sync/bulk-link-books` — auth: jwt.
|
||||||
|
|
||||||
|
Canonical reference: docs/developer/api/book-matching/bulk_link_books.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||||
|
type: text/markdown
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
info:
|
||||||
|
name: Create Device File Alias
|
||||||
|
type: http
|
||||||
|
seq: 3
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: POST
|
||||||
|
url: '{{base_url}}/api/devices/{{device_id}}/file-aliases'
|
||||||
|
auth: inherit
|
||||||
|
body:
|
||||||
|
type: json
|
||||||
|
data: |-
|
||||||
|
{
|
||||||
|
"media_item_id": "550e8400-e29b-41d4-a716-446655440001",
|
||||||
|
"file_name": "book.epub",
|
||||||
|
"file_hash": "abc123..."
|
||||||
|
}
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
scripts:
|
||||||
|
- type: tests
|
||||||
|
code: |-
|
||||||
|
test("status is 201", function() {
|
||||||
|
expect(res.status).to.eql(201);
|
||||||
|
});
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |-
|
||||||
|
# Create Device File Alias
|
||||||
|
|
||||||
|
`POST /api/devices/:id/file-aliases` — auth: jwt.
|
||||||
|
|
||||||
|
Canonical reference: docs/developer/api/book-matching/create_device_file_alias.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||||
|
type: text/markdown
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
info:
|
||||||
|
name: Delete Device File Alias
|
||||||
|
type: http
|
||||||
|
seq: 4
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: DELETE
|
||||||
|
url: '{{base_url}}/api/devices/{{device_id}}/file-aliases/{{alias_id}}'
|
||||||
|
auth: inherit
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
scripts:
|
||||||
|
- type: tests
|
||||||
|
code: |-
|
||||||
|
test("status is 204", function() {
|
||||||
|
expect(res.status).to.eql(204);
|
||||||
|
});
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |-
|
||||||
|
# Delete Device File Alias
|
||||||
|
|
||||||
|
`DELETE /api/devices/:id/file-aliases/:aliasId` — auth: jwt.
|
||||||
|
|
||||||
|
Canonical reference: docs/developer/api/book-matching/delete_device_file_alias.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||||
|
type: text/markdown
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
info:
|
||||||
|
name: Get Book Matches
|
||||||
|
type: http
|
||||||
|
seq: 5
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: GET
|
||||||
|
url: '{{base_url}}/api/books/match'
|
||||||
|
auth: inherit
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
scripts:
|
||||||
|
- type: tests
|
||||||
|
code: |-
|
||||||
|
test("status is 200", function() {
|
||||||
|
expect(res.status).to.eql(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |-
|
||||||
|
# Get Book Matches
|
||||||
|
|
||||||
|
`GET /api/books/match` — auth: jwt.
|
||||||
|
|
||||||
|
Canonical reference: docs/developer/api/book-matching/get_book_matches.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||||
|
type: text/markdown
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
info:
|
||||||
|
name: Get Device File Aliases
|
||||||
|
type: http
|
||||||
|
seq: 6
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: GET
|
||||||
|
url: '{{base_url}}/api/devices/{{device_id}}/file-aliases'
|
||||||
|
auth: inherit
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
scripts:
|
||||||
|
- type: tests
|
||||||
|
code: |-
|
||||||
|
test("status is 200", function() {
|
||||||
|
expect(res.status).to.eql(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |-
|
||||||
|
# Get Device File Aliases
|
||||||
|
|
||||||
|
`GET /api/devices/:id/file-aliases` — auth: jwt.
|
||||||
|
|
||||||
|
Canonical reference: docs/developer/api/book-matching/get_device_file_aliases.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||||
|
type: text/markdown
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
info:
|
||||||
|
name: Get Unlinked Book Suggestions
|
||||||
|
type: http
|
||||||
|
seq: 7
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: GET
|
||||||
|
url: '{{base_url}}/api/sync/unlinked-books/{{unlinked_book_id}}/suggestions'
|
||||||
|
auth: inherit
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
scripts:
|
||||||
|
- type: tests
|
||||||
|
code: |-
|
||||||
|
test("status is 200", function() {
|
||||||
|
expect(res.status).to.eql(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |-
|
||||||
|
# Get Unlinked Book Suggestions
|
||||||
|
|
||||||
|
`GET /api/sync/unlinked-books/:id/suggestions` — auth: jwt.
|
||||||
|
|
||||||
|
Canonical reference: docs/developer/api/book-matching/get_unlinked_book_suggestions.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||||
|
type: text/markdown
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
info:
|
||||||
|
name: Get Unlinked Books
|
||||||
|
type: http
|
||||||
|
seq: 8
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: GET
|
||||||
|
url: '{{base_url}}/api/devices/{{device_id}}/sync/unlinked-books'
|
||||||
|
auth: inherit
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
scripts:
|
||||||
|
- type: tests
|
||||||
|
code: |-
|
||||||
|
test("status is 200", function() {
|
||||||
|
expect(res.status).to.eql(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |-
|
||||||
|
# Get Unlinked Books
|
||||||
|
|
||||||
|
`GET /api/devices/:deviceId/sync/unlinked-books` — auth: jwt.
|
||||||
|
|
||||||
|
Canonical reference: docs/developer/api/book-matching/get_unlinked_books.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||||
|
type: text/markdown
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
info:
|
||||||
|
name: Link Book
|
||||||
|
type: http
|
||||||
|
seq: 9
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: POST
|
||||||
|
url: '{{base_url}}/api/devices/{{device_id}}/sync/link-book'
|
||||||
|
auth: inherit
|
||||||
|
body:
|
||||||
|
type: json
|
||||||
|
data: |-
|
||||||
|
{
|
||||||
|
"device_file": {
|
||||||
|
"file_path": "/books/hobbit.epub",
|
||||||
|
"sha256": "abc123...",
|
||||||
|
"title": "The Hobbit"
|
||||||
|
},
|
||||||
|
"media_item_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"confidence_score": 1.0
|
||||||
|
}
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
scripts:
|
||||||
|
- type: tests
|
||||||
|
code: |-
|
||||||
|
test("status is 200", function() {
|
||||||
|
expect(res.status).to.eql(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |-
|
||||||
|
# Link Book
|
||||||
|
|
||||||
|
`POST /api/devices/:deviceId/sync/link-book` — auth: jwt.
|
||||||
|
|
||||||
|
Canonical reference: docs/developer/api/book-matching/link_book.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||||
|
type: text/markdown
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
info:
|
||||||
|
name: Search Books for Matching
|
||||||
|
type: http
|
||||||
|
seq: 10
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: POST
|
||||||
|
url: '{{base_url}}/api/sync/books/query'
|
||||||
|
auth: inherit
|
||||||
|
body:
|
||||||
|
type: json
|
||||||
|
data: |-
|
||||||
|
{
|
||||||
|
"identifiers": ["isbn:978-0345391802", "uuid:abc-123"],
|
||||||
|
"sha256": "a1b2c3d4e5f6abc123...",
|
||||||
|
"title": "The Hobbit",
|
||||||
|
"author": "J.R.R. Tolkien",
|
||||||
|
"file_size": 2456789
|
||||||
|
}
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
scripts:
|
||||||
|
- type: tests
|
||||||
|
code: |-
|
||||||
|
test("status is 200", function() {
|
||||||
|
expect(res.status).to.eql(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |-
|
||||||
|
# Search Books for Matching
|
||||||
|
|
||||||
|
`POST /api/sync/books/query` — auth: jwt.
|
||||||
|
|
||||||
|
Canonical reference: docs/developer/api/book-matching/search_books.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||||
|
type: text/markdown
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
info:
|
||||||
|
name: Update Device File Alias
|
||||||
|
type: http
|
||||||
|
seq: 11
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: PUT
|
||||||
|
url: '{{base_url}}/api/devices/{{device_id}}/file-aliases/{{alias_id}}'
|
||||||
|
auth: inherit
|
||||||
|
body:
|
||||||
|
type: json
|
||||||
|
data: |-
|
||||||
|
{
|
||||||
|
"file_name": "updated-book.epub",
|
||||||
|
"file_hash": "newhash123..."
|
||||||
|
}
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
scripts:
|
||||||
|
- type: tests
|
||||||
|
code: |-
|
||||||
|
test("status is 200", function() {
|
||||||
|
expect(res.status).to.eql(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |-
|
||||||
|
# Update Device File Alias
|
||||||
|
|
||||||
|
`PUT /api/devices/:id/file-aliases/:aliasId` — auth: jwt.
|
||||||
|
|
||||||
|
Canonical reference: docs/developer/api/book-matching/update_device_file_alias.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||||
|
type: text/markdown
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
info:
|
|
||||||
name: Bulk Delete Media Items
|
|
||||||
type: http
|
|
||||||
seq: 1
|
|
||||||
http:
|
|
||||||
method: POST
|
|
||||||
url: '{{base_url}}/api/media-items/bulk-delete'
|
|
||||||
auth: inherit
|
|
||||||
body:
|
|
||||||
type: json
|
|
||||||
data: "{\n \"media_item_ids\": [\n \"{{bookId1"
|
|
||||||
headers:
|
|
||||||
- key: Content-Type
|
|
||||||
value: application/json
|
|
||||||
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 ]"
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
info:
|
|
||||||
name: Bulk Update Media Items
|
|
||||||
type: http
|
|
||||||
seq: 2
|
|
||||||
http:
|
|
||||||
method: POST
|
|
||||||
url: '{{base_url}}/api/media-items/bulk-update'
|
|
||||||
auth: inherit
|
|
||||||
body:
|
|
||||||
type: json
|
|
||||||
data: "{\n \"media_item_updates\": [\n {\n \"media_item_id\": \"{{bookId1"
|
|
||||||
headers:
|
|
||||||
- key: Content-Type
|
|
||||||
value: application/json
|
|
||||||
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\"]"
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
info:
|
|
||||||
name: Add Books to Collection
|
|
||||||
type: http
|
|
||||||
seq: 6
|
|
||||||
http:
|
|
||||||
method: POST
|
|
||||||
url: '{{base_url}}/api/collections/{{collection_id}}/books'
|
|
||||||
auth: inherit
|
|
||||||
body:
|
|
||||||
type: json
|
|
||||||
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"
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
info:
|
|
||||||
name: Create Collection
|
|
||||||
type: http
|
|
||||||
seq: 1
|
|
||||||
http:
|
|
||||||
method: POST
|
|
||||||
url: '{{base_url}}/api/collections'
|
|
||||||
auth: inherit
|
|
||||||
body:
|
|
||||||
type: json
|
|
||||||
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"
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
info:
|
|
||||||
name: Create Device Mapping
|
|
||||||
type: http
|
|
||||||
seq: 9
|
|
||||||
http:
|
|
||||||
method: POST
|
|
||||||
url: '{{base_url}}/api/devices/{{device_id}}/collections'
|
|
||||||
auth: inherit
|
|
||||||
body:
|
|
||||||
type: json
|
|
||||||
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"
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
info:
|
|
||||||
name: Delete Collection
|
|
||||||
type: http
|
|
||||||
seq: 5
|
|
||||||
http:
|
|
||||||
method: DELETE
|
|
||||||
url: '{{base_url}}/api/collections/{{collection_id}}'
|
|
||||||
auth: inherit
|
|
||||||
|
|
||||||
docs: |-
|
|
||||||
## Delete Collection
|
|
||||||
|
|
||||||
Permanently deletes a collection and all its mappings.
|
|
||||||
|
|
||||||
**Method:** DELETE
|
|
||||||
|
|
||||||
**Endpoint:** /api/collections/{collection_id}
|
|
||||||
|
|
||||||
**Authentication:** Required (Bearer token)
|
|
||||||
|
|
||||||
**Path Parameters:**
|
|
||||||
- `collection_id` (string): Collection ID
|
|
||||||
|
|
||||||
**Response:**
|
|
||||||
- `message` (string): Success message
|
|
||||||
|
|
||||||
**Status Codes:**
|
|
||||||
- 200: Success
|
|
||||||
- 401: Unauthorized
|
|
||||||
- 404: Collection not found
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
info:
|
|
||||||
name: Delete Device Mapping
|
|
||||||
type: http
|
|
||||||
seq: 11
|
|
||||||
http:
|
|
||||||
method: DELETE
|
|
||||||
url: '{{base_url}}/api/devices/{{device_id}}/collections/{{mapping_id}}'
|
|
||||||
auth: inherit
|
|
||||||
|
|
||||||
docs: |-
|
|
||||||
## Delete Device Mapping
|
|
||||||
|
|
||||||
Removes a collection-to-device mapping.
|
|
||||||
|
|
||||||
**Method:** DELETE
|
|
||||||
|
|
||||||
**Endpoint:** /api/devices/{device_id}/collections/{mapping_id}
|
|
||||||
|
|
||||||
**Authentication:** Required (Bearer token)
|
|
||||||
|
|
||||||
**Path Parameters:**
|
|
||||||
- `device_id` (string): Device ID
|
|
||||||
- `mapping_id` (string): Mapping ID
|
|
||||||
|
|
||||||
**Response:**
|
|
||||||
- `message` (string): Success message
|
|
||||||
|
|
||||||
**Status Codes:**
|
|
||||||
- 200: Success
|
|
||||||
- 401: Unauthorized
|
|
||||||
- 404: Device or mapping not found
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
info:
|
|
||||||
name: Get Book Collections
|
|
||||||
type: http
|
|
||||||
seq: 12
|
|
||||||
|
|
||||||
http:
|
|
||||||
method: GET
|
|
||||||
url: "{{base_url}}/api/collections/books"
|
|
||||||
body:
|
|
||||||
type: json
|
|
||||||
data: |-
|
|
||||||
{
|
|
||||||
"collection_id": "{{collection_id}}"
|
|
||||||
}
|
|
||||||
auth: inherit
|
|
||||||
|
|
||||||
settings:
|
|
||||||
encodeUrl: true
|
|
||||||
timeout: 0
|
|
||||||
followRedirects: true
|
|
||||||
maxRedirects: 5
|
|
||||||
|
|
||||||
docs: |-
|
|
||||||
## Get Book Collections
|
|
||||||
|
|
||||||
Retrieves all collections that a specific book belongs to.
|
|
||||||
|
|
||||||
**Method:** GET
|
|
||||||
|
|
||||||
**Endpoint:** /api/collections/books
|
|
||||||
|
|
||||||
**Authentication:** Required (Bearer token)
|
|
||||||
|
|
||||||
**Query Parameters:**
|
|
||||||
- `book_id` (string, required): Book ID
|
|
||||||
|
|
||||||
**Response:**
|
|
||||||
- Array of collection objects containing the book
|
|
||||||
|
|
||||||
**Status Codes:**
|
|
||||||
- 200: Success
|
|
||||||
- 400: Invalid request
|
|
||||||
- 401: Unauthorized
|
|
||||||
- 404: Book not found
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
info:
|
|
||||||
name: Get Collection
|
|
||||||
type: http
|
|
||||||
seq: 3
|
|
||||||
http:
|
|
||||||
method: GET
|
|
||||||
url: '{{base_url}}/api/collections/{{collection_id}}'
|
|
||||||
auth: inherit
|
|
||||||
|
|
||||||
docs: |-
|
|
||||||
## Get Collection
|
|
||||||
|
|
||||||
Retrieves detailed information about a specific collection.
|
|
||||||
|
|
||||||
**Method:** GET
|
|
||||||
|
|
||||||
**Endpoint:** /api/collections/{collection_id}
|
|
||||||
|
|
||||||
**Authentication:** Required (Bearer token)
|
|
||||||
|
|
||||||
**Path Parameters:**
|
|
||||||
- `collection_id` (string): Collection ID
|
|
||||||
|
|
||||||
**Response:**
|
|
||||||
- Collection object with all fields including books, rules, and mappings
|
|
||||||
|
|
||||||
**Status Codes:**
|
|
||||||
- 200: Success
|
|
||||||
- 401: Unauthorized
|
|
||||||
- 404: Collection not found
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
info:
|
|
||||||
name: Get Collections
|
|
||||||
type: http
|
|
||||||
seq: 2
|
|
||||||
http:
|
|
||||||
method: GET
|
|
||||||
url: '{{base_url}}/api/collections?include_auto=true&sort_by=name'
|
|
||||||
auth: inherit
|
|
||||||
|
|
||||||
docs: |-
|
|
||||||
## Get Collections
|
|
||||||
|
|
||||||
Retrieves all collections for the authenticated user.
|
|
||||||
|
|
||||||
**Method:** GET
|
|
||||||
|
|
||||||
**Endpoint:** /api/collections
|
|
||||||
|
|
||||||
**Authentication:** Required (Bearer token)
|
|
||||||
|
|
||||||
**Query Parameters:**
|
|
||||||
- `include_auto` (boolean, optional): Include auto-created collections (default false)
|
|
||||||
- `sort_by` (string, optional): Sort field (name, created_at, book_count)
|
|
||||||
- `order` (string, optional): Sort order (asc, desc)
|
|
||||||
|
|
||||||
**Response:**
|
|
||||||
- Array of collection objects
|
|
||||||
|
|
||||||
**Status Codes:**
|
|
||||||
- 200: Success
|
|
||||||
- 401: Unauthorized
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
info:
|
|
||||||
name: Get Device Mappings
|
|
||||||
type: http
|
|
||||||
seq: 8
|
|
||||||
http:
|
|
||||||
method: GET
|
|
||||||
url: '{{base_url}}/api/devices/{{device_id}}/collections'
|
|
||||||
auth: inherit
|
|
||||||
|
|
||||||
docs: |-
|
|
||||||
## Get Device Mappings
|
|
||||||
|
|
||||||
Retrieves all collection mappings for a specific device.
|
|
||||||
|
|
||||||
**Method:** GET
|
|
||||||
|
|
||||||
**Endpoint:** /api/devices/{device_id}/collections
|
|
||||||
|
|
||||||
**Authentication:** Required (Bearer token)
|
|
||||||
|
|
||||||
**Path Parameters:**
|
|
||||||
- `device_id` (string): Device ID
|
|
||||||
|
|
||||||
**Response:**
|
|
||||||
- Array of device mapping objects with collection details
|
|
||||||
|
|
||||||
**Status Codes:**
|
|
||||||
- 200: Success
|
|
||||||
- 401: Unauthorized
|
|
||||||
- 404: Device not found
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
info:
|
|
||||||
name: Remove Book from Collection
|
|
||||||
type: http
|
|
||||||
seq: 7
|
|
||||||
http:
|
|
||||||
method: DELETE
|
|
||||||
url: '{{base_url}}/api/collections/{{collection_id}}/books/{{book_id}}'
|
|
||||||
auth: inherit
|
|
||||||
|
|
||||||
docs: |-
|
|
||||||
## Remove Book from Collection
|
|
||||||
|
|
||||||
Removes a single book from a collection.
|
|
||||||
|
|
||||||
**Method:** DELETE
|
|
||||||
|
|
||||||
**Endpoint:** /api/collections/{collection_id}/books/{book_id}
|
|
||||||
|
|
||||||
**Authentication:** Required (Bearer token)
|
|
||||||
|
|
||||||
**Path Parameters:**
|
|
||||||
- `collection_id` (string): Collection ID
|
|
||||||
- `book_id` (string): Book ID
|
|
||||||
|
|
||||||
**Response:**
|
|
||||||
- `message` (string): Success message
|
|
||||||
|
|
||||||
**Status Codes:**
|
|
||||||
- 200: Success
|
|
||||||
- 401: Unauthorized
|
|
||||||
- 404: Collection or book not found
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
info:
|
|
||||||
name: Update Collection
|
|
||||||
type: http
|
|
||||||
seq: 4
|
|
||||||
http:
|
|
||||||
method: PUT
|
|
||||||
url: '{{base_url}}/api/collections/{{collection_id}}'
|
|
||||||
auth: inherit
|
|
||||||
body:
|
|
||||||
type: json
|
|
||||||
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"
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
info:
|
|
||||||
name: Update Device Mapping
|
|
||||||
type: http
|
|
||||||
seq: 10
|
|
||||||
http:
|
|
||||||
method: PUT
|
|
||||||
url: '{{base_url}}/api/devices/{{device_id}}/collections/{{mapping_id}}'
|
|
||||||
auth: inherit
|
|
||||||
body:
|
|
||||||
type: json
|
|
||||||
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"
|
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
info:
|
||||||
|
name: Add Auto-Assign Rule
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: POST
|
||||||
|
url: '{{base_url}}/api/collections/{{collection_id}}/rules'
|
||||||
|
auth: inherit
|
||||||
|
body:
|
||||||
|
type: json
|
||||||
|
data: |-
|
||||||
|
{
|
||||||
|
"field": "genre",
|
||||||
|
"operator": "equals",
|
||||||
|
"value": "Science Fiction",
|
||||||
|
"priority": 1,
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
scripts:
|
||||||
|
- type: tests
|
||||||
|
code: |-
|
||||||
|
test("status is 201", function() {
|
||||||
|
expect(res.status).to.eql(201);
|
||||||
|
});
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |-
|
||||||
|
# Add Auto-Assign Rule
|
||||||
|
|
||||||
|
`POST /api/collections/:id/rules` — auth: jwt.
|
||||||
|
|
||||||
|
Canonical reference: docs/developer/api/collections/add_auto_assign_rule.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||||
|
type: text/markdown
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
info:
|
||||||
|
name: Bulk Add Books
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: POST
|
||||||
|
url: '{{base_url}}/api/collections/bulk-add-books'
|
||||||
|
auth: inherit
|
||||||
|
body:
|
||||||
|
type: json
|
||||||
|
data: |-
|
||||||
|
{
|
||||||
|
"operations": [
|
||||||
|
{
|
||||||
|
"collection_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"book_ids": [
|
||||||
|
"660e8400-e29b-41d4-a716-446655440001"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |-
|
||||||
|
# Bulk Add Books
|
||||||
|
|
||||||
|
`POST /api/collections/bulk-add-books` — auth: jwt.
|
||||||
|
|
||||||
|
Canonical reference: docs/developer/api/collections/bulk_add_books.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||||
|
type: text/markdown
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
info:
|
||||||
|
name: Bulk Assign Books
|
||||||
|
type: http
|
||||||
|
seq: 3
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: POST
|
||||||
|
url: '{{base_url}}/api/collections/{{collection_id}}/books'
|
||||||
|
auth: inherit
|
||||||
|
body:
|
||||||
|
type: json
|
||||||
|
data: |-
|
||||||
|
{
|
||||||
|
"book_ids": [
|
||||||
|
"660e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"770e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"880e8400-e29b-41d4-a716-446655440000"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
scripts:
|
||||||
|
- type: tests
|
||||||
|
code: |-
|
||||||
|
test("status is 204", function() {
|
||||||
|
expect(res.status).to.eql(204);
|
||||||
|
});
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |-
|
||||||
|
# Bulk Assign Books
|
||||||
|
|
||||||
|
`POST /api/collections/:id/books` — auth: jwt.
|
||||||
|
|
||||||
|
Canonical reference: docs/developer/api/collections/bulk_assign.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||||
|
type: text/markdown
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
info:
|
||||||
|
name: Bulk Remove Books
|
||||||
|
type: http
|
||||||
|
seq: 4
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: POST
|
||||||
|
url: '{{base_url}}/api/collections/{{collection_id}}/books/bulk-remove'
|
||||||
|
auth: inherit
|
||||||
|
body:
|
||||||
|
type: json
|
||||||
|
data: |-
|
||||||
|
{
|
||||||
|
"book_ids": [
|
||||||
|
"550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"660e8400-e29b-41d4-a716-446655440001"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |-
|
||||||
|
# Bulk Remove Books
|
||||||
|
|
||||||
|
`POST /api/collections/:id/books/bulk-remove` — auth: jwt.
|
||||||
|
|
||||||
|
Canonical reference: docs/developer/api/collections/bulk_remove_books.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||||
|
type: text/markdown
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
meta:
|
|
||||||
name: Create Custom Section with Manual Books
|
|
||||||
type: http
|
|
||||||
seq: 2
|
|
||||||
http:
|
|
||||||
method: POST
|
|
||||||
url: '{{base_url}}/api/collections'
|
|
||||||
auth: inherit
|
|
||||||
body:
|
|
||||||
type: json
|
|
||||||
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
|
|
||||||
code: "function onResponse(res) {\ntest(\"status must be 201 with valid request\", function() {\n expect(res.status).to.eql(201);\n});\n\ntest(\"response contains collection id\", function() {\n expect(res.body.id).to.exist;\nexpect(res.body.id).to.be.a('string');\n });\n\ntest(\"response contains collection name\", function() {\n expect(res.body.name).to.eql(\"My Favorites\");\n });\n}"
|
|
||||||
docs: "Create a custom collection with manually selected books.\n\n**Endpoint**: POST /api/collections\n**Auth**: Required (Bearer token)\n\n## Example Request\n\n```json\n{\n \"library_id\": \"cc23c3a7-f8fb-451a-a78d-2a16df1b725a\",\n \"name\": \"My Favorites\",\n \"icon\": \"⭐\",\n \"description\": \"My favorite books\",\n \"show_on_dashboard\": true,\n \"auto_assign_rules\": \"\",\n \"manual_book_ids\": [\"book-id-1\", \"book-id-2\"],\n \"match_type\": \"all\"\n}\n```\n\n## Response\n\nReturns the created collection with generated ID."
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
meta:
|
|
||||||
name: Create Custom Section Missing Required Fields
|
|
||||||
type: http
|
|
||||||
seq: 3
|
|
||||||
http:
|
|
||||||
method: POST
|
|
||||||
url: '{{base_url}}/api/collections'
|
|
||||||
auth: inherit
|
|
||||||
body:
|
|
||||||
type: json
|
|
||||||
data: "{\n \"library_id\": \"{{library_id}}\",\n \"icon\": \"\U0001F4DA\"\n}"
|
|
||||||
runtime:
|
|
||||||
scripts:
|
|
||||||
- type: after-response
|
|
||||||
code: "function onResponse(res) {\ntest(\"status must be 400 with missing name\", function() {\n expect(res.status).to.eql(400);\n});\n\ntest(\"response contains error message\", function() {\n expect(res.body.error).to.exist;\n });\n}"
|
|
||||||
docs: 'Test error handling when creating a collection without required fields.
|
|
||||||
|
|
||||||
|
|
||||||
**Expected Response**: 400 Bad Request with error message.'
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
meta:
|
|
||||||
name: Create Custom Section with Filter Rules
|
|
||||||
type: http
|
|
||||||
seq: 1
|
|
||||||
http:
|
|
||||||
method: POST
|
|
||||||
url: '{{base_url}}/api/collections'
|
|
||||||
auth: inherit
|
|
||||||
body:
|
|
||||||
type: json
|
|
||||||
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
|
|
||||||
code: "function onResponse(res) {\ntest(\"status must be 201 with valid request\", function() {\n expect(res.status).to.eql(201);\n});\n\ntest(\"response contains collection id\", function() {\n expect(res.body.id).to.exist;\nexpect(res.body.id).to.be.a('string');\n });\n\ntest(\"response contains collection name\", function() {\n expect(res.body.name).to.eql(\"Science Fiction Books\");\n });\n\ntest(\"response contains is_system false\", function() {\n expect(res.body.is_system).to.exist;\n expect(res.body.is_system).to.eql(false);\n });\n}"
|
|
||||||
docs: "Create a custom collection with filter rules.\n\n**Endpoint**: POST /api/collections\n**Auth**: Required (Bearer token)\n\n## Request Body\n\n| Field | Type | Required | Description |\n|-------|------|----------|-------------|\n| library_id | string | Yes | Library UUID |\n| name | string | Yes | Collection name |\n| icon | string | No | Emoji icon |\n| description | string | No | Collection description |\n| show_on_dashboard | boolean | No | Show on dashboard (default: true) |\n| auto_assign_rules | string | No | JSON string of filter rules |\n| manual_book_ids | array | No | Array of book IDs |\n| match_type | string | No | \"all\" (AND) or \"any\" (OR) |\n\n## Example Request\n\n```json\n{\n \"library_id\": \"cc23c3a7-f8fb-451a-a78d-2a16df1b725a\",\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}\n```\n\n## Response\n\nReturns the created collection with generated ID."
|
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
info:
|
||||||
|
name: Create Collection
|
||||||
|
type: http
|
||||||
|
seq: 5
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: POST
|
||||||
|
url: '{{base_url}}/api/collections'
|
||||||
|
auth: inherit
|
||||||
|
body:
|
||||||
|
type: json
|
||||||
|
data: |-
|
||||||
|
{
|
||||||
|
"name": "To Read",
|
||||||
|
"description": "Books I want to read soon",
|
||||||
|
"color": "#00FF00",
|
||||||
|
"icon": "📖",
|
||||||
|
"collection_type": "static",
|
||||||
|
"auto_assign_rules": [],
|
||||||
|
"view_settings": { "kobo": { "view_mode": "grid" } }
|
||||||
|
}
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |-
|
||||||
|
# Create Collection
|
||||||
|
|
||||||
|
`POST /api/collections` — auth: jwt.
|
||||||
|
|
||||||
|
Canonical reference: docs/developer/api/collections/create_collection.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||||
|
type: text/markdown
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
info:
|
||||||
|
name: Create Shelf Mapping
|
||||||
|
type: http
|
||||||
|
seq: 6
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: POST
|
||||||
|
url: '{{base_url}}/api/devices/{{device_id}}/collections'
|
||||||
|
auth: inherit
|
||||||
|
body:
|
||||||
|
type: json
|
||||||
|
data: |-
|
||||||
|
{
|
||||||
|
"collection_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"device_shelf_name": "Sci-Fi",
|
||||||
|
"sync_direction": "bidirectional"
|
||||||
|
}
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
scripts:
|
||||||
|
- type: tests
|
||||||
|
code: |-
|
||||||
|
test("status is 201", function() {
|
||||||
|
expect(res.status).to.eql(201);
|
||||||
|
});
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |-
|
||||||
|
# Create Shelf Mapping
|
||||||
|
|
||||||
|
`POST /api/devices/:id/collections` — auth: jwt.
|
||||||
|
|
||||||
|
Canonical reference: docs/developer/api/collections/create_shelf_mapping.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||||
|
type: text/markdown
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
info:
|
||||||
|
name: Delete Collection
|
||||||
|
type: http
|
||||||
|
seq: 7
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: DELETE
|
||||||
|
url: '{{base_url}}/api/collections/{{collection_id}}'
|
||||||
|
auth: inherit
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
scripts:
|
||||||
|
- type: tests
|
||||||
|
code: |-
|
||||||
|
test("status is 204", function() {
|
||||||
|
expect(res.status).to.eql(204);
|
||||||
|
});
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |-
|
||||||
|
# Delete Collection
|
||||||
|
|
||||||
|
`DELETE /api/collections/:id` — auth: jwt.
|
||||||
|
|
||||||
|
Canonical reference: docs/developer/api/collections/delete_collection.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||||
|
type: text/markdown
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
info:
|
||||||
|
name: Delete Shelf Mapping
|
||||||
|
type: http
|
||||||
|
seq: 8
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: DELETE
|
||||||
|
url: '{{base_url}}/api/devices/{{device_id}}/collections/{{collection_id}}'
|
||||||
|
auth: inherit
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
scripts:
|
||||||
|
- type: tests
|
||||||
|
code: |-
|
||||||
|
test("status is 204", function() {
|
||||||
|
expect(res.status).to.eql(204);
|
||||||
|
});
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |-
|
||||||
|
# Delete Shelf Mapping
|
||||||
|
|
||||||
|
`DELETE /api/devices/:id/collections/:collectionId` — auth: jwt.
|
||||||
|
|
||||||
|
Canonical reference: docs/developer/api/collections/delete_shelf_mapping.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||||
|
type: text/markdown
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
info:
|
||||||
|
name: Get Collection
|
||||||
|
type: http
|
||||||
|
seq: 9
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: GET
|
||||||
|
url: '{{base_url}}/api/collections/{{collection_id}}'
|
||||||
|
auth: inherit
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
scripts:
|
||||||
|
- type: tests
|
||||||
|
code: |-
|
||||||
|
test("status is 200", function() {
|
||||||
|
expect(res.status).to.eql(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |-
|
||||||
|
# Get Collection
|
||||||
|
|
||||||
|
`GET /api/collections/:id` — auth: jwt.
|
||||||
|
|
||||||
|
Canonical reference: docs/developer/api/collections/get_collection.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||||
|
type: text/markdown
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
info:
|
||||||
|
name: Get Book Collections
|
||||||
|
type: http
|
||||||
|
seq: 10
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: GET
|
||||||
|
url: '{{base_url}}/api/collections/{{collection_id}}/books'
|
||||||
|
auth: inherit
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
scripts:
|
||||||
|
- type: tests
|
||||||
|
code: |-
|
||||||
|
test("status is 200", function() {
|
||||||
|
expect(res.status).to.eql(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |-
|
||||||
|
# Get Book Collections
|
||||||
|
|
||||||
|
`GET /api/collections/:id/books` — auth: jwt.
|
||||||
|
|
||||||
|
Canonical reference: docs/developer/api/collections/get_collection_books.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||||
|
type: text/markdown
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
info:
|
||||||
|
name: List Collections
|
||||||
|
type: http
|
||||||
|
seq: 11
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: GET
|
||||||
|
url: '{{base_url}}/api/collections'
|
||||||
|
auth: inherit
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
scripts:
|
||||||
|
- type: tests
|
||||||
|
code: |-
|
||||||
|
test("status is 200", function() {
|
||||||
|
expect(res.status).to.eql(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |-
|
||||||
|
# List Collections
|
||||||
|
|
||||||
|
`GET /api/collections` — auth: jwt.
|
||||||
|
|
||||||
|
Canonical reference: docs/developer/api/collections/list_collections.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||||
|
type: text/markdown
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
meta:
|
|
||||||
name: Preview Collection
|
|
||||||
type: http
|
|
||||||
seq: 1
|
|
||||||
http:
|
|
||||||
method: POST
|
|
||||||
url: '{{base_url}}/api/collections/preview'
|
|
||||||
auth: inherit
|
|
||||||
body:
|
|
||||||
type: json
|
|
||||||
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
|
|
||||||
code: "function onResponse(res) {\ntest(\"status must be 200 with valid request\", function() {\n expect(res.status).to.eql(200);\n});\n\ntest(\"response contains items array\", function() {\n expect(res.body.items).to.exist;\nexpect(res.body.items).to.be.an('array');\n });\n}"
|
|
||||||
docs: "Preview collection with filter rules before saving.\n\n**Endpoint**: POST /api/collections/preview\n**Auth**: Required (Bearer token)\n\n## Request Body\n\n| Field | Type | Required | Description |\n|-------|------|----------|-------------|\n| library_id | string | Yes | Library UUID |\n| rules | array | No | Filter rules to evaluate |\n| manual_book_ids | array | No | Manually selected book IDs |\n| limit | int | No | Max items to return (default: 20, max: 100) |\n\n## Example Request\n\n```json\n{\n \"library_id\": \"cc23c3a7-f8fb-451a-a78d-2a16df1b725a\",\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}\n```\n\n## Response\n\nReturns array of matching book items.\n\nThis endpoint is used by the Custom Section Builder to show users what books will match their filter rules before they save the collection."
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
info:
|
||||||
|
name: Preview Collection
|
||||||
|
type: http
|
||||||
|
seq: 12
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: POST
|
||||||
|
url: '{{base_url}}/api/collections/preview'
|
||||||
|
auth: inherit
|
||||||
|
body:
|
||||||
|
type: json
|
||||||
|
data: |-
|
||||||
|
{
|
||||||
|
"library_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"rules": [],
|
||||||
|
"manual_book_ids": [],
|
||||||
|
"limit": 20
|
||||||
|
}
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |-
|
||||||
|
# Preview Collection
|
||||||
|
|
||||||
|
`POST /api/collections/preview` — auth: jwt.
|
||||||
|
|
||||||
|
Canonical reference: docs/developer/api/collections/preview_collection.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||||
|
type: text/markdown
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
info:
|
||||||
|
name: Remove Auto-Assign Rule
|
||||||
|
type: http
|
||||||
|
seq: 13
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: DELETE
|
||||||
|
url: '{{base_url}}/api/collections/{{collection_id}}/rules/{{rule_id}}'
|
||||||
|
auth: inherit
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
scripts:
|
||||||
|
- type: tests
|
||||||
|
code: |-
|
||||||
|
test("status is 204", function() {
|
||||||
|
expect(res.status).to.eql(204);
|
||||||
|
});
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |-
|
||||||
|
# Remove Auto-Assign Rule
|
||||||
|
|
||||||
|
`DELETE /api/collections/:id/rules/:ruleId` — auth: jwt.
|
||||||
|
|
||||||
|
Canonical reference: docs/developer/api/collections/remove_auto_assign_rule.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||||
|
type: text/markdown
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
info:
|
||||||
|
name: Reorder Collection Items
|
||||||
|
type: http
|
||||||
|
seq: 14
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: PUT
|
||||||
|
url: '{{base_url}}/api/collections/{{collection_id}}/items/order'
|
||||||
|
auth: inherit
|
||||||
|
body:
|
||||||
|
type: json
|
||||||
|
data: |-
|
||||||
|
{
|
||||||
|
"item_ids": [
|
||||||
|
"550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"660e8400-e29b-41d4-a716-446655440001"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |-
|
||||||
|
# Reorder Collection Items
|
||||||
|
|
||||||
|
`PUT /api/collections/:id/items/order` — auth: jwt.
|
||||||
|
|
||||||
|
Canonical reference: docs/developer/api/collections/reorder_items.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||||
|
type: text/markdown
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
info:
|
|
||||||
name: Bulk Add Books to Collections
|
|
||||||
type: http
|
|
||||||
seq: 1
|
|
||||||
http:
|
|
||||||
method: POST
|
|
||||||
url: '{{base_url}}/api/collections/bulk-add-books'
|
|
||||||
auth: inherit
|
|
||||||
body:
|
|
||||||
type: json
|
|
||||||
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\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\"]"
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
info:
|
|
||||||
name: Bulk Remove Books - All Books
|
|
||||||
type: http
|
|
||||||
seq: 5
|
|
||||||
http:
|
|
||||||
method: POST
|
|
||||||
url: '{{base_url}}/api/collections/{{collection_id}}/books/bulk-remove'
|
|
||||||
auth: inherit
|
|
||||||
body:
|
|
||||||
type: json
|
|
||||||
data: "{\n \"book_ids\": [\n \"{{bookId1"
|
|
||||||
headers:
|
|
||||||
- key: Content-Type
|
|
||||||
value: application/json
|
|
||||||
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"
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
info:
|
|
||||||
name: Bulk Remove Books - Empty List
|
|
||||||
type: http
|
|
||||||
seq: 2
|
|
||||||
http:
|
|
||||||
method: POST
|
|
||||||
url: '{{base_url}}/api/collections/{{collection_id}}/books/bulk-remove'
|
|
||||||
auth: inherit
|
|
||||||
body:
|
|
||||||
type: json
|
|
||||||
data: "{\n \"book_ids\": []"
|
|
||||||
headers:
|
|
||||||
- key: Content-Type
|
|
||||||
value: application/json
|
|
||||||
- key: Authorization
|
|
||||||
value: Bearer {{authToken
|
|
||||||
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."
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
info:
|
|
||||||
name: Bulk Remove Books - Invalid IDs
|
|
||||||
type: http
|
|
||||||
seq: 4
|
|
||||||
http:
|
|
||||||
method: POST
|
|
||||||
url: '{{base_url}}/api/collections/{{collection_id}}/books/bulk-remove'
|
|
||||||
auth: inherit
|
|
||||||
body:
|
|
||||||
type: json
|
|
||||||
data: "{\n \"book_ids\": [\n \"{{bookId1"
|
|
||||||
headers:
|
|
||||||
- key: Content-Type
|
|
||||||
value: application/json
|
|
||||||
- key: Authorization
|
|
||||||
value: Bearer {{authToken
|
|
||||||
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."
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
info:
|
|
||||||
name: Bulk Remove Books - Single Book
|
|
||||||
type: http
|
|
||||||
seq: 3
|
|
||||||
http:
|
|
||||||
method: POST
|
|
||||||
url: '{{base_url}}/api/collections/{{collection_id}}/books/bulk-remove'
|
|
||||||
auth: inherit
|
|
||||||
body:
|
|
||||||
type: json
|
|
||||||
data: "{\n \"book_ids\": [\n \"{{bookId1"
|
|
||||||
headers:
|
|
||||||
- key: Content-Type
|
|
||||||
value: application/json
|
|
||||||
- key: Authorization
|
|
||||||
value: Bearer {{authToken
|
|
||||||
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."
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
info:
|
|
||||||
name: Test Collection Rules - Author Contains
|
|
||||||
type: http
|
|
||||||
seq: 2
|
|
||||||
http:
|
|
||||||
method: POST
|
|
||||||
url: '{{base_url}}/api/collections/test-rules'
|
|
||||||
auth: inherit
|
|
||||||
body:
|
|
||||||
type: json
|
|
||||||
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\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."
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
info:
|
|
||||||
name: Test Collection Rules - Copyright Year Greater Than
|
|
||||||
type: http
|
|
||||||
seq: 3
|
|
||||||
http:
|
|
||||||
method: POST
|
|
||||||
url: '{{base_url}}/api/collections/test-rules'
|
|
||||||
auth: inherit
|
|
||||||
body:
|
|
||||||
type: json
|
|
||||||
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\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."
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
info:
|
|
||||||
name: Test Collection Rules - Empty Rules Array
|
|
||||||
type: http
|
|
||||||
seq: 5
|
|
||||||
http:
|
|
||||||
method: POST
|
|
||||||
url: '{{base_url}}/api/collections/test-rules'
|
|
||||||
auth: inherit
|
|
||||||
body:
|
|
||||||
type: json
|
|
||||||
data: "{\n \"rules\": []"
|
|
||||||
headers:
|
|
||||||
- key: Content-Type
|
|
||||||
value: application/json
|
|
||||||
- key: Authorization
|
|
||||||
value: Bearer {{authToken
|
|
||||||
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."
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
info:
|
|
||||||
name: Test Collection Rules - No Matches
|
|
||||||
type: http
|
|
||||||
seq: 4
|
|
||||||
http:
|
|
||||||
method: POST
|
|
||||||
url: '{{base_url}}/api/collections/test-rules'
|
|
||||||
auth: inherit
|
|
||||||
body:
|
|
||||||
type: json
|
|
||||||
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\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."
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
info:
|
|
||||||
name: Test Collection Rules
|
|
||||||
type: http
|
|
||||||
seq: 1
|
|
||||||
http:
|
|
||||||
method: POST
|
|
||||||
url: '{{base_url}}/api/collections/test-rules'
|
|
||||||
auth: inherit
|
|
||||||
body:
|
|
||||||
type: json
|
|
||||||
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\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\""
|
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
info:
|
||||||
|
name: Test Rule
|
||||||
|
type: http
|
||||||
|
seq: 15
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: POST
|
||||||
|
url: '{{base_url}}/api/collections/test-rules'
|
||||||
|
auth: inherit
|
||||||
|
body:
|
||||||
|
type: json
|
||||||
|
data: |-
|
||||||
|
{
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"field": "genre",
|
||||||
|
"operator": "equals",
|
||||||
|
"value": "Science Fiction"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"field": "author",
|
||||||
|
"operator": "contains",
|
||||||
|
"value": "Asimov"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
scripts:
|
||||||
|
- type: tests
|
||||||
|
code: |-
|
||||||
|
test("status is 200", function() {
|
||||||
|
expect(res.status).to.eql(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |-
|
||||||
|
# Test Rule
|
||||||
|
|
||||||
|
`POST /api/collections/test-rules` — auth: jwt.
|
||||||
|
|
||||||
|
Canonical reference: docs/developer/api/collections/test_rule.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||||
|
type: text/markdown
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
info:
|
||||||
|
name: Update Collection
|
||||||
|
type: http
|
||||||
|
seq: 16
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: PUT
|
||||||
|
url: '{{base_url}}/api/collections/{{collection_id}}'
|
||||||
|
auth: inherit
|
||||||
|
body:
|
||||||
|
type: json
|
||||||
|
data: |-
|
||||||
|
{
|
||||||
|
"name": "To Read (2026)",
|
||||||
|
"description": "Updated description",
|
||||||
|
"color": "#33CCFF",
|
||||||
|
"icon": "📚",
|
||||||
|
"auto_assign_rules": [],
|
||||||
|
"view_settings": { "hide_finished": true },
|
||||||
|
"hide_finished": ["true"],
|
||||||
|
"show_up_next": ["true"]
|
||||||
|
}
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |-
|
||||||
|
# Update Collection
|
||||||
|
|
||||||
|
`PUT /api/collections/:id` — auth: jwt.
|
||||||
|
|
||||||
|
Canonical reference: docs/developer/api/collections/update_collection.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||||
|
type: text/markdown
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
info:
|
||||||
|
name: Update Collection Rule
|
||||||
|
type: http
|
||||||
|
seq: 17
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: PUT
|
||||||
|
url: '{{base_url}}/api/collections/{{collection_id}}/rules/{{rule_id}}'
|
||||||
|
auth: inherit
|
||||||
|
body:
|
||||||
|
type: json
|
||||||
|
data: |-
|
||||||
|
{
|
||||||
|
"enabled": false
|
||||||
|
}
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |-
|
||||||
|
# Update Collection Rule
|
||||||
|
|
||||||
|
`PUT /api/collections/:id/rules/:ruleId` — auth: jwt.
|
||||||
|
|
||||||
|
Canonical reference: docs/developer/api/collections/update_rule.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||||
|
type: text/markdown
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
info:
|
|
||||||
name: Delete Conflict
|
|
||||||
type: http
|
|
||||||
seq: 4
|
|
||||||
http:
|
|
||||||
method: DELETE
|
|
||||||
url: '{{base_url}}/api/conflicts/{{conflict_id}}'
|
|
||||||
auth: inherit
|
|
||||||
body:
|
|
||||||
type: none
|
|
||||||
headers:
|
|
||||||
- key: Authorization
|
|
||||||
value: Bearer {{token
|
|
||||||
|
|
||||||
docs: |-
|
|
||||||
## Delete Conflict
|
|
||||||
|
|
||||||
Permanently deletes a specific conflict record from the system.
|
|
||||||
|
|
||||||
**Method:** DELETE
|
|
||||||
|
|
||||||
**Endpoint:** /api/conflicts/{conflict_id
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
info:
|
|
||||||
name: Dismiss All Resolved Conflicts
|
|
||||||
type: http
|
|
||||||
seq: 5
|
|
||||||
http:
|
|
||||||
method: POST
|
|
||||||
url: '{{base_url}}/api/conflicts/dismiss-all'
|
|
||||||
auth: inherit
|
|
||||||
body:
|
|
||||||
type: none
|
|
||||||
headers:
|
|
||||||
- key: Authorization
|
|
||||||
value: Bearer {{token
|
|
||||||
|
|
||||||
docs: |-
|
|
||||||
## Dismiss All Resolved Conflicts
|
|
||||||
|
|
||||||
Deletes all resolved conflicts for the authenticated user, cleaning up the conflict list.
|
|
||||||
|
|
||||||
**Method:** POST
|
|
||||||
|
|
||||||
**Endpoint:** /api/conflicts/dismiss-all
|
|
||||||
|
|
||||||
**Authentication:** Bearer token
|
|
||||||
|
|
||||||
**Response:**
|
|
||||||
- `deleted` (number): Number of conflict records that were deleted
|
|
||||||
|
|
||||||
**Status Codes:**
|
|
||||||
- 200: Success - conflicts deleted
|
|
||||||
- 401: Unauthorized
|
|
||||||
- 500: Internal server error
|
|
||||||
|
|
||||||
**Example Response:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"deleted": 5
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
info:
|
|
||||||
name: Get Conflict Details
|
|
||||||
type: http
|
|
||||||
seq: 2
|
|
||||||
http:
|
|
||||||
method: GET
|
|
||||||
url: '{{base_url}}/api/conflicts/{{conflict_id}}'
|
|
||||||
auth: inherit
|
|
||||||
body:
|
|
||||||
type: none
|
|
||||||
headers:
|
|
||||||
- key: Authorization
|
|
||||||
value: Bearer {{token
|
|
||||||
|
|
||||||
docs: |-
|
|
||||||
## Get Conflict Details
|
|
||||||
|
|
||||||
Retrieves detailed information about a specific conflict, including side-by-side comparison of conflicting data from all sources.
|
|
||||||
|
|
||||||
**Method:** GET
|
|
||||||
|
|
||||||
**Endpoint:** /api/conflicts/{conflict_id
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
info:
|
|
||||||
name: List Conflicts
|
|
||||||
type: http
|
|
||||||
seq: 1
|
|
||||||
http:
|
|
||||||
method: GET
|
|
||||||
url: '{{base_url}}/api/conflicts?status=unresolved'
|
|
||||||
auth: inherit
|
|
||||||
body:
|
|
||||||
type: none
|
|
||||||
headers:
|
|
||||||
- key: Authorization
|
|
||||||
value: Bearer {{token
|
|
||||||
|
|
||||||
docs: |-
|
|
||||||
## List Conflicts
|
|
||||||
|
|
||||||
Lists all sync conflicts for the authenticated user with optional filtering.
|
|
||||||
|
|
||||||
**Method:** GET
|
|
||||||
|
|
||||||
**Endpoint:** /api/conflicts
|
|
||||||
|
|
||||||
**Authentication:** Bearer token
|
|
||||||
|
|
||||||
**Query Parameters:**
|
|
||||||
- `status` (string, optional): Filter by resolution status
|
|
||||||
- `unresolved`: Only unresolved conflicts (default)
|
|
||||||
- `user_resolved`: Conflicts resolved by user
|
|
||||||
- `auto_resolved`: Automatically resolved conflicts
|
|
||||||
- `all`: All conflicts regardless of status
|
|
||||||
- `type` (string, optional): Filter by conflict type
|
|
||||||
- `progress`: Reading progress conflicts
|
|
||||||
- `note`: Bookmark/note conflicts
|
|
||||||
- `highlight`: Highlight conflicts
|
|
||||||
|
|
||||||
**Response:**
|
|
||||||
- `conflicts` (array): Array of conflict objects
|
|
||||||
- `total` (number): Total number of conflicts matching filters
|
|
||||||
- `unresolved` (number): Number of unresolved conflicts
|
|
||||||
|
|
||||||
**Each Conflict Object:**
|
|
||||||
- `id` (string): Conflict UUID
|
|
||||||
- `media_item_id` (string): Associated book UUID
|
|
||||||
- `media_item_title` (string): Book title
|
|
||||||
- `conflict_type` (string): Type of conflict (progress, note, highlight)
|
|
||||||
- `conflict_data` (object): Side-by-side comparison of conflicting data
|
|
||||||
- `koreader`: Data from KOReader device
|
|
||||||
- `kobo`: Data from Kobo device
|
|
||||||
- `web`: Data from web interface
|
|
||||||
- `resolution_status` (string): Current status (unresolved, user_resolved, auto_resolved)
|
|
||||||
- `created_at` (string): ISO 8601 timestamp when conflict was detected
|
|
||||||
|
|
||||||
**Status Codes:**
|
|
||||||
- 200: Success
|
|
||||||
- 401: Unauthorized
|
|
||||||
- 500: Internal server error
|
|
||||||
|
|
||||||
**Example Request:**
|
|
||||||
```
|
|
||||||
GET /api/conflicts?status=unresolved&type=progress
|
|
||||||
```
|
|
||||||
|
|
||||||
**Example Response:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"conflicts": [
|
|
||||||
{
|
|
||||||
"id": "conflict-uuid",
|
|
||||||
"media_item_id": "book-uuid",
|
|
||||||
"media_item_title": "Foundation",
|
|
||||||
"conflict_type": "progress",
|
|
||||||
"conflict_data": {
|
|
||||||
"koreader": { "percentage": 0.65, "epubcfi": "..."
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
info:
|
|
||||||
name: Resolve Conflict
|
|
||||||
type: http
|
|
||||||
seq: 3
|
|
||||||
http:
|
|
||||||
method: POST
|
|
||||||
url: '{{base_url}}/api/conflicts/{{conflict_id}}/resolve'
|
|
||||||
auth: inherit
|
|
||||||
body:
|
|
||||||
type: json
|
|
||||||
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\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"
|
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
info:
|
||||||
|
name: Bulk Dismiss Conflicts
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: POST
|
||||||
|
url: '{{base_url}}/api/conflicts/bulk-dismiss'
|
||||||
|
auth: inherit
|
||||||
|
body:
|
||||||
|
type: json
|
||||||
|
data: |-
|
||||||
|
{
|
||||||
|
"conflict_ids": [
|
||||||
|
"550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"660e8400-e29b-41d4-a716-446655440001"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
scripts:
|
||||||
|
- type: tests
|
||||||
|
code: |-
|
||||||
|
test("status is 200", function() {
|
||||||
|
expect(res.status).to.eql(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |-
|
||||||
|
# Bulk Dismiss Conflicts
|
||||||
|
|
||||||
|
`POST /api/conflicts/bulk-dismiss` — auth: jwt.
|
||||||
|
|
||||||
|
Canonical reference: docs/developer/api/conflicts/bulk_dismiss_conflicts.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||||
|
type: text/markdown
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
info:
|
||||||
|
name: Bulk Resolve Conflicts
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: POST
|
||||||
|
url: '{{base_url}}/api/conflicts/bulk-resolve'
|
||||||
|
auth: inherit
|
||||||
|
body:
|
||||||
|
type: json
|
||||||
|
data: |-
|
||||||
|
{
|
||||||
|
"conflict_ids": [
|
||||||
|
"550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"660e8400-e29b-41d4-a716-446655440001"
|
||||||
|
],
|
||||||
|
"strategy": "highest_progress"
|
||||||
|
}
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
scripts:
|
||||||
|
- type: tests
|
||||||
|
code: |-
|
||||||
|
test("status is 200", function() {
|
||||||
|
expect(res.status).to.eql(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |-
|
||||||
|
# Bulk Resolve Conflicts
|
||||||
|
|
||||||
|
`POST /api/conflicts/bulk-resolve` — auth: jwt.
|
||||||
|
|
||||||
|
Canonical reference: docs/developer/api/conflicts/bulk_resolve_conflicts.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||||
|
type: text/markdown
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
info:
|
||||||
|
name: Delete Conflict
|
||||||
|
type: http
|
||||||
|
seq: 3
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: DELETE
|
||||||
|
url: '{{base_url}}/api/conflicts/{{media_item_id}}'
|
||||||
|
auth: inherit
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
scripts:
|
||||||
|
- type: tests
|
||||||
|
code: |-
|
||||||
|
test("status is 204", function() {
|
||||||
|
expect(res.status).to.eql(204);
|
||||||
|
});
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |-
|
||||||
|
# Delete Conflict
|
||||||
|
|
||||||
|
`DELETE /api/conflicts/:id` — auth: jwt.
|
||||||
|
|
||||||
|
Canonical reference: docs/developer/api/conflicts/delete_conflict.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||||
|
type: text/markdown
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
info:
|
||||||
|
name: Dismiss All Resolved
|
||||||
|
type: http
|
||||||
|
seq: 4
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: POST
|
||||||
|
url: '{{base_url}}/api/conflicts/dismiss-all'
|
||||||
|
auth: inherit
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
scripts:
|
||||||
|
- type: tests
|
||||||
|
code: |-
|
||||||
|
test("status is 200", function() {
|
||||||
|
expect(res.status).to.eql(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |-
|
||||||
|
# Dismiss All Resolved
|
||||||
|
|
||||||
|
`POST /api/conflicts/dismiss-all` — auth: jwt.
|
||||||
|
|
||||||
|
Canonical reference: docs/developer/api/conflicts/dismiss_all_resolved.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||||
|
type: text/markdown
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
info:
|
||||||
|
name: Get Conflict Details
|
||||||
|
type: http
|
||||||
|
seq: 5
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: GET
|
||||||
|
url: '{{base_url}}/api/conflicts/{{media_item_id}}'
|
||||||
|
auth: inherit
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
scripts:
|
||||||
|
- type: tests
|
||||||
|
code: |-
|
||||||
|
test("status is 200", function() {
|
||||||
|
expect(res.status).to.eql(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |-
|
||||||
|
# Get Conflict Details
|
||||||
|
|
||||||
|
`GET /api/conflicts/:id` — auth: jwt.
|
||||||
|
|
||||||
|
Canonical reference: docs/developer/api/conflicts/get_conflict.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||||
|
type: text/markdown
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
info:
|
||||||
|
name: List Conflicts
|
||||||
|
type: http
|
||||||
|
seq: 6
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: GET
|
||||||
|
url: '{{base_url}}/api/conflicts'
|
||||||
|
auth: inherit
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
scripts:
|
||||||
|
- type: tests
|
||||||
|
code: |-
|
||||||
|
test("status is 200", function() {
|
||||||
|
expect(res.status).to.eql(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |-
|
||||||
|
# List Conflicts
|
||||||
|
|
||||||
|
`GET /api/conflicts` — auth: jwt.
|
||||||
|
|
||||||
|
Canonical reference: docs/developer/api/conflicts/list_conflicts.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||||
|
type: text/markdown
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
info:
|
||||||
|
name: Resolve Conflict
|
||||||
|
type: http
|
||||||
|
seq: 7
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: POST
|
||||||
|
url: '{{base_url}}/api/conflicts/{{media_item_id}}/resolve'
|
||||||
|
auth: inherit
|
||||||
|
body:
|
||||||
|
type: json
|
||||||
|
data: |-
|
||||||
|
{
|
||||||
|
"winner": "kobo"
|
||||||
|
}
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
scripts:
|
||||||
|
- type: tests
|
||||||
|
code: |-
|
||||||
|
test("status is 200", function() {
|
||||||
|
expect(res.status).to.eql(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |-
|
||||||
|
# Resolve Conflict
|
||||||
|
|
||||||
|
`POST /api/conflicts/:id/resolve` — auth: jwt.
|
||||||
|
|
||||||
|
Canonical reference: docs/developer/api/conflicts/resolve_conflict.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||||
|
type: text/markdown
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
info:
|
|
||||||
name: Bulk Dismiss Conflicts
|
|
||||||
type: http
|
|
||||||
seq: 1
|
|
||||||
http:
|
|
||||||
method: POST
|
|
||||||
url: '{{base_url}}/api/conflicts/bulk-dismiss'
|
|
||||||
auth: inherit
|
|
||||||
body:
|
|
||||||
type: json
|
|
||||||
data: "{\n \"conflict_ids\": [\n \"{{conflictId1"
|
|
||||||
headers:
|
|
||||||
- key: Content-Type
|
|
||||||
value: application/json
|
|
||||||
- key: Authorization
|
|
||||||
value: Bearer {{authToken
|
|
||||||
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\"]"
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
info:
|
|
||||||
name: Bulk Resolve Conflicts
|
|
||||||
type: http
|
|
||||||
seq: 1
|
|
||||||
http:
|
|
||||||
method: POST
|
|
||||||
url: '{{base_url}}/api/conflicts/bulk-resolve'
|
|
||||||
auth: inherit
|
|
||||||
body:
|
|
||||||
type: json
|
|
||||||
data: "{\n \"conflict_ids\": [\n \"{{conflictId1"
|
|
||||||
headers:
|
|
||||||
- key: Content-Type
|
|
||||||
value: application/json
|
|
||||||
- key: Authorization
|
|
||||||
value: Bearer {{authToken
|
|
||||||
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\""
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
info:
|
|
||||||
name: Bulk Resolve with Highest Progress Strategy
|
|
||||||
type: http
|
|
||||||
seq: 1
|
|
||||||
http:
|
|
||||||
method: POST
|
|
||||||
url: '{{base_url}}/api/conflicts/bulk-resolve'
|
|
||||||
auth: inherit
|
|
||||||
body:
|
|
||||||
type: json
|
|
||||||
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\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."
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
info:
|
|
||||||
name: Create Collection with Dashboard
|
|
||||||
type: http
|
|
||||||
seq: 4
|
|
||||||
http:
|
|
||||||
method: POST
|
|
||||||
url: '{{base_url}}/api/collections'
|
|
||||||
auth: inherit
|
|
||||||
runtime:
|
|
||||||
scripts:
|
|
||||||
- type: tests
|
|
||||||
code: "test(\"creates collection successfully\", function() {\n expect(res.status).to.eql(201);"
|
|
||||||
|
|
||||||
docs: |-
|
|
||||||
Create a new collection with dashboard visibility enabled.
|
|
||||||
|
|
||||||
**Endpoint**: POST /api/collections
|
|
||||||
**Auth**: Required (Bearer token)
|
|
||||||
|
|
||||||
## Request Body
|
|
||||||
|
|
||||||
| Field | Type | Required | Description |
|
|
||||||
|-------|------|----------|-------------|
|
|
||||||
| name | string | Yes | Collection name |
|
|
||||||
| description | string | No | Collection description |
|
|
||||||
| color | string | No | Hex color code |
|
|
||||||
| icon | string | No | Icon emoji or name |
|
|
||||||
| show_on_dashboard | boolean | No | Show on dashboard (default: false) |
|
|
||||||
|
|
||||||
## Example Request
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"name": "My Favorites",
|
|
||||||
"description": "My favorite books",
|
|
||||||
"color": "#FF5733",
|
|
||||||
"icon": "⭐",
|
|
||||||
"show_on_dashboard": true
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
info:
|
|
||||||
name: Get Dashboard Sections
|
|
||||||
type: http
|
|
||||||
seq: 1
|
|
||||||
http:
|
|
||||||
method: GET
|
|
||||||
url: '{{base_url}}/api/dashboard/sections?library_id={{library_id}}'
|
|
||||||
auth: inherit
|
|
||||||
body:
|
|
||||||
type: none
|
|
||||||
runtime:
|
|
||||||
scripts:
|
|
||||||
- type: tests
|
|
||||||
code: "test(\"status must be 200 with auth\", function() {\n expect(res.status).to.eql(200);\n\
|
|
||||||
});\n\ntest(\"response contains sections array\", function() {\n expect(res.body.sections).to.exist;\n\
|
|
||||||
expect(res.body.sections).to.be.an('array');\n });"
|
|
||||||
|
|
||||||
docs: |-
|
|
||||||
Get all dashboard sections for a specific library.
|
|
||||||
|
|
||||||
**Endpoint**: GET /api/dashboard/sections
|
|
||||||
**Auth**: Required (Bearer token via auth: inherit)
|
|
||||||
|
|
||||||
## Query Parameters
|
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
|
||||||
|-----------|------|----------|-------------|
|
|
||||||
| library_id | string | Yes | Library UUID |
|
|
||||||
| limit | int | No | Items per section (default: 20, max: 100) |
|
|
||||||
|
|
||||||
## Response
|
|
||||||
|
|
||||||
Returns array of sections including:
|
|
||||||
- System collections (continue-reading, recently-added, recently-read, not-started)
|
|
||||||
- User collections marked with show_on_dashboard: true
|
|
||||||
|
|
||||||
## Section Structure
|
|
||||||
|
|
||||||
Each section has:
|
|
||||||
- `id`: Collection name (string)
|
|
||||||
- `is_system`: Boolean indicating if this is a system collection
|
|
||||||
- `title`: Display title
|
|
||||||
- `description`: Collection description
|
|
||||||
- `icon`: Emoji icon
|
|
||||||
- `items`: Array of BookInfo objects
|
|
||||||
- `view_all_url`: URL to view all items (system collections only)
|
|
||||||
- `priority`: Display order (lower numbers first)
|
|
||||||
|
|
||||||
## Example Response
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"sections": [
|
|
||||||
{
|
|
||||||
"id": "continue-reading",
|
|
||||||
"is_system": true,
|
|
||||||
"title": "Continue Reading",
|
|
||||||
"description": "Books you're currently reading (0 < progress < 1)",
|
|
||||||
"icon": "📖",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"media_item_id": "uuid",
|
|
||||||
"title": "Book Title",
|
|
||||||
"author": "Author Name",
|
|
||||||
"cover_image_path": "/path/to/cover.jpg"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"view_all_url": "/section/continue-reading",
|
|
||||||
"priority": 1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
info:
|
|
||||||
name: Get Dashboard Sections by Library
|
|
||||||
type: http
|
|
||||||
seq: 3
|
|
||||||
http:
|
|
||||||
method: GET
|
|
||||||
url: '{{base_url}}/api/dashboard/sections'
|
|
||||||
auth: inherit
|
|
||||||
runtime:
|
|
||||||
scripts:
|
|
||||||
- type: tests
|
|
||||||
code: "test(\"status must be 200\", function() {\n expect(res.status).to.eql(200);"
|
|
||||||
|
|
||||||
docs: |-
|
|
||||||
Get all dashboard sections for a specific library.
|
|
||||||
|
|
||||||
**Endpoint**: GET /api/dashboard/sections
|
|
||||||
**Auth**: Required (Bearer token)
|
|
||||||
|
|
||||||
## Query Parameters
|
|
||||||
|
|
||||||
| Parameter | Type | Required | Description |
|
|
||||||
|-----------|------|----------|-------------|
|
|
||||||
| library_id | string | Yes | Library UUID |
|
|
||||||
|
|
||||||
## Response
|
|
||||||
|
|
||||||
Returns array of sections including:
|
|
||||||
- Smart sections (continue-reading, in-progress, recently-added, etc.)
|
|
||||||
- User collections marked for dashboard
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
info:
|
||||||
|
name: Get Dashboard Preferences
|
||||||
|
type: http
|
||||||
|
seq: 1
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: GET
|
||||||
|
url: '{{base_url}}/api/dashboard/preferences'
|
||||||
|
auth: inherit
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
scripts:
|
||||||
|
- type: tests
|
||||||
|
code: |-
|
||||||
|
test("status is 200", function() {
|
||||||
|
expect(res.status).to.eql(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |-
|
||||||
|
# Get Dashboard Preferences
|
||||||
|
|
||||||
|
`GET /api/dashboard/preferences` — auth: jwt.
|
||||||
|
|
||||||
|
Canonical reference: docs/developer/api/dashboard/get_preferences.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||||
|
type: text/markdown
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
info:
|
||||||
|
name: Get Dashboard Sections
|
||||||
|
type: http
|
||||||
|
seq: 2
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: GET
|
||||||
|
url: '{{base_url}}/api/dashboard/sections'
|
||||||
|
auth: inherit
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
|
||||||
|
runtime:
|
||||||
|
scripts:
|
||||||
|
- type: tests
|
||||||
|
code: |-
|
||||||
|
test("status is 200", function() {
|
||||||
|
expect(res.status).to.eql(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |-
|
||||||
|
# Get Dashboard Sections
|
||||||
|
|
||||||
|
`GET /api/dashboard/sections` — auth: jwt.
|
||||||
|
|
||||||
|
Canonical reference: docs/developer/api/dashboard/get_sections.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||||
|
type: text/markdown
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
info:
|
|
||||||
name: Restore System Collection
|
|
||||||
type: http
|
|
||||||
seq: 3
|
|
||||||
http:
|
|
||||||
method: POST
|
|
||||||
url: '{{base_url}}/api/dashboard/restore-system-collection'
|
|
||||||
auth: inherit
|
|
||||||
body:
|
|
||||||
type: json
|
|
||||||
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.\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."
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
info:
|
||||||
|
name: Restore System Collection
|
||||||
|
type: http
|
||||||
|
seq: 3
|
||||||
|
|
||||||
|
http:
|
||||||
|
method: POST
|
||||||
|
url: '{{base_url}}/api/dashboard/restore-system-collection'
|
||||||
|
auth: inherit
|
||||||
|
body:
|
||||||
|
type: json
|
||||||
|
data: |-
|
||||||
|
{
|
||||||
|
"collection_name": "Continue Reading",
|
||||||
|
"reset_type": "full"
|
||||||
|
}
|
||||||
|
settings:
|
||||||
|
encodeUrl: true
|
||||||
|
timeout: 0
|
||||||
|
followRedirects: true
|
||||||
|
maxRedirects: 5
|
||||||
|
|
||||||
|
docs:
|
||||||
|
content: |-
|
||||||
|
# Restore System Collection
|
||||||
|
|
||||||
|
`POST /api/dashboard/restore-system-collection` — auth: jwt.
|
||||||
|
|
||||||
|
Canonical reference: docs/developer/api/dashboard/restore_system_collection.md (docs site → Developer → API). This request is GENERATED from that doc; edit the doc, then run the drift test with -update.
|
||||||
|
type: text/markdown
|
||||||
Loaded 100 of 749 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user