Database changes to make it better practise
This commit is contained in:
@@ -3,6 +3,8 @@ import {
|
||||
MessageEntity,
|
||||
UserEntity,
|
||||
RoomEntity,
|
||||
RoomChannelEntity,
|
||||
RoomMemberEntity,
|
||||
ReactionEntity,
|
||||
BanEntity,
|
||||
AttachmentEntity,
|
||||
@@ -13,6 +15,8 @@ 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(RoomChannelEntity).clear();
|
||||
await dataSource.getRepository(RoomMemberEntity).clear();
|
||||
await dataSource.getRepository(ReactionEntity).clear();
|
||||
await dataSource.getRepository(BanEntity).clear();
|
||||
await dataSource.getRepository(AttachmentEntity).clear();
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
import { DataSource } from 'typeorm';
|
||||
import { RoomEntity, MessageEntity } from '../../../entities';
|
||||
import {
|
||||
RoomChannelEntity,
|
||||
RoomEntity,
|
||||
RoomMemberEntity,
|
||||
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 });
|
||||
await dataSource.transaction(async (manager) => {
|
||||
await manager.getRepository(RoomChannelEntity).delete({ roomId });
|
||||
await manager.getRepository(RoomMemberEntity).delete({ roomId });
|
||||
await manager.getRepository(RoomEntity).delete({ id: roomId });
|
||||
await manager.getRepository(MessageEntity).delete({ roomId });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
import { DataSource } from 'typeorm';
|
||||
import { MessageEntity } from '../../../entities';
|
||||
import { replaceMessageReactions } from '../../relations';
|
||||
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 dataSource.transaction(async (manager) => {
|
||||
const repo = manager.getRepository(MessageEntity);
|
||||
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,
|
||||
isDeleted: message.isDeleted ? 1 : 0,
|
||||
replyToId: message.replyToId ?? null
|
||||
});
|
||||
|
||||
await repo.save(entity);
|
||||
await repo.save(entity);
|
||||
await replaceMessageReactions(manager, message.id, message.reactions ?? []);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,31 +1,36 @@
|
||||
import { DataSource } from 'typeorm';
|
||||
import { RoomEntity } from '../../../entities';
|
||||
import { replaceRoomRelations } from '../../relations';
|
||||
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,
|
||||
hasPassword: room.hasPassword ? 1 : 0,
|
||||
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,
|
||||
members: room.members != null ? JSON.stringify(room.members) : null,
|
||||
sourceId: room.sourceId ?? null,
|
||||
sourceName: room.sourceName ?? null,
|
||||
sourceUrl: room.sourceUrl ?? null
|
||||
});
|
||||
await dataSource.transaction(async (manager) => {
|
||||
const repo = manager.getRepository(RoomEntity);
|
||||
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,
|
||||
hasPassword: room.hasPassword ? 1 : 0,
|
||||
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,
|
||||
sourceId: room.sourceId ?? null,
|
||||
sourceName: room.sourceName ?? null,
|
||||
sourceUrl: room.sourceUrl ?? null
|
||||
});
|
||||
|
||||
await repo.save(entity);
|
||||
await repo.save(entity);
|
||||
await replaceRoomRelations(manager, room.id, {
|
||||
channels: room.channels ?? [],
|
||||
members: room.members ?? []
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,41 +1,45 @@
|
||||
import { DataSource } from 'typeorm';
|
||||
import { MessageEntity } from '../../../entities';
|
||||
import { replaceMessageReactions } from '../../relations';
|
||||
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 } });
|
||||
await dataSource.transaction(async (manager) => {
|
||||
const repo = manager.getRepository(MessageEntity);
|
||||
const existing = await repo.findOne({ where: { id: messageId } });
|
||||
|
||||
if (!existing)
|
||||
return;
|
||||
if (!existing)
|
||||
return;
|
||||
|
||||
if (updates.channelId !== undefined)
|
||||
existing.channelId = updates.channelId ?? null;
|
||||
if (updates.channelId !== undefined)
|
||||
existing.channelId = updates.channelId ?? null;
|
||||
|
||||
if (updates.senderId !== undefined)
|
||||
existing.senderId = updates.senderId;
|
||||
if (updates.senderId !== undefined)
|
||||
existing.senderId = updates.senderId;
|
||||
|
||||
if (updates.senderName !== undefined)
|
||||
existing.senderName = updates.senderName;
|
||||
if (updates.senderName !== undefined)
|
||||
existing.senderName = updates.senderName;
|
||||
|
||||
if (updates.content !== undefined)
|
||||
existing.content = updates.content;
|
||||
if (updates.content !== undefined)
|
||||
existing.content = updates.content;
|
||||
|
||||
if (updates.timestamp !== undefined)
|
||||
existing.timestamp = updates.timestamp;
|
||||
if (updates.timestamp !== undefined)
|
||||
existing.timestamp = updates.timestamp;
|
||||
|
||||
if (updates.editedAt !== undefined)
|
||||
existing.editedAt = updates.editedAt ?? null;
|
||||
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.isDeleted !== undefined)
|
||||
existing.isDeleted = updates.isDeleted ? 1 : 0;
|
||||
if (updates.replyToId !== undefined)
|
||||
existing.replyToId = updates.replyToId ?? null;
|
||||
|
||||
if (updates.replyToId !== undefined)
|
||||
existing.replyToId = updates.replyToId ?? null;
|
||||
await repo.save(existing);
|
||||
|
||||
await repo.save(existing);
|
||||
if (updates.reactions !== undefined) {
|
||||
await replaceMessageReactions(manager, messageId, updates.reactions ?? []);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { DataSource } from 'typeorm';
|
||||
import { RoomEntity } from '../../../entities';
|
||||
import { replaceRoomRelations } from '../../relations';
|
||||
import { UpdateRoomCommand } from '../../types';
|
||||
import {
|
||||
applyUpdates,
|
||||
@@ -12,20 +13,26 @@ const ROOM_TRANSFORMS: TransformMap = {
|
||||
hasPassword: boolToInt,
|
||||
isPrivate: boolToInt,
|
||||
userCount: (val) => (val ?? 0),
|
||||
permissions: jsonOrNull,
|
||||
channels: jsonOrNull,
|
||||
members: jsonOrNull
|
||||
permissions: 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 } });
|
||||
await dataSource.transaction(async (manager) => {
|
||||
const repo = manager.getRepository(RoomEntity);
|
||||
const existing = await repo.findOne({ where: { id: roomId } });
|
||||
|
||||
if (!existing)
|
||||
return;
|
||||
if (!existing)
|
||||
return;
|
||||
|
||||
applyUpdates(existing, updates, ROOM_TRANSFORMS);
|
||||
await repo.save(existing);
|
||||
const { channels, members, ...entityUpdates } = updates;
|
||||
|
||||
applyUpdates(existing, entityUpdates, ROOM_TRANSFORMS);
|
||||
await repo.save(existing);
|
||||
await replaceRoomRelations(manager, roomId, {
|
||||
channels,
|
||||
members
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -9,10 +9,14 @@ import { RoomEntity } from '../entities/RoomEntity';
|
||||
import { ReactionEntity } from '../entities/ReactionEntity';
|
||||
import { BanEntity } from '../entities/BanEntity';
|
||||
import { AttachmentEntity } from '../entities/AttachmentEntity';
|
||||
import {
|
||||
ReactionPayload,
|
||||
RoomPayload
|
||||
} from './types';
|
||||
|
||||
const DELETED_MESSAGE_CONTENT = '[Message deleted]';
|
||||
|
||||
export function rowToMessage(row: MessageEntity) {
|
||||
export function rowToMessage(row: MessageEntity, reactions: ReactionPayload[] = []) {
|
||||
const isDeleted = !!row.isDeleted;
|
||||
|
||||
return {
|
||||
@@ -24,7 +28,7 @@ export function rowToMessage(row: MessageEntity) {
|
||||
content: isDeleted ? DELETED_MESSAGE_CONTENT : row.content,
|
||||
timestamp: row.timestamp,
|
||||
editedAt: row.editedAt ?? undefined,
|
||||
reactions: isDeleted ? [] : JSON.parse(row.reactions || '[]') as unknown[],
|
||||
reactions: isDeleted ? [] : reactions,
|
||||
isDeleted,
|
||||
replyToId: row.replyToId ?? undefined
|
||||
};
|
||||
@@ -49,7 +53,10 @@ export function rowToUser(row: UserEntity) {
|
||||
};
|
||||
}
|
||||
|
||||
export function rowToRoom(row: RoomEntity) {
|
||||
export function rowToRoom(
|
||||
row: RoomEntity,
|
||||
relations: Pick<RoomPayload, 'channels' | 'members'> = { channels: [], members: [] }
|
||||
) {
|
||||
return {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
@@ -65,8 +72,8 @@ export function rowToRoom(row: RoomEntity) {
|
||||
icon: row.icon ?? undefined,
|
||||
iconUpdatedAt: row.iconUpdatedAt ?? undefined,
|
||||
permissions: row.permissions ? JSON.parse(row.permissions) : undefined,
|
||||
channels: row.channels ? JSON.parse(row.channels) : undefined,
|
||||
members: row.members ? JSON.parse(row.members) : undefined,
|
||||
channels: relations.channels ?? [],
|
||||
members: relations.members ?? [],
|
||||
sourceId: row.sourceId ?? undefined,
|
||||
sourceName: row.sourceName ?? undefined,
|
||||
sourceUrl: row.sourceUrl ?? undefined
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { DataSource } from 'typeorm';
|
||||
import { RoomEntity } from '../../../entities';
|
||||
import { rowToRoom } from '../../mappers';
|
||||
import { loadRoomRelationsMap } from '../../relations';
|
||||
|
||||
export async function handleGetAllRooms(dataSource: DataSource) {
|
||||
const repo = dataSource.getRepository(RoomEntity);
|
||||
const rows = await repo.find();
|
||||
const relationsByRoomId = await loadRoomRelationsMap(dataSource, rows.map((row) => row.id));
|
||||
|
||||
return rows.map(rowToRoom);
|
||||
return rows.map((row) => rowToRoom(row, relationsByRoomId.get(row.id)));
|
||||
}
|
||||
|
||||
@@ -2,10 +2,17 @@ import { DataSource } from 'typeorm';
|
||||
import { MessageEntity } from '../../../entities';
|
||||
import { GetMessageByIdQuery } from '../../types';
|
||||
import { rowToMessage } from '../../mappers';
|
||||
import { loadMessageReactionsMap } from '../../relations';
|
||||
|
||||
export async function handleGetMessageById(query: GetMessageByIdQuery, dataSource: DataSource) {
|
||||
const repo = dataSource.getRepository(MessageEntity);
|
||||
const row = await repo.findOne({ where: { id: query.payload.messageId } });
|
||||
|
||||
return row ? rowToMessage(row) : null;
|
||||
if (!row) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const reactionsByMessageId = await loadMessageReactionsMap(dataSource, [row.id]);
|
||||
|
||||
return rowToMessage(row, reactionsByMessageId.get(row.id) ?? []);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { DataSource } from 'typeorm';
|
||||
import { MessageEntity } from '../../../entities';
|
||||
import { GetMessagesQuery } from '../../types';
|
||||
import { rowToMessage } from '../../mappers';
|
||||
import { loadMessageReactionsMap } from '../../relations';
|
||||
|
||||
export async function handleGetMessages(query: GetMessagesQuery, dataSource: DataSource) {
|
||||
const repo = dataSource.getRepository(MessageEntity);
|
||||
@@ -12,6 +13,7 @@ export async function handleGetMessages(query: GetMessagesQuery, dataSource: Dat
|
||||
take: limit,
|
||||
skip: offset
|
||||
});
|
||||
const reactionsByMessageId = await loadMessageReactionsMap(dataSource, rows.map((row) => row.id));
|
||||
|
||||
return rows.map(rowToMessage);
|
||||
return rows.map((row) => rowToMessage(row, reactionsByMessageId.get(row.id) ?? []));
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { DataSource, MoreThan } from 'typeorm';
|
||||
import { MessageEntity } from '../../../entities';
|
||||
import { GetMessagesSinceQuery } from '../../types';
|
||||
import { rowToMessage } from '../../mappers';
|
||||
import { loadMessageReactionsMap } from '../../relations';
|
||||
|
||||
export async function handleGetMessagesSince(query: GetMessagesSinceQuery, dataSource: DataSource) {
|
||||
const repo = dataSource.getRepository(MessageEntity);
|
||||
@@ -13,6 +14,7 @@ export async function handleGetMessagesSince(query: GetMessagesSinceQuery, dataS
|
||||
},
|
||||
order: { timestamp: 'ASC' }
|
||||
});
|
||||
const reactionsByMessageId = await loadMessageReactionsMap(dataSource, rows.map((row) => row.id));
|
||||
|
||||
return rows.map(rowToMessage);
|
||||
return rows.map((row) => rowToMessage(row, reactionsByMessageId.get(row.id) ?? []));
|
||||
}
|
||||
|
||||
@@ -2,10 +2,17 @@ import { DataSource } from 'typeorm';
|
||||
import { RoomEntity } from '../../../entities';
|
||||
import { GetRoomQuery } from '../../types';
|
||||
import { rowToRoom } from '../../mappers';
|
||||
import { loadRoomRelationsMap } from '../../relations';
|
||||
|
||||
export async function handleGetRoom(query: GetRoomQuery, dataSource: DataSource) {
|
||||
const repo = dataSource.getRepository(RoomEntity);
|
||||
const row = await repo.findOne({ where: { id: query.payload.roomId } });
|
||||
|
||||
return row ? rowToRoom(row) : null;
|
||||
if (!row) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const relationsByRoomId = await loadRoomRelationsMap(dataSource, [row.id]);
|
||||
|
||||
return rowToRoom(row, relationsByRoomId.get(row.id));
|
||||
}
|
||||
|
||||
426
electron/cqrs/relations.ts
Normal file
426
electron/cqrs/relations.ts
Normal file
@@ -0,0 +1,426 @@
|
||||
import {
|
||||
DataSource,
|
||||
EntityManager,
|
||||
In
|
||||
} from 'typeorm';
|
||||
import {
|
||||
ReactionEntity,
|
||||
RoomChannelEntity,
|
||||
RoomMemberEntity
|
||||
} from '../entities';
|
||||
import { ReactionPayload } from './types';
|
||||
|
||||
type ChannelType = 'text' | 'voice';
|
||||
type RoomMemberRole = 'host' | 'admin' | 'moderator' | 'member';
|
||||
|
||||
export interface RoomChannelRecord {
|
||||
id: string;
|
||||
name: string;
|
||||
type: ChannelType;
|
||||
position: number;
|
||||
}
|
||||
|
||||
export interface RoomMemberRecord {
|
||||
id: string;
|
||||
oderId?: string;
|
||||
username: string;
|
||||
displayName: string;
|
||||
avatarUrl?: string;
|
||||
role: RoomMemberRole;
|
||||
joinedAt: number;
|
||||
lastSeenAt: number;
|
||||
}
|
||||
|
||||
interface RoomRelationRecord {
|
||||
channels: RoomChannelRecord[];
|
||||
members: RoomMemberRecord[];
|
||||
}
|
||||
|
||||
function isFiniteNumber(value: unknown): value is number {
|
||||
return typeof value === 'number' && Number.isFinite(value);
|
||||
}
|
||||
|
||||
function normalizeChannelName(name: string): string {
|
||||
return name.trim().replace(/\s+/g, ' ');
|
||||
}
|
||||
|
||||
function channelNameKey(type: ChannelType, name: string): string {
|
||||
return `${type}:${normalizeChannelName(name).toLocaleLowerCase()}`;
|
||||
}
|
||||
|
||||
function memberKey(member: { id?: string; oderId?: string }): string {
|
||||
return member.oderId?.trim() || member.id?.trim() || '';
|
||||
}
|
||||
|
||||
function fallbackDisplayName(member: Partial<RoomMemberRecord>): string {
|
||||
return member.displayName || member.username || member.oderId || member.id || 'User';
|
||||
}
|
||||
|
||||
function fallbackUsername(member: Partial<RoomMemberRecord>): string {
|
||||
const base = fallbackDisplayName(member)
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, '_');
|
||||
|
||||
return base || member.oderId || member.id || 'user';
|
||||
}
|
||||
|
||||
function normalizeRoomMemberRole(value: unknown): RoomMemberRole {
|
||||
return value === 'host' || value === 'admin' || value === 'moderator' || value === 'member'
|
||||
? value
|
||||
: 'member';
|
||||
}
|
||||
|
||||
function mergeRoomMemberRole(
|
||||
existingRole: RoomMemberRole,
|
||||
incomingRole: RoomMemberRole,
|
||||
preferIncoming: boolean
|
||||
): RoomMemberRole {
|
||||
if (existingRole === incomingRole) {
|
||||
return existingRole;
|
||||
}
|
||||
|
||||
if (incomingRole === 'member' && existingRole !== 'member') {
|
||||
return existingRole;
|
||||
}
|
||||
|
||||
if (existingRole === 'member' && incomingRole !== 'member') {
|
||||
return incomingRole;
|
||||
}
|
||||
|
||||
return preferIncoming ? incomingRole : existingRole;
|
||||
}
|
||||
|
||||
function compareRoomMembers(firstMember: RoomMemberRecord, secondMember: RoomMemberRecord): number {
|
||||
const displayNameCompare = firstMember.displayName.localeCompare(secondMember.displayName, undefined, { sensitivity: 'base' });
|
||||
|
||||
if (displayNameCompare !== 0) {
|
||||
return displayNameCompare;
|
||||
}
|
||||
|
||||
return memberKey(firstMember).localeCompare(memberKey(secondMember));
|
||||
}
|
||||
|
||||
function normalizeRoomMember(rawMember: Record<string, unknown>, now: number): RoomMemberRecord | null {
|
||||
const normalizedId = typeof rawMember['id'] === 'string' ? rawMember['id'].trim() : '';
|
||||
const normalizedOderId = typeof rawMember['oderId'] === 'string' ? rawMember['oderId'].trim() : '';
|
||||
const normalizedKey = normalizedOderId || normalizedId;
|
||||
|
||||
if (!normalizedKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const lastSeenAt = isFiniteNumber(rawMember['lastSeenAt'])
|
||||
? rawMember['lastSeenAt']
|
||||
: isFiniteNumber(rawMember['joinedAt'])
|
||||
? rawMember['joinedAt']
|
||||
: now;
|
||||
const joinedAt = isFiniteNumber(rawMember['joinedAt']) ? rawMember['joinedAt'] : lastSeenAt;
|
||||
const username = typeof rawMember['username'] === 'string' ? rawMember['username'].trim() : '';
|
||||
const displayName = typeof rawMember['displayName'] === 'string' ? rawMember['displayName'].trim() : '';
|
||||
const avatarUrl = typeof rawMember['avatarUrl'] === 'string' ? rawMember['avatarUrl'].trim() : '';
|
||||
|
||||
const member: RoomMemberRecord = {
|
||||
id: normalizedId || normalizedKey,
|
||||
oderId: normalizedOderId || undefined,
|
||||
username: username || fallbackUsername({ id: normalizedId || normalizedKey, oderId: normalizedOderId || undefined, displayName }),
|
||||
displayName: displayName || fallbackDisplayName({ id: normalizedId || normalizedKey, oderId: normalizedOderId || undefined, username }),
|
||||
avatarUrl: avatarUrl || undefined,
|
||||
role: normalizeRoomMemberRole(rawMember['role']),
|
||||
joinedAt,
|
||||
lastSeenAt
|
||||
};
|
||||
|
||||
return member;
|
||||
}
|
||||
|
||||
function mergeRoomMembers(existingMember: RoomMemberRecord | undefined, incomingMember: RoomMemberRecord): RoomMemberRecord {
|
||||
if (!existingMember) {
|
||||
return incomingMember;
|
||||
}
|
||||
|
||||
const preferIncoming = incomingMember.lastSeenAt >= existingMember.lastSeenAt;
|
||||
|
||||
return {
|
||||
id: existingMember.id || incomingMember.id,
|
||||
oderId: incomingMember.oderId || existingMember.oderId,
|
||||
username: preferIncoming
|
||||
? (incomingMember.username || existingMember.username)
|
||||
: (existingMember.username || incomingMember.username),
|
||||
displayName: preferIncoming
|
||||
? (incomingMember.displayName || existingMember.displayName)
|
||||
: (existingMember.displayName || incomingMember.displayName),
|
||||
avatarUrl: preferIncoming
|
||||
? (incomingMember.avatarUrl || existingMember.avatarUrl)
|
||||
: (existingMember.avatarUrl || incomingMember.avatarUrl),
|
||||
role: mergeRoomMemberRole(existingMember.role, incomingMember.role, preferIncoming),
|
||||
joinedAt: Math.min(existingMember.joinedAt, incomingMember.joinedAt),
|
||||
lastSeenAt: Math.max(existingMember.lastSeenAt, incomingMember.lastSeenAt)
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeReactionPayloads(rawReactions: unknown, messageId: string): ReactionPayload[] {
|
||||
if (!Array.isArray(rawReactions)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const seen = new Set<string>();
|
||||
const reactions: ReactionPayload[] = [];
|
||||
|
||||
for (const rawReaction of rawReactions) {
|
||||
if (!rawReaction || typeof rawReaction !== 'object') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const reaction = rawReaction as Record<string, unknown>;
|
||||
const emoji = typeof reaction['emoji'] === 'string' ? reaction['emoji'] : '';
|
||||
const userId = typeof reaction['userId'] === 'string' ? reaction['userId'] : '';
|
||||
const dedupeKey = `${userId}:${emoji}`;
|
||||
|
||||
if (!emoji || seen.has(dedupeKey)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
seen.add(dedupeKey);
|
||||
reactions.push({
|
||||
id: typeof reaction['id'] === 'string' && reaction['id'].trim() ? reaction['id'] : `${messageId}:${dedupeKey}`,
|
||||
messageId,
|
||||
oderId: typeof reaction['oderId'] === 'string' ? reaction['oderId'] : '',
|
||||
userId,
|
||||
emoji,
|
||||
timestamp: isFiniteNumber(reaction['timestamp']) ? reaction['timestamp'] : 0
|
||||
});
|
||||
}
|
||||
|
||||
return reactions;
|
||||
}
|
||||
|
||||
export function normalizeRoomChannels(rawChannels: unknown): RoomChannelRecord[] {
|
||||
if (!Array.isArray(rawChannels)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const seenIds = new Set<string>();
|
||||
const seenNames = new Set<string>();
|
||||
const channels: RoomChannelRecord[] = [];
|
||||
|
||||
for (const [index, rawChannel] of rawChannels.entries()) {
|
||||
if (!rawChannel || typeof rawChannel !== 'object') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const channel = rawChannel as Record<string, unknown>;
|
||||
const id = typeof channel['id'] === 'string' ? channel['id'].trim() : '';
|
||||
const name = typeof channel['name'] === 'string' ? normalizeChannelName(channel['name']) : '';
|
||||
const type = channel['type'] === 'text' || channel['type'] === 'voice' ? channel['type'] : null;
|
||||
const position = isFiniteNumber(channel['position']) ? channel['position'] : index;
|
||||
const nameKey = type ? channelNameKey(type, name) : '';
|
||||
|
||||
if (!id || !name || !type || seenIds.has(id) || seenNames.has(nameKey)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
seenIds.add(id);
|
||||
seenNames.add(nameKey);
|
||||
channels.push({
|
||||
id,
|
||||
name,
|
||||
type,
|
||||
position
|
||||
});
|
||||
}
|
||||
|
||||
return channels;
|
||||
}
|
||||
|
||||
export function normalizeRoomMembers(rawMembers: unknown, now: number = Date.now()): RoomMemberRecord[] {
|
||||
if (!Array.isArray(rawMembers)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const membersByKey = new Map<string, RoomMemberRecord>();
|
||||
|
||||
for (const rawMember of rawMembers) {
|
||||
if (!rawMember || typeof rawMember !== 'object') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const member = normalizeRoomMember(rawMember as Record<string, unknown>, now);
|
||||
|
||||
if (!member) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const key = memberKey(member);
|
||||
|
||||
membersByKey.set(key, mergeRoomMembers(membersByKey.get(key), member));
|
||||
}
|
||||
|
||||
return Array.from(membersByKey.values()).sort(compareRoomMembers);
|
||||
}
|
||||
|
||||
export async function replaceMessageReactions(
|
||||
manager: EntityManager,
|
||||
messageId: string,
|
||||
rawReactions: unknown
|
||||
): Promise<void> {
|
||||
const repo = manager.getRepository(ReactionEntity);
|
||||
|
||||
await repo.delete({ messageId });
|
||||
|
||||
const reactions = normalizeReactionPayloads(rawReactions, messageId);
|
||||
|
||||
if (reactions.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await repo.insert(
|
||||
reactions.map((reaction) => ({
|
||||
id: reaction.id,
|
||||
messageId,
|
||||
oderId: reaction.oderId || null,
|
||||
userId: reaction.userId || null,
|
||||
emoji: reaction.emoji,
|
||||
timestamp: reaction.timestamp
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
export async function loadMessageReactionsMap(
|
||||
dataSource: DataSource,
|
||||
messageIds: readonly string[]
|
||||
): Promise<Map<string, ReactionPayload[]>> {
|
||||
const groupedReactions = new Map<string, ReactionPayload[]>();
|
||||
|
||||
if (messageIds.length === 0) {
|
||||
return groupedReactions;
|
||||
}
|
||||
|
||||
const rows = await dataSource.getRepository(ReactionEntity).find({
|
||||
where: { messageId: In([...messageIds]) }
|
||||
});
|
||||
|
||||
for (const row of rows) {
|
||||
const reactions = groupedReactions.get(row.messageId) ?? [];
|
||||
|
||||
reactions.push({
|
||||
id: row.id,
|
||||
messageId: row.messageId,
|
||||
oderId: row.oderId ?? '',
|
||||
userId: row.userId ?? '',
|
||||
emoji: row.emoji,
|
||||
timestamp: row.timestamp
|
||||
});
|
||||
groupedReactions.set(row.messageId, reactions);
|
||||
}
|
||||
|
||||
for (const reactions of groupedReactions.values()) {
|
||||
reactions.sort((first, second) => first.timestamp - second.timestamp);
|
||||
}
|
||||
|
||||
return groupedReactions;
|
||||
}
|
||||
|
||||
export async function replaceRoomRelations(
|
||||
manager: EntityManager,
|
||||
roomId: string,
|
||||
options: { channels?: unknown; members?: unknown }
|
||||
): Promise<void> {
|
||||
if (options.channels !== undefined) {
|
||||
const channelRepo = manager.getRepository(RoomChannelEntity);
|
||||
const channels = normalizeRoomChannels(options.channels);
|
||||
|
||||
await channelRepo.delete({ roomId });
|
||||
|
||||
if (channels.length > 0) {
|
||||
await channelRepo.insert(
|
||||
channels.map((channel) => ({
|
||||
roomId,
|
||||
channelId: channel.id,
|
||||
name: channel.name,
|
||||
type: channel.type,
|
||||
position: channel.position
|
||||
}))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.members !== undefined) {
|
||||
const memberRepo = manager.getRepository(RoomMemberEntity);
|
||||
const members = normalizeRoomMembers(options.members);
|
||||
|
||||
await memberRepo.delete({ roomId });
|
||||
|
||||
if (members.length > 0) {
|
||||
await memberRepo.insert(
|
||||
members.map((member) => ({
|
||||
roomId,
|
||||
memberKey: memberKey(member),
|
||||
id: member.id,
|
||||
oderId: member.oderId ?? null,
|
||||
username: member.username,
|
||||
displayName: member.displayName,
|
||||
avatarUrl: member.avatarUrl ?? null,
|
||||
role: member.role,
|
||||
joinedAt: member.joinedAt,
|
||||
lastSeenAt: member.lastSeenAt
|
||||
}))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadRoomRelationsMap(
|
||||
dataSource: DataSource,
|
||||
roomIds: readonly string[]
|
||||
): Promise<Map<string, RoomRelationRecord>> {
|
||||
const groupedRelations = new Map<string, RoomRelationRecord>();
|
||||
|
||||
if (roomIds.length === 0) {
|
||||
return groupedRelations;
|
||||
}
|
||||
|
||||
const [channelRows, memberRows] = await Promise.all([
|
||||
dataSource.getRepository(RoomChannelEntity).find({
|
||||
where: { roomId: In([...roomIds]) }
|
||||
}),
|
||||
dataSource.getRepository(RoomMemberEntity).find({
|
||||
where: { roomId: In([...roomIds]) }
|
||||
})
|
||||
]);
|
||||
|
||||
for (const row of channelRows) {
|
||||
const relation = groupedRelations.get(row.roomId) ?? { channels: [], members: [] };
|
||||
|
||||
relation.channels.push({
|
||||
id: row.channelId,
|
||||
name: row.name,
|
||||
type: row.type,
|
||||
position: row.position
|
||||
});
|
||||
groupedRelations.set(row.roomId, relation);
|
||||
}
|
||||
|
||||
for (const row of memberRows) {
|
||||
const relation = groupedRelations.get(row.roomId) ?? { channels: [], members: [] };
|
||||
|
||||
relation.members.push({
|
||||
id: row.id,
|
||||
oderId: row.oderId ?? undefined,
|
||||
username: row.username,
|
||||
displayName: row.displayName,
|
||||
avatarUrl: row.avatarUrl ?? undefined,
|
||||
role: row.role,
|
||||
joinedAt: row.joinedAt,
|
||||
lastSeenAt: row.lastSeenAt
|
||||
});
|
||||
groupedRelations.set(row.roomId, relation);
|
||||
}
|
||||
|
||||
for (const relation of groupedRelations.values()) {
|
||||
relation.channels.sort((first, second) => first.position - second.position || first.name.localeCompare(second.name));
|
||||
relation.members.sort(compareRoomMembers);
|
||||
}
|
||||
|
||||
return groupedRelations;
|
||||
}
|
||||
Reference in New Issue
Block a user