https://project.mdnd-it.cc/work_packages/94
This commit is contained in:
2025-08-23 04:25:28 +02:00
parent 725516ad6c
commit 19cfa031d0
25823 changed files with 1095587 additions and 2801760 deletions
@@ -0,0 +1,138 @@
import { Repository } from 'typeorm';
import { AppDataSource } from '../ormconfig';
import { ChatArchiveAggregate } from '../../Domain/Chat/ChatArchiveAggregate';
import { IChatArchiveRepository } from '../../Domain/IRepository/IChatArchiveRepository';
import { logDatabase, logError } from '../../Application/Services/Logger';
import { ChatState } from '../../Domain/Chat/ChatAggregate';
export class ChatArchiveRepository implements IChatArchiveRepository {
private repo: Repository<ChatArchiveAggregate>;
constructor() {
this.repo = AppDataSource.getRepository(ChatArchiveAggregate);
}
async create(archive: Partial<ChatArchiveAggregate>) {
const startTime = Date.now();
try {
const result = await this.repo.save(archive);
logDatabase('Chat archive created successfully', undefined, Date.now() - startTime, {
archiveId: result.id,
chatId: result.chatId,
messageCount: result.archivedMessages?.length || 0
});
return result;
} catch (error) {
logError('ChatArchiveRepository.create error', error as Error);
throw new Error('Failed to create chat archive in database');
}
}
async findAll() {
const startTime = Date.now();
try {
const result = await this.repo.find();
logDatabase('All chat archives retrieved', undefined, Date.now() - startTime, {
count: result.length
});
return result;
} catch (error) {
logError('ChatArchiveRepository.findAll error', error as Error);
throw new Error('Failed to retrieve chat archives from database');
}
}
async findById(id: string) {
const startTime = Date.now();
try {
const result = await this.repo.findOneBy({ id });
logDatabase('Chat archive retrieved by id', `findById(${id})`, Date.now() - startTime, {
archiveId: id,
found: !!result
});
return result;
} catch (error) {
logError('ChatArchiveRepository.findById error', error as Error);
throw new Error('Failed to find chat archive by id');
}
}
async findByChatId(chatId: string) {
const startTime = Date.now();
try {
const result = await this.repo
.find({
where: { chatId },
order: { archivedAt: 'DESC' }
});
logDatabase('Chat archives retrieved by chat id', `findByChatId(${chatId})`, Date.now() - startTime, {
chatId,
count: result.length
});
return result;
} catch (error) {
logError('ChatArchiveRepository.findByChatId error', error as Error);
throw new Error('Failed to find chat archives by chat id');
}
}
async findByGameId(gameId: string) {
const startTime = Date.now();
try {
const result = await this.repo
.find({
where: { gameId },
order: { archivedAt: 'DESC' }
});
logDatabase('Chat archives retrieved by game id', `findByGameId(${gameId})`, Date.now() - startTime, {
gameId,
count: result.length
});
return result;
} catch (error) {
logError('ChatArchiveRepository.findByGameId error', error as Error);
throw new Error('Failed to find chat archives by game id');
}
}
async delete(id: string) {
const startTime = Date.now();
try {
const result = await this.repo.delete(id);
logDatabase('Chat archive deleted', `delete(${id})`, Date.now() - startTime, {
archiveId: id,
affected: result.affected
});
return result;
} catch (error) {
logError('ChatArchiveRepository.delete error', error as Error);
throw new Error('Failed to delete chat archive');
}
}
async cleanup(olderThanDays: number) {
const startTime = Date.now();
try {
const cutoffDate = new Date(Date.now() - olderThanDays * 24 * 60 * 60 * 1000);
const result = await this.repo
.createQueryBuilder()
.delete()
.where('archivedAt < :cutoffDate', { cutoffDate })
.execute();
logDatabase('Chat archive cleanup completed', `cleanup(${olderThanDays} days)`, Date.now() - startTime, {
olderThanDays,
deleted: result.affected,
cutoffDate
});
return result.affected || 0;
} catch (error) {
logError('ChatArchiveRepository.cleanup error', error as Error);
throw new Error('Failed to cleanup old chat archives');
}
}
}
@@ -0,0 +1,358 @@
import { Repository, MoreThan, Not } from 'typeorm';
import { AppDataSource } from '../ormconfig';
import { ChatAggregate, ChatState, ChatType } from '../../Domain/Chat/ChatAggregate';
import { ChatArchiveAggregate } from '../../Domain/Chat/ChatArchiveAggregate';
import { IChatRepository } from '../../Domain/IRepository/IChatRepository';
import { logDatabase, logError } from '../../Application/Services/Logger';
export class ChatRepository implements IChatRepository {
private repo: Repository<ChatAggregate>;
private archiveRepo: Repository<ChatArchiveAggregate>;
constructor() {
this.repo = AppDataSource.getRepository(ChatAggregate);
this.archiveRepo = AppDataSource.getRepository(ChatArchiveAggregate);
}
async create(chat: Partial<ChatAggregate>) {
const startTime = Date.now();
try {
const result = await this.repo.save(chat);
logDatabase('Chat created successfully', undefined, Date.now() - startTime, {
chatId: result.id,
type: result.type,
participants: result.users?.length || 0
});
return result;
} catch (error) {
logError('ChatRepository.create error', error as Error);
throw new Error('Failed to create chat in database');
}
}
async findByPage(from: number, to: number): Promise<{ chats: ChatAggregate[], totalCount: number }> {
const startTime = Date.now();
try {
const skip = from;
const take = to - from + 1;
const [chats, totalCount] = await this.repo.findAndCount({
where: { state: Not(ChatState.SOFT_DELETE) },
order: { createDate: 'DESC' },
skip,
take
});
logDatabase('Chats page retrieved successfully', undefined, Date.now() - startTime, {
from,
to,
returned: chats.length,
totalCount
});
return { chats, totalCount };
} catch (error) {
logError('ChatRepository.findByPage error', error as Error);
throw new Error('Failed to retrieve chats page from database');
}
}
async findByPageIncludingDeleted(from: number, to: number): Promise<{ chats: ChatAggregate[], totalCount: number }> {
const startTime = Date.now();
try {
const skip = from;
const take = to - from + 1;
const [chats, totalCount] = await this.repo.findAndCount({
order: { createDate: 'DESC' },
skip,
take
});
logDatabase('Chats page retrieved successfully (including deleted)', undefined, Date.now() - startTime, {
from,
to,
returned: chats.length,
totalCount
});
return { chats, totalCount };
} catch (error) {
logError('ChatRepository.findByPageIncludingDeleted error', error as Error);
throw new Error('Failed to retrieve chats page from database');
}
}
async findById(id: string) {
const startTime = Date.now();
try {
const result = await this.repo.findOne({
where: {
id,
state: Not(ChatState.SOFT_DELETE)
}
});
logDatabase('Chat findById query completed', undefined, Date.now() - startTime, {
found: !!result,
chatId: id
});
return result;
} catch (error) {
logError('ChatRepository.findById error', error as Error);
throw new Error('Failed to retrieve chat from database');
}
}
async findByIdIncludingDeleted(id: string) {
const startTime = Date.now();
try {
const result = await this.repo.findOneBy({ id });
logDatabase('Chat findByIdIncludingDeleted query completed', undefined, Date.now() - startTime, {
found: !!result,
chatId: id
});
return result;
} catch (error) {
logError('ChatRepository.findByIdIncludingDeleted error', error as Error);
throw new Error('Failed to retrieve chat from database');
}
}
async findByUserId(userId: string) {
const startTime = Date.now();
try {
const result = await this.repo
.createQueryBuilder('chat')
.where(':userId = ANY(chat.users)', { userId })
.andWhere('chat.state != :softDelete', { softDelete: ChatState.SOFT_DELETE })
.getMany();
logDatabase('Chats retrieved by user id', `findByUserId(${userId})`, Date.now() - startTime, {
userId,
count: result.length
});
return result;
} catch (error) {
logError('ChatRepository.findByUserId error', error as Error);
throw new Error('Failed to find chats by user id');
}
}
async findByUserIdIncludingDeleted(userId: string) {
const startTime = Date.now();
try {
const result = await this.repo
.createQueryBuilder('chat')
.where(':userId = ANY(chat.users)', { userId })
.getMany();
logDatabase('Chats retrieved by user id (including deleted)', `findByUserIdIncludingDeleted(${userId})`, Date.now() - startTime, {
userId,
count: result.length
});
return result;
} catch (error) {
logError('ChatRepository.findByUserIdIncludingDeleted error', error as Error);
throw new Error('Failed to find all chats by user id');
}
}
async findByGameId(gameId: string) {
const startTime = Date.now();
try {
const result = await this.repo.findOneBy({
gameId,
type: ChatType.GAME,
state: ChatState.ACTIVE
});
logDatabase('Chat retrieved by game id', `findByGameId(${gameId})`, Date.now() - startTime, {
gameId,
found: !!result
});
return result;
} catch (error) {
logError('ChatRepository.findByGameId error', error as Error);
throw new Error('Failed to find chat by game id');
}
}
async findActiveChatsForUser(userId: string) {
const startTime = Date.now();
try {
const result = await this.repo
.createQueryBuilder('chat')
.where(':userId = ANY(chat.users)', { userId })
.andWhere('chat.state = :state', { state: ChatState.ACTIVE })
.orderBy('chat.lastActivity', 'DESC')
.getMany();
logDatabase('Active chats retrieved for user', `findActiveChatsForUser(${userId})`, Date.now() - startTime, {
userId,
count: result.length
});
return result;
} catch (error) {
logError('ChatRepository.findActiveChatsForUser error', error as Error);
throw new Error('Failed to find active chats for user');
}
}
async findInactiveChats(inactivityMinutes: number) {
const startTime = Date.now();
try {
const cutoffDate = new Date(Date.now() - inactivityMinutes * 60 * 1000);
const result = await this.repo
.createQueryBuilder('chat')
.where('chat.state = :state', { state: ChatState.ACTIVE })
.andWhere('(chat.lastActivity < :cutoffDate OR chat.lastActivity IS NULL)', { cutoffDate })
.getMany();
logDatabase('Inactive chats retrieved', `findInactiveChats(${inactivityMinutes}min)`, Date.now() - startTime, {
inactivityMinutes,
count: result.length,
cutoffDate
});
return result;
} catch (error) {
logError('ChatRepository.findInactiveChats error', error as Error);
throw new Error('Failed to find inactive chats');
}
}
async update(id: string, update: Partial<ChatAggregate>) {
const startTime = Date.now();
try {
await this.repo.update(id, update);
const result = await this.findById(id);
logDatabase('Chat updated successfully', `update(${id})`, Date.now() - startTime, {
chatId: id,
updatedFields: Object.keys(update),
success: !!result
});
return result;
} catch (error) {
logError('ChatRepository.update error', error as Error);
throw new Error('Failed to update chat in database');
}
}
async delete(id: string) {
const startTime = Date.now();
try {
const result = await this.repo.delete(id);
logDatabase('Chat deleted', `delete(${id})`, Date.now() - startTime, {
chatId: id,
affected: result.affected
});
return result;
} catch (error) {
logError('ChatRepository.delete error', error as Error);
throw new Error('Failed to delete chat');
}
}
async softDelete(id: string) {
const startTime = Date.now();
try {
await this.repo.update(id, { state: ChatState.SOFT_DELETE });
const result = await this.findById(id);
logDatabase('Chat soft deleted', `softDelete(${id})`, Date.now() - startTime, {
chatId: id,
success: !!result
});
return result;
} catch (error) {
logError('ChatRepository.softDelete error', error as Error);
throw new Error('Failed to soft delete chat');
}
}
async archiveChat(chat: ChatAggregate) {
const startTime = Date.now();
try {
const archive = new ChatArchiveAggregate();
archive.chatId = chat.id;
archive.archivedMessages = chat.messages;
archive.archivedAt = new Date();
archive.chatType = chat.type;
archive.chatName = chat.name;
archive.gameId = chat.gameId;
archive.participants = chat.users;
const archivedResult = await this.archiveRepo.save(archive);
await this.repo.update(chat.id, {
state: ChatState.ARCHIVE,
messages: [],
archiveDate: new Date()
});
logDatabase('Chat archived successfully', `archiveChat(${chat.id})`, Date.now() - startTime, {
chatId: chat.id,
messageCount: chat.messages.length,
archiveId: archivedResult.id
});
return archivedResult;
} catch (error) {
logError('ChatRepository.archiveChat error', error as Error);
throw new Error('Failed to archive chat');
}
}
async getArchivedChat(chatId: string) {
const startTime = Date.now();
try {
const result = await this.archiveRepo.findOneBy({ chatId });
logDatabase('Archived chat retrieved', `getArchivedChat(${chatId})`, Date.now() - startTime, {
chatId,
found: !!result
});
return result;
} catch (error) {
logError('ChatRepository.getArchivedChat error', error as Error);
throw new Error('Failed to retrieve archived chat');
}
}
async restoreFromArchive(chatId: string) {
const startTime = Date.now();
try {
const archive = await this.archiveRepo.findOneBy({ chatId });
if (!archive) {
return null;
}
// Game chats cannot be restored, only viewed
if (archive.chatType === ChatType.GAME) {
logDatabase('Game chat restore attempt blocked', `restoreFromArchive(${chatId})`, Date.now() - startTime, {
chatId,
chatType: archive.chatType,
blocked: true
});
return null;
}
// Restore messages to the chat
await this.repo.update(chatId, {
state: ChatState.ACTIVE,
messages: archive.archivedMessages,
lastActivity: new Date(),
archiveDate: null
});
const result = await this.findById(chatId);
logDatabase('Chat restored from archive', `restoreFromArchive(${chatId})`, Date.now() - startTime, {
chatId,
messageCount: archive.archivedMessages.length,
success: !!result
});
return result;
} catch (error) {
logError('ChatRepository.restoreFromArchive error', error as Error);
throw new Error('Failed to restore chat from archive');
}
}
}
@@ -0,0 +1,125 @@
import { Repository, Not } from 'typeorm';
import { AppDataSource } from '../ormconfig';
import { ContactAggregate, ContactState } from '../../Domain/Contact/ContactAggregate';
import { IContactRepository } from '../../Domain/IRepository/IContactRepository';
import { logDatabase, logError } from '../../Application/Services/Logger';
export class ContactRepository implements IContactRepository {
private repo: Repository<ContactAggregate>;
constructor() {
this.repo = AppDataSource.getRepository(ContactAggregate);
}
async create(contact: Partial<ContactAggregate>) {
return this.repo.save(contact);
}
async findById(id: string) {
return this.repo
.createQueryBuilder('contact')
.where('contact.id = :id', { id })
.andWhere('contact.state != :softDelete', { softDelete: ContactState.SOFT_DELETE })
.getOne();
}
async findByPage(from: number, to: number): Promise<{ contacts: ContactAggregate[], totalCount: number }> {
const startTime = performance.now();
try {
const limit = to - from + 1;
const offset = from;
// Get total count for pagination
const totalCount = await this.repo.count({
where: {
state: Not(ContactState.SOFT_DELETE)
}
});
// Get paginated results
const contacts = await this.repo
.createQueryBuilder('contact')
.where('contact.state != :softDelete', { softDelete: ContactState.SOFT_DELETE })
.orderBy('contact.createDate', 'DESC')
.limit(limit)
.offset(offset)
.getMany();
const endTime = performance.now();
logDatabase('Contact page query completed', `executionTime: ${Math.round(endTime - startTime)}ms, found: ${contacts.length}, total: ${totalCount}, from: ${from}, to: ${to}`);
return { contacts, totalCount };
} catch (error) {
const endTime = performance.now();
logDatabase('Contact page query failed', `executionTime: ${Math.round(endTime - startTime)}ms, from: ${from}, to: ${to}`);
logError('ContactRepository.findByPage error', error instanceof Error ? error : new Error(String(error)));
throw new Error('Failed to get contacts page from database');
}
}
async findByPageIncludingDeleted(from: number, to: number): Promise<{ contacts: ContactAggregate[], totalCount: number }> {
const startTime = performance.now();
try {
const limit = to - from + 1;
const offset = from;
// Get total count for pagination
const totalCount = await this.repo.count();
// Get paginated results
const contacts = await this.repo
.createQueryBuilder('contact')
.orderBy('contact.createDate', 'DESC')
.limit(limit)
.offset(offset)
.getMany();
const endTime = performance.now();
logDatabase('Contact page query completed (including deleted)', `executionTime: ${Math.round(endTime - startTime)}ms, found: ${contacts.length}, total: ${totalCount}, from: ${from}, to: ${to}`);
return { contacts, totalCount };
} catch (error) {
const endTime = performance.now();
logDatabase('Contact page query failed (including deleted)', `executionTime: ${Math.round(endTime - startTime)}ms, from: ${from}, to: ${to}`);
logError('ContactRepository.findByPageIncludingDeleted error', error instanceof Error ? error : new Error(String(error)));
throw new Error('Failed to get contacts page from database');
}
}
async update(id: string, update: Partial<ContactAggregate>) {
await this.repo.update(id, update);
return this.findById(id);
}
async delete(id: string) {
return this.repo.delete(id);
}
async softDelete(id: string) {
await this.repo.update(id, { state: ContactState.SOFT_DELETE });
return this.findById(id);
}
async findByIdIncludingDeleted(id: string) {
return this.repo.findOneBy({ id }); // Returns contact regardless of state
}
async searchIncludingDeleted(searchTerm: string) {
return this.repo
.createQueryBuilder('contact')
.where('contact.name ILIKE :searchTerm', { searchTerm: `%${searchTerm}%` })
.orWhere('contact.email ILIKE :searchTerm', { searchTerm: `%${searchTerm}%` })
.orWhere('contact.txt ILIKE :searchTerm', { searchTerm: `%${searchTerm}%` })
.getMany();
}
async search(searchTerm: string) {
return this.repo
.createQueryBuilder('contact')
.where('contact.name ILIKE :searchTerm', { searchTerm: `%${searchTerm}%` })
.orWhere('contact.email ILIKE :searchTerm', { searchTerm: `%${searchTerm}%` })
.orWhere('contact.txt ILIKE :searchTerm', { searchTerm: `%${searchTerm}%` })
.andWhere('contact.state != :softDelete', { softDelete: ContactState.SOFT_DELETE })
.getMany();
}
}
@@ -0,0 +1,307 @@
import { Repository, Not } from 'typeorm';
import { AppDataSource } from '../ormconfig';
import { DeckAggregate, State, CType } from '../../Domain/Deck/DeckAggregate';
import { IDeckRepository } from '../../Domain/IRepository/IDeckRepository';
import { logDatabase, logError } from '../../Application/Services/Logger';
import { AdminBypassService } from '../../Application/Services/AdminBypassService';
export class DeckRepository implements IDeckRepository {
private repo: Repository<DeckAggregate>;
constructor() {
this.repo = AppDataSource.getRepository(DeckAggregate);
}
async create(deck: Partial<DeckAggregate>) {
return this.repo.save(deck);
}
async findByPage(from: number, to: number): Promise<{ decks: DeckAggregate[], totalCount: number }> {
const startTime = performance.now();
try {
const limit = to - from + 1;
const offset = from;
// Get total count for pagination
const totalCount = await this.repo.count({
where: { state: Not(State.SOFT_DELETE) }
});
// Get paginated results
const decks = await this.repo.find({
where: { state: Not(State.SOFT_DELETE) },
order: { updatedate: 'DESC' },
take: limit,
skip: offset
});
const endTime = performance.now();
logDatabase('Deck page query completed', `executionTime: ${Math.round(endTime - startTime)}ms, found: ${decks.length}, total: ${totalCount}, from: ${from}, to: ${to}`);
return { decks, totalCount };
} catch (error) {
const endTime = performance.now();
logDatabase('Deck page query failed', `executionTime: ${Math.round(endTime - startTime)}ms, from: ${from}, to: ${to}`);
logError('DeckRepository.findByPage error', error instanceof Error ? error : new Error(String(error)));
throw new Error('Failed to get decks page from database');
}
}
async findByPageIncludingDeleted(from: number, to: number): Promise<{ decks: DeckAggregate[], totalCount: number }> {
const startTime = performance.now();
try {
const limit = to - from + 1;
const offset = from;
// Get total count for pagination
const totalCount = await this.repo.count();
// Get paginated results
const decks = await this.repo.find({
order: { updatedate: 'DESC' },
take: limit,
skip: offset
});
const endTime = performance.now();
logDatabase('Deck page query completed (including deleted)', `executionTime: ${Math.round(endTime - startTime)}ms, found: ${decks.length}, total: ${totalCount}, from: ${from}, to: ${to}`);
return { decks, totalCount };
} catch (error) {
const endTime = performance.now();
logDatabase('Deck page query failed (including deleted)', `executionTime: ${Math.round(endTime - startTime)}ms, from: ${from}, to: ${to}`);
logError('DeckRepository.findByPageIncludingDeleted error', error instanceof Error ? error : new Error(String(error)));
throw new Error('Failed to get decks page from database');
}
}
async findById(id: string) {
return this.repo.findOne({
where: {
id,
state: Not(State.SOFT_DELETE)
}
});
}
async findByIdIncludingDeleted(id: string) {
return this.repo.findOneBy({ id });
}
async update(id: string, update: Partial<DeckAggregate>) {
await this.repo.update(id, update);
return this.findById(id);
}
async delete(id: string) {
return this.repo.delete(id);
}
async softDelete(id: string) {
await this.repo.update(id, { state: State.SOFT_DELETE });
return this.findById(id);
}
async search(query: string, limit: number = 20, offset: number = 0): Promise<{ decks: DeckAggregate[], totalCount: number }> {
const startTime = performance.now();
try {
const searchPattern = `%${query.toLowerCase()}%`;
const queryBuilder = this.repo.createQueryBuilder('deck')
.where('deck.state != :softDelete', { softDelete: State.SOFT_DELETE })
.andWhere('LOWER(deck.name) LIKE :pattern', { pattern: searchPattern });
const totalCount = await queryBuilder.getCount();
const decks = await queryBuilder
.orderBy('deck.name', 'ASC')
.limit(limit)
.offset(offset)
.getMany();
const endTime = performance.now();
logDatabase('Deck search completed', `executionTime: ${Math.round(endTime - startTime)}ms, found: ${decks.length}, total: ${totalCount}, searchTerm: "${query}", limit: ${limit}, offset: ${offset}`);
return { decks, totalCount };
} catch (error) {
const endTime = performance.now();
logDatabase('Deck search failed', `executionTime: ${Math.round(endTime - startTime)}ms, searchTerm: "${query}"`);
logError('DeckRepository.search error', error instanceof Error ? error : new Error(String(error)));
throw new Error('Failed to search decks in database');
}
}
async searchIncludingDeleted(query: string, limit: number = 20, offset: number = 0): Promise<{ decks: DeckAggregate[], totalCount: number }> {
const startTime = performance.now();
try {
const searchPattern = `%${query.toLowerCase()}%`;
const queryBuilder = this.repo.createQueryBuilder('deck')
.where('LOWER(deck.name) LIKE :pattern', { pattern: searchPattern });
const totalCount = await queryBuilder.getCount();
const decks = await queryBuilder
.orderBy('deck.name', 'ASC')
.limit(limit)
.offset(offset)
.getMany();
const endTime = performance.now();
logDatabase('Deck search completed (including deleted)', `executionTime: ${Math.round(endTime - startTime)}ms, found: ${decks.length}, total: ${totalCount}, searchTerm: "${query}", limit: ${limit}, offset: ${offset}`);
return { decks, totalCount };
} catch (error) {
const endTime = performance.now();
logDatabase('Deck search failed (including deleted)', `executionTime: ${Math.round(endTime - startTime)}ms, searchTerm: "${query}"`);
logError('DeckRepository.searchIncludingDeleted error', error instanceof Error ? error : new Error(String(error)));
throw new Error('Failed to search all decks in database');
}
}
/**
* Count active (non-soft-deleted) decks for a specific user
* @param userId - User ID to count decks for
* @returns Number of active decks
*/
async countActiveByUserId(userId: string): Promise<number> {
const startTime = performance.now();
try {
const count = await this.repo.count({
where: {
userid: userId,
state: Not(State.SOFT_DELETE)
}
});
const endTime = performance.now();
logDatabase('User active deck count completed', `executionTime: ${Math.round(endTime - startTime)}ms, userId: ${userId}, count: ${count}`);
return count;
} catch (error) {
const endTime = performance.now();
logDatabase('User active deck count failed', `executionTime: ${Math.round(endTime - startTime)}ms, userId: ${userId}`);
logError('DeckRepository.countActiveByUserId error', error instanceof Error ? error : new Error(String(error)));
throw new Error('Failed to count active decks for user');
}
}
/**
* Count organizational decks for a specific user
* @param userId - User ID to count organizational decks for
* @returns Number of organizational decks
*/
async countOrganizationalByUserId(userId: string): Promise<number> {
const startTime = performance.now();
try {
const count = await this.repo.count({
where: {
userid: userId,
ctype: CType.ORGANIZATION,
state: Not(State.SOFT_DELETE)
}
});
const endTime = performance.now();
logDatabase('User organizational deck count completed', `executionTime: ${Math.round(endTime - startTime)}ms, userId: ${userId}, count: ${count}`);
return count;
} catch (error) {
const endTime = performance.now();
logDatabase('User organizational deck count failed', `executionTime: ${Math.round(endTime - startTime)}ms, userId: ${userId}`);
logError('DeckRepository.countOrganizationalByUserId error', error instanceof Error ? error : new Error(String(error)));
throw new Error('Failed to count organizational decks for user');
}
}
/**
* Find decks with filtering based on user permissions and mandatory pagination
* @param userId - User ID for filtering
* @param userOrgId - User's organization ID (if any)
* @param isAdmin - Whether user is admin (bypasses filtering)
* @param from - Start index for pagination (default: 0)
* @param to - End index for pagination (default: 49)
* @returns Paginated filtered list of decks with total count
*/
async findFilteredDecks(userId: string, userOrgId?: string | null, isAdmin?: boolean, from: number = 0, to: number = 49): Promise<{ decks: DeckAggregate[], totalCount: number }> {
const startTime = performance.now();
try {
// Validate pagination parameters
if (from < 0 || to < from) {
throw new Error('Invalid pagination parameters');
}
const limit = to - from + 1;
if (limit > 100) {
throw new Error('Page size too large. Maximum 100 records per request');
}
const skip = from;
const take = limit;
// Admin gets ALL decks with pagination
if (isAdmin) {
AdminBypassService.logAdminBypass(
'FIND_FILTERED_DECKS_BYPASS',
userId,
'all-decks-filtered',
{
bypassType: 'admin-all-decks-filtered',
userOrgId,
from,
to,
operation: 'read'
}
);
const [decks, totalCount] = await this.repo.findAndCount({
where: { state: Not(State.SOFT_DELETE) },
relations: ['organization'],
order: { creationdate: 'DESC' },
skip,
take
});
const endTime = performance.now();
logDatabase('Admin filtered deck query completed', `executionTime: ${Math.round(endTime - startTime)}ms, userId: ${userId}, found: ${decks.length}, totalCount: ${totalCount}, isAdmin: true`);
return { decks, totalCount };
}
// Regular user complex filtering
const queryBuilder = this.repo.createQueryBuilder('deck')
.leftJoinAndSelect('deck.organization', 'org')
.where('deck.state != :deletedState', { deletedState: State.SOFT_DELETE });
queryBuilder.andWhere('(' +
// User's private decks
'(deck.userid = :userId AND deck.ctype = :privateType) OR ' +
// All public decks
'(deck.ctype = :publicType)' +
// Organization decks from same org (if user has org)
(userOrgId ? ' OR (deck.ctype = :orgType AND org.id = :orgId)' : '') +
')', {
userId,
privateType: CType.PRIVATE,
publicType: CType.PUBLIC,
...(userOrgId && { orgType: CType.ORGANIZATION, orgId: userOrgId })
});
queryBuilder
.orderBy('deck.creationdate', 'DESC')
.skip(skip)
.take(take);
const [decks, totalCount] = await queryBuilder.getManyAndCount();
const endTime = performance.now();
logDatabase('User filtered deck query completed', `executionTime: ${Math.round(endTime - startTime)}ms, userId: ${userId}, userOrgId: ${userOrgId}, found: ${decks.length}, totalCount: ${totalCount}, isAdmin: false`);
return { decks, totalCount };
} catch (error) {
const endTime = performance.now();
logDatabase('Filtered deck query failed', `executionTime: ${Math.round(endTime - startTime)}ms, userId: ${userId}, isAdmin: ${isAdmin}`);
logError('DeckRepository.findFilteredDecks error', error instanceof Error ? error : new Error(String(error)));
throw new Error('Failed to find filtered decks');
}
}
}
@@ -0,0 +1,164 @@
import { Repository, Not } from 'typeorm';
import { AppDataSource } from '../ormconfig';
import { OrganizationAggregate, OrganizationState } from '../../Domain/Organization/OrganizationAggregate';
import { IOrganizationRepository } from '../../Domain/IRepository/IOrganizationRepository';
import { logDatabase, logError } from '../../Application/Services/Logger';
export class OrganizationRepository implements IOrganizationRepository {
private repo: Repository<OrganizationAggregate>;
constructor() {
this.repo = AppDataSource.getRepository(OrganizationAggregate);
}
async create(org: Partial<OrganizationAggregate>) {
return this.repo.save(org);
}
async findByPage(from: number, to: number): Promise<{ organizations: OrganizationAggregate[], totalCount: number }> {
const startTime = performance.now();
try {
const limit = to - from + 1;
const offset = from;
// Get total count for pagination
const totalCount = await this.repo.count({
where: { state: Not(OrganizationState.SOFT_DELETE) }
});
// Get paginated results
const organizations = await this.repo.find({
where: { state: Not(OrganizationState.SOFT_DELETE) },
order: { name: 'ASC' },
take: limit,
skip: offset
});
const endTime = performance.now();
logDatabase('Organization page query completed', `executionTime: ${Math.round(endTime - startTime)}ms, found: ${organizations.length}, total: ${totalCount}, from: ${from}, to: ${to}`);
return { organizations, totalCount };
} catch (error) {
const endTime = performance.now();
logDatabase('Organization page query failed', `executionTime: ${Math.round(endTime - startTime)}ms, from: ${from}, to: ${to}`);
logError('OrganizationRepository.findByPage error', error instanceof Error ? error : new Error(String(error)));
throw new Error('Failed to get organizations page from database');
}
}
async findByPageIncludingDeleted(from: number, to: number): Promise<{ organizations: OrganizationAggregate[], totalCount: number }> {
const startTime = performance.now();
try {
const limit = to - from + 1;
const offset = from;
// Get total count for pagination
const totalCount = await this.repo.count();
// Get paginated results
const organizations = await this.repo.find({
order: { name: 'ASC' },
take: limit,
skip: offset
});
const endTime = performance.now();
logDatabase('Organization page query completed (including deleted)', `executionTime: ${Math.round(endTime - startTime)}ms, found: ${organizations.length}, total: ${totalCount}, from: ${from}, to: ${to}`);
return { organizations, totalCount };
} catch (error) {
const endTime = performance.now();
logDatabase('Organization page query failed (including deleted)', `executionTime: ${Math.round(endTime - startTime)}ms, from: ${from}, to: ${to}`);
logError('OrganizationRepository.findByPageIncludingDeleted error', error instanceof Error ? error : new Error(String(error)));
throw new Error('Failed to get organizations page from database');
}
}
async findById(id: string) {
return this.repo.findOne({
where: {
id,
state: Not(OrganizationState.SOFT_DELETE)
}
});
}
async findByIdIncludingDeleted(id: string) {
return this.repo.findOneBy({ id });
}
async update(id: string, update: Partial<OrganizationAggregate>) {
await this.repo.update(id, update);
return this.findById(id);
}
async delete(id: string) {
return this.repo.delete(id);
}
async softDelete(id: string) {
await this.repo.update(id, { state: OrganizationState.SOFT_DELETE });
return this.findById(id);
}
async search(query: string, limit: number = 20, offset: number = 0): Promise<{ organizations: OrganizationAggregate[], totalCount: number }> {
const startTime = performance.now();
try {
const searchPattern = `%${query.toLowerCase()}%`;
const queryBuilder = this.repo.createQueryBuilder('org')
.where('org.state != :softDelete', { softDelete: OrganizationState.SOFT_DELETE })
.andWhere('(LOWER(org.name) LIKE :pattern OR LOWER(org.contactfname) LIKE :pattern OR LOWER(org.contactlname) LIKE :pattern OR LOWER(org.contactemail) LIKE :pattern OR LOWER(CONCAT(org.contactfname, \' \', org.contactlname)) LIKE :pattern)', { pattern: searchPattern });
const totalCount = await queryBuilder.getCount();
const organizations = await queryBuilder
.orderBy('org.name', 'ASC')
.limit(limit)
.offset(offset)
.getMany();
const endTime = performance.now();
logDatabase('Organization search completed', `executionTime: ${Math.round(endTime - startTime)}ms, found: ${organizations.length}, total: ${totalCount}, searchTerm: "${query}", limit: ${limit}, offset: ${offset}`);
return { organizations, totalCount };
} catch (error) {
const endTime = performance.now();
logDatabase('Organization search failed', `executionTime: ${Math.round(endTime - startTime)}ms, searchTerm: "${query}"`);
logError('OrganizationRepository.search error', error instanceof Error ? error : new Error(String(error)));
throw new Error('Failed to search organizations in database');
}
}
async searchIncludingDeleted(query: string, limit: number = 20, offset: number = 0): Promise<{ organizations: OrganizationAggregate[], totalCount: number }> {
const startTime = performance.now();
try {
const searchPattern = `%${query.toLowerCase()}%`;
const queryBuilder = this.repo.createQueryBuilder('org')
.where('LOWER(org.name) LIKE :pattern', { pattern: searchPattern })
.orWhere('LOWER(org.contactfname) LIKE :pattern', { pattern: searchPattern })
.orWhere('LOWER(org.contactlname) LIKE :pattern', { pattern: searchPattern })
.orWhere('LOWER(org.contactemail) LIKE :pattern', { pattern: searchPattern })
.orWhere('LOWER(CONCAT(org.contactfname, \' \', org.contactlname)) LIKE :pattern', { pattern: searchPattern });
const totalCount = await queryBuilder.getCount();
const organizations = await queryBuilder
.orderBy('org.name', 'ASC')
.limit(limit)
.offset(offset)
.getMany();
const endTime = performance.now();
logDatabase('Organization search completed (including deleted)', `executionTime: ${Math.round(endTime - startTime)}ms, found: ${organizations.length}, total: ${totalCount}, searchTerm: "${query}", limit: ${limit}, offset: ${offset}`);
return { organizations, totalCount };
} catch (error) {
const endTime = performance.now();
logDatabase('Organization search failed (including deleted)', `executionTime: ${Math.round(endTime - startTime)}ms, searchTerm: "${query}"`);
logError('OrganizationRepository.searchIncludingDeleted error', error instanceof Error ? error : new Error(String(error)));
throw new Error('Failed to search all organizations in database');
}
}
}
@@ -0,0 +1,349 @@
import { Repository, Not } from 'typeorm';
import { AppDataSource } from '../ormconfig';
import { UserAggregate, UserState } from '../../Domain/User/UserAggregate';
import { IUserRepository } from '../../Domain/IRepository/IUserRepository';
import { logDatabase, logError } from '../../Application/Services/Logger';
export class UserRepository implements IUserRepository {
private repo: Repository<UserAggregate>;
constructor() {
this.repo = AppDataSource.getRepository(UserAggregate);
}
async create(user: Partial<UserAggregate>) {
const startTime = Date.now();
try {
const result = await this.repo.save(user);
logDatabase('User created successfully', undefined, Date.now() - startTime, {
userId: result.id,
username: user.username,
email: user.email
});
return result;
} catch (error) {
logError('UserRepository.create error', error as Error);
// Handle unique constraint violations
if (error instanceof Error && (error.message.includes('duplicate') || error.message.includes('unique'))) {
throw new Error('User with this username or email already exists');
}
throw new Error('Failed to create user in database');
}
}
async findByPage(from: number, to: number): Promise<{ users: UserAggregate[], totalCount: number }> {
const startTime = Date.now();
try {
const limit = to - from + 1;
const offset = from;
// Get total count for pagination
const totalCount = await this.repo.count({
where: { state: Not(UserState.SOFT_DELETE) }
});
// Get paginated results
const users = await this.repo.find({
where: { state: Not(UserState.SOFT_DELETE) },
order: { regdate: 'DESC' },
take: limit,
skip: offset
});
logDatabase('User page query completed', `from: ${from}, to: ${to}`, Date.now() - startTime, {
found: users.length,
total: totalCount
});
return { users, totalCount };
} catch (error) {
logError('UserRepository.findByPage error', error as Error);
throw new Error('Failed to get users page from database');
}
}
async findByPageIncludingDeleted(from: number, to: number): Promise<{ users: UserAggregate[], totalCount: number }> {
const startTime = Date.now();
try {
const limit = to - from + 1;
const offset = from;
// Get total count for pagination
const totalCount = await this.repo.count();
// Get paginated results
const users = await this.repo.find({
order: { regdate: 'DESC' },
take: limit,
skip: offset
});
logDatabase('User page query completed (including deleted)', `from: ${from}, to: ${to}`, Date.now() - startTime, {
found: users.length,
total: totalCount
});
return { users, totalCount };
} catch (error) {
logError('UserRepository.findByPageIncludingDeleted error', error as Error);
throw new Error('Failed to get users page from database');
}
}
async findById(id: string) {
const startTime = Date.now();
try {
const result = await this.repo.findOne({
where: {
id,
state: Not(UserState.SOFT_DELETE)
}
});
logDatabase('User findById query completed', `findOneBy({ id: ${id} })`, Date.now() - startTime, {
found: !!result,
userId: id
});
return result;
} catch (error) {
logError('UserRepository.findById error', error as Error);
if (error instanceof Error && error.message.includes('invalid input syntax for type uuid')) {
return null;
}
throw new Error('Failed to retrieve user from database');
}
}
async findByIdIncludingDeleted(id: string) {
const startTime = Date.now();
try {
const result = await this.repo.findOneBy({ id });
logDatabase('User findByIdIncludingDeleted query completed', `findOneBy({ id: ${id} })`, Date.now() - startTime, {
found: !!result,
userId: id
});
return result;
} catch (error) {
logError('UserRepository.findByIdIncludingDeleted error', error as Error);
if (error instanceof Error && error.message.includes('invalid input syntax for type uuid')) {
return null;
}
throw new Error('Failed to retrieve user from database');
}
}
async findByUsername(username: string) {
const startTime = Date.now();
try {
const result = await this.repo.findOneBy({ username });
logDatabase('User findByUsername query completed', `findOneBy({ username: ${username} })`, Date.now() - startTime, {
found: !!result,
username
});
return result;
} catch (error) {
logError('UserRepository.findByUsername error', error as Error);
throw new Error('Failed to retrieve user by username from database');
}
}
async findByEmail(email: string) {
const startTime = Date.now();
try {
const result = await this.repo.findOneBy({ email });
logDatabase('User findByEmail query completed', `findOneBy({ email: ${email} })`, Date.now() - startTime, {
found: !!result,
email
});
return result;
} catch (error) {
logError('UserRepository.findByEmail error', error as Error);
throw new Error('Failed to retrieve user by email from database');
}
}
async findByToken(token: string) {
const startTime = Date.now();
try {
const result = await this.repo.findOneBy({ token: token });
logDatabase('User findByToken query completed', `findOneBy({ token })`, Date.now() - startTime, {
found: !!result,
tokenPrefix: token.substring(0, 8) + '...'
});
return result;
} catch (error) {
logError('UserRepository.findByToken error', error as Error);
throw new Error('Failed to retrieve user by token from database');
}
}
async update(id: string, update: Partial<UserAggregate>) {
const startTime = Date.now();
try {
await this.repo.update(id, update);
const result = await this.findById(id);
logDatabase('User updated successfully', `update(${id})`, Date.now() - startTime, {
userId: id,
updatedFields: Object.keys(update),
success: !!result
});
return result;
} catch (error) {
logError('UserRepository.update error', error as Error);
// Handle unique constraint violations
if (error instanceof Error && (error.message.includes('duplicate') || error.message.includes('unique'))) {
throw new Error('Username or email already exists');
}
// Handle invalid UUID format
if (error instanceof Error && error.message.includes('invalid input syntax for type uuid')) {
throw new Error('Invalid user ID format');
}
throw new Error('Failed to update user in database');
}
}
async delete(id: string) {
const startTime = Date.now();
try {
const result = await this.repo.delete(id);
logDatabase('User deleted successfully', `delete(${id})`, Date.now() - startTime, {
userId: id,
affected: result.affected
});
return result;
} catch (error) {
logError('UserRepository.delete error', error as Error);
// Handle invalid UUID format
if (error instanceof Error && error.message.includes('invalid input syntax for type uuid')) {
throw new Error('Invalid user ID format');
}
throw new Error('Failed to delete user from database');
}
}
async softDelete(id: string) {
const startTime = Date.now();
try {
await this.repo.update(id, { state: UserState.SOFT_DELETE });
const result = await this.findById(id);
logDatabase('User soft deleted successfully', `update(${id}, { state: SOFT_DELETE })`, Date.now() - startTime, {
userId: id,
success: !!result
});
return result;
} catch (error) {
logError('UserRepository.softDelete error', error as Error);
// Handle invalid UUID format
if (error instanceof Error && error.message.includes('invalid input syntax for type uuid')) {
throw new Error('Invalid user ID format');
}
throw new Error('Failed to soft delete user in database');
}
}
async deactivate(id: string) {
const startTime = Date.now();
try {
await this.repo.update(id, { state: UserState.DEACTIVATED });
const result = await this.findById(id);
logDatabase('User deactivated successfully', `update(${id}, { state: DEACTIVATED })`, Date.now() - startTime, {
userId: id,
success: !!result
});
return result;
} catch (error) {
logError('UserRepository.deactivate error', error as Error);
// Handle invalid UUID format
if (error instanceof Error && error.message.includes('invalid input syntax for type uuid')) {
throw new Error('Invalid user ID format');
}
throw new Error('Failed to deactivate user in database');
}
}
async search(query: string, limit: number = 20, offset: number = 0): Promise<{ users: UserAggregate[], totalCount: number }> {
const startTime = Date.now();
try {
const searchPattern = `%${query.toLowerCase()}%`;
const queryBuilder = this.repo.createQueryBuilder('user')
.where('user.state != :softDelete', { softDelete: UserState.SOFT_DELETE })
.andWhere('(LOWER(user.username) LIKE :pattern OR LOWER(user.email) LIKE :pattern OR LOWER(user.fname) LIKE :pattern OR LOWER(user.lname) LIKE :pattern OR LOWER(CONCAT(user.fname, \' \', user.lname)) LIKE :pattern)', { pattern: searchPattern });
const totalCount = await queryBuilder.getCount();
const users = await queryBuilder
.orderBy('user.username', 'ASC')
.limit(limit)
.offset(offset)
.getMany();
logDatabase('User search completed',
`search query: ${query.substring(0, 50)}...`,
Date.now() - startTime, {
query,
limit,
offset,
totalCount,
returnedCount: users.length
});
return { users, totalCount };
} catch (error) {
logError('UserRepository.search error', error as Error);
throw new Error('Failed to search users in database');
}
}
async searchIncludingDeleted(query: string, limit: number = 20, offset: number = 0): Promise<{ users: UserAggregate[], totalCount: number }> {
const startTime = Date.now();
try {
const searchPattern = `%${query.toLowerCase()}%`;
const queryBuilder = this.repo.createQueryBuilder('user')
.where('LOWER(user.username) LIKE :pattern', { pattern: searchPattern })
.orWhere('LOWER(user.email) LIKE :pattern', { pattern: searchPattern })
.orWhere('LOWER(user.fname) LIKE :pattern', { pattern: searchPattern })
.orWhere('LOWER(user.lname) LIKE :pattern', { pattern: searchPattern })
.orWhere('LOWER(CONCAT(user.fname, \' \', user.lname)) LIKE :pattern', { pattern: searchPattern });
const totalCount = await queryBuilder.getCount();
const users = await queryBuilder
.orderBy('user.username', 'ASC')
.limit(limit)
.offset(offset)
.getMany();
logDatabase('User search completed (including deleted)',
`search query: ${query.substring(0, 50)}...`,
Date.now() - startTime, {
query,
limit,
offset,
totalCount,
returnedCount: users.length
});
return { users, totalCount };
} catch (error) {
logError('UserRepository.searchIncludingDeleted error', error as Error);
throw new Error('Failed to search all users in database');
}
}
}