@@ -88,17 +88,17 @@ export class DB {
88
88
89
89
async createValidationToken (
90
90
email : string ,
91
- accessTokenId : number ,
91
+ grantId : number ,
92
92
validationToken : string ,
93
93
) {
94
94
const insertResult = await this . db
95
95
. prepare (
96
96
sql `
97
- INSERT INTO validation_tokens (email, access_token_id , token_value)
97
+ INSERT INTO validation_tokens (email, grant_id , token_value)
98
98
VALUES (?1, ?2, ?3)
99
99
` ,
100
100
)
101
- . bind ( email , accessTokenId , validationToken )
101
+ . bind ( email , grantId , validationToken )
102
102
. run ( )
103
103
104
104
if ( ! insertResult . success || ! insertResult . meta . last_row_id ) {
@@ -116,6 +116,7 @@ export class DB {
116
116
sql `
117
117
SELECT id, email, grant_id FROM validation_tokens
118
118
WHERE grant_id = ?1 AND token_value = ?2
119
+ LIMIT 1
119
120
` ,
120
121
)
121
122
. bind ( grantId , validationToken )
@@ -129,6 +130,7 @@ export class DB {
129
130
sql `
130
131
SELECT id FROM users
131
132
WHERE email = ?1
133
+ LIMIT 1
132
134
` ,
133
135
)
134
136
. bind ( validationResult . email )
@@ -142,12 +144,12 @@ export class DB {
142
144
userId = createdUser . id
143
145
}
144
146
145
- // set access token to user id
147
+ // set grant to user id
146
148
const claimGrantResult = await this . db
147
149
. prepare (
148
150
sql `
149
151
UPDATE grants
150
- SET user_id = ?1, updated_at = CURRENT_TIMESTAMP
152
+ SET user_id = ?1, updated_at = CURRENT_TIMESTAMP
151
153
WHERE id = ?2
152
154
` ,
153
155
)
@@ -264,27 +266,85 @@ export class DB {
264
266
}
265
267
266
268
async getEntry ( userId : number , id : number ) {
267
- const result = await this . db
269
+ const entryResult = await this . db
268
270
. prepare ( sql `SELECT * FROM entries WHERE id = ?1 AND user_id = ?2` )
269
271
. bind ( id , userId )
270
272
. first ( )
271
273
272
- if ( ! result ) return null
274
+ if ( ! entryResult ) return null
273
275
274
- return entrySchema . parse ( snakeToCamel ( result ) )
275
- }
276
+ const entry = entrySchema . parse ( snakeToCamel ( entryResult ) )
276
277
277
- async listEntries ( userId : number ) {
278
- const results = await this . db
278
+ // Get tags for this entry
279
+ const tagsResult = await this . db
279
280
. prepare (
280
- sql `SELECT * FROM entries WHERE user_id = ?1 ORDER BY created_at DESC` ,
281
+ sql `
282
+ SELECT t.id, t.name
283
+ FROM tags t
284
+ JOIN entry_tags et ON et.tag_id = t.id
285
+ WHERE et.entry_id = ?1
286
+ ORDER BY t.name
287
+ ` ,
281
288
)
282
- . bind ( userId )
289
+ . bind ( id )
290
+ . all ( )
291
+
292
+ const tags = z
293
+ . array (
294
+ z . object ( {
295
+ id : z . number ( ) ,
296
+ name : z . string ( ) ,
297
+ } ) ,
298
+ )
299
+ . parse ( tagsResult . results . map ( ( result ) => snakeToCamel ( result ) ) )
300
+
301
+ return {
302
+ ...entry ,
303
+ tags,
304
+ }
305
+ }
306
+
307
+ async listEntries ( userId : number , tagIds ?: number [ ] ) {
308
+ const queryParts = [
309
+ sql `SELECT DISTINCT e.*, COUNT(et.id) as tag_count` ,
310
+ sql `FROM entries e` ,
311
+ sql `LEFT JOIN entry_tags et ON e.id = et.entry_id` ,
312
+ sql `WHERE e.user_id = ?1` ,
313
+ ]
314
+ const params : number [ ] = [ userId ]
315
+
316
+ if ( tagIds && tagIds . length > 0 ) {
317
+ queryParts . push ( sql `AND EXISTS (
318
+ SELECT 1 FROM entry_tags et2
319
+ WHERE et2.entry_id = e.id
320
+ AND et2.tag_id IN (${ tagIds . map ( ( _ , i ) => `?${ i + 2 } ` ) . join ( ',' ) } )
321
+ )` )
322
+ params . push ( ...tagIds )
323
+ }
324
+
325
+ queryParts . push ( sql `GROUP BY e.id` , sql `ORDER BY e.created_at DESC` )
326
+
327
+ const query = queryParts . join ( ' ' )
328
+ const results = await this . db
329
+ . prepare ( query )
330
+ . bind ( ...params )
283
331
. all ( )
284
332
285
333
return z
286
- . array ( entrySchema )
287
- . parse ( results . results . map ( ( result ) => snakeToCamel ( result ) ) )
334
+ . array (
335
+ z . object ( {
336
+ id : z . number ( ) ,
337
+ title : z . string ( ) ,
338
+ tagCount : z . number ( ) ,
339
+ } ) ,
340
+ )
341
+ . parse (
342
+ results . results . map ( ( result ) => ( {
343
+ id : result . id ,
344
+ title : result . title ,
345
+ tagCount : result . tag_count ,
346
+ } ) ) ,
347
+ )
288
348
}
289
349
290
350
async updateEntry (
@@ -297,8 +357,9 @@ export class DB {
297
357
throw new Error ( `Entry with ID ${ id } not found` )
298
358
}
299
359
360
+ // Only include fields that are explicitly provided (even if null) but not undefined
300
361
const updates = Object . entries ( entry )
301
- . filter ( ( [ key , value ] ) => value !== undefined && key !== 'userId' )
362
+ . filter ( ( [ key , value ] ) => key !== 'userId' && value !== undefined )
302
363
. map (
303
364
( [ key ] , index ) =>
304
365
`${ key === 'isPrivate' ? 'is_private' : key === 'isFavorite' ? 'is_favorite' : key } = ?${ index + 3 } ` ,
@@ -319,7 +380,7 @@ export class DB {
319
380
id ,
320
381
userId ,
321
382
...Object . entries ( entry )
322
- . filter ( ( [ key , value ] ) => value !== undefined && key !== 'userId' )
383
+ . filter ( ( [ key , value ] ) => key !== 'userId' && value !== undefined )
323
384
. map ( ( [ , value ] ) => value ) ,
324
385
]
325
386
@@ -343,13 +404,6 @@ export class DB {
343
404
throw new Error ( `Entry with ID ${ id } not found` )
344
405
}
345
406
346
- // First delete all entry tags
347
- await this . db
348
- . prepare ( sql `DELETE FROM entry_tags WHERE entry_id = ?1` )
349
- . bind ( id )
350
- . run ( )
351
-
352
- // Then delete the entry
353
407
const deleteResult = await this . db
354
408
. prepare ( sql `DELETE FROM entries WHERE id = ?1 AND user_id = ?2` )
355
409
. bind ( id , userId )
@@ -400,12 +454,24 @@ export class DB {
400
454
401
455
async listTags ( userId : number ) {
402
456
const results = await this . db
403
- . prepare ( sql `SELECT * FROM tags WHERE user_id = ?1 ORDER BY name` )
457
+ . prepare (
458
+ sql `
459
+ SELECT id, name
460
+ FROM tags
461
+ WHERE user_id = ?1
462
+ ORDER BY name
463
+ ` ,
464
+ )
404
465
. bind ( userId )
405
466
. all ( )
406
467
407
468
return z
408
- . array ( tagSchema )
469
+ . array (
470
+ z . object ( {
471
+ id : z . number ( ) ,
472
+ name : z . string ( ) ,
473
+ } ) ,
474
+ )
409
475
. parse ( results . results . map ( ( result ) => snakeToCamel ( result ) ) )
410
476
}
411
477
@@ -429,13 +495,14 @@ export class DB {
429
495
}
430
496
431
497
const ps = this . db . prepare ( sql `
432
- UPDATE tags
433
- SET ${ updates } , updated_at = CURRENT_TIMESTAMP
498
+ UPDATE tags
499
+ SET ${ updates } , updated_at = CURRENT_TIMESTAMP
434
500
WHERE id = ?1 AND user_id = ?2
435
501
` )
436
502
437
503
const updateValues = [
438
504
id ,
505
+ userId ,
439
506
...Object . entries ( tag )
440
507
. filter ( ( [ , value ] ) => value !== undefined )
441
508
. map ( ( [ , value ] ) => value ) ,
@@ -461,13 +528,6 @@ export class DB {
461
528
throw new Error ( `Tag with ID ${ id } not found` )
462
529
}
463
530
464
- // First delete all entry tags
465
- await this . db
466
- . prepare ( sql `DELETE FROM entry_tags WHERE tag_id = ?1 AND user_id = ?2` )
467
- . bind ( id , userId )
468
- . run ( )
469
-
470
- // Then delete the tag
471
531
const deleteResult = await this . db
472
532
. prepare ( sql `DELETE FROM tags WHERE id = ?1 AND user_id = ?2` )
473
533
. bind ( id , userId )
0 commit comments