From c0c22453f1d78582273f4bcd7996857ad7a3a48e Mon Sep 17 00:00:00 2001 From: Moritz Utcke Date: Tue, 29 Apr 2025 12:20:21 -0300 Subject: [PATCH] Datenbank --- .../20250429143447_log/migration.sql | 10 +++ .../20250429145532_messages/migration.sql | 64 +++++++++++++++++++ .../migration.sql | 56 ++++++++++++++++ prisma/schema/Attachment.prisma | 9 +++ prisma/schema/Benutzer.prisma | 5 +- prisma/schema/Conversation.prisma | 9 +++ prisma/schema/Log.prisma | 7 ++ prisma/schema/Message.prisma | 17 +++++ prisma/schema/Participant.prisma | 11 ++++ server.ts | 1 + src/generated/zod/attachment.ts | 8 +++ src/generated/zod/conversation.ts | 8 +++ src/generated/zod/index.ts | 5 ++ src/generated/zod/log.ts | 15 +++++ src/generated/zod/message.ts | 10 +++ src/generated/zod/participant.ts | 8 +++ src/modules/ChatModule.svelte | 18 ++++++ src/pages/chat.astro | 8 +++ src/server/logger.ts | 36 +++++++++++ .../cards/cardReview.svelte => ws/client.ts} | 0 src/ws/server.ts | 30 +++++++++ src/ws/types.ts | 60 +++++++++++++++++ 22 files changed, 394 insertions(+), 1 deletion(-) create mode 100644 prisma/migrations/20250429143447_log/migration.sql create mode 100644 prisma/migrations/20250429145532_messages/migration.sql create mode 100644 prisma/migrations/20250429151551_conversations/migration.sql create mode 100644 prisma/schema/Attachment.prisma create mode 100644 prisma/schema/Conversation.prisma create mode 100644 prisma/schema/Log.prisma create mode 100644 prisma/schema/Message.prisma create mode 100644 prisma/schema/Participant.prisma create mode 100644 src/generated/zod/attachment.ts create mode 100644 src/generated/zod/conversation.ts create mode 100644 src/generated/zod/log.ts create mode 100644 src/generated/zod/message.ts create mode 100644 src/generated/zod/participant.ts create mode 100644 src/modules/ChatModule.svelte create mode 100644 src/pages/chat.astro create mode 100644 src/server/logger.ts rename src/{components/design/sidebars/cards/cardReview.svelte => ws/client.ts} (100%) create mode 100644 src/ws/server.ts create mode 100644 src/ws/types.ts diff --git a/prisma/migrations/20250429143447_log/migration.sql b/prisma/migrations/20250429143447_log/migration.sql new file mode 100644 index 00000000..bdf619e1 --- /dev/null +++ b/prisma/migrations/20250429143447_log/migration.sql @@ -0,0 +1,10 @@ +-- CreateTable +CREATE TABLE "Log" ( + "id" SERIAL NOT NULL, + "level" TEXT NOT NULL, + "message" TEXT NOT NULL, + "meta" JSONB NOT NULL, + "timestamp" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Log_pkey" PRIMARY KEY ("id") +); diff --git a/prisma/migrations/20250429145532_messages/migration.sql b/prisma/migrations/20250429145532_messages/migration.sql new file mode 100644 index 00000000..675976c0 --- /dev/null +++ b/prisma/migrations/20250429145532_messages/migration.sql @@ -0,0 +1,64 @@ +-- CreateTable +CREATE TABLE "Attachment" ( + "id" TEXT NOT NULL, + "name" TEXT, + "kategorie" TEXT, + "mime" TEXT NOT NULL, + + CONSTRAINT "Attachment_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Message" ( + "id" TEXT NOT NULL, + "content" TEXT NOT NULL, + "sender_id" TEXT NOT NULL, + "reply_to_id" TEXT, + "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Message_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "_AttachmentToMessage" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL, + + CONSTRAINT "_AttachmentToMessage_AB_pkey" PRIMARY KEY ("A","B") +); + +-- CreateTable +CREATE TABLE "_recipients" ( + "A" VARCHAR(11) NOT NULL, + "B" TEXT NOT NULL, + + CONSTRAINT "_recipients_AB_pkey" PRIMARY KEY ("A","B") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Attachment_id_key" ON "Attachment"("id"); + +-- CreateIndex +CREATE INDEX "_AttachmentToMessage_B_index" ON "_AttachmentToMessage"("B"); + +-- CreateIndex +CREATE INDEX "_recipients_B_index" ON "_recipients"("B"); + +-- AddForeignKey +ALTER TABLE "Message" ADD CONSTRAINT "Message_sender_id_fkey" FOREIGN KEY ("sender_id") REFERENCES "benutzer"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Message" ADD CONSTRAINT "Message_reply_to_id_fkey" FOREIGN KEY ("reply_to_id") REFERENCES "Message"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_AttachmentToMessage" ADD CONSTRAINT "_AttachmentToMessage_A_fkey" FOREIGN KEY ("A") REFERENCES "Attachment"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_AttachmentToMessage" ADD CONSTRAINT "_AttachmentToMessage_B_fkey" FOREIGN KEY ("B") REFERENCES "Message"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_recipients" ADD CONSTRAINT "_recipients_A_fkey" FOREIGN KEY ("A") REFERENCES "benutzer"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_recipients" ADD CONSTRAINT "_recipients_B_fkey" FOREIGN KEY ("B") REFERENCES "Message"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20250429151551_conversations/migration.sql b/prisma/migrations/20250429151551_conversations/migration.sql new file mode 100644 index 00000000..717721e5 --- /dev/null +++ b/prisma/migrations/20250429151551_conversations/migration.sql @@ -0,0 +1,56 @@ +/* + Warnings: + + - You are about to drop the column `created_at` on the `Message` table. All the data in the column will be lost. + - You are about to drop the column `updated_at` on the `Message` table. All the data in the column will be lost. + - You are about to drop the `_recipients` table. If the table is not empty, all the data it contains will be lost. + - Added the required column `conversation_id` to the `Message` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropForeignKey +ALTER TABLE "_recipients" DROP CONSTRAINT "_recipients_A_fkey"; + +-- DropForeignKey +ALTER TABLE "_recipients" DROP CONSTRAINT "_recipients_B_fkey"; + +-- AlterTable +ALTER TABLE "Message" DROP COLUMN "created_at", +DROP COLUMN "updated_at", +ADD COLUMN "conversation_id" TEXT NOT NULL, +ADD COLUMN "sentAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; + +-- DropTable +DROP TABLE "_recipients"; + +-- CreateTable +CREATE TABLE "Conversation" ( + "id" TEXT NOT NULL, + "name" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL +); + +-- CreateTable +CREATE TABLE "Participant" ( + "id" TEXT NOT NULL, + "benutzer_id" TEXT NOT NULL, + "conversation_id" TEXT NOT NULL, + "joined_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Participant_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Conversation_id_key" ON "Conversation"("id"); + +-- CreateIndex +CREATE UNIQUE INDEX "Participant_benutzer_id_conversation_id_key" ON "Participant"("benutzer_id", "conversation_id"); + +-- AddForeignKey +ALTER TABLE "Message" ADD CONSTRAINT "Message_conversation_id_fkey" FOREIGN KEY ("conversation_id") REFERENCES "Conversation"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Participant" ADD CONSTRAINT "Participant_benutzer_id_fkey" FOREIGN KEY ("benutzer_id") REFERENCES "benutzer"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Participant" ADD CONSTRAINT "Participant_conversation_id_fkey" FOREIGN KEY ("conversation_id") REFERENCES "Conversation"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/schema/Attachment.prisma b/prisma/schema/Attachment.prisma new file mode 100644 index 00000000..42c489e4 --- /dev/null +++ b/prisma/schema/Attachment.prisma @@ -0,0 +1,9 @@ +model Attachment { + id String @id @unique @default(uuid()) + + name String? + kategorie String? + mime String + + attached_to_messages Message[] +} \ No newline at end of file diff --git a/prisma/schema/Benutzer.prisma b/prisma/schema/Benutzer.prisma index 7a646d66..eb581e0a 100644 --- a/prisma/schema/Benutzer.prisma +++ b/prisma/schema/Benutzer.prisma @@ -49,7 +49,10 @@ model Benutzer { BearbeiteteTickets Tickets[] @relation("BearbeiteteTickets") events Event[] - @@map("benutzer") + conversations Participant[] + messages Message[] + + @@map("benutzer") } diff --git a/prisma/schema/Conversation.prisma b/prisma/schema/Conversation.prisma new file mode 100644 index 00000000..e42311ef --- /dev/null +++ b/prisma/schema/Conversation.prisma @@ -0,0 +1,9 @@ +model Conversation { + id String @unique @default(cuid()) + + name String? + participants Participant[] + messages Message[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} diff --git a/prisma/schema/Log.prisma b/prisma/schema/Log.prisma new file mode 100644 index 00000000..56af0bfc --- /dev/null +++ b/prisma/schema/Log.prisma @@ -0,0 +1,7 @@ +model Log { + id Int @id @default(autoincrement()) + level String + message String + meta Json + timestamp DateTime @default(now()) +} \ No newline at end of file diff --git a/prisma/schema/Message.prisma b/prisma/schema/Message.prisma new file mode 100644 index 00000000..3222df95 --- /dev/null +++ b/prisma/schema/Message.prisma @@ -0,0 +1,17 @@ +model Message { + id String @id @default(cuid()) + + attachments Attachment[] + + reply_to_id String? // Nullable because not all messages are replies + reply_to Message? @relation("MessageReplies", fields: [reply_to_id], references: [id]) + replies Message[] @relation("MessageReplies") + + content String + sender_id String + conversation_id String + sentAt DateTime @default(now()) + + sender Benutzer @relation(fields: [sender_id], references: [id]) + conversation Conversation @relation(fields: [conversation_id], references: [id]) +} diff --git a/prisma/schema/Participant.prisma b/prisma/schema/Participant.prisma new file mode 100644 index 00000000..5de6dd4c --- /dev/null +++ b/prisma/schema/Participant.prisma @@ -0,0 +1,11 @@ +model Participant { + id String @id @default(cuid()) + benutzer_id String + conversation_id String + joined_at DateTime @default(now()) + + benutzer Benutzer @relation(fields: [benutzer_id], references: [id]) + conversation Conversation @relation(fields: [conversation_id], references: [id]) + + @@unique([benutzer_id, conversation_id]) +} diff --git a/server.ts b/server.ts index 1fba8129..e90c7160 100644 --- a/server.ts +++ b/server.ts @@ -2,6 +2,7 @@ import express from 'express'; import { H, Handlers } from '@highlight-run/node' // @ts-ignore import { handler as ssrHandler } from './dist/server/entry.mjs'; +import { server } from "./src/ws/server"; const highlightConfig = { projectID: '1jdkoe52', diff --git a/src/generated/zod/attachment.ts b/src/generated/zod/attachment.ts new file mode 100644 index 00000000..ed822324 --- /dev/null +++ b/src/generated/zod/attachment.ts @@ -0,0 +1,8 @@ +import * as z from "zod" + +export const AttachmentSchema = z.object({ + id: z.string(), + name: z.string().nullish(), + kategorie: z.string().nullish(), + mime: z.string(), +}) diff --git a/src/generated/zod/conversation.ts b/src/generated/zod/conversation.ts new file mode 100644 index 00000000..4adfc40d --- /dev/null +++ b/src/generated/zod/conversation.ts @@ -0,0 +1,8 @@ +import * as z from "zod" + +export const ConversationSchema = z.object({ + id: z.string(), + name: z.string().nullish(), + createdAt: z.date(), + updatedAt: z.date(), +}) diff --git a/src/generated/zod/index.ts b/src/generated/zod/index.ts index 37875cdf..124745f9 100644 --- a/src/generated/zod/index.ts +++ b/src/generated/zod/index.ts @@ -1,16 +1,21 @@ export * from "./anteilshaber" export * from "./apirequests" +export * from "./attachment" export * from "./aufnahme" export * from "./bedarfsausweisgewerbe" export * from "./bedarfsausweiswohnen" export * from "./benutzer" export * from "./bild" +export * from "./conversation" export * from "./event" export * from "./gegeinpreisung" export * from "./gegnachweisgewerbe" export * from "./gegnachweiswohnen" export * from "./klimafaktoren" +export * from "./log" +export * from "./message" export * from "./objekt" +export * from "./participant" export * from "./postleitzahlen" export * from "./rechnung" export * from "./refreshtokens" diff --git a/src/generated/zod/log.ts b/src/generated/zod/log.ts new file mode 100644 index 00000000..7d8552a2 --- /dev/null +++ b/src/generated/zod/log.ts @@ -0,0 +1,15 @@ +import * as z from "zod" + +// Helper schema for JSON fields +type Literal = boolean | number | string +type Json = Literal | { [key: string]: Json } | Json[] +const literalSchema = z.union([z.string(), z.number(), z.boolean()]) +const jsonSchema: z.ZodSchema = z.lazy(() => z.union([literalSchema, z.array(jsonSchema), z.record(jsonSchema)])) + +export const LogSchema = z.object({ + id: z.number().int(), + level: z.string(), + message: z.string(), + meta: jsonSchema, + timestamp: z.date(), +}) diff --git a/src/generated/zod/message.ts b/src/generated/zod/message.ts new file mode 100644 index 00000000..17f59cd6 --- /dev/null +++ b/src/generated/zod/message.ts @@ -0,0 +1,10 @@ +import * as z from "zod" + +export const MessageSchema = z.object({ + id: z.string(), + reply_to_id: z.string().nullish(), + content: z.string(), + sender_id: z.string(), + conversation_id: z.string(), + sentAt: z.date(), +}) diff --git a/src/generated/zod/participant.ts b/src/generated/zod/participant.ts new file mode 100644 index 00000000..00c61986 --- /dev/null +++ b/src/generated/zod/participant.ts @@ -0,0 +1,8 @@ +import * as z from "zod" + +export const ParticipantSchema = z.object({ + id: z.string(), + benutzer_id: z.string(), + conversation_id: z.string(), + joined_at: z.date(), +}) diff --git a/src/modules/ChatModule.svelte b/src/modules/ChatModule.svelte new file mode 100644 index 00000000..aa520315 --- /dev/null +++ b/src/modules/ChatModule.svelte @@ -0,0 +1,18 @@ + + +
+ +
\ No newline at end of file diff --git a/src/pages/chat.astro b/src/pages/chat.astro new file mode 100644 index 00000000..441f3354 --- /dev/null +++ b/src/pages/chat.astro @@ -0,0 +1,8 @@ +--- +import Layout from "#layouts/Layout.astro"; +import ChatModule from "#modules/ChatModule.svelte"; +--- + + + + \ No newline at end of file diff --git a/src/server/logger.ts b/src/server/logger.ts new file mode 100644 index 00000000..7aefec67 --- /dev/null +++ b/src/server/logger.ts @@ -0,0 +1,36 @@ +import { prisma } from "#lib/server/prisma.js"; +import winston from "winston"; +import Transport from "winston-transport"; + +class DatabaseTransport extends Transport { + constructor() { + super(); + } + + async log(info: any, callback: () => void) { + setImmediate(() => { + this.emit("logged", info); + }); + + const { level, message, ...meta } = info; + + await prisma.log.create({ + data: { + level, + message, + meta: JSON.stringify(meta), + }, + }); + + callback(); + } +} + +export const logger = winston.createLogger({ + level: "info", + format: winston.format.json(), + transports: [new DatabaseTransport(), new winston.transports.Console({ + handleExceptions: true, + handleRejections: true, + })] +}); diff --git a/src/components/design/sidebars/cards/cardReview.svelte b/src/ws/client.ts similarity index 100% rename from src/components/design/sidebars/cards/cardReview.svelte rename to src/ws/client.ts diff --git a/src/ws/server.ts b/src/ws/server.ts new file mode 100644 index 00000000..8ee02b9e --- /dev/null +++ b/src/ws/server.ts @@ -0,0 +1,30 @@ +import { logger } from '#server/logger.js'; +import { WebSocketServer } from 'ws'; + +const server = new WebSocketServer({ port: 8080 }); + + + +server.on('connection', (ws, req) => { + logger.info("Client connected to websocket", { + ip: req.socket.remoteAddress + }) + + ws.on('message', (message) => { + logger.info("Received websocket message", { + message + }); + + ws.send(`Server received: ${message}`); + }); + + ws.on('close', () => { + logger.info('Client disconnected', { + ip: req.socket.remoteAddress + }); + }); +}); + +logger.info(`Websocket server listening on port ${8080}`) + +export { server }; \ No newline at end of file diff --git a/src/ws/types.ts b/src/ws/types.ts new file mode 100644 index 00000000..77d10d00 --- /dev/null +++ b/src/ws/types.ts @@ -0,0 +1,60 @@ +import { Conversation, Message } from "#lib/client/prisma.js"; + +export type WebsocketClientToServerMessage = + | { + type: "messages/get"; + payload: { + conversation_id: string; + filters?: { + after?: Date; + before?: Date; + }; + }; + } + | { + type: "message/send"; + payload: { + conversation_id: string; + content: string; + reply_to_id?: string; + }; + } + | { + type: "conversations/get"; + payload: { + includeMessages?: boolean; + }; + } + | { + type: "conversation/create"; + payload: { + participant_ids: string[]; // includes sender + name?: string; // optional for group + }; + }; + +/* ---------------------------- Server to client ---------------------------- */ + +export type WebsocketServerToClientMessage = + | { + type: "messages/list"; + payload: { + conversation_id: string; + messages: Message[]; + }; + } + | { + type: "message/new"; + payload: Message; + } + | { + type: "conversations/list"; + payload: Conversation[]; + } + | { + type: "error"; + payload: { + code: string; + message: string; + }; + };