src
modules
cards
application
infrastructure
tests
shared
···
49
49
// Validate URL
50
50
const urlResult = URL.create(query.url);
51
51
if (urlResult.isErr()) {
52
52
-
return err(new ValidationError(`Invalid URL: ${urlResult.error.message}`));
52
52
+
return err(
53
53
+
new ValidationError(`Invalid URL: ${urlResult.error.message}`),
54
54
+
);
53
55
}
54
56
55
57
// Set defaults
···
11
11
async executeImpl(req: Request, res: Response): Promise<any> {
12
12
try {
13
13
const { url } = req.query;
14
14
-
const page = req.query.page ? parseInt(req.query.page as string) : undefined;
15
15
-
const limit = req.query.limit ? parseInt(req.query.limit as string) : undefined;
14
14
+
const page = req.query.page
15
15
+
? parseInt(req.query.page as string)
16
16
+
: undefined;
17
17
+
const limit = req.query.limit
18
18
+
? parseInt(req.query.limit as string)
19
19
+
: undefined;
16
20
const sortBy = req.query.sortBy as CardSortField;
17
21
const sortOrder = req.query.sortOrder as SortOrder;
18
22
···
275
275
})
276
276
.from(libraryMemberships)
277
277
.innerJoin(cards, eq(libraryMemberships.cardId, cards.id))
278
278
-
.where(
279
279
-
and(
280
280
-
eq(cards.url, url),
281
281
-
eq(cards.type, CardTypeEnum.URL),
282
282
-
),
283
283
-
)
278
278
+
.where(and(eq(cards.url, url), eq(cards.type, CardTypeEnum.URL)))
284
279
.limit(limit)
285
280
.offset(offset);
286
281
···
291
286
.select({ count: count() })
292
287
.from(libraryMemberships)
293
288
.innerJoin(cards, eq(libraryMemberships.cardId, cards.id))
294
294
-
.where(
295
295
-
and(
296
296
-
eq(cards.url, url),
297
297
-
eq(cards.type, CardTypeEnum.URL),
298
298
-
),
299
299
-
);
289
289
+
.where(and(eq(cards.url, url), eq(cards.type, CardTypeEnum.URL)));
300
290
301
291
const totalCount = totalCountResult[0]?.count || 0;
302
292
const hasMore = offset + librariesResult.length < totalCount;
···
37
37
38
38
// Performance indexes
39
39
// Optimizes sorting cards by type and update time in query results
40
40
-
typeUpdatedAtIdx: index('idx_cards_type_updated_at')
41
41
-
.on(table.type, table.updatedAt.desc()),
40
40
+
typeUpdatedAtIdx: index('idx_cards_type_updated_at').on(
41
41
+
table.type,
42
42
+
table.updatedAt.desc(),
43
43
+
),
42
44
// Index for getLibrariesForUrl - fast URL+type lookups
43
43
-
urlTypeIdx: index('idx_cards_url_type')
44
44
-
.on(table.url, table.type),
45
45
+
urlTypeIdx: index('idx_cards_url_type').on(table.url, table.type),
45
46
// Partial index for finding NOTE cards by parent - only indexes NOTE type cards
46
47
parentTypeIdx: index('idx_cards_parent_type')
47
48
.on(table.parentCardId, table.type)
···
74
74
),
75
75
// Performance indexes
76
76
// Index for getCardsInCollection - sorted by add time
77
77
-
collectionAddedIdx: index('idx_collection_cards_collection_added')
78
78
-
.on(table.collectionId, table.addedAt.desc()),
77
77
+
collectionAddedIdx: index('idx_collection_cards_collection_added').on(
78
78
+
table.collectionId,
79
79
+
table.addedAt.desc(),
80
80
+
),
79
81
// Index for finding collections containing a card
80
80
-
cardCollectionIdx: index('idx_collection_cards_card_collection')
81
81
-
.on(table.cardId),
82
82
+
cardCollectionIdx: index('idx_collection_cards_card_collection').on(
83
83
+
table.cardId,
84
84
+
),
82
85
};
83
86
},
84
87
);
···
28
28
cardUsersIdx: index('idx_card_users').on(table.cardId),
29
29
// Performance indexes
30
30
// Index for getUrlCardsOfUser
31
31
-
userTypeCoveringIdx: index('idx_library_memberships_user_type_covering')
32
32
-
.on(table.userId, table.addedAt.desc()),
31
31
+
userTypeCoveringIdx: index(
32
32
+
'idx_library_memberships_user_type_covering',
33
33
+
).on(table.userId, table.addedAt.desc()),
33
34
};
34
35
},
35
36
);
···
100
100
ON library_memberships(user_id, added_at DESC)
101
101
INCLUDE (card_id)
102
102
`);
103
103
-
103
103
+
104
104
// Optimizes sorting cards by type and update time in query results
105
105
await db.execute(sql`
106
106
CREATE INDEX IF NOT EXISTS idx_cards_type_updated_at
107
107
ON cards(type, updated_at DESC)
108
108
`);
109
109
-
109
109
+
110
110
// Covering index for getLibrariesForUrl - fast URL+type lookups with card ID included
111
111
await db.execute(sql`
112
112
CREATE INDEX IF NOT EXISTS idx_cards_url_type
113
113
ON cards(url, type) INCLUDE (id)
114
114
`);
115
115
-
115
115
+
116
116
// Covering index for getCardsInCollection - sorted by add time with cardId included
117
117
await db.execute(sql`
118
118
CREATE INDEX IF NOT EXISTS idx_collection_cards_collection_added
119
119
ON collection_cards(collection_id, added_at DESC)
120
120
INCLUDE (card_id)
121
121
`);
122
122
-
122
122
+
123
123
// Partial index for finding NOTE cards by parent - only indexes NOTE type cards
124
124
await db.execute(sql`
125
125
CREATE INDEX IF NOT EXISTS idx_cards_parent_type
126
126
ON cards(parent_card_id, type) WHERE type = 'NOTE'
127
127
`);
128
128
-
128
128
+
129
129
// Covering index for finding collections containing a card - avoids table lookups
130
130
await db.execute(sql`
131
131
CREATE INDEX IF NOT EXISTS idx_collection_cards_card_collection
···
224
224
"name": "cards_parent_card_id_cards_id_fk",
225
225
"tableFrom": "cards",
226
226
"tableTo": "cards",
227
227
-
"columnsFrom": [
228
228
-
"parent_card_id"
229
229
-
],
230
230
-
"columnsTo": [
231
231
-
"id"
232
232
-
],
227
227
+
"columnsFrom": ["parent_card_id"],
228
228
+
"columnsTo": ["id"],
233
229
"onDelete": "no action",
234
230
"onUpdate": "no action"
235
231
},
···
237
233
"name": "cards_published_record_id_published_records_id_fk",
238
234
"tableFrom": "cards",
239
235
"tableTo": "published_records",
240
240
-
"columnsFrom": [
241
241
-
"published_record_id"
242
242
-
],
243
243
-
"columnsTo": [
244
244
-
"id"
245
245
-
],
236
236
+
"columnsFrom": ["published_record_id"],
237
237
+
"columnsTo": ["id"],
246
238
"onDelete": "no action",
247
239
"onUpdate": "no action"
248
240
}
···
368
360
"name": "collection_cards_collection_id_collections_id_fk",
369
361
"tableFrom": "collection_cards",
370
362
"tableTo": "collections",
371
371
-
"columnsFrom": [
372
372
-
"collection_id"
373
373
-
],
374
374
-
"columnsTo": [
375
375
-
"id"
376
376
-
],
363
363
+
"columnsFrom": ["collection_id"],
364
364
+
"columnsTo": ["id"],
377
365
"onDelete": "cascade",
378
366
"onUpdate": "no action"
379
367
},
···
381
369
"name": "collection_cards_card_id_cards_id_fk",
382
370
"tableFrom": "collection_cards",
383
371
"tableTo": "cards",
384
384
-
"columnsFrom": [
385
385
-
"card_id"
386
386
-
],
387
387
-
"columnsTo": [
388
388
-
"id"
389
389
-
],
372
372
+
"columnsFrom": ["card_id"],
373
373
+
"columnsTo": ["id"],
390
374
"onDelete": "cascade",
391
375
"onUpdate": "no action"
392
376
},
···
394
378
"name": "collection_cards_published_record_id_published_records_id_fk",
395
379
"tableFrom": "collection_cards",
396
380
"tableTo": "published_records",
397
397
-
"columnsFrom": [
398
398
-
"published_record_id"
399
399
-
],
400
400
-
"columnsTo": [
401
401
-
"id"
402
402
-
],
381
381
+
"columnsFrom": ["published_record_id"],
382
382
+
"columnsTo": ["id"],
403
383
"onDelete": "no action",
404
384
"onUpdate": "no action"
405
385
}
···
439
419
"name": "collection_collaborators_collection_id_collections_id_fk",
440
420
"tableFrom": "collection_collaborators",
441
421
"tableTo": "collections",
442
442
-
"columnsFrom": [
443
443
-
"collection_id"
444
444
-
],
445
445
-
"columnsTo": [
446
446
-
"id"
447
447
-
],
422
422
+
"columnsFrom": ["collection_id"],
423
423
+
"columnsTo": ["id"],
448
424
"onDelete": "cascade",
449
425
"onUpdate": "no action"
450
426
}
···
560
536
"name": "collections_published_record_id_published_records_id_fk",
561
537
"tableFrom": "collections",
562
538
"tableTo": "published_records",
563
563
-
"columnsFrom": [
564
564
-
"published_record_id"
565
565
-
],
566
566
-
"columnsTo": [
567
567
-
"id"
568
568
-
],
539
539
+
"columnsFrom": ["published_record_id"],
540
540
+
"columnsTo": ["id"],
569
541
"onDelete": "no action",
570
542
"onUpdate": "no action"
571
543
}
···
664
636
"name": "library_memberships_card_id_cards_id_fk",
665
637
"tableFrom": "library_memberships",
666
638
"tableTo": "cards",
667
667
-
"columnsFrom": [
668
668
-
"card_id"
669
669
-
],
670
670
-
"columnsTo": [
671
671
-
"id"
672
672
-
],
639
639
+
"columnsFrom": ["card_id"],
640
640
+
"columnsTo": ["id"],
673
641
"onDelete": "cascade",
674
642
"onUpdate": "no action"
675
643
},
···
677
645
"name": "library_memberships_published_record_id_published_records_id_fk",
678
646
"tableFrom": "library_memberships",
679
647
"tableTo": "published_records",
680
680
-
"columnsFrom": [
681
681
-
"published_record_id"
682
682
-
],
683
683
-
"columnsTo": [
684
684
-
"id"
685
685
-
],
648
648
+
"columnsFrom": ["published_record_id"],
649
649
+
"columnsTo": ["id"],
686
650
"onDelete": "no action",
687
651
"onUpdate": "no action"
688
652
}
···
690
654
"compositePrimaryKeys": {
691
655
"library_memberships_card_id_user_id_pk": {
692
656
"name": "library_memberships_card_id_user_id_pk",
693
693
-
"columns": [
694
694
-
"card_id",
695
695
-
"user_id"
696
696
-
]
657
657
+
"columns": ["card_id", "user_id"]
697
658
}
698
659
},
699
660
"uniqueConstraints": {},
···
925
886
"name": "auth_refresh_tokens_user_did_users_id_fk",
926
887
"tableFrom": "auth_refresh_tokens",
927
888
"tableTo": "users",
928
928
-
"columnsFrom": [
929
929
-
"user_did"
930
930
-
],
931
931
-
"columnsTo": [
932
932
-
"id"
933
933
-
],
889
889
+
"columnsFrom": ["user_did"],
890
890
+
"columnsTo": ["id"],
934
891
"onDelete": "no action",
935
892
"onUpdate": "no action"
936
893
}
···
990
947
"schemas": {},
991
948
"tables": {}
992
949
}
993
993
-
}
950
950
+
}