docs: Add Phase 4.6 - CreateCollection manual books support
Add comprehensive documentation for Phase 4.6 which enables the CreateCollection endpoint to support manual book selection alongside auto-assign rules. This is required for the Custom Section Builder. Key additions: - Phase 4.6: Update CreateCollection Endpoint (30-45 min) - Add ManualBookIDs field to CreateCollectionRequest struct - Implement graceful handling of invalid book IDs - Add validation (max 50 book IDs) to prevent DoS - Reuse existing AddBookToCollection service method - Maintain backward compatibility (field is optional) - Updated Phase 12.5: Collections Bruno tests - create-collection-with-manual-books.bru - create-collection-too-many-books.bru (validation test) - create-collection-invalid-book-id.bru - create-collection-rules-only.bru - create-collection-unauthorized.bru - Added section 13.3: Collections API documentation - manual_book_ids field documentation - Validation limits (max 50 items) - Example combining auto-assign + manual books - Error handling explanation Design decisions: - Graceful degradation: Collection created even if some books fail - Reuse existing infrastructure: No new service methods needed - Backward compatible: Optional field doesn't break existing clients - UI constraint: 50 book limit prevents abuse while allowing flexibility
This commit is contained in:
+524
-96
@@ -1069,6 +1069,138 @@ post:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### **Phase 4.6: Update CreateCollection Endpoint** (30-45 min)
|
||||||
|
|
||||||
|
**REQUIRED for Custom Section Builder**: The `CreateCollection` endpoint must support adding manual books when creating a collection.
|
||||||
|
|
||||||
|
**Why this is needed:**
|
||||||
|
- Custom Section Builder allows users to select books manually AND use filter rules
|
||||||
|
- Both features can be combined (rules + manual selection)
|
||||||
|
- Single API call is cleaner than separate create + add operations
|
||||||
|
|
||||||
|
**File: `internal/handlers/collections.go`** (MODIFY existing)
|
||||||
|
|
||||||
|
**Step 1: Add `ManualBookIDs` field to `CreateCollectionRequest`**
|
||||||
|
|
||||||
|
After line 40, add the new field:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type CreateCollectionRequest struct {
|
||||||
|
Name string `json:"name" validate:"required"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Color string `json:"color"`
|
||||||
|
Icon string `json:"icon"`
|
||||||
|
AutoAssignRules []services.Rule `json:"auto_assign_rules"`
|
||||||
|
ViewSettings map[string]interface{} `json:"view_settings"`
|
||||||
|
ManualBookIDs []string `json:"manual_book_ids" validate:"max=50"` // NEW
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2: Update `CreateCollection` handler**
|
||||||
|
|
||||||
|
Modify the `CreateCollection` function (lines 73-112) to handle manual books:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (h *CollectionHandler) CreateCollection(c echo.Context) error {
|
||||||
|
user := c.Get("user").(database.Users)
|
||||||
|
userUUID := uuid.UUID(user.ID.Bytes)
|
||||||
|
|
||||||
|
var req CreateCollectionRequest
|
||||||
|
if err := c.Bind(&req); err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
|
||||||
|
}
|
||||||
|
if err := c.Validate(&req); err != nil {
|
||||||
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
collection, err := h.collectionService.CreateCollection(
|
||||||
|
c.Request().Context(),
|
||||||
|
userUUID,
|
||||||
|
req.Name,
|
||||||
|
req.Description,
|
||||||
|
req.Color,
|
||||||
|
req.Icon,
|
||||||
|
req.AutoAssignRules,
|
||||||
|
req.ViewSettings,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||||
|
}
|
||||||
|
|
||||||
|
// NEW: Add manual books if provided
|
||||||
|
if len(req.ManualBookIDs) > 0 {
|
||||||
|
collectionUUID := uuid.UUID(collection.ID.Bytes)
|
||||||
|
addedCount := 0
|
||||||
|
|
||||||
|
for _, bookIDStr := range req.ManualBookIDs {
|
||||||
|
bookID, err := uuid.Parse(bookIDStr)
|
||||||
|
if err != nil {
|
||||||
|
// Skip invalid book IDs, log error
|
||||||
|
c.Logger().Errorf("Invalid book ID %s: %v", bookIDStr, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
err = h.collectionService.AddBookToCollection(c.Request().Context(), collectionUUID, bookID, userUUID)
|
||||||
|
if err != nil {
|
||||||
|
// Log error but continue adding other books
|
||||||
|
c.Logger().Errorf("Failed to add book %s to collection: %v", bookIDStr, err)
|
||||||
|
} else {
|
||||||
|
addedCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Logger().Infof("Added %d/%d manual books to collection %s", addedCount, len(req.ManualBookIDs), collection.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
bookCount := int32(0)
|
||||||
|
return c.JSON(http.StatusCreated, map[string]interface{}{
|
||||||
|
"id": uuid.UUID(collection.ID.Bytes).String(),
|
||||||
|
"user_id": uuid.UUID(collection.UserID.Bytes).String(),
|
||||||
|
"name": collection.Name,
|
||||||
|
"description": textToString(collection.Description),
|
||||||
|
"color": textToString(collection.Color),
|
||||||
|
"icon": textToString(collection.Icon),
|
||||||
|
"auto_assign_rules": collection.AutoAssignRules,
|
||||||
|
"view_settings": collection.ViewSettings,
|
||||||
|
"book_count": bookCount,
|
||||||
|
"created_at": collection.CreatedAt.Time.String(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Implementation Details:**
|
||||||
|
- ✅ Reuses existing `AddBookToCollection` service method
|
||||||
|
- ✅ Validates request (max 50 book IDs)
|
||||||
|
- ✅ Returns 400 if more than 50 book IDs provided
|
||||||
|
- ✅ Gracefully handles invalid book IDs (skips them, logs error)
|
||||||
|
- ✅ Continues adding remaining books if one fails
|
||||||
|
- ✅ Backward compatible (field is optional)
|
||||||
|
- ✅ No database schema changes needed
|
||||||
|
|
||||||
|
**Validation Rule:**
|
||||||
|
```go
|
||||||
|
// Add to validator in main.go (around line 130)
|
||||||
|
v.RegisterValidation("max", func(fl validator.FieldLevel) bool {
|
||||||
|
field := fl.Field()
|
||||||
|
if field.Kind() != reflect.Slice {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return field.Len() <= 50
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**Testing:**
|
||||||
|
```bash
|
||||||
|
# Verify compilation
|
||||||
|
go build ./internal/handlers/...
|
||||||
|
|
||||||
|
# Manual test with Bruno
|
||||||
|
cd bruno/collections
|
||||||
|
bru run --env local create-collection-with-manual-books.bru
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### **Phase 5: Bruno API Tests** (1 hour)
|
### **Phase 5: Bruno API Tests** (1 hour)
|
||||||
|
|
||||||
**File: `bruno/dashboard/**`** (update existing tests)
|
**File: `bruno/dashboard/**`** (update existing tests)
|
||||||
@@ -1183,6 +1315,7 @@ type Config struct {
|
|||||||
LoginTracker *ratelimit.LoginAttemptTracker
|
LoginTracker *ratelimit.LoginAttemptTracker
|
||||||
ScannerHandler *handlers.Handler
|
ScannerHandler *handlers.Handler
|
||||||
DashboardService *services.DashboardService // NEW: For dashboard data fetching
|
DashboardService *services.DashboardService // NEW: For dashboard data fetching
|
||||||
|
DashboardHandler *handlers.DashboardHandler // NEW: For dashboard API endpoints
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -2750,111 +2883,135 @@ No additional work needed - this section references the preview endpoint added e
|
|||||||
---
|
---
|
||||||
|
|
||||||
### **Phase 10.6: Final Integration & Testing** (1 hour)
|
### **Phase 10.6: Final Integration & Testing** (1 hour)
|
||||||
user := c.Get("user").(database.Users)
|
|
||||||
userUUID := uuid.UUID(user.ID.Bytes)
|
|
||||||
|
|
||||||
var req struct {
|
**CRITICAL**: Before proceeding to Phase 11 (Unit Tests), verify all components integrate correctly.
|
||||||
LibraryID string `json:"library_id"`
|
|
||||||
Rules []Rule `json:"rules"`
|
|
||||||
ManualBookIDs []string `json:"manual_book_ids"`
|
|
||||||
Limit int `json:"limit"`
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.Bind(&req); err != nil {
|
#### Verification Checklist
|
||||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid request"})
|
|
||||||
}
|
|
||||||
|
|
||||||
libUUID, err := uuid.Parse(req.LibraryID)
|
**Build Verification:**
|
||||||
if err != nil {
|
- [ ] TypeScript modules compile: `npm run build:ts`
|
||||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid library_id"})
|
- Verify: `web/static/dashboard.js` exists
|
||||||
}
|
- Verify: `web/static/custom-section-builder.js` exists
|
||||||
|
- Check for no compilation errors
|
||||||
|
- [ ] Templates generate successfully: `templ generate --path templates`
|
||||||
|
- Verify: `templates/dashboard_templ.go` exists
|
||||||
|
- Verify: `templates/custom_section_templ.go` exists
|
||||||
|
- [ ] Go build succeeds: `go build ./cmd/server`
|
||||||
|
- Verify: No compilation errors
|
||||||
|
- Check all imports resolve correctly
|
||||||
|
|
||||||
if req.Limit <= 0 || req.Limit > 100 {
|
**Bruno API Tests:**
|
||||||
req.Limit = 20
|
- [ ] Dashboard endpoints pass: `cd bruno/dashboard && bru run --env local`
|
||||||
}
|
- get-sections-success.bru
|
||||||
|
- get-sections-missing-library-id.bru
|
||||||
|
- get-sections-unauthorized.bru
|
||||||
|
- put-preferences-success.bru
|
||||||
|
- restore-system-collection-success.bru
|
||||||
|
- restore-system-collection-invalid-name.bru
|
||||||
|
- [ ] Collections preview tests pass:
|
||||||
|
- preview-collection-success.bru
|
||||||
|
- preview-collection-manual-selection.bru
|
||||||
|
- preview-collection-combined.bru
|
||||||
|
- preview-collection-invalid-library.bru
|
||||||
|
|
||||||
// Get all library items
|
**Manual Integration Testing:**
|
||||||
allItems, err := h.db.GetLibraryItems(c.Request().Context(), pgtype.UUID{Bytes: libUUID, Valid: true})
|
- [ ] Dashboard loads successfully
|
||||||
if err != nil {
|
- Navigate to `/dashboard?library_id=<valid_uuid>`
|
||||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to load library items"})
|
- Verify 4 system collections appear
|
||||||
}
|
- Verify collections show books correctly
|
||||||
|
- [ ] Library switching works
|
||||||
|
- Select different library from dropdown
|
||||||
|
- Verify page updates without full reload
|
||||||
|
- Verify loading spinner appears/disappears
|
||||||
|
- [ ] Dashboard settings modal functions
|
||||||
|
- Open settings modal
|
||||||
|
- Toggle collection visibility
|
||||||
|
- Drag to reorder collections
|
||||||
|
- Save preferences
|
||||||
|
- Verify changes persist on page reload
|
||||||
|
- [ ] System collection restore works
|
||||||
|
- Customize a system collection (hide it)
|
||||||
|
- Click "Restore" button
|
||||||
|
- Confirm restoration
|
||||||
|
- Verify collection reappears with defaults
|
||||||
|
- [ ] Custom section builder works end-to-end
|
||||||
|
- Navigate to `/custom-section`
|
||||||
|
- Add filter rules
|
||||||
|
- Search and select books manually
|
||||||
|
- Click "Refresh Preview"
|
||||||
|
- Verify preview shows matching books
|
||||||
|
- Save custom section
|
||||||
|
- Verify section appears on dashboard
|
||||||
|
|
||||||
// Evaluate rules for each item
|
**Type Safety Verification:**
|
||||||
var matchedItems []database.MediaItems
|
- [ ] API responses match TypeScript types
|
||||||
for _, item := range allItems {
|
- Check `is_system` is boolean (not string)
|
||||||
evaluations := h.collectionService.EvaluateRules(item, req.Rules)
|
- Check `media_item_id` field exists (not `id`)
|
||||||
for _, eval := range evaluations {
|
- Verify field names match (`hidden_collections`, `collection_order`)
|
||||||
if eval.Matches {
|
- [ ] No TypeScript type errors
|
||||||
matchedItems = append(matchedItems, item)
|
- Check browser console for type errors
|
||||||
break
|
- Verify all API calls use correct field names
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add manually selected books
|
**Database Verification:**
|
||||||
for _, bookID := range req.ManualBookIDs {
|
- [ ] System collections exist
|
||||||
bookUUID, err := uuid.Parse(bookID)
|
```sql
|
||||||
if err != nil {
|
SELECT name, query_type, priority, is_system_collection
|
||||||
continue
|
FROM collections
|
||||||
}
|
WHERE user_id IS NULL;
|
||||||
|
|
||||||
for _, item := range allItems {
|
|
||||||
itemUUID, _ := uuid.FromBytes(item.ID.Bytes[0:16])
|
|
||||||
if itemUUID == bookUUID {
|
|
||||||
// Check if already in matched items
|
|
||||||
alreadyAdded := false
|
|
||||||
for _, added := range matchedItems {
|
|
||||||
addedUUID, _ := uuid.FromBytes(added.ID.Bytes[0:16])
|
|
||||||
if addedUUID == bookUUID {
|
|
||||||
alreadyAdded = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !alreadyAdded {
|
|
||||||
matchedItems = append(matchedItems, item)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Limit results
|
|
||||||
if len(matchedItems) > req.Limit {
|
|
||||||
matchedItems = matchedItems[:req.Limit]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert to BookInfo for response
|
|
||||||
var bookCards []BookInfo
|
|
||||||
for _, item := range matchedItems {
|
|
||||||
itemUUID, _ := uuid.FromBytes(item.ID.Bytes[0:16])
|
|
||||||
bookCards = append(bookCards, BookInfo{
|
|
||||||
MediaItemID: itemUUID.String(),
|
|
||||||
Title: item.Title,
|
|
||||||
Author: textToString(item.Author),
|
|
||||||
CoverImagePath: textToString(item.CoverImagePath),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.JSON(http.StatusOK, map[string]interface{}{"items": bookCards})
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
Should return 4 rows
|
||||||
Add route to `internal/router/dashboard.go`:
|
- [ ] User preferences table exists
|
||||||
|
```sql
|
||||||
```go
|
\d user_dashboard_preferences
|
||||||
// In registerDashboardRoutes function:
|
|
||||||
collections.POST("/preview", cfg.CollectionHandler.PreviewCollection)
|
|
||||||
```
|
```
|
||||||
|
Verify all columns present
|
||||||
|
|
||||||
**Key Points**:
|
**Performance Smoke Test:**
|
||||||
- ✅ 13+ filter fields provide exceeding flexibility
|
- [ ] Dashboard loads within 2 seconds
|
||||||
- ✅ Live preview without saving
|
- Test with library containing 100+ items
|
||||||
- ✅ Search + multi-select for manual book addition
|
- Verify carousel scrolling is smooth
|
||||||
- ✅ AND/OR logic support
|
- Check no memory leaks in browser console
|
||||||
- ✅ Procedural TypeScript (no OOP)
|
|
||||||
- ✅ TailwindCSS classes only
|
**Error Handling Verification:**
|
||||||
- ✅ Uses shared types (BookInfo from collections.go)
|
- [ ] Invalid library_id shows error
|
||||||
- ✅ Inline onclick handlers acceptable per PROJECT_GUIDELINES.md flexibility
|
- [ ] Unauthorized requests return 401
|
||||||
|
- [ ] Network errors show toast notifications
|
||||||
|
- [ ] Empty collections display "No items" message
|
||||||
|
|
||||||
|
#### Troubleshooting Common Issues
|
||||||
|
|
||||||
|
**Issue: Collections not appearing**
|
||||||
|
- Check `show_on_dashboard = true` in database
|
||||||
|
- Verify user hasn't hidden collection in preferences
|
||||||
|
- Check browser console for JavaScript errors
|
||||||
|
|
||||||
|
**Issue: TypeScript compilation fails**
|
||||||
|
- Verify all type definitions in `web/src/types/api.d.ts`
|
||||||
|
- Check import statements use correct paths
|
||||||
|
- Ensure no missing dependencies in `package.json`
|
||||||
|
|
||||||
|
**Issue: Templates don't generate**
|
||||||
|
- Verify template syntax is correct
|
||||||
|
- Check for unclosed tags
|
||||||
|
- Run `go install github.com/a-h/templ/cmd/templ@latest` to update templ
|
||||||
|
|
||||||
|
**Issue: Bruno tests fail**
|
||||||
|
- Verify server is running
|
||||||
|
- Check environment variables in `bruno/.env`
|
||||||
|
- Ensure test database has seed data
|
||||||
|
|
||||||
|
#### Success Criteria
|
||||||
|
|
||||||
|
Phase 10.6 is complete when:
|
||||||
|
- ✅ All builds succeed (Go, TypeScript, Templates)
|
||||||
|
- ✅ All Bruno tests pass
|
||||||
|
- ✅ Manual testing confirms features work
|
||||||
|
- ✅ No console errors in browser
|
||||||
|
- ✅ Dashboard loads within 2 seconds
|
||||||
|
- ✅ Custom section builder creates sections successfully
|
||||||
|
- ✅ System collection restore works
|
||||||
|
|
||||||
|
**IMPORTANT**: Do not proceed to Phase 11 until all verification items pass. Integration issues discovered here are easier to fix before writing comprehensive unit tests.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -3976,6 +4133,178 @@ bru run --env local
|
|||||||
- ✅ `media_item_id` field present (not `id`)
|
- ✅ `media_item_id` field present (not `id`)
|
||||||
- ✅ Error cases handled correctly
|
- ✅ Error cases handled correctly
|
||||||
|
|
||||||
|
#### 12.5 Create Collections Endpoint Tests
|
||||||
|
|
||||||
|
**IMPORTANT**: Tests for CreateCollection with manual_book_ids support
|
||||||
|
|
||||||
|
**File: `bruno/collections/create-collection-with-manual-books.bru`** (NEW)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: Create Collection with Manual Books
|
||||||
|
meta:
|
||||||
|
group: Collections API
|
||||||
|
pre_request: Login as regular user
|
||||||
|
|
||||||
|
req:
|
||||||
|
method: POST
|
||||||
|
url: {{baseUrl}}/api/collections
|
||||||
|
headers:
|
||||||
|
Authorization: Bearer {{token}}
|
||||||
|
Content-Type: application/json
|
||||||
|
body:
|
||||||
|
name: "Sci-Fi Favorites"
|
||||||
|
description: "My favorite sci-fi books"
|
||||||
|
icon: "🚀"
|
||||||
|
color: "#9333ea"
|
||||||
|
auto_assign_rules:
|
||||||
|
- id: rule1
|
||||||
|
field: genre
|
||||||
|
operator: equals
|
||||||
|
value: Sci-Fi
|
||||||
|
priority: 1
|
||||||
|
manual_book_ids:
|
||||||
|
- {{bookId1}}
|
||||||
|
- {{bookId2}}
|
||||||
|
view_settings: {}
|
||||||
|
|
||||||
|
assertions:
|
||||||
|
- status: 201
|
||||||
|
- jsonpath: "$.id"
|
||||||
|
exists: true
|
||||||
|
- jsonpath: "$.name"
|
||||||
|
equals: "Sci-Fi Favorites"
|
||||||
|
```
|
||||||
|
|
||||||
|
**File: `bruno/collections/create-collection-too-many-books.bru`** (NEW)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: Create Collection - Too Many Manual Books (Validation Test)
|
||||||
|
meta:
|
||||||
|
group: Collections API
|
||||||
|
pre_request: Login as regular user
|
||||||
|
|
||||||
|
req:
|
||||||
|
method: POST
|
||||||
|
url: {{baseUrl}}/api/collections
|
||||||
|
headers:
|
||||||
|
Authorization: Bearer {{token}}
|
||||||
|
Content-Type: application/json
|
||||||
|
body:
|
||||||
|
name: "Test Collection"
|
||||||
|
manual_book_ids:
|
||||||
|
# Generate 51 book IDs to exceed max limit
|
||||||
|
- {{bookId1}}
|
||||||
|
- {{bookId2}}
|
||||||
|
- {{bookId3}}
|
||||||
|
- {{bookId4}}
|
||||||
|
- {{bookId5}}
|
||||||
|
# ... (total of 51 IDs)
|
||||||
|
|
||||||
|
assertions:
|
||||||
|
- status: 400
|
||||||
|
- jsonpath: "$.error"
|
||||||
|
exists: true
|
||||||
|
```
|
||||||
|
|
||||||
|
**File: `bruno/collections/create-collection-invalid-book-id.bru`** (NEW)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: Create Collection - Invalid Book IDs
|
||||||
|
meta:
|
||||||
|
group: Collections API
|
||||||
|
pre_request: Login as regular user
|
||||||
|
|
||||||
|
req:
|
||||||
|
method: POST
|
||||||
|
url: {{baseUrl}}/api/collections
|
||||||
|
headers:
|
||||||
|
Authorization: Bearer {{token}}
|
||||||
|
Content-Type: application/json
|
||||||
|
body:
|
||||||
|
name: "Test Collection"
|
||||||
|
manual_book_ids:
|
||||||
|
- invalid-uuid-format
|
||||||
|
- {{bookId1}}
|
||||||
|
- another-invalid-uuid
|
||||||
|
|
||||||
|
assertions:
|
||||||
|
- status: 201
|
||||||
|
- jsonpath: "$.id"
|
||||||
|
exists: true
|
||||||
|
# Collection should be created, valid books added, invalid IDs skipped
|
||||||
|
```
|
||||||
|
|
||||||
|
**File: `bruno/collections/create-collection-rules-only.bru`** (NEW)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: Create Collection - Auto-Assign Rules Only
|
||||||
|
meta:
|
||||||
|
group: Collections API
|
||||||
|
pre_request: Login as regular user
|
||||||
|
|
||||||
|
req:
|
||||||
|
method: POST
|
||||||
|
url: {{baseUrl}}/api/collections
|
||||||
|
headers:
|
||||||
|
Authorization: Bearer {{token}}
|
||||||
|
Content-Type: application/json
|
||||||
|
body:
|
||||||
|
name: "High Rated Books"
|
||||||
|
description: "Books with rating > 4"
|
||||||
|
icon: "⭐"
|
||||||
|
color: "#FFD700"
|
||||||
|
auto_assign_rules:
|
||||||
|
- id: rule1
|
||||||
|
field: rating
|
||||||
|
operator: greater_than
|
||||||
|
value: "4"
|
||||||
|
priority: 1
|
||||||
|
# manual_book_ids not provided (optional field)
|
||||||
|
|
||||||
|
assertions:
|
||||||
|
- status: 201
|
||||||
|
- jsonpath: "$.auto_assign_rules"
|
||||||
|
exists: true
|
||||||
|
```
|
||||||
|
|
||||||
|
**Update Bruno test directory structure**:
|
||||||
|
```
|
||||||
|
bruno/
|
||||||
|
├── dashboard/
|
||||||
|
│ ├── get-sections-success.bru
|
||||||
|
│ ├── get-sections-missing-library-id.bru
|
||||||
|
│ ├── get-sections-invalid-library-id.bru
|
||||||
|
│ ├── get-sections-unauthorized.bru
|
||||||
|
│ ├── put-preferences-success.bru
|
||||||
|
│ ├── put-preferences-unauthorized.bru
|
||||||
|
│ ├── restore-system-collection-success.bru
|
||||||
|
│ ├── restore-system-collection-invalid-name.bru
|
||||||
|
│ ├── restore-system-collection-unauthorized.bru
|
||||||
|
│ ├── preview-collection-success.bru
|
||||||
|
│ ├── preview-collection-manual-selection.bru
|
||||||
|
│ ├── preview-collection-combined.bru
|
||||||
|
│ ├── preview-collection-invalid-library.bru
|
||||||
|
│ └── preview-collection-unauthorized.bru
|
||||||
|
└── collections/ # NEW DIRECTORY
|
||||||
|
├── create-collection-with-manual-books.bru
|
||||||
|
├── create-collection-too-many-books.bru
|
||||||
|
├── create-collection-invalid-book-id.bru
|
||||||
|
├── create-collection-rules-only.bru
|
||||||
|
├── create-collection-unauthorized.bru
|
||||||
|
└── get-collections.bru
|
||||||
|
```
|
||||||
|
|
||||||
|
**Run Bruno tests**:
|
||||||
|
```bash
|
||||||
|
# Test dashboard endpoints
|
||||||
|
cd bruno/dashboard
|
||||||
|
bru run --env local
|
||||||
|
|
||||||
|
# Test collections endpoints
|
||||||
|
cd bruno/collections
|
||||||
|
bru run --env local
|
||||||
|
```
|
||||||
|
|
||||||
#### 12.5 Create Collections Preview Tests
|
#### 12.5 Create Collections Preview Tests
|
||||||
|
|
||||||
**File: `bruno/dashboard/preview-collection-success.bru`**
|
**File: `bruno/dashboard/preview-collection-success.bru`**
|
||||||
@@ -4319,7 +4648,106 @@ Key features:
|
|||||||
- AND/OR logic support for combining rules
|
- AND/OR logic support for combining rules
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 13.3 User Documentation
|
#### 13.3 Collections API Documentation Update
|
||||||
|
|
||||||
|
**File: `docs/developer/api/collections/create_collection.md`** (UPDATE existing)
|
||||||
|
|
||||||
|
**Add `manual_book_ids` field to request body table:**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Request Body
|
||||||
|
|
||||||
|
| Field | Type | Required | Description |
|
||||||
|
|--------|------|-----------|-------------|
|
||||||
|
| name | string | Yes | Collection name (max 255 chars) |
|
||||||
|
| description | string | No | Collection description |
|
||||||
|
| color | string | No | Hex color code (e.g., "#FF5733") |
|
||||||
|
| icon | string | No | Emoji icon (e.g., "🚀", "📖") |
|
||||||
|
| auto_assign_rules | array | No | Array of rule objects |
|
||||||
|
| manual_book_ids | array | No | Array of book UUIDs to manually add (max 50) |
|
||||||
|
| view_settings | object | No | Per-device display preferences |
|
||||||
|
```
|
||||||
|
|
||||||
|
**Add validation section:**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Validation
|
||||||
|
|
||||||
|
- `manual_book_ids` array is limited to 50 items
|
||||||
|
- Returns `400 Bad Request` if more than 50 book IDs provided
|
||||||
|
- Invalid book UUIDs are skipped (don't prevent collection creation)
|
||||||
|
- Duplicate book IDs are automatically ignored (database constraint)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Add example with manual books:**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
### Example Request (Auto-Assign Rules + Manual Books)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Sci-Fi Favorites",
|
||||||
|
"description": "My favorite sci-fi books plus manual picks",
|
||||||
|
"icon": "🚀",
|
||||||
|
"color": "#9333ea",
|
||||||
|
"auto_assign_rules": [
|
||||||
|
{
|
||||||
|
"field": "genre",
|
||||||
|
"operator": "equals",
|
||||||
|
"value": "Sci-Fi",
|
||||||
|
"priority": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"manual_book_ids": [
|
||||||
|
"550e8400-e29b-41d4-a716-446655440000",
|
||||||
|
"550e8400-e29b-41d4-a716-446655440001"
|
||||||
|
],
|
||||||
|
"view_settings": {
|
||||||
|
"kobo": {
|
||||||
|
"view_mode": "grid"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Notes:**
|
||||||
|
- You can combine `auto_assign_rules` AND `manual_book_ids`
|
||||||
|
- Manual books are added regardless of whether they match the auto-assign rules
|
||||||
|
- Invalid book IDs are skipped with errors logged
|
||||||
|
- Maximum 50 manual books per collection (UI constraint)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Add error response example:**
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
### Error Responses
|
||||||
|
|
||||||
|
| Code | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| 400 | Invalid request (validation failed, > 50 manual books) |
|
||||||
|
| 400 | Invalid request (validation failed) |
|
||||||
|
| 401 | Authentication required |
|
||||||
|
| 500 | Internal server error |
|
||||||
|
|
||||||
|
**Example: Too Many Manual Books**
|
||||||
|
|
||||||
|
Request:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Test",
|
||||||
|
"manual_book_ids": [ ... 51 book IDs ... ]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Response (400):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": "Validation failed"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 13.4 User Documentation
|
||||||
|
|
||||||
**File: `docs/user/dashboard.md`** (UPDATE existing)
|
**File: `docs/user/dashboard.md`** (UPDATE existing)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user