prepared queries

This commit is contained in:
Niki Wix Skaarup 2025-06-16 21:44:48 +02:00
parent d10a9e2bde
commit 0348b07325
Signed by: nikiskaarup
GPG key ID: FC2F1B116F6E788C
3 changed files with 90 additions and 39 deletions

View file

@ -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=="],

View file

@ -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",

View file

@ -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<typeof result[number]>;
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() {