Skip to content

Commit f91d08d

Browse files
committed
really great start
1 parent 6605adb commit f91d08d

16 files changed

+10953
-225
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,5 @@ data.db
1313
# file as well, but since this is for a workshop
1414
# we're going to keep them around.
1515
# .env
16+
17+
.wrangler/
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { config as defaultConfig } from '@epic-web/config/eslint'
2+
3+
/** @type {import("eslint").Linter.Config[]} */
4+
export default [
5+
...defaultConfig,
6+
{
7+
ignores: ['./.wrangler/**'],
8+
},
9+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
{
2+
"name": "exercises_99.final_01.solution",
3+
"private": true,
4+
"type": "module",
5+
"main": "index.js",
6+
"scripts": {
7+
"lint": "eslint --fix .",
8+
"format": "prettier --write .",
9+
"pretypecheck": "wrangler types ./types/worker-configuration.d.ts",
10+
"typecheck": "tsc",
11+
"build": "wrangler build",
12+
"dev": "wrangler dev"
13+
},
14+
"devDependencies": {
15+
"@epic-web/config": "^1.19.0",
16+
"@modelcontextprotocol/inspector": "^0.10.0",
17+
"@types/node": "^22.14.1",
18+
"eslint": "^9.24.0",
19+
"prettier": "^3.5.3",
20+
"typescript": "^5.8.3",
21+
"wrangler": "^4.12.0"
22+
},
23+
"dependencies": {
24+
"@epic-web/invariant": "^1.0.0",
25+
"@modelcontextprotocol/sdk": "^1.10.0",
26+
"agents": "^0.0.60",
27+
"zod": "^3.24.3"
28+
},
29+
"license": "GPL-3.0-only"
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
/// <reference path="../../types/worker-configuration.d.ts" />
2+
3+
import { z } from 'zod'
4+
import { migrate } from './migrations.ts'
5+
import {
6+
type Entry,
7+
type NewEntry,
8+
type Tag,
9+
type NewTag,
10+
type EntryTag,
11+
type NewEntryTag,
12+
entrySchema,
13+
newEntrySchema,
14+
tagSchema,
15+
newTagSchema,
16+
entryTagSchema,
17+
newEntryTagSchema,
18+
} from './schema.ts'
19+
import { sql, snakeToCamel } from './utils.ts'
20+
21+
export interface Env {
22+
EPIC_ME_DB: D1Database
23+
}
24+
25+
export type { Entry, NewEntry, Tag, NewTag, EntryTag, NewEntryTag }
26+
27+
export class DB {
28+
constructor(private db: D1Database) {}
29+
30+
static async getInstance(env: Env) {
31+
const db = new DB(env.EPIC_ME_DB)
32+
await migrate(env.EPIC_ME_DB)
33+
return db
34+
}
35+
36+
// Entry Methods
37+
async createEntry(entry: z.input<typeof newEntrySchema>) {
38+
// Validate input
39+
const validatedEntry = newEntrySchema.parse(entry)
40+
41+
const ps = this.db.prepare(sql`
42+
INSERT INTO entries (
43+
title, content, mood, location, weather,
44+
is_private, is_favorite
45+
) VALUES (
46+
?1, ?2, ?3, ?4, ?5,
47+
?6, ?7
48+
)
49+
`)
50+
51+
const insertResult = await ps
52+
.bind(
53+
validatedEntry.title,
54+
validatedEntry.content,
55+
validatedEntry.mood,
56+
validatedEntry.location,
57+
validatedEntry.weather,
58+
validatedEntry.isPrivate,
59+
validatedEntry.isFavorite,
60+
)
61+
.run()
62+
63+
if (!insertResult.success || !insertResult.meta.last_row_id) {
64+
throw new Error('Failed to create entry: ' + insertResult.error)
65+
}
66+
67+
// Fetch the created entry
68+
const createdEntry = await this.getEntry(insertResult.meta.last_row_id)
69+
if (!createdEntry) {
70+
throw new Error('Failed to fetch created entry')
71+
}
72+
73+
return createdEntry
74+
}
75+
76+
async getEntry(id: number) {
77+
const result = await this.db
78+
.prepare(sql`SELECT * FROM entries WHERE id = ?1`)
79+
.bind(id)
80+
.first()
81+
82+
if (!result) return null
83+
84+
return entrySchema.parse(snakeToCamel(result))
85+
}
86+
87+
async listEntries() {
88+
const results = await this.db
89+
.prepare(sql`SELECT * FROM entries ORDER BY created_at DESC`)
90+
.all()
91+
92+
return z
93+
.array(entrySchema)
94+
.parse(results.results.map((result) => snakeToCamel(result)))
95+
}
96+
97+
async updateEntry(
98+
id: number,
99+
entry: Partial<z.input<typeof newEntrySchema>>,
100+
) {
101+
const existingEntry = await this.getEntry(id)
102+
if (!existingEntry) {
103+
throw new Error(`Entry with ID ${id} not found`)
104+
}
105+
106+
const updates = Object.entries(entry)
107+
.filter(([, value]) => value !== undefined)
108+
.map(
109+
([key], index) =>
110+
`${key === 'isPrivate' ? 'is_private' : key === 'isFavorite' ? 'is_favorite' : key} = ?${index + 2}`,
111+
)
112+
.join(', ')
113+
114+
if (!updates) {
115+
return existingEntry
116+
}
117+
118+
const ps = this.db.prepare(sql`
119+
UPDATE entries
120+
SET ${updates}, updated_at = CURRENT_TIMESTAMP
121+
WHERE id = ?1
122+
`)
123+
124+
const updateValues = [
125+
id,
126+
...Object.entries(entry)
127+
.filter(([, value]) => value !== undefined)
128+
.map(([, value]) => value),
129+
]
130+
131+
const updateResult = await ps.bind(...updateValues).run()
132+
133+
if (!updateResult.success) {
134+
throw new Error('Failed to update entry: ' + updateResult.error)
135+
}
136+
137+
const updatedEntry = await this.getEntry(id)
138+
if (!updatedEntry) {
139+
throw new Error('Failed to fetch updated entry')
140+
}
141+
142+
return updatedEntry
143+
}
144+
145+
async deleteEntry(id: number) {
146+
const existingEntry = await this.getEntry(id)
147+
if (!existingEntry) {
148+
throw new Error(`Entry with ID ${id} not found`)
149+
}
150+
151+
// First delete all entry tags
152+
await this.db
153+
.prepare(sql`DELETE FROM entry_tags WHERE entry_id = ?1`)
154+
.bind(id)
155+
.run()
156+
157+
// Then delete the entry
158+
const deleteResult = await this.db
159+
.prepare(sql`DELETE FROM entries WHERE id = ?1`)
160+
.bind(id)
161+
.run()
162+
163+
if (!deleteResult.success) {
164+
throw new Error('Failed to delete entry: ' + deleteResult.error)
165+
}
166+
167+
return true
168+
}
169+
170+
// Tag Methods
171+
async createTag(tag: NewTag) {
172+
const validatedTag = newTagSchema.parse(tag)
173+
174+
const ps = this.db.prepare(sql`
175+
INSERT INTO tags (name, description)
176+
VALUES (?1, ?2)
177+
`)
178+
179+
const insertResult = await ps
180+
.bind(validatedTag.name, validatedTag.description)
181+
.run()
182+
183+
if (!insertResult.success || !insertResult.meta.last_row_id) {
184+
throw new Error('Failed to create tag: ' + insertResult.error)
185+
}
186+
187+
const createdTag = await this.getTag(insertResult.meta.last_row_id)
188+
if (!createdTag) {
189+
throw new Error('Failed to fetch created tag')
190+
}
191+
192+
return createdTag
193+
}
194+
195+
async getTag(id: number) {
196+
const result = await this.db
197+
.prepare(sql`SELECT * FROM tags WHERE id = ?1`)
198+
.bind(id)
199+
.first()
200+
201+
if (!result) return null
202+
203+
return tagSchema.parse(snakeToCamel(result))
204+
}
205+
206+
async listTags() {
207+
const results = await this.db
208+
.prepare(sql`SELECT * FROM tags ORDER BY name`)
209+
.all()
210+
211+
return z
212+
.array(tagSchema)
213+
.parse(results.results.map((result) => snakeToCamel(result)))
214+
}
215+
216+
async updateTag(id: number, tag: Partial<z.input<typeof newTagSchema>>) {
217+
const existingTag = await this.getTag(id)
218+
if (!existingTag) {
219+
throw new Error(`Tag with ID ${id} not found`)
220+
}
221+
222+
const updates = Object.entries(tag)
223+
.filter(([, value]) => value !== undefined)
224+
.map(([key], index) => `${key} = ?${index + 2}`)
225+
.join(', ')
226+
227+
if (!updates) {
228+
return existingTag
229+
}
230+
231+
const ps = this.db.prepare(sql`
232+
UPDATE tags
233+
SET ${updates}, updated_at = CURRENT_TIMESTAMP
234+
WHERE id = ?1
235+
`)
236+
237+
const updateValues = [
238+
id,
239+
...Object.entries(tag)
240+
.filter(([, value]) => value !== undefined)
241+
.map(([, value]) => value),
242+
]
243+
244+
const updateResult = await ps.bind(...updateValues).run()
245+
246+
if (!updateResult.success) {
247+
throw new Error('Failed to update tag: ' + updateResult.error)
248+
}
249+
250+
const updatedTag = await this.getTag(id)
251+
if (!updatedTag) {
252+
throw new Error('Failed to fetch updated tag')
253+
}
254+
255+
return updatedTag
256+
}
257+
258+
async deleteTag(id: number) {
259+
const existingTag = await this.getTag(id)
260+
if (!existingTag) {
261+
throw new Error(`Tag with ID ${id} not found`)
262+
}
263+
264+
// First delete all entry tags
265+
await this.db
266+
.prepare(sql`DELETE FROM entry_tags WHERE tag_id = ?1`)
267+
.bind(id)
268+
.run()
269+
270+
// Then delete the tag
271+
const deleteResult = await this.db
272+
.prepare(sql`DELETE FROM tags WHERE id = ?1`)
273+
.bind(id)
274+
.run()
275+
276+
if (!deleteResult.success) {
277+
throw new Error('Failed to delete tag: ' + deleteResult.error)
278+
}
279+
280+
return true
281+
}
282+
283+
// Entry Tag Methods
284+
async addTagToEntry(entryTag: NewEntryTag) {
285+
const validatedEntryTag = newEntryTagSchema.parse(entryTag)
286+
287+
const ps = this.db.prepare(sql`
288+
INSERT INTO entry_tags (entry_id, tag_id)
289+
VALUES (?1, ?2)
290+
`)
291+
292+
const insertResult = await ps
293+
.bind(validatedEntryTag.entryId, validatedEntryTag.tagId)
294+
.run()
295+
296+
if (!insertResult.success || !insertResult.meta.last_row_id) {
297+
throw new Error('Failed to add tag to entry: ' + insertResult.error)
298+
}
299+
300+
const created = await this.getEntryTag(insertResult.meta.last_row_id)
301+
if (!created) {
302+
throw new Error('Failed to fetch created entry tag')
303+
}
304+
305+
return created
306+
}
307+
308+
async getEntryTag(id: number) {
309+
const result = await this.db
310+
.prepare(sql`SELECT * FROM entry_tags WHERE id = ?1`)
311+
.bind(id)
312+
.first()
313+
314+
if (!result) return null
315+
316+
return entryTagSchema.parse(snakeToCamel(result))
317+
}
318+
319+
async getEntryTags(entryId: number) {
320+
const results = await this.db
321+
.prepare(
322+
sql`
323+
SELECT t.*
324+
FROM tags t
325+
JOIN entry_tags et ON et.tag_id = t.id
326+
WHERE et.entry_id = ?1
327+
ORDER BY t.name
328+
`,
329+
)
330+
.bind(entryId)
331+
.all()
332+
333+
return z
334+
.array(tagSchema)
335+
.parse(results.results.map((result) => snakeToCamel(result)))
336+
}
337+
}

0 commit comments

Comments
 (0)