Database changes to make it better practise
This commit is contained in:
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 ?? []
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
160
server/src/cqrs/relations.ts
Normal file
160
server/src/cqrs/relations.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user