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