This repository has no description
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}