This repository has no description
0

Configure Feed

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

semble / src / modules / cards / infrastructure / repositories / mappers / CardMapper.ts
16 kB 542 lines
1import { UniqueEntityID } from '../../../../../shared/domain/UniqueEntityID'; 2import { Card } from '../../../domain/Card'; 3import { CardId } from '../../../domain/value-objects/CardId'; 4import { CardType, CardTypeEnum } from '../../../domain/value-objects/CardType'; 5import { CardContent } from '../../../domain/value-objects/CardContent'; 6import { CuratorId } from '../../../domain/value-objects/CuratorId'; 7import { PublishedRecordId } from '../../../domain/value-objects/PublishedRecordId'; 8import { URL } from '../../../domain/value-objects/URL'; 9import { UrlMetadata } from '../../../domain/value-objects/UrlMetadata'; 10import { UrlType } from '../../../domain/value-objects/UrlType'; 11import { err, ok, Result } from '../../../../../shared/core/Result'; 12import { v4 as uuid } from 'uuid'; 13import { 14 UrlCardQueryResultDTO, 15 CollectionCardQueryResultDTO, 16 UrlCardViewDTO, 17} from '../../../domain/ICardQueryRepository'; 18 19// Type-safe content data interfaces 20interface UrlContentData { 21 url: string; 22 metadata?: { 23 url: string; 24 title?: string; 25 description?: string; 26 author?: string; 27 publishedDate?: string; 28 siteName?: string; 29 imageUrl?: string; 30 type?: string; 31 retrievedAt: string; 32 doi?: string; 33 isbn?: string; 34 }; 35} 36 37export interface NoteContentData { 38 text: string; 39} 40 41export type CardContentData = UrlContentData | NoteContentData; 42 43// Persistence layer types 44export interface CardPersistenceData { 45 card: { 46 id: string; 47 authorId: string; 48 type: string; 49 contentData: CardContentData; 50 url?: string; 51 urlType?: string; 52 parentCardId?: string; 53 viaCardId?: string; 54 libraryCount: number; 55 createdAt: Date; 56 updatedAt: Date; 57 }; 58 libraryMemberships: Array<{ 59 cardId: string; 60 userId: string; 61 addedAt: Date; 62 publishedRecordId?: string; 63 }>; 64 publishedRecord?: { 65 id: string; 66 uri: string; 67 cid: string; 68 recordedAt?: Date; 69 }; 70 membershipPublishedRecords?: Array<{ 71 id: string; 72 uri: string; 73 cid: string; 74 recordedAt?: Date; 75 }>; 76} 77 78// Raw data for URL card queries 79export interface RawUrlCardData { 80 id: string; 81 authorId: string; 82 url: string; 83 contentData: any; 84 libraryCount: number; 85 urlLibraryCount: number; 86 urlInLibrary?: boolean; 87 createdAt: Date; 88 updatedAt: Date; 89 collections: { 90 id: string; 91 name: string; 92 authorId: string; 93 }[]; 94 note?: { 95 id: string; 96 contentData: any; 97 }; 98} 99 100// Database representation of a card 101export interface CardDTO { 102 id: string; 103 curatorId: string; 104 type: string; 105 contentData: CardContentData; // Type-safe JSON data for the content 106 url?: string; 107 urlType?: string; 108 parentCardId?: string; 109 viaCardId?: string; 110 publishedRecordId?: { 111 uri: string; 112 cid: string; 113 }; 114 libraryCount: number; 115 libraryMemberships: Array<{ 116 userId: string; 117 addedAt: Date; 118 publishedRecordId?: { 119 uri: string; 120 cid: string; 121 }; 122 }>; 123 createdAt: Date; 124 updatedAt: Date; 125} 126 127export class CardMapper { 128 public static toDomain(dto: CardDTO): Result<Card> { 129 try { 130 const curatorIdOrError = CuratorId.create(dto.curatorId); 131 if (curatorIdOrError.isErr()) return err(curatorIdOrError.error); 132 133 const cardTypeOrError = CardType.create(dto.type as CardTypeEnum); 134 if (cardTypeOrError.isErr()) return err(cardTypeOrError.error); 135 136 // Create content based on type 137 const contentOrError = this.createCardContent( 138 dto.type as CardTypeEnum, 139 dto.contentData, 140 ); 141 if (contentOrError.isErr()) return err(contentOrError.error); 142 143 // Create optional URL 144 let url: URL | undefined; 145 if (dto.url) { 146 const urlOrError = URL.create(dto.url); 147 if (urlOrError.isErr()) return err(urlOrError.error); 148 url = urlOrError.value; 149 } 150 151 // Create optional parent card ID 152 let parentCardId: CardId | undefined; 153 if (dto.parentCardId) { 154 const parentCardIdOrError = CardId.createFromString(dto.parentCardId); 155 if (parentCardIdOrError.isErr()) return err(parentCardIdOrError.error); 156 parentCardId = parentCardIdOrError.value; 157 } 158 159 // Create optional via card ID 160 let viaCardId: CardId | undefined; 161 if (dto.viaCardId) { 162 const viaCardIdOrError = CardId.createFromString(dto.viaCardId); 163 if (viaCardIdOrError.isErr()) return err(viaCardIdOrError.error); 164 viaCardId = viaCardIdOrError.value; 165 } 166 167 // Create optional published record ID 168 let publishedRecordId: PublishedRecordId | undefined; 169 if (dto.publishedRecordId) { 170 publishedRecordId = PublishedRecordId.create({ 171 uri: dto.publishedRecordId.uri, 172 cid: dto.publishedRecordId.cid, 173 }); 174 } 175 const libraryMemberships = dto.libraryMemberships.map((membership) => { 176 const curatorIdResult = CuratorId.create(membership.userId); 177 if (curatorIdResult.isErr()) { 178 throw new Error( 179 `Invalid curator ID in library membership: ${membership.userId}`, 180 ); 181 } 182 const curatorId = curatorIdResult.value; 183 let publishedRecordId: PublishedRecordId | undefined; 184 if (membership.publishedRecordId) { 185 // We need to parse the AT URI to get the CID - this is a simplified approach 186 // In practice, you might need a more robust URI parser 187 publishedRecordId = PublishedRecordId.create({ 188 uri: membership.publishedRecordId.uri, 189 cid: membership.publishedRecordId.cid, 190 }); 191 } 192 return { 193 curatorId, 194 addedAt: membership.addedAt, 195 publishedRecordId: publishedRecordId, 196 }; 197 }); 198 199 // Create the card 200 const cardOrError = Card.create( 201 { 202 curatorId: curatorIdOrError.value, 203 type: cardTypeOrError.value, 204 content: contentOrError.value, 205 url, 206 parentCardId, 207 viaCardId, 208 publishedRecordId, 209 libraryMemberships, 210 libraryCount: dto.libraryCount, 211 createdAt: dto.createdAt, 212 updatedAt: dto.updatedAt, 213 }, 214 new UniqueEntityID(dto.id), 215 ); 216 217 if (cardOrError.isErr()) return err(cardOrError.error); 218 219 // Set timestamps manually since Card.create sets them to now 220 const card = cardOrError.value; 221 card.props.createdAt = dto.createdAt; 222 card.props.updatedAt = dto.updatedAt; 223 224 return ok(card); 225 } catch (error) { 226 return err(error as Error); 227 } 228 } 229 230 private static createCardContent( 231 type: CardTypeEnum, 232 data: CardContentData, 233 ): Result<CardContent> { 234 try { 235 switch (type) { 236 case CardTypeEnum.URL: { 237 const urlData = data as UrlContentData; 238 const urlOrError = URL.create(urlData.url); 239 if (urlOrError.isErr()) return err(urlOrError.error); 240 241 let metadata: UrlMetadata | undefined; 242 if (urlData.metadata) { 243 const metadataResult = UrlMetadata.create({ 244 url: urlData.metadata.url, 245 title: urlData.metadata.title, 246 description: urlData.metadata.description, 247 author: urlData.metadata.author, 248 publishedDate: urlData.metadata.publishedDate 249 ? new Date(urlData.metadata.publishedDate) 250 : undefined, 251 siteName: urlData.metadata.siteName, 252 imageUrl: urlData.metadata.imageUrl, 253 type: urlData.metadata.type as UrlType, 254 retrievedAt: new Date(urlData.metadata.retrievedAt), 255 }); 256 if (metadataResult.isErr()) return err(metadataResult.error); 257 metadata = metadataResult.value; 258 } 259 260 return CardContent.createUrlContent(urlOrError.value, metadata); 261 } 262 263 case CardTypeEnum.NOTE: { 264 const noteData = data as NoteContentData; 265 return CardContent.createNoteContent(noteData.text); 266 } 267 268 default: 269 return err(new Error(`Unknown card type: ${type}`)); 270 } 271 } catch (error) { 272 return err(error as Error); 273 } 274 } 275 276 public static toPersistence(card: Card): CardPersistenceData { 277 const content = card.content; 278 let contentData: CardContentData; 279 280 // Extract content data based on type 281 if (content.type === CardTypeEnum.URL) { 282 const urlContent = content.urlContent!; 283 contentData = { 284 url: urlContent.url.value, 285 metadata: urlContent.metadata 286 ? { 287 url: urlContent.metadata.url, 288 title: urlContent.metadata.title, 289 description: urlContent.metadata.description, 290 author: urlContent.metadata.author, 291 publishedDate: urlContent.metadata.publishedDate?.toISOString(), 292 siteName: urlContent.metadata.siteName, 293 imageUrl: urlContent.metadata.imageUrl, 294 type: urlContent.metadata.type, 295 retrievedAt: urlContent.metadata.retrievedAt?.toISOString(), 296 doi: urlContent.metadata.doi, 297 isbn: urlContent.metadata.isbn, 298 } 299 : undefined, 300 } as UrlContentData; 301 } else if (content.type === CardTypeEnum.NOTE) { 302 const noteContent = content.noteContent!; 303 contentData = { 304 text: noteContent.text, 305 } as NoteContentData; 306 } else { 307 throw new Error(`Unknown card type: ${content.type}`); 308 } 309 310 // Collect all published records that need to be created 311 const publishedRecord = card.publishedRecordId 312 ? { 313 id: uuid(), 314 uri: card.publishedRecordId.uri, 315 cid: card.publishedRecordId.cid, 316 } 317 : undefined; 318 319 const membershipPublishedRecords: Array<{ 320 id: string; 321 uri: string; 322 cid: string; 323 }> = []; 324 325 // Map library memberships and collect their published records 326 const libraryMemberships = card.libraryMemberships.map((membership) => { 327 let publishedRecordId: string | undefined; 328 329 if (membership.publishedRecordId) { 330 publishedRecordId = uuid(); 331 membershipPublishedRecords.push({ 332 id: publishedRecordId, 333 uri: membership.publishedRecordId.uri, 334 cid: membership.publishedRecordId.cid, 335 }); 336 } 337 338 return { 339 cardId: card.cardId.getStringValue(), 340 userId: membership.curatorId.value, 341 addedAt: membership.addedAt, 342 publishedRecordId, 343 }; 344 }); 345 346 return { 347 card: { 348 id: card.cardId.getStringValue(), 349 authorId: card.curatorId.value, 350 type: card.type.value, 351 contentData, 352 url: card.url?.value, 353 urlType: 354 content.type === CardTypeEnum.URL 355 ? content.urlContent?.metadata?.type 356 : undefined, 357 parentCardId: card.parentCardId?.getStringValue(), 358 viaCardId: card.viaCardId?.getStringValue(), 359 libraryCount: card.libraryCount, 360 createdAt: card.createdAt, 361 updatedAt: card.updatedAt, 362 }, 363 libraryMemberships, 364 publishedRecord, 365 membershipPublishedRecords: 366 membershipPublishedRecords.length > 0 367 ? membershipPublishedRecords 368 : undefined, 369 }; 370 } 371 372 public static toUrlCardQueryResult( 373 raw: RawUrlCardData, 374 ): UrlCardQueryResultDTO { 375 // Extract URL metadata from contentData 376 const cardContent = { 377 url: raw.contentData?.url, 378 title: raw.contentData?.metadata?.title, 379 description: raw.contentData?.metadata?.description, 380 author: raw.contentData?.metadata?.author, 381 publishedDate: raw.contentData?.metadata?.publishedDate 382 ? new Date(raw.contentData.metadata.publishedDate) 383 : undefined, 384 siteName: raw.contentData?.metadata?.siteName, 385 imageUrl: raw.contentData?.metadata?.imageUrl, 386 type: raw.contentData?.metadata?.type, 387 retrievedAt: raw.contentData?.metadata?.retrievedAt 388 ? new Date(raw.contentData.metadata.retrievedAt) 389 : undefined, 390 doi: raw.contentData?.metadata?.doi, 391 isbn: raw.contentData?.metadata?.isbn, 392 }; 393 394 // Extract note text from note's contentData 395 const note = raw.note 396 ? { 397 id: raw.note.id, 398 text: raw.note.contentData?.text || '', 399 } 400 : undefined; 401 402 return { 403 id: raw.id, 404 type: CardTypeEnum.URL, 405 url: raw.url, 406 cardContent, 407 libraryCount: raw.libraryCount, 408 urlLibraryCount: raw.urlLibraryCount, 409 urlInLibrary: raw.urlInLibrary, 410 createdAt: raw.createdAt, 411 updatedAt: raw.updatedAt, 412 authorId: raw.authorId, 413 collections: raw.collections, 414 note, 415 }; 416 } 417 418 public static toCollectionCardQueryResult(raw: { 419 id: string; 420 authorId: string; 421 url: string; 422 contentData: any; 423 libraryCount: number; 424 urlLibraryCount: number; 425 urlInLibrary?: boolean; 426 createdAt: Date; 427 updatedAt: Date; 428 note?: { 429 id: string; 430 contentData: any; 431 }; 432 }): CollectionCardQueryResultDTO { 433 // Extract URL metadata from contentData 434 const cardContent = { 435 url: raw.contentData.url, 436 title: raw.contentData?.metadata?.title, 437 description: raw.contentData?.metadata?.description, 438 author: raw.contentData?.metadata?.author, 439 publishedDate: raw.contentData?.metadata?.publishedDate 440 ? new Date(raw.contentData.metadata.publishedDate) 441 : undefined, 442 siteName: raw.contentData?.metadata?.siteName, 443 imageUrl: raw.contentData?.metadata?.imageUrl, 444 type: raw.contentData?.metadata?.type, 445 retrievedAt: raw.contentData?.metadata?.retrievedAt 446 ? new Date(raw.contentData.metadata.retrievedAt) 447 : undefined, 448 doi: raw.contentData?.metadata?.doi, 449 isbn: raw.contentData?.metadata?.isbn, 450 }; 451 452 // Extract note text from note's contentData 453 const note = raw.note 454 ? { 455 id: raw.note.id, 456 text: raw.note.contentData?.text || '', 457 } 458 : undefined; 459 460 return { 461 id: raw.id, 462 type: CardTypeEnum.URL, 463 url: raw.url, 464 cardContent, 465 libraryCount: raw.libraryCount, 466 urlLibraryCount: raw.urlLibraryCount, 467 urlInLibrary: raw.urlInLibrary, 468 createdAt: raw.createdAt, 469 updatedAt: raw.updatedAt, 470 authorId: raw.authorId, 471 note, 472 }; 473 } 474 475 public static toUrlCardViewDTO(raw: { 476 id: string; 477 type: string; 478 authorId: string; 479 url: string; 480 contentData: UrlContentData; 481 libraryCount: number; 482 urlLibraryCount: number; 483 urlInLibrary?: boolean; 484 createdAt: Date; 485 updatedAt: Date; 486 inLibraries: { 487 userId: string; 488 }[]; 489 inCollections: { 490 id: string; 491 name: string; 492 authorId: string; 493 }[]; 494 note?: { 495 id: string; 496 contentData: any; 497 }; 498 }): UrlCardViewDTO { 499 // Extract URL metadata from contentData 500 const cardContent = { 501 url: raw.contentData.url, 502 title: raw.contentData?.metadata?.title, 503 description: raw.contentData?.metadata?.description, 504 author: raw.contentData?.metadata?.author, 505 publishedDate: raw.contentData?.metadata?.publishedDate 506 ? new Date(raw.contentData.metadata.publishedDate) 507 : undefined, 508 siteName: raw.contentData?.metadata?.siteName, 509 imageUrl: raw.contentData?.metadata?.imageUrl, 510 type: raw.contentData?.metadata?.type, 511 retrievedAt: raw.contentData?.metadata?.retrievedAt 512 ? new Date(raw.contentData.metadata.retrievedAt) 513 : undefined, 514 doi: raw.contentData?.metadata?.doi, 515 isbn: raw.contentData?.metadata?.isbn, 516 }; 517 518 // Extract note text from note's contentData 519 const note = raw.note 520 ? { 521 id: raw.note.id, 522 text: raw.note.contentData?.text || '', 523 } 524 : undefined; 525 526 return { 527 id: raw.id, 528 type: CardTypeEnum.URL, 529 url: raw.url, 530 cardContent, 531 libraryCount: raw.libraryCount, 532 urlLibraryCount: raw.urlLibraryCount, 533 urlInLibrary: raw.urlInLibrary, 534 createdAt: raw.createdAt, 535 updatedAt: raw.updatedAt, 536 authorId: raw.authorId, 537 collections: raw.inCollections, 538 libraries: raw.inLibraries, 539 note, 540 }; 541 } 542}