1 Commits

Author SHA1 Message Date
Moritz Utcke
c0c22453f1 Datenbank 2025-04-29 12:20:21 -03:00
22 changed files with 394 additions and 1 deletions

View File

@@ -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")
);

View File

@@ -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;

View File

@@ -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;

View File

@@ -0,0 +1,9 @@
model Attachment {
id String @id @unique @default(uuid())
name String?
kategorie String?
mime String
attached_to_messages Message[]
}

View File

@@ -49,7 +49,10 @@ model Benutzer {
BearbeiteteTickets Tickets[] @relation("BearbeiteteTickets")
events Event[]
@@map("benutzer")
conversations Participant[]
messages Message[]
@@map("benutzer")
}

View File

@@ -0,0 +1,9 @@
model Conversation {
id String @unique @default(cuid())
name String?
participants Participant[]
messages Message[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

7
prisma/schema/Log.prisma Normal file
View File

@@ -0,0 +1,7 @@
model Log {
id Int @id @default(autoincrement())
level String
message String
meta Json
timestamp DateTime @default(now())
}

View File

@@ -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])
}

View File

@@ -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])
}

View File

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

View File

@@ -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(),
})

View File

@@ -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(),
})

View File

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

15
src/generated/zod/log.ts Normal file
View File

@@ -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<Json> = 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(),
})

View File

@@ -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(),
})

View File

@@ -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(),
})

View File

@@ -0,0 +1,18 @@
<script lang="ts">
const socket = new WebSocket("http://localhost:8080");
socket.on("open", () => {
console.log("Connected to WebSocket");
socket.send("Hello")
})
socket.on("message", (data) => {
console.log(data);
})
</script>
<div>
</div>

8
src/pages/chat.astro Normal file
View File

@@ -0,0 +1,8 @@
---
import Layout from "#layouts/Layout.astro";
import ChatModule from "#modules/ChatModule.svelte";
---
<Layout title="Websockets">
<ChatModule client:only></ChatModule>
</Layout>

36
src/server/logger.ts Normal file
View File

@@ -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,
})]
});

30
src/ws/server.ts Normal file
View File

@@ -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 };

60
src/ws/types.ts Normal file
View File

@@ -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;
};
};