vibe coded v1

This commit is contained in:
khannurien
2026-03-16 07:34:49 +00:00
parent 6207a7549f
commit e88fed4e98
48 changed files with 4303 additions and 595 deletions

View File

@@ -1,7 +1,9 @@
import { DatabaseSync, type SQLOutputValue } from "node:sqlite";
import { Dump, type User } from "./interfaces.ts";
import { Dump, type RichContent, type User } from "./interfaces.ts";
export const db = new DatabaseSync("api/sql/gerbeur.db");
db.exec("PRAGMA journal_mode = WAL;");
db.exec("PRAGMA foreign_keys = ON;");
/**
* Database Row Types
@@ -9,10 +11,17 @@ export const db = new DatabaseSync("api/sql/gerbeur.db");
export interface DumpRow {
id: string;
kind: string;
title: string;
description: string | null;
comment: string | null;
user_id: string;
created_at: string;
url: string | null;
rich_content: string | null;
file_name: string | null;
file_mime: string | null;
file_size: number | null;
vote_count: number;
[key: string]: SQLOutputValue; // Index signature
}
@@ -22,6 +31,7 @@ export interface UserRow {
password_hash: string;
is_admin: number;
created_at: string;
avatar_mime: string | null;
[key: string]: SQLOutputValue; // Index signature
}
@@ -33,11 +43,22 @@ export function isDumpRow(obj: Record<string, SQLOutputValue>): obj is DumpRow {
return !!obj &&
typeof obj === "object" &&
"id" in obj && typeof obj.id === "string" &&
"kind" in obj && typeof obj.kind === "string" &&
"title" in obj && typeof obj.title === "string" &&
"description" in obj &&
(typeof obj.description === "string" || obj.description === null) &&
"comment" in obj &&
(typeof obj.comment === "string" || obj.comment === null) &&
"user_id" in obj && typeof obj.user_id === "string" &&
"created_at" in obj && typeof obj.created_at === "string";
"created_at" in obj && typeof obj.created_at === "string" &&
"url" in obj && (typeof obj.url === "string" || obj.url === null) &&
"rich_content" in obj &&
(typeof obj.rich_content === "string" || obj.rich_content === null) &&
"file_name" in obj &&
(typeof obj.file_name === "string" || obj.file_name === null) &&
"file_mime" in obj &&
(typeof obj.file_mime === "string" || obj.file_mime === null) &&
"file_size" in obj &&
(typeof obj.file_size === "number" || obj.file_size === null) &&
"vote_count" in obj && typeof obj.vote_count === "number";
}
export function isUserRow(obj: Record<string, SQLOutputValue>): obj is UserRow {
@@ -47,32 +68,48 @@ export function isUserRow(obj: Record<string, SQLOutputValue>): obj is UserRow {
"username" in obj && typeof obj.username === "string" &&
"password_hash" in obj && typeof obj.password_hash === "string" &&
"is_admin" in obj && typeof obj.is_admin === "number" &&
"created_at" in obj && typeof obj.created_at === "string";
"created_at" in obj && typeof obj.created_at === "string" &&
"avatar_mime" in obj &&
(typeof obj.avatar_mime === "string" || obj.avatar_mime === null);
}
/**
* Conversion Helpers
*/
export function dumpRowToApi(
row: DumpRow,
): Dump {
export function dumpRowToApi(row: DumpRow): Dump {
return {
id: row.id,
kind: row.kind as "url" | "file",
title: row.title,
description: row.description ?? undefined,
comment: row.comment ?? undefined,
userId: row.user_id,
createdAt: new Date(row.created_at),
url: row.url ?? undefined,
richContent: row.rich_content
? (JSON.parse(row.rich_content) as RichContent)
: undefined,
fileName: row.file_name ?? undefined,
fileMime: row.file_mime ?? undefined,
fileSize: row.file_size ?? undefined,
voteCount: row.vote_count,
};
}
export function dumpApiToRow(dump: Dump): DumpRow {
return {
id: dump.id,
kind: dump.kind,
title: dump.title,
description: dump.description ?? null,
comment: dump.comment ?? null,
user_id: dump.userId,
created_at: dump.createdAt.toISOString(),
url: dump.url ?? null,
rich_content: dump.richContent ? JSON.stringify(dump.richContent) : null,
file_name: dump.fileName ?? null,
file_mime: dump.fileMime ?? null,
file_size: dump.fileSize ?? null,
vote_count: dump.voteCount,
};
}
@@ -83,6 +120,7 @@ export function userRowToApi(row: UserRow): User {
passwordHash: row.password_hash,
isAdmin: Boolean(row.is_admin),
createdAt: new Date(row.created_at),
avatarMime: row.avatar_mime ?? undefined,
};
}
@@ -93,5 +131,6 @@ export function userApiToRow(user: User): UserRow {
password_hash: user.passwordHash,
is_admin: user.isAdmin ? 1 : 0,
created_at: user.createdAt.toISOString(),
avatar_mime: user.avatarMime ?? null,
};
}

View File

@@ -2,12 +2,29 @@
* Backend
*/
export interface RichContent {
type: string;
url: string;
siteName?: string;
title?: string;
description?: string;
thumbnailUrl?: string;
videoId?: string;
}
export interface Dump {
id: string;
kind: "url" | "file";
title: string;
description?: string;
comment?: string;
userId: string;
createdAt: Date;
url?: string;
richContent?: RichContent;
fileName?: string;
fileMime?: string;
fileSize?: number;
voteCount: number;
}
/**
@@ -20,6 +37,7 @@ export interface User {
passwordHash: string;
isAdmin: boolean;
createdAt: Date;
avatarMime?: string;
}
export interface LoginUserRequest {
@@ -127,28 +145,94 @@ export class APIException extends Error {
* Request DTOs
*/
export interface CreateDumpRequest {
title: string;
description?: string;
export interface CreateUrlDumpRequest {
url: string;
comment?: string;
}
export function isCreateDumpRequest(obj: unknown): obj is CreateDumpRequest {
export function isCreateUrlDumpRequest(
obj: unknown,
): obj is CreateUrlDumpRequest {
return !!obj &&
typeof obj === "object" &&
"title" in obj && typeof obj.title === "string" &&
(!("description" in obj) ||
(typeof obj.description === "string" || obj.description === null));
"url" in obj && typeof obj.url === "string" &&
(!("comment" in obj) ||
typeof obj.comment === "string" || obj.comment === null);
}
export interface UpdateDumpRequest {
title?: string;
description?: string;
url?: string;
comment?: string;
}
export function isUpdateDumpRequest(obj: unknown): obj is UpdateDumpRequest {
return !!obj &&
typeof obj === "object" &&
(!("title" in obj) || typeof obj.title === "string") &&
(!("description" in obj) ||
(typeof obj.description === "string" || obj.description === null));
(!("url" in obj) || typeof obj.url === "string" || obj.url === null) &&
(!("comment" in obj) ||
typeof obj.comment === "string" || obj.comment === null);
}
/**
* WebSockets
*/
export interface VoteCastMessage {
type: "vote_cast";
dumpId: string;
userId: string;
}
export interface VoteAckMessageFailure {
type: "vote_ack";
dumpId: string;
success: false;
error: APIError;
}
export interface VoteAckMessageSuccess {
type: "vote_ack";
dumpId: string;
action: "cast" | "remove";
success: true;
voteCount: number;
error?: never;
}
export type VoteAckMessage = VoteAckMessageSuccess | VoteAckMessageFailure;
export interface VoteRemoveMessage {
type: "vote_remove";
dumpId: string;
}
export interface VotesUpdateMessage {
type: "votes_update";
dumpId: string;
voteCount: number;
}
export interface OnlineUser {
userId: string;
username: string;
hasAvatar: boolean;
}
export interface WelcomeMessage {
type: "welcome";
users: OnlineUser[];
myVotes: string[];
}
export interface PresenceUpdateMessage {
type: "presence_update";
users: OnlineUser[];
}
export interface PingMessage {
type: "ping";
}
export interface PongMessage {
type: "pong";
}