Skip to content

Commit 0a1410c

Browse files
committed
fix things
1 parent 1256deb commit 0a1410c

File tree

4 files changed

+188
-83
lines changed

4 files changed

+188
-83
lines changed

exercises/99.final/01.solution/src/db/index.ts

+95-35
Original file line numberDiff line numberDiff line change
@@ -88,17 +88,17 @@ export class DB {
8888

8989
async createValidationToken(
9090
email: string,
91-
accessTokenId: number,
91+
grantId: number,
9292
validationToken: string,
9393
) {
9494
const insertResult = await this.db
9595
.prepare(
9696
sql`
97-
INSERT INTO validation_tokens (email, access_token_id, token_value)
97+
INSERT INTO validation_tokens (email, grant_id, token_value)
9898
VALUES (?1, ?2, ?3)
9999
`,
100100
)
101-
.bind(email, accessTokenId, validationToken)
101+
.bind(email, grantId, validationToken)
102102
.run()
103103

104104
if (!insertResult.success || !insertResult.meta.last_row_id) {
@@ -116,6 +116,7 @@ export class DB {
116116
sql`
117117
SELECT id, email, grant_id FROM validation_tokens
118118
WHERE grant_id = ?1 AND token_value = ?2
119+
LIMIT 1
119120
`,
120121
)
121122
.bind(grantId, validationToken)
@@ -129,6 +130,7 @@ export class DB {
129130
sql`
130131
SELECT id FROM users
131132
WHERE email = ?1
133+
LIMIT 1
132134
`,
133135
)
134136
.bind(validationResult.email)
@@ -142,12 +144,12 @@ export class DB {
142144
userId = createdUser.id
143145
}
144146

145-
// set access token to user id
147+
// set grant to user id
146148
const claimGrantResult = await this.db
147149
.prepare(
148150
sql`
149151
UPDATE grants
150-
SET user_id = ?1, updated_at = CURRENT_TIMESTAMP
152+
SET user_id = ?1, updated_at = CURRENT_TIMESTAMP
151153
WHERE id = ?2
152154
`,
153155
)
@@ -264,27 +266,85 @@ export class DB {
264266
}
265267

266268
async getEntry(userId: number, id: number) {
267-
const result = await this.db
269+
const entryResult = await this.db
268270
.prepare(sql`SELECT * FROM entries WHERE id = ?1 AND user_id = ?2`)
269271
.bind(id, userId)
270272
.first()
271273

272-
if (!result) return null
274+
if (!entryResult) return null
273275

274-
return entrySchema.parse(snakeToCamel(result))
275-
}
276+
const entry = entrySchema.parse(snakeToCamel(entryResult))
276277

277-
async listEntries(userId: number) {
278-
const results = await this.db
278+
// Get tags for this entry
279+
const tagsResult = await this.db
279280
.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+
`,
281288
)
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)
283331
.all()
284332

285333
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+
)
288348
}
289349

290350
async updateEntry(
@@ -297,8 +357,9 @@ export class DB {
297357
throw new Error(`Entry with ID ${id} not found`)
298358
}
299359

360+
// Only include fields that are explicitly provided (even if null) but not undefined
300361
const updates = Object.entries(entry)
301-
.filter(([key, value]) => value !== undefined && key !== 'userId')
362+
.filter(([key, value]) => key !== 'userId' && value !== undefined)
302363
.map(
303364
([key], index) =>
304365
`${key === 'isPrivate' ? 'is_private' : key === 'isFavorite' ? 'is_favorite' : key} = ?${index + 3}`,
@@ -319,7 +380,7 @@ export class DB {
319380
id,
320381
userId,
321382
...Object.entries(entry)
322-
.filter(([key, value]) => value !== undefined && key !== 'userId')
383+
.filter(([key, value]) => key !== 'userId' && value !== undefined)
323384
.map(([, value]) => value),
324385
]
325386

@@ -343,13 +404,6 @@ export class DB {
343404
throw new Error(`Entry with ID ${id} not found`)
344405
}
345406

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
353407
const deleteResult = await this.db
354408
.prepare(sql`DELETE FROM entries WHERE id = ?1 AND user_id = ?2`)
355409
.bind(id, userId)
@@ -400,12 +454,24 @@ export class DB {
400454

401455
async listTags(userId: number) {
402456
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+
)
404465
.bind(userId)
405466
.all()
406467

407468
return z
408-
.array(tagSchema)
469+
.array(
470+
z.object({
471+
id: z.number(),
472+
name: z.string(),
473+
}),
474+
)
409475
.parse(results.results.map((result) => snakeToCamel(result)))
410476
}
411477

@@ -429,13 +495,14 @@ export class DB {
429495
}
430496

431497
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
434500
WHERE id = ?1 AND user_id = ?2
435501
`)
436502

437503
const updateValues = [
438504
id,
505+
userId,
439506
...Object.entries(tag)
440507
.filter(([, value]) => value !== undefined)
441508
.map(([, value]) => value),
@@ -461,13 +528,6 @@ export class DB {
461528
throw new Error(`Tag with ID ${id} not found`)
462529
}
463530

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
471531
const deleteResult = await this.db
472532
.prepare(sql`DELETE FROM tags WHERE id = ?1 AND user_id = ?2`)
473533
.bind(id, userId)

exercises/99.final/01.solution/src/db/migrations.ts

+31-11
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const migrations = [
2727
created_at integer DEFAULT (CURRENT_TIMESTAMP) NOT NULL,
2828
updated_at integer DEFAULT (CURRENT_TIMESTAMP) NOT NULL
2929
);
30+
CREATE INDEX IF NOT EXISTS idx_users_email ON users(email);
3031
`),
3132
// This is a mapping of a grant_user_id (accessible via props.grantId)
3233
// to the user_id that can be used to claim the grant. If user_id is
@@ -41,19 +42,26 @@ const migrations = [
4142
grant_user_id text NOT NULL,
4243
user_id integer,
4344
created_at integer DEFAULT (CURRENT_TIMESTAMP) NOT NULL,
44-
updated_at integer DEFAULT (CURRENT_TIMESTAMP) NOT NULL
45+
updated_at integer DEFAULT (CURRENT_TIMESTAMP) NOT NULL,
46+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
4547
);
48+
CREATE INDEX IF NOT EXISTS idx_grants_user_id ON grants(user_id);
49+
CREATE INDEX IF NOT EXISTS idx_grants_grant_user_id ON grants(grant_user_id);
4650
`),
47-
// An OTP emailed to the user to allow them to claim an access_token
51+
// An OTP emailed to the user to allow them to claim a grant
4852
db.prepare(sql`
4953
CREATE TABLE IF NOT EXISTS validation_tokens (
5054
id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
5155
token_value text NOT NULL,
5256
email text NOT NULL,
53-
grant_id text NOT NULL,
57+
grant_id integer NOT NULL,
5458
created_at integer DEFAULT (CURRENT_TIMESTAMP) NOT NULL,
55-
updated_at integer DEFAULT (CURRENT_TIMESTAMP) NOT NULL
59+
updated_at integer DEFAULT (CURRENT_TIMESTAMP) NOT NULL,
60+
FOREIGN KEY (grant_id) REFERENCES grants(id) ON DELETE CASCADE
5661
);
62+
CREATE INDEX IF NOT EXISTS idx_validation_tokens_email ON validation_tokens(email);
63+
CREATE INDEX IF NOT EXISTS idx_validation_tokens_token ON validation_tokens(token_value);
64+
CREATE INDEX IF NOT EXISTS idx_validation_tokens_grant ON validation_tokens(grant_id);
5765
`),
5866
db.prepare(sql`
5967
CREATE TABLE IF NOT EXISTS entries (
@@ -64,21 +72,28 @@ const migrations = [
6472
mood text,
6573
location text,
6674
weather text,
67-
is_private integer DEFAULT true NOT NULL,
68-
is_favorite integer DEFAULT false NOT NULL,
75+
is_private integer DEFAULT 1 NOT NULL CHECK (is_private IN (0, 1)),
76+
is_favorite integer DEFAULT 0 NOT NULL CHECK (is_favorite IN (0, 1)),
6977
created_at integer DEFAULT (CURRENT_TIMESTAMP) NOT NULL,
70-
updated_at integer DEFAULT (CURRENT_TIMESTAMP) NOT NULL
78+
updated_at integer DEFAULT (CURRENT_TIMESTAMP) NOT NULL,
79+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
7180
);
81+
CREATE INDEX IF NOT EXISTS idx_entries_user_id ON entries(user_id);
82+
CREATE INDEX IF NOT EXISTS idx_entries_created_at ON entries(created_at);
83+
CREATE INDEX IF NOT EXISTS idx_entries_is_private ON entries(is_private);
7284
`),
7385
db.prepare(sql`
7486
CREATE TABLE IF NOT EXISTS tags (
7587
id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
7688
user_id integer NOT NULL,
77-
name text NOT NULL UNIQUE,
89+
name text NOT NULL,
7890
description text,
7991
created_at integer DEFAULT (CURRENT_TIMESTAMP) NOT NULL,
80-
updated_at integer DEFAULT (CURRENT_TIMESTAMP) NOT NULL
92+
updated_at integer DEFAULT (CURRENT_TIMESTAMP) NOT NULL,
93+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
94+
UNIQUE(user_id, name)
8195
);
96+
CREATE INDEX IF NOT EXISTS idx_tags_user_id ON tags(user_id);
8297
`),
8398
db.prepare(sql`
8499
CREATE TABLE IF NOT EXISTS entry_tags (
@@ -88,9 +103,14 @@ const migrations = [
88103
tag_id integer NOT NULL,
89104
created_at integer DEFAULT (CURRENT_TIMESTAMP) NOT NULL,
90105
updated_at integer DEFAULT (CURRENT_TIMESTAMP) NOT NULL,
91-
FOREIGN KEY (entry_id) REFERENCES entries(id) ON UPDATE no action ON DELETE no action,
92-
FOREIGN KEY (tag_id) REFERENCES tags(id) ON UPDATE no action ON DELETE no action
106+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
107+
FOREIGN KEY (entry_id) REFERENCES entries(id) ON DELETE CASCADE,
108+
FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE,
109+
UNIQUE(entry_id, tag_id)
93110
);
111+
CREATE INDEX IF NOT EXISTS idx_entry_tags_user ON entry_tags(user_id);
112+
CREATE INDEX IF NOT EXISTS idx_entry_tags_entry ON entry_tags(entry_id);
113+
CREATE INDEX IF NOT EXISTS idx_entry_tags_tag ON entry_tags(tag_id);
94114
`),
95115
])
96116
console.log('Successfully created all tables')

exercises/99.final/01.solution/src/db/schema.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export const validationTokenSchema = z.object({
3030
id: z.coerce.number(),
3131
tokenValue: z.string(),
3232
email: z.string().email(),
33-
accessTokenId: z.coerce.number(),
33+
grantId: z.coerce.number(),
3434
createdAt: timestampSchema,
3535
updatedAt: timestampSchema,
3636
})

0 commit comments

Comments
 (0)