Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6a73ab5aa | ||
|
|
afbf1029a9 | ||
|
|
c645b8e207 | ||
|
|
bdb9132b85 | ||
|
|
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 |
No files matched your search
@@ -86,3 +86,4 @@ session-*.md
|
||||
# Binary media fixtures: never commit (generate with ffmpeg at test time,
|
||||
# see docs/contributing/development.md → Media fixtures)
|
||||
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
|
||||
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
|
||||
|
||||
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
|
||||
# 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
|
||||
# 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.
|
||||
lint-rendering:
|
||||
@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:]]*$$'); \
|
||||
allowed=$$(grep -F "$$f:" web/src/rendering-allowlist.txt | cut -d: -f2); \
|
||||
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 759 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user