restructured and simplified favicon
This commit is contained in:
parent
44d5fafc66
commit
5179d87cda
20 changed files with 388 additions and 333 deletions
|
@ -5,26 +5,11 @@ root = true
|
||||||
|
|
||||||
[*]
|
[*]
|
||||||
indent_style = space
|
indent_style = space
|
||||||
indent_size = 4
|
indent_size = 2
|
||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
trim_trailing_whitespace = true
|
trim_trailing_whitespace = true
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
|
|
||||||
[*.md]
|
[*.md]
|
||||||
indent_size = 2
|
|
||||||
trim_trailing_whitespace = false
|
trim_trailing_whitespace = false
|
||||||
|
|
||||||
[*.js]
|
|
||||||
indent_style = tab
|
|
||||||
|
|
||||||
[*.ts]
|
|
||||||
indent_style = tab
|
|
||||||
|
|
||||||
[*.yml]
|
|
||||||
indent_style = space
|
|
||||||
indent_size = 2
|
|
||||||
|
|
||||||
[*.json]
|
|
||||||
indent_style = space
|
|
||||||
indent_size = 2
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
preload = ["./image-plugin.ts"]
|
||||||
|
|
||||||
[install.scopes]
|
[install.scopes]
|
||||||
"@jsr" = "https://npm.jsr.io"
|
"@jsr" = "https://npm.jsr.io"
|
||||||
|
|
||||||
|
|
30
image-plugin.ts
Normal file
30
image-plugin.ts
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import { plugin, type BunPlugin } from "bun";
|
||||||
|
import { readFileSync } from 'fs';
|
||||||
|
|
||||||
|
const imageLoaderPlugin: BunPlugin = {
|
||||||
|
name: "image-loader",
|
||||||
|
setup(builder) {
|
||||||
|
builder.onLoad({ filter: /\.(png|jpg|jpeg|webp)/ }, async ({ path }) => {
|
||||||
|
try {
|
||||||
|
const file = Bun.file(path);
|
||||||
|
const bytes = await file.bytes();
|
||||||
|
return {
|
||||||
|
exports: { default: bytes },
|
||||||
|
loader: "object",
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
if (err instanceof Error) {
|
||||||
|
throw new Error(`Failed to load file: ${err.message}`);
|
||||||
|
} else if (typeof err === 'string') {
|
||||||
|
throw new Error(`Failed to load file: ${err}`);
|
||||||
|
}
|
||||||
|
throw new Error("Failed to load file");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
plugin(imageLoaderPlugin);
|
||||||
|
|
||||||
|
export default imageLoaderPlugin;
|
|
@ -3,14 +3,14 @@
|
||||||
"version": "1.0.50",
|
"version": "1.0.50",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"module": "src/server.ts",
|
"module": "src/index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"migrate": "bun --bun run ./src/db/migrate.ts",
|
"migrate": "bun --bun run ./src/db/migrate.ts",
|
||||||
"generate": "bunx --bun drizzle-kit generate --config=./drizzle.config.ts",
|
"generate": "bunx --bun drizzle-kit generate --config=./drizzle.config.ts",
|
||||||
"studio": "bunx --bun drizzle-kit studio --config=./drizzle.config.ts",
|
"studio": "bunx --bun drizzle-kit studio --config=./drizzle.config.ts",
|
||||||
"up": "drizzle-kit up --config=./drizzle.config.ts",
|
"up": "drizzle-kit up --config=./drizzle.config.ts",
|
||||||
"start": "bun migrate && bun src/server.ts",
|
"start": "bun migrate && bun src/index.ts",
|
||||||
"dev": "bun run --watch src/server.ts",
|
"dev": "bun run --watch src/index.ts",
|
||||||
"lint": "oxlint .",
|
"lint": "oxlint .",
|
||||||
"format": "prettier --write ."
|
"format": "prettier --write ."
|
||||||
},
|
},
|
||||||
|
|
0
src/client/.gitkeep
Normal file
0
src/client/.gitkeep
Normal file
25
src/images.d.ts
vendored
Normal file
25
src/images.d.ts
vendored
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
|
||||||
|
declare module '*.png' {
|
||||||
|
const value: Uint8Array<ArrayBufferLike>;
|
||||||
|
// const file: Bun.BunFile;
|
||||||
|
// export { file };
|
||||||
|
export default value;
|
||||||
|
}
|
||||||
|
declare module '*.jpeg' {
|
||||||
|
const value: Uint8Array<ArrayBufferLike>;
|
||||||
|
// const file: Bun.BunFile;
|
||||||
|
// export { file };
|
||||||
|
export default value;
|
||||||
|
}
|
||||||
|
declare module '*.jpg' {
|
||||||
|
const value: Uint8Array<ArrayBufferLike>;
|
||||||
|
// const file: Bun.BunFile;
|
||||||
|
// export { file };
|
||||||
|
export default value;
|
||||||
|
}
|
||||||
|
declare module '*.webp' {
|
||||||
|
const value: Uint8Array<ArrayBufferLike>;
|
||||||
|
// const file: Bun.BunFile;
|
||||||
|
// export { file };
|
||||||
|
export default value;
|
||||||
|
}
|
|
@ -1,23 +0,0 @@
|
||||||
@import 'tailwindcss' source('.');
|
|
||||||
|
|
||||||
@theme {
|
|
||||||
--breakout-size: calc((var(--breakpoint-xl) - var(--breakpoint-lg)) / 2);
|
|
||||||
--ultrawide-val: minmax(calc(var(--spacing) * 4), 1fr);
|
|
||||||
--breakout-val: minmax(0, var(--breakout-size));
|
|
||||||
--content-val: min(100% - calc(var(--spacing) * 8), var(--breakpoint-lg));
|
|
||||||
}
|
|
||||||
|
|
||||||
@layer base {
|
|
||||||
* {
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content {
|
|
||||||
grid-template-columns:
|
|
||||||
[ultrawide-start] var(--ultrawide-val) [breakout-start] var(--breakout-val) [content-start] var(--content-val) [content-end] var(--breakout-val) [breakout-end] var(--ultrawide-val) [ultrawide-end];
|
|
||||||
}
|
|
||||||
|
|
||||||
.content>* {
|
|
||||||
grid-column: content;
|
|
||||||
}
|
|
||||||
}
|
|
9
src/index.d.ts
vendored
9
src/index.d.ts
vendored
|
@ -1,9 +0,0 @@
|
||||||
import type { Database } from 'bun:sqlite';
|
|
||||||
import type { BunSQLiteDatabase } from 'drizzle-orm/bun-sqlite';
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
var db: Database;
|
|
||||||
var drizzleDB: BunSQLiteDatabase;
|
|
||||||
var performanceObserver: PerformanceObserver;
|
|
||||||
var wrappedTimers: Map<string, import('../wrapped-timer').WrappedTimer>;
|
|
||||||
}
|
|
File diff suppressed because one or more lines are too long
158
src/index.ts
Normal file
158
src/index.ts
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
import { env } from "bun";
|
||||||
|
import homepage from '@routes/index.html';
|
||||||
|
import robotsTxt from '@static/robots.txt';
|
||||||
|
import sitemapTxt from '@static/sitemap.txt';
|
||||||
|
import favicon from '@static/favicon.png';
|
||||||
|
import { entry } from "@server/db/schema";
|
||||||
|
import { desc, eq } from "drizzle-orm";
|
||||||
|
import { drizzleDB } from "@server/db";
|
||||||
|
|
||||||
|
const development = env.NODE_ENV !== 'production';
|
||||||
|
|
||||||
|
const faviconInit = { headers: new Headers({ 'Content-Type': 'image/png' }) };
|
||||||
|
|
||||||
|
console.log('woo');
|
||||||
|
Bun.serve({
|
||||||
|
routes: {
|
||||||
|
'/': homepage,
|
||||||
|
'/robots.txt': new Response(robotsTxt, {
|
||||||
|
headers: { 'Content-Type': 'text/plain' },
|
||||||
|
}),
|
||||||
|
'/sitemap.txt': new Response(sitemapTxt, {
|
||||||
|
headers: { 'Content-Type': 'text/plain' },
|
||||||
|
}),
|
||||||
|
'/favicon.ico': new Response(favicon, faviconInit),
|
||||||
|
'/health': new Response('OK'),
|
||||||
|
'/api/entries': {
|
||||||
|
async GET(req) {
|
||||||
|
if (!isAuthenticated(req)) return unauthorizedResp();
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
return new Response(JSON.stringify(entries), {
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async PUT(req) {
|
||||||
|
if (!isAuthenticated(req)) return unauthorizedResp();
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
let created = true;
|
||||||
|
|
||||||
|
if (result.length === 0) {
|
||||||
|
await drizzleDB.insert(entry).values(body).execute();
|
||||||
|
} else if (result.length === 1) {
|
||||||
|
const row = result[0] as NonNullable<typeof result[number]>;
|
||||||
|
created = false;
|
||||||
|
await drizzleDB.update(entry).set(body).where(eq(entry.id, row.id)).execute();
|
||||||
|
} else {
|
||||||
|
return new Response('Invalid data, multiple matches?', { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(JSON.stringify({ created }), {
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'/api/entries/:id': {
|
||||||
|
async GET(req) {
|
||||||
|
if (!isAuthenticated(req)) return unauthorizedResp();
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
return new Response(JSON.stringify(result), {
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async PUT(req) {
|
||||||
|
if (!isAuthenticated(req)) return unauthorizedResp();
|
||||||
|
|
||||||
|
const id = Number.parseInt(req.params.id, 10);
|
||||||
|
if (Number.isNaN(id)) return new Response('Invalid id', { status: 400 });
|
||||||
|
|
||||||
|
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;
|
||||||
|
return new Response(JSON.stringify({ created: false }), {
|
||||||
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async DELETE(req) {
|
||||||
|
if (!isAuthenticated(req)) return unauthorizedResp();
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
return new Response('OK');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'/api/entries/:id/check/:finished': {
|
||||||
|
async PUT(req) {
|
||||||
|
if (!isAuthenticated(req)) return unauthorizedResp();
|
||||||
|
|
||||||
|
const id = Number.parseInt(req.params.id, 10);
|
||||||
|
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();
|
||||||
|
|
||||||
|
return new Response('OK');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
development,
|
||||||
|
reusePort: true,
|
||||||
|
port: env.PORT || 3000,
|
||||||
|
// async fetch(req, server) {
|
||||||
|
// console.log(req);
|
||||||
|
// return new Response("Not found", { status: 404 });
|
||||||
|
// },
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Server started on port:', env.PORT ? Number.parseInt(env.PORT, 10) : 3000);
|
||||||
|
|
||||||
|
async function getEntryFromReq(req: Request) {
|
||||||
|
const json = await req.json();
|
||||||
|
const body = json as { name: string, href: string }
|
||||||
|
if (!body.name || !body.href || typeof body.name !== 'string' || typeof body.href !== 'string') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim and lowercase the name to ensure consistency and uniqueness
|
||||||
|
body.name = body.name.trim().toLocaleLowerCase();
|
||||||
|
|
||||||
|
return body;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAuthenticated(req: Request) {
|
||||||
|
const auth = req.headers.get('authorization');
|
||||||
|
if (!auth) return false;
|
||||||
|
const [type, bearer] = auth.split(' ');
|
||||||
|
if (type !== 'Bearer') return false;
|
||||||
|
// Check if the token is valid
|
||||||
|
return bearer === env.BEARER_TOKEN;
|
||||||
|
}
|
||||||
|
|
||||||
|
const unauthorizedHeaders = new Headers({
|
||||||
|
'WWW-Authenticate': `Bearer realm='sign', error="invalid_request"`
|
||||||
|
});
|
||||||
|
|
||||||
|
function unauthorizedResp() {
|
||||||
|
return new Response('Unauthorized', { status: 401, headers: unauthorizedHeaders });
|
||||||
|
}
|
23
src/routes/index.css
Normal file
23
src/routes/index.css
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
@import 'tailwindcss' source('../');
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--breakout-size: calc((var(--breakpoint-xl) - var(--breakpoint-lg)) / 2);
|
||||||
|
--ultrawide-val: minmax(calc(var(--spacing) * 4), 1fr);
|
||||||
|
--breakout-val: minmax(0, var(--breakout-size));
|
||||||
|
--content-val: min(100% - calc(var(--spacing) * 8), var(--breakpoint-lg));
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
grid-template-columns:
|
||||||
|
[ultrawide-start] var(--ultrawide-val) [breakout-start] var(--breakout-val) [content-start] var(--content-val) [content-end] var(--breakout-val) [breakout-end] var(--ultrawide-val) [ultrawide-end];
|
||||||
|
}
|
||||||
|
|
||||||
|
.content>* {
|
||||||
|
grid-column: content;
|
||||||
|
}
|
||||||
|
}
|
21
src/routes/index.html
Normal file
21
src/routes/index.html
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en" class="min-h-dvh">
|
||||||
|
<head>
|
||||||
|
<title>Progress tracker API</title>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
href="@static/favicon.png"
|
||||||
|
/>
|
||||||
|
<link rel="stylesheet" href="./index.css" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body
|
||||||
|
class="min-h-dvh bg-gray-950 bg-cover bg-fixed bg-center bg-no-repeat font-mono text-pretty text-gray-100 selection:bg-pink-600 selection:text-gray-200"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col items-center justify-center min-h-screen">
|
||||||
|
<h1>progress tracker api</h1>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
157
src/server.ts
157
src/server.ts
|
@ -1,157 +0,0 @@
|
||||||
import { env } from "bun";
|
|
||||||
import homepage from './index.html';
|
|
||||||
import robotsTxt from './static/robots.txt';
|
|
||||||
import sitemapTxt from './static/sitemap.txt';
|
|
||||||
// @ts-ignore ts2307
|
|
||||||
import icon from './static/favicon.png' with { type: 'file' };
|
|
||||||
import { entry } from "./db/schema";
|
|
||||||
import { desc, eq } from "drizzle-orm";
|
|
||||||
import { drizzleDB } from "./db";
|
|
||||||
const favicon = await Bun.file(icon).bytes();
|
|
||||||
|
|
||||||
const development = env.NODE_ENV !== 'production';
|
|
||||||
|
|
||||||
Bun.serve({
|
|
||||||
routes: {
|
|
||||||
'/': homepage,
|
|
||||||
'/robots.txt': new Response(robotsTxt, {
|
|
||||||
headers: { 'Content-Type': 'text/plain' },
|
|
||||||
}),
|
|
||||||
'/sitemap.txt': new Response(sitemapTxt, {
|
|
||||||
headers: { 'Content-Type': 'text/plain' },
|
|
||||||
}),
|
|
||||||
'/favicon.ico': new Response(favicon, {
|
|
||||||
headers: { 'Content-Type': 'image/png' },
|
|
||||||
}),
|
|
||||||
'/health': new Response('OK'),
|
|
||||||
'/api/entries': {
|
|
||||||
async GET(req) {
|
|
||||||
if (!isAuthenticated(req)) return unauthorizedResp();
|
|
||||||
|
|
||||||
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));
|
|
||||||
|
|
||||||
return new Response(JSON.stringify(entries), {
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
});
|
|
||||||
},
|
|
||||||
async PUT(req) {
|
|
||||||
if (!isAuthenticated(req)) return unauthorizedResp();
|
|
||||||
|
|
||||||
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));
|
|
||||||
|
|
||||||
let created = true;
|
|
||||||
|
|
||||||
if (result.length === 0) {
|
|
||||||
await drizzleDB.insert(entry).values(body).execute();
|
|
||||||
} else if (result.length === 1) {
|
|
||||||
const row = result[0];
|
|
||||||
created = false;
|
|
||||||
await drizzleDB.update(entry).set(body).where(eq(entry.id, row.id)).execute();
|
|
||||||
} else {
|
|
||||||
return new Response('Invalid data, multiple matches?', { status: 400 });
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Response(JSON.stringify({ created }), {
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'/api/entries/:id': {
|
|
||||||
async GET(req) {
|
|
||||||
if (!isAuthenticated(req)) return unauthorizedResp();
|
|
||||||
|
|
||||||
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));
|
|
||||||
|
|
||||||
return new Response(JSON.stringify(result), {
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
});
|
|
||||||
},
|
|
||||||
async PUT(req) {
|
|
||||||
if (!isAuthenticated(req)) return unauthorizedResp();
|
|
||||||
|
|
||||||
const id = Number.parseInt(req.params.id, 10);
|
|
||||||
if (Number.isNaN(id)) return new Response('Invalid id', { status: 400 });
|
|
||||||
|
|
||||||
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;
|
|
||||||
return new Response(JSON.stringify({ created: false }), {
|
|
||||||
headers: { 'Content-Type': 'application/json' }
|
|
||||||
});
|
|
||||||
},
|
|
||||||
async DELETE(req) {
|
|
||||||
if (!isAuthenticated(req)) return unauthorizedResp();
|
|
||||||
|
|
||||||
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();
|
|
||||||
|
|
||||||
return new Response('OK');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'/api/entries/:id/check/:finished': {
|
|
||||||
async PUT(req) {
|
|
||||||
if (!isAuthenticated(req)) return unauthorizedResp();
|
|
||||||
|
|
||||||
const id = Number.parseInt(req.params.id, 10);
|
|
||||||
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();
|
|
||||||
|
|
||||||
return new Response('OK');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
development,
|
|
||||||
reusePort: true,
|
|
||||||
port: env.PORT || 3000,
|
|
||||||
// async fetch(req, server) {
|
|
||||||
// console.log(req);
|
|
||||||
// return new Response("Not found", { status: 404 });
|
|
||||||
// },
|
|
||||||
});
|
|
||||||
|
|
||||||
async function getEntryFromReq(req: Request) {
|
|
||||||
const json = await req.json();
|
|
||||||
const body = json as { name: string, href: string }
|
|
||||||
if (!body.name || !body.href || typeof body.name !== 'string' || typeof body.href !== 'string') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trim and lowercase the name to ensure consistency and uniqueness
|
|
||||||
body.name = body.name.trim().toLocaleLowerCase();
|
|
||||||
|
|
||||||
return body;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isAuthenticated(req: Request) {
|
|
||||||
const auth = req.headers.get('authorization');
|
|
||||||
if (!auth) return false;
|
|
||||||
const [type, bearer] = auth.split(' ');
|
|
||||||
if (type !== 'Bearer') return false;
|
|
||||||
// Check if the token is valid
|
|
||||||
return bearer === env.BEARER_TOKEN;
|
|
||||||
}
|
|
||||||
|
|
||||||
const unauthorizedHeaders = new Headers({
|
|
||||||
'WWW-Authenticate': `Bearer realm='sign', error="invalid_request"`
|
|
||||||
});
|
|
||||||
|
|
||||||
function unauthorizedResp() {
|
|
||||||
return new Response('Unauthorized', { status: 401, headers: unauthorizedHeaders });
|
|
||||||
}
|
|
|
@ -2,33 +2,33 @@ import { Database } from 'bun:sqlite';
|
||||||
import { env } from 'bun';
|
import { env } from 'bun';
|
||||||
import { drizzle } from 'drizzle-orm/bun-sqlite';
|
import { drizzle } from 'drizzle-orm/bun-sqlite';
|
||||||
// import { migrate } from 'drizzle-orm/bun-sqlite/migrator';
|
// import { migrate } from 'drizzle-orm/bun-sqlite/migrator';
|
||||||
import { createWrappedTimer } from '../wrapped-timer';
|
import { createWrappedTimer } from '@server/wrapped-timer';
|
||||||
|
|
||||||
function initDb() {
|
function initDb() {
|
||||||
// global.db.exec('PRAGMA journal_mode = delete');
|
// global.db.exec('PRAGMA journal_mode = delete');
|
||||||
global.db.exec('PRAGMA journal_mode = WAL');
|
global.db.exec('PRAGMA journal_mode = WAL');
|
||||||
global.db.exec('PRAGMA synchronous = NORMAL');
|
global.db.exec('PRAGMA synchronous = NORMAL');
|
||||||
global.db.exec('PRAGMA auto_vacuum = INCREMENTAL');
|
global.db.exec('PRAGMA auto_vacuum = INCREMENTAL');
|
||||||
global.db.exec('PRAGMA wal_autocheckpoint = 1000');
|
global.db.exec('PRAGMA wal_autocheckpoint = 1000');
|
||||||
}
|
}
|
||||||
|
|
||||||
function incrementalVacuumDb() {
|
function incrementalVacuumDb() {
|
||||||
global.db.exec('PRAGMA incremental_vacuum');
|
global.db.exec('PRAGMA incremental_vacuum');
|
||||||
}
|
}
|
||||||
|
|
||||||
function optimizeDb() {
|
function optimizeDb() {
|
||||||
global.db.exec('PRAGMA optimize');
|
global.db.exec('PRAGMA optimize');
|
||||||
}
|
}
|
||||||
|
|
||||||
function vacuumDb() {
|
function vacuumDb() {
|
||||||
global.db.exec('vacuum');
|
global.db.exec('vacuum');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (global.db === undefined || global.drizzleDB === undefined) {
|
if (global.db === undefined || global.drizzleDB === undefined) {
|
||||||
global.db = new Database(`${env.SQLITE_DB_PATH}/${env.SQLITE_DB_NAME}`, { create: true, strict: true });
|
global.db = new Database(`${env.SQLITE_DB_PATH}/${env.SQLITE_DB_NAME}`, { create: true, strict: true });
|
||||||
initDb();
|
initDb();
|
||||||
global.drizzleDB = drizzle(global.db);
|
global.drizzleDB = drizzle(global.db);
|
||||||
// migrate(global.drizzleDB, { migrationsFolder: './drizzle' });
|
// migrate(global.drizzleDB, { migrationsFolder: './drizzle' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const incrementalVacuumInterval = 1000 * 30; // 30 seconds
|
const incrementalVacuumInterval = 1000 * 30; // 30 seconds
|
9
src/server/index.d.ts
vendored
Normal file
9
src/server/index.d.ts
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import type { Database } from 'bun:sqlite';
|
||||||
|
import type { BunSQLiteDatabase } from 'drizzle-orm/bun-sqlite';
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
var db: Database;
|
||||||
|
var drizzleDB: BunSQLiteDatabase;
|
||||||
|
var performanceObserver: PerformanceObserver;
|
||||||
|
var wrappedTimers: Map<string, import('@server/wrapped-timer').WrappedTimer>;
|
||||||
|
}
|
78
src/server/wrapped-timer.ts
Normal file
78
src/server/wrapped-timer.ts
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
if (global.wrappedTimers === undefined) {
|
||||||
|
global.wrappedTimers = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WrappedTimer = { timer: Timer | undefined; running: boolean; };
|
||||||
|
export type WrappedTimerResult = { callback: ReturnType<typeof getCallbackHandler>; wrappedTimer: WrappedTimer; };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a callback handler for a wrapped timer that prevents the callback from running if it is already running
|
||||||
|
* and prevents the callback from running if it is already running.
|
||||||
|
* @template TArgs
|
||||||
|
* @param {string} key unique identifier for the timer
|
||||||
|
* @param {(...args: Array<TArgs>) => (Promise<void> | void)} callback function to run
|
||||||
|
* @param {Array<TArgs>} args arguments to pass to the callback
|
||||||
|
* @returns {() => Promise<void>}
|
||||||
|
*/
|
||||||
|
function getCallbackHandler<TArgs>(key: string, callback: (...args: Array<TArgs>) => (Promise<void> | void), ...args: Array<TArgs>): () => Promise<void> {
|
||||||
|
return async () => {
|
||||||
|
const thisTimer = global.wrappedTimers.get(key);
|
||||||
|
if (thisTimer === undefined) {
|
||||||
|
console.debug(`Wrapped timer ${key} does not exist`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (thisTimer.running) {
|
||||||
|
console.debug(`Wrapped timer ${key} is already running`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
thisTimer.running = true;
|
||||||
|
await callback(...args);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
} finally {
|
||||||
|
thisTimer.running = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a wrapped timer aka interval
|
||||||
|
* @template TArgs
|
||||||
|
* @param {string} key unique identifier for the timer
|
||||||
|
* @param {number} interval in milliseconds
|
||||||
|
* @param {(...args: Array<TArgs>) => (Promise<void> | void)} callback function to run
|
||||||
|
* @param {Array<TArgs>} args arguments to pass to the callback
|
||||||
|
* @returns {WrappedTimerResult}
|
||||||
|
*/
|
||||||
|
export function createWrappedTimer<TArgs>(key: string, callback: (...args: Array<TArgs>) => (Promise<void> | void), interval: number, ...args: Array<TArgs>): WrappedTimerResult {
|
||||||
|
const thisTimer = global.wrappedTimers.get(key);
|
||||||
|
const handler = getCallbackHandler(key, callback, ...args);
|
||||||
|
|
||||||
|
if (thisTimer !== undefined) {
|
||||||
|
console.debug(`Wrapped timer ${key} already exists, clearing timer`);
|
||||||
|
clearInterval(thisTimer.timer);
|
||||||
|
console.debug(`Wrapped timer ${key} set with interval ${interval}ms`);
|
||||||
|
thisTimer.timer = setInterval(handler, interval);
|
||||||
|
|
||||||
|
return {
|
||||||
|
callback: handler,
|
||||||
|
wrappedTimer: thisTimer,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
console.debug(`Wrapped timer ${key} created with interval ${interval}ms`);
|
||||||
|
|
||||||
|
const wrappedTimer: WrappedTimer = {
|
||||||
|
timer: setInterval(handler, interval),
|
||||||
|
running: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
global.wrappedTimers.set(key, wrappedTimer);
|
||||||
|
|
||||||
|
return {
|
||||||
|
callback: handler,
|
||||||
|
wrappedTimer,
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,78 +0,0 @@
|
||||||
if (global.wrappedTimers === undefined) {
|
|
||||||
global.wrappedTimers = new Map();
|
|
||||||
}
|
|
||||||
|
|
||||||
type WrappedTimer = { timer: Timer | undefined; running: boolean; };
|
|
||||||
type WrappedTimerResult = { callback: ReturnType<typeof getCallbackHandler>; wrappedTimer: WrappedTimer; };
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a callback handler for a wrapped timer that prevents the callback from running if it is already running
|
|
||||||
* and prevents the callback from running if it is already running.
|
|
||||||
* @template TArgs
|
|
||||||
* @param {string} key unique identifier for the timer
|
|
||||||
* @param {(...args: Array<TArgs>) => (Promise<void> | void)} callback function to run
|
|
||||||
* @param {Array<TArgs>} args arguments to pass to the callback
|
|
||||||
* @returns {() => Promise<void>}
|
|
||||||
*/
|
|
||||||
function getCallbackHandler<TArgs>(key: string, callback: (...args: Array<TArgs>) => (Promise<void> | void), ...args: Array<TArgs>): () => Promise<void> {
|
|
||||||
return async () => {
|
|
||||||
const thisTimer = global.wrappedTimers.get(key);
|
|
||||||
if (thisTimer === undefined) {
|
|
||||||
console.debug(`Wrapped timer ${key} does not exist`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (thisTimer.running) {
|
|
||||||
console.debug(`Wrapped timer ${key} is already running`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
thisTimer.running = true;
|
|
||||||
await callback(...args);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
} finally {
|
|
||||||
thisTimer.running = false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a wrapped timer aka interval
|
|
||||||
* @template TArgs
|
|
||||||
* @param {string} key unique identifier for the timer
|
|
||||||
* @param {number} interval in milliseconds
|
|
||||||
* @param {(...args: Array<TArgs>) => (Promise<void> | void)} callback function to run
|
|
||||||
* @param {Array<TArgs>} args arguments to pass to the callback
|
|
||||||
* @returns {WrappedTimerResult}
|
|
||||||
*/
|
|
||||||
export function createWrappedTimer<TArgs>(key: string, callback: (...args: Array<TArgs>) => (Promise<void> | void), interval: number, ...args: Array<TArgs>): WrappedTimerResult {
|
|
||||||
const thisTimer = global.wrappedTimers.get(key);
|
|
||||||
const handler = getCallbackHandler(key, callback, ...args);
|
|
||||||
|
|
||||||
if (thisTimer !== undefined) {
|
|
||||||
console.debug(`Wrapped timer ${key} already exists, clearing timer`);
|
|
||||||
clearInterval(thisTimer.timer);
|
|
||||||
console.debug(`Wrapped timer ${key} set with interval ${interval}ms`);
|
|
||||||
thisTimer.timer = setInterval(handler, interval);
|
|
||||||
|
|
||||||
return {
|
|
||||||
callback: handler,
|
|
||||||
wrappedTimer: thisTimer,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
console.debug(`Wrapped timer ${key} created with interval ${interval}ms`);
|
|
||||||
|
|
||||||
const wrappedTimer: WrappedTimer = {
|
|
||||||
timer: setInterval(handler, interval),
|
|
||||||
running: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
global.wrappedTimers.set(key, wrappedTimer);
|
|
||||||
|
|
||||||
return {
|
|
||||||
callback: handler,
|
|
||||||
wrappedTimer,
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,17 +1,15 @@
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
// Enable latest features
|
// Environment setup & latest features
|
||||||
"lib": ["ESNext"],
|
"lib": [
|
||||||
|
"ESNext",
|
||||||
|
"DOM",
|
||||||
|
],
|
||||||
"target": "ESNext",
|
"target": "ESNext",
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleDetection": "force",
|
"moduleDetection": "force",
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"checkJs": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"sourceMap": true,
|
|
||||||
// Bundler mode
|
// Bundler mode
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
|
@ -21,13 +19,27 @@
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
// Some stricter flags (disabled by default)
|
// Some stricter flags (disabled by default)
|
||||||
"noUnusedLocals": false,
|
"noUnusedLocals": false,
|
||||||
"noUnusedParameters": false,
|
"noUnusedParameters": false,
|
||||||
"noPropertyAccessFromIndexSignature": false
|
"noPropertyAccessFromIndexSignature": false,
|
||||||
}
|
"paths": {
|
||||||
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
|
"@client/*": [
|
||||||
//
|
"./src/client/*"
|
||||||
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
|
],
|
||||||
// from the referenced tsconfig.json - TypeScript does not merge them in
|
"@server/*": [
|
||||||
|
"./src/server/*"
|
||||||
|
],
|
||||||
|
"@routes/*": [
|
||||||
|
"./src/routes/*"
|
||||||
|
],
|
||||||
|
"@static/*": [
|
||||||
|
"./src/static/*"
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exclude": [
|
||||||
|
"node_modules"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue