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