A social knowledge tool for researchers built on ATProto

use cardId for update url card associations

+68 -25
+33 -9
src/modules/cards/application/useCases/commands/UpdateUrlCardAssociationsUseCase.ts
··· 5 5 import { IEventPublisher } from '../../../../../shared/application/events/IEventPublisher'; 6 6 import { ICardRepository } from '../../../domain/ICardRepository'; 7 7 import { INoteCardInput } from '../../../domain/CardFactory'; 8 + import { CardId } from '../../../domain/value-objects/CardId'; 8 9 import { CollectionId } from '../../../domain/value-objects/CollectionId'; 9 10 import { CuratorId } from '../../../domain/value-objects/CuratorId'; 10 11 import { CardTypeEnum } from '../../../domain/value-objects/CardType'; ··· 15 16 import { CardLibraryService } from '../../../domain/services/CardLibraryService'; 16 17 17 18 export interface UpdateUrlCardAssociationsDTO { 18 - url: string; 19 + cardId: string; 19 20 curatorId: string; 20 21 note?: string; 21 22 addToCollections?: string[]; ··· 71 72 } 72 73 const curatorId = curatorIdResult.value; 73 74 74 - // Validate URL 75 - const urlResult = URL.create(request.url); 76 - if (urlResult.isErr()) { 75 + // Validate and create CardId 76 + const cardIdResult = CardId.createFromString(request.cardId); 77 + if (cardIdResult.isErr()) { 77 78 return err( 78 - new ValidationError(`Invalid URL: ${urlResult.error.message}`), 79 + new ValidationError( 80 + `Invalid card ID: ${cardIdResult.error.message}`, 81 + ), 79 82 ); 80 83 } 81 - const url = urlResult.value; 84 + const cardId = cardIdResult.value; 82 85 83 86 // Find the URL card - it must already exist 84 - const existingUrlCardResult = 85 - await this.cardRepository.findUsersUrlCardByUrl(url, curatorId); 87 + const existingUrlCardResult = await this.cardRepository.findById(cardId); 86 88 if (existingUrlCardResult.isErr()) { 87 89 return err( 88 90 AppError.UnexpectedError.create(existingUrlCardResult.error), ··· 98 100 ); 99 101 } 100 102 103 + // Verify it's a URL card 104 + if (!urlCard.isUrlCard) { 105 + return err( 106 + new ValidationError('Card must be a URL card to update associations.'), 107 + ); 108 + } 109 + 110 + // Verify ownership 111 + if (!urlCard.curatorId.equals(curatorId)) { 112 + return err( 113 + new ValidationError('You do not have permission to update this card.'), 114 + ); 115 + } 116 + 117 + // Get the URL from the card for note operations 118 + if (!urlCard.url) { 119 + return err( 120 + new ValidationError('URL card must have a URL property.'), 121 + ); 122 + } 123 + const url = urlCard.url; 124 + 101 125 let noteCard; 102 126 103 127 // Handle note updates/creation ··· 141 165 type: CardTypeEnum.NOTE, 142 166 text: request.note, 143 167 parentCardId: urlCard.cardId.getStringValue(), 144 - url: request.url, 168 + url: url.value, 145 169 }; 146 170 147 171 const noteCardResult = CardFactory.create({
+5 -4
src/modules/cards/infrastructure/http/controllers/UpdateUrlCardAssociationsController.ts
··· 12 12 13 13 async executeImpl(req: AuthenticatedRequest, res: Response): Promise<any> { 14 14 try { 15 - const { url, note, addToCollections, removeFromCollections } = req.body; 15 + const { cardId, note, addToCollections, removeFromCollections } = 16 + req.body; 16 17 const curatorId = req.did; 17 18 18 19 if (!curatorId) { 19 20 return this.unauthorized(res); 20 21 } 21 22 22 - if (!url) { 23 - return this.badRequest(res, 'URL is required'); 23 + if (!cardId) { 24 + return this.badRequest(res, 'Card ID is required'); 24 25 } 25 26 26 27 const result = await this.updateUrlCardAssociationsUseCase.execute({ 27 - url, 28 + cardId, 28 29 curatorId, 29 30 note, 30 31 addToCollections,
+30 -12
src/modules/cards/tests/application/UpdateUrlCardAssociationsUseCase.test.ts
··· 81 81 curatorId: curatorId.value, 82 82 }); 83 83 expect(addResult.isOk()).toBe(true); 84 + const urlCardId = addResult.unwrap().urlCardId; 84 85 85 86 // Now create a note for it 86 87 const updateRequest = { 87 - url, 88 + cardId: urlCardId, 88 89 curatorId: curatorId.value, 89 90 note: 'This is my note', 90 91 }; ··· 118 119 119 120 // Now update the note 120 121 const updateRequest = { 121 - url, 122 + cardId: addResponse.urlCardId, 122 123 curatorId: curatorId.value, 123 124 note: 'Updated note', 124 125 }; ··· 140 141 141 142 it('should fail if URL card does not exist', async () => { 142 143 const request = { 143 - url: 'https://example.com/nonexistent', 144 + cardId: 'nonexistent-card-id', 144 145 curatorId: curatorId.value, 145 146 note: 'This should fail', 146 147 }; ··· 183 184 curatorId: curatorId.value, 184 185 }); 185 186 expect(addResult.isOk()).toBe(true); 187 + const urlCardId = addResult.unwrap().urlCardId; 186 188 187 189 // Add to collections 188 190 const updateRequest = { 189 - url, 191 + cardId: urlCardId, 190 192 curatorId: curatorId.value, 191 193 addToCollections: [ 192 194 collection1.collectionId.getStringValue(), ··· 229 231 curatorId: curatorId.value, 230 232 }); 231 233 expect(addResult.isOk()).toBe(true); 234 + const urlCardId = addResult.unwrap().urlCardId; 232 235 233 236 // Remove from collection 234 237 const updateRequest = { 235 - url, 238 + cardId: urlCardId, 236 239 curatorId: curatorId.value, 237 240 removeFromCollections: [collection.collectionId.getStringValue()], 238 241 }; ··· 283 286 curatorId: curatorId.value, 284 287 }); 285 288 expect(addResult.isOk()).toBe(true); 289 + const urlCardId = addResult.unwrap().urlCardId; 286 290 287 291 // Add to collection2 and collection3, remove from collection1 288 292 const updateRequest = { 289 - url, 293 + cardId: urlCardId, 290 294 curatorId: curatorId.value, 291 295 addToCollections: [ 292 296 collection2.collectionId.getStringValue(), ··· 335 339 curatorId: curatorId.value, 336 340 }); 337 341 expect(addResult.isOk()).toBe(true); 342 + const urlCardId = addResult.unwrap().urlCardId; 338 343 339 344 // Update note and add to collection 340 345 const updateRequest = { 341 - url, 346 + cardId: urlCardId, 342 347 curatorId: curatorId.value, 343 348 note: 'My note about this article', 344 349 addToCollections: [collection.collectionId.getStringValue()], ··· 367 372 }); 368 373 369 374 describe('Validation', () => { 370 - it('should fail with invalid URL', async () => { 375 + it('should fail when card does not exist', async () => { 371 376 const request = { 372 - url: 'not-a-valid-url', 377 + cardId: 'nonexistent-card-id', 373 378 curatorId: curatorId.value, 374 379 note: 'This should fail', 375 380 }; ··· 378 383 379 384 expect(result.isErr()).toBe(true); 380 385 if (result.isErr()) { 381 - expect(result.error.message).toContain('Invalid URL'); 386 + expect(result.error.message).toContain( 387 + 'URL card not found. Please add the URL to your library first.', 388 + ); 382 389 } 383 390 }); 384 391 385 392 it('should fail with invalid curator ID', async () => { 393 + const url = 'https://example.com/article'; 394 + 395 + // Add URL to library first 396 + const addResult = await addUrlToLibraryUseCase.execute({ 397 + url, 398 + curatorId: curatorId.value, 399 + }); 400 + expect(addResult.isOk()).toBe(true); 401 + const urlCardId = addResult.unwrap().urlCardId; 402 + 386 403 const request = { 387 - url: 'https://example.com/article', 404 + cardId: urlCardId, 388 405 curatorId: 'invalid-curator-id', 389 406 note: 'This should fail', 390 407 }; ··· 406 423 curatorId: curatorId.value, 407 424 }); 408 425 expect(addResult.isOk()).toBe(true); 426 + const urlCardId = addResult.unwrap().urlCardId; 409 427 410 428 const request = { 411 - url, 429 + cardId: urlCardId, 412 430 curatorId: curatorId.value, 413 431 addToCollections: ['invalid-collection-id'], 414 432 };