Refacor electron app and add migrations

This commit is contained in:
2026-03-04 01:38:43 +01:00
parent 4e95ae77c5
commit be91b6dfe8
70 changed files with 1824 additions and 923 deletions

View File

@@ -0,0 +1,20 @@
import { DataSource } from 'typeorm';
import {
MessageEntity,
UserEntity,
RoomEntity,
ReactionEntity,
BanEntity,
AttachmentEntity,
MetaEntity
} from '../../../entities';
export async function handleClearAllData(dataSource: DataSource): Promise<void> {
await dataSource.getRepository(MessageEntity).clear();
await dataSource.getRepository(UserEntity).clear();
await dataSource.getRepository(RoomEntity).clear();
await dataSource.getRepository(ReactionEntity).clear();
await dataSource.getRepository(BanEntity).clear();
await dataSource.getRepository(AttachmentEntity).clear();
await dataSource.getRepository(MetaEntity).clear();
}

View File

@@ -0,0 +1,9 @@
import { DataSource } from 'typeorm';
import { MessageEntity } from '../../../entities';
import { ClearRoomMessagesCommand } from '../../types';
export async function handleClearRoomMessages(command: ClearRoomMessagesCommand, dataSource: DataSource): Promise<void> {
const repo = dataSource.getRepository(MessageEntity);
await repo.delete({ roomId: command.payload.roomId });
}

View File

@@ -0,0 +1,9 @@
import { DataSource } from 'typeorm';
import { AttachmentEntity } from '../../../entities';
import { DeleteAttachmentsForMessageCommand } from '../../types';
export async function handleDeleteAttachmentsForMessage(command: DeleteAttachmentsForMessageCommand, dataSource: DataSource): Promise<void> {
const repo = dataSource.getRepository(AttachmentEntity);
await repo.delete({ messageId: command.payload.messageId });
}

View File

@@ -0,0 +1,9 @@
import { DataSource } from 'typeorm';
import { MessageEntity } from '../../../entities';
import { DeleteMessageCommand } from '../../types';
export async function handleDeleteMessage(command: DeleteMessageCommand, dataSource: DataSource): Promise<void> {
const repo = dataSource.getRepository(MessageEntity);
await repo.delete({ id: command.payload.messageId });
}

View File

@@ -0,0 +1,9 @@
import { DataSource } from 'typeorm';
import { RoomEntity, MessageEntity } from '../../../entities';
import { DeleteRoomCommand } from '../../types';
export async function handleDeleteRoom(command: DeleteRoomCommand, dataSource: DataSource): Promise<void> {
const { roomId } = command.payload;
await dataSource.getRepository(RoomEntity).delete({ id: roomId });
await dataSource.getRepository(MessageEntity).delete({ roomId });
}

View File

@@ -0,0 +1,9 @@
import { DataSource } from 'typeorm';
import { BanEntity } from '../../../entities';
import { RemoveBanCommand } from '../../types';
export async function handleRemoveBan(command: RemoveBanCommand, dataSource: DataSource): Promise<void> {
const repo = dataSource.getRepository(BanEntity);
await repo.delete({ oderId: command.payload.oderId });
}

View File

@@ -0,0 +1,10 @@
import { DataSource } from 'typeorm';
import { ReactionEntity } from '../../../entities';
import { RemoveReactionCommand } from '../../types';
export async function handleRemoveReaction(command: RemoveReactionCommand, dataSource: DataSource): Promise<void> {
const repo = dataSource.getRepository(ReactionEntity);
const { messageId, userId, emoji } = command.payload;
await repo.delete({ messageId, userId, emoji });
}

View File

@@ -0,0 +1,21 @@
import { DataSource } from 'typeorm';
import { AttachmentEntity } from '../../../entities';
import { SaveAttachmentCommand } from '../../types';
export async function handleSaveAttachment(command: SaveAttachmentCommand, dataSource: DataSource): Promise<void> {
const repo = dataSource.getRepository(AttachmentEntity);
const { attachment } = command.payload;
const entity = repo.create({
id: attachment.id,
messageId: attachment.messageId,
filename: attachment.filename,
size: attachment.size,
mime: attachment.mime,
isImage: attachment.isImage ? 1 : 0,
uploaderPeerId: attachment.uploaderPeerId ?? null,
filePath: attachment.filePath ?? null,
savedPath: attachment.savedPath ?? null
});
await repo.save(entity);
}

View File

@@ -0,0 +1,20 @@
import { DataSource } from 'typeorm';
import { BanEntity } from '../../../entities';
import { SaveBanCommand } from '../../types';
export async function handleSaveBan(command: SaveBanCommand, dataSource: DataSource): Promise<void> {
const repo = dataSource.getRepository(BanEntity);
const { ban } = command.payload;
const entity = repo.create({
oderId: ban.oderId,
roomId: ban.roomId,
userId: ban.userId ?? null,
bannedBy: ban.bannedBy,
displayName: ban.displayName ?? null,
reason: ban.reason ?? null,
expiresAt: ban.expiresAt ?? null,
timestamp: ban.timestamp
});
await repo.save(entity);
}

View File

@@ -0,0 +1,24 @@
import { DataSource } from 'typeorm';
import { MessageEntity } from '../../../entities';
import { SaveMessageCommand } from '../../types';
export async function handleSaveMessage(command: SaveMessageCommand, dataSource: DataSource): Promise<void> {
const repo = dataSource.getRepository(MessageEntity);
const { message } = command.payload;
const entity = repo.create({
id: message.id,
roomId: message.roomId,
channelId: message.channelId ?? null,
senderId: message.senderId,
senderName: message.senderName,
content: message.content,
timestamp: message.timestamp,
editedAt: message.editedAt ?? null,
reactions: JSON.stringify(message.reactions ?? []),
isDeleted: message.isDeleted ? 1 : 0,
replyToId: message.replyToId ?? null,
});
await repo.save(entity);
}

View File

@@ -0,0 +1,26 @@
import { DataSource } from 'typeorm';
import { ReactionEntity } from '../../../entities';
import { SaveReactionCommand } from '../../types';
export async function handleSaveReaction(command: SaveReactionCommand, dataSource: DataSource): Promise<void> {
const repo = dataSource.getRepository(ReactionEntity);
const { reaction } = command.payload;
// Deduplicate: skip if same messageId + userId + emoji already exists
const existing = await repo.findOne({
where: { messageId: reaction.messageId, userId: reaction.userId, emoji: reaction.emoji }
});
if (existing)
return;
const entity = repo.create({
id: reaction.id,
messageId: reaction.messageId,
oderId: reaction.oderId ?? null,
userId: reaction.userId ?? null,
emoji: reaction.emoji,
timestamp: reaction.timestamp
});
await repo.save(entity);
}

View File

@@ -0,0 +1,26 @@
import { DataSource } from 'typeorm';
import { RoomEntity } from '../../../entities';
import { SaveRoomCommand } from '../../types';
export async function handleSaveRoom(command: SaveRoomCommand, dataSource: DataSource): Promise<void> {
const repo = dataSource.getRepository(RoomEntity);
const { room } = command.payload;
const entity = repo.create({
id: room.id,
name: room.name,
description: room.description ?? null,
topic: room.topic ?? null,
hostId: room.hostId,
password: room.password ?? null,
isPrivate: room.isPrivate ? 1 : 0,
createdAt: room.createdAt,
userCount: room.userCount ?? 0,
maxUsers: room.maxUsers ?? null,
icon: room.icon ?? null,
iconUpdatedAt: room.iconUpdatedAt ?? null,
permissions: room.permissions != null ? JSON.stringify(room.permissions) : null,
channels: room.channels != null ? JSON.stringify(room.channels) : null
});
await repo.save(entity);
}

View File

@@ -0,0 +1,26 @@
import { DataSource } from 'typeorm';
import { UserEntity } from '../../../entities';
import { SaveUserCommand } from '../../types';
export async function handleSaveUser(command: SaveUserCommand, dataSource: DataSource): Promise<void> {
const repo = dataSource.getRepository(UserEntity);
const { user } = command.payload;
const entity = repo.create({
id: user.id,
oderId: user.oderId ?? null,
username: user.username ?? null,
displayName: user.displayName ?? null,
avatarUrl: user.avatarUrl ?? null,
status: user.status ?? null,
role: user.role ?? null,
joinedAt: user.joinedAt ?? null,
peerId: user.peerId ?? null,
isOnline: user.isOnline ? 1 : 0,
isAdmin: user.isAdmin ? 1 : 0,
isRoomOwner: user.isRoomOwner ? 1 : 0,
voiceState: user.voiceState != null ? JSON.stringify(user.voiceState) : null,
screenShareState: user.screenShareState != null ? JSON.stringify(user.screenShareState) : null
});
await repo.save(entity);
}

View File

@@ -0,0 +1,9 @@
import { DataSource } from 'typeorm';
import { MetaEntity } from '../../../entities';
import { SetCurrentUserIdCommand } from '../../types';
export async function handleSetCurrentUserId(command: SetCurrentUserIdCommand, dataSource: DataSource): Promise<void> {
const repo = dataSource.getRepository(MetaEntity);
await repo.save({ key: 'currentUserId', value: command.payload.userId });
}

View File

@@ -0,0 +1,41 @@
import { DataSource } from 'typeorm';
import { MessageEntity } from '../../../entities';
import { UpdateMessageCommand } from '../../types';
export async function handleUpdateMessage(command: UpdateMessageCommand, dataSource: DataSource): Promise<void> {
const repo = dataSource.getRepository(MessageEntity);
const { messageId, updates } = command.payload;
const existing = await repo.findOne({ where: { id: messageId } });
if (!existing)
return;
if (updates.channelId !== undefined)
existing.channelId = updates.channelId ?? null;
if (updates.senderId !== undefined)
existing.senderId = updates.senderId;
if (updates.senderName !== undefined)
existing.senderName = updates.senderName;
if (updates.content !== undefined)
existing.content = updates.content;
if (updates.timestamp !== undefined)
existing.timestamp = updates.timestamp;
if (updates.editedAt !== undefined)
existing.editedAt = updates.editedAt ?? null;
if (updates.reactions !== undefined)
existing.reactions = JSON.stringify(updates.reactions ?? []);
if (updates.isDeleted !== undefined)
existing.isDeleted = updates.isDeleted ? 1 : 0;
if (updates.replyToId !== undefined)
existing.replyToId = updates.replyToId ?? null;
await repo.save(existing);
}

View File

@@ -0,0 +1,29 @@
import { DataSource } from 'typeorm';
import { RoomEntity } from '../../../entities';
import { UpdateRoomCommand } from '../../types';
import {
applyUpdates,
boolToInt,
jsonOrNull,
TransformMap
} from './utils/applyUpdates';
const ROOM_TRANSFORMS: TransformMap = {
isPrivate: boolToInt,
userCount: (val) => (val ?? 0),
permissions: jsonOrNull,
channels: jsonOrNull
};
export async function handleUpdateRoom(command: UpdateRoomCommand, dataSource: DataSource): Promise<void> {
const repo = dataSource.getRepository(RoomEntity);
const { roomId, updates } = command.payload;
const existing = await repo.findOne({ where: { id: roomId } });
if (!existing)
return;
applyUpdates(existing, updates, ROOM_TRANSFORMS);
await repo.save(existing);
}

View File

@@ -0,0 +1,30 @@
import { DataSource } from 'typeorm';
import { UserEntity } from '../../../entities';
import { UpdateUserCommand } from '../../types';
import {
applyUpdates,
boolToInt,
jsonOrNull,
TransformMap
} from './utils/applyUpdates';
const USER_TRANSFORMS: TransformMap = {
isOnline: boolToInt,
isAdmin: boolToInt,
isRoomOwner: boolToInt,
voiceState: jsonOrNull,
screenShareState: jsonOrNull
};
export async function handleUpdateUser(command: UpdateUserCommand, dataSource: DataSource): Promise<void> {
const repo = dataSource.getRepository(UserEntity);
const { userId, updates } = command.payload;
const existing = await repo.findOne({ where: { id: userId } });
if (!existing)
return;
applyUpdates(existing, updates, USER_TRANSFORMS);
await repo.save(existing);
}

View File

@@ -0,0 +1,32 @@
/** Converts a boolean-like value to SQLite's 0/1 integer representation. */
export const boolToInt = (val: unknown) => (val ? 1 : 0);
/** Serialises an object to a JSON string, or returns null if the value is null/undefined. */
export const jsonOrNull = (val: unknown) => (val != null ? JSON.stringify(val) : null);
/** A map of field names to transform functions that handle special serialisation. */
export type TransformMap = Partial<Record<string, (val: unknown) => unknown>>;
/**
* Applies a partial `updates` object onto an existing entity.
*
* - Fields absent from `updates` (undefined) are skipped entirely.
* - Fields listed in `transforms` are passed through their transform function first.
* - All other fields are written as-is, falling back to null when the value is null/undefined.
*/
export function applyUpdates<T extends object>(
entity: T,
updates: Partial<Record<string, unknown>>,
transforms: TransformMap = {}
): void {
const target = entity as unknown as Record<string, unknown>;
for (const [key, value] of Object.entries(updates)) {
if (value === undefined)
continue;
const transform = transforms[key];
target[key] = transform ? transform(value) : (value ?? null);
}
}