Backend Complete: Interface Refactoring & Service Container Enhancements
Repository Interface Optimization: - Created IBaseRepository.ts and IPaginatedRepository.ts - Refactored all 7 repository interfaces to extend base interfaces - Eliminated ~200 lines of redundant code (70% reduction) - Improved type safety and maintainability Dependency Injection Improvements: - Added EmailService and GameTokenService to DIContainer - Updated CreateUserCommandHandler constructor for DI - Updated RequestPasswordResetCommandHandler constructor for DI - Enhanced testability and service consistency Environment Configuration: - Created comprehensive .env.example with 40+ variables - Organized into 12 logical sections (Database, Security, Email, etc.) - Added security guidelines and best practices - Documented all backend environment requirements Documentation: - Added comprehensive codebase review - Created refactoring summary report - Added frontend implementation guide Impact: Improved code quality, reduced maintenance overhead, enhanced developer experience
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class Full1757939815984 implements MigrationInterface {
|
||||
name = 'Full1757939815984'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`CREATE TABLE "Chats" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "type" character varying(50) NOT NULL DEFAULT 'direct', "name" character varying(255), "gameId" uuid, "createdBy" uuid, "users" uuid array NOT NULL, "messages" json NOT NULL DEFAULT '[]', "lastActivity" TIMESTAMP, "createDate" TIMESTAMP NOT NULL DEFAULT now(), "updateDate" TIMESTAMP NOT NULL DEFAULT now(), "state" integer NOT NULL DEFAULT '0', "archiveDate" TIMESTAMP, CONSTRAINT "PK_64c36c2b8d86a0d5de4cf64de8d" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE TABLE "Users" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "orgid" uuid, "username" character varying(100) NOT NULL, "password" character varying(255) NOT NULL, "email" character varying(255) NOT NULL, "fname" character varying(100) NOT NULL, "lname" character varying(100) NOT NULL, "token" character varying(255), "TokenExpires" TIMESTAMP, "phone" character varying(20), "state" integer NOT NULL DEFAULT '0', "regdate" TIMESTAMP NOT NULL DEFAULT now(), "updatedate" TIMESTAMP NOT NULL DEFAULT now(), "Orglogindate" TIMESTAMP, CONSTRAINT "UQ_ffc81a3b97dcbf8e320d5106c0d" UNIQUE ("username"), CONSTRAINT "UQ_3c3ab3f49a87e6ddb607f3c4945" UNIQUE ("email"), CONSTRAINT "PK_16d4f7d636df336db11d87413e3" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE TABLE "Contacts" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying(255) NOT NULL, "email" character varying(255) NOT NULL, "userid" uuid, "type" integer NOT NULL, "txt" text NOT NULL, "state" integer NOT NULL DEFAULT '0', "createDate" TIMESTAMP NOT NULL DEFAULT now(), "updateDate" TIMESTAMP NOT NULL DEFAULT now(), "adminResponse" text, "responseDate" TIMESTAMP, "respondedBy" uuid, CONSTRAINT "PK_68782cec65c8eef577c62958273" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE TABLE "ChatArchives" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "chatId" uuid NOT NULL, "archivedMessages" json NOT NULL, "archivedAt" TIMESTAMP NOT NULL, "createDate" TIMESTAMP NOT NULL DEFAULT now(), "chatType" character varying(50) NOT NULL, "chatName" character varying(255), "gameId" uuid, "participants" uuid array NOT NULL, CONSTRAINT "PK_fe62979fc2061d7afe278d3f14e" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE TABLE "Games" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "gamecode" character varying(10) NOT NULL, "maxplayers" integer NOT NULL, "logintype" integer NOT NULL DEFAULT '0', "createdby" character varying(255), "orgid" character varying(255), "gamedecks" json NOT NULL, "players" json NOT NULL DEFAULT '[]', "started" boolean NOT NULL DEFAULT false, "finished" boolean NOT NULL DEFAULT false, "winner" character varying(255), "state" integer NOT NULL DEFAULT '0', "create_date" TIMESTAMP NOT NULL DEFAULT now(), "start_date" TIMESTAMP, "end_date" TIMESTAMP, "update_date" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "UQ_9d52c646079cbe6f242a85c5c41" UNIQUE ("gamecode"), CONSTRAINT "PK_1950492f583d31609c5e9fbbe12" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE TABLE "Organizations" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying(255) NOT NULL, "contactfname" character varying(100) NOT NULL, "contactlname" character varying(100) NOT NULL, "contactphone" character varying(20) NOT NULL, "contactemail" character varying(255) NOT NULL, "state" integer NOT NULL DEFAULT '0', "regdate" TIMESTAMP NOT NULL DEFAULT now(), "updatedate" TIMESTAMP NOT NULL DEFAULT now(), "url" character varying(500), "userinorg" integer NOT NULL DEFAULT '0', "maxOrganizationalDecks" integer, CONSTRAINT "PK_e0690a31419f6666194423526f2" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE TABLE "Decks" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying(255) NOT NULL, "type" integer NOT NULL, "user_id" uuid NOT NULL, "creation_date" TIMESTAMP NOT NULL DEFAULT now(), "cards" json NOT NULL, "played_number" integer NOT NULL DEFAULT '0', "ctype" integer NOT NULL DEFAULT '0', "update_date" TIMESTAMP NOT NULL DEFAULT now(), "state" integer NOT NULL DEFAULT '0', "organization_id" uuid, CONSTRAINT "PK_001f26cb3ec39c1f25269943473" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`ALTER TABLE "Decks" ADD CONSTRAINT "FK_06ee28f90d68543a03b14aebe13" FOREIGN KEY ("organization_id") REFERENCES "Organizations"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "Decks" DROP CONSTRAINT "FK_06ee28f90d68543a03b14aebe13"`);
|
||||
await queryRunner.query(`DROP TABLE "Decks"`);
|
||||
await queryRunner.query(`DROP TABLE "Organizations"`);
|
||||
await queryRunner.query(`DROP TABLE "Games"`);
|
||||
await queryRunner.query(`DROP TABLE "ChatArchives"`);
|
||||
await queryRunner.query(`DROP TABLE "Contacts"`);
|
||||
await queryRunner.query(`DROP TABLE "Users"`);
|
||||
await queryRunner.query(`DROP TABLE "Chats"`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class Full1757939815062 implements MigrationInterface {
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.findByIdIncludingDeleted(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,419 @@
|
||||
import { Repository, Not, In } from 'typeorm';
|
||||
import { AppDataSource } from '../ormconfig';
|
||||
import { GameAggregate, GameState } from '../../Domain/Game/GameAggregate';
|
||||
import { IGameRepository } from '../../Domain/IRepository/IGameRepository';
|
||||
import { logDatabase, logError } from '../../Application/Services/Logger';
|
||||
|
||||
export class GameRepository implements IGameRepository {
|
||||
private repo: Repository<GameAggregate>;
|
||||
constructor() {
|
||||
this.repo = AppDataSource.getRepository(GameAggregate);
|
||||
}
|
||||
|
||||
async create(game: Partial<GameAggregate>): Promise<GameAggregate> {
|
||||
const startTime = performance.now();
|
||||
try {
|
||||
const result = await this.repo.save(game);
|
||||
const endTime = performance.now();
|
||||
logDatabase('Game created', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${result.id}, gameCode: ${result.gamecode}`);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const endTime = performance.now();
|
||||
logDatabase('Game creation failed', `executionTime: ${Math.round(endTime - startTime)}ms`);
|
||||
logError('GameRepository.create error', error instanceof Error ? error : new Error(String(error)));
|
||||
throw new Error('Failed to create game in database');
|
||||
}
|
||||
}
|
||||
|
||||
async findByPage(from: number, to: number): Promise<{ games: GameAggregate[], 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(GameState.CANCELLED) }
|
||||
});
|
||||
|
||||
// Get paginated results
|
||||
const games = await this.repo.find({
|
||||
where: { state: Not(GameState.CANCELLED) },
|
||||
order: { updatedate: 'DESC' },
|
||||
take: limit,
|
||||
skip: offset
|
||||
});
|
||||
|
||||
const endTime = performance.now();
|
||||
logDatabase('Game page query completed', `executionTime: ${Math.round(endTime - startTime)}ms, found: ${games.length}, total: ${totalCount}, from: ${from}, to: ${to}`);
|
||||
|
||||
return { games, totalCount };
|
||||
} catch (error) {
|
||||
const endTime = performance.now();
|
||||
logDatabase('Game page query failed', `executionTime: ${Math.round(endTime - startTime)}ms, from: ${from}, to: ${to}`);
|
||||
logError('GameRepository.findByPage error', error instanceof Error ? error : new Error(String(error)));
|
||||
throw new Error('Failed to get games page from database');
|
||||
}
|
||||
}
|
||||
|
||||
async findByPageIncludingDeleted(from: number, to: number): Promise<{ games: GameAggregate[], totalCount: number }> {
|
||||
const startTime = performance.now();
|
||||
try {
|
||||
const limit = to - from + 1;
|
||||
const offset = from;
|
||||
|
||||
// Get total count for pagination (including deleted)
|
||||
const totalCount = await this.repo.count();
|
||||
|
||||
// Get paginated results (including deleted)
|
||||
const games = await this.repo.find({
|
||||
order: { updatedate: 'DESC' },
|
||||
take: limit,
|
||||
skip: offset
|
||||
});
|
||||
|
||||
const endTime = performance.now();
|
||||
logDatabase('Game page query (including deleted) completed', `executionTime: ${Math.round(endTime - startTime)}ms, found: ${games.length}, total: ${totalCount}, from: ${from}, to: ${to}`);
|
||||
|
||||
return { games, totalCount };
|
||||
} catch (error) {
|
||||
const endTime = performance.now();
|
||||
logDatabase('Game page query (including deleted) failed', `executionTime: ${Math.round(endTime - startTime)}ms, from: ${from}, to: ${to}`);
|
||||
logError('GameRepository.findByPageIncludingDeleted error', error instanceof Error ? error : new Error(String(error)));
|
||||
throw new Error('Failed to get games page (including deleted) from database');
|
||||
}
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<GameAggregate | null> {
|
||||
const startTime = performance.now();
|
||||
try {
|
||||
const result = await this.repo.findOne({
|
||||
where: { id, state: Not(GameState.CANCELLED) }
|
||||
});
|
||||
const endTime = performance.now();
|
||||
logDatabase('Game findById completed', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${id}, found: ${!!result}`);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const endTime = performance.now();
|
||||
logDatabase('Game findById failed', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${id}`);
|
||||
logError('GameRepository.findById error', error instanceof Error ? error : new Error(String(error)));
|
||||
throw new Error('Failed to find game by id in database');
|
||||
}
|
||||
}
|
||||
|
||||
async findByIdIncludingDeleted(id: string): Promise<GameAggregate | null> {
|
||||
const startTime = performance.now();
|
||||
try {
|
||||
const result = await this.repo.findOne({
|
||||
where: { id }
|
||||
});
|
||||
const endTime = performance.now();
|
||||
logDatabase('Game findByIdIncludingDeleted completed', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${id}, found: ${!!result}`);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const endTime = performance.now();
|
||||
logDatabase('Game findByIdIncludingDeleted failed', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${id}`);
|
||||
logError('GameRepository.findByIdIncludingDeleted error', error instanceof Error ? error : new Error(String(error)));
|
||||
throw new Error('Failed to find game by id (including deleted) in database');
|
||||
}
|
||||
}
|
||||
|
||||
async findByGameCode(gamecode: string): Promise<GameAggregate | null> {
|
||||
const startTime = performance.now();
|
||||
try {
|
||||
const result = await this.repo.findOne({
|
||||
where: { gamecode, state: Not(GameState.CANCELLED) }
|
||||
});
|
||||
const endTime = performance.now();
|
||||
logDatabase('Game findByGameCode completed', `executionTime: ${Math.round(endTime - startTime)}ms, gameCode: ${gamecode}, found: ${!!result}`);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const endTime = performance.now();
|
||||
logDatabase('Game findByGameCode failed', `executionTime: ${Math.round(endTime - startTime)}ms, gameCode: ${gamecode}`);
|
||||
logError('GameRepository.findByGameCode error', error instanceof Error ? error : new Error(String(error)));
|
||||
throw new Error('Failed to find game by game code in database');
|
||||
}
|
||||
}
|
||||
|
||||
async search(query: string, limit?: number, offset?: number): Promise<{ games: GameAggregate[], totalCount: number }> {
|
||||
const startTime = performance.now();
|
||||
try {
|
||||
const queryBuilder = this.repo.createQueryBuilder('game')
|
||||
.where('game.state != :cancelledState', { cancelledState: GameState.CANCELLED })
|
||||
.andWhere('(game.gamecode ILIKE :query)', { query: `%${query}%` });
|
||||
|
||||
// Get total count
|
||||
const totalCount = await queryBuilder.getCount();
|
||||
|
||||
// Apply pagination if provided
|
||||
if (limit !== undefined) {
|
||||
queryBuilder.take(limit);
|
||||
}
|
||||
if (offset !== undefined) {
|
||||
queryBuilder.skip(offset);
|
||||
}
|
||||
|
||||
const games = await queryBuilder.orderBy('game.updatedate', 'DESC').getMany();
|
||||
|
||||
const endTime = performance.now();
|
||||
logDatabase('Game search completed', `executionTime: ${Math.round(endTime - startTime)}ms, query: ${query}, found: ${games.length}, total: ${totalCount}`);
|
||||
|
||||
return { games, totalCount };
|
||||
} catch (error) {
|
||||
const endTime = performance.now();
|
||||
logDatabase('Game search failed', `executionTime: ${Math.round(endTime - startTime)}ms, query: ${query}`);
|
||||
logError('GameRepository.search error', error instanceof Error ? error : new Error(String(error)));
|
||||
throw new Error('Failed to search games in database');
|
||||
}
|
||||
}
|
||||
|
||||
async searchIncludingDeleted(query: string, limit?: number, offset?: number): Promise<{ games: GameAggregate[], totalCount: number }> {
|
||||
const startTime = performance.now();
|
||||
try {
|
||||
const queryBuilder = this.repo.createQueryBuilder('game')
|
||||
.where('(game.gamecode ILIKE :query)', { query: `%${query}%` });
|
||||
|
||||
// Get total count
|
||||
const totalCount = await queryBuilder.getCount();
|
||||
|
||||
// Apply pagination if provided
|
||||
if (limit !== undefined) {
|
||||
queryBuilder.take(limit);
|
||||
}
|
||||
if (offset !== undefined) {
|
||||
queryBuilder.skip(offset);
|
||||
}
|
||||
|
||||
const games = await queryBuilder.orderBy('game.updatedate', 'DESC').getMany();
|
||||
|
||||
const endTime = performance.now();
|
||||
logDatabase('Game search (including deleted) completed', `executionTime: ${Math.round(endTime - startTime)}ms, query: ${query}, found: ${games.length}, total: ${totalCount}`);
|
||||
|
||||
return { games, totalCount };
|
||||
} catch (error) {
|
||||
const endTime = performance.now();
|
||||
logDatabase('Game search (including deleted) failed', `executionTime: ${Math.round(endTime - startTime)}ms, query: ${query}`);
|
||||
logError('GameRepository.searchIncludingDeleted error', error instanceof Error ? error : new Error(String(error)));
|
||||
throw new Error('Failed to search games (including deleted) in database');
|
||||
}
|
||||
}
|
||||
|
||||
async update(id: string, update: Partial<GameAggregate>): Promise<GameAggregate | null> {
|
||||
const startTime = performance.now();
|
||||
try {
|
||||
await this.repo.update(id, update);
|
||||
const result = await this.findById(id);
|
||||
const endTime = performance.now();
|
||||
logDatabase('Game update completed', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${id}, updated: ${!!result}`);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const endTime = performance.now();
|
||||
logDatabase('Game update failed', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${id}`);
|
||||
logError('GameRepository.update error', error instanceof Error ? error : new Error(String(error)));
|
||||
throw new Error('Failed to update game in database');
|
||||
}
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<any> {
|
||||
const startTime = performance.now();
|
||||
try {
|
||||
const result = await this.repo.delete(id);
|
||||
const endTime = performance.now();
|
||||
logDatabase('Game delete completed', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${id}, affected: ${result.affected}`);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const endTime = performance.now();
|
||||
logDatabase('Game delete failed', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${id}`);
|
||||
logError('GameRepository.delete error', error instanceof Error ? error : new Error(String(error)));
|
||||
throw new Error('Failed to delete game from database');
|
||||
}
|
||||
}
|
||||
|
||||
async softDelete(id: string): Promise<GameAggregate | null> {
|
||||
const startTime = performance.now();
|
||||
try {
|
||||
await this.repo.update(id, { state: GameState.CANCELLED });
|
||||
const result = await this.findByIdIncludingDeleted(id);
|
||||
const endTime = performance.now();
|
||||
logDatabase('Game soft delete completed', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${id}, updated: ${!!result}`);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const endTime = performance.now();
|
||||
logDatabase('Game soft delete failed', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${id}`);
|
||||
logError('GameRepository.softDelete error', error instanceof Error ? error : new Error(String(error)));
|
||||
throw new Error('Failed to soft delete game in database');
|
||||
}
|
||||
}
|
||||
|
||||
// Game-specific methods
|
||||
async findActiveGames(): Promise<GameAggregate[]> {
|
||||
const startTime = performance.now();
|
||||
try {
|
||||
const games = await this.repo.find({
|
||||
where: { state: GameState.ACTIVE },
|
||||
order: { updatedate: 'DESC' }
|
||||
});
|
||||
const endTime = performance.now();
|
||||
logDatabase('Active games query completed', `executionTime: ${Math.round(endTime - startTime)}ms, found: ${games.length}`);
|
||||
return games;
|
||||
} catch (error) {
|
||||
const endTime = performance.now();
|
||||
logDatabase('Active games query failed', `executionTime: ${Math.round(endTime - startTime)}ms`);
|
||||
logError('GameRepository.findActiveGames error', error instanceof Error ? error : new Error(String(error)));
|
||||
throw new Error('Failed to find active games in database');
|
||||
}
|
||||
}
|
||||
|
||||
async findGamesByPlayer(playerId: string): Promise<GameAggregate[]> {
|
||||
const startTime = performance.now();
|
||||
try {
|
||||
const queryBuilder = this.repo.createQueryBuilder('game')
|
||||
.where('game.state != :cancelledState', { cancelledState: GameState.CANCELLED })
|
||||
.andWhere('JSON_CONTAINS(game.players, :playerId)', { playerId: `"${playerId}"` })
|
||||
.orderBy('game.updatedate', 'DESC');
|
||||
|
||||
const games = await queryBuilder.getMany();
|
||||
const endTime = performance.now();
|
||||
logDatabase('Games by player query completed', `executionTime: ${Math.round(endTime - startTime)}ms, playerId: ${playerId}, found: ${games.length}`);
|
||||
return games;
|
||||
} catch (error) {
|
||||
const endTime = performance.now();
|
||||
logDatabase('Games by player query failed', `executionTime: ${Math.round(endTime - startTime)}ms, playerId: ${playerId}`);
|
||||
logError('GameRepository.findGamesByPlayer error', error instanceof Error ? error : new Error(String(error)));
|
||||
throw new Error('Failed to find games by player in database');
|
||||
}
|
||||
}
|
||||
|
||||
async findWaitingGames(): Promise<GameAggregate[]> {
|
||||
const startTime = performance.now();
|
||||
try {
|
||||
const games = await this.repo.find({
|
||||
where: { state: GameState.WAITING },
|
||||
order: { createdate: 'ASC' }
|
||||
});
|
||||
const endTime = performance.now();
|
||||
logDatabase('Waiting games query completed', `executionTime: ${Math.round(endTime - startTime)}ms, found: ${games.length}`);
|
||||
return games;
|
||||
} catch (error) {
|
||||
const endTime = performance.now();
|
||||
logDatabase('Waiting games query failed', `executionTime: ${Math.round(endTime - startTime)}ms`);
|
||||
logError('GameRepository.findWaitingGames error', error instanceof Error ? error : new Error(String(error)));
|
||||
throw new Error('Failed to find waiting games in database');
|
||||
}
|
||||
}
|
||||
|
||||
async findFinishedGames(from?: number, to?: number): Promise<{ games: GameAggregate[], totalCount: number }> {
|
||||
const startTime = performance.now();
|
||||
try {
|
||||
const queryBuilder = this.repo.createQueryBuilder('game')
|
||||
.where('game.state = :finishedState', { finishedState: GameState.FINISHED })
|
||||
.orderBy('game.enddate', 'DESC');
|
||||
|
||||
// Get total count
|
||||
const totalCount = await queryBuilder.getCount();
|
||||
|
||||
// Apply pagination if provided
|
||||
if (from !== undefined && to !== undefined) {
|
||||
const limit = to - from + 1;
|
||||
const offset = from;
|
||||
queryBuilder.take(limit).skip(offset);
|
||||
}
|
||||
|
||||
const games = await queryBuilder.getMany();
|
||||
const endTime = performance.now();
|
||||
logDatabase('Finished games query completed', `executionTime: ${Math.round(endTime - startTime)}ms, found: ${games.length}, total: ${totalCount}`);
|
||||
return { games, totalCount };
|
||||
} catch (error) {
|
||||
const endTime = performance.now();
|
||||
logDatabase('Finished games query failed', `executionTime: ${Math.round(endTime - startTime)}ms`);
|
||||
logError('GameRepository.findFinishedGames error', error instanceof Error ? error : new Error(String(error)));
|
||||
throw new Error('Failed to find finished games in database');
|
||||
}
|
||||
}
|
||||
|
||||
async addPlayerToGame(gameId: string, playerId: string): Promise<GameAggregate | null> {
|
||||
const startTime = performance.now();
|
||||
try {
|
||||
const game = await this.findById(gameId);
|
||||
if (!game) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if player is already in the game
|
||||
if (game.players.includes(playerId)) {
|
||||
return game;
|
||||
}
|
||||
|
||||
// Check if game is full
|
||||
if (game.players.length >= game.maxplayers) {
|
||||
throw new Error('Game is full');
|
||||
}
|
||||
|
||||
const updatedPlayers = [...game.players, playerId];
|
||||
const result = await this.update(gameId, { players: updatedPlayers });
|
||||
|
||||
const endTime = performance.now();
|
||||
logDatabase('Player added to game', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${gameId}, playerId: ${playerId}`);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const endTime = performance.now();
|
||||
logDatabase('Add player to game failed', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${gameId}, playerId: ${playerId}`);
|
||||
logError('GameRepository.addPlayerToGame error', error instanceof Error ? error : new Error(String(error)));
|
||||
throw new Error('Failed to add player to game in database');
|
||||
}
|
||||
}
|
||||
|
||||
async removePlayerFromGame(gameId: string, playerId: string): Promise<GameAggregate | null> {
|
||||
const startTime = performance.now();
|
||||
try {
|
||||
const game = await this.findById(gameId);
|
||||
if (!game) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const updatedPlayers = game.players.filter(id => id !== playerId);
|
||||
const result = await this.update(gameId, { players: updatedPlayers });
|
||||
|
||||
const endTime = performance.now();
|
||||
logDatabase('Player removed from game', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${gameId}, playerId: ${playerId}`);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const endTime = performance.now();
|
||||
logDatabase('Remove player from game failed', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${gameId}, playerId: ${playerId}`);
|
||||
logError('GameRepository.removePlayerFromGame error', error instanceof Error ? error : new Error(String(error)));
|
||||
throw new Error('Failed to remove player from game in database');
|
||||
}
|
||||
}
|
||||
|
||||
async updateGameState(gameId: string, started: boolean, finished?: boolean, winner?: string): Promise<GameAggregate | null> {
|
||||
const startTime = performance.now();
|
||||
try {
|
||||
const updateData: Partial<GameAggregate> = { started };
|
||||
|
||||
if (started && !finished) {
|
||||
updateData.state = GameState.ACTIVE;
|
||||
updateData.startdate = new Date();
|
||||
}
|
||||
|
||||
if (finished) {
|
||||
updateData.finished = true;
|
||||
updateData.state = GameState.FINISHED;
|
||||
updateData.enddate = new Date();
|
||||
if (winner) {
|
||||
updateData.winner = winner;
|
||||
}
|
||||
}
|
||||
|
||||
const result = await this.update(gameId, updateData);
|
||||
|
||||
const endTime = performance.now();
|
||||
logDatabase('Game state updated', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${gameId}, started: ${started}, finished: ${finished}, winner: ${winner}`);
|
||||
return result;
|
||||
} catch (error) {
|
||||
const endTime = performance.now();
|
||||
logDatabase('Game state update failed', `executionTime: ${Math.round(endTime - startTime)}ms, gameId: ${gameId}`);
|
||||
logError('GameRepository.updateGameState error', error instanceof Error ? error : new Error(String(error)));
|
||||
throw new Error('Failed to update game state in database');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { DataSource } from 'typeorm';
|
||||
import { join } from 'path';
|
||||
|
||||
export const AppDataSource = new DataSource({
|
||||
type: 'postgres',
|
||||
host: process.env.DB_HOST || 'localhost',
|
||||
port: parseInt(process.env.DB_PORT || '5432'),
|
||||
username: process.env.DB_USERNAME || 'postgres',
|
||||
password: process.env.DB_PASSWORD || 'postgres',
|
||||
database: process.env.DB_NAME || 'serpentrace',
|
||||
synchronize: false, // Set to false when using migrations
|
||||
logging: process.env.NODE_ENV === 'development',
|
||||
entities: [join(__dirname, '../Domain/**/*Aggregate.ts')],
|
||||
migrations: [join(__dirname, './Migrations/*.ts')],
|
||||
migrationsTableName: 'migrations',
|
||||
migrationsRun: false // Let migrations run manually
|
||||
});
|
||||
Reference in New Issue
Block a user