This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

semble / .agent / logs / 20260204_open_collection_card_removal.md
10 kB 360 lines
1# Open Collection Card Removal Analysis 2 3**Date:** 2026-02-04 4**Issue:** Cards being removed from open collections when users remove them from their library 5 6--- 7 8## Original Issue 9 10### Observed Behavior 11 12When a user removed a card from their library, the card was also being removed from open collections, even when those collections were created and owned by other users. 13 14**Example Scenario:** 15 16- User A creates Open Collection X 17- User B creates Card Y and adds it to Collection X 18- User B removes Card Y from their library 19- **Result:** Card Y disappears from Collection X (unexpected) 20 21### Expected Behavior 22 23When a user removes a card from their library: 24 25- The card should only be removed from collections owned by the card's author 26- Cards should remain in open collections created by other users 27 28--- 29 30## Changes Made 31 32### 1. CardLibraryService.ts 33 34**File:** `src/modules/cards/domain/services/CardLibraryService.ts` 35 36**Change:** Added conditional check to only remove from collections when the user removing the card is also the card author. 37 38```typescript 39// Only remove from collections if the user removing is the card author 40// This ensures only card authors can clean up their own collections 41if (card.curatorId.equals(curatorId)) { 42 // Get all collections owned by the card's author that contain this card 43 const collectionsResult = 44 await this.collectionRepository.findByCuratorIdContainingCard( 45 card.curatorId, // Changed from curatorId to card.curatorId 46 card.cardId, 47 ); 48 49 // ... remove from collections logic 50} 51``` 52 53**Before:** `findByCuratorIdContainingCard(curatorId, card.cardId)` 54 55- Found collections owned by the person removing the card 56 57**After:** Wrapped in conditional + `findByCuratorIdContainingCard(card.curatorId, card.cardId)` 58 59- Only executes if person removing = card author 60- Finds collections owned by the card author 61 62### 2. Test Coverage Added 63 64**File:** `src/modules/cards/tests/application/RemoveCardFromLibraryUseCase.test.ts` 65 66Added three new test cases: 67 681. **"should not remove from collections when non-author removes card from library"** 69 - Alice creates Card A and Collection X 70 - Bob adds Card A to his library 71 - Bob removes Card A from his library 72 - ✅ Card A remains in Alice's Collection X 73 742. **"should not remove card from open collections owned by others when card author removes from library"** 75 - Alice creates Card A 76 - Bob creates Open Collection Y 77 - Alice adds Card A to Bob's Collection Y 78 - Alice removes Card A from her library 79 - ⚠️ Card A gets deleted (CASCADE removes from Collection Y) 80 813. **"should only remove from author-owned collections, not from open collections by others"** 82 - Alice creates Card A 83 - Alice creates Collection X, Bob creates Collection Y 84 - Alice adds Card A to both collections 85 - Bob adds Card A to his library (prevents deletion) 86 - Alice removes Card A from her library 87 - ✅ Card removed from Collection X, remains in Collection Y 88 89--- 90 91## Analysis of `findByCuratorIdContainingCard` 92 93### Method Purpose 94 95**File:** `src/modules/cards/infrastructure/repositories/DrizzleCollectionRepository.ts:298-391` 96 97```typescript 98async findByCuratorIdContainingCard( 99 authorId: CuratorId, 100 cardId: CardId, 101): Promise<Result<Collection[]>> 102``` 103 104### SQL Query Logic 105 106```typescript 107.where( 108 and( 109 eq(collections.authorId, authorIdString), // Collection author = provided ID 110 eq(collectionCards.cardId, cardIdString), // Collection contains the card 111 ), 112); 113``` 114 115Finds collections where: 116 117- **Collection author** equals the provided `authorId` parameter 118- **Collection contains** the specified `cardId` 119 120### Conclusion 121 122**Method is working correctly.** It properly filters collections by author ID and did NOT contribute to removing cards from other users' collections through the collection cleanup logic. 123 124--- 125 126## Root Cause Discovery: Card Deletion CASCADE 127 128### The Real Culprit 129 130The observed behavior of cards disappearing from other users' collections was NOT caused by the collection cleanup logic, but by **card deletion with database CASCADE**. 131 132**File:** `src/modules/cards/application/useCases/commands/RemoveCardFromLibraryUseCase.ts:112-150` 133 134```typescript 135// Handle deletion with proper ordering for URL cards 136if (updatedCard.libraryCount === 0 && updatedCard.curatorId.equals(curatorId)) { 137 // Delete the card from the database 138 const deleteResult = await this.cardRepository.delete(updatedCard.cardId); 139} 140``` 141 142### Deletion Flow 143 144When a user removes a card from their library: 145 1461. Card's `libraryCount` is decremented 1472. If `libraryCount === 0` AND user is the card author: 148 - Card is **deleted entirely** from the database 1493. Database CASCADE (ON DELETE CASCADE) automatically removes the card from ALL collections 1504. This includes collections owned by other users 151 152### Example with CASCADE 153 154**Scenario:** 155 156- User B creates Card Y (User B is card author) 157- User B adds Card Y to their library (libraryCount = 1) 158- User A creates Open Collection X 159- User B adds Card Y to Collection X 160- User B removes Card Y from library 161 162**What happens:** 163 1641. Card Y's libraryCount becomes 0 1652. User B is card author ✓ 1663. Card Y gets **DELETED** 1674. Database CASCADE removes Card Y from Collection X 1685. Card Y disappears from User A's collection 169 170### Database Schema 171 172**File:** `src/modules/cards/tests/test-utils/createTestSchema.ts` 173 174```sql 175CREATE TABLE IF NOT EXISTS collection_cards ( 176 id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), 177 collection_id UUID NOT NULL REFERENCES collections(id) ON DELETE CASCADE, 178 card_id UUID NOT NULL REFERENCES cards(id) ON DELETE CASCADE, -- CASCADE here! 179 ... 180) 181``` 182 183When a card is deleted, `ON DELETE CASCADE` automatically removes all `collection_cards` entries. 184 185--- 186 187## Current State 188 189### What Our Changes Fixed ✅ 190 1911. **Collection cleanup logic now correct:** 192 - Only card authors can trigger collection cleanup 193 - Only removes from collections owned by the card author 194 - Non-authors removing cards from their library doesn't affect any collections 195 1962. **Test coverage:** 197 - Comprehensive tests verify the new behavior 198 - Edge cases covered (non-author removal, mixed collections, etc.) 199 200### What Our Changes DID NOT Fix ⚠️ 201 202**Card deletion CASCADE still affects other users' collections:** 203 204Even with our changes, cards are still removed from other users' collections when: 205 2061. Card author removes the card from their library 2072. Card's `libraryCount` drops to 0 2083. Card gets deleted (because author is removing and libraryCount = 0) 2094. CASCADE deletion removes from ALL collections, including those owned by others 210 211**Example:** 212 213- User B creates Card Y 214- User B adds to their library (libraryCount = 1) 215- User A adds Card Y to their Open Collection X 216- User B removes Card Y from library 217- Card Y deleted (libraryCount = 0, curator = owner) 218- CASCADE removes from Collection X ❌ 219 220--- 221 222## Recommendations 223 224### Option 1: Prevent Deletion if Card Exists in ANY Collections 225 226**Change:** Don't delete cards if they exist in any collection, regardless of ownership. 227 228```typescript 229// Before deletion check: 230const collections = await this.collectionRepository.findByCardId(card.cardId); 231if (collections.length > 0) { 232 // Don't delete - card is in use in collections 233 return ok(card); 234} 235 236// Only delete if not in any collections 237if (updatedCard.libraryCount === 0 && updatedCard.curatorId.equals(curatorId)) { 238 // Safe to delete 239} 240``` 241 242**Pros:** 243 244- Cards remain accessible in collections even if removed from all libraries 245- Preserves curated collections 246 247**Cons:** 248 249- Cards may accumulate without being in any library 250- Need cleanup mechanism for orphaned cards 251 252### Option 2: Only Prevent if in Card Author's Collections 253 254**Change:** Don't delete cards if they exist in collections owned by the card author. 255 256```typescript 257const authorCollections = 258 await this.collectionRepository.findByCuratorIdContainingCard( 259 card.curatorId, 260 card.cardId, 261 ); 262 263if (authorCollections.length > 0) { 264 // Don't delete - card is in author's collections 265 return ok(card); 266} 267``` 268 269**Pros:** 270 271- Card authors maintain control over their own collections 272- Still allows deletion if only in others' collections 273 274**Cons:** 275 276- Cards can still disappear from other users' collections 277- Doesn't solve the original issue 278 279### Option 3: Soft Delete with Visibility Flag 280 281**Change:** Mark cards as "deleted" but keep them in database. 282 283```typescript 284// Instead of deleting: 285card.markAsDeleted(); 286await this.cardRepository.save(card); 287 288// Collections can still reference the card 289// UI can choose to hide/show deleted cards 290``` 291 292**Pros:** 293 294- No CASCADE issues 295- Collections maintain integrity 296- Can implement "undelete" functionality 297 298**Cons:** 299 300- More complex implementation 301- Need to handle deleted cards in queries 302 303### Option 4: Collection-Based Ownership 304 305**Change:** Cards exist as long as they're in at least one collection OR library. 306 307```typescript 308const totalReferences = 309 updatedCard.libraryCount + 310 (await this.collectionRepository.findByCardId(card.cardId)).length; 311 312if (totalReferences === 0 && updatedCard.curatorId.equals(curatorId)) { 313 // Only delete if not in ANY library or collection 314 await this.cardRepository.delete(updatedCard.cardId); 315} 316``` 317 318**Pros:** 319 320- Natural model - collections are "collections of cards" 321- Solves the CASCADE issue 322- Cards exist as long as they're referenced anywhere 323 324**Cons:** 325 326- Changes the ownership model 327- Need to handle card updates when author removes from library 328 329--- 330 331## Summary 332 333### Key Findings 334 3351.`findByCuratorIdContainingCard` is working correctly 3362. ✅ Collection cleanup logic now only targets card author's collections 3373. ⚠️ Card deletion CASCADE is the root cause of cards disappearing from other users' collections 3384. ⚠️ Our changes fixed collection cleanup but NOT CASCADE deletion 339 340### Next Decision Point 341 342**Should cards be deleted when they exist in collections owned by other users?** 343 344This is a product/design decision that affects: 345 346- Card ownership model 347- Collection integrity 348- User expectations 349- Database cleanup strategy 350 351Current implementation: Cards are deleted when `libraryCount = 0` and user is card author, regardless of collection membership. 352 353### Files Modified 354 355- `src/modules/cards/domain/services/CardLibraryService.ts` 356- `src/modules/cards/tests/application/RemoveCardFromLibraryUseCase.test.ts` 357 358### Type Check 359 360✅ All type checks pass: `npm run build:check`