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 548 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 uri?: string; 84 contentData: any; 85 libraryCount: number; 86 urlLibraryCount: number; 87 urlInLibrary?: boolean; 88 createdAt: Date; 89 updatedAt: Date; 90 collections: { 91 id: string; 92 name: string; 93 authorId: string; 94 accessType: string; 95 }[]; 96 note?: { 97 id: string; 98 contentData: any; 99 }; 100} 101 102// Database representation of a card 103export interface CardDTO { 104 id: string; 105 curatorId: string; 106 type: string; 107 contentData: CardContentData; // Type-safe JSON data for the content 108 url?: string; 109 urlType?: string; 110 parentCardId?: string; 111 viaCardId?: string; 112 publishedRecordId?: { 113 uri: string; 114 cid: string; 115 }; 116 libraryCount: number; 117 libraryMemberships: Array<{ 118 userId: string; 119 addedAt: Date; 120 publishedRecordId?: { 121 uri: string; 122 cid: string; 123 }; 124 }>; 125 createdAt: Date; 126 updatedAt: Date; 127} 128 129export class CardMapper { 130 public static toDomain(dto: CardDTO): Result<Card> { 131 try { 132 const curatorIdOrError = CuratorId.create(dto.curatorId); 133 if (curatorIdOrError.isErr()) return err(curatorIdOrError.error); 134 135 const cardTypeOrError = CardType.create(dto.type as CardTypeEnum); 136 if (cardTypeOrError.isErr()) return err(cardTypeOrError.error); 137 138 // Create content based on type 139 const contentOrError = this.createCardContent( 140 dto.type as CardTypeEnum, 141 dto.contentData, 142 ); 143 if (contentOrError.isErr()) return err(contentOrError.error); 144 145 // Create optional URL 146 let url: URL | undefined; 147 if (dto.url) { 148 const urlOrError = URL.create(dto.url); 149 if (urlOrError.isErr()) return err(urlOrError.error); 150 url = urlOrError.value; 151 } 152 153 // Create optional parent card ID 154 let parentCardId: CardId | undefined; 155 if (dto.parentCardId) { 156 const parentCardIdOrError = CardId.createFromString(dto.parentCardId); 157 if (parentCardIdOrError.isErr()) return err(parentCardIdOrError.error); 158 parentCardId = parentCardIdOrError.value; 159 } 160 161 // Create optional via card ID 162 let viaCardId: CardId | undefined; 163 if (dto.viaCardId) { 164 const viaCardIdOrError = CardId.createFromString(dto.viaCardId); 165 if (viaCardIdOrError.isErr()) return err(viaCardIdOrError.error); 166 viaCardId = viaCardIdOrError.value; 167 } 168 169 // Create optional published record ID 170 let publishedRecordId: PublishedRecordId | undefined; 171 if (dto.publishedRecordId) { 172 publishedRecordId = PublishedRecordId.create({ 173 uri: dto.publishedRecordId.uri, 174 cid: dto.publishedRecordId.cid, 175 }); 176 } 177 const libraryMemberships = dto.libraryMemberships.map((membership) => { 178 const curatorIdResult = CuratorId.create(membership.userId); 179 if (curatorIdResult.isErr()) { 180 throw new Error( 181 `Invalid curator ID in library membership: ${membership.userId}`, 182 ); 183 } 184 const curatorId = curatorIdResult.value; 185 let publishedRecordId: PublishedRecordId | undefined; 186 if (membership.publishedRecordId) { 187 // We need to parse the AT URI to get the CID - this is a simplified approach 188 // In practice, you might need a more robust URI parser 189 publishedRecordId = PublishedRecordId.create({ 190 uri: membership.publishedRecordId.uri, 191 cid: membership.publishedRecordId.cid, 192 }); 193 } 194 return { 195 curatorId, 196 addedAt: membership.addedAt, 197 publishedRecordId: publishedRecordId, 198 }; 199 }); 200 201 // Create the card 202 const cardOrError = Card.create( 203 { 204 curatorId: curatorIdOrError.value, 205 type: cardTypeOrError.value, 206 content: contentOrError.value, 207 url, 208 parentCardId, 209 viaCardId, 210 publishedRecordId, 211 libraryMemberships, 212 libraryCount: dto.libraryCount, 213 createdAt: dto.createdAt, 214 updatedAt: dto.updatedAt, 215 }, 216 new UniqueEntityID(dto.id), 217 ); 218 219 if (cardOrError.isErr()) return err(cardOrError.error); 220 221 // Set timestamps manually since Card.create sets them to now 222 const card = cardOrError.value; 223 card.props.createdAt = dto.createdAt; 224 card.props.updatedAt = dto.updatedAt; 225 226 return ok(card); 227 } catch (error) { 228 return err(error as Error); 229 } 230 } 231 232 private static createCardContent( 233 type: CardTypeEnum, 234 data: CardContentData, 235 ): Result<CardContent> { 236 try { 237 switch (type) { 238 case CardTypeEnum.URL: { 239 const urlData = data as UrlContentData; 240 const urlOrError = URL.create(urlData.url); 241 if (urlOrError.isErr()) return err(urlOrError.error); 242 243 let metadata: UrlMetadata | undefined; 244 if (urlData.metadata) { 245 const metadataResult = UrlMetadata.create({ 246 url: urlData.metadata.url, 247 title: urlData.metadata.title, 248 description: urlData.metadata.description, 249 author: urlData.metadata.author, 250 publishedDate: urlData.metadata.publishedDate 251 ? new Date(urlData.metadata.publishedDate) 252 : undefined, 253 siteName: urlData.metadata.siteName, 254 imageUrl: urlData.metadata.imageUrl, 255 type: urlData.metadata.type as UrlType, 256 retrievedAt: new Date(urlData.metadata.retrievedAt), 257 }); 258 if (metadataResult.isErr()) return err(metadataResult.error); 259 metadata = metadataResult.value; 260 } 261 262 return CardContent.createUrlContent(urlOrError.value, metadata); 263 } 264 265 case CardTypeEnum.NOTE: { 266 const noteData = data as NoteContentData; 267 return CardContent.createNoteContent(noteData.text); 268 } 269 270 default: 271 return err(new Error(`Unknown card type: ${type}`)); 272 } 273 } catch (error) { 274 return err(error as Error); 275 } 276 } 277 278 public static toPersistence(card: Card): CardPersistenceData { 279 const content = card.content; 280 let contentData: CardContentData; 281 282 // Extract content data based on type 283 if (content.type === CardTypeEnum.URL) { 284 const urlContent = content.urlContent!; 285 contentData = { 286 url: urlContent.url.value, 287 metadata: urlContent.metadata 288 ? { 289 url: urlContent.metadata.url, 290 title: urlContent.metadata.title, 291 description: urlContent.metadata.description, 292 author: urlContent.metadata.author, 293 siteName: urlContent.metadata.siteName, 294 imageUrl: urlContent.metadata.imageUrl, 295 type: urlContent.metadata.type, 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 uri: raw.uri, 407 cardContent, 408 libraryCount: raw.libraryCount, 409 urlLibraryCount: raw.urlLibraryCount, 410 urlInLibrary: raw.urlInLibrary, 411 createdAt: raw.createdAt, 412 updatedAt: raw.updatedAt, 413 authorId: raw.authorId, 414 collections: raw.collections, 415 note, 416 }; 417 } 418 419 public static toCollectionCardQueryResult(raw: { 420 id: string; 421 authorId: string; 422 url: string; 423 uri?: string; 424 contentData: any; 425 libraryCount: number; 426 urlLibraryCount: number; 427 urlInLibrary?: boolean; 428 createdAt: Date; 429 updatedAt: Date; 430 note?: { 431 id: string; 432 contentData: any; 433 }; 434 }): CollectionCardQueryResultDTO { 435 // Extract URL metadata from contentData 436 const cardContent = { 437 url: raw.contentData.url, 438 title: raw.contentData?.metadata?.title, 439 description: raw.contentData?.metadata?.description, 440 author: raw.contentData?.metadata?.author, 441 publishedDate: raw.contentData?.metadata?.publishedDate 442 ? new Date(raw.contentData.metadata.publishedDate) 443 : undefined, 444 siteName: raw.contentData?.metadata?.siteName, 445 imageUrl: raw.contentData?.metadata?.imageUrl, 446 type: raw.contentData?.metadata?.type, 447 retrievedAt: raw.contentData?.metadata?.retrievedAt 448 ? new Date(raw.contentData.metadata.retrievedAt) 449 : undefined, 450 doi: raw.contentData?.metadata?.doi, 451 isbn: raw.contentData?.metadata?.isbn, 452 }; 453 454 // Extract note text from note's contentData 455 const note = raw.note 456 ? { 457 id: raw.note.id, 458 text: raw.note.contentData?.text || '', 459 } 460 : undefined; 461 462 return { 463 id: raw.id, 464 type: CardTypeEnum.URL, 465 url: raw.url, 466 uri: raw.uri, 467 cardContent, 468 libraryCount: raw.libraryCount, 469 urlLibraryCount: raw.urlLibraryCount, 470 urlInLibrary: raw.urlInLibrary, 471 createdAt: raw.createdAt, 472 updatedAt: raw.updatedAt, 473 authorId: raw.authorId, 474 note, 475 }; 476 } 477 478 public static toUrlCardViewDTO(raw: { 479 id: string; 480 type: string; 481 authorId: string; 482 url: string; 483 uri?: string; 484 contentData: UrlContentData; 485 libraryCount: number; 486 urlLibraryCount: number; 487 urlInLibrary?: boolean; 488 createdAt: Date; 489 updatedAt: Date; 490 inLibraries: { 491 userId: string; 492 }[]; 493 inCollections: { 494 id: string; 495 name: string; 496 authorId: string; 497 accessType: string; 498 }[]; 499 note?: { 500 id: string; 501 contentData: any; 502 }; 503 }): UrlCardViewDTO { 504 // Extract URL metadata from contentData 505 const cardContent = { 506 url: raw.contentData.url, 507 title: raw.contentData?.metadata?.title, 508 description: raw.contentData?.metadata?.description, 509 author: raw.contentData?.metadata?.author, 510 publishedDate: raw.contentData?.metadata?.publishedDate 511 ? new Date(raw.contentData.metadata.publishedDate) 512 : undefined, 513 siteName: raw.contentData?.metadata?.siteName, 514 imageUrl: raw.contentData?.metadata?.imageUrl, 515 type: raw.contentData?.metadata?.type, 516 retrievedAt: raw.contentData?.metadata?.retrievedAt 517 ? new Date(raw.contentData.metadata.retrievedAt) 518 : undefined, 519 doi: raw.contentData?.metadata?.doi, 520 isbn: raw.contentData?.metadata?.isbn, 521 }; 522 523 // Extract note text from note's contentData 524 const note = raw.note 525 ? { 526 id: raw.note.id, 527 text: raw.note.contentData?.text || '', 528 } 529 : undefined; 530 531 return { 532 id: raw.id, 533 type: CardTypeEnum.URL, 534 url: raw.url, 535 uri: raw.uri, 536 cardContent, 537 libraryCount: raw.libraryCount, 538 urlLibraryCount: raw.urlLibraryCount, 539 urlInLibrary: raw.urlInLibrary, 540 createdAt: raw.createdAt, 541 updatedAt: raw.updatedAt, 542 authorId: raw.authorId, 543 collections: raw.inCollections, 544 libraries: raw.inLibraries, 545 note, 546 }; 547 } 548}