This repository has no description
0

Configure Feed

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

refactor: Implement context-based publishing control for domain services

Co-authored-by: aider (anthropic/claude-sonnet-4-20250514) <aider@aider.chat>

+195 -78
+14 -3
src/modules/atproto/application/useCases/ProcessCardFirehoseEventUseCase.ts
··· 11 11 UrlContent, 12 12 } from '../../infrastructure/lexicon/types/network/cosmik/card'; 13 13 import { AddUrlToLibraryUseCase } from '../../../cards/application/useCases/commands/AddUrlToLibraryUseCase'; 14 - import { UpdateUrlCardAssociationsUseCase } from '../../../cards/application/useCases/commands/UpdateUrlCardAssociationsUseCase'; 14 + import { UpdateUrlCardAssociationsUseCase, OperationContext } from '../../../cards/application/useCases/commands/UpdateUrlCardAssociationsUseCase'; 15 15 import { RemoveCardFromLibraryUseCase } from '../../../cards/application/useCases/commands/RemoveCardFromLibraryUseCase'; 16 16 17 17 export interface ProcessCardFirehoseEventDTO { ··· 130 130 cardId: parentCardId.value.getStringValue(), 131 131 curatorId: curatorDid, 132 132 note: noteContent.text, 133 - // TODO: Handle publishedRecordId for note cards in UpdateUrlCardAssociationsUseCase 133 + context: OperationContext.FIREHOSE_EVENT, 134 + publishedRecordIds: { 135 + noteCard: publishedRecordId, 136 + }, 134 137 }); 135 138 136 139 if (result.isErr()) { ··· 200 203 return ok(undefined); 201 204 } 202 205 206 + const publishedRecordId = PublishedRecordId.create({ 207 + uri: request.atUri, 208 + cid: request.cid, 209 + }); 210 + 203 211 const result = await this.updateUrlCardAssociationsUseCase.execute({ 204 212 cardId: parentCardId.value.getStringValue(), 205 213 curatorId: curatorDid, 206 214 note: noteContent.text, 207 - // TODO: Handle publishedRecordId for note cards in UpdateUrlCardAssociationsUseCase 215 + context: OperationContext.FIREHOSE_EVENT, 216 + publishedRecordIds: { 217 + noteCard: publishedRecordId, 218 + }, 208 219 }); 209 220 210 221 if (result.isErr()) {
+9 -3
src/modules/atproto/application/useCases/ProcessCollectionLinkFirehoseEventUseCase.ts
··· 6 6 import { PublishedRecordId } from '../../../cards/domain/value-objects/PublishedRecordId'; 7 7 import { ATUri } from '../../domain/ATUri'; 8 8 import { Record as CollectionLinkRecord } from '../../infrastructure/lexicon/types/network/cosmik/collectionLink'; 9 - import { UpdateUrlCardAssociationsUseCase } from '../../../cards/application/useCases/commands/UpdateUrlCardAssociationsUseCase'; 9 + import { UpdateUrlCardAssociationsUseCase, OperationContext } from '../../../cards/application/useCases/commands/UpdateUrlCardAssociationsUseCase'; 10 10 11 11 export interface ProcessCollectionLinkFirehoseEventDTO { 12 12 atUri: string; ··· 84 84 cid: request.cid, 85 85 }); 86 86 87 - // TODO: Need to modify UpdateUrlCardAssociationsUseCase to accept collection link published record ID 87 + const collectionLinkMap = new Map<string, PublishedRecordId>(); 88 + collectionLinkMap.set(collectionId.value.getStringValue(), publishedRecordId); 89 + 88 90 const result = await this.updateUrlCardAssociationsUseCase.execute({ 89 91 cardId: cardId.value.getStringValue(), 90 92 curatorId: curatorDid, 91 93 addToCollections: [collectionId.value.getStringValue()], 94 + context: OperationContext.FIREHOSE_EVENT, 95 + publishedRecordIds: { 96 + collectionLinks: collectionLinkMap, 97 + }, 92 98 }); 93 99 94 100 if (result.isErr()) { ··· 133 139 cid: request.cid || 'deleted', 134 140 }); 135 141 136 - // TODO: Need to modify UpdateUrlCardAssociationsUseCase to accept collection link published record ID 137 142 const result = await this.updateUrlCardAssociationsUseCase.execute({ 138 143 cardId: linkInfoResult.value.cardId.getStringValue(), 139 144 curatorId: curatorDid, 140 145 removeFromCollections: [linkInfoResult.value.collectionId.getStringValue()], 146 + context: OperationContext.FIREHOSE_EVENT, 141 147 }); 142 148 143 149 if (result.isErr()) {
+43 -2
src/modules/cards/application/useCases/commands/UpdateUrlCardAssociationsUseCase.ts
··· 13 13 import { CardContent } from '../../../domain/value-objects/CardContent'; 14 14 import { CardFactory } from '../../../domain/CardFactory'; 15 15 import { CardLibraryService } from '../../../domain/services/CardLibraryService'; 16 + import { PublishedRecordId } from '../../../domain/value-objects/PublishedRecordId'; 16 17 import { AuthenticationError } from '../../../../../shared/core/AuthenticationError'; 17 18 19 + export enum OperationContext { 20 + USER_INITIATED = 'user_initiated', 21 + FIREHOSE_EVENT = 'firehose_event', 22 + SYSTEM_MIGRATION = 'system_migration' 23 + } 24 + 18 25 export interface UpdateUrlCardAssociationsDTO { 19 26 cardId: string; 20 27 curatorId: string; 21 28 note?: string; 22 29 addToCollections?: string[]; 23 30 removeFromCollections?: string[]; 31 + context?: OperationContext; 32 + publishedRecordIds?: { 33 + noteCard?: PublishedRecordId; 34 + collectionLinks?: Map<string, PublishedRecordId>; 35 + }; 24 36 } 25 37 26 38 export interface UpdateUrlCardAssociationsResponseDTO { ··· 151 163 return err(new ValidationError(updateContentResult.error.message)); 152 164 } 153 165 166 + // Determine service options based on context 167 + const isFirehoseEvent = request.context === OperationContext.FIREHOSE_EVENT; 168 + const noteCardOptions = isFirehoseEvent && request.publishedRecordIds?.noteCard ? { 169 + skipPublishing: true, 170 + publishedRecordId: request.publishedRecordIds.noteCard, 171 + } : undefined; 172 + 154 173 // Update note card in library (handles save and republish) 155 174 const updateNoteResult = 156 - await this.cardLibraryService.updateCardInLibrary(noteCard, curatorId); 175 + await this.cardLibraryService.updateCardInLibrary(noteCard, curatorId, noteCardOptions); 157 176 if (updateNoteResult.isErr()) { 158 177 // Propagate authentication errors 159 178 if ( ··· 201 220 ); 202 221 } 203 222 223 + // Determine service options based on context 224 + const isFirehoseEvent = request.context === OperationContext.FIREHOSE_EVENT; 225 + const noteCardOptions = isFirehoseEvent && request.publishedRecordIds?.noteCard ? { 226 + skipPublishing: true, 227 + publishedRecordId: request.publishedRecordIds.noteCard, 228 + } : undefined; 229 + 204 230 // Add note card to library using domain service 205 231 const addNoteCardToLibraryResult = 206 - await this.cardLibraryService.addCardToLibrary(noteCard, curatorId); 232 + await this.cardLibraryService.addCardToLibrary(noteCard, curatorId, noteCardOptions); 207 233 if (addNoteCardToLibraryResult.isErr()) { 208 234 // Propagate authentication errors 209 235 if ( ··· 246 272 collectionIds.push(collectionIdResult.value); 247 273 } 248 274 275 + // Determine service options based on context 276 + const isFirehoseEvent = request.context === OperationContext.FIREHOSE_EVENT; 277 + const collectionOptions = isFirehoseEvent && request.publishedRecordIds?.collectionLinks ? { 278 + skipPublishing: true, 279 + publishedRecordIds: request.publishedRecordIds.collectionLinks, 280 + } : undefined; 281 + 249 282 const addToCollectionsResult = 250 283 await this.cardCollectionService.addCardToCollections( 251 284 urlCard, 252 285 collectionIds, 253 286 curatorId, 287 + collectionOptions, 254 288 ); 255 289 if (addToCollectionsResult.isErr()) { 256 290 // Propagate authentication errors ··· 300 334 collectionIds.push(collectionIdResult.value); 301 335 } 302 336 337 + // Determine service options based on context 338 + const isFirehoseEvent = request.context === OperationContext.FIREHOSE_EVENT; 339 + const collectionOptions = isFirehoseEvent ? { 340 + skipPublishing: true, 341 + } : undefined; 342 + 303 343 const removeFromCollectionsResult = 304 344 await this.cardCollectionService.removeCardFromCollections( 305 345 urlCard, 306 346 collectionIds, 307 347 curatorId, 348 + collectionOptions, 308 349 ); 309 350 if (removeFromCollectionsResult.isErr()) { 310 351 // Propagate authentication errors
+41 -21
src/modules/cards/domain/services/CardCollectionService.ts
··· 9 9 import { DomainService } from '../../../../shared/domain/DomainService'; 10 10 import { AuthenticationError } from '../../../../shared/core/AuthenticationError'; 11 11 12 + export interface CardCollectionServiceOptions { 13 + skipPublishing?: boolean; 14 + publishedRecordIds?: Map<string, PublishedRecordId>; // collectionId -> publishedRecordId 15 + } 16 + 12 17 export class CardCollectionValidationError extends Error { 13 18 constructor(message: string) { 14 19 super(message); ··· 26 31 card: Card, 27 32 collectionId: CollectionId, 28 33 curatorId: CuratorId, 34 + options?: CardCollectionServiceOptions, 29 35 ): Promise< 30 36 Result< 31 37 Collection, ··· 61 67 ); 62 68 } 63 69 64 - // Publish the collection link 65 - const publishLinkResult = 66 - await this.collectionPublisher.publishCardAddedToCollection( 67 - card, 68 - collection, 69 - curatorId, 70 - ); 71 - if (publishLinkResult.isErr()) { 72 - // Propagate authentication errors 73 - if (publishLinkResult.error instanceof AuthenticationError) { 74 - return err(publishLinkResult.error); 70 + // Handle publishing based on options 71 + if (options?.skipPublishing && options?.publishedRecordIds) { 72 + const publishedRecordId = options.publishedRecordIds.get(collectionId.getStringValue()); 73 + if (publishedRecordId) { 74 + // Skip publishing and use provided record ID 75 + collection.markCardLinkAsPublished(card.cardId, publishedRecordId); 75 76 } 76 - return err( 77 - new CardCollectionValidationError( 78 - `Failed to publish collection link: ${publishLinkResult.error.message}`, 79 - ), 80 - ); 81 - } 77 + } else { 78 + // Publish the collection link normally 79 + const publishLinkResult = 80 + await this.collectionPublisher.publishCardAddedToCollection( 81 + card, 82 + collection, 83 + curatorId, 84 + ); 85 + if (publishLinkResult.isErr()) { 86 + // Propagate authentication errors 87 + if (publishLinkResult.error instanceof AuthenticationError) { 88 + return err(publishLinkResult.error); 89 + } 90 + return err( 91 + new CardCollectionValidationError( 92 + `Failed to publish collection link: ${publishLinkResult.error.message}`, 93 + ), 94 + ); 95 + } 82 96 83 - // Mark the card link as published in the collection 84 - collection.markCardLinkAsPublished(card.cardId, publishLinkResult.value); 97 + // Mark the card link as published in the collection 98 + collection.markCardLinkAsPublished(card.cardId, publishLinkResult.value); 99 + } 85 100 86 101 // Save the updated collection 87 102 const saveCollectionResult = ··· 100 115 card: Card, 101 116 collectionIds: CollectionId[], 102 117 curatorId: CuratorId, 118 + options?: CardCollectionServiceOptions, 103 119 ): Promise< 104 120 Result< 105 121 Collection[], ··· 115 131 card, 116 132 collectionId, 117 133 curatorId, 134 + options, 118 135 ); 119 136 if (result.isErr()) { 120 137 return err(result.error); ··· 128 145 card: Card, 129 146 collectionId: CollectionId, 130 147 curatorId: CuratorId, 148 + options?: CardCollectionServiceOptions, 131 149 ): Promise< 132 150 Result< 133 151 Collection | null, ··· 162 180 return ok(null); 163 181 } 164 182 165 - // If the card link was published, unpublish it 166 - if (cardLink.publishedRecordId) { 183 + // Handle unpublishing based on options 184 + if (!options?.skipPublishing && cardLink.publishedRecordId) { 167 185 const unpublishLinkResult = 168 186 await this.collectionPublisher.unpublishCardAddedToCollection( 169 187 cardLink.publishedRecordId, ··· 208 226 card: Card, 209 227 collectionIds: CollectionId[], 210 228 curatorId: CuratorId, 229 + options?: CardCollectionServiceOptions, 211 230 ): Promise< 212 231 Result< 213 232 Collection[], ··· 223 242 card, 224 243 collectionId, 225 244 curatorId, 245 + options, 226 246 ); 227 247 if (result.isErr()) { 228 248 return err(result.error);
+88 -49
src/modules/cards/domain/services/CardLibraryService.ts
··· 10 10 import { PublishedRecordId } from '../value-objects/PublishedRecordId'; 11 11 import { AuthenticationError } from '../../../../shared/core/AuthenticationError'; 12 12 13 + export interface CardLibraryServiceOptions { 14 + skipPublishing?: boolean; 15 + publishedRecordId?: PublishedRecordId; 16 + } 17 + 13 18 export class CardLibraryValidationError extends Error { 14 19 constructor(message: string) { 15 20 super(message); ··· 28 33 async updateCardInLibrary( 29 34 card: Card, 30 35 curatorId: CuratorId, 36 + options?: CardLibraryServiceOptions, 31 37 ): Promise< 32 38 Result< 33 39 Card, ··· 73 79 } 74 80 75 81 if (libraryInfo?.publishedRecordId) { 76 - // Card is published - republish to update it 77 - const republishResult = await this.cardPublisher.publishCardToLibrary( 78 - card, 79 - curatorId, 80 - parentCardPublishedRecordId, 81 - ); 82 - if (republishResult.isErr()) { 83 - if (republishResult.error instanceof AuthenticationError) { 84 - return err(republishResult.error); 82 + // Handle republishing based on options 83 + if (options?.skipPublishing && options?.publishedRecordId) { 84 + // Skip republishing and use provided record ID 85 + const updatePublishedResult = card.markCardInLibraryAsPublished( 86 + curatorId, 87 + options.publishedRecordId, 88 + ); 89 + if (updatePublishedResult.isErr()) { 90 + return err( 91 + new CardLibraryValidationError( 92 + `Failed to update published record: ${updatePublishedResult.error.message}`, 93 + ), 94 + ); 85 95 } 86 - return err( 87 - new CardLibraryValidationError( 88 - `Failed to republish updated card: ${republishResult.error.message}`, 89 - ), 96 + } else { 97 + // Card is published - republish to update it 98 + const republishResult = await this.cardPublisher.publishCardToLibrary( 99 + card, 100 + curatorId, 101 + parentCardPublishedRecordId, 90 102 ); 91 - } 103 + if (republishResult.isErr()) { 104 + if (republishResult.error instanceof AuthenticationError) { 105 + return err(republishResult.error); 106 + } 107 + return err( 108 + new CardLibraryValidationError( 109 + `Failed to republish updated card: ${republishResult.error.message}`, 110 + ), 111 + ); 112 + } 92 113 93 - // Update the published record ID if it changed 94 - const updatePublishedResult = card.markCardInLibraryAsPublished( 95 - curatorId, 96 - republishResult.value, 97 - ); 98 - if (updatePublishedResult.isErr()) { 99 - return err( 100 - new CardLibraryValidationError( 101 - `Failed to update published record: ${updatePublishedResult.error.message}`, 102 - ), 114 + // Update the published record ID if it changed 115 + const updatePublishedResult = card.markCardInLibraryAsPublished( 116 + curatorId, 117 + republishResult.value, 103 118 ); 119 + if (updatePublishedResult.isErr()) { 120 + return err( 121 + new CardLibraryValidationError( 122 + `Failed to update published record: ${updatePublishedResult.error.message}`, 123 + ), 124 + ); 125 + } 104 126 } 105 127 } 106 128 ··· 119 141 async addCardToLibrary( 120 142 card: Card, 121 143 curatorId: CuratorId, 144 + options?: CardLibraryServiceOptions, 122 145 ): Promise< 123 146 Result< 124 147 Card, ··· 208 231 ); 209 232 } 210 233 211 - // Publish card to library 212 - const publishResult = await this.cardPublisher.publishCardToLibrary( 213 - card, 214 - curatorId, 215 - parentCardPublishedRecordId, 216 - ); 217 - if (publishResult.isErr()) { 218 - // Propagate authentication errors 219 - if (publishResult.error instanceof AuthenticationError) { 220 - return err(publishResult.error); 234 + // Handle publishing based on options 235 + if (options?.skipPublishing && options?.publishedRecordId) { 236 + // Skip publishing and use provided record ID 237 + const markCardAsPublishedResult = card.markCardInLibraryAsPublished( 238 + curatorId, 239 + options.publishedRecordId, 240 + ); 241 + if (markCardAsPublishedResult.isErr()) { 242 + return err( 243 + new CardLibraryValidationError( 244 + `Failed to mark card as published in library: ${markCardAsPublishedResult.error.message}`, 245 + ), 246 + ); 221 247 } 222 - return err( 223 - new CardLibraryValidationError( 224 - `Failed to publish card to library: ${publishResult.error.message}`, 225 - ), 248 + } else { 249 + // Publish card to library normally 250 + const publishResult = await this.cardPublisher.publishCardToLibrary( 251 + card, 252 + curatorId, 253 + parentCardPublishedRecordId, 226 254 ); 227 - } 255 + if (publishResult.isErr()) { 256 + // Propagate authentication errors 257 + if (publishResult.error instanceof AuthenticationError) { 258 + return err(publishResult.error); 259 + } 260 + return err( 261 + new CardLibraryValidationError( 262 + `Failed to publish card to library: ${publishResult.error.message}`, 263 + ), 264 + ); 265 + } 228 266 229 - // Mark card as published in library 230 - const markCardAsPublishedResult = card.markCardInLibraryAsPublished( 231 - curatorId, 232 - publishResult.value, 233 - ); 234 - if (markCardAsPublishedResult.isErr()) { 235 - return err( 236 - new CardLibraryValidationError( 237 - `Failed to mark card as published in library: ${markCardAsPublishedResult.error.message}`, 238 - ), 267 + // Mark card as published in library 268 + const markCardAsPublishedResult = card.markCardInLibraryAsPublished( 269 + curatorId, 270 + publishResult.value, 239 271 ); 272 + if (markCardAsPublishedResult.isErr()) { 273 + return err( 274 + new CardLibraryValidationError( 275 + `Failed to mark card as published in library: ${markCardAsPublishedResult.error.message}`, 276 + ), 277 + ); 278 + } 240 279 } 241 280 242 281 // Save updated card