Datenbank
This commit is contained in:
10
prisma/migrations/20250429143447_log/migration.sql
Normal file
10
prisma/migrations/20250429143447_log/migration.sql
Normal 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")
|
||||||
|
);
|
||||||
64
prisma/migrations/20250429145532_messages/migration.sql
Normal file
64
prisma/migrations/20250429145532_messages/migration.sql
Normal 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;
|
||||||
56
prisma/migrations/20250429151551_conversations/migration.sql
Normal file
56
prisma/migrations/20250429151551_conversations/migration.sql
Normal 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;
|
||||||
9
prisma/schema/Attachment.prisma
Normal file
9
prisma/schema/Attachment.prisma
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
model Attachment {
|
||||||
|
id String @id @unique @default(uuid())
|
||||||
|
|
||||||
|
name String?
|
||||||
|
kategorie String?
|
||||||
|
mime String
|
||||||
|
|
||||||
|
attached_to_messages Message[]
|
||||||
|
}
|
||||||
@@ -49,7 +49,10 @@ model Benutzer {
|
|||||||
BearbeiteteTickets Tickets[] @relation("BearbeiteteTickets")
|
BearbeiteteTickets Tickets[] @relation("BearbeiteteTickets")
|
||||||
events Event[]
|
events Event[]
|
||||||
|
|
||||||
@@map("benutzer")
|
conversations Participant[]
|
||||||
|
messages Message[]
|
||||||
|
|
||||||
|
@@map("benutzer")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
9
prisma/schema/Conversation.prisma
Normal file
9
prisma/schema/Conversation.prisma
Normal 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
7
prisma/schema/Log.prisma
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
model Log {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
level String
|
||||||
|
message String
|
||||||
|
meta Json
|
||||||
|
timestamp DateTime @default(now())
|
||||||
|
}
|
||||||
17
prisma/schema/Message.prisma
Normal file
17
prisma/schema/Message.prisma
Normal 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])
|
||||||
|
}
|
||||||
11
prisma/schema/Participant.prisma
Normal file
11
prisma/schema/Participant.prisma
Normal 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])
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import express from 'express';
|
|||||||
import { H, Handlers } from '@highlight-run/node'
|
import { H, Handlers } from '@highlight-run/node'
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { handler as ssrHandler } from './dist/server/entry.mjs';
|
import { handler as ssrHandler } from './dist/server/entry.mjs';
|
||||||
|
import { server } from "./src/ws/server";
|
||||||
|
|
||||||
const highlightConfig = {
|
const highlightConfig = {
|
||||||
projectID: '1jdkoe52',
|
projectID: '1jdkoe52',
|
||||||
|
|||||||
8
src/generated/zod/attachment.ts
Normal file
8
src/generated/zod/attachment.ts
Normal 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(),
|
||||||
|
})
|
||||||
8
src/generated/zod/conversation.ts
Normal file
8
src/generated/zod/conversation.ts
Normal 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(),
|
||||||
|
})
|
||||||
@@ -1,16 +1,21 @@
|
|||||||
export * from "./anteilshaber"
|
export * from "./anteilshaber"
|
||||||
export * from "./apirequests"
|
export * from "./apirequests"
|
||||||
|
export * from "./attachment"
|
||||||
export * from "./aufnahme"
|
export * from "./aufnahme"
|
||||||
export * from "./bedarfsausweisgewerbe"
|
export * from "./bedarfsausweisgewerbe"
|
||||||
export * from "./bedarfsausweiswohnen"
|
export * from "./bedarfsausweiswohnen"
|
||||||
export * from "./benutzer"
|
export * from "./benutzer"
|
||||||
export * from "./bild"
|
export * from "./bild"
|
||||||
|
export * from "./conversation"
|
||||||
export * from "./event"
|
export * from "./event"
|
||||||
export * from "./gegeinpreisung"
|
export * from "./gegeinpreisung"
|
||||||
export * from "./gegnachweisgewerbe"
|
export * from "./gegnachweisgewerbe"
|
||||||
export * from "./gegnachweiswohnen"
|
export * from "./gegnachweiswohnen"
|
||||||
export * from "./klimafaktoren"
|
export * from "./klimafaktoren"
|
||||||
|
export * from "./log"
|
||||||
|
export * from "./message"
|
||||||
export * from "./objekt"
|
export * from "./objekt"
|
||||||
|
export * from "./participant"
|
||||||
export * from "./postleitzahlen"
|
export * from "./postleitzahlen"
|
||||||
export * from "./rechnung"
|
export * from "./rechnung"
|
||||||
export * from "./refreshtokens"
|
export * from "./refreshtokens"
|
||||||
|
|||||||
15
src/generated/zod/log.ts
Normal file
15
src/generated/zod/log.ts
Normal 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(),
|
||||||
|
})
|
||||||
10
src/generated/zod/message.ts
Normal file
10
src/generated/zod/message.ts
Normal 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(),
|
||||||
|
})
|
||||||
8
src/generated/zod/participant.ts
Normal file
8
src/generated/zod/participant.ts
Normal 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(),
|
||||||
|
})
|
||||||
18
src/modules/ChatModule.svelte
Normal file
18
src/modules/ChatModule.svelte
Normal 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
8
src/pages/chat.astro
Normal 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
36
src/server/logger.ts
Normal 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
30
src/ws/server.ts
Normal 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
60
src/ws/types.ts
Normal 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;
|
||||||
|
};
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user