Database changes to make it better practise

This commit is contained in:
2026-04-02 01:32:08 +02:00
parent 5d7e045764
commit 314a26325f
36 changed files with 1453 additions and 193 deletions

View File

@@ -1,6 +1,8 @@
import { DataSource } from 'typeorm';
import {
ServerChannelEntity,
ServerEntity,
ServerTagEntity,
JoinRequestEntity,
ServerMembershipEntity,
ServerInviteEntity,
@@ -11,9 +13,13 @@ import { DeleteServerCommand } from '../../types';
export async function handleDeleteServer(command: DeleteServerCommand, dataSource: DataSource): Promise<void> {
const { serverId } = command.payload;
await dataSource.getRepository(JoinRequestEntity).delete({ serverId });
await dataSource.getRepository(ServerMembershipEntity).delete({ serverId });
await dataSource.getRepository(ServerInviteEntity).delete({ serverId });
await dataSource.getRepository(ServerBanEntity).delete({ serverId });
await dataSource.getRepository(ServerEntity).delete(serverId);
await dataSource.transaction(async (manager) => {
await manager.getRepository(ServerTagEntity).delete({ serverId });
await manager.getRepository(ServerChannelEntity).delete({ serverId });
await manager.getRepository(JoinRequestEntity).delete({ serverId });
await manager.getRepository(ServerMembershipEntity).delete({ serverId });
await manager.getRepository(ServerInviteEntity).delete({ serverId });
await manager.getRepository(ServerBanEntity).delete({ serverId });
await manager.getRepository(ServerEntity).delete(serverId);
});
}

View File

@@ -1,25 +1,30 @@
import { DataSource } from 'typeorm';
import { ServerEntity } from '../../../entities';
import { replaceServerRelations } from '../../relations';
import { UpsertServerCommand } from '../../types';
export async function handleUpsertServer(command: UpsertServerCommand, dataSource: DataSource): Promise<void> {
const repo = dataSource.getRepository(ServerEntity);
const { server } = command.payload;
const entity = repo.create({
id: server.id,
name: server.name,
description: server.description ?? null,
ownerId: server.ownerId,
ownerPublicKey: server.ownerPublicKey,
passwordHash: server.passwordHash ?? null,
isPrivate: server.isPrivate ? 1 : 0,
maxUsers: server.maxUsers,
currentUsers: server.currentUsers,
tags: JSON.stringify(server.tags),
channels: JSON.stringify(server.channels ?? []),
createdAt: server.createdAt,
lastSeen: server.lastSeen
});
await dataSource.transaction(async (manager) => {
const repo = manager.getRepository(ServerEntity);
const entity = repo.create({
id: server.id,
name: server.name,
description: server.description ?? null,
ownerId: server.ownerId,
ownerPublicKey: server.ownerPublicKey,
passwordHash: server.passwordHash ?? null,
isPrivate: server.isPrivate ? 1 : 0,
maxUsers: server.maxUsers,
currentUsers: server.currentUsers,
createdAt: server.createdAt,
lastSeen: server.lastSeen
});
await repo.save(entity);
await repo.save(entity);
await replaceServerRelations(manager, server.id, {
tags: server.tags,
channels: server.channels ?? []
});
});
}

View File

@@ -3,67 +3,10 @@ import { ServerEntity } from '../entities/ServerEntity';
import { JoinRequestEntity } from '../entities/JoinRequestEntity';
import {
AuthUserPayload,
ServerChannelPayload,
ServerPayload,
JoinRequestPayload
} from './types';
function channelNameKey(type: ServerChannelPayload['type'], name: string): string {
return `${type}:${name.toLocaleLowerCase()}`;
}
function parseStringArray(raw: string | null | undefined): string[] {
try {
const parsed = JSON.parse(raw || '[]');
return Array.isArray(parsed)
? parsed.filter((value): value is string => typeof value === 'string')
: [];
} catch {
return [];
}
}
function parseServerChannels(raw: string | null | undefined): ServerChannelPayload[] {
try {
const parsed = JSON.parse(raw || '[]');
if (!Array.isArray(parsed)) {
return [];
}
const seenIds = new Set<string>();
const seenNames = new Set<string>();
return parsed
.filter((channel): channel is Record<string, unknown> => !!channel && typeof channel === 'object')
.map((channel, index) => {
const id = typeof channel.id === 'string' ? channel.id.trim() : '';
const name = typeof channel.name === 'string' ? channel.name.trim().replace(/\s+/g, ' ') : '';
const type = channel.type === 'text' || channel.type === 'voice' ? channel.type : null;
const position = typeof channel.position === 'number' ? channel.position : index;
const nameKey = type ? channelNameKey(type, name) : '';
if (!id || !name || !type || seenIds.has(id) || seenNames.has(nameKey)) {
return null;
}
seenIds.add(id);
seenNames.add(nameKey);
return {
id,
name,
type,
position
} satisfies ServerChannelPayload;
})
.filter((channel): channel is ServerChannelPayload => !!channel);
} catch {
return [];
}
}
export function rowToAuthUser(row: AuthUserEntity): AuthUserPayload {
return {
id: row.id,
@@ -74,7 +17,10 @@ export function rowToAuthUser(row: AuthUserEntity): AuthUserPayload {
};
}
export function rowToServer(row: ServerEntity): ServerPayload {
export function rowToServer(
row: ServerEntity,
relations: Pick<ServerPayload, 'tags' | 'channels'> = { tags: [], channels: [] }
): ServerPayload {
return {
id: row.id,
name: row.name,
@@ -86,8 +32,8 @@ export function rowToServer(row: ServerEntity): ServerPayload {
isPrivate: !!row.isPrivate,
maxUsers: row.maxUsers,
currentUsers: row.currentUsers,
tags: parseStringArray(row.tags),
channels: parseServerChannels(row.channels),
tags: relations.tags ?? [],
channels: relations.channels ?? [],
createdAt: row.createdAt,
lastSeen: row.lastSeen
};

View File

@@ -1,10 +1,12 @@
import { DataSource } from 'typeorm';
import { ServerEntity } from '../../../entities';
import { rowToServer } from '../../mappers';
import { loadServerRelationsMap } from '../../relations';
export async function handleGetAllPublicServers(dataSource: DataSource) {
const repo = dataSource.getRepository(ServerEntity);
const rows = await repo.find({ where: { isPrivate: 0 } });
const relationsByServerId = await loadServerRelationsMap(dataSource, rows.map((row) => row.id));
return rows.map(rowToServer);
return rows.map((row) => rowToServer(row, relationsByServerId.get(row.id)));
}

View File

@@ -2,10 +2,17 @@ import { DataSource } from 'typeorm';
import { ServerEntity } from '../../../entities';
import { GetServerByIdQuery } from '../../types';
import { rowToServer } from '../../mappers';
import { loadServerRelationsMap } from '../../relations';
export async function handleGetServerById(query: GetServerByIdQuery, dataSource: DataSource) {
const repo = dataSource.getRepository(ServerEntity);
const row = await repo.findOne({ where: { id: query.payload.serverId } });
return row ? rowToServer(row) : null;
if (!row) {
return null;
}
const relationsByServerId = await loadServerRelationsMap(dataSource, [row.id]);
return rowToServer(row, relationsByServerId.get(row.id));
}

View File

@@ -0,0 +1,160 @@
import {
DataSource,
EntityManager,
In
} from 'typeorm';
import {
ServerChannelEntity,
ServerTagEntity
} from '../entities';
import { ServerChannelPayload } from './types';
interface ServerRelationRecord {
tags: string[];
channels: ServerChannelPayload[];
}
function normalizeChannelName(name: string): string {
return name.trim().replace(/\s+/g, ' ');
}
function channelNameKey(type: ServerChannelPayload['type'], name: string): string {
return `${type}:${normalizeChannelName(name).toLocaleLowerCase()}`;
}
function isFiniteNumber(value: unknown): value is number {
return typeof value === 'number' && Number.isFinite(value);
}
export function normalizeServerTags(rawTags: unknown): string[] {
if (!Array.isArray(rawTags)) {
return [];
}
return rawTags.filter((tag): tag is string => typeof tag === 'string');
}
export function normalizeServerChannels(rawChannels: unknown): ServerChannelPayload[] {
if (!Array.isArray(rawChannels)) {
return [];
}
const seenIds = new Set<string>();
const seenNames = new Set<string>();
const channels: ServerChannelPayload[] = [];
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 async function replaceServerRelations(
manager: EntityManager,
serverId: string,
options: { tags: unknown; channels: unknown }
): Promise<void> {
const tagRepo = manager.getRepository(ServerTagEntity);
const channelRepo = manager.getRepository(ServerChannelEntity);
const tags = normalizeServerTags(options.tags);
const channels = normalizeServerChannels(options.channels);
await tagRepo.delete({ serverId });
await channelRepo.delete({ serverId });
if (tags.length > 0) {
await tagRepo.insert(
tags.map((tag, position) => ({
serverId,
position,
value: tag
}))
);
}
if (channels.length > 0) {
await channelRepo.insert(
channels.map((channel) => ({
serverId,
channelId: channel.id,
name: channel.name,
type: channel.type,
position: channel.position
}))
);
}
}
export async function loadServerRelationsMap(
dataSource: DataSource,
serverIds: readonly string[]
): Promise<Map<string, ServerRelationRecord>> {
const groupedRelations = new Map<string, ServerRelationRecord>();
if (serverIds.length === 0) {
return groupedRelations;
}
const [tagRows, channelRows] = await Promise.all([
dataSource.getRepository(ServerTagEntity).find({
where: { serverId: In([...serverIds]) }
}),
dataSource.getRepository(ServerChannelEntity).find({
where: { serverId: In([...serverIds]) }
})
]);
for (const row of tagRows) {
const relation = groupedRelations.get(row.serverId) ?? { tags: [], channels: [] };
relation.tags.push(row.value);
groupedRelations.set(row.serverId, relation);
}
for (const row of channelRows) {
const relation = groupedRelations.get(row.serverId) ?? { tags: [], channels: [] };
relation.channels.push({
id: row.channelId,
name: row.name,
type: row.type,
position: row.position
});
groupedRelations.set(row.serverId, relation);
}
for (const [serverId, relation] of groupedRelations) {
const orderedTags = tagRows
.filter((row) => row.serverId === serverId)
.sort((first, second) => first.position - second.position)
.map((row) => row.value);
relation.tags = orderedTags;
relation.channels.sort((first, second) => first.position - second.position || first.name.localeCompare(second.name));
}
return groupedRelations;
}