diff --git a/bun.lock b/bun.lock index d071a2b..f5dfe07 100644 --- a/bun.lock +++ b/bun.lock @@ -76,21 +76,21 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.19.12", "", { "os": "win32", "cpu": "x64" }, "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA=="], - "@oxlint/darwin-arm64": ["@oxlint/darwin-arm64@0.18.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-lJanO+V1JcV4QpfkFo/4L8qyjoL4jqSrOgA4bsf2JK0QH1w+bpQ4979dDF0HXsp1uJoyYVtkVuT8wP4C9TdJzg=="], + "@oxlint/darwin-arm64": ["@oxlint/darwin-arm64@1.0.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Ei8wLh65Th/si5EY6mfQIXVpdXbJWOoh56FaxxPgVxTeJaj3NHUIlxICHkvTZ5dz8bnOFcbS/+9MaW8Qkzfm9g=="], - "@oxlint/darwin-x64": ["@oxlint/darwin-x64@0.18.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-UiGJh67KSoS2RyEQQCjp5qKHBcu1vk/5kaF4f9db4JJmatpyEuqDjuvE6GBqhqWAKh064IvSVubQtvRp0v1M6w=="], + "@oxlint/darwin-x64": ["@oxlint/darwin-x64@1.0.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-dbdtQ+rJTUb4jFKTzV+j08yYcR8lZssLF10n7MggK/jI7pBtoQN04cupzYdkxOWSy6uDXjDmWYFDIqlTqV7zOg=="], - "@oxlint/linux-arm64-gnu": ["@oxlint/linux-arm64-gnu@0.18.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-3CsWnXoAB26GxlQzPiJKzd6lY0Z/S3Or6oAF/AwbS+Bn2hiB73Kt+BFOcFp5m3EOEDdGYHfRwIn2y1wUf76diA=="], + "@oxlint/linux-arm64-gnu": ["@oxlint/linux-arm64-gnu@1.0.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-71wy9zMxsAeRhCFQjUkDLT8N5tm10L5FxNxsUcEsezgM187X9tPGP1gwlFpYig7F+bg2X1dijFuTA/FSe0YpKg=="], - "@oxlint/linux-arm64-musl": ["@oxlint/linux-arm64-musl@0.18.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-gzVgf1GT+WHsujlj4MtQUvzILJ+SlYPib+xLx0aFIHE5jrnd6ZUuT+Nxrnn/cLA+f+9cPmDD7JByVksPFHhT2w=="], + "@oxlint/linux-arm64-musl": ["@oxlint/linux-arm64-musl@1.0.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UbD4+2k7aGZOFtKK/yeESX7Fv0w9gQbcjrjr1HGY7QOYg7XlFlqzycZdPS6XbAuKA5oOXFpafaYOD4AyX3p2AA=="], - "@oxlint/linux-x64-gnu": ["@oxlint/linux-x64-gnu@0.18.0", "", { "os": "linux", "cpu": "x64" }, "sha512-D94AgHk4qZt2v658OXZP13Re/Q1/hRQhMdOy+pzeih9POcTY31NOHmBInyI2OjZzUMT3JzUKc+efAmX7rlDJFg=="], + "@oxlint/linux-x64-gnu": ["@oxlint/linux-x64-gnu@1.0.0", "", { "os": "linux", "cpu": "x64" }, "sha512-0NXWqsm65I3VaLgADW4y9r7Pwurqgs2fr1lqoTyTIlidD18LQ3UMAWp8NzBPMCYzw8c/rTgOzsFf0gLtxzMtwg=="], - "@oxlint/linux-x64-musl": ["@oxlint/linux-x64-musl@0.18.0", "", { "os": "linux", "cpu": "x64" }, "sha512-B3kGrEE68F7qvyVaH2ihYB00E/iQOsi2SN+NeDht9FyUiftZRtIYlvZ/VE8l8yxXX/TFtwhed3bg2/IvaJ8Tpg=="], + "@oxlint/linux-x64-musl": ["@oxlint/linux-x64-musl@1.0.0", "", { "os": "linux", "cpu": "x64" }, "sha512-AY1NLnVQI+tBeuaB8KCriWfiD6O1zZFAQHphRDcZiqSz4mauNq9FFuffW0N9RSR9hYttGr0UVdQ6eK72RhzOYg=="], - "@oxlint/win32-arm64": ["@oxlint/win32-arm64@0.18.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-d+q8Vkhy4va7UZAETov1BWDA5G9XItY6HR7j57Uso5xfqVmFvnk2+L0LH9Z/PPFwNNR/37vBB364bJ4V6v/DlQ=="], + "@oxlint/win32-arm64": ["@oxlint/win32-arm64@1.0.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-X9y2KAdoqT/jy/sITGDZNMJHJAmhDhofItBnCf2DWS1HPakdtCAKGX9KMx6SivTbtPn1+JpZgfHn4Y7rNMvujQ=="], - "@oxlint/win32-x64": ["@oxlint/win32-x64@0.18.0", "", { "os": "win32", "cpu": "x64" }, "sha512-gMxC4XrhnR5XRtjqhIvkPI1yyinwkSjp7VgeOTf4mDk0xwYROwg/mSWnKfDn7tXUKoWQLDsSfAp/SS8uSAQ9Bg=="], + "@oxlint/win32-x64": ["@oxlint/win32-x64@1.0.0", "", { "os": "win32", "cpu": "x64" }, "sha512-x2eQwZCfRUi6GG0lhRuC54O6TK2uW7UbIvERh83vPi0ftd+rtGUuJauNdyC+pPx+iwFToFVet43/5MBMu4bMWg=="], "@petamoriken/float16": ["@petamoriken/float16@3.9.2", "", {}, "sha512-VgffxawQde93xKxT3qap3OH+meZf7VaSB5Sqd4Rqc+FP5alWbpOyan/7tRbOAvynjpG3GpdtAuGU/NdhQpmrog=="], @@ -172,7 +172,7 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "oxlint": ["oxlint@0.18.0", "", { "optionalDependencies": { "@oxlint/darwin-arm64": "0.18.0", "@oxlint/darwin-x64": "0.18.0", "@oxlint/linux-arm64-gnu": "0.18.0", "@oxlint/linux-arm64-musl": "0.18.0", "@oxlint/linux-x64-gnu": "0.18.0", "@oxlint/linux-x64-musl": "0.18.0", "@oxlint/win32-arm64": "0.18.0", "@oxlint/win32-x64": "0.18.0" }, "bin": { "oxlint": "bin/oxlint", "oxc_language_server": "bin/oxc_language_server" } }, "sha512-Dni48uC00HoBn63I1aFXS7ta7whJOMK5yBc/PQjQVQ1EWutHpcExhWO73XcOeljKUNsEkNWF/yTUFOIN6HbZiA=="], + "oxlint": ["oxlint@1.0.0", "", { "optionalDependencies": { "@oxlint/darwin-arm64": "1.0.0", "@oxlint/darwin-x64": "1.0.0", "@oxlint/linux-arm64-gnu": "1.0.0", "@oxlint/linux-arm64-musl": "1.0.0", "@oxlint/linux-x64-gnu": "1.0.0", "@oxlint/linux-x64-musl": "1.0.0", "@oxlint/win32-arm64": "1.0.0", "@oxlint/win32-x64": "1.0.0" }, "bin": { "oxlint": "bin/oxlint", "oxc_language_server": "bin/oxc_language_server" } }, "sha512-yyeryHnd21wPBLBEF4Uf8hvzJlftrIGHxyUaqFaP2JYiZ9cbiColygZhrezvv/Z/aThCmYu3j6iJMxlVPxNt6g=="], "pioppo": ["pioppo@1.2.1", "", { "dependencies": { "dettle": "^1.0.5", "when-exit": "^2.1.4" } }, "sha512-1oErGVWD6wFDPmrJWEY1Cj2p829UGT6Fw9OItYFxLkWtBjCvQSMC8wA5IcAR5ms/6gqiY8pnJvIV/+/Imyobew=="], @@ -206,7 +206,7 @@ "stubborn-fs": ["stubborn-fs@1.2.5", "", {}, "sha512-H2N9c26eXjzL/S/K+i/RHHcFanE74dptvvjM8iwzwbVcWY/zjBbgRqF3K0DY4+OD+uTTASTBvDoxPDaPN02D7g=="], - "tailwindcss": ["tailwindcss@4.1.8", "", {}, "sha512-kjeW8gjdxasbmFKpVGrGd5T4i40mV5J2Rasw48QARfYeQ8YS9x02ON9SFWax3Qf616rt4Cp3nVNIj6Hd1mP3og=="], + "tailwindcss": ["tailwindcss@4.1.9", "", {}, "sha512-anBZRcvfNMsQdHB9XSGzAtIQWlhs49uK75jfkwrqjRUbjt4d7q9RE1wR1xWyfYZhLFnFX4ahWp88Au2lcEw5IQ=="], "tiny-bin": ["tiny-bin@1.11.1", "", { "dependencies": { "ansi-purge": "^1.0.1", "fast-string-width": "^1.1.0", "get-current-package": "^1.0.1", "tiny-colors": "^2.2.2", "tiny-levenshtein": "^1.0.1", "tiny-parse-argv": "^2.8.2", "tiny-updater": "^3.5.3" } }, "sha512-UFC5EwtmCkFshKOBgXZzNFJsHpZrtbWZ/jQj+pwoIGUUbmenlQGGVDOwVqVOuG1nTxICSd+GLp3b+j7dUKZr2Q=="], diff --git a/package.json b/package.json index 4afb62d..4a622aa 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "bun-plugin-tailwind": "^0.0.15", "drizzle-kit": "^0.30.6", "drizzle-orm": "^0.41.0", - "tailwindcss": "^4.1.8" + "tailwindcss": "^4.1.9" }, "devDependencies": { "@types/bun": "latest", diff --git a/src/index.ts b/src/index.ts index 5be59c4..f699d73 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,11 @@ import homepage from '@routes/index.html'; -import { drizzleDB } from "@server/db"; -import { entry } from "@server/db/schema"; +import { drizzleDB } from '@server/db'; +import { entry } from '@server/db/schema'; import favicon from '@static/favicon.png'; import robotsTxt from '@static/robots.txt'; import sitemapTxt from '@static/sitemap.txt'; -import { env } from "bun"; -import { desc, eq } from "drizzle-orm"; +import { env } from 'bun'; +import { desc, eq, sql } from 'drizzle-orm'; const development = env.NODE_ENV !== 'production'; @@ -26,6 +26,58 @@ function etagResponse(etag: string, cacheControl?: string) { let entriesETag = ''; let entriesNotModified = false; +const fetchEntries = drizzleDB + .select({ + id: entry.id, + name: entry.name, + href: entry.href, + finished: entry.finished, + updated_at: entry.updatedAt, + }) + .from(entry) + .orderBy(desc(entry.updatedAt)) + .prepare(); + +const entryExists = drizzleDB + .select({ id: entry.id }) + .from(entry) + .where(eq(entry.name, sql.placeholder('name'))) + .prepare(); + +const insertEntry = drizzleDB + .insert(entry) + .values({ + name: sql.placeholder('name'), + href: sql.placeholder('href'), + }) + .prepare(); + +const updateEntry = drizzleDB + .update(entry) + .set({ + name: sql.placeholder('name') as any, + href: sql.placeholder('href') as any, + }) + .where(eq(entry.id, sql.placeholder('id'))) + .prepare(); + +const fetchEntryById = drizzleDB + .select() + .from(entry) + .where(eq(entry.id, sql.placeholder('id'))) + .prepare(); + +const deleteEntry = drizzleDB + .delete(entry) + .where(eq(entry.id, sql.placeholder('id'))) + .prepare(); + +const updateEntryFinished = drizzleDB + .update(entry) + .set({ finished: sql.placeholder('finished') as any }) + .where(eq(entry.id, sql.placeholder('id'))) + .prepare(); + Bun.serve({ routes: { '/': homepage, @@ -46,16 +98,13 @@ Bun.serve({ } entriesNotModified = true; - const entries = await drizzleDB - .select({ id: entry.id, name: entry.name, href: entry.href, finished: entry.finished, updated_at: entry.updatedAt }) - .from(entry) - .orderBy(desc(entry.updatedAt)); + const entries = await fetchEntries.execute(); const body = JSON.stringify(entries); entriesETag = getEtag(body); return new Response(body, { - headers: { 'Content-Type': 'application/json', 'ETag': entriesETag } + headers: { 'Content-Type': 'application/json', ETag: entriesETag }, }); }, async PUT(req) { @@ -64,23 +113,27 @@ Bun.serve({ const body = await getEntryFromReq(req); if (!body) return new Response('Invalid data', { status: 400 }); - const result = await drizzleDB.select({ id: entry.id }).from(entry).where(eq(entry.name, body.name)); + const result = await entryExists.execute({ name: body.name }); let created = true; if (result.length === 0) { - await drizzleDB.insert(entry).values(body).execute(); + await insertEntry.execute(body); } else if (result.length === 1) { - const row = result[0] as NonNullable; + const row = result[0] as NonNullable<(typeof result)[number]>; created = false; - await drizzleDB.update(entry).set(body).where(eq(entry.id, row.id)).execute(); + await updateEntry.execute({ + id: row.id, + name: body.name, + href: body.href, + }); } else { return new Response('Invalid data, multiple matches?', { status: 400 }); } entriesNotModified = false; return new Response(JSON.stringify({ created }), { - headers: { 'Content-Type': 'application/json' } + headers: { 'Content-Type': 'application/json' }, }); }, }, @@ -91,10 +144,10 @@ Bun.serve({ const id = Number.parseInt(req.params.id, 10); if (Number.isNaN(id)) return new Response('Invalid id', { status: 400 }); - const result = await drizzleDB.select().from(entry).where(eq(entry.id, id)); + const result = await fetchEntryById.execute({ id }); return new Response(JSON.stringify(result), { - headers: { 'Content-Type': 'application/json' } + headers: { 'Content-Type': 'application/json' }, }); }, async PUT(req) { @@ -106,13 +159,15 @@ Bun.serve({ const body = await getEntryFromReq(req); if (!body) return new Response('Invalid data', { status: 400 }); - await drizzleDB.update(entry).set(body).where(eq(entry.id, id)).execute(); - - let created = false; + await updateEntry.execute({ + id, + name: body.name, + href: body.href, + }); entriesNotModified = false; return new Response(JSON.stringify({ created: false }), { - headers: { 'Content-Type': 'application/json' } + headers: { 'Content-Type': 'application/json' }, }); }, async DELETE(req) { @@ -121,7 +176,7 @@ Bun.serve({ const id = Number.parseInt(req.params.id, 10); if (Number.isNaN(id)) return new Response('Invalid id', { status: 400 }); - await drizzleDB.delete(entry).where(eq(entry.id, id)).execute(); + await deleteEntry.execute({ id }); entriesNotModified = false; return new Response('OK'); @@ -135,11 +190,7 @@ Bun.serve({ if (Number.isNaN(id)) return new Response('Invalid id', { status: 400 }); const finished = req.params.finished === 'true'; - await drizzleDB - .update(entry) - .set({ finished: finished }) - .where(eq(entry.id, id)) - .execute(); + await updateEntryFinished.execute({ id, finished }); entriesNotModified = false; return new Response('OK'); @@ -159,7 +210,7 @@ console.log('Server started on port:', env.PORT ? Number.parseInt(env.PORT, 10) async function getEntryFromReq(req: Request) { const json = await req.json(); - const body = json as { name: string, href: string } + const body = json as { name: string; href: string }; if (!body.name || !body.href || typeof body.name !== 'string' || typeof body.href !== 'string') { return null; } @@ -180,7 +231,7 @@ function isAuthenticated(req: Request) { } const unauthorizedHeaders = new Headers({ - 'WWW-Authenticate': `Bearer realm='sign', error="invalid_request"` + 'WWW-Authenticate': `Bearer realm='sign', error="invalid_request"`, }); function unauthorizedResp() {