backend v4 half

This commit is contained in:
2025-07-18 09:20:40 +02:00
parent aba7a506ad
commit 725516ad6c
4183 changed files with 217684 additions and 75056 deletions
@@ -0,0 +1,289 @@
import { IsString, IsOptional, IsNumber, IsDate, IsEnum, Length, IsBoolean } from 'class-validator';
import { Type } from 'class-transformer';
import { Privacy, CardType } from '../entities/cards.entity';
// Cards Creation DTO
export class CreateCardsDto {
@IsString()
@Length(1, 255)
Title!: string;
@IsString()
Description!: string;
@IsEnum(Privacy)
Privacy!: Privacy;
@IsEnum(CardType)
Type!: CardType;
@IsNumber()
Creator!: number;
@IsNumber()
no_question!: number;
@IsOptional()
@IsNumber()
CompanyId?: number;
}
// Cards Update DTO
export class UpdateCardsDto {
@IsOptional()
@IsString()
@Length(1, 255)
Title?: string;
@IsOptional()
@IsString()
Description?: string;
@IsOptional()
@IsEnum(Privacy)
Privacy?: Privacy;
@IsOptional()
@IsEnum(CardType)
Type?: CardType;
@IsOptional()
@IsNumber()
no_question?: number;
@IsOptional()
@IsNumber()
CompanyId?: number;
}
// Cards Request DTO (for queries/filters)
export class CardsRequestDto {
@IsOptional()
@IsString()
@Length(1, 26)
guid?: string;
@IsOptional()
@IsString()
@Length(1, 255)
Title?: string;
@IsOptional()
@IsEnum(Privacy)
Privacy?: Privacy;
@IsOptional()
@IsEnum(CardType)
Type?: CardType;
@IsOptional()
@IsNumber()
Creator?: number;
@IsOptional()
@IsNumber()
CompanyId?: number;
@IsOptional()
@IsDate()
@Type(() => Date)
CreationDateFrom?: Date;
@IsOptional()
@IsDate()
@Type(() => Date)
CreationDateTo?: Date;
@IsOptional()
@IsNumber()
page?: number;
@IsOptional()
@IsNumber()
limit?: number;
}
// Cards Response DTO (for API responses)
export class CardsResponseDto {
@IsNumber()
CardId!: number;
@IsString()
guid!: string;
@IsString()
Title!: string;
@IsString()
Description!: string;
@IsEnum(Privacy)
Privacy!: Privacy;
@IsEnum(CardType)
Type!: CardType;
@IsNumber()
Creator!: number;
@IsDate()
Creation_Date!: Date;
@IsNumber()
no_question!: number;
@IsOptional()
@IsNumber()
CompanyId?: number;
// Creator information
@IsOptional()
@IsString()
creatorUsername?: string;
@IsOptional()
@IsString()
creatorName?: string;
// Company information
@IsOptional()
@IsString()
companyName?: string;
}
// Cards Remove DTO
export class RemoveCardsDto {
@IsString()
@Length(1, 26)
guid!: string;
@IsOptional()
@IsString()
reason?: string;
@IsOptional()
@IsBoolean()
hardDelete?: boolean; // true for permanent deletion, false for soft delete
}
// Cards Summary DTO (for lists/summaries)
export class CardsSummaryDto {
@IsNumber()
CardId!: number;
@IsString()
guid!: string;
@IsString()
Title!: string;
@IsEnum(Privacy)
Privacy!: Privacy;
@IsEnum(CardType)
Type!: CardType;
@IsNumber()
no_question!: number;
@IsDate()
Creation_Date!: Date;
@IsString()
creatorUsername!: string;
@IsOptional()
@IsString()
companyName?: string;
}
// Cards Statistics DTO
export class CardsStatisticsDto {
@IsNumber()
totalCards!: number;
@IsNumber()
publicCards!: number;
@IsNumber()
companyCards!: number;
@IsNumber()
privateCards!: number;
@IsNumber()
questionCards!: number;
@IsNumber()
jokerCards!: number;
@IsNumber()
luckCards!: number;
@IsNumber()
totalQuestions!: number;
@IsNumber()
averageQuestionsPerCard!: number;
}
// Cards Filter DTO (for advanced filtering)
export class CardsFilterDto {
@IsOptional()
@IsEnum(Privacy)
privacy?: Privacy;
@IsOptional()
@IsEnum(CardType)
type?: CardType;
@IsOptional()
@IsNumber()
creatorId?: number;
@IsOptional()
@IsNumber()
companyId?: number;
@IsOptional()
@IsNumber()
minQuestions?: number;
@IsOptional()
@IsNumber()
maxQuestions?: number;
@IsOptional()
@IsString()
searchTitle?: string;
@IsOptional()
@IsString()
searchDescription?: string;
@IsOptional()
@IsDate()
@Type(() => Date)
createdAfter?: Date;
@IsOptional()
@IsDate()
@Type(() => Date)
createdBefore?: Date;
@IsOptional()
@IsString()
sortBy?: string; // 'title', 'creation_date', 'no_question'
@IsOptional()
@IsString()
sortOrder?: 'ASC' | 'DESC';
@IsOptional()
@IsNumber()
page?: number;
@IsOptional()
@IsNumber()
limit?: number;
}
@@ -0,0 +1,231 @@
import { IsString, IsOptional, IsNumber, IsDate, Length, IsBoolean } from 'class-validator';
import { Type } from 'class-transformer';
// Chat Creation DTO
export class CreateChatDto {
@IsString()
@Length(1, 255)
Chatuuid!: string;
@IsString()
message!: string;
@IsNumber()
UserId!: number;
}
// Chat Update DTO
export class UpdateChatDto {
@IsOptional()
@IsString()
message?: string;
@IsOptional()
@IsString()
@Length(1, 255)
Chatuuid?: string;
}
// Chat Request DTO (for queries/filters)
export class ChatRequestDto {
@IsOptional()
@IsString()
@Length(1, 26)
guid?: string;
@IsOptional()
@IsString()
@Length(1, 255)
Chatuuid?: string;
@IsOptional()
@IsNumber()
UserId?: number;
@IsOptional()
@IsDate()
@Type(() => Date)
createdAfter?: Date;
@IsOptional()
@IsDate()
@Type(() => Date)
createdBefore?: Date;
@IsOptional()
@IsString()
searchMessage?: string;
@IsOptional()
@IsNumber()
page?: number;
@IsOptional()
@IsNumber()
limit?: number;
}
// Chat Response DTO (for API responses)
export class ChatResponseDto {
@IsNumber()
id!: number;
@IsString()
guid!: string;
@IsString()
Chatuuid!: string;
@IsString()
message!: string;
@IsDate()
createdAt!: Date;
@IsDate()
updatedAt!: Date;
@IsNumber()
UserId!: number;
// User information
@IsOptional()
@IsString()
username?: string;
@IsOptional()
@IsString()
userFullName?: string;
}
// Chat Remove DTO
export class RemoveChatDto {
@IsString()
@Length(1, 26)
guid!: string;
@IsOptional()
@IsString()
reason?: string;
@IsOptional()
@IsBoolean()
hardDelete?: boolean; // true for permanent deletion, false for soft delete
}
// Chat Summary DTO (for lists/summaries)
export class ChatSummaryDto {
@IsNumber()
id!: number;
@IsString()
guid!: string;
@IsString()
Chatuuid!: string;
@IsString()
message!: string;
@IsDate()
createdAt!: Date;
@IsString()
username!: string;
}
// Chat Room DTO (for chat room management)
export class ChatRoomDto {
@IsString()
@Length(1, 255)
Chatuuid!: string;
@IsNumber()
messageCount!: number;
@IsNumber()
participantCount!: number;
@IsDate()
lastActivity!: Date;
@IsDate()
createdAt!: Date;
@IsOptional()
@IsString()
roomName?: string;
@IsOptional()
@IsString()
roomDescription?: string;
}
// Chat Statistics DTO
export class ChatStatisticsDto {
@IsNumber()
totalChats!: number;
@IsNumber()
totalChatRooms!: number;
@IsNumber()
activeUsers!: number;
@IsNumber()
messagesLastHour!: number;
@IsNumber()
messagesLastDay!: number;
@IsNumber()
messagesLastWeek!: number;
@IsString()
mostActiveRoom!: string;
@IsNumber()
averageMessagesPerRoom!: number;
}
// Chat Filter DTO (for advanced filtering)
export class ChatFilterDto {
@IsOptional()
@IsString()
@Length(1, 255)
Chatuuid?: string;
@IsOptional()
@IsNumber()
UserId?: number;
@IsOptional()
@IsString()
messageContains?: string;
@IsOptional()
@IsDate()
@Type(() => Date)
createdAfter?: Date;
@IsOptional()
@IsDate()
@Type(() => Date)
createdBefore?: Date;
@IsOptional()
@IsString()
sortBy?: string; // 'createdAt', 'message', 'username'
@IsOptional()
@IsString()
sortOrder?: 'ASC' | 'DESC';
@IsOptional()
@IsNumber()
page?: number;
@IsOptional()
@IsNumber()
limit?: number;
}
@@ -1,83 +1,198 @@
export class CompanyBasicDto {
CompanyId: number;
Name: string;
import { IsString, IsOptional, IsNumber, IsDate, IsUrl, Length, IsEmail, IsBoolean } from 'class-validator';
import { Type } from 'class-transformer';
constructor(CompanyId: number, Name: string) {
this.CompanyId = CompanyId;
this.Name = Name;
}
// Company Creation DTO
export class CreateCompanyDto {
@IsString()
@Length(1, 255)
Name!: string;
@IsString()
@Length(1, 255)
ContactFirstName!: string;
@IsString()
@Length(1, 255)
ContactLastName!: string;
@IsEmail()
@Length(1, 255)
ContactEmail!: string;
@IsUrl()
@Length(1, 255)
LoginURL!: string;
}
export class CompanyUpdateDto {
Name?: string;
ContactFirstName?: string;
ContactLastName?: string;
ContactEmail?: string;
FirstAPI?: string;
TokenAPI?: string;
// Company Update DTO
export class UpdateCompanyDto {
@IsOptional()
@IsString()
@Length(1, 255)
Name?: string;
constructor(data: Partial<CompanyUpdateDto> = {}) {
this.Name = data.Name;
this.ContactFirstName = data.ContactFirstName;
this.ContactLastName = data.ContactLastName;
this.ContactEmail = data.ContactEmail;
this.FirstAPI = data.FirstAPI;
this.TokenAPI = data.TokenAPI;
}
@IsOptional()
@IsString()
@Length(1, 255)
ContactFirstName?: string;
@IsOptional()
@IsString()
@Length(1, 255)
ContactLastName?: string;
@IsOptional()
@IsEmail()
@Length(1, 255)
ContactEmail?: string;
@IsOptional()
@IsUrl()
@Length(1, 255)
LoginURL?: string;
}
export class CompanyCreateDto {
Name: string;
ContactFirstName: string;
ContactLastName: string;
ContactEmail: string;
FirstAPI: string;
TokenAPI: string;
// Company Request DTO (for queries/filters)
export class CompanyRequestDto {
@IsOptional()
@IsString()
@Length(1, 26)
guid?: string;
constructor(
Name: string,
ContactFirstName: string,
ContactLastName: string,
ContactEmail: string,
FirstAPI: string,
TokenAPI: string
) {
this.Name = Name;
this.ContactFirstName = ContactFirstName;
this.ContactLastName = ContactLastName;
this.ContactEmail = ContactEmail;
this.FirstAPI = FirstAPI;
this.TokenAPI = TokenAPI;
}
@IsOptional()
@IsString()
@Length(1, 255)
Name?: string;
@IsOptional()
@IsEmail()
ContactEmail?: string;
@IsOptional()
@IsDate()
@Type(() => Date)
RegDateFrom?: Date;
@IsOptional()
@IsDate()
@Type(() => Date)
RegDateTo?: Date;
@IsOptional()
@IsNumber()
page?: number;
@IsOptional()
@IsNumber()
limit?: number;
}
// Company Response DTO (for API responses)
export class CompanyResponseDto {
CompanyId: number;
Name: string;
ContactFirstName: string;
ContactLastName: string;
ContactEmail: string;
FirstAPI: string;
TokenAPI: string;
RegDate: Date;
@IsNumber()
CompanyId!: number;
constructor(
CompanyId: number,
Name: string,
ContactFirstName: string,
ContactLastName: string,
ContactEmail: string,
FirstAPI: string,
TokenAPI: string,
RegDate: Date
) {
this.CompanyId = CompanyId;
this.Name = Name;
this.ContactFirstName = ContactFirstName;
this.ContactLastName = ContactLastName;
this.ContactEmail = ContactEmail;
this.FirstAPI = FirstAPI;
this.TokenAPI = TokenAPI;
this.RegDate = RegDate;
}
}
@IsString()
GUID!: string;
@IsString()
Name!: string;
@IsString()
ContactFirstName!: string;
@IsString()
ContactLastName!: string;
@IsEmail()
ContactEmail!: string;
@IsUrl()
LoginURL!: string;
@IsDate()
RegDate!: Date;
@IsDate()
UpdatedDate!: Date;
@IsOptional()
@IsNumber()
userCount?: number;
@IsOptional()
@IsNumber()
cardCount?: number;
}
// Company Remove DTO
export class RemoveCompanyDto {
@IsString()
@Length(1, 26)
guid!: string;
@IsOptional()
@IsString()
reason?: string;
@IsOptional()
@IsBoolean()
hardDelete?: boolean; // true for permanent deletion, false for soft delete
}
// Company Summary DTO (for lists/summaries)
export class CompanySummaryDto {
@IsNumber()
CompanyId!: number;
@IsString()
GUID!: string;
@IsString()
Name!: string;
@IsEmail()
ContactEmail!: string;
@IsDate()
RegDate!: Date;
@IsNumber()
userCount!: number;
@IsNumber()
cardCount!: number;
}
// Company Statistics DTO
export class CompanyStatisticsDto {
@IsNumber()
CompanyId!: number;
@IsString()
Name!: string;
@IsNumber()
totalUsers!: number;
@IsNumber()
activeUsers!: number;
@IsNumber()
premiumUsers!: number;
@IsNumber()
totalCards!: number;
@IsNumber()
publicCards!: number;
@IsNumber()
companyCards!: number;
@IsNumber()
privateCards!: number;
@IsDate()
lastActivity!: Date;
}
@@ -0,0 +1,322 @@
import { IsString, IsOptional, IsNumber, IsDate, IsEmail, Length, IsBoolean } from 'class-validator';
import { Type } from 'class-transformer';
// Company Contact Creation DTO
export class CreateCompanyContactDto {
@IsString()
@Length(1, 255)
name!: string;
@IsEmail()
@Length(1, 255)
email!: string;
@IsString()
message!: string;
@IsNumber()
companyId!: number;
@IsOptional()
@IsNumber()
userId?: number;
}
// Company Contact Update DTO
export class UpdateCompanyContactDto {
@IsOptional()
@IsString()
@Length(1, 255)
name?: string;
@IsOptional()
@IsEmail()
@Length(1, 255)
email?: string;
@IsOptional()
@IsString()
message?: string;
@IsOptional()
@IsNumber()
companyId?: number;
@IsOptional()
@IsNumber()
userId?: number;
}
// Company Contact Request DTO (for queries/filters)
export class CompanyContactRequestDto {
@IsOptional()
@IsString()
@Length(1, 26)
guid?: string;
@IsOptional()
@IsString()
@Length(1, 255)
name?: string;
@IsOptional()
@IsEmail()
email?: string;
@IsOptional()
@IsNumber()
companyId?: number;
@IsOptional()
@IsNumber()
userId?: number;
@IsOptional()
@IsDate()
@Type(() => Date)
createdAfter?: Date;
@IsOptional()
@IsDate()
@Type(() => Date)
createdBefore?: Date;
@IsOptional()
@IsString()
searchMessage?: string;
@IsOptional()
@IsNumber()
page?: number;
@IsOptional()
@IsNumber()
limit?: number;
}
// Company Contact Response DTO (for API responses)
export class CompanyContactResponseDto {
@IsNumber()
id!: number;
@IsString()
guid!: string;
@IsString()
name!: string;
@IsEmail()
email!: string;
@IsString()
message!: string;
@IsDate()
createdAt!: Date;
@IsDate()
updatedAt!: Date;
// Company information
@IsNumber()
companyId!: number;
@IsString()
companyName!: string;
@IsString()
companyGuid!: string;
// User information (if associated with a user)
@IsOptional()
@IsNumber()
userId?: number;
@IsOptional()
@IsString()
username?: string;
@IsOptional()
@IsString()
userFullName?: string;
}
// Company Contact Remove DTO
export class RemoveCompanyContactDto {
@IsString()
@Length(1, 26)
guid!: string;
@IsOptional()
@IsString()
reason?: string;
@IsOptional()
@IsBoolean()
hardDelete?: boolean; // true for permanent deletion, false for soft delete
}
// Company Contact Summary DTO (for lists/summaries)
export class CompanyContactSummaryDto {
@IsNumber()
id!: number;
@IsString()
guid!: string;
@IsString()
name!: string;
@IsEmail()
email!: string;
@IsString()
messagePreview!: string; // First 100 characters of message
@IsDate()
createdAt!: Date;
@IsString()
companyName!: string;
@IsOptional()
@IsString()
username?: string;
}
// Company Contact Statistics DTO
export class CompanyContactStatisticsDto {
@IsNumber()
totalContacts!: number;
@IsNumber()
contactsLastHour!: number;
@IsNumber()
contactsLastDay!: number;
@IsNumber()
contactsLastWeek!: number;
@IsNumber()
contactsLastMonth!: number;
@IsNumber()
registeredUserContacts!: number;
@IsNumber()
anonymousContacts!: number;
@IsNumber()
averageMessageLength!: number;
@IsString()
mostActiveDay!: string;
@IsNumber()
uniqueCompanies!: number;
@IsString()
topCompanyByContacts!: string;
}
// Company Contact Filter DTO (for advanced filtering)
export class CompanyContactFilterDto {
@IsOptional()
@IsString()
nameContains?: string;
@IsOptional()
@IsEmail()
email?: string;
@IsOptional()
@IsString()
emailDomain?: string;
@IsOptional()
@IsString()
messageContains?: string;
@IsOptional()
@IsNumber()
companyId?: number;
@IsOptional()
@IsString()
companyName?: string;
@IsOptional()
@IsNumber()
userId?: number;
@IsOptional()
@IsBoolean()
isRegisteredUser?: boolean;
@IsOptional()
@IsDate()
@Type(() => Date)
createdAfter?: Date;
@IsOptional()
@IsDate()
@Type(() => Date)
createdBefore?: Date;
@IsOptional()
@IsNumber()
minMessageLength?: number;
@IsOptional()
@IsNumber()
maxMessageLength?: number;
@IsOptional()
@IsString()
sortBy?: string; // 'name', 'email', 'createdAt', 'message', 'companyName'
@IsOptional()
@IsString()
sortOrder?: 'ASC' | 'DESC';
@IsOptional()
@IsNumber()
page?: number;
@IsOptional()
@IsNumber()
limit?: number;
}
// Company Contact by Company DTO (for retrieving all contacts for a specific company)
export class CompanyContactsByCompanyDto {
@IsNumber()
companyId!: number;
@IsOptional()
@IsDate()
@Type(() => Date)
fromDate?: Date;
@IsOptional()
@IsDate()
@Type(() => Date)
toDate?: Date;
@IsOptional()
@IsNumber()
page?: number;
@IsOptional()
@IsNumber()
limit?: number;
@IsOptional()
@IsString()
sortBy?: string;
@IsOptional()
@IsString()
sortOrder?: 'ASC' | 'DESC';
}
@@ -0,0 +1,239 @@
import { IsString, IsOptional, IsNumber, IsDate, IsEmail, Length, IsBoolean } from 'class-validator';
import { Type } from 'class-transformer';
export class CreateContactDto {
@IsString()
@Length(1, 255)
name!: string;
@IsEmail()
@Length(1, 255)
email!: string;
@IsString()
message!: string;
@IsOptional()
@IsNumber()
userId?: number;
}
export class UpdateContactDto {
@IsOptional()
@IsString()
@Length(1, 255)
name?: string;
@IsOptional()
@IsEmail()
@Length(1, 255)
email?: string;
@IsOptional()
@IsString()
message?: string;
}
export class ContactRequestDto {
@IsOptional()
@IsString()
@Length(1, 26)
guid?: string;
@IsOptional()
@IsString()
@Length(1, 255)
name?: string;
@IsOptional()
@IsEmail()
email?: string;
@IsOptional()
@IsNumber()
userId?: number;
@IsOptional()
@IsDate()
@Type(() => Date)
createdAfter?: Date;
@IsOptional()
@IsDate()
@Type(() => Date)
createdBefore?: Date;
@IsOptional()
@IsString()
searchMessage?: string;
@IsOptional()
@IsNumber()
page?: number;
@IsOptional()
@IsNumber()
limit?: number;
}
export class ContactResponseDto {
@IsNumber()
id!: number;
@IsString()
guid!: string;
@IsString()
name!: string;
@IsEmail()
email!: string;
@IsString()
message!: string;
@IsDate()
createdAt!: Date;
@IsDate()
updatedAt!: Date;
@IsOptional()
@IsNumber()
userId?: number;
@IsOptional()
@IsString()
username?: string;
@IsOptional()
@IsString()
userFullName?: string;
}
export class RemoveContactDto {
@IsString()
@Length(1, 26)
guid!: string;
@IsOptional()
@IsString()
reason?: string;
@IsOptional()
@IsBoolean()
hardDelete?: boolean;
}
export class ContactSummaryDto {
@IsNumber()
id!: number;
@IsString()
guid!: string;
@IsString()
name!: string;
@IsEmail()
email!: string;
@IsString()
messagePreview!: string;
@IsDate()
createdAt!: Date;
@IsOptional()
@IsString()
username?: string;
}
export class ContactStatisticsDto {
@IsNumber()
totalContacts!: number;
@IsNumber()
contactsLastHour!: number;
@IsNumber()
contactsLastDay!: number;
@IsNumber()
contactsLastWeek!: number;
@IsNumber()
contactsLastMonth!: number;
@IsNumber()
registeredUserContacts!: number;
@IsNumber()
anonymousContacts!: number;
@IsNumber()
averageMessageLength!: number;
@IsString()
mostActiveDay!: string;
}
export class ContactFilterDto {
@IsOptional()
@IsString()
nameContains?: string;
@IsOptional()
@IsEmail()
email?: string;
@IsOptional()
@IsString()
emailDomain?: string;
@IsOptional()
@IsString()
messageContains?: string;
@IsOptional()
@IsNumber()
userId?: number;
@IsOptional()
@IsBoolean()
isRegisteredUser?: boolean;
@IsOptional()
@IsDate()
@Type(() => Date)
createdAfter?: Date;
@IsOptional()
@IsDate()
@Type(() => Date)
createdBefore?: Date;
@IsOptional()
@IsNumber()
minMessageLength?: number;
@IsOptional()
@IsNumber()
maxMessageLength?: number;
@IsOptional()
@IsString()
sortBy?: string;
@IsOptional()
@IsString()
sortOrder?: 'ASC' | 'DESC';
@IsOptional()
@IsNumber()
page?: number;
@IsOptional()
@IsNumber()
limit?: number;
}
@@ -1,59 +0,0 @@
export class QBankBasicDto {
QBankId: number;
Title: string;
constructor(QBankId: number, Title: string) {
this.QBankId = QBankId;
this.Title = Title;
}
}
export class QBankUpdateDto {
Title?: string;
no_question?: number;
constructor(data: Partial<QBankUpdateDto> = {}) {
this.Title = data.Title;
this.no_question = data.no_question;
}
}
export class QBankCreateDto {
Title: string;
no_question: number;
constructor(Title: string, no_question: number) {
this.Title = Title;
this.no_question = no_question;
}
}
export class QBankResponseDto {
QBankId: number;
Title: string;
Creator: number;
Creation_Date: Date;
no_question: number;
constructor(
QBankId: number,
Title: string,
Creator: number,
Creation_Date: Date,
no_question: number
) {
this.QBankId = QBankId;
this.Title = Title;
this.Creator = Creator;
this.Creation_Date = Creation_Date;
this.no_question = no_question;
}
}
export class QBankListResponseDto {
questionBanks: QBankResponseDto[];
constructor(questionBanks: QBankResponseDto[]) {
this.questionBanks = questionBanks;
}
}
+311 -81
View File
@@ -1,96 +1,326 @@
export class UserBasicDto3 {
id: number;
username: string;
CompanyId?: number;
import { IsEmail, IsString, IsOptional, IsEnum, IsNumber, IsDate, IsBoolean, Length, MinLength } from 'class-validator';
import { Type } from 'class-transformer';
import { UserStatus, UserAuthLevel } from '../entities/user.entity';
constructor(id: number, username: string, CompanyId?: number) {
this.id = id;
this.username = username;
this.CompanyId = CompanyId || 0;
}
}
export class UserBasicDto {
id: number;
username: string;
CompanyId?: number;
// JWT Token DTO
export class JwtTokenDto {
@IsNumber()
userId!: number;
constructor(id: number, username: string, CompanyId?: number) {
this.id = id;
this.username = username;
this.CompanyId = CompanyId || 0; // Default to 0 if not provided
}
@IsString()
@Length(1, 255)
username!: string;
@IsOptional()
@IsNumber()
companyId?: number;
@IsEnum(UserAuthLevel)
authLevel!: UserAuthLevel;
@IsEnum(UserStatus)
status!: UserStatus;
}
export class UserUpdateDto {
username?: string;
FirstName?: string;
LastName?: string;
email?: string;
password?: string;
// Login Response DTO
export class LoginResponseDto {
@IsString()
@Length(1, 255)
username!: string;
constructor(data: Partial<UserUpdateDto> = {}) {
this.username = data.username;
this.FirstName = data.FirstName;
this.LastName = data.LastName;
this.email = data.email;
this.password = data.password;
}
@IsNumber()
id!: number;
@IsOptional()
@IsNumber()
companyId?: number;
@IsEnum(UserAuthLevel)
authLevel!: UserAuthLevel;
@IsEnum(UserStatus)
status!: UserStatus;
@IsString()
token!: string;
}
export class UserCreateDto {
username: string;
FirstName: string;
LastName: string;
email: string;
password: string;
CompanyId?: number; // Add optional CompanyId
// User Creation DTO
export class CreateUserDto {
@IsString()
@Length(1, 255)
username!: string;
constructor(
username: string,
FirstName: string,
LastName: string,
email: string,
password: string,
CompanyId?: number // Add optional parameter
) {
this.username = username;
this.FirstName = FirstName;
this.LastName = LastName;
this.email = email;
this.password = password;
this.CompanyId = CompanyId; // Don't set default value
}
@IsString()
@Length(1, 255)
FirstName!: string;
@IsString()
@Length(1, 255)
LastName!: string;
@IsEmail()
@Length(1, 255)
email!: string;
@IsString()
@MinLength(8)
@Length(1, 255)
password!: string;
@IsOptional()
@IsNumber()
CompanyId?: number;
@IsOptional()
@IsEnum(UserAuthLevel)
authLevel?: UserAuthLevel;
@IsOptional()
@IsEnum(UserStatus)
status?: UserStatus;
}
// User Update DTO
export class UpdateUserDto {
@IsOptional()
@IsString()
@Length(1, 255)
username?: string;
@IsOptional()
@IsString()
@Length(1, 255)
FirstName?: string;
@IsOptional()
@IsString()
@Length(1, 255)
LastName?: string;
@IsOptional()
@IsEmail()
@Length(1, 255)
email?: string;
@IsOptional()
@IsString()
@MinLength(8)
@Length(1, 255)
password?: string;
@IsOptional()
@IsEnum(UserStatus)
status?: UserStatus;
@IsOptional()
@IsEnum(UserAuthLevel)
authLevel?: UserAuthLevel;
@IsOptional()
@IsString()
@Length(1, 32)
securityToken?: string;
@IsOptional()
@IsDate()
@Type(() => Date)
securityTokenExpiry?: Date;
@IsOptional()
@IsDate()
@Type(() => Date)
premiumExpirationDate?: Date;
@IsOptional()
@IsNumber()
CompanyId?: number;
@IsOptional()
@IsBoolean()
companyRegistered?: boolean;
@IsOptional()
@IsDate()
@Type(() => Date)
companyRegistrationDate?: Date;
}
// User Request DTO (for queries/filters)
export class UserRequestDto {
@IsOptional()
@IsString()
@Length(1, 26)
guid?: string;
@IsOptional()
@IsString()
@Length(1, 255)
username?: string;
@IsOptional()
@IsEmail()
email?: string;
@IsOptional()
@IsEnum(UserStatus)
status?: UserStatus;
@IsOptional()
@IsEnum(UserAuthLevel)
authLevel?: UserAuthLevel;
@IsOptional()
@IsNumber()
CompanyId?: number;
@IsOptional()
@IsBoolean()
companyRegistered?: boolean;
@IsOptional()
@IsNumber()
page?: number;
@IsOptional()
@IsNumber()
limit?: number;
}
// User Response DTO (for API responses)
export class UserResponseDto {
id: number;
username: string;
FirstName: string;
LastName: string;
email: string;
RegDate: Date;
@IsNumber()
id!: number;
constructor(
id: number,
username: string,
FirstName: string,
LastName: string,
email: string,
RegDate: Date
) {
this.id = id;
this.username = username;
this.FirstName = FirstName;
this.LastName = LastName;
this.email = email;
this.RegDate = RegDate;
}
@IsString()
guid!: string;
@IsString()
username!: string;
@IsString()
FirstName!: string;
@IsString()
LastName!: string;
@IsEmail()
email!: string;
@IsDate()
createdAt!: Date;
@IsDate()
updatedAt!: Date;
@IsEnum(UserStatus)
status!: UserStatus;
@IsEnum(UserAuthLevel)
authLevel!: UserAuthLevel;
@IsOptional()
@IsDate()
premiumExpirationDate?: Date;
@IsOptional()
@IsNumber()
CompanyId?: number;
@IsBoolean()
companyRegistered!: boolean;
@IsOptional()
@IsDate()
companyRegistrationDate?: Date;
// Helper method flags
@IsBoolean()
isConfirmed!: boolean;
@IsBoolean()
isPremium!: boolean;
@IsBoolean()
isAdmin!: boolean;
@IsBoolean()
canLogin!: boolean;
@IsBoolean()
hasCompany!: boolean;
@IsBoolean()
needsCompanyReregistration!: boolean;
}
export class UsersListResponseDto {
users: UserResponseDto[];
// User Remove DTO
export class RemoveUserDto {
@IsString()
@Length(1, 26)
guid!: string;
constructor(users: UserResponseDto[]) {
this.users = users;
}
}
@IsOptional()
@IsString()
reason?: string;
@IsOptional()
@IsBoolean()
hardDelete?: boolean; // true for permanent deletion, false for soft delete
}
// Login Request DTO
export class LoginRequestDto {
@IsString()
@Length(1, 255)
username!: string;
@IsString()
@Length(1, 255)
password!: string;
}
// Password Reset Request DTO
export class PasswordResetRequestDto {
@IsEmail()
email!: string;
}
// Password Reset DTO
export class PasswordResetDto {
@IsString()
@Length(32, 32)
token!: string;
@IsString()
@MinLength(8)
@Length(1, 255)
newPassword!: string;
}
// Email Confirmation DTO
export class EmailConfirmationDto {
@IsString()
@Length(32, 32)
token!: string;
}
// Premium Subscription DTO
export class PremiumSubscriptionDto {
@IsString()
@Length(1, 26)
userGuid!: string;
@IsNumber()
months!: number;
}
// Company Registration DTO
export class CompanyRegistrationDto {
@IsString()
@Length(1, 26)
userGuid!: string;
@IsNumber()
companyId!: number;
}
@@ -0,0 +1,229 @@
# Soft Delete Implementation
This document describes the soft delete functionality implemented across all entities in the system.
## Overview
Soft delete allows entities to be marked as deleted without actually removing them from the database. This provides:
- Data recovery capabilities
- Audit trails
- Referential integrity maintenance
- Historical data preservation
## Implementation Details
### Entity Changes
All entities now include:
- `@DeleteDateColumn() deletedAt?: Date` - Timestamp when the entity was soft deleted
- `isDeleted(): boolean` - Helper method to check if entity is soft deleted
- `softDelete(): void` - Helper method to soft delete the entity
- `restore(): void` - Helper method to restore a soft deleted entity
### Database Schema Changes
Each entity table will have a new column:
- `deletedAt` (DATETIME, nullable) - NULL for active records, timestamp for deleted records
### Repository Methods
The BaseRepository now includes:
#### Core Soft Delete Methods
- `softDelete(id: number): Promise<boolean>` - Soft delete an entity by ID
- `restore(id: number): Promise<boolean>` - Restore a soft deleted entity by ID
#### Query Methods
- `findWithDeleted(): Promise<T[]>` - Find all entities including soft deleted ones
- `findOnlyDeleted(): Promise<T[]>` - Find only soft deleted entities
#### Default Behavior
- `findAll()`, `findById()`, and other query methods automatically exclude soft deleted entities
- This is handled by TypeORM's built-in soft delete functionality
## Entity-Specific Implementations
### User Entity
- Integrates with existing `UserStatus.DELETED` enum
- `isDeleted()` checks both status and `deletedAt` fields
- `softDelete()` sets both `deletedAt` and `status = DELETED`
- `restoreFromSoftDelete()` clears `deletedAt` and resets status to `CONFIRMED`
### Other Entities
- Standard soft delete implementation
- `isDeleted()` checks `deletedAt` field
- `softDelete()` sets `deletedAt` to current timestamp
- `restore()` clears `deletedAt` field
## Usage Examples
### Basic Soft Delete Operations
```typescript
// Get repository
const userRepository = repositoryManager.getUserRepository();
// Soft delete a user
await userRepository.softDelete(123);
// Restore a soft deleted user
await userRepository.restore(123);
// Find all active users (excludes soft deleted)
const activeUsers = await userRepository.findAll();
// Find all users including soft deleted
const allUsers = await userRepository.findWithDeleted();
// Find only soft deleted users
const deletedUsers = await userRepository.findOnlyDeleted();
```
### Entity-Level Operations
```typescript
// Using entity helper methods
const user = await userRepository.findById(123);
if (user) {
// Check if soft deleted
if (user.isDeleted()) {
console.log('User is soft deleted');
}
// Soft delete using entity method
user.softDelete();
await userRepository.update(user.id, user);
// Restore using entity method
user.restore();
await userRepository.update(user.id, user);
}
```
### Advanced Queries
```typescript
// Find entities with specific conditions including soft deleted
const results = await repository.repository.find({
where: {
companyId: 456,
// other conditions
},
withDeleted: true // Include soft deleted entities
});
// Find entities that were deleted within the last 30 days
const recentlyDeleted = await repository.repository
.createQueryBuilder('entity')
.where('entity.deletedAt IS NOT NULL')
.andWhere('entity.deletedAt > :thirtyDaysAgo', {
thirtyDaysAgo: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)
})
.withDeleted()
.getMany();
```
## Migration Requirements
To implement soft delete in an existing database, you'll need to run migrations to:
1. Add `deletedAt` column to each table
2. Update indexes to consider the `deletedAt` column
3. Update any existing queries that need to handle soft deleted data
Example migration:
```sql
-- Add deletedAt column to each table
ALTER TABLE Cards ADD COLUMN deletedAt DATETIME NULL;
ALTER TABLE Chat ADD COLUMN deletedAt DATETIME NULL;
ALTER TABLE Company ADD COLUMN deletedAt DATETIME NULL;
ALTER TABLE Contact ADD COLUMN deletedAt DATETIME NULL;
ALTER TABLE CompanyContact ADD COLUMN deletedAt DATETIME NULL;
ALTER TABLE User ADD COLUMN deletedAt DATETIME NULL;
-- Add indexes for better performance
CREATE INDEX idx_cards_deleted_at ON Cards(deletedAt);
CREATE INDEX idx_chat_deleted_at ON Chat(deletedAt);
CREATE INDEX idx_company_deleted_at ON Company(deletedAt);
CREATE INDEX idx_contact_deleted_at ON Contact(deletedAt);
CREATE INDEX idx_companycontact_deleted_at ON CompanyContact(deletedAt);
CREATE INDEX idx_user_deleted_at ON User(deletedAt);
```
## Logging and Monitoring
All soft delete operations are automatically logged with:
- Operation type (SOFT_DELETE, RESTORE, FIND_WITH_DELETED, etc.)
- Entity name and ID
- Execution time
- Success/failure status
- Error details if applicable
## Best Practices
### When to Use Soft Delete
- ✅ User accounts (for compliance/audit)
- ✅ Important business data (orders, transactions)
- ✅ Content that may need recovery
- ✅ Entities with complex relationships
### When to Use Hard Delete
- ❌ Temporary data (sessions, cache)
- ❌ Log entries (if storage is limited)
- ❌ Test data
- ❌ Truly sensitive data that must be permanently removed
### Development Guidelines
1. **Always use repository methods** for delete operations
2. **Consider cascade behavior** when soft deleting parent entities
3. **Test restore functionality** thoroughly
4. **Monitor soft delete usage** to prevent data bloat
5. **Implement cleanup procedures** for old soft deleted data
6. **Document business rules** for when entities should be permanently deleted
### Performance Considerations
1. **Index the `deletedAt` column** for better query performance
2. **Consider composite indexes** for frequently queried combinations
3. **Implement archival processes** for old soft deleted data
4. **Monitor query performance** as soft deleted data accumulates
5. **Use `findOnlyDeleted()` sparingly** as it requires full table scans
## Error Handling
The soft delete system includes comprehensive error handling:
- Database constraint violations
- Referential integrity issues
- Concurrent modification conflicts
- Network/connection issues
All errors are logged with full context for debugging and monitoring.
## Testing
Ensure your tests cover:
- Basic soft delete/restore operations
- Query behavior with soft deleted data
- Entity helper methods
- Error conditions
- Performance under load
- Data consistency after soft delete operations
## Compliance and Audit
The soft delete system supports:
- Data retention policies
- Audit trail requirements
- Compliance with data protection regulations
- Recovery procedures for accidental deletions
- Historical data analysis
## Future Enhancements
Potential improvements:
- Automated cleanup of old soft deleted data
- Soft delete cascading rules
- Bulk soft delete operations
- Soft delete analytics dashboard
- Integration with data archival systems
@@ -0,0 +1,73 @@
//Contains default details for cards entity
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn, DeleteDateColumn } from 'typeorm';
import { User } from './user.entity';
import { Company } from './company.entity';
export enum Privacy {
Private = 'private',
Public = 'public',
Company = 'company'
}
export enum CardType {
Question = 'question',
Joker = 'joker',
Luck = 'luck'
}
@Entity('Cards')
export class Cards {
@PrimaryGeneratedColumn()
CardId!: number;
@Column({ type: 'varchar', length: 26, nullable: false })
guid!: string;
@Column({ type: 'varchar', length: 255 })
Title!: string;
@Column({ type: 'text' })
Description!: string;
@Column({ type: 'enum', enum: Privacy })
Privacy!: Privacy;
@Column({ type: 'enum', enum: CardType })
Type!: CardType;
@Column({ type: 'int', nullable: false })
Creator!: number;
@Column({ type: 'datetime' })
Creation_Date!: Date;
@Column({ type: 'int' })
no_question!: number;
@Column({ type: 'int', nullable: true })
CompanyId?: number;
@DeleteDateColumn()
deletedAt?: Date;
@ManyToOne(() => User, user => user.questionBanks)
@JoinColumn({ name: 'Creator' })
creator!: User;
@ManyToOne(() => Company, company => company.cards, { nullable: true })
@JoinColumn({ name: 'CompanyId' })
company?: Company;
// Soft delete helper methods
isDeleted(): boolean {
return this.deletedAt !== null && this.deletedAt !== undefined;
}
softDelete(): void {
this.deletedAt = new Date();
}
restore(): void {
this.deletedAt = undefined;
}
}
@@ -0,0 +1,51 @@
//contains the entity definition for Chat that is replyable to a User
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToMany, JoinColumn, DeleteDateColumn } from 'typeorm';
import { User } from './user.entity';
@Entity('Chat')
export class Chat {
@PrimaryGeneratedColumn()
id!: number;
@Column({ type: 'varchar', length: 26, nullable: false })
guid!: string;
@Column({ type: 'varchar', length: 255, nullable: false })
Chatuuid!: string;
@Column({ type: 'text', nullable: false })
message!: string;
@CreateDateColumn()
createdAt!: Date;
@UpdateDateColumn()
updatedAt!: Date;
@Column({ type: 'int', nullable: false })
userId!: number;
@Column({ type: 'int', nullable: false })
UserId!: number;
@DeleteDateColumn()
deletedAt?: Date;
@ManyToMany(() => User, user => user.chats)
@JoinColumn({ name: 'UserId' })
user!: User;
// Soft delete helper methods
isDeleted(): boolean {
return this.deletedAt !== null && this.deletedAt !== undefined;
}
softDelete(): void {
this.deletedAt = new Date();
}
restore(): void {
this.deletedAt = undefined;
}
}
@@ -3,37 +3,61 @@ import {
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
OneToMany
OneToMany,
UpdateDateColumn,
DeleteDateColumn
} from 'typeorm';
import { User } from './user.entity';
import { Cards } from './cards.entity';
@Entity('Company')
export class Company {
@PrimaryGeneratedColumn()
CompanyId!: number;
@PrimaryGeneratedColumn()
CompanyId!: number;
@Column({ type: 'varchar', length: 255, nullable: false })
Name!: string;
@Column({ type: 'varchar', length: 26, nullable: false })
GUID!: string;
@Column({ type: 'varchar', length: 255, nullable: false })
ContactFirstName!: string;
@Column({ type: 'varchar', length: 255, nullable: false })
Name!: string;
@Column({ type: 'varchar', length: 255, nullable: false })
ContactLastName!: string;
@Column({ type: 'varchar', length: 255, nullable: false })
ContactFirstName!: string;
@Column({ type: 'varchar', length: 255, nullable: false })
ContactEmail!: string;
@Column({ type: 'varchar', length: 255, nullable: false })
ContactLastName!: string;
@Column({ type: 'varchar', length: 255, nullable: false })
FirstAPI!: string;
@Column({ type: 'varchar', length: 255, nullable: false })
ContactEmail!: string;
@Column({ type: 'varchar', length: 255, nullable: false })
TokenAPI!: string;
@Column({ type: 'varchar', length: 255, nullable: false })
LoginURL!: string;
@CreateDateColumn()
RegDate!: Date;
@CreateDateColumn()
RegDate!: Date;
@UpdateDateColumn()
UpdatedDate!: Date;
@DeleteDateColumn()
deletedAt?: Date;
// Relation to User entity (optional)
@OneToMany(() => User, user => user.company)
users!: User[];
@OneToMany(() => Cards, cards => cards.company)
cards!: Cards[];
// Soft delete helper methods
isDeleted(): boolean {
return this.deletedAt !== null && this.deletedAt !== undefined;
}
softDelete(): void {
this.deletedAt = new Date();
}
restore(): void {
this.deletedAt = undefined;
}
}
@@ -0,0 +1,51 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, DeleteDateColumn } from 'typeorm';
import { User } from './user.entity';
@Entity('companyContact')
export class CompanyContact {
@PrimaryGeneratedColumn()
id!: number;
@Column({ type: 'varchar', length: 26, nullable: false })
guid!: string;
@Column({ type: 'varchar', length: 255, nullable: false })
CompanyName!: string;
@Column({ type: 'varchar', length: 255, nullable: false })
ContactFirstName!: string;
@Column({ type: 'varchar', length: 255, nullable: false })
ContactLastName!: string;
@Column({ type: 'varchar', length: 255, nullable: false })
email!: string;
@Column({ type: 'varchar', length: 255, nullable: false })
phone!: string;
@Column({ type: 'text', nullable: true })
message?: string;
@CreateDateColumn()
createdAt!: Date;
@DeleteDateColumn()
deletedAt?: Date;
@ManyToOne(() => User, user => user.companyContacts, { nullable: true })
createdBy!: User;
// Soft delete helper methods
isDeleted(): boolean {
return this.deletedAt !== null && this.deletedAt !== undefined;
}
softDelete(): void {
this.deletedAt = new Date();
}
restore(): void {
this.deletedAt = undefined;
}
}
@@ -0,0 +1,45 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, ManyToOne, DeleteDateColumn } from 'typeorm';
import { User } from './user.entity';
@Entity('contact')
export class Contact {
@PrimaryGeneratedColumn()
id!: number;
@Column({ type: 'varchar', length: 26, nullable: false })
guid!: string;
@Column({ type: 'varchar', length: 255, nullable: false })
name!: string;
@Column({ type: 'varchar', length: 255, nullable: false })
email!: string;
@Column({ type: 'text', nullable: false })
message!: string;
@CreateDateColumn()
createdAt!: Date;
@UpdateDateColumn()
updatedAt!: Date;
@DeleteDateColumn()
deletedAt?: Date;
@ManyToOne(() => User, user => user.contacts)
user!: User;
// Soft delete helper methods
isDeleted(): boolean {
return this.deletedAt !== null && this.deletedAt !== undefined;
}
softDelete(): void {
this.deletedAt = new Date();
}
restore(): void {
this.deletedAt = undefined;
}
}
@@ -1,24 +0,0 @@
import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, JoinColumn } from 'typeorm';
import { User } from './user.entity';
@Entity('QBank')
export class QBank {
@PrimaryGeneratedColumn()
QBankId!: number;
@Column({ type: 'varchar', length: 255 })
Title!: string;
@Column()
Creator!: number;
@Column({ type: 'datetime' })
Creation_Date!: Date;
@Column({ type: 'int' })
no_question!: number;
@ManyToOne(() => User, user => user.questionBanks)
@JoinColumn({ name: 'Creator' })
creator!: User;
}
@@ -1,22 +1,44 @@
import {
Entity,
Column,
//Class contains the entity definition for User
import { Entity,
PrimaryGeneratedColumn,
Column,
CreateDateColumn,
UpdateDateColumn,
ManyToOne,
OneToMany,
JoinColumn,
OneToMany
} from "typeorm";
ManyToMany,
DeleteDateColumn
} from 'typeorm';
import {Company} from "./company.entity";
import {QBank} from "./qbank.entity";
// Import related entities
import { Company } from './company.entity';
import { Contact } from './contact.entity';
import { CompanyContact } from './companyContact.entity';
import { Cards } from './cards.entity';
import { Chat } from './chat.entity';
export enum UserStatus {
DELETED = 0,
PENDING_CONFIRMATION = 1,
CONFIRMED = 2,
DEACTIVATED = 3
}
export enum UserAuthLevel {
STANDARD = 0,
PREMIUM = 1,
ADMIN = 2
}
@Entity('user')
export class User {
@PrimaryGeneratedColumn()
id!: number;
@Column({ type: 'varchar', length: 26, nullable: false, unique: true })
guid!: string;
@Column({ type: 'varchar', length: 255 , nullable: false, unique: true })
username!: string;
@@ -33,18 +55,154 @@ export class User {
password!: string;
@CreateDateColumn()
RegDate!: Date;
createdAt!: Date;
@UpdateDateColumn()
updatedAt!: Date;
@Column({ type: 'enum', enum: UserStatus, default: UserStatus.PENDING_CONFIRMATION })
status!: UserStatus;
@Column({ type: 'enum', enum: UserAuthLevel, default: UserAuthLevel.STANDARD })
authLevel!: UserAuthLevel;
@Column({ type: 'varchar', length: 32, nullable: true })
securityToken?: string;
@Column({ type: 'datetime', nullable: true })
securityTokenExpiry?: Date;
@Column({ type: 'date', nullable: true })
premiumExpirationDate?: Date;
// Company registration fields
@Column({ nullable: true })
CompanyId?: number;
@Column({ type: 'varchar', length: 255, nullable: true })
CompanyToken?: string;
@Column({ type: 'boolean', default: false })
companyRegistered!: boolean
@Column({ type: 'datetime', nullable: true })
companyRegistrationDate?: Date;
@DeleteDateColumn()
deletedAt?: Date;
// Relations
@ManyToOne(() => Company, (company) => company.users)
@JoinColumn({ name: 'CompanyId' })
company?: Company;
@OneToMany(() => QBank, qbank => qbank.creator)
questionBanks!: QBank[];
@OneToMany(() => Cards, cards => cards.creator)
questionBanks!: Cards[];
@OneToMany(() => Contact, contact => contact.user)
contacts!: Contact[];
@OneToMany(() => CompanyContact, companyContact => companyContact.createdBy)
companyContacts!: CompanyContact[];
@ManyToMany(() => Chat, chat => chat.user)
chats!: Chat[];
// Helper methods
isConfirmed(): boolean {
return this.status === UserStatus.CONFIRMED;
}
isPremium(): boolean {
return this.authLevel === UserAuthLevel.PREMIUM &&
this.premiumExpirationDate !== undefined &&
this.premiumExpirationDate !== null &&
this.premiumExpirationDate > new Date();
}
isAdmin(): boolean {
return this.authLevel === UserAuthLevel.ADMIN;
}
isDeleted(): boolean {
return this.status === UserStatus.DELETED || (this.deletedAt !== null && this.deletedAt !== undefined);
}
isDeactivated(): boolean {
return this.status === UserStatus.DEACTIVATED;
}
canLogin(): boolean {
return this.status === UserStatus.CONFIRMED && !this.isDeleted();
}
setPremium(months: number): void {
this.authLevel = UserAuthLevel.PREMIUM;
const expiryDate = new Date();
expiryDate.setMonth(expiryDate.getMonth() + months);
this.premiumExpirationDate = expiryDate;
}
revokePremium(): void {
if (this.authLevel === UserAuthLevel.PREMIUM) {
this.authLevel = UserAuthLevel.STANDARD;
this.premiumExpirationDate = undefined;
}
}
// Company registration methods
hasCompany(): boolean {
return this.CompanyId !== undefined && this.CompanyId !== null;
}
isCompanyRegistered(): boolean {
return this.companyRegistered === true;
}
needsCompanyReregistration(): boolean {
if (!this.hasCompany() || !this.companyRegistrationDate) {
return false;
}
const oneMonthAgo = new Date();
oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1);
return this.companyRegistrationDate < oneMonthAgo;
}
resetCompanyRegistration(): void {
this.companyRegistered = false;
this.companyRegistrationDate = undefined;
}
registerCompany(companyId: number): void {
this.CompanyId = companyId;
}
verifyCompanyRegistration(): void {
this.companyRegistered = true;
this.companyRegistrationDate = new Date();
}
// Contact methods
addContact(contact: Contact): void {
if (!this.contacts) {
this.contacts = [];
}
this.contacts.push(contact);
}
getContactsData(): Contact[] {
return this.contacts || [];
}
// Soft delete helper methods
softDelete(): void {
this.deletedAt = new Date();
this.status = UserStatus.DELETED;
}
restoreFromSoftDelete(): void {
this.deletedAt = undefined;
if (this.status === UserStatus.DELETED) {
this.status = UserStatus.CONFIRMED;
}
}
}
@@ -0,0 +1,237 @@
# Database Mappers
This directory contains mapper classes that handle the conversion between DTOs (Data Transfer Objects) and entities. Each mapper provides methods to convert between different data representations, ensuring clean separation of concerns and proper data validation.
## Overview
The mappers are designed to:
- Convert DTOs to entities for database operations
- Convert entities to DTOs for API responses
- Handle data transformations and validations
- Provide utility methods for common operations
- Support filtering and pagination
## Available Mappers
### 1. UserMapper (`user.mapper.ts`)
Handles user-related data transformations including authentication DTOs.
**Key Methods:**
- `toEntity(createUserDto)` - Convert CreateUserDto to User entity
- `toResponseDto(user)` - Convert User entity to UserResponseDto
- `toJwtTokenDto(user)` - Convert User entity to JWT token payload
- `toLoginResponseDto(user, token)` - Create login response with token
- `updateEntity(user, updateUserDto)` - Update User entity with DTO data
- `toPublicResponseDto(user)` - Create sanitized public response
### 2. CompanyMapper (`company.mapper.ts`)
Handles company-related data transformations.
**Key Methods:**
- `toEntity(createCompanyDto)` - Convert CreateCompanyDto to Company entity
- `toResponseDto(company)` - Convert Company entity to CompanyResponseDto
- `toSummaryDto(company)` - Convert Company entity to CompanySummaryDto
- `toStatisticsDto(company, stats)` - Create statistics DTO
- `updateEntity(company, updateCompanyDto)` - Update Company entity
- `toPublicResponseDto(company)` - Create public company response
### 3. CardsMapper (`cards.mapper.ts`)
Handles cards/question bank data transformations.
**Key Methods:**
- `toEntity(createCardsDto)` - Convert CreateCardsDto to Cards entity
- `toResponseDto(card)` - Convert Cards entity to CardsResponseDto
- `toSummaryDto(card)` - Convert Cards entity to CardsSummaryDto
- `updateEntity(card, updateCardsDto)` - Update Cards entity
- `canUserAccessCard(card, userId, userCompanyId)` - Check access permissions
- `toFilteredResponseDto(card, userId, userCompanyId)` - Create filtered response
### 4. ChatMapper (`chat.mapper.ts`)
Handles chat message data transformations.
**Key Methods:**
- `toEntity(createChatDto)` - Convert CreateChatDto to Chat entity
- `toResponseDto(chat)` - Convert Chat entity to ChatResponseDto
- `toSummaryDto(chat)` - Convert Chat entity to ChatSummaryDto
- `updateEntity(chat, updateChatDto)` - Update Chat entity
- `groupChatsByRoom(chats)` - Group chats by room/chatuuid
- `getChatRoomSummary(chats)` - Get room statistics
### 5. ContactMapper (`contact.mapper.ts`)
Handles contact form data transformations.
**Key Methods:**
- `toEntity(createContactDto)` - Convert CreateContactDto to Contact entity
- `toResponseDto(contact)` - Convert Contact entity to ContactResponseDto
- `toSummaryDto(contact)` - Convert Contact entity to ContactSummaryDto
- `updateEntity(contact, updateContactDto)` - Update Contact entity
- `getContactPriority(contact)` - Determine contact priority level
- `getContactFrequencyStats(contacts)` - Calculate frequency statistics
### 6. CompanyContactMapper (`companyContact.mapper.ts`)
Handles company contact form data transformations.
**Key Methods:**
- `toEntity(createCompanyContactDto)` - Convert DTO to CompanyContact entity
- `toResponseDto(companyContact)` - Convert entity to response DTO
- `toSummaryDto(companyContact)` - Convert entity to summary DTO
- `updateEntity(companyContact, updateDto)` - Update entity
- `groupContactsByCompany(contacts)` - Group contacts by company
- `getContactPriority(contact)` - Determine contact priority
## Common Features
All mappers include:
### Array Conversion Methods
- `toResponseDtoArray(entities)` - Convert entity arrays to response DTO arrays
- `toSummaryDtoArray(entities)` - Convert entity arrays to summary DTO arrays
### Query Parameter Conversion
- `toQueryParams(requestDto)` - Convert request DTOs to query parameters
- `toFilterConditions(filterDto)` - Convert filter DTOs to query conditions
### Deletion Support
- `toRemoveParams(removeDto)` - Convert remove DTOs to deletion parameters
### Validation Methods
- `canMapToResponse(entity)` - Check if entity can be mapped to response
- Various permission checking methods
## Utility Classes
### MapperUtils
Provides common utility methods for all mappers:
- `applyPagination(items, page, limit)` - Apply pagination to arrays
- `applySorting(items, sortBy, sortOrder)` - Apply sorting to arrays
- `applyTextFilter(items, searchText, searchFields)` - Apply text filtering
- `applyDateRangeFilter(items, dateField, fromDate, toDate)` - Apply date filtering
- `generateMessagePreview(message, length)` - Generate message previews
- `getTimeAgo(date)` - Get human-readable time difference
- `groupBy(array, property)` - Group array by property
- `calculateStats(numbers)` - Calculate min/max/avg/sum statistics
## Usage Examples
### Basic Entity to DTO Conversion
```typescript
import { UserMapper } from './mappers';
// Convert user entity to response DTO
const user = await userRepository.findOne(userId);
const userResponse = UserMapper.toResponseDto(user);
// Convert user entity array to response DTO array
const users = await userRepository.find();
const usersResponse = UserMapper.toResponseDtoArray(users);
```
### Creating New Entities
```typescript
import { CompanyMapper } from './mappers';
// Convert create DTO to entity
const createCompanyDto = new CreateCompanyDto();
// ... populate DTO
const company = CompanyMapper.toEntity(createCompanyDto);
const savedCompany = await companyRepository.save(company);
```
### Updating Existing Entities
```typescript
import { CardsMapper } from './mappers';
// Update existing entity with DTO data
const card = await cardsRepository.findOne(cardId);
const updateCardsDto = new UpdateCardsDto();
// ... populate DTO
const updatedCard = CardsMapper.updateEntity(card, updateCardsDto);
await cardsRepository.save(updatedCard);
```
### Authentication and JWT
```typescript
import { UserMapper } from './mappers';
// Create JWT token payload
const user = await userRepository.findOne(userId);
const jwtPayload = UserMapper.toJwtTokenDto(user);
// Create login response with token
const token = jwt.sign(jwtPayload, JWT_SECRET);
const loginResponse = UserMapper.toLoginResponseDto(user, token);
```
### Filtering and Permissions
```typescript
import { CardsMapper } from './mappers';
// Filter cards based on user permissions
const cards = await cardsRepository.find();
const filteredCards = cards
.map(card => CardsMapper.toFilteredResponseDto(card, userId, userCompanyId))
.filter(card => card !== null);
```
### Statistics and Analytics
```typescript
import { ContactMapper, MapperUtils } from './mappers';
// Get contact frequency statistics
const contacts = await contactRepository.find();
const stats = ContactMapper.getContactFrequencyStats(contacts);
// Group contacts by domain
const domainGroups = ContactMapper.groupContactsByDomain(contacts);
// Apply pagination and sorting
const paginatedContacts = MapperUtils.applyPagination(contacts, page, limit);
const sortedContacts = MapperUtils.applySorting(paginatedContacts, 'createdAt', 'DESC');
```
## Best Practices
1. **Always use mappers** for data conversion instead of manual mapping
2. **Validate permissions** before mapping sensitive data
3. **Use summary DTOs** for list views to reduce payload size
4. **Handle null/undefined values** gracefully in mapper methods
5. **Include related data** only when necessary using conditional mapping
6. **Use utility methods** for common operations like pagination and sorting
7. **Implement proper error handling** for invalid data transformations
8. **Keep mappers stateless** - all methods should be static
9. **Document custom mapper methods** for complex business logic
10. **Test mappers thoroughly** with various data scenarios
## Error Handling
All mappers include basic error handling, but you should wrap mapper calls in try-catch blocks:
```typescript
try {
const responseDto = UserMapper.toResponseDto(user);
return responseDto;
} catch (error) {
console.error('Mapping error:', error);
throw new Error('Failed to map user data');
}
```
## Extension Points
To add new mapping functionality:
1. Add new methods to existing mappers
2. Create new mapper classes for new entities
3. Update the index.ts file to export new mappers
4. Add utility methods to MapperUtils for common operations
5. Update this README with new functionality
## Dependencies
The mappers depend on:
- Entity classes from `../entities/`
- DTO classes from `../dto/`
- TypeScript decorators from `class-validator` and `class-transformer`
- TypeORM entity relations
Make sure all dependencies are properly installed and configured before using the mappers.
@@ -0,0 +1,163 @@
import { Cards, Privacy, CardType } from '../entities/cards.entity';
import {
CreateCardsDto,
UpdateCardsDto,
CardsResponseDto,
CardsSummaryDto,
CardsRequestDto,
CardsStatisticsDto,
CardsFilterDto,
RemoveCardsDto
} from '../dto/cards.dto';
export class CardsMapper {
static toEntity(createCardsDto: CreateCardsDto): Cards {
const card = new Cards();
card.Title = createCardsDto.Title;
card.Description = createCardsDto.Description;
card.Privacy = createCardsDto.Privacy;
card.Type = createCardsDto.Type;
card.Creator = createCardsDto.Creator;
card.no_question = createCardsDto.no_question;
card.CompanyId = createCardsDto.CompanyId;
card.Creation_Date = new Date();
return card;
}
static toResponseDto(card: Cards): CardsResponseDto {
const dto = new CardsResponseDto();
dto.CardId = card.CardId;
dto.guid = card.guid;
dto.Title = card.Title;
dto.Description = card.Description;
dto.Privacy = card.Privacy;
dto.Type = card.Type;
dto.Creator = card.Creator;
dto.Creation_Date = card.Creation_Date;
dto.no_question = card.no_question;
dto.CompanyId = card.CompanyId;
if (card.creator) {
dto.creatorUsername = card.creator.username;
dto.creatorName = `${card.creator.FirstName} ${card.creator.LastName}`;
}
if (card.company) {
dto.companyName = card.company.Name;
}
return dto;
}
static toSummaryDto(card: Cards): CardsSummaryDto {
const dto = new CardsSummaryDto();
dto.CardId = card.CardId;
dto.guid = card.guid;
dto.Title = card.Title;
dto.Privacy = card.Privacy;
dto.Type = card.Type;
dto.no_question = card.no_question;
dto.Creation_Date = card.Creation_Date;
dto.creatorUsername = card.creator?.username || '';
dto.companyName = card.company?.Name;
return dto;
}
static updateEntity(card: Cards, updateCardsDto: UpdateCardsDto): Cards {
if (updateCardsDto.Title !== undefined) card.Title = updateCardsDto.Title;
if (updateCardsDto.Description !== undefined) card.Description = updateCardsDto.Description;
if (updateCardsDto.Privacy !== undefined) card.Privacy = updateCardsDto.Privacy;
if (updateCardsDto.Type !== undefined) card.Type = updateCardsDto.Type;
if (updateCardsDto.no_question !== undefined) card.no_question = updateCardsDto.no_question;
if (updateCardsDto.CompanyId !== undefined) card.CompanyId = updateCardsDto.CompanyId;
return card;
}
static toResponseDtoArray(cards: Cards[]): CardsResponseDto[] {
return cards.map(card => this.toResponseDto(card));
}
static toSummaryDtoArray(cards: Cards[]): CardsSummaryDto[] {
return cards.map(card => this.toSummaryDto(card));
}
static toQueryParams(requestDto: CardsRequestDto): any {
const params: any = {};
if (requestDto.guid) params.guid = requestDto.guid;
if (requestDto.Title) params.Title = requestDto.Title;
if (requestDto.Privacy !== undefined) params.Privacy = requestDto.Privacy;
if (requestDto.Type !== undefined) params.Type = requestDto.Type;
if (requestDto.Creator) params.Creator = requestDto.Creator;
if (requestDto.CompanyId) params.CompanyId = requestDto.CompanyId;
if (requestDto.CreationDateFrom) params.CreationDateFrom = requestDto.CreationDateFrom;
if (requestDto.CreationDateTo) params.CreationDateTo = requestDto.CreationDateTo;
return params;
}
static toFilterConditions(filterDto: CardsFilterDto): any {
const conditions: any = {};
if (filterDto.privacy !== undefined) conditions.Privacy = filterDto.privacy;
if (filterDto.type !== undefined) conditions.Type = filterDto.type;
if (filterDto.creatorId) conditions.Creator = filterDto.creatorId;
if (filterDto.companyId) conditions.CompanyId = filterDto.companyId;
if (filterDto.minQuestions) conditions.minQuestions = filterDto.minQuestions;
if (filterDto.maxQuestions) conditions.maxQuestions = filterDto.maxQuestions;
if (filterDto.searchTitle) conditions.searchTitle = `%${filterDto.searchTitle}%`;
if (filterDto.searchDescription) conditions.searchDescription = `%${filterDto.searchDescription}%`;
return conditions;
}
static toRemoveParams(removeDto: RemoveCardsDto): any {
return {
guid: removeDto.guid,
reason: removeDto.reason,
hardDelete: removeDto.hardDelete || false
};
}
static createStatisticsDto(stats: any): CardsStatisticsDto {
const dto = new CardsStatisticsDto();
dto.totalCards = stats.totalCards || 0;
dto.publicCards = stats.publicCards || 0;
dto.companyCards = stats.companyCards || 0;
dto.privateCards = stats.privateCards || 0;
dto.questionCards = stats.questionCards || 0;
dto.jokerCards = stats.jokerCards || 0;
dto.luckCards = stats.luckCards || 0;
dto.totalQuestions = stats.totalQuestions || 0;
dto.averageQuestionsPerCard = stats.averageQuestionsPerCard || 0;
return dto;
}
static canMapToResponse(card: Cards): boolean {
return card.guid !== null && card.guid !== undefined;
}
static canUserAccessCard(card: Cards, userId: number, userCompanyId?: number): boolean {
if (card.Privacy === Privacy.Public) return true;
if (card.Privacy === Privacy.Private && card.Creator === userId) return true;
if (card.Privacy === Privacy.Company && card.CompanyId === userCompanyId) return true;
return false;
}
static getCardTypeDisplayName(cardType: CardType): string {
switch (cardType) {
case CardType.Question:
return 'Question Card';
case CardType.Joker:
return 'Joker Card';
case CardType.Luck:
return 'Luck Card';
default:
return 'Unknown Card';
}
}
static getPrivacyDisplayName(privacy: Privacy): string {
switch (privacy) {
case Privacy.Public:
return 'Public';
case Privacy.Private:
return 'Private';
case Privacy.Company:
return 'Company';
default:
return 'Unknown';
}
}
static toFilteredResponseDto(card: Cards, userId: number, userCompanyId?: number): CardsResponseDto | null {
if (!this.canUserAccessCard(card, userId, userCompanyId)) {
return null;
}
return this.toResponseDto(card);
}
}
@@ -0,0 +1,170 @@
import { Chat } from '../entities/chat.entity';
import {
CreateChatDto,
UpdateChatDto,
ChatResponseDto,
ChatSummaryDto,
ChatRequestDto,
ChatStatisticsDto,
ChatFilterDto,
ChatRoomDto,
RemoveChatDto
} from '../dto/chat.dto';
export class ChatMapper {
static toEntity(createChatDto: CreateChatDto): Chat {
const chat = new Chat();
chat.Chatuuid = createChatDto.Chatuuid;
chat.message = createChatDto.message;
chat.UserId = createChatDto.UserId;
chat.userId = createChatDto.UserId; // Handle both field names
return chat;
}
static toResponseDto(chat: Chat): ChatResponseDto {
const dto = new ChatResponseDto();
dto.id = chat.id;
dto.guid = chat.guid;
dto.Chatuuid = chat.Chatuuid;
dto.message = chat.message;
dto.createdAt = chat.createdAt;
dto.updatedAt = chat.updatedAt;
dto.UserId = chat.UserId || chat.userId;
if (chat.user) {
dto.username = chat.user.username;
dto.userFullName = `${chat.user.FirstName} ${chat.user.LastName}`;
}
return dto;
}
static toSummaryDto(chat: Chat): ChatSummaryDto {
const dto = new ChatSummaryDto();
dto.id = chat.id;
dto.guid = chat.guid;
dto.Chatuuid = chat.Chatuuid;
dto.message = chat.message;
dto.createdAt = chat.createdAt;
dto.username = chat.user?.username || '';
return dto;
}
static updateEntity(chat: Chat, updateChatDto: UpdateChatDto): Chat {
if (updateChatDto.message !== undefined) chat.message = updateChatDto.message;
if (updateChatDto.Chatuuid !== undefined) chat.Chatuuid = updateChatDto.Chatuuid;
return chat;
}
static toResponseDtoArray(chats: Chat[]): ChatResponseDto[] {
return chats.map(chat => this.toResponseDto(chat));
}
static toSummaryDtoArray(chats: Chat[]): ChatSummaryDto[] {
return chats.map(chat => this.toSummaryDto(chat));
}
static toQueryParams(requestDto: ChatRequestDto): any {
const params: any = {};
if (requestDto.guid) params.guid = requestDto.guid;
if (requestDto.Chatuuid) params.Chatuuid = requestDto.Chatuuid;
if (requestDto.UserId) params.UserId = requestDto.UserId;
if (requestDto.createdAfter) params.createdAfter = requestDto.createdAfter;
if (requestDto.createdBefore) params.createdBefore = requestDto.createdBefore;
if (requestDto.searchMessage) params.searchMessage = `%${requestDto.searchMessage}%`;
return params;
}
static toFilterConditions(filterDto: ChatFilterDto): any {
const conditions: any = {};
if (filterDto.Chatuuid) conditions.Chatuuid = filterDto.Chatuuid;
if (filterDto.UserId) conditions.UserId = filterDto.UserId;
if (filterDto.messageContains) conditions.messageContains = `%${filterDto.messageContains}%`;
if (filterDto.createdAfter) conditions.createdAfter = filterDto.createdAfter;
if (filterDto.createdBefore) conditions.createdBefore = filterDto.createdBefore;
if (filterDto.sortBy) conditions.sortBy = filterDto.sortBy;
if (filterDto.sortOrder) conditions.sortOrder = filterDto.sortOrder;
return conditions;
}
static toRemoveParams(removeDto: RemoveChatDto): any {
return {
guid: removeDto.guid,
reason: removeDto.reason,
hardDelete: removeDto.hardDelete || false
};
}
static createStatisticsDto(stats: any): ChatStatisticsDto {
const dto = new ChatStatisticsDto();
dto.totalChats = stats.totalChats || 0;
dto.totalChatRooms = stats.totalChatRooms || 0;
dto.activeUsers = stats.activeUsers || 0;
dto.messagesLastHour = stats.messagesLastHour || 0;
dto.messagesLastDay = stats.messagesLastDay || 0;
dto.messagesLastWeek = stats.messagesLastWeek || 0;
dto.mostActiveRoom = stats.mostActiveRoom || '';
dto.averageMessagesPerRoom = stats.averageMessagesPerRoom || 0;
return dto;
}
static createChatRoomDto(chatUuid: string, stats: any): ChatRoomDto {
const dto = new ChatRoomDto();
dto.Chatuuid = chatUuid;
dto.messageCount = stats.messageCount || 0;
dto.participantCount = stats.participantCount || 0;
dto.lastActivity = stats.lastActivity || new Date();
dto.createdAt = stats.createdAt || new Date();
dto.roomName = stats.roomName;
dto.roomDescription = stats.roomDescription;
return dto;
}
static groupChatsByRoom(chats: Chat[]): Map<string, Chat[]> {
const roomMap = new Map<string, Chat[]>();
chats.forEach(chat => {
if (!roomMap.has(chat.Chatuuid)) {
roomMap.set(chat.Chatuuid, []);
}
roomMap.get(chat.Chatuuid)!.push(chat);
});
return roomMap;
}
static getChatRoomSummary(chats: Chat[]): ChatRoomDto[] {
const roomMap = this.groupChatsByRoom(chats);
const roomSummaries: ChatRoomDto[] = [];
roomMap.forEach((roomChats, chatUuid) => {
const uniqueUsers = new Set(roomChats.map(chat => chat.UserId || chat.userId));
const lastActivity = roomChats.reduce((latest, chat) =>
chat.createdAt > latest ? chat.createdAt : latest,
roomChats[0].createdAt
);
const firstActivity = roomChats.reduce((earliest, chat) =>
chat.createdAt < earliest ? chat.createdAt : earliest,
roomChats[0].createdAt
);
const roomDto = new ChatRoomDto();
roomDto.Chatuuid = chatUuid;
roomDto.messageCount = roomChats.length;
roomDto.participantCount = uniqueUsers.size;
roomDto.lastActivity = lastActivity;
roomDto.createdAt = firstActivity;
roomSummaries.push(roomDto);
});
return roomSummaries;
}
static canMapToResponse(chat: Chat): boolean {
return chat.guid !== null && chat.guid !== undefined;
}
static canUserAccessChat(chat: Chat, userId: number): boolean {
return true;
}
static getMessagePreview(message: string, length: number = 100): string {
if (message.length <= length) return message;
return message.substring(0, length) + '...';
}
static toFilteredResponseDto(chat: Chat, userId: number): ChatResponseDto | null {
if (!this.canUserAccessChat(chat, userId)) {
return null;
}
return this.toResponseDto(chat);
}
}
@@ -1,43 +1,117 @@
import { Company } from '../entities/company.entity';
import { CompanyBasicDto, CompanyUpdateDto, CompanyCreateDto, CompanyResponseDto } from '../dto/company.dto';
import {
CreateCompanyDto,
UpdateCompanyDto,
CompanyResponseDto,
CompanySummaryDto,
CompanyRequestDto,
CompanyStatisticsDto,
RemoveCompanyDto
} from '../dto/company.dto';
export class CompanyMapper {
static toBasicDto(company: Company): CompanyBasicDto {
return new CompanyBasicDto(company.CompanyId, company.Name);
}
static toResponseDto(company: Company): CompanyResponseDto {
return new CompanyResponseDto(
company.CompanyId,
company.Name,
company.ContactFirstName,
company.ContactLastName,
company.ContactEmail,
company.FirstAPI,
company.TokenAPI,
company.RegDate
);
}
static toEntity(createDto: CompanyCreateDto): Company {
const company = new Company();
company.Name = createDto.Name;
company.ContactFirstName = createDto.ContactFirstName;
company.ContactLastName = createDto.ContactLastName;
company.ContactEmail = createDto.ContactEmail;
company.FirstAPI = createDto.FirstAPI;
company.TokenAPI = createDto.TokenAPI;
return company;
}
static updateEntity(original: Company, updateDto: CompanyUpdateDto): Company {
if (updateDto.Name !== undefined) original.Name = updateDto.Name;
if (updateDto.ContactFirstName !== undefined) original.ContactFirstName = updateDto.ContactFirstName;
if (updateDto.ContactLastName !== undefined) original.ContactLastName = updateDto.ContactLastName;
if (updateDto.ContactEmail !== undefined) original.ContactEmail = updateDto.ContactEmail;
if (updateDto.FirstAPI !== undefined) original.FirstAPI = updateDto.FirstAPI;
if (updateDto.TokenAPI !== undefined) original.TokenAPI = updateDto.TokenAPI;
return original;
}
}
static toEntity(createCompanyDto: CreateCompanyDto): Company {
const company = new Company();
company.Name = createCompanyDto.Name;
company.ContactFirstName = createCompanyDto.ContactFirstName;
company.ContactLastName = createCompanyDto.ContactLastName;
company.ContactEmail = createCompanyDto.ContactEmail;
company.LoginURL = createCompanyDto.LoginURL;
return company;
}
static toResponseDto(company: Company): CompanyResponseDto {
const dto = new CompanyResponseDto();
dto.CompanyId = company.CompanyId;
dto.GUID = company.GUID;
dto.Name = company.Name;
dto.ContactFirstName = company.ContactFirstName;
dto.ContactLastName = company.ContactLastName;
dto.ContactEmail = company.ContactEmail;
dto.LoginURL = company.LoginURL;
dto.RegDate = company.RegDate;
dto.UpdatedDate = company.UpdatedDate;
dto.userCount = company.users?.length || 0;
dto.cardCount = company.cards?.length || 0;
return dto;
}
static toSummaryDto(company: Company): CompanySummaryDto {
const dto = new CompanySummaryDto();
dto.CompanyId = company.CompanyId;
dto.GUID = company.GUID;
dto.Name = company.Name;
dto.ContactEmail = company.ContactEmail;
dto.RegDate = company.RegDate;
dto.userCount = company.users?.length || 0;
dto.cardCount = company.cards?.length || 0;
return dto;
}
static toStatisticsDto(company: Company, stats: any): CompanyStatisticsDto {
const dto = new CompanyStatisticsDto();
dto.CompanyId = company.CompanyId;
dto.Name = company.Name;
dto.totalUsers = stats.totalUsers || 0;
dto.activeUsers = stats.activeUsers || 0;
dto.premiumUsers = stats.premiumUsers || 0;
dto.totalCards = stats.totalCards || 0;
dto.publicCards = stats.publicCards || 0;
dto.companyCards = stats.companyCards || 0;
dto.privateCards = stats.privateCards || 0;
dto.lastActivity = stats.lastActivity || new Date();
return dto;
}
static updateEntity(company: Company, updateCompanyDto: UpdateCompanyDto): Company {
if (updateCompanyDto.Name !== undefined) company.Name = updateCompanyDto.Name;
if (updateCompanyDto.ContactFirstName !== undefined) company.ContactFirstName = updateCompanyDto.ContactFirstName;
if (updateCompanyDto.ContactLastName !== undefined) company.ContactLastName = updateCompanyDto.ContactLastName;
if (updateCompanyDto.ContactEmail !== undefined) company.ContactEmail = updateCompanyDto.ContactEmail;
if (updateCompanyDto.LoginURL !== undefined) company.LoginURL = updateCompanyDto.LoginURL;
return company;
}
static toResponseDtoArray(companies: Company[]): CompanyResponseDto[] {
return companies.map(company => this.toResponseDto(company));
}
static toSummaryDtoArray(companies: Company[]): CompanySummaryDto[] {
return companies.map(company => this.toSummaryDto(company));
}
static toQueryParams(requestDto: CompanyRequestDto): any {
const params: any = {};
if (requestDto.guid) params.GUID = requestDto.guid;
if (requestDto.Name) params.Name = requestDto.Name;
if (requestDto.ContactEmail) params.ContactEmail = requestDto.ContactEmail;
if (requestDto.RegDateFrom) params.RegDateFrom = requestDto.RegDateFrom;
if (requestDto.RegDateTo) params.RegDateTo = requestDto.RegDateTo;
return params;
}
static toRemoveParams(removeDto: RemoveCompanyDto): any {
return {
guid: removeDto.guid,
reason: removeDto.reason,
hardDelete: removeDto.hardDelete || false
};
}
static getFullContactName(company: Company): string {
return `${company.ContactFirstName} ${company.ContactLastName}`;
}
static canMapToResponse(company: Company): boolean {
return company.GUID !== null && company.GUID !== undefined;
}
static toResponseDtoWithRelations(company: Company): CompanyResponseDto {
const dto = this.toResponseDto(company);
return dto;
}
static toPublicResponseDto(company: Company): Partial<CompanyResponseDto> {
const dto = this.toResponseDto(company);
const publicDto: Partial<CompanyResponseDto> = {
CompanyId: dto.CompanyId,
GUID: dto.GUID,
Name: dto.Name,
RegDate: dto.RegDate,
userCount: dto.userCount,
cardCount: dto.cardCount
};
return publicDto;
}
}
@@ -0,0 +1,215 @@
import { CompanyContact } from '../entities/companyContact.entity';
import {
CreateCompanyContactDto,
UpdateCompanyContactDto,
CompanyContactResponseDto,
CompanyContactSummaryDto,
CompanyContactRequestDto,
CompanyContactStatisticsDto,
CompanyContactFilterDto,
CompanyContactsByCompanyDto,
RemoveCompanyContactDto
} from '../dto/companyContact.dto';
export class CompanyContactMapper {
static toEntity(createCompanyContactDto: CreateCompanyContactDto): CompanyContact {
const companyContact = new CompanyContact();
companyContact.CompanyName = createCompanyContactDto.name; // Note: field name difference
companyContact.ContactFirstName = createCompanyContactDto.name; // Adjust as needed
companyContact.ContactLastName = ''; // You may need to split the name
companyContact.email = createCompanyContactDto.email;
companyContact.phone = ''; // Default empty, add to DTO if needed
companyContact.message = createCompanyContactDto.message;
return companyContact;
}
static toResponseDto(companyContact: CompanyContact): CompanyContactResponseDto {
const dto = new CompanyContactResponseDto();
dto.id = companyContact.id;
dto.guid = companyContact.guid;
dto.name = `${companyContact.ContactFirstName} ${companyContact.ContactLastName}`.trim();
dto.email = companyContact.email;
dto.message = companyContact.message || '';
dto.createdAt = companyContact.createdAt;
dto.updatedAt = companyContact.createdAt; // CompanyContact doesn't have updatedAt
dto.companyId = 0; // CompanyContact doesn't have companyId, adjust as needed
dto.companyName = companyContact.CompanyName;
dto.companyGuid = ''; // Not available in current entity
if (companyContact.createdBy) {
dto.userId = companyContact.createdBy.id;
dto.username = companyContact.createdBy.username;
dto.userFullName = `${companyContact.createdBy.FirstName} ${companyContact.createdBy.LastName}`;
}
return dto;
}
static toSummaryDto(companyContact: CompanyContact): CompanyContactSummaryDto {
const dto = new CompanyContactSummaryDto();
dto.id = companyContact.id;
dto.guid = companyContact.guid;
dto.name = `${companyContact.ContactFirstName} ${companyContact.ContactLastName}`.trim();
dto.email = companyContact.email;
dto.messagePreview = this.getMessagePreview(companyContact.message || '');
dto.createdAt = companyContact.createdAt;
dto.companyName = companyContact.CompanyName;
dto.username = companyContact.createdBy?.username;
return dto;
}
static updateEntity(companyContact: CompanyContact, updateCompanyContactDto: UpdateCompanyContactDto): CompanyContact {
if (updateCompanyContactDto.name !== undefined) {
companyContact.ContactFirstName = updateCompanyContactDto.name;
}
if (updateCompanyContactDto.email !== undefined) companyContact.email = updateCompanyContactDto.email;
if (updateCompanyContactDto.message !== undefined) companyContact.message = updateCompanyContactDto.message;
return companyContact;
}
static toResponseDtoArray(companyContacts: CompanyContact[]): CompanyContactResponseDto[] {
return companyContacts.map(companyContact => this.toResponseDto(companyContact));
}
static toSummaryDtoArray(companyContacts: CompanyContact[]): CompanyContactSummaryDto[] {
return companyContacts.map(companyContact => this.toSummaryDto(companyContact));
}
static toQueryParams(requestDto: CompanyContactRequestDto): any {
const params: any = {};
if (requestDto.guid) params.guid = requestDto.guid;
if (requestDto.name) params.name = requestDto.name;
if (requestDto.email) params.email = requestDto.email;
if (requestDto.companyId) params.companyId = requestDto.companyId;
if (requestDto.userId) params.userId = requestDto.userId;
if (requestDto.createdAfter) params.createdAfter = requestDto.createdAfter;
if (requestDto.createdBefore) params.createdBefore = requestDto.createdBefore;
if (requestDto.searchMessage) params.searchMessage = `%${requestDto.searchMessage}%`;
return params;
}
static toFilterConditions(filterDto: CompanyContactFilterDto): any {
const conditions: any = {};
if (filterDto.nameContains) conditions.nameContains = `%${filterDto.nameContains}%`;
if (filterDto.email) conditions.email = filterDto.email;
if (filterDto.emailDomain) conditions.emailDomain = `%@${filterDto.emailDomain}`;
if (filterDto.messageContains) conditions.messageContains = `%${filterDto.messageContains}%`;
if (filterDto.companyId) conditions.companyId = filterDto.companyId;
if (filterDto.companyName) conditions.companyName = `%${filterDto.companyName}%`;
if (filterDto.userId) conditions.userId = filterDto.userId;
if (filterDto.isRegisteredUser !== undefined) conditions.isRegisteredUser = filterDto.isRegisteredUser;
if (filterDto.createdAfter) conditions.createdAfter = filterDto.createdAfter;
if (filterDto.createdBefore) conditions.createdBefore = filterDto.createdBefore;
if (filterDto.minMessageLength) conditions.minMessageLength = filterDto.minMessageLength;
if (filterDto.maxMessageLength) conditions.maxMessageLength = filterDto.maxMessageLength;
if (filterDto.sortBy) conditions.sortBy = filterDto.sortBy;
if (filterDto.sortOrder) conditions.sortOrder = filterDto.sortOrder;
return conditions;
}
static toCompanyQueryParams(requestDto: CompanyContactsByCompanyDto): any {
const params: any = {};
params.companyId = requestDto.companyId;
if (requestDto.fromDate) params.fromDate = requestDto.fromDate;
if (requestDto.toDate) params.toDate = requestDto.toDate;
if (requestDto.page) params.page = requestDto.page;
if (requestDto.limit) params.limit = requestDto.limit;
if (requestDto.sortBy) params.sortBy = requestDto.sortBy;
if (requestDto.sortOrder) params.sortOrder = requestDto.sortOrder;
return params;
}
static toRemoveParams(removeDto: RemoveCompanyContactDto): any {
return {
guid: removeDto.guid,
reason: removeDto.reason,
hardDelete: removeDto.hardDelete || false
};
}
static createStatisticsDto(stats: any): CompanyContactStatisticsDto {
const dto = new CompanyContactStatisticsDto();
dto.totalContacts = stats.totalContacts || 0;
dto.contactsLastHour = stats.contactsLastHour || 0;
dto.contactsLastDay = stats.contactsLastDay || 0;
dto.contactsLastWeek = stats.contactsLastWeek || 0;
dto.contactsLastMonth = stats.contactsLastMonth || 0;
dto.registeredUserContacts = stats.registeredUserContacts || 0;
dto.anonymousContacts = stats.anonymousContacts || 0;
dto.averageMessageLength = stats.averageMessageLength || 0;
dto.mostActiveDay = stats.mostActiveDay || '';
dto.uniqueCompanies = stats.uniqueCompanies || 0;
dto.topCompanyByContacts = stats.topCompanyByContacts || '';
return dto;
}
static getMessagePreview(message: string, length: number = 100): string {
if (message.length <= length) return message;
return message.substring(0, length) + '...';
}
static canMapToResponse(companyContact: CompanyContact): boolean {
return companyContact.guid !== null && companyContact.guid !== undefined;
}
static isFromRegisteredUser(companyContact: CompanyContact): boolean {
return companyContact.createdBy !== null && companyContact.createdBy !== undefined;
}
static extractEmailDomain(email: string): string {
const parts = email.split('@');
return parts.length === 2 ? parts[1] : '';
}
static getFullContactName(companyContact: CompanyContact): string {
return `${companyContact.ContactFirstName} ${companyContact.ContactLastName}`.trim();
}
static getContactPriority(companyContact: CompanyContact): 'high' | 'medium' | 'low' {
if (!companyContact.message) return 'low';
const message = companyContact.message.toLowerCase();
const highPriorityKeywords = ['urgent', 'emergency', 'critical', 'important', 'asap'];
const mediumPriorityKeywords = ['question', 'problem', 'issue', 'help', 'support'];
if (highPriorityKeywords.some(keyword => message.includes(keyword))) {
return 'high';
}
if (mediumPriorityKeywords.some(keyword => message.includes(keyword))) {
return 'medium';
}
return 'low';
}
static groupContactsByCompany(companyContacts: CompanyContact[]): Map<string, CompanyContact[]> {
const companyMap = new Map<string, CompanyContact[]>();
companyContacts.forEach(contact => {
const companyName = contact.CompanyName;
if (!companyMap.has(companyName)) {
companyMap.set(companyName, []);
}
companyMap.get(companyName)!.push(contact);
});
return companyMap;
}
static getContactFrequencyStats(companyContacts: CompanyContact[]): any {
const now = new Date();
const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000);
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
const oneWeekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
const oneMonthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
return {
lastHour: companyContacts.filter(c => c.createdAt >= oneHourAgo).length,
lastDay: companyContacts.filter(c => c.createdAt >= oneDayAgo).length,
lastWeek: companyContacts.filter(c => c.createdAt >= oneWeekAgo).length,
lastMonth: companyContacts.filter(c => c.createdAt >= oneMonthAgo).length,
total: companyContacts.length
};
}
static toFilteredResponseDto(companyContact: CompanyContact, canViewFullDetails: boolean): CompanyContactResponseDto | any {
if (canViewFullDetails) {
return this.toResponseDto(companyContact);
}
const dto = this.toResponseDto(companyContact);
return {
id: dto.id,
guid: dto.guid,
name: dto.name,
companyName: dto.companyName,
createdAt: dto.createdAt,
messagePreview: this.getMessagePreview(dto.message, 50)
};
}
}
@@ -0,0 +1,180 @@
import { Contact } from '../entities/contact.entity';
import {
CreateContactDto,
UpdateContactDto,
ContactResponseDto,
ContactSummaryDto,
ContactRequestDto,
ContactStatisticsDto,
ContactFilterDto,
RemoveContactDto
} from '../dto/contact.dto';
export class ContactMapper {
static toEntity(createContactDto: CreateContactDto): Contact {
const contact = new Contact();
contact.name = createContactDto.name;
contact.email = createContactDto.email;
contact.message = createContactDto.message;
return contact;
}
static toResponseDto(contact: Contact): ContactResponseDto {
const dto = new ContactResponseDto();
dto.id = contact.id;
dto.guid = contact.guid;
dto.name = contact.name;
dto.email = contact.email;
dto.message = contact.message;
dto.createdAt = contact.createdAt;
dto.updatedAt = contact.updatedAt;
if (contact.user) {
dto.userId = contact.user.id;
dto.username = contact.user.username;
dto.userFullName = `${contact.user.FirstName} ${contact.user.LastName}`;
}
return dto;
}
static toSummaryDto(contact: Contact): ContactSummaryDto {
const dto = new ContactSummaryDto();
dto.id = contact.id;
dto.guid = contact.guid;
dto.name = contact.name;
dto.email = contact.email;
dto.messagePreview = this.getMessagePreview(contact.message);
dto.createdAt = contact.createdAt;
dto.username = contact.user?.username;
return dto;
}
static updateEntity(contact: Contact, updateContactDto: UpdateContactDto): Contact {
if (updateContactDto.name !== undefined) contact.name = updateContactDto.name;
if (updateContactDto.email !== undefined) contact.email = updateContactDto.email;
if (updateContactDto.message !== undefined) contact.message = updateContactDto.message;
return contact;
}
static toResponseDtoArray(contacts: Contact[]): ContactResponseDto[] {
return contacts.map(contact => this.toResponseDto(contact));
}
static toSummaryDtoArray(contacts: Contact[]): ContactSummaryDto[] {
return contacts.map(contact => this.toSummaryDto(contact));
}
static toQueryParams(requestDto: ContactRequestDto): any {
const params: any = {};
if (requestDto.guid) params.guid = requestDto.guid;
if (requestDto.name) params.name = requestDto.name;
if (requestDto.email) params.email = requestDto.email;
if (requestDto.userId) params.userId = requestDto.userId;
if (requestDto.createdAfter) params.createdAfter = requestDto.createdAfter;
if (requestDto.createdBefore) params.createdBefore = requestDto.createdBefore;
if (requestDto.searchMessage) params.searchMessage = `%${requestDto.searchMessage}%`;
return params;
}
static toFilterConditions(filterDto: ContactFilterDto): any {
const conditions: any = {};
if (filterDto.nameContains) conditions.nameContains = `%${filterDto.nameContains}%`;
if (filterDto.email) conditions.email = filterDto.email;
if (filterDto.emailDomain) conditions.emailDomain = `%@${filterDto.emailDomain}`;
if (filterDto.messageContains) conditions.messageContains = `%${filterDto.messageContains}%`;
if (filterDto.userId) conditions.userId = filterDto.userId;
if (filterDto.isRegisteredUser !== undefined) conditions.isRegisteredUser = filterDto.isRegisteredUser;
if (filterDto.createdAfter) conditions.createdAfter = filterDto.createdAfter;
if (filterDto.createdBefore) conditions.createdBefore = filterDto.createdBefore;
if (filterDto.minMessageLength) conditions.minMessageLength = filterDto.minMessageLength;
if (filterDto.maxMessageLength) conditions.maxMessageLength = filterDto.maxMessageLength;
if (filterDto.sortBy) conditions.sortBy = filterDto.sortBy;
if (filterDto.sortOrder) conditions.sortOrder = filterDto.sortOrder;
return conditions;
}
static toRemoveParams(removeDto: RemoveContactDto): any {
return {
guid: removeDto.guid,
reason: removeDto.reason,
hardDelete: removeDto.hardDelete || false
};
}
static createStatisticsDto(stats: any): ContactStatisticsDto {
const dto = new ContactStatisticsDto();
dto.totalContacts = stats.totalContacts || 0;
dto.contactsLastHour = stats.contactsLastHour || 0;
dto.contactsLastDay = stats.contactsLastDay || 0;
dto.contactsLastWeek = stats.contactsLastWeek || 0;
dto.contactsLastMonth = stats.contactsLastMonth || 0;
dto.registeredUserContacts = stats.registeredUserContacts || 0;
dto.anonymousContacts = stats.anonymousContacts || 0;
dto.averageMessageLength = stats.averageMessageLength || 0;
dto.mostActiveDay = stats.mostActiveDay || '';
return dto;
}
static getMessagePreview(message: string, length: number = 100): string {
if (message.length <= length) return message;
return message.substring(0, length) + '...';
}
static canMapToResponse(contact: Contact): boolean {
return contact.guid !== null && contact.guid !== undefined;
}
static isFromRegisteredUser(contact: Contact): boolean {
return contact.user !== null && contact.user !== undefined;
}
static extractEmailDomain(email: string): string {
const parts = email.split('@');
return parts.length === 2 ? parts[1] : '';
}
static getContactPriority(contact: Contact): 'high' | 'medium' | 'low' {
const message = contact.message.toLowerCase();
const highPriorityKeywords = ['urgent', 'emergency', 'critical', 'important', 'asap'];
const mediumPriorityKeywords = ['question', 'problem', 'issue', 'help', 'support'];
if (highPriorityKeywords.some(keyword => message.includes(keyword))) {
return 'high';
}
if (mediumPriorityKeywords.some(keyword => message.includes(keyword))) {
return 'medium';
}
return 'low';
}
static toFilteredResponseDto(contact: Contact, canViewFullDetails: boolean): ContactResponseDto | any {
if (canViewFullDetails) {
return this.toResponseDto(contact);
}
const dto = this.toResponseDto(contact);
return {
id: dto.id,
guid: dto.guid,
name: dto.name,
createdAt: dto.createdAt,
messagePreview: this.getMessagePreview(dto.message, 50)
};
}
static groupContactsByDomain(contacts: Contact[]): Map<string, Contact[]> {
const domainMap = new Map<string, Contact[]>();
contacts.forEach(contact => {
const domain = this.extractEmailDomain(contact.email);
if (!domainMap.has(domain)) {
domainMap.set(domain, []);
}
domainMap.get(domain)!.push(contact);
});
return domainMap;
}
static getContactFrequencyStats(contacts: Contact[]): any {
const now = new Date();
const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000);
const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000);
const oneWeekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
const oneMonthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
return {
lastHour: contacts.filter(c => c.createdAt >= oneHourAgo).length,
lastDay: contacts.filter(c => c.createdAt >= oneDayAgo).length,
lastWeek: contacts.filter(c => c.createdAt >= oneWeekAgo).length,
lastMonth: contacts.filter(c => c.createdAt >= oneMonthAgo).length,
total: contacts.length
};
}
}
@@ -0,0 +1,157 @@
export { UserMapper } from './user.mapper';
export { CompanyMapper } from './company.mapper';
export { CardsMapper } from './cards.mapper';
export { ChatMapper } from './chat.mapper';
export { ContactMapper } from './contact.mapper';
export { CompanyContactMapper } from './companyContact.mapper';
export * from '../dto/user.dto';
export * from '../dto/company.dto';
export * from '../dto/cards.dto';
export * from '../dto/chat.dto';
export * from '../dto/contact.dto';
export * from '../dto/companyContact.dto';
export * from '../entities/user.entity';
export * from '../entities/company.entity';
export * from '../entities/cards.entity';
export * from '../entities/chat.entity';
export * from '../entities/contact.entity';
export * from '../entities/companyContact.entity';
export interface MapperConfig {
includeRelations?: boolean;
includePrivateFields?: boolean;
userId?: number;
userCompanyId?: number;
}
export interface IMapper<TEntity, TCreateDto, TUpdateDto, TResponseDto> {
toEntity(createDto: TCreateDto): TEntity;
toResponseDto(entity: TEntity): TResponseDto;
updateEntity(entity: TEntity, updateDto: TUpdateDto): TEntity;
toResponseDtoArray(entities: TEntity[]): TResponseDto[];
canMapToResponse(entity: TEntity): boolean;
}
export class MapperUtils {
static applyPagination<T>(items: T[], page?: number, limit?: number): T[] {
if (!page || !limit) return items;
const startIndex = (page - 1) * limit;
return items.slice(startIndex, startIndex + limit);
}
static applySorting<T>(items: T[], sortBy?: string, sortOrder?: 'ASC' | 'DESC'): T[] {
if (!sortBy) return items;
return items.sort((a, b) => {
const aValue = (a as any)[sortBy];
const bValue = (b as any)[sortBy];
if (aValue < bValue) return sortOrder === 'DESC' ? 1 : -1;
if (aValue > bValue) return sortOrder === 'DESC' ? -1 : 1;
return 0;
});
}
static applyTextFilter<T>(items: T[], searchText: string, searchFields: string[]): T[] {
if (!searchText) return items;
const lowerSearchText = searchText.toLowerCase();
return items.filter(item => {
return searchFields.some(field => {
const fieldValue = (item as any)[field];
return fieldValue && fieldValue.toString().toLowerCase().includes(lowerSearchText);
});
});
}
static applyDateRangeFilter<T>(items: T[], dateField: string, fromDate?: Date, toDate?: Date): T[] {
return items.filter(item => {
const itemDate = (item as any)[dateField];
if (!itemDate) return false;
if (fromDate && itemDate < fromDate) return false;
if (toDate && itemDate > toDate) return false;
return true;
});
}
static enumToDisplayString(enumValue: any): string {
return enumValue.toString().replace(/_/g, ' ').toLowerCase()
.replace(/\b\w/g, (char: string) => char.toUpperCase());
}
static isValidEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
}
static sanitizeSearchString(searchText: string): string {
return searchText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
static generateMessagePreview(message: string, length: number = 100): string {
if (message.length <= length) return message;
return message.substring(0, length).trim() + '...';
}
static getTimeAgo(date: Date): string {
const now = new Date();
const diff = now.getTime() - date.getTime();
const minutes = Math.floor(diff / (1000 * 60));
const hours = Math.floor(diff / (1000 * 60 * 60));
const days = Math.floor(diff / (1000 * 60 * 60 * 24));
if (minutes < 60) return `${minutes} minutes ago`;
if (hours < 24) return `${hours} hours ago`;
if (days < 30) return `${days} days ago`;
return date.toLocaleDateString();
}
static deepClone<T>(obj: T): T {
return JSON.parse(JSON.stringify(obj));
}
static hasRequiredProperties<T>(obj: any, requiredProps: (keyof T)[]): boolean {
return requiredProps.every(prop => obj.hasOwnProperty(prop) && obj[prop] !== undefined);
}
static removeUndefinedProperties<T>(obj: T): T {
const cleaned = { ...obj } as any;
Object.keys(cleaned).forEach(key => {
if (cleaned[key] === undefined) {
delete cleaned[key];
}
});
return cleaned;
}
static groupBy<T, K extends keyof T>(array: T[], property: K): Map<T[K], T[]> {
const map = new Map<T[K], T[]>();
array.forEach(item => {
const key = item[property];
if (!map.has(key)) {
map.set(key, []);
}
map.get(key)!.push(item);
});
return map;
}
static getUniqueValues<T>(array: T[]): T[] {
return [...new Set(array)];
}
static calculateStats(numbers: number[]): { min: number; max: number; avg: number; sum: number } {
if (numbers.length === 0) return { min: 0, max: 0, avg: 0, sum: 0 };
const sum = numbers.reduce((acc, num) => acc + num, 0);
const avg = sum / numbers.length;
const min = Math.min(...numbers);
const max = Math.max(...numbers);
return { min, max, avg, sum };
}
}
@@ -1,38 +0,0 @@
import { QBank } from '../entities/qbank.entity';
import { QBankBasicDto, QBankUpdateDto, QBankCreateDto, QBankResponseDto } from '../dto/qbank.dto';
export class QBankMapper {
static toBasicDto(qbank: QBank): QBankBasicDto {
return new QBankBasicDto(qbank.QBankId, qbank.Title);
}
static toResponseDto(qbank: QBank): QBankResponseDto {
return new QBankResponseDto(
qbank.QBankId,
qbank.Title,
qbank.Creator,
qbank.Creation_Date,
qbank.no_question
);
}
static toEntity(createDto: QBankCreateDto, creatorId: number): QBank {
const qbank = new QBank();
qbank.Title = createDto.Title;
qbank.Creator = creatorId;
qbank.Creation_Date = new Date();
qbank.no_question = createDto.no_question;
return qbank;
}
static updateEntity(original: QBank, updateDto: QBankUpdateDto): QBank {
if (updateDto.Title !== undefined) original.Title = updateDto.Title;
if (updateDto.no_question !== undefined) original.no_question = updateDto.no_question;
return original;
}
static QBankListResponseDto(qbanks: QBank[]): QBankResponseDto[] {
return qbanks.map(qbank => this.toResponseDto(qbank));
}
}
@@ -1,49 +1,119 @@
import { User } from '../entities/user.entity';
import { UserBasicDto, UserUpdateDto, UserCreateDto, UserResponseDto } from '../dto/user.dto';
import { User, UserStatus, UserAuthLevel } from '../entities/user.entity';
import { Company } from '../entities/company.entity';
import {
CreateUserDto,
UpdateUserDto,
UserResponseDto,
JwtTokenDto,
LoginResponseDto,
UserRequestDto,
LoginRequestDto,
RemoveUserDto,
PasswordResetRequestDto,
PasswordResetDto,
EmailConfirmationDto,
PremiumSubscriptionDto,
CompanyRegistrationDto
} from '../dto/user.dto';
export class UserMapper {
static toBasicDto(user: User): UserBasicDto {
return new UserBasicDto(user.id, user.username, user.CompanyId || undefined);
}
static toResponseDto(user: User): UserResponseDto {
return new UserResponseDto(
user.id,
user.username,
user.FirstName,
user.LastName,
user.email,
user.RegDate
);
}
static toEntity(createDto: UserCreateDto): User {
const user = new User();
user.username = createDto.username;
user.FirstName = createDto.FirstName;
user.LastName = createDto.LastName;
user.email = createDto.email;
user.password = createDto.password;
// Only set CompanyId if it's provided and not null/undefined
if (createDto.CompanyId !== undefined && createDto.CompanyId !== null) {
user.CompanyId = createDto.CompanyId;
static toEntity(createUserDto: CreateUserDto): User {
const user = new User();
user.username = createUserDto.username;
user.FirstName = createUserDto.FirstName;
user.LastName = createUserDto.LastName;
user.email = createUserDto.email;
user.password = createUserDto.password;
user.CompanyId = createUserDto.CompanyId;
user.authLevel = createUserDto.authLevel || UserAuthLevel.STANDARD;
user.status = createUserDto.status || UserStatus.PENDING_CONFIRMATION;
return user;
}
// If not provided, leave it as undefined (which will be NULL in database)
return user;
}
static updateEntity(original: User, updateDto: UserUpdateDto): User {
if (updateDto.username !== undefined) original.username = updateDto.username;
if (updateDto.FirstName !== undefined) original.FirstName = updateDto.FirstName;
if (updateDto.LastName !== undefined) original.LastName = updateDto.LastName;
if (updateDto.email !== undefined) original.email = updateDto.email;
if (updateDto.password !== undefined) original.password = updateDto.password;
return original;
}
static toResponseDto(user: User): UserResponseDto {
const dto = new UserResponseDto();
dto.id = user.id;
dto.guid = user.guid;
dto.username = user.username;
dto.FirstName = user.FirstName;
dto.LastName = user.LastName;
dto.email = user.email;
dto.createdAt = user.createdAt;
dto.updatedAt = user.updatedAt;
dto.status = user.status;
dto.authLevel = user.authLevel;
dto.premiumExpirationDate = user.premiumExpirationDate;
dto.CompanyId = user.CompanyId;
dto.companyRegistered = user.companyRegistered;
dto.companyRegistrationDate = user.companyRegistrationDate;
return dto;
}
static UsersListResponseDto(users: User[]): UserResponseDto[] {
return users.map(user => this.toResponseDto(user));
}
}
static toJwtTokenDto(user: User): JwtTokenDto {
const dto = new JwtTokenDto();
dto.userId = user.id;
dto.username = user.username;
dto.companyId = user.CompanyId;
dto.authLevel = user.authLevel;
dto.status = user.status;
return dto;
}
static toLoginResponseDto(user: User, token: string): LoginResponseDto {
const dto = new LoginResponseDto();
dto.username = user.username;
dto.id = user.id;
dto.companyId = user.CompanyId;
dto.authLevel = user.authLevel;
dto.status = user.status;
dto.token = token;
return dto;
}
static updateEntity(user: User, updateUserDto: UpdateUserDto): User {
if (updateUserDto.username !== undefined) user.username = updateUserDto.username;
if (updateUserDto.FirstName !== undefined) user.FirstName = updateUserDto.FirstName;
if (updateUserDto.LastName !== undefined) user.LastName = updateUserDto.LastName;
if (updateUserDto.email !== undefined) user.email = updateUserDto.email;
if (updateUserDto.password !== undefined) user.password = updateUserDto.password;
if (updateUserDto.status !== undefined) user.status = updateUserDto.status;
if (updateUserDto.authLevel !== undefined) user.authLevel = updateUserDto.authLevel;
if (updateUserDto.securityToken !== undefined) user.securityToken = updateUserDto.securityToken;
if (updateUserDto.securityTokenExpiry !== undefined) user.securityTokenExpiry = updateUserDto.securityTokenExpiry;
if (updateUserDto.premiumExpirationDate !== undefined) user.premiumExpirationDate = updateUserDto.premiumExpirationDate;
if (updateUserDto.CompanyId !== undefined) user.CompanyId = updateUserDto.CompanyId;
if (updateUserDto.companyRegistered !== undefined) user.companyRegistered = updateUserDto.companyRegistered;
if (updateUserDto.companyRegistrationDate !== undefined) user.companyRegistrationDate = updateUserDto.companyRegistrationDate;
return user;
}
static toResponseDtoArray(users: User[]): UserResponseDto[] {
return users.map(user => this.toResponseDto(user));
}
static toQueryParams(requestDto: UserRequestDto): any {
const params: any = {};
if (requestDto.guid) params.guid = requestDto.guid;
if (requestDto.username) params.username = requestDto.username;
if (requestDto.email) params.email = requestDto.email;
if (requestDto.status !== undefined) params.status = requestDto.status;
if (requestDto.authLevel !== undefined) params.authLevel = requestDto.authLevel;
if (requestDto.CompanyId) params.CompanyId = requestDto.CompanyId;
if (requestDto.companyRegistered !== undefined) params.companyRegistered = requestDto.companyRegistered;
return params;
}
static canMapToResponse(user: User): boolean {
return user.status !== UserStatus.DELETED;
}
static toPublicResponseDto(user: User): Partial<UserResponseDto> {
const dto = this.toResponseDto(user);
const publicDto: Partial<UserResponseDto> = { ...dto };
delete publicDto.email;
delete publicDto.premiumExpirationDate;
delete publicDto.companyRegistrationDate;
return publicDto;
}
}
@@ -1,24 +0,0 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class ModifiedUser1751827771881 implements MigrationInterface {
name = 'ModifiedUser1751827771881'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE \`Company\` (\`CompanyId\` int NOT NULL AUTO_INCREMENT, \`Name\` varchar(255) NOT NULL, \`ContactFirstName\` varchar(255) NOT NULL, \`ContactLastName\` varchar(255) NOT NULL, \`ContactEmail\` varchar(255) NOT NULL, \`FirstAPI\` varchar(255) NOT NULL, \`TokenAPI\` varchar(255) NOT NULL, \`RegDate\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), PRIMARY KEY (\`CompanyId\`)) ENGINE=InnoDB`);
await queryRunner.query(`CREATE TABLE \`user\` (\`id\` int NOT NULL AUTO_INCREMENT, \`username\` varchar(255) NOT NULL, \`FirstName\` varchar(255) NOT NULL, \`LastName\` varchar(255) NOT NULL, \`email\` varchar(255) NOT NULL, \`password\` varchar(255) NOT NULL, \`RegDate\` datetime(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6), \`CompanyId\` int NULL, \`CompanyToken\` varchar(255) NULL, UNIQUE INDEX \`IDX_78a916df40e02a9deb1c4b75ed\` (\`username\`), UNIQUE INDEX \`IDX_e12875dfb3b1d92d7d7c5377e2\` (\`email\`), PRIMARY KEY (\`id\`)) ENGINE=InnoDB`);
await queryRunner.query(`CREATE TABLE \`QBank\` (\`QBankId\` int NOT NULL AUTO_INCREMENT, \`Title\` varchar(255) NOT NULL, \`Creator\` int NOT NULL, \`Creation_Date\` datetime NOT NULL, \`no_question\` int NOT NULL, PRIMARY KEY (\`QBankId\`)) ENGINE=InnoDB`);
await queryRunner.query(`ALTER TABLE \`user\` ADD CONSTRAINT \`FK_85994badd6742add52ae81cfb15\` FOREIGN KEY (\`CompanyId\`) REFERENCES \`Company\`(\`CompanyId\`) ON DELETE NO ACTION ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE \`QBank\` ADD CONSTRAINT \`FK_ce24a25a13d18a471bd7c0df014\` FOREIGN KEY (\`Creator\`) REFERENCES \`user\`(\`id\`) ON DELETE NO ACTION ON UPDATE NO ACTION`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE \`QBank\` DROP FOREIGN KEY \`FK_ce24a25a13d18a471bd7c0df014\``);
await queryRunner.query(`ALTER TABLE \`user\` DROP FOREIGN KEY \`FK_85994badd6742add52ae81cfb15\``);
await queryRunner.query(`DROP TABLE \`QBank\``);
await queryRunner.query(`DROP INDEX \`IDX_e12875dfb3b1d92d7d7c5377e2\` ON \`user\``);
await queryRunner.query(`DROP INDEX \`IDX_78a916df40e02a9deb1c4b75ed\` ON \`user\``);
await queryRunner.query(`DROP TABLE \`user\``);
await queryRunner.query(`DROP TABLE \`Company\``);
}
}
@@ -1,11 +0,0 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class ModifiedUser1751827770376 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
}
public async down(queryRunner: QueryRunner): Promise<void> {
}
}
@@ -1,155 +0,0 @@
import { Repository, DataSource } from 'typeorm';
import { Company } from '../Database/entities/company.entity';
import { CompanyCreateDto, CompanyUpdateDto, CompanyResponseDto, CompanyBasicDto } from '../Database/dto/company.dto';
import { CompanyMapper } from '../Database/mappers/company.mapper';
import { ICompanyRepository } from './interfaces/ICompanyRepository';
export class CompanyRepository implements ICompanyRepository {
private repository: Repository<Company>;
constructor(dataSource: DataSource) {
this.repository = dataSource.getRepository(Company);
}
// Basic CRUD operations
async create(companyCreateDto: CompanyCreateDto): Promise<CompanyResponseDto> {
const company = CompanyMapper.toEntity(companyCreateDto);
const savedCompany = await this.repository.save(company);
return CompanyMapper.toResponseDto(savedCompany);
}
async findById(id: number): Promise<CompanyResponseDto | null> {
const company = await this.repository.findOne({ where: { CompanyId: id } });
return company ? CompanyMapper.toResponseDto(company) : null;
}
async findByName(name: string): Promise<CompanyResponseDto | null> {
const company = await this.repository.findOne({ where: { Name: name } });
return company ? CompanyMapper.toResponseDto(company) : null;
}
// async findByContactEmail(email: string): Promise<CompanyResponseDto | null> {
// const company = await this.repository.findOne({ where: { ContactEmail: email } });
// return company ? CompanyMapper.toResponseDto(company) : null;
// }
async findAll(): Promise<CompanyResponseDto[]> {
const companies = await this.repository.find({
order: { RegDate: 'DESC' }
});
return companies.map(company => CompanyMapper.toResponseDto(company));
}
async update(id: number, companyUpdateDto: CompanyUpdateDto): Promise<CompanyResponseDto | null> {
const company = await this.repository.findOne({ where: { CompanyId: id } });
if (!company) return null;
const updatedCompany = CompanyMapper.updateEntity(company, companyUpdateDto);
const savedCompany = await this.repository.save(updatedCompany);
return CompanyMapper.toResponseDto(savedCompany);
}
async deleteById(id: number): Promise<boolean> {
const result = await this.repository.delete({ CompanyId: id });
return result.affected !== 0;
}
// async exists(id: number): Promise<boolean> {
// const count = await this.repository.count({ where: { CompanyId: id } });
// return count > 0;
// }
// async nameExists(name: string): Promise<boolean> {
// const count = await this.repository.count({ where: { Name: name } });
// return count > 0;
// }
// async contactEmailExists(email: string): Promise<boolean> {
// const count = await this.repository.count({ where: { ContactEmail: email } });
// return count > 0;
// }
// Search and filtering
// async findByPartialName(partialName: string): Promise<CompanyResponseDto[]> {
// const companies = await this.repository
// .createQueryBuilder('company')
// .where('company.Name LIKE :name', { name: `%${partialName}%` })
// .orderBy('company.RegDate', 'DESC')
// .getMany();
// return companies.map(company => CompanyMapper.toResponseDto(company));
// }
// async findByPartialContactName(partialName: string): Promise<CompanyResponseDto[]> {
// const companies = await this.repository
// .createQueryBuilder('company')
// .where('company.ContactFirstName LIKE :name', { name: `%${partialName}%` })
// .orWhere('company.ContactLastName LIKE :name', { name: `%${partialName}%` })
// .orderBy('company.RegDate', 'DESC')
// .getMany();
// return companies.map(company => CompanyMapper.toResponseDto(company));
// }
// Pagination
// async findWithPagination(skip: number, take: number): Promise<{ companies: CompanyResponseDto[], total: number }> {
// const [companies, total] = await this.repository.findAndCount({
// skip,
// take,
// order: { RegDate: 'DESC' }
// });
// const companyDtos = companies.map(company => CompanyMapper.toResponseDto(company));
// return {
// companies: companyDtos,
// total
// };
// }
// Basic info
async findBasicById(id: number): Promise<CompanyBasicDto | null> {
const company = await this.repository.findOne({ where: { CompanyId: id } });
return company ? CompanyMapper.toBasicDto(company) : null;
}
async findAllBasic(): Promise<CompanyBasicDto[]> {
const companies = await this.repository.find({
order: { RegDate: 'DESC' }
});
return companies.map(company => CompanyMapper.toBasicDto(company));
}
// Relations
async findWithUsers(id: number): Promise<CompanyResponseDto | null> {
const company = await this.repository.findOne({
where: { CompanyId: id },
relations: ['users']
});
return company ? CompanyMapper.toResponseDto(company) : null;
}
// Counting
// async count(): Promise<number> {
// return await this.repository.count();
// }
// Date range queries
// async findByDateRange(startDate: Date, endDate: Date): Promise<CompanyResponseDto[]> {
// const companies = await this.repository
// .createQueryBuilder('company')
// .where('company.RegDate >= :startDate', { startDate })
// .andWhere('company.RegDate <= :endDate', { endDate })
// .orderBy('company.RegDate', 'DESC')
// .getMany();
// return companies.map(company => CompanyMapper.toResponseDto(company));
// }
// Bulk operations
// async createMany(companyCreateDtos: CompanyCreateDto[]): Promise<CompanyResponseDto[]> {
// const companies = companyCreateDtos.map(dto => CompanyMapper.toEntity(dto));
// const savedCompanies = await this.repository.save(companies);
// return savedCompanies.map(company => CompanyMapper.toResponseDto(company));
// }
// async deleteByIds(ids: number[]): Promise<boolean> {
// const result = await this.repository.delete(ids);
// return result.affected !== 0;
// }
}
@@ -1,203 +0,0 @@
import { Repository, DataSource } from 'typeorm';
import { QBank } from '../Database/entities/qbank.entity';
import { QBankCreateDto, QBankUpdateDto, QBankResponseDto, QBankBasicDto, QBankListResponseDto } from '../Database/dto/qbank.dto';
import { QBankMapper } from '../Database/mappers/qbank.mapper';
import { IQBankRepository } from './interfaces/IQBankRepository';
export class QBankRepository implements IQBankRepository {
private repository: Repository<QBank>;
constructor(dataSource: DataSource) {
this.repository = dataSource.getRepository(QBank);
}
// Basic CRUD operations
async create(qbankCreateDto: QBankCreateDto, creatorId: number): Promise<QBankResponseDto> {
const qbank = QBankMapper.toEntity(qbankCreateDto, creatorId);
const savedQBank = await this.repository.save(qbank);
return QBankMapper.toResponseDto(savedQBank);
}
async findById(id: number): Promise<QBankResponseDto | null> {
const qbank = await this.repository.findOne({ where: { QBankId: id } });
return qbank ? QBankMapper.toResponseDto(qbank) : null;
}
async findByTitle(title: string): Promise<QBankResponseDto | null> {
const qbank = await this.repository.findOne({ where: { Title: title } });
return qbank ? QBankMapper.toResponseDto(qbank) : null;
}
async findAll(): Promise<QBankListResponseDto> {
const qbanks = await this.repository.find();
const qbankDtos = qbanks.map(qbank => QBankMapper.toResponseDto(qbank));
return new QBankListResponseDto(qbankDtos);
}
async update(id: number, qbankUpdateDto: QBankUpdateDto): Promise<QBankResponseDto | null> {
const qbank = await this.repository.findOne({ where: { QBankId: id } });
if (!qbank) return null;
const updatedQBank = QBankMapper.updateEntity(qbank, qbankUpdateDto);
const savedQBank = await this.repository.save(updatedQBank);
return QBankMapper.toResponseDto(savedQBank);
}
async deleteById(id: number): Promise<boolean> {
const result = await this.repository.delete({ QBankId: id });
return result.affected !== 0;
}
async exists(id: number): Promise<boolean> {
const count = await this.repository.count({ where: { QBankId: id } });
return count > 0;
}
async titleExists(title: string): Promise<boolean> {
const count = await this.repository.count({ where: { Title: title } });
return count > 0;
}
// Creator related queries
async findByCreator(creatorId: number): Promise<QBankListResponseDto> {
const qbanks = await this.repository.find({ where: { Creator: creatorId } });
const qbankDtos = qbanks.map(qbank => QBankMapper.toResponseDto(qbank));
return new QBankListResponseDto(qbankDtos);
}
async findByCreatorAndTitle(creatorId: number, title: string): Promise<QBankResponseDto | null> {
const qbank = await this.repository.findOne({
where: { Creator: creatorId, Title: title }
});
return qbank ? QBankMapper.toResponseDto(qbank) : null;
}
// Search and filtering
async findByPartialTitle(partialTitle: string): Promise<QBankListResponseDto> {
const qbanks = await this.repository
.createQueryBuilder('qbank')
.where('qbank.Title LIKE :title', { title: `%${partialTitle}%` })
.getMany();
const qbankDtos = qbanks.map(qbank => QBankMapper.toResponseDto(qbank));
return new QBankListResponseDto(qbankDtos);
}
async findByQuestionCount(minCount: number, maxCount?: number): Promise<QBankListResponseDto> {
let query = this.repository
.createQueryBuilder('qbank')
.where('qbank.no_question >= :minCount', { minCount });
if (maxCount !== undefined) {
query = query.andWhere('qbank.no_question <= :maxCount', { maxCount });
}
const qbanks = await query.getMany();
const qbankDtos = qbanks.map(qbank => QBankMapper.toResponseDto(qbank));
return new QBankListResponseDto(qbankDtos);
}
// Pagination
async findWithPagination(skip: number, take: number): Promise<{ qbanks: QBankListResponseDto, total: number }> {
const [qbanks, total] = await this.repository.findAndCount({
skip,
take,
order: { Creation_Date: 'DESC' }
});
const qbankDtos = qbanks.map(qbank => QBankMapper.toResponseDto(qbank));
return {
qbanks: new QBankListResponseDto(qbankDtos),
total
};
}
// Basic info
async findBasicById(id: number): Promise<QBankBasicDto | null> {
const qbank = await this.repository.findOne({ where: { QBankId: id } });
return qbank ? QBankMapper.toBasicDto(qbank) : null;
}
async findAllBasic(): Promise<QBankBasicDto[]> {
const qbanks = await this.repository.find();
return qbanks.map(qbank => QBankMapper.toBasicDto(qbank));
}
async findBasicByCreator(creatorId: number): Promise<QBankBasicDto[]> {
const qbanks = await this.repository.find({ where: { Creator: creatorId } });
return qbanks.map(qbank => QBankMapper.toBasicDto(qbank));
}
// Relations
async findWithCreator(id: number): Promise<QBankResponseDto | null> {
const qbank = await this.repository.findOne({
where: { QBankId: id },
relations: ['creator']
});
return qbank ? QBankMapper.toResponseDto(qbank) : null;
}
// Counting
async count(): Promise<number> {
return await this.repository.count();
}
async countByCreator(creatorId: number): Promise<number> {
return await this.repository.count({ where: { Creator: creatorId } });
}
// Date range queries
async findByDateRange(startDate: Date, endDate: Date): Promise<QBankListResponseDto> {
const qbanks = await this.repository
.createQueryBuilder('qbank')
.where('qbank.Creation_Date >= :startDate', { startDate })
.andWhere('qbank.Creation_Date <= :endDate', { endDate })
.getMany();
const qbankDtos = qbanks.map(qbank => QBankMapper.toResponseDto(qbank));
return new QBankListResponseDto(qbankDtos);
}
async findByCreatorAndDateRange(creatorId: number, startDate: Date, endDate: Date): Promise<QBankListResponseDto> {
const qbanks = await this.repository
.createQueryBuilder('qbank')
.where('qbank.Creator = :creatorId', { creatorId })
.andWhere('qbank.Creation_Date >= :startDate', { startDate })
.andWhere('qbank.Creation_Date <= :endDate', { endDate })
.getMany();
const qbankDtos = qbanks.map(qbank => QBankMapper.toResponseDto(qbank));
return new QBankListResponseDto(qbankDtos);
}
// Bulk operations
async createMany(qbankCreateDtos: QBankCreateDto[], creatorId: number): Promise<QBankListResponseDto> {
const qbanks = qbankCreateDtos.map(dto => QBankMapper.toEntity(dto, creatorId));
const savedQBanks = await this.repository.save(qbanks);
const qbankDtos = savedQBanks.map(qbank => QBankMapper.toResponseDto(qbank));
return new QBankListResponseDto(qbankDtos);
}
async deleteByIds(ids: number[]): Promise<boolean> {
const result = await this.repository.delete(ids);
return result.affected !== 0;
}
async deleteByCreator(creatorId: number): Promise<boolean> {
const result = await this.repository.delete({ Creator: creatorId });
return result.affected !== 0;
}
// Statistics
async getTotalQuestions(): Promise<number> {
const result = await this.repository
.createQueryBuilder('qbank')
.select('SUM(qbank.no_question)', 'total')
.getRawOne();
return parseInt(result.total) || 0;
}
async getAverageQuestionsPerBank(): Promise<number> {
const result = await this.repository
.createQueryBuilder('qbank')
.select('AVG(qbank.no_question)', 'average')
.getRawOne();
return parseFloat(result.average) || 0;
}
}
@@ -1,205 +0,0 @@
import { Repository, DataSource } from 'typeorm';
import { User } from '../Database/entities/user.entity';
import { UserCreateDto, UserUpdateDto, UserResponseDto, UserBasicDto, UsersListResponseDto } from '../Database/dto/user.dto';
import { UserMapper } from '../Database/mappers/user.mapper';
import { IUserRepository } from './interfaces/IUserRepository';
export class UserRepository implements IUserRepository {
private repository: Repository<User>;
constructor(dataSource: DataSource) {
// console.log('🗃️ UserRepository constructor called with dataSource:', !!dataSource);
this.repository = dataSource.getRepository(User);
// console.log('🗃️ TypeORM Repository initialized:', !!this.repository);
}
// Basic CRUD operations
async create(userCreateDto: UserCreateDto): Promise<UserResponseDto> {
// console.log('Creating user with DTO:', userCreateDto);
const user = UserMapper.toEntity(userCreateDto);
// console.log('Creating user with DTO:', user);
const savedUser = await this.repository.save(user);
// console.log('User created:', savedUser);
return UserMapper.toResponseDto(savedUser);
}
async findById(id: number): Promise<UserResponseDto | null> {
const user = await this.repository.findOne({ where: { id } });
return user ? UserMapper.toResponseDto(user) : null;
}
async findByUsername(username: string): Promise<UserResponseDto | null> {
const user = await this.repository.findOne({ where: { username } });
return user ? UserMapper.toResponseDto(user) : null;
}
async findByEmail(email: string): Promise<UserResponseDto | null> {
const user = await this.repository.findOne({ where: { email } });
return user ? UserMapper.toResponseDto(user) : null;
}
async findAll(): Promise<UsersListResponseDto> {
const users = await this.repository.find();
const userDtos = users.map(user => UserMapper.toResponseDto(user));
return new UsersListResponseDto(userDtos);
}
async update(id: number, userUpdateDto: UserUpdateDto): Promise<UserResponseDto | null> {
const user = await this.repository.findOne({ where: { id } });
if (!user) return null;
const updatedUser = UserMapper.updateEntity(user, userUpdateDto);
const savedUser = await this.repository.save(updatedUser);
return UserMapper.toResponseDto(savedUser);
}
async deleteById(id: number): Promise<boolean> {
const result = await this.repository.delete(id);
return result.affected !== 0;
}
async exists(id: number): Promise<boolean> {
const count = await this.repository.count({ where: { id } });
return count > 0;
}
async usernameExists(username: string): Promise<boolean> {
const count = await this.repository.count({ where: { username } });
return count > 0;
}
async emailExists(email: string): Promise<boolean> {
const count = await this.repository.count({ where: { email } });
return count > 0;
}
// Company related queries
// async findByCompanyId(companyId: number): Promise<UsersListResponseDto> {
// const users = await this.repository.find({ where: { CompanyId: companyId } });
// const userDtos = users.map(user => UserMapper.toResponseDto(user));
// return new UsersListResponseDto(userDtos);
// }
// async findByCompanyToken(companyToken: string): Promise<UsersListResponseDto> {
// const users = await this.repository.find({ where: { CompanyToken: companyToken } });
// const userDtos = users.map(user => UserMapper.toResponseDto(user));
// return new UsersListResponseDto(userDtos);
// }
// Search and filtering
// async findByPartialUsername(partialUsername: string): Promise<UsersListResponseDto> {
// const users = await this.repository
// .createQueryBuilder('user')
// .where('user.username LIKE :username', { username: `%${partialUsername}%` })
// .getMany();
// const userDtos = users.map(user => UserMapper.toResponseDto(user));
// return new UsersListResponseDto(userDtos);
// }
// async findByPartialName(partialName: string): Promise<UsersListResponseDto> {
// const users = await this.repository
// .createQueryBuilder('user')
// .where('user.FirstName LIKE :name', { name: `%${partialName}%` })
// .orWhere('user.LastName LIKE :name', { name: `%${partialName}%` })
// .getMany();
// const userDtos = users.map(user => UserMapper.toResponseDto(user));
// return new UsersListResponseDto(userDtos);
// }
// Pagination
// async findWithPagination(skip: number, take: number): Promise<{ users: UsersListResponseDto, total: number }> {
// const [users, total] = await this.repository.findAndCount({
// skip,
// take,
// order: { RegDate: 'DESC' }
// });
// const userDtos = users.map(user => UserMapper.toResponseDto(user));
// return {
// users: new UsersListResponseDto(userDtos),
// total
// };
// }
// Basic info
// async findBasicById(id: number): Promise<UserBasicDto | null> {
// const user = await this.repository.findOne({ where: { id } });
// return user ? UserMapper.toBasicDto(user) : null;
// }
async findAllBasic(): Promise<UserBasicDto[]> {
const users = await this.repository.find();
return users.map(user => UserMapper.toBasicDto(user));
}
// Relations
async findWithCompany(id: number): Promise<UserResponseDto | null> {
const user = await this.repository.findOne({
where: { id },
relations: ['company']
});
return user ? UserMapper.toResponseDto(user) : null;
}
async findWithQuestionBanks(id: number): Promise<UserResponseDto | null> {
const user = await this.repository.findOne({
where: { id },
relations: ['questionBanks']
});
return user ? UserMapper.toResponseDto(user) : null;
}
// async findWithAllRelations(id: number): Promise<UserResponseDto | null> {
// const user = await this.repository.findOne({
// where: { id },
// relations: ['company', 'questionBanks']
// });
// return user ? UserMapper.toResponseDto(user) : null;
// }
// Counting
// async count(): Promise<number> {
// return await this.repository.count();
// }
// async countByCompany(companyId: number): Promise<number> {
// return await this.repository.count({ where: { CompanyId: companyId } });
// }
// Date range queries
// async findByDateRange(startDate: Date, endDate: Date): Promise<UsersListResponseDto> {
// const users = await this.repository
// .createQueryBuilder('user')
// .where('user.RegDate >= :startDate', { startDate })
// .andWhere('user.RegDate <= :endDate', { endDate })
// .getMany();
// const userDtos = users.map(user => UserMapper.toResponseDto(user));
// return new UsersListResponseDto(userDtos);
// }
// Bulk operations
// async createMany(userCreateDtos: UserCreateDto[]): Promise<UsersListResponseDto> {
// const users = userCreateDtos.map(dto => UserMapper.toEntity(dto));
// const savedUsers = await this.repository.save(users);
// const userDtos = savedUsers.map(user => UserMapper.toResponseDto(user));
// return new UsersListResponseDto(userDtos);
// }
// async deleteByIds(ids: number[]): Promise<boolean> {
// const result = await this.repository.delete(ids);
// return result.affected !== 0;
// }
// Authentication support (returns raw entity for password verification)
async findRawByUsername(username: string): Promise<User | null> {
console
return await this.repository.findOne({ where: { username } });
}
// async findRawById(id: number): Promise<User | null> {
// return await this.repository.findOne({ where: { id } });
// }
async findRawByEmail(email: string): Promise<User | null> {
return await this.repository.findOne({ where: { email } });
}
}
@@ -1,39 +0,0 @@
import { CompanyCreateDto, CompanyUpdateDto, CompanyResponseDto, CompanyBasicDto } from '../../Database/dto/company.dto';
export interface ICompanyRepository {
// Basic CRUD operations
create(companyCreateDto: CompanyCreateDto): Promise<CompanyResponseDto>;
findById(id: number): Promise<CompanyResponseDto | null>;
findByName(name: string): Promise<CompanyResponseDto | null>;
// findByContactEmail(email: string): Promise<CompanyResponseDto | null>;
findAll(): Promise<CompanyResponseDto[]>;
update(id: number, companyUpdateDto: CompanyUpdateDto): Promise<CompanyResponseDto | null>;
deleteById(id: number): Promise<boolean>;
// exists(id: number): Promise<boolean>;
// nameExists(name: string): Promise<boolean>;
// contactEmailExists(email: string): Promise<boolean>;
// Search and filtering
// // findByPartialName(partialName: string): Promise<CompanyResponseDto[]>;
// // findByPartialContactName(partialName: string): Promise<CompanyResponseDto[]>;
// Pagination
// findWithPagination(skip: number, take: number): Promise<{ companies: CompanyResponseDto[], total: number }>;
// Basic info
findBasicById(id: number): Promise<CompanyBasicDto | null>;
findAllBasic(): Promise<CompanyBasicDto[]>;
// Relations
// findWithUsers(id: number): Promise<CompanyResponseDto | null>;
// Counting
// count(): Promise<number>;
// Date range queries
// findByDateRange(startDate: Date, endDate: Date): Promise<CompanyResponseDto[]>;
// Bulk operations
// createMany(companyCreateDtos: CompanyCreateDto[]): Promise<CompanyResponseDto[]>;
// deleteByIds(ids: number[]): Promise<boolean>;
}
@@ -1,50 +0,0 @@
import { QBank } from '../../Database/entities/qbank.entity';
import { QBankCreateDto, QBankUpdateDto, QBankResponseDto, QBankBasicDto, QBankListResponseDto } from '../../Database/dto/qbank.dto';
export interface IQBankRepository {
// Basic CRUD operations
create(qbankCreateDto: QBankCreateDto, creatorId: number): Promise<QBankResponseDto>;
findById(id: number): Promise<QBankResponseDto | null>;
findByTitle(title: string): Promise<QBankResponseDto | null>;
findAll(): Promise<QBankListResponseDto>;
update(id: number, qbankUpdateDto: QBankUpdateDto): Promise<QBankResponseDto | null>;
deleteById(id: number): Promise<boolean>;
exists(id: number): Promise<boolean>;
titleExists(title: string): Promise<boolean>;
// Creator related queries
findByCreator(creatorId: number): Promise<QBankListResponseDto>;
findByCreatorAndTitle(creatorId: number, title: string): Promise<QBankResponseDto | null>;
// Search and filtering
findByPartialTitle(partialTitle: string): Promise<QBankListResponseDto>;
findByQuestionCount(minCount: number, maxCount?: number): Promise<QBankListResponseDto>;
// Pagination
findWithPagination(skip: number, take: number): Promise<{ qbanks: QBankListResponseDto, total: number }>;
// Basic info
findBasicById(id: number): Promise<QBankBasicDto | null>;
findAllBasic(): Promise<QBankBasicDto[]>;
findBasicByCreator(creatorId: number): Promise<QBankBasicDto[]>;
// Relations
findWithCreator(id: number): Promise<QBankResponseDto | null>;
// Counting
count(): Promise<number>;
countByCreator(creatorId: number): Promise<number>;
// Date range queries
findByDateRange(startDate: Date, endDate: Date): Promise<QBankListResponseDto>;
findByCreatorAndDateRange(creatorId: number, startDate: Date, endDate: Date): Promise<QBankListResponseDto>;
// Bulk operations
createMany(qbankCreateDtos: QBankCreateDto[], creatorId: number): Promise<QBankListResponseDto>;
deleteByIds(ids: number[]): Promise<boolean>;
deleteByCreator(creatorId: number): Promise<boolean>;
// Statistics
getTotalQuestions(): Promise<number>;
getAverageQuestionsPerBank(): Promise<number>;
}
@@ -1,52 +0,0 @@
import { User } from '../../Database/entities/user.entity';
import { UserCreateDto, UserUpdateDto, UserResponseDto, UserBasicDto, UsersListResponseDto } from '../../Database/dto/user.dto';
export interface IUserRepository {
// Basic CRUD operations
create(userCreateDto: UserCreateDto): Promise<UserResponseDto>;
findById(id: number): Promise<UserResponseDto | null>;
// findByUsername(username: string): Promise<UserResponseDto | null>;
findByEmail(email: string): Promise<UserResponseDto | null>;
findAll(): Promise<UsersListResponseDto>;
update(id: number, userUpdateDto: UserUpdateDto): Promise<UserResponseDto | null>;
deleteById(id: number): Promise<boolean>;
exists(id: number): Promise<boolean>;
usernameExists(username: string): Promise<boolean>;
emailExists(email: string): Promise<boolean>;
// Company related queries
// findByCompanyId(companyId: number): Promise<UsersListResponseDto>;
// findByCompanyToken(companyToken: string): Promise<UsersListResponseDto>;
// Search and filtering
// findByPartialUsername(partialUsername: string): Promise<UsersListResponseDto>;
// findByPartialName(partialName: string): Promise<UsersListResponseDto>;
// Pagination
// findWithPagination(skip: number, take: number): Promise<{ users: UsersListResponseDto, total: number }>;
// Basic info
// findBasicById(id: number): Promise<UserBasicDto | null>;
findAllBasic(): Promise<UserBasicDto[]>;
// Relations
findWithCompany(id: number): Promise<UserResponseDto | null>;
findWithQuestionBanks(id: number): Promise<UserResponseDto | null>;
// findWithAllRelations(id: number): Promise<UserResponseDto | null>;
// Counting
// count(): Promise<number>;
// countByCompany(companyId: number): Promise<number>;
// Date range queries
// findByDateRange(startDate: Date, endDate: Date): Promise<UsersListResponseDto>;
// Bulk operations
// createMany(userCreateDtos: UserCreateDto[]): Promise<UsersListResponseDto>;
// deleteByIds(ids: number[]): Promise<boolean>;
// Authentication support (returns raw entity for password verification)
findRawByUsername(username: string): Promise<User | null>;
// findRawById(id: number): Promise<User | null>;
findRawByEmail(email: string): Promise<User | null>;
}
@@ -0,0 +1,582 @@
import { DataSource, Repository, FindOptionsWhere, FindManyOptions, EntityTarget, ObjectLiteral, EntityManager, Not, IsNull } from 'typeorm';
import { IBaseRepository } from './IRepository/IBaseRepository';
import { appLogger } from '../Utils/EnhancedLogger';
export abstract class BaseRepository<T extends ObjectLiteral> implements IBaseRepository<T> {
protected repository: Repository<T>;
protected dataSource: DataSource;
protected entityName: string;
constructor(dataSource: DataSource, entity: EntityTarget<T>) {
this.dataSource = dataSource;
this.repository = dataSource.getRepository(entity);
this.entityName = entity.toString();
}
async findAll(): Promise<T[]> {
const startTime = Date.now();
try {
const result = await this.repository.find();
const duration = Date.now() - startTime;
await appLogger.database('FIND_ALL', this.entityName, duration, {
resultCount: result.length,
operation: 'findAll'
});
return result;
} catch (error) {
const duration = Date.now() - startTime;
await appLogger.errorEvent(
error instanceof Error ? error.message : String(error),
'database_operation',
{
operation: 'findAll',
entity: this.entityName,
duration,
stack: error instanceof Error ? error.stack : undefined
}
);
throw error;
}
}
async findById(id: number): Promise<T | null> {
const startTime = Date.now();
try {
const result = await this.repository.findOne({ where: { id } as unknown as FindOptionsWhere<T> });
const duration = Date.now() - startTime;
await appLogger.database('FIND_BY_ID', this.entityName, duration, {
id,
found: !!result,
operation: 'findById'
});
return result;
} catch (error) {
const duration = Date.now() - startTime;
await appLogger.errorEvent(
error instanceof Error ? error.message : String(error),
'database_operation',
{
operation: 'findById',
entity: this.entityName,
id,
duration,
stack: error instanceof Error ? error.stack : undefined
}
);
throw error;
}
}
async create(entity: Partial<T>): Promise<T> {
const startTime = Date.now();
try {
const newEntity = this.repository.create(entity as any);
const result = await this.repository.save(newEntity) as unknown as T;
const duration = Date.now() - startTime;
await appLogger.database('CREATE', this.entityName, duration, {
operation: 'create',
entityId: (result as any).id || 'unknown'
});
return result;
} catch (error) {
const duration = Date.now() - startTime;
await appLogger.errorEvent(
error instanceof Error ? error.message : String(error),
'database_operation',
{
operation: 'create',
entity: this.entityName,
duration,
stack: error instanceof Error ? error.stack : undefined
}
);
throw error;
}
}
async update(id: number, entity: Partial<T>): Promise<T | null> {
const startTime = Date.now();
try {
const result = await this.repository.update(id, entity as any);
const duration = Date.now() - startTime;
await appLogger.database('UPDATE', this.entityName, duration, {
id,
affected: result.affected,
operation: 'update'
});
if ((result.affected ?? 0) > 0) {
return await this.findById(id);
}
return null;
} catch (error) {
const duration = Date.now() - startTime;
await appLogger.errorEvent(
error instanceof Error ? error.message : String(error),
'database_operation',
{
operation: 'update',
entity: this.entityName,
id,
duration,
stack: error instanceof Error ? error.stack : undefined
}
);
throw error;
}
}
async delete(id: number): Promise<boolean> {
const startTime = Date.now();
try {
const result = await this.repository.delete(id);
const duration = Date.now() - startTime;
await appLogger.database('DELETE', this.entityName, duration, {
id,
affected: result.affected,
operation: 'delete'
});
return (result.affected ?? 0) > 0;
} catch (error) {
const duration = Date.now() - startTime;
await appLogger.errorEvent(
error instanceof Error ? error.message : String(error),
'database_operation',
{
operation: 'delete',
entity: this.entityName,
id,
duration,
stack: error instanceof Error ? error.stack : undefined
}
);
throw error;
}
}
async softDelete(id: number): Promise<boolean> {
const startTime = Date.now();
try {
const result = await this.repository.softDelete(id);
const duration = Date.now() - startTime;
await appLogger.database('SOFT_DELETE', this.entityName, duration, {
id,
affected: result.affected,
operation: 'softDelete'
});
return (result.affected ?? 0) > 0;
} catch (error) {
const duration = Date.now() - startTime;
await appLogger.errorEvent(
error instanceof Error ? error.message : String(error),
'database_operation',
{
operation: 'softDelete',
entity: this.entityName,
id,
duration,
stack: error instanceof Error ? error.stack : undefined
}
);
throw error;
}
}
async restore(id: number): Promise<boolean> {
const startTime = Date.now();
try {
const result = await this.repository.restore(id);
const duration = Date.now() - startTime;
await appLogger.database('RESTORE', this.entityName, duration, {
id,
affected: result.affected,
operation: 'restore'
});
return (result.affected ?? 0) > 0;
} catch (error) {
const duration = Date.now() - startTime;
await appLogger.errorEvent(
error instanceof Error ? error.message : String(error),
'database_operation',
{
operation: 'restore',
entity: this.entityName,
id,
duration,
stack: error instanceof Error ? error.stack : undefined
}
);
throw error;
}
}
async findWithPagination(page: number, limit: number): Promise<{ data: T[]; total: number; page: number; limit: number }> {
const startTime = Date.now();
try {
const skip = (page - 1) * limit;
const [data, total] = await this.repository.findAndCount({
skip,
take: limit
});
const duration = Date.now() - startTime;
await appLogger.database('FIND_WITH_PAGINATION', this.entityName, duration, {
page,
limit,
total,
resultCount: data.length,
operation: 'findWithPagination'
});
return { data, total, page, limit };
} catch (error) {
const duration = Date.now() - startTime;
await appLogger.errorEvent(
error instanceof Error ? error.message : String(error),
'database_operation',
{
operation: 'findWithPagination',
entity: this.entityName,
page,
limit,
duration,
stack: error instanceof Error ? error.stack : undefined
}
);
throw error;
}
}
async findWithRelations(relations: string[]): Promise<T[]> {
const startTime = Date.now();
try {
const result = await this.repository.find({ relations });
const duration = Date.now() - startTime;
await appLogger.database('FIND_WITH_RELATIONS', this.entityName, duration, {
relations,
resultCount: result.length,
operation: 'findWithRelations'
});
return result;
} catch (error) {
const duration = Date.now() - startTime;
await appLogger.errorEvent(
error instanceof Error ? error.message : String(error),
'database_operation',
{
operation: 'findWithRelations',
entity: this.entityName,
relations,
duration,
stack: error instanceof Error ? error.stack : undefined
}
);
throw error;
}
}
async search(options: FindManyOptions<T>): Promise<T[]> {
const startTime = Date.now();
try {
const result = await this.repository.find(options);
const duration = Date.now() - startTime;
await appLogger.database('SEARCH', this.entityName, duration, {
resultCount: result.length,
operation: 'search'
});
return result;
} catch (error) {
const duration = Date.now() - startTime;
await appLogger.errorEvent(
error instanceof Error ? error.message : String(error),
'database_operation',
{
operation: 'search',
entity: this.entityName,
duration,
stack: error instanceof Error ? error.stack : undefined
}
);
throw error;
}
}
async count(where?: FindOptionsWhere<T>): Promise<number> {
const startTime = Date.now();
try {
const result = await this.repository.count({ where });
const duration = Date.now() - startTime;
await appLogger.database('COUNT', this.entityName, duration, {
count: result,
operation: 'count'
});
return result;
} catch (error) {
const duration = Date.now() - startTime;
await appLogger.errorEvent(
error instanceof Error ? error.message : String(error),
'database_operation',
{
operation: 'count',
entity: this.entityName,
duration,
stack: error instanceof Error ? error.stack : undefined
}
);
throw error;
}
}
async bulkCreate(entities: Partial<T>[]): Promise<T[]> {
const startTime = Date.now();
try {
const newEntities = this.repository.create(entities as any);
const result = await this.repository.save(newEntities);
const duration = Date.now() - startTime;
await appLogger.database('BULK_CREATE', this.entityName, duration, {
inputCount: entities.length,
createdCount: result.length,
operation: 'bulkCreate'
});
return result;
} catch (error) {
const duration = Date.now() - startTime;
await appLogger.errorEvent(
error instanceof Error ? error.message : String(error),
'database_operation',
{
operation: 'bulkCreate',
entity: this.entityName,
inputCount: entities.length,
duration,
stack: error instanceof Error ? error.stack : undefined
}
);
throw error;
}
}
async bulkUpdate(where: FindOptionsWhere<T>, update: Partial<T>): Promise<boolean> {
const startTime = Date.now();
try {
const result = await this.repository.update(where as any, update as any);
const duration = Date.now() - startTime;
await appLogger.database('BULK_UPDATE', this.entityName, duration, {
affected: result.affected,
operation: 'bulkUpdate'
});
return (result.affected ?? 0) > 0;
} catch (error) {
const duration = Date.now() - startTime;
await appLogger.errorEvent(
error instanceof Error ? error.message : String(error),
'database_operation',
{
operation: 'bulkUpdate',
entity: this.entityName,
duration,
stack: error instanceof Error ? error.stack : undefined
}
);
throw error;
}
}
async bulkDelete(where: FindOptionsWhere<T>): Promise<boolean> {
const startTime = Date.now();
try {
const result = await this.repository.delete(where as any);
const duration = Date.now() - startTime;
await appLogger.database('BULK_DELETE', this.entityName, duration, {
affected: result.affected,
operation: 'bulkDelete'
});
return (result.affected ?? 0) > 0;
} catch (error) {
const duration = Date.now() - startTime;
await appLogger.errorEvent(
error instanceof Error ? error.message : String(error),
'database_operation',
{
operation: 'bulkDelete',
entity: this.entityName,
duration,
stack: error instanceof Error ? error.stack : undefined
}
);
throw error;
}
}
async withTransaction<R>(operation: (manager: EntityManager) => Promise<R>): Promise<R> {
const startTime = Date.now();
try {
const result = await this.dataSource.transaction(operation);
const duration = Date.now() - startTime;
await appLogger.database('TRANSACTION', this.entityName, duration, {
operation: 'withTransaction'
});
return result;
} catch (error) {
const duration = Date.now() - startTime;
await appLogger.errorEvent(
error instanceof Error ? error.message : String(error),
'database_operation',
{
operation: 'withTransaction',
entity: this.entityName,
duration,
stack: error instanceof Error ? error.stack : undefined
}
);
throw error;
}
}
async findByGuid(guid: string): Promise<T | null> {
const startTime = Date.now();
try {
const result = await this.repository.findOne({ where: { guid } as unknown as FindOptionsWhere<T> });
const duration = Date.now() - startTime;
await appLogger.database('FIND_BY_GUID', this.entityName, duration, {
guid,
found: !!result,
operation: 'findByGuid'
});
return result;
} catch (error) {
const duration = Date.now() - startTime;
await appLogger.errorEvent(
error instanceof Error ? error.message : String(error),
'database_operation',
{
operation: 'findByGuid',
entity: this.entityName,
guid,
duration,
stack: error instanceof Error ? error.stack : undefined
}
);
throw error;
}
}
async exists(id: number): Promise<boolean> {
const startTime = Date.now();
try {
const count = await this.repository.count({ where: { id } as unknown as FindOptionsWhere<T> });
const result = count > 0;
const duration = Date.now() - startTime;
await appLogger.database('EXISTS', this.entityName, duration, {
id,
exists: result,
operation: 'exists'
});
return result;
} catch (error) {
const duration = Date.now() - startTime;
await appLogger.errorEvent(
error instanceof Error ? error.message : String(error),
'database_operation',
{
operation: 'exists',
entity: this.entityName,
id,
duration,
stack: error instanceof Error ? error.stack : undefined
}
);
throw error;
}
}
async findWithDeleted(): Promise<T[]> {
const startTime = Date.now();
try {
const result = await this.repository.find({ withDeleted: true });
const duration = Date.now() - startTime;
await appLogger.database('FIND_WITH_DELETED', this.entityName, duration, {
resultCount: result.length,
operation: 'findWithDeleted'
});
return result;
} catch (error) {
const duration = Date.now() - startTime;
await appLogger.errorEvent(
error instanceof Error ? error.message : String(error),
'database_operation',
{
operation: 'findWithDeleted',
entity: this.entityName,
duration,
stack: error instanceof Error ? error.stack : undefined
}
);
throw error;
}
}
async findOnlyDeleted(): Promise<T[]> {
const startTime = Date.now();
try {
const result = await this.repository.find({
where: { deletedAt: Not(IsNull()) } as unknown as FindOptionsWhere<T>,
withDeleted: true
});
const duration = Date.now() - startTime;
await appLogger.database('FIND_ONLY_DELETED', this.entityName, duration, {
resultCount: result.length,
operation: 'findOnlyDeleted'
});
return result;
} catch (error) {
const duration = Date.now() - startTime;
await appLogger.errorEvent(
error instanceof Error ? error.message : String(error),
'database_operation',
{
operation: 'findOnlyDeleted',
entity: this.entityName,
duration,
stack: error instanceof Error ? error.stack : undefined
}
);
throw error;
}
}
}
@@ -0,0 +1,295 @@
import { DataSource, Like, MoreThan, LessThan, IsNull, Not } from 'typeorm';
import { BaseRepository } from './BaseRepository';
import { ICardsRepository } from './IRepository/ICardsRepository';
import { Cards, Privacy, CardType } from '../DataBase/entities/cards.entity';
export class CardsRepository extends BaseRepository<Cards> implements ICardsRepository {
constructor(dataSource: DataSource) {
super(dataSource, Cards);
}
async findByTitle(title: string): Promise<Cards[]> {
return await this.repository.find({ where: { Title: Like(`%${title}%`) } });
}
async findByCreator(creatorId: number): Promise<Cards[]> {
return await this.repository.find({ where: { Creator: creatorId } });
}
async findByPrivacy(privacy: Privacy): Promise<Cards[]> {
return await this.repository.find({ where: { Privacy: privacy } });
}
async findByType(type: CardType): Promise<Cards[]> {
return await this.repository.find({ where: { Type: type } });
}
async findByCompanyId(companyId: number): Promise<Cards[]> {
return await this.repository.find({ where: { CompanyId: companyId } });
}
async findByGuid(guid: string): Promise<Cards | null> {
return await this.repository.findOne({ where: { guid } });
}
async findPublicCards(): Promise<Cards[]> {
return await this.repository.find({
where: { Privacy: Privacy.Public }
});
}
async findPrivateCards(): Promise<Cards[]> {
return await this.repository.find({
where: { Privacy: Privacy.Private }
});
}
async findCompanyCards(): Promise<Cards[]> {
return await this.repository.find({
where: { Privacy: Privacy.Company }
});
}
async findCardsByQuestionCount(minQuestions: number, maxQuestions?: number): Promise<Cards[]> {
const whereCondition: any = {
no_question: MoreThan(minQuestions - 1)
};
if (maxQuestions) {
whereCondition.no_question = LessThan(maxQuestions + 1);
}
return await this.repository.find({ where: whereCondition });
}
async findRecentCards(days: number): Promise<Cards[]> {
const dateThreshold = new Date();
dateThreshold.setDate(dateThreshold.getDate() - days);
return await this.repository.find({
where: {
Creation_Date: MoreThan(dateThreshold)
},
order: { Creation_Date: 'DESC' }
});
}
async findAccessibleCards(userId: number, companyId?: number): Promise<Cards[]> {
const conditions: any[] = [
{ Privacy: Privacy.Public },
{ Creator: userId }
];
if (companyId) {
conditions.push({
Privacy: Privacy.Company,
CompanyId: companyId
});
}
return await this.repository.find({
where: conditions
});
}
async findCardsByCreatorAndPrivacy(creatorId: number, privacy: Privacy): Promise<Cards[]> {
return await this.repository.find({
where: {
Creator: creatorId,
Privacy: privacy
}
});
}
async findCardsByCompanyAndType(companyId: number, type: CardType): Promise<Cards[]> {
return await this.repository.find({
where: {
CompanyId: companyId,
Type: type
}
});
}
async updateCardPrivacy(cardId: number, privacy: Privacy): Promise<boolean> {
const result = await this.repository.update(cardId, {
Privacy: privacy
});
return (result.affected ?? 0) > 0;
}
async updateCardTitle(cardId: number, title: string): Promise<boolean> {
const result = await this.repository.update(cardId, {
Title: title
});
return (result.affected ?? 0) > 0;
}
async updateCardDescription(cardId: number, description: string): Promise<boolean> {
const result = await this.repository.update(cardId, {
Description: description
});
return (result.affected ?? 0) > 0;
}
async updateQuestionCount(cardId: number, questionCount: number): Promise<boolean> {
const result = await this.repository.update(cardId, {
no_question: questionCount
});
return (result.affected ?? 0) > 0;
}
async assignToCompany(cardId: number, companyId: number): Promise<boolean> {
const result = await this.repository.update(cardId, {
CompanyId: companyId,
Privacy: Privacy.Company
});
return (result.affected ?? 0) > 0;
}
async removeFromCompany(cardId: number): Promise<boolean> {
const result = await this.repository.update(cardId, {
CompanyId: undefined,
Privacy: Privacy.Private
});
return (result.affected ?? 0) > 0;
}
async searchCards(searchTerm: string): Promise<Cards[]> {
return await this.repository.find({
where: [
{ Title: Like(`%${searchTerm}%`) },
{ Description: Like(`%${searchTerm}%`) }
]
});
}
async searchPublicCards(searchTerm: string): Promise<Cards[]> {
return await this.repository.find({
where: [
{ Title: Like(`%${searchTerm}%`), Privacy: Privacy.Public },
{ Description: Like(`%${searchTerm}%`), Privacy: Privacy.Public }
]
});
}
async findWithCreatorRelation(): Promise<Cards[]> {
return await this.findWithRelations(['creator']);
}
async findWithCompanyRelation(): Promise<Cards[]> {
return await this.findWithRelations(['company']);
}
async findWithAllRelations(): Promise<Cards[]> {
return await this.findWithRelations(['creator', 'company']);
}
async getCardsStatistics(): Promise<any> {
const totalCards = await this.repository.count();
const publicCards = await this.repository.count({ where: { Privacy: Privacy.Public } });
const privateCards = await this.repository.count({ where: { Privacy: Privacy.Private } });
const companyCards = await this.repository.count({ where: { Privacy: Privacy.Company } });
const questionCards = await this.repository.count({ where: { Type: CardType.Question } });
const jokerCards = await this.repository.count({ where: { Type: CardType.Joker } });
const luckCards = await this.repository.count({ where: { Type: CardType.Luck } });
return {
totalCards,
publicCards,
privateCards,
companyCards,
questionCards,
jokerCards,
luckCards
};
}
async getCardsByPrivacyStats(): Promise<any> {
const publicCards = await this.repository.count({ where: { Privacy: Privacy.Public } });
const privateCards = await this.repository.count({ where: { Privacy: Privacy.Private } });
const companyCards = await this.repository.count({ where: { Privacy: Privacy.Company } });
return {
publicCards,
privateCards,
companyCards
};
}
async getCardsByTypeStats(): Promise<any> {
const questionCards = await this.repository.count({ where: { Type: CardType.Question } });
const jokerCards = await this.repository.count({ where: { Type: CardType.Joker } });
const luckCards = await this.repository.count({ where: { Type: CardType.Luck } });
return {
questionCards,
jokerCards,
luckCards
};
}
async getTopCreators(limit: number = 10): Promise<any[]> {
const result = await this.repository
.createQueryBuilder('cards')
.select('cards.Creator', 'creatorId')
.addSelect('COUNT(*)', 'cardCount')
.leftJoinAndSelect('cards.creator', 'creator')
.groupBy('cards.Creator')
.orderBy('cardCount', 'DESC')
.limit(limit)
.getRawAndEntities();
return result.entities;
}
async getAverageQuestionCount(): Promise<number> {
const result = await this.repository
.createQueryBuilder('cards')
.select('AVG(cards.no_question)', 'average')
.getRawOne();
return parseFloat(result.average) || 0;
}
async findByCreationDateRange(startDate: Date, endDate: Date): Promise<Cards[]> {
return await this.repository.find({
where: {
Creation_Date: MoreThan(startDate) && LessThan(endDate)
},
order: { Creation_Date: 'DESC' }
});
}
async searchByTitle(title: string): Promise<Cards[]> {
return await this.repository.find({
where: { Title: Like(`%${title}%`) }
});
}
async searchByDescription(description: string): Promise<Cards[]> {
return await this.repository.find({
where: { Description: Like(`%${description}%`) }
});
}
async findByQuestionRange(minQuestions: number, maxQuestions: number): Promise<Cards[]> {
return await this.repository.find({
where: {
no_question: MoreThan(minQuestions - 1) && LessThan(maxQuestions + 1)
}
});
}
async getMostActiveCreators(limit: number = 10): Promise<any[]> {
const result = await this.repository
.createQueryBuilder('cards')
.select('cards.Creator', 'creatorId')
.addSelect('COUNT(*)', 'cardCount')
.leftJoinAndSelect('cards.creator', 'creator')
.groupBy('cards.Creator')
.orderBy('cardCount', 'DESC')
.limit(limit)
.getRawAndEntities();
return result.entities;
}
}
@@ -0,0 +1,384 @@
import { DataSource, Like, MoreThan, LessThan, In } from 'typeorm';
import { BaseRepository } from './BaseRepository';
import { IChatRepository } from './IRepository/IChatRepository';
import { Chat } from '../DataBase/entities/chat.entity';
export class ChatRepository extends BaseRepository<Chat> implements IChatRepository {
constructor(dataSource: DataSource) {
super(dataSource, Chat);
}
async findByChatUuid(chatUuid: string): Promise<Chat[]> {
return await this.repository.find({
where: { Chatuuid: chatUuid },
order: { createdAt: 'ASC' }
});
}
async findByUserId(userId: number): Promise<Chat[]> {
return await this.repository.find({
where: { UserId: userId },
order: { createdAt: 'DESC' }
});
}
async findByGuid(guid: string): Promise<Chat | null> {
return await this.repository.findOne({ where: { guid } });
}
async findRecentMessages(chatUuid: string, limit: number): Promise<Chat[]> {
return await this.repository.find({
where: { Chatuuid: chatUuid },
order: { createdAt: 'DESC' },
take: limit
});
}
async findMessagesByDateRange(chatUuid: string, startDate: Date, endDate: Date): Promise<Chat[]> {
return await this.repository.find({
where: {
Chatuuid: chatUuid,
createdAt: MoreThan(startDate) && LessThan(endDate)
},
order: { createdAt: 'ASC' }
});
}
async findMessagesByUser(userId: number, limit: number): Promise<Chat[]> {
return await this.repository.find({
where: { UserId: userId },
order: { createdAt: 'DESC' },
take: limit
});
}
async findLatestMessage(chatUuid: string): Promise<Chat | null> {
return await this.repository.findOne({
where: { Chatuuid: chatUuid },
order: { createdAt: 'DESC' }
});
}
async findOldestMessage(chatUuid: string): Promise<Chat | null> {
return await this.repository.findOne({
where: { Chatuuid: chatUuid },
order: { createdAt: 'ASC' }
});
}
async getMessageCount(chatUuid: string): Promise<number> {
return await this.repository.count({
where: { Chatuuid: chatUuid }
});
}
async getUserMessageCount(userId: number, chatUuid: string): Promise<number> {
return await this.repository.count({
where: {
UserId: userId,
Chatuuid: chatUuid
}
});
}
async getUniqueUsers(chatUuid: string): Promise<number[]> {
const result = await this.repository
.createQueryBuilder('chat')
.select('DISTINCT chat.UserId', 'userId')
.where('chat.Chatuuid = :chatUuid', { chatUuid })
.getRawMany();
return result.map(r => r.userId);
}
async getActiveChatRooms(): Promise<string[]> {
const result = await this.repository
.createQueryBuilder('chat')
.select('DISTINCT chat.Chatuuid', 'chatUuid')
.getRawMany();
return result.map(r => r.chatUuid);
}
async searchMessages(searchTerm: string): Promise<Chat[]> {
return await this.repository.find({
where: {
message: Like(`%${searchTerm}%`)
},
order: { createdAt: 'DESC' }
});
}
async searchAllMessages(searchTerm: string): Promise<Chat[]> {
return await this.repository.find({
where: {
message: Like(`%${searchTerm}%`)
},
order: { createdAt: 'DESC' }
});
}
async getRecentActivity(hours: number): Promise<Chat[]> {
const hoursAgo = new Date();
hoursAgo.setHours(hoursAgo.getHours() - hours);
return await this.repository.find({
where: {
createdAt: MoreThan(hoursAgo)
},
order: { createdAt: 'DESC' }
});
}
async getUserActivity(userId: number, hours: number): Promise<Chat[]> {
const hoursAgo = new Date();
hoursAgo.setHours(hoursAgo.getHours() - hours);
return await this.repository.find({
where: {
UserId: userId,
createdAt: MoreThan(hoursAgo)
},
order: { createdAt: 'DESC' }
});
}
async findWithUserRelation(): Promise<Chat[]> {
return await this.findWithRelations(['user']);
}
async getChatRoomStats(chatUuid: string): Promise<any> {
const messageCount = await this.getMessageCount(chatUuid);
const uniqueUsers = await this.getUniqueUsers(chatUuid);
const latestMessage = await this.findLatestMessage(chatUuid);
const oldestMessage = await this.findOldestMessage(chatUuid);
return {
messageCount,
uniqueUserCount: uniqueUsers.length,
uniqueUsers,
latestMessage,
oldestMessage
};
}
async getActiveUsers(chatUuid: string, hours: number = 24): Promise<number[]> {
const hoursAgo = new Date();
hoursAgo.setHours(hoursAgo.getHours() - hours);
const result = await this.repository
.createQueryBuilder('chat')
.select('DISTINCT chat.UserId', 'userId')
.where('chat.Chatuuid = :chatUuid', { chatUuid })
.andWhere('chat.createdAt > :hoursAgo', { hoursAgo })
.getRawMany();
return result.map(r => r.userId);
}
async getMostActiveUsers(limit: number = 10): Promise<any[]> {
const result = await this.repository
.createQueryBuilder('chat')
.select('chat.UserId', 'userId')
.addSelect('COUNT(*)', 'messageCount')
.leftJoinAndSelect('chat.user', 'user')
.groupBy('chat.UserId')
.orderBy('messageCount', 'DESC')
.limit(limit)
.getRawAndEntities();
return result.entities;
}
async getChatRoomActivity(chatUuid: string, days: number = 7): Promise<any[]> {
const daysAgo = new Date();
daysAgo.setDate(daysAgo.getDate() - days);
const result = await this.repository
.createQueryBuilder('chat')
.select('DATE(chat.createdAt)', 'date')
.addSelect('COUNT(*)', 'messageCount')
.where('chat.Chatuuid = :chatUuid', { chatUuid })
.andWhere('chat.createdAt > :daysAgo', { daysAgo })
.groupBy('DATE(chat.createdAt)')
.orderBy('date', 'ASC')
.getRawMany();
return result;
}
async getMessagesAfter(chatUuid: string, messageId: number): Promise<Chat[]> {
return await this.repository.find({
where: {
Chatuuid: chatUuid,
id: MoreThan(messageId)
},
order: { createdAt: 'ASC' }
});
}
async getMessagesBefore(chatUuid: string, messageId: number): Promise<Chat[]> {
return await this.repository.find({
where: {
Chatuuid: chatUuid,
id: LessThan(messageId)
},
order: { createdAt: 'DESC' }
});
}
async bulkDeleteMessages(chatUuid: string, messageIds: number[]): Promise<boolean> {
const result = await this.repository.delete({
Chatuuid: chatUuid,
id: In(messageIds)
});
return (result.affected ?? 0) > 0;
}
async deleteOldMessages(chatUuid: string, olderThanDays: number): Promise<boolean> {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - olderThanDays);
const result = await this.repository.delete({
Chatuuid: chatUuid,
createdAt: LessThan(cutoffDate)
});
return (result.affected ?? 0) > 0;
}
async updateMessage(messageId: number, newMessage: string): Promise<boolean> {
const result = await this.repository.update(messageId, {
message: newMessage,
updatedAt: new Date()
});
return (result.affected ?? 0) > 0;
}
async findByMessage(message: string): Promise<Chat[]> {
return await this.repository.find({
where: { message: Like(`%${message}%`) },
order: { createdAt: 'DESC' }
});
}
async findByDateRange(startDate: Date, endDate: Date): Promise<Chat[]> {
return await this.repository.find({
where: {
createdAt: MoreThan(startDate) && LessThan(endDate)
},
order: { createdAt: 'ASC' }
});
}
async getChatRooms(): Promise<string[]> {
const result = await this.repository
.createQueryBuilder('chat')
.select('DISTINCT chat.Chatuuid', 'chatUuid')
.getRawMany();
return result.map(r => r.chatUuid);
}
async getParticipantCount(chatUuid: string): Promise<number> {
const uniqueUsers = await this.getUniqueUsers(chatUuid);
return uniqueUsers.length;
}
async getMessagesByTimeRange(chatUuid: string, hours: number): Promise<Chat[]> {
const hoursAgo = new Date();
hoursAgo.setHours(hoursAgo.getHours() - hours);
return await this.repository.find({
where: {
Chatuuid: chatUuid,
createdAt: MoreThan(hoursAgo)
},
order: { createdAt: 'ASC' }
});
}
async getLastActivity(chatUuid: string): Promise<Date | null> {
const latestMessage = await this.findLatestMessage(chatUuid);
return latestMessage ? latestMessage.createdAt : null;
}
async getUserLastActivity(userId: number): Promise<Date | null> {
const latestMessage = await this.repository.findOne({
where: { UserId: userId },
order: { createdAt: 'DESC' }
});
return latestMessage ? latestMessage.createdAt : null;
}
async getTopActiveChatRooms(limit: number = 10): Promise<any[]> {
const result = await this.repository
.createQueryBuilder('chat')
.select('chat.Chatuuid', 'chatUuid')
.addSelect('COUNT(*)', 'messageCount')
.groupBy('chat.Chatuuid')
.orderBy('messageCount', 'DESC')
.limit(limit)
.getRawMany();
return result;
}
async getChatStatistics(): Promise<any> {
const totalMessages = await this.repository.count();
const totalChatRooms = await this.getChatRooms();
const totalUsers = await this.repository
.createQueryBuilder('chat')
.select('COUNT(DISTINCT chat.UserId)', 'count')
.getRawOne();
const today = new Date();
today.setHours(0, 0, 0, 0);
const todayMessages = await this.repository.count({
where: { createdAt: MoreThan(today) }
});
const lastWeek = new Date();
lastWeek.setDate(lastWeek.getDate() - 7);
const weeklyMessages = await this.repository.count({
where: { createdAt: MoreThan(lastWeek) }
});
return {
totalMessages,
totalChatRooms: totalChatRooms.length,
totalUsers: parseInt(totalUsers.count),
todayMessages,
weeklyMessages
};
}
async getActiveRooms(hours: number = 24): Promise<string[]> {
const hoursAgo = new Date();
hoursAgo.setHours(hoursAgo.getHours() - hours);
const result = await this.repository
.createQueryBuilder('chat')
.select('DISTINCT chat.Chatuuid', 'chatUuid')
.where('chat.createdAt > :hoursAgo', { hoursAgo })
.getRawMany();
return result.map(r => r.chatUuid);
}
async getUserChatActivity(userId: number, days: number = 7): Promise<any[]> {
const daysAgo = new Date();
daysAgo.setDate(daysAgo.getDate() - days);
const result = await this.repository
.createQueryBuilder('chat')
.select('DATE(chat.createdAt)', 'date')
.addSelect('COUNT(*)', 'messageCount')
.where('chat.UserId = :userId', { userId })
.andWhere('chat.createdAt > :daysAgo', { daysAgo })
.groupBy('DATE(chat.createdAt)')
.orderBy('date', 'ASC')
.getRawMany();
return result;
}
}
@@ -0,0 +1,441 @@
import { DataSource, Like, MoreThan, LessThan, Not, IsNull } from 'typeorm';
import { BaseRepository } from './BaseRepository';
import { ICompanyContactRepository } from './IRepository/ICompanyContactRepository';
import { CompanyContact } from '../DataBase/entities/companyContact.entity';
export class CompanyContactRepository extends BaseRepository<CompanyContact> implements ICompanyContactRepository {
constructor(dataSource: DataSource) {
super(dataSource, CompanyContact);
}
async findByEmail(email: string): Promise<CompanyContact[]> {
return await this.repository.find({
where: { email: Like(`%${email}%`) },
order: { createdAt: 'DESC' }
});
}
async findByCompanyName(companyName: string): Promise<CompanyContact[]> {
return await this.repository.find({
where: { CompanyName: Like(`%${companyName}%`) },
order: { createdAt: 'DESC' }
});
}
async findByContactName(firstName: string, lastName: string): Promise<CompanyContact[]> {
return await this.repository.find({
where: {
ContactFirstName: Like(`%${firstName}%`),
ContactLastName: Like(`%${lastName}%`)
},
order: { createdAt: 'DESC' }
});
}
async findByPhone(phone: string): Promise<CompanyContact[]> {
return await this.repository.find({
where: { phone: Like(`%${phone}%`) },
order: { createdAt: 'DESC' }
});
}
async findByGuid(guid: string): Promise<CompanyContact | null> {
return await this.repository.findOne({ where: { guid } });
}
async findByCreatedBy(userId: number): Promise<CompanyContact[]> {
return await this.repository.find({
where: { createdBy: { id: userId } },
order: { createdAt: 'DESC' }
});
}
async findByDateRange(startDate: Date, endDate: Date): Promise<CompanyContact[]> {
return await this.repository.find({
where: {
createdAt: MoreThan(startDate) && LessThan(endDate)
},
order: { createdAt: 'DESC' }
});
}
async findRecent(days: number): Promise<CompanyContact[]> {
const daysAgo = new Date();
daysAgo.setDate(daysAgo.getDate() - days);
return await this.repository.find({
where: {
createdAt: MoreThan(daysAgo)
},
order: { createdAt: 'DESC' }
});
}
async findByEmailDomain(domain: string): Promise<CompanyContact[]> {
return await this.repository.find({
where: { email: Like(`%@${domain}`) },
order: { createdAt: 'DESC' }
});
}
async findWithMessage(): Promise<CompanyContact[]> {
return await this.repository.find({
where: { message: Not(IsNull()) },
order: { createdAt: 'DESC' }
});
}
async findWithoutMessage(): Promise<CompanyContact[]> {
return await this.repository.find({
where: { message: IsNull() },
order: { createdAt: 'DESC' }
});
}
async searchByCompanyName(companyName: string): Promise<CompanyContact[]> {
return await this.repository.find({
where: { CompanyName: Like(`%${companyName}%`) },
order: { createdAt: 'DESC' }
});
}
async searchByContactName(name: string): Promise<CompanyContact[]> {
return await this.repository.find({
where: [
{ ContactFirstName: Like(`%${name}%`) },
{ ContactLastName: Like(`%${name}%`) }
],
order: { createdAt: 'DESC' }
});
}
async searchByMessage(searchTerm: string): Promise<CompanyContact[]> {
return await this.repository.find({
where: {
message: Like(`%${searchTerm}%`)
},
order: { createdAt: 'DESC' }
});
}
async searchAll(searchTerm: string): Promise<CompanyContact[]> {
return await this.repository.find({
where: [
{ CompanyName: Like(`%${searchTerm}%`) },
{ ContactFirstName: Like(`%${searchTerm}%`) },
{ ContactLastName: Like(`%${searchTerm}%`) },
{ email: Like(`%${searchTerm}%`) },
{ phone: Like(`%${searchTerm}%`) },
{ message: Like(`%${searchTerm}%`) }
],
order: { createdAt: 'DESC' }
});
}
async findByExactEmail(email: string): Promise<CompanyContact[]> {
return await this.repository.find({
where: { email },
order: { createdAt: 'DESC' }
});
}
async findByExactPhone(phone: string): Promise<CompanyContact[]> {
return await this.repository.find({
where: { phone },
order: { createdAt: 'DESC' }
});
}
async updateMessage(contactId: number, message: string): Promise<boolean> {
const result = await this.repository.update(contactId, {
message
});
return (result.affected ?? 0) > 0;
}
async updateContactInfo(contactId: number, firstName: string, lastName: string, email: string, phone: string): Promise<boolean> {
const result = await this.repository.update(contactId, {
ContactFirstName: firstName,
ContactLastName: lastName,
email,
phone
});
return (result.affected ?? 0) > 0;
}
async updateCompanyName(contactId: number, companyName: string): Promise<boolean> {
const result = await this.repository.update(contactId, {
CompanyName: companyName
});
return (result.affected ?? 0) > 0;
}
async findWithCreatedByRelation(): Promise<CompanyContact[]> {
return await this.findWithRelations(['createdBy']);
}
async getCompanyContactStatistics(): Promise<any> {
const totalContacts = await this.repository.count();
const today = new Date();
today.setHours(0, 0, 0, 0);
const todayContacts = await this.repository.count({
where: { createdAt: MoreThan(today) }
});
const thisWeek = new Date();
thisWeek.setDate(thisWeek.getDate() - 7);
const weeklyContacts = await this.repository.count({
where: { createdAt: MoreThan(thisWeek) }
});
const thisMonth = new Date();
thisMonth.setDate(1);
thisMonth.setHours(0, 0, 0, 0);
const monthlyContacts = await this.repository.count({
where: { createdAt: MoreThan(thisMonth) }
});
const withMessage = await this.repository.count({
where: { message: Not(IsNull()) }
});
return {
totalContacts,
todayContacts,
weeklyContacts,
monthlyContacts,
withMessage,
withoutMessage: totalContacts - withMessage
};
}
async getTopCompaniesByContacts(limit: number = 10): Promise<any[]> {
const result = await this.repository
.createQueryBuilder('companyContact')
.select('companyContact.CompanyName', 'companyName')
.addSelect('COUNT(*)', 'contactCount')
.groupBy('companyContact.CompanyName')
.orderBy('contactCount', 'DESC')
.limit(limit)
.getRawMany();
return result;
}
async getContactsByEmailDomain(): Promise<any[]> {
const result = await this.repository
.createQueryBuilder('companyContact')
.select('SUBSTRING(companyContact.email, LOCATE(\'@\', companyContact.email) + 1)', 'domain')
.addSelect('COUNT(*)', 'count')
.groupBy('domain')
.orderBy('count', 'DESC')
.getRawMany();
return result;
}
async getMonthlyContactTrends(months: number = 12): Promise<any[]> {
const monthsAgo = new Date();
monthsAgo.setMonth(monthsAgo.getMonth() - months);
const result = await this.repository
.createQueryBuilder('companyContact')
.select('YEAR(companyContact.createdAt)', 'year')
.addSelect('MONTH(companyContact.createdAt)', 'month')
.addSelect('COUNT(*)', 'count')
.where('companyContact.createdAt > :monthsAgo', { monthsAgo })
.groupBy('YEAR(companyContact.createdAt), MONTH(companyContact.createdAt)')
.orderBy('year, month', 'ASC')
.getRawMany();
return result;
}
async getTopEmailDomains(limit: number = 10): Promise<any[]> {
const result = await this.repository
.createQueryBuilder('companyContact')
.select('SUBSTRING(companyContact.email, LOCATE(\'@\', companyContact.email) + 1)', 'domain')
.addSelect('COUNT(*)', 'count')
.groupBy('domain')
.orderBy('count', 'DESC')
.limit(limit)
.getRawMany();
return result;
}
async getContactFrequency(): Promise<any[]> {
const result = await this.repository
.createQueryBuilder('companyContact')
.select('companyContact.email', 'email')
.addSelect('COUNT(*)', 'count')
.groupBy('companyContact.email')
.orderBy('count', 'DESC')
.getRawMany();
return result;
}
async getMostActiveCompanies(limit: number = 10): Promise<any[]> {
const result = await this.repository
.createQueryBuilder('companyContact')
.select('companyContact.CompanyName', 'companyName')
.addSelect('COUNT(*)', 'contactCount')
.addSelect('MAX(companyContact.createdAt)', 'lastContact')
.groupBy('companyContact.CompanyName')
.orderBy('contactCount', 'DESC')
.addOrderBy('lastContact', 'DESC')
.limit(limit)
.getRawMany();
return result;
}
async getCompaniesRequiringFollowup(days: number = 30): Promise<any[]> {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - days);
const result = await this.repository
.createQueryBuilder('companyContact')
.select('companyContact.CompanyName', 'companyName')
.addSelect('MAX(companyContact.createdAt)', 'lastContact')
.addSelect('COUNT(*)', 'contactCount')
.groupBy('companyContact.CompanyName')
.having('MAX(companyContact.createdAt) < :cutoffDate', { cutoffDate })
.orderBy('lastContact', 'ASC')
.getRawMany();
return result;
}
async getAverageMessageLength(): Promise<number> {
const result = await this.repository
.createQueryBuilder('companyContact')
.select('AVG(LENGTH(companyContact.message))', 'avgLength')
.where('companyContact.message IS NOT NULL')
.getRawOne();
return parseFloat(result.avgLength) || 0;
}
async bulkUpdateCompanyName(oldName: string, newName: string): Promise<boolean> {
const result = await this.repository.update(
{ CompanyName: oldName },
{ CompanyName: newName }
);
return (result.affected ?? 0) > 0;
}
async deleteOldContacts(olderThanDays: number): Promise<boolean> {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - olderThanDays);
const result = await this.repository.delete({
createdAt: LessThan(cutoffDate)
});
return (result.affected ?? 0) > 0;
}
async findRegisteredUserContacts(): Promise<CompanyContact[]> {
return await this.repository.find({
relations: ['createdBy'],
where: { createdBy: Not(IsNull()) },
order: { createdAt: 'DESC' }
});
}
async findAnonymousContacts(): Promise<CompanyContact[]> {
return await this.repository.find({
where: { createdBy: IsNull() },
order: { createdAt: 'DESC' }
});
}
async findByMessageLength(minLength: number, maxLength?: number): Promise<CompanyContact[]> {
const query = this.repository.createQueryBuilder('companyContact');
query.where('LENGTH(companyContact.message) >= :minLength', { minLength });
if (maxLength) {
query.andWhere('LENGTH(companyContact.message) <= :maxLength', { maxLength });
}
return await query.orderBy('companyContact.createdAt', 'DESC').getMany();
}
async getContactsByCompanyStats(): Promise<any[]> {
const result = await this.repository
.createQueryBuilder('companyContact')
.select('companyContact.CompanyName', 'companyName')
.addSelect('COUNT(*)', 'contactCount')
.addSelect('MAX(companyContact.createdAt)', 'lastContact')
.addSelect('MIN(companyContact.createdAt)', 'firstContact')
.groupBy('companyContact.CompanyName')
.orderBy('contactCount', 'DESC')
.getRawMany();
return result;
}
async getContactsByDomainStats(): Promise<any[]> {
const result = await this.repository
.createQueryBuilder('companyContact')
.select('SUBSTRING(companyContact.email, LOCATE(\'@\', companyContact.email) + 1)', 'domain')
.addSelect('COUNT(*)', 'count')
.addSelect('COUNT(DISTINCT companyContact.CompanyName)', 'uniqueCompanies')
.groupBy('domain')
.orderBy('count', 'DESC')
.getRawMany();
return result;
}
async getMostActiveDay(): Promise<any> {
const result = await this.repository
.createQueryBuilder('companyContact')
.select('DAYNAME(companyContact.createdAt)', 'dayName')
.addSelect('COUNT(*)', 'count')
.groupBy('DAYNAME(companyContact.createdAt)')
.orderBy('count', 'DESC')
.limit(1)
.getRawOne();
return result;
}
async getRecentContacts(limit: number = 10): Promise<CompanyContact[]> {
return await this.repository.find({
order: { createdAt: 'DESC' },
take: limit
});
}
async getUnansweredContacts(): Promise<CompanyContact[]> {
const threeDaysAgo = new Date();
threeDaysAgo.setDate(threeDaysAgo.getDate() - 3);
return await this.repository.find({
where: {
createdAt: LessThan(threeDaysAgo)
},
order: { createdAt: 'ASC' }
});
}
async getContactsByDateStats(days: number = 30): Promise<any[]> {
const daysAgo = new Date();
daysAgo.setDate(daysAgo.getDate() - days);
const result = await this.repository
.createQueryBuilder('companyContact')
.select('DATE(companyContact.createdAt)', 'date')
.addSelect('COUNT(*)', 'count')
.where('companyContact.createdAt > :daysAgo', { daysAgo })
.groupBy('DATE(companyContact.createdAt)')
.orderBy('date', 'DESC')
.getRawMany();
return result;
}
}
@@ -0,0 +1,227 @@
import { DataSource, Like, MoreThan, LessThan } from 'typeorm';
import { BaseRepository } from './BaseRepository';
import { ICompanyRepository } from './IRepository/ICompanyRepository';
import { Company } from '../DataBase/entities/company.entity';
export class CompanyRepository extends BaseRepository<Company> implements ICompanyRepository {
constructor(dataSource: DataSource) {
super(dataSource, Company);
}
async findByName(name: string): Promise<Company | null> {
return await this.repository.findOne({ where: { Name: name } });
}
async findByContactEmail(email: string): Promise<Company | null> {
return await this.repository.findOne({ where: { ContactEmail: email } });
}
async findByLoginURL(loginURL: string): Promise<Company | null> {
return await this.repository.findOne({ where: { LoginURL: loginURL } });
}
async findRegisteredAfter(date: Date): Promise<Company[]> {
return await this.repository.find({
where: {
RegDate: MoreThan(date)
},
order: { RegDate: 'DESC' }
});
}
async findRegisteredBefore(date: Date): Promise<Company[]> {
return await this.repository.find({
where: {
RegDate: LessThan(date)
},
order: { RegDate: 'DESC' }
});
}
async findByGUID(guid: string): Promise<Company | null> {
return await this.repository.findOne({ where: { GUID: guid } });
}
async findByContactName(firstName: string, lastName: string): Promise<Company[]> {
return await this.repository.find({
where: {
ContactFirstName: Like(`%${firstName}%`),
ContactLastName: Like(`%${lastName}%`)
}
});
}
async findRecentlyRegistered(days: number): Promise<Company[]> {
const dateThreshold = new Date();
dateThreshold.setDate(dateThreshold.getDate() - days);
return await this.repository.find({
where: {
RegDate: MoreThan(dateThreshold)
},
order: { RegDate: 'DESC' }
});
}
async findRecentlyUpdated(days: number): Promise<Company[]> {
const dateThreshold = new Date();
dateThreshold.setDate(dateThreshold.getDate() - days);
return await this.repository.find({
where: {
UpdatedDate: MoreThan(dateThreshold)
},
order: { UpdatedDate: 'DESC' }
});
}
async updateContactInfo(companyId: number, firstName: string, lastName: string, email: string): Promise<boolean> {
const result = await this.repository.update(companyId, {
ContactFirstName: firstName,
ContactLastName: lastName,
ContactEmail: email,
UpdatedDate: new Date()
});
return (result.affected ?? 0) > 0;
}
async updateLoginURL(companyId: number, loginURL: string): Promise<boolean> {
const result = await this.repository.update(companyId, {
LoginURL: loginURL,
UpdatedDate: new Date()
});
return (result.affected ?? 0) > 0;
}
async updateCompanyName(companyId: number, name: string): Promise<boolean> {
const result = await this.repository.update(companyId, {
Name: name,
UpdatedDate: new Date()
});
return (result.affected ?? 0) > 0;
}
async searchCompanies(searchTerm: string): Promise<Company[]> {
return await this.repository.find({
where: [
{ Name: Like(`%${searchTerm}%`) },
{ ContactFirstName: Like(`%${searchTerm}%`) },
{ ContactLastName: Like(`%${searchTerm}%`) },
{ ContactEmail: Like(`%${searchTerm}%`) }
]
});
}
async searchByName(name: string): Promise<Company[]> {
return await this.repository.find({
where: {
Name: Like(`%${name}%`)
}
});
}
async findWithUsersRelation(): Promise<Company[]> {
return await this.findWithRelations(['users']);
}
async findWithCardsRelation(): Promise<Company[]> {
return await this.findWithRelations(['cards']);
}
async findWithAllRelations(): Promise<Company[]> {
return await this.findWithRelations(['users', 'cards']);
}
async findWithUserCount(): Promise<any[]> {
return await this.repository
.createQueryBuilder('company')
.leftJoinAndSelect('company.users', 'users')
.loadRelationCountAndMap('company.userCount', 'company.users')
.getMany();
}
async findWithCardCount(): Promise<any[]> {
return await this.repository
.createQueryBuilder('company')
.leftJoinAndSelect('company.cards', 'cards')
.loadRelationCountAndMap('company.cardCount', 'company.cards')
.getMany();
}
async getCompanyStatistics(): Promise<any> {
const totalCompanies = await this.repository.count();
const currentMonth = new Date();
currentMonth.setDate(1);
currentMonth.setHours(0, 0, 0, 0);
const lastMonth = new Date(currentMonth);
lastMonth.setMonth(lastMonth.getMonth() - 1);
const companiesThisMonth = await this.repository.count({
where: {
RegDate: MoreThan(currentMonth)
}
});
const companiesLastMonth = await this.repository.count({
where: {
RegDate: MoreThan(lastMonth)
}
});
return {
totalCompanies,
companiesThisMonth,
companiesLastMonth,
monthlyGrowth: companiesThisMonth - companiesLastMonth
};
}
async getTopCompaniesByUserCount(limit: number = 10): Promise<any[]> {
return await this.repository
.createQueryBuilder('company')
.leftJoinAndSelect('company.users', 'users')
.loadRelationCountAndMap('company.userCount', 'company.users')
.orderBy('userCount', 'DESC')
.limit(limit)
.getMany();
}
async getTopCompaniesByCardCount(limit: number = 10): Promise<any[]> {
return await this.repository
.createQueryBuilder('company')
.leftJoinAndSelect('company.cards', 'cards')
.loadRelationCountAndMap('company.cardCount', 'company.cards')
.orderBy('cardCount', 'DESC')
.limit(limit)
.getMany();
}
async getTopCompaniesByUsers(limit: number = 10): Promise<any[]> {
return await this.repository
.createQueryBuilder('company')
.leftJoinAndSelect('company.users', 'users')
.loadRelationCountAndMap('company.userCount', 'company.users')
.orderBy('userCount', 'DESC')
.limit(limit)
.getMany();
}
async getTopCompaniesByCards(limit: number = 10): Promise<any[]> {
return await this.repository
.createQueryBuilder('company')
.leftJoinAndSelect('company.cards', 'cards')
.loadRelationCountAndMap('company.cardCount', 'company.cards')
.orderBy('cardCount', 'DESC')
.limit(limit)
.getMany();
}
async getRecentlyActiveCompanies(limit: number = 10): Promise<Company[]> {
return await this.repository.find({
order: { UpdatedDate: 'DESC' },
take: limit
});
}
}
@@ -0,0 +1,354 @@
import { DataSource, Like, MoreThan, LessThan, Not, IsNull } from 'typeorm';
import { BaseRepository } from './BaseRepository';
import { IContactRepository } from './IRepository/IContactRepository';
import { Contact } from '../DataBase/entities/contact.entity';
export class ContactRepository extends BaseRepository<Contact> implements IContactRepository {
constructor(dataSource: DataSource) {
super(dataSource, Contact);
}
async findByEmailDomain(domain: string): Promise<Contact[]> {
return await this.repository.find({
where: { email: Like(`%@${domain}`) },
order: { createdAt: 'DESC' }
});
}
async getContactsByDateStats(): Promise<any[]> {
const result = await this.repository
.createQueryBuilder('contact')
.select('DATE(contact.createdAt)', 'date')
.addSelect('COUNT(*)', 'count')
.groupBy('DATE(contact.createdAt)')
.orderBy('date', 'ASC')
.getRawMany();
return result;
}
async getContactsByDomainStats(): Promise<any[]> {
const result = await this.repository
.createQueryBuilder('contact')
.select('SUBSTRING(contact.email, LOCATE(\'@\', contact.email) + 1)', 'domain')
.addSelect('COUNT(*)', 'count')
.groupBy('domain')
.orderBy('count', 'DESC')
.getRawMany();
return result;
}
async getMostActiveDay(): Promise<string> {
const result = await this.repository
.createQueryBuilder('contact')
.select('DATE(contact.createdAt)', 'date')
.addSelect('COUNT(*)', 'count')
.groupBy('DATE(contact.createdAt)')
.orderBy('count', 'DESC')
.limit(1)
.getRawOne();
return result ? result.date : '';
}
async getRecentContacts(limit: number = 10): Promise<Contact[]> {
return await this.repository.find({
order: { createdAt: 'DESC' },
take: limit
});
}
async findByEmail(email: string): Promise<Contact[]> {
return await this.repository.find({
where: { email: Like(`%${email}%`) },
order: { createdAt: 'DESC' }
});
}
async findByName(name: string): Promise<Contact[]> {
return await this.repository.find({
where: { name: Like(`%${name}%`) },
order: { createdAt: 'DESC' }
});
}
async findByGuid(guid: string): Promise<Contact | null> {
return await this.repository.findOne({ where: { guid } });
}
async findByUserId(userId: number): Promise<Contact[]> {
return await this.repository.find({
where: { user: { id: userId } },
order: { createdAt: 'DESC' }
});
}
async findByDateRange(startDate: Date, endDate: Date): Promise<Contact[]> {
return await this.repository.find({
where: {
createdAt: MoreThan(startDate) && LessThan(endDate)
},
order: { createdAt: 'DESC' }
});
}
async findRecent(days: number): Promise<Contact[]> {
const daysAgo = new Date();
daysAgo.setDate(daysAgo.getDate() - days);
return await this.repository.find({
where: {
createdAt: MoreThan(daysAgo)
},
order: { createdAt: 'DESC' }
});
}
async searchByMessage(searchTerm: string): Promise<Contact[]> {
return await this.repository.find({
where: {
message: Like(`%${searchTerm}%`)
},
order: { createdAt: 'DESC' }
});
}
async searchContacts(searchTerm: string): Promise<Contact[]> {
return await this.repository.find({
where: [
{ name: Like(`%${searchTerm}%`) },
{ email: Like(`%${searchTerm}%`) },
{ message: Like(`%${searchTerm}%`) }
],
order: { createdAt: 'DESC' }
});
}
async findByExactEmail(email: string): Promise<Contact[]> {
return await this.repository.find({
where: { email },
order: { createdAt: 'DESC' }
});
}
async getTodaysContacts(): Promise<Contact[]> {
const today = new Date();
today.setHours(0, 0, 0, 0);
return await this.repository.find({
where: {
createdAt: MoreThan(today)
},
order: { createdAt: 'DESC' }
});
}
async getContactsByMonth(year: number, month: number): Promise<Contact[]> {
const startDate = new Date(year, month - 1, 1);
const endDate = new Date(year, month, 0);
return await this.findByDateRange(startDate, endDate);
}
async getContactsByYear(year: number): Promise<Contact[]> {
const startDate = new Date(year, 0, 1);
const endDate = new Date(year, 11, 31);
return await this.findByDateRange(startDate, endDate);
}
async updateContactMessage(contactId: number, message: string): Promise<boolean> {
const result = await this.repository.update(contactId, {
message,
updatedAt: new Date()
});
return (result.affected ?? 0) > 0;
}
async markAsRead(contactId: number): Promise<boolean> {
const result = await this.repository.update(contactId, {
updatedAt: new Date()
});
return (result.affected ?? 0) > 0;
}
async findWithUserRelation(): Promise<Contact[]> {
return await this.findWithRelations(['user']);
}
async getContactStatistics(): Promise<any> {
const totalContacts = await this.repository.count();
const today = new Date();
today.setHours(0, 0, 0, 0);
const todayContacts = await this.repository.count({
where: { createdAt: MoreThan(today) }
});
const thisWeek = new Date();
thisWeek.setDate(thisWeek.getDate() - 7);
const weeklyContacts = await this.repository.count({
where: { createdAt: MoreThan(thisWeek) }
});
const thisMonth = new Date();
thisMonth.setDate(1);
thisMonth.setHours(0, 0, 0, 0);
const monthlyContacts = await this.repository.count({
where: { createdAt: MoreThan(thisMonth) }
});
return {
totalContacts,
todayContacts,
weeklyContacts,
monthlyContacts
};
}
async getContactFrequency(): Promise<any[]> {
const result = await this.repository
.createQueryBuilder('contact')
.select('contact.email', 'email')
.addSelect('COUNT(*)', 'count')
.groupBy('contact.email')
.orderBy('count', 'DESC')
.getRawMany();
return result;
}
async getContactsByEmailDomain(domain: string): Promise<Contact[]> {
return await this.repository.find({
where: { email: Like(`%@${domain}`) },
order: { createdAt: 'DESC' }
});
}
async findRegisteredUserContacts(): Promise<Contact[]> {
return await this.repository.find({
relations: ['user'],
where: { user: Not(IsNull()) },
order: { createdAt: 'DESC' }
});
}
async findAnonymousContacts(): Promise<Contact[]> {
return await this.repository.find({
where: { user: IsNull() },
order: { createdAt: 'DESC' }
});
}
async findByMessageLength(minLength: number, maxLength?: number): Promise<Contact[]> {
const query = this.repository.createQueryBuilder('contact');
query.where('LENGTH(contact.message) >= :minLength', { minLength });
if (maxLength) {
query.andWhere('LENGTH(contact.message) <= :maxLength', { maxLength });
}
return await query.orderBy('contact.createdAt', 'DESC').getMany();
}
async getContactsByStatus(status: string): Promise<Contact[]> {
return await this.repository.find({
order: { createdAt: 'DESC' }
});
}
async getUnansweredContacts(): Promise<Contact[]> {
const threeDaysAgo = new Date();
threeDaysAgo.setDate(threeDaysAgo.getDate() - 3);
return await this.repository.find({
where: {
createdAt: LessThan(threeDaysAgo),
updatedAt: LessThan(threeDaysAgo)
},
order: { createdAt: 'ASC' }
});
}
async getResponseTimeStatistics(): Promise<any> {
const result = await this.repository
.createQueryBuilder('contact')
.select('AVG(TIMESTAMPDIFF(HOUR, contact.createdAt, contact.updatedAt))', 'avgResponseTime')
.where('contact.updatedAt > contact.createdAt')
.getRawOne();
return {
averageResponseTimeHours: parseFloat(result.avgResponseTime) || 0
};
}
async getTopEmailDomains(limit: number = 10): Promise<any[]> {
const result = await this.repository
.createQueryBuilder('contact')
.select('SUBSTRING(contact.email, LOCATE(\'@\', contact.email) + 1)', 'domain')
.addSelect('COUNT(*)', 'count')
.groupBy('domain')
.orderBy('count', 'DESC')
.limit(limit)
.getRawMany();
return result;
}
async getMonthlyContactTrends(months: number = 12): Promise<any[]> {
const monthsAgo = new Date();
monthsAgo.setMonth(monthsAgo.getMonth() - months);
const result = await this.repository
.createQueryBuilder('contact')
.select('YEAR(contact.createdAt)', 'year')
.addSelect('MONTH(contact.createdAt)', 'month')
.addSelect('COUNT(*)', 'count')
.where('contact.createdAt > :monthsAgo', { monthsAgo })
.groupBy('YEAR(contact.createdAt), MONTH(contact.createdAt)')
.orderBy('year, month', 'ASC')
.getRawMany();
return result;
}
async getAverageMessageLength(): Promise<number> {
const result = await this.repository
.createQueryBuilder('contact')
.select('AVG(LENGTH(contact.message))', 'avgLength')
.getRawOne();
return parseFloat(result.avgLength) || 0;
}
async getLongestMessage(): Promise<Contact | null> {
return await this.repository.findOne({
order: { message: 'DESC' }
});
}
async getShortestMessage(): Promise<Contact | null> {
return await this.repository.findOne({
order: { message: 'ASC' }
});
}
async bulkUpdateStatus(contactIds: number[]): Promise<boolean> {
const result = await this.repository.update(contactIds, {
updatedAt: new Date()
});
return (result.affected ?? 0) > 0;
}
async getContactsRequiringResponse(): Promise<Contact[]> {
const threeDaysAgo = new Date();
threeDaysAgo.setDate(threeDaysAgo.getDate() - 3);
return await this.repository.find({
where: {
createdAt: LessThan(threeDaysAgo),
updatedAt: LessThan(threeDaysAgo)
},
order: { createdAt: 'ASC' }
});
}
}
@@ -0,0 +1,75 @@
import { DataSource } from 'typeorm';
import { RepositoryFactory } from './RepositoryManager';
import { AppDataSource } from '../../ormconfig';
import { appLogger } from '../Utils/EnhancedLogger';
const dataSource = AppDataSource;
export async function initializeDatabase(): Promise<DataSource> {
try {
const options = dataSource.options as any;
await appLogger.database('CONNECTION_ATTEMPT', undefined, undefined, {
host: options.host,
port: options.port,
database: options.database,
type: options.type
});
await dataSource.initialize();
await appLogger.database('CONNECTION_SUCCESS', undefined, undefined, {
host: options.host,
port: options.port,
database: options.database,
type: options.type
});
RepositoryFactory.initialize(dataSource);
await appLogger.system('Repository factory initialized');
return dataSource;
} catch (error) {
const options = dataSource.options as any;
await appLogger.errorEvent(
error instanceof Error ? error.message : String(error),
'database_initialization',
{
host: options.host,
port: options.port,
database: options.database,
type: options.type,
stack: error instanceof Error ? error.stack : undefined
}
);
throw error;
}
}
export async function closeDatabase(): Promise<void> {
try {
await RepositoryFactory.closeConnection();
const options = dataSource.options as any;
await appLogger.database('CONNECTION_CLOSED', undefined, undefined, {
host: options.host,
port: options.port,
database: options.database
});
} catch (error) {
await appLogger.errorEvent(
error instanceof Error ? error.message : String(error),
'database_close',
{
stack: error instanceof Error ? error.stack : undefined
}
);
throw error;
}
}
export function getRepositoryManager() {
return RepositoryFactory.getInstance();
}
export { dataSource };
@@ -0,0 +1,183 @@
import { createLogger, format, transports } from 'winston';
import { DATABASE_LOG_CONFIG, PERFORMANCE_THRESHOLDS, LOG_FILTERS, getEnvironmentConfig } from './LoggingConfig';
// Database operation logger
export const dbLogger = createLogger(DATABASE_LOG_CONFIG);
// Apply environment-specific configuration
const envConfig = getEnvironmentConfig();
dbLogger.level = envConfig.level;
// Performance monitoring
export class PerformanceMonitor {
private static timers: Map<string, number> = new Map();
static startTimer(operationId: string): void {
this.timers.set(operationId, Date.now());
}
static endTimer(operationId: string, operation: string, entityName: string): number {
const startTime = this.timers.get(operationId);
if (!startTime) {
dbLogger.warn('Timer not found for operation', { operationId, operation, entityName });
return 0;
}
const duration = Date.now() - startTime;
this.timers.delete(operationId);
// Log based on performance thresholds
if (duration > PERFORMANCE_THRESHOLDS.VERY_SLOW_OPERATION) {
dbLogger.error('Very slow database operation detected', {
operation,
entityName,
duration,
operationId,
threshold: PERFORMANCE_THRESHOLDS.VERY_SLOW_OPERATION,
severity: 'critical'
});
} else if (duration > PERFORMANCE_THRESHOLDS.SLOW_OPERATION) {
dbLogger.warn('Slow database operation detected', {
operation,
entityName,
duration,
operationId,
threshold: PERFORMANCE_THRESHOLDS.SLOW_OPERATION,
severity: 'warning'
});
} else {
dbLogger.debug('Database operation completed', {
operation,
entityName,
duration,
operationId
});
}
return duration;
}
static generateOperationId(): string {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
}
// Database operation types for logging
export enum DBOperation {
CREATE = 'CREATE',
READ = 'READ',
UPDATE = 'UPDATE',
DELETE = 'DELETE',
SOFT_DELETE = 'SOFT_DELETE',
BULK_CREATE = 'BULK_CREATE',
BULK_UPDATE = 'BULK_UPDATE',
BULK_DELETE = 'BULK_DELETE',
FIND = 'FIND',
SEARCH = 'SEARCH',
COUNT = 'COUNT',
AGGREGATE = 'AGGREGATE',
TRANSACTION = 'TRANSACTION'
}
// Logging decorators
export function LogDBOperation(operation: DBOperation, entityName: string) {
return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {
const method = descriptor.value;
descriptor.value = async function (...args: any[]) {
const operationId = PerformanceMonitor.generateOperationId();
const startTime = Date.now();
PerformanceMonitor.startTimer(operationId);
dbLogger.info('Database operation started', {
operation,
entityName,
method: propertyName,
operationId,
args: args.length > 0 ? 'provided' : 'none'
});
try {
const result = await method.apply(this, args);
const duration = PerformanceMonitor.endTimer(operationId, operation, entityName);
dbLogger.info('Database operation successful', {
operation,
entityName,
method: propertyName,
operationId,
duration,
resultCount: Array.isArray(result) ? result.length : result ? 1 : 0
});
return result;
} catch (error) {
const duration = Date.now() - startTime;
dbLogger.error('Database operation failed', {
operation,
entityName,
method: propertyName,
operationId,
duration,
error: error instanceof Error ? error.message : String(error),
stack: error instanceof Error ? error.stack : undefined
});
throw error;
}
};
};
}
// Connection logging
export function logDatabaseConnection(status: 'connecting' | 'connected' | 'disconnected' | 'error', details?: any) {
const logData = {
event: 'database_connection',
status,
timestamp: new Date().toISOString(),
...details
};
if (status === 'error') {
dbLogger.error('Database connection error', logData);
} else {
dbLogger.info('Database connection status', logData);
}
}
// Query logging
export function logQuery(query: string, parameters?: any[], duration?: number) {
const envConfig = getEnvironmentConfig();
if (!envConfig.logQueries) return;
const sanitizedQuery = LOG_FILTERS.sanitizeQuery(query);
const sanitizedParams = parameters ? LOG_FILTERS.sanitizeParameters(parameters) : [];
const logData = {
event: 'database_query',
query: sanitizedQuery,
parameters: sanitizedParams,
duration: duration || 0,
timestamp: new Date().toISOString()
};
if (duration && duration > PERFORMANCE_THRESHOLDS.VERY_SLOW_QUERY) {
dbLogger.error('Very slow query detected', {
...logData,
severity: 'critical',
threshold: PERFORMANCE_THRESHOLDS.VERY_SLOW_QUERY
});
} else if (duration && duration > PERFORMANCE_THRESHOLDS.SLOW_QUERY) {
dbLogger.warn('Slow query detected', {
...logData,
severity: 'warning',
threshold: PERFORMANCE_THRESHOLDS.SLOW_QUERY
});
} else {
dbLogger.debug('Database query executed', logData);
}
}
@@ -0,0 +1,98 @@
import { initializeDatabase, getRepositoryManager, closeDatabase } from './DatabaseConfig';
import { UserStatus, UserAuthLevel } from '../DataBase/entities/user.entity';
// Example usage of the repository system
async function exampleUsage() {
try {
// Initialize database connection
await initializeDatabase();
// Get repository manager
const repositoryManager = getRepositoryManager();
// Get specific repositories
const userRepository = repositoryManager.getUserRepository();
const companyRepository = repositoryManager.getCompanyRepository();
const cardsRepository = repositoryManager.getCardsRepository();
const chatRepository = repositoryManager.getChatRepository();
const contactRepository = repositoryManager.getContactRepository();
const companyContactRepository = repositoryManager.getCompanyContactRepository();
// Example operations
// 1. User operations
const allUsers = await userRepository.findAll();
console.log('Total users:', allUsers.length);
const userByEmail = await userRepository.findByEmail('john@example.com');
console.log('User found:', userByEmail);
const userStats = await userRepository.getUserStatistics();
console.log('User statistics:', userStats);
// 2. Company operations
const allCompanies = await companyRepository.findAll();
console.log('Total companies:', allCompanies.length);
const companyStats = await companyRepository.getCompanyStatistics();
console.log('Company statistics:', companyStats);
// 3. Cards operations
const publicCards = await cardsRepository.findPublicCards();
console.log('Public cards:', publicCards.length);
const cardStats = await cardsRepository.getCardsStatistics();
console.log('Cards statistics:', cardStats);
// 4. Chat operations
const chatRooms = await chatRepository.getChatRooms();
console.log('Chat rooms:', chatRooms);
const chatStats = await chatRepository.getChatStatistics();
console.log('Chat statistics:', chatStats);
// 5. Contact operations
const recentContacts = await contactRepository.findRecent(7);
console.log('Recent contacts (last 7 days):', recentContacts.length);
const contactStats = await contactRepository.getContactStatistics();
console.log('Contact statistics:', contactStats);
// 6. Company contact operations
const topCompanies = await companyContactRepository.getTopCompaniesByContacts(5);
console.log('Top 5 companies by contacts:', topCompanies);
const companyContactStats = await companyContactRepository.getCompanyContactStatistics();
console.log('Company contact statistics:', companyContactStats);
// Example of creating a new user (you would typically use DTOs for this)
console.log('For creating new entities, use the appropriate DTOs and mappers');
// Example of finding users
const userByUsername = await userRepository.findByUsername('testuser');
console.log('User found by username:', userByUsername);
// Example of updating user premium status
if (userByUsername) {
const premiumSet = await userRepository.setPremium(userByUsername.id, 12); // 12 months
console.log('Premium set:', premiumSet);
}
// Example of searching
const searchResults = await userRepository.searchUsers('john');
console.log('Search results:', searchResults.length);
// Example of getting expired premium users
const expiredUsers = await userRepository.getUsersWithExpiredPremium();
console.log('Expired premium users:', expiredUsers.length);
} catch (error) {
console.error('Error in example usage:', error);
} finally {
// Close database connection
await closeDatabase();
}
}
// Export examples for use
export { exampleUsage };
@@ -0,0 +1,14 @@
export interface IBaseRepository<T> {
findAll(): Promise<T[]>;
findById(id: number): Promise<T | null>;
findByGuid(guid: string): Promise<T | null>;
create(entity: T): Promise<T>;
update(id: number, entity: Partial<T>): Promise<T | null>;
delete(id: number): Promise<boolean>;
softDelete(id: number): Promise<boolean>;
restore(id: number): Promise<boolean>;
findWithDeleted(): Promise<T[]>;
findOnlyDeleted(): Promise<T[]>;
count(): Promise<number>;
exists(id: number): Promise<boolean>;
}
@@ -0,0 +1,22 @@
import { IBaseRepository } from './IBaseRepository';
import { Cards, Privacy, CardType } from '../../DataBase/entities/cards.entity';
export interface ICardsRepository extends IBaseRepository<Cards> {
findByTitle(title: string): Promise<Cards[]>;
findByCreator(creatorId: number): Promise<Cards[]>;
findByCompanyId(companyId: number): Promise<Cards[]>;
findByPrivacy(privacy: Privacy): Promise<Cards[]>;
findByType(type: CardType): Promise<Cards[]>;
findByCreationDateRange(startDate: Date, endDate: Date): Promise<Cards[]>;
findPublicCards(): Promise<Cards[]>;
findPrivateCards(userId: number): Promise<Cards[]>;
findCompanyCards(companyId: number): Promise<Cards[]>;
findAccessibleCards(userId: number, companyId?: number): Promise<Cards[]>;
searchByTitle(searchTerm: string): Promise<Cards[]>;
searchByDescription(searchTerm: string): Promise<Cards[]>;
findByQuestionRange(minQuestions: number, maxQuestions: number): Promise<Cards[]>;
getCardsStatistics(): Promise<any>;
getCardsByPrivacyStats(): Promise<any>;
getCardsByTypeStats(): Promise<any>;
getMostActiveCreators(limit: number): Promise<any[]>;
}
@@ -0,0 +1,21 @@
import { IBaseRepository } from './IBaseRepository';
import { Chat } from '../../DataBase/entities/chat.entity';
export interface IChatRepository extends IBaseRepository<Chat> {
findByChatUuid(chatUuid: string): Promise<Chat[]>;
findByUserId(userId: number): Promise<Chat[]>;
findByMessage(message: string): Promise<Chat[]>;
findByDateRange(startDate: Date, endDate: Date): Promise<Chat[]>;
findRecentMessages(chatUuid: string, limit: number): Promise<Chat[]>;
findMessagesByUser(userId: number, limit: number): Promise<Chat[]>;
searchMessages(searchTerm: string): Promise<Chat[]>;
getChatRooms(): Promise<string[]>;
getChatRoomStats(chatUuid: string): Promise<any>;
getActiveUsers(chatUuid: string): Promise<number[]>;
getMessageCount(chatUuid: string): Promise<number>;
getParticipantCount(chatUuid: string): Promise<number>;
getLastActivity(chatUuid: string): Promise<Date | null>;
getChatStatistics(): Promise<any>;
getActiveRooms(limit: number): Promise<any[]>;
getUserChatActivity(userId: number): Promise<any>;
}
@@ -0,0 +1,24 @@
import { IBaseRepository } from './IBaseRepository';
import { CompanyContact } from '../../DataBase/entities/companyContact.entity';
export interface ICompanyContactRepository extends IBaseRepository<CompanyContact> {
findByCompanyName(companyName: string): Promise<CompanyContact[]>;
findByEmail(email: string): Promise<CompanyContact[]>;
findByPhone(phone: string): Promise<CompanyContact[]>;
findByCreatedBy(userId: number): Promise<CompanyContact[]>;
findByDateRange(startDate: Date, endDate: Date): Promise<CompanyContact[]>;
findByEmailDomain(domain: string): Promise<CompanyContact[]>;
findRegisteredUserContacts(): Promise<CompanyContact[]>;
findAnonymousContacts(): Promise<CompanyContact[]>;
searchByMessage(searchTerm: string): Promise<CompanyContact[]>;
findByMessageLength(minLength: number, maxLength: number): Promise<CompanyContact[]>;
getCompanyContactStatistics(): Promise<any>;
getContactsByCompanyStats(): Promise<any>;
getContactsByDateStats(): Promise<any>;
getContactsByDomainStats(): Promise<any>;
getAverageMessageLength(): Promise<number>;
getMostActiveDay(): Promise<string>;
getContactFrequency(): Promise<any>;
getRecentContacts(limit: number): Promise<CompanyContact[]>;
getTopCompaniesByContacts(limit: number): Promise<any[]>;
}
@@ -0,0 +1,16 @@
import { IBaseRepository } from './IBaseRepository';
import { Company } from '../../DataBase/entities/company.entity';
export interface ICompanyRepository extends IBaseRepository<Company> {
findByName(name: string): Promise<Company | null>;
findByContactEmail(email: string): Promise<Company | null>;
findByLoginURL(loginURL: string): Promise<Company | null>;
findRegisteredAfter(date: Date): Promise<Company[]>;
findRegisteredBefore(date: Date): Promise<Company[]>;
findWithUserCount(): Promise<Array<Company & { userCount: number }>>;
findWithCardCount(): Promise<Array<Company & { cardCount: number }>>;
searchByName(searchTerm: string): Promise<Company[]>;
getCompanyStatistics(companyId: number): Promise<any>;
getTopCompaniesByUsers(limit: number): Promise<Company[]>;
getTopCompaniesByCards(limit: number): Promise<Company[]>;
}
@@ -0,0 +1,21 @@
import { IBaseRepository } from './IBaseRepository';
import { Contact } from '../../DataBase/entities/contact.entity';
export interface IContactRepository extends IBaseRepository<Contact> {
findByName(name: string): Promise<Contact[]>;
findByEmail(email: string): Promise<Contact[]>;
findByUserId(userId: number): Promise<Contact[]>;
findByDateRange(startDate: Date, endDate: Date): Promise<Contact[]>;
findByEmailDomain(domain: string): Promise<Contact[]>;
findRegisteredUserContacts(): Promise<Contact[]>;
findAnonymousContacts(): Promise<Contact[]>;
searchByMessage(searchTerm: string): Promise<Contact[]>;
findByMessageLength(minLength: number, maxLength: number): Promise<Contact[]>;
getContactStatistics(): Promise<any>;
getContactsByDateStats(): Promise<any>;
getContactsByDomainStats(): Promise<any>;
getAverageMessageLength(): Promise<number>;
getMostActiveDay(): Promise<string>;
getContactFrequency(): Promise<any>;
getRecentContacts(limit: number): Promise<Contact[]>;
}
@@ -0,0 +1,21 @@
import { IBaseRepository } from './IBaseRepository';
import { User, UserStatus, UserAuthLevel } from '../../DataBase/entities/user.entity';
export interface IUserRepository extends IBaseRepository<User> {
findByUsername(username: string): Promise<User | null>;
findByEmail(email: string): Promise<User | null>;
findByStatus(status: UserStatus): Promise<User[]>;
findByAuthLevel(authLevel: UserAuthLevel): Promise<User[]>;
findByCompanyId(companyId: number): Promise<User[]>;
findPremiumUsers(): Promise<User[]>;
findBySecurityToken(token: string): Promise<User | null>;
updateSecurityToken(userId: number, token: string, expiry: Date): Promise<boolean>;
clearSecurityToken(userId: number): Promise<boolean>;
setPremium(userId: number, months: number): Promise<boolean>;
revokePremium(userId: number): Promise<boolean>;
registerCompany(userId: number, companyId: number): Promise<boolean>;
verifyCompanyRegistration(userId: number): Promise<boolean>;
resetCompanyRegistration(userId: number): Promise<boolean>;
getUsersWithExpiredPremium(): Promise<User[]>;
getUsersNeedingCompanyReregistration(): Promise<User[]>;
}
@@ -0,0 +1,204 @@
import { LoggerOptions } from 'winston';
import { format, transports } from 'winston';
// Log levels configuration
export const LOG_LEVELS = {
error: 0,
warn: 1,
info: 2,
debug: 3,
verbose: 4
};
// Database logging configuration
export const DATABASE_LOG_CONFIG: LoggerOptions = {
level: process.env.DB_LOG_LEVEL || 'info',
levels: LOG_LEVELS,
format: format.combine(
format.timestamp({
format: 'YYYY-MM-DD HH:mm:ss'
}),
format.errors({ stack: true }),
format.splat(),
format.json()
),
defaultMeta: {
service: 'database',
application: 'serpentrace-backend'
},
transports: [
// Console transport
new transports.Console({
format: format.combine(
format.colorize(),
format.printf(({ timestamp, level, message, service, ...meta }) => {
return `${timestamp} [${level}] [${service}] ${message} ${
Object.keys(meta).length ? JSON.stringify(meta, null, 2) : ''
}`;
})
)
}),
// File transport for all database operations
new transports.File({
filename: 'logs/database.log',
format: format.combine(
format.timestamp(),
format.json()
),
maxsize: 5242880, // 5MB
maxFiles: 10,
tailable: true
}),
// File transport for errors only
new transports.File({
filename: 'logs/database-errors.log',
level: 'error',
format: format.combine(
format.timestamp(),
format.json()
),
maxsize: 5242880, // 5MB
maxFiles: 5,
tailable: true
}),
// File transport for slow operations
new transports.File({
filename: 'logs/database-slow.log',
level: 'warn',
format: format.combine(
format.timestamp(),
format.json(),
format((info) => {
const message = typeof info.message === 'string' ? info.message : '';
return message.includes('slow') || (info as any).event === 'slow_query' ? info : false;
})()
),
maxsize: 5242880, // 5MB
maxFiles: 3,
tailable: true
})
]
};
// Performance thresholds
export const PERFORMANCE_THRESHOLDS = {
SLOW_QUERY: parseInt(process.env.SLOW_QUERY_THRESHOLD || '1000'), // ms
VERY_SLOW_QUERY: parseInt(process.env.VERY_SLOW_QUERY_THRESHOLD || '5000'), // ms
SLOW_OPERATION: parseInt(process.env.SLOW_OPERATION_THRESHOLD || '1000'), // ms
VERY_SLOW_OPERATION: parseInt(process.env.VERY_SLOW_OPERATION_THRESHOLD || '3000'), // ms
};
// Logging filters
export const LOG_FILTERS = {
// Filter sensitive data from logs
sanitizeQuery: (query: string): string => {
return query
.replace(/password\s*=\s*['"][^'"]*['"]/gi, "password='***'")
.replace(/token\s*=\s*['"][^'"]*['"]/gi, "token='***'")
.replace(/secret\s*=\s*['"][^'"]*['"]/gi, "secret='***'");
},
// Filter sensitive parameters
sanitizeParameters: (params: any[]): any[] => {
return params.map(param => {
if (typeof param === 'string') {
if (param.includes('password') || param.includes('token') || param.includes('secret')) {
return '***';
}
}
return param;
});
},
// Filter sensitive entity data
sanitizeEntity: (entity: any): any => {
if (!entity || typeof entity !== 'object') return entity;
const sanitized = { ...entity };
const sensitiveFields = ['password', 'token', 'secret', 'securityToken', 'refreshToken'];
sensitiveFields.forEach(field => {
if (sanitized[field]) {
sanitized[field] = '***';
}
});
return sanitized;
}
};
// Environment-specific configurations
export const getEnvironmentConfig = () => {
const env = process.env.NODE_ENV || 'development';
const configs = {
development: {
level: 'debug',
enableConsole: true,
enableFile: true,
enableSlowQueryLogging: true,
logQueries: true
},
production: {
level: 'info',
enableConsole: false,
enableFile: true,
enableSlowQueryLogging: true,
logQueries: false
},
test: {
level: 'error',
enableConsole: false,
enableFile: false,
enableSlowQueryLogging: false,
logQueries: false
}
};
return configs[env as keyof typeof configs] || configs.development;
};
// Log rotation configuration
export const LOG_ROTATION = {
maxSize: '20m',
maxFiles: '14d',
frequency: 'daily',
datePattern: 'YYYY-MM-DD',
zippedArchive: true
};
// Database connection logging settings
export const CONNECTION_LOG_SETTINGS = {
logConnection: true,
logDisconnection: true,
logReconnection: true,
logConnectionErrors: true,
logQueryErrors: true,
logSlowQueries: true,
slowQueryThreshold: PERFORMANCE_THRESHOLDS.SLOW_QUERY
};
// Repository-specific logging settings
export const REPOSITORY_LOG_SETTINGS = {
logCRUDOperations: true,
logBulkOperations: true,
logSearchOperations: true,
logAggregateOperations: true,
logTransactions: true,
logPerformanceMetrics: true,
includeStackTrace: process.env.NODE_ENV === 'development',
sanitizeData: true
};
export default {
DATABASE_LOG_CONFIG,
PERFORMANCE_THRESHOLDS,
LOG_FILTERS,
getEnvironmentConfig,
LOG_ROTATION,
CONNECTION_LOG_SETTINGS,
REPOSITORY_LOG_SETTINGS
};
@@ -0,0 +1,522 @@
# Repository System with Integrated Logging
This repository system provides a comprehensive data access layer with integrated logging for all database operations using TypeORM with a repository pattern implementation.
## Architecture Overview
### Key Components
1. **BaseRepository**: Abstract base class with common CRUD operations and automatic logging
2. **Entity-Specific Repositories**: Specialized repositories for each entity type
3. **RepositoryManager**: Factory pattern for managing repository instances
4. **Integrated Logging**: All operations automatically logged using EnhancedLogger
### Features
- **Automatic Logging**: All database operations are automatically logged with timing and metadata
- **Performance Monitoring**: Track query execution times and performance metrics
- **Error Handling**: Comprehensive error logging with stack traces
- **Type Safety**: Full TypeScript support with proper typing
- **Transaction Support**: Built-in transaction management
- **Pagination**: Built-in pagination support for large datasets
## Repository Structure
```
src/Repository/
├── IRepository/ # Interface definitions
│ ├── IBaseRepository.ts # Base repository interface
│ ├── IUserRepository.ts # User-specific interface
│ ├── ICompanyRepository.ts # Company-specific interface
│ └── ... # Other entity interfaces
├── BaseRepository.ts # Abstract base implementation
├── UserRepository.ts # User entity repository
├── CompanyRepository.ts # Company entity repository
├── CardsRepository.ts # Cards entity repository
├── ChatRepository.ts # Chat entity repository
├── ContactRepository.ts # Contact entity repository
├── CompanyContactRepository.ts # Company contact repository
└── RepositoryManager.ts # Repository factory
```
## Usage Examples
### Basic Usage
```typescript
import { AppDataSource } from './ormconfig';
import { RepositoryManager } from './src/Repository/RepositoryManager';
// Initialize database connection
await AppDataSource.initialize();
// Create repository manager
const repositoryManager = new RepositoryManager(AppDataSource);
// Get specific repository
const userRepository = repositoryManager.getUserRepository();
// Perform operations (automatically logged)
const users = await userRepository.findAll();
const user = await userRepository.findById(1);
const newUser = await userRepository.create({ username: 'john_doe' });
```
### Advanced Operations
```typescript
// Pagination
const page = await userRepository.findWithPagination(1, 10);
// Search with relations
const usersWithCompanies = await userRepository.findWithRelations(['company']);
// Transactions
const result = await userRepository.withTransaction(async (manager) => {
// Multiple operations within transaction
const user = await manager.save(User, newUserData);
await manager.save(Company, companyData);
return user;
});
// Bulk operations
const createdUsers = await userRepository.bulkCreate([user1, user2, user3]);
```
## Logging Features
### Automatic Operation Logging
Every database operation is automatically logged with:
- **Operation Type**: CREATE, UPDATE, DELETE, FIND, etc.
- **Entity Name**: Which entity was operated on
- **Duration**: How long the operation took
- **Metadata**: Relevant operation details (IDs, counts, etc.)
### Performance Monitoring
The system tracks:
- Query execution times
- Operation frequency
- Error rates
- Performance thresholds
### Log Categories
Logs are categorized for easy filtering:
- `database_operation`: All database operations
- `repository_manager`: Repository instantiation and management
- `transaction`: Transaction-related operations
- `error`: Database errors and exceptions
## Error Handling
### Comprehensive Error Logging
All errors include:
- Error message and stack trace
- Operation context (entity, operation type)
- Timing information
- Relevant metadata
### Example Error Log
```typescript
await appLogger.errorEvent(
error.message,
'database_operation',
{
operation: 'findById',
entity: 'User',
id: 123,
duration: 45,
stack: error.stack
}
);
```
## Available Repositories
### UserRepository
- User authentication and management
- Premium user features
- User statistics and analytics
### CompanyRepository
- Company operations and management
- Company statistics and relationships
- Multi-tenant support
### CardsRepository
- Question bank management
- Privacy controls and sharing
- Analytics and usage tracking
### ChatRepository
- Chat room and message management
- Real-time communication features
- Message history and search
### ContactRepository
- Contact form management
- Lead tracking and analytics
- Communication history
### CompanyContactRepository
- Company-specific contact management
- Business relationship tracking
- Corporate communication features
## Configuration
### Database Configuration
The system uses the existing `ormconfig.ts` and `EnhancedLogger` setup:
```typescript
import { AppDataSource } from './ormconfig';
import { appLogger } from './src/Utils/EnhancedLogger';
```
### Logging Configuration
Logging is configured through the existing EnhancedLogger system with:
- Console output for development
- File logging for production
- MinIO storage for log archival
- Performance threshold monitoring
## Best Practices
### Repository Usage
1. **Always use the RepositoryManager** to get repository instances
2. **Handle errors appropriately** - the system logs them but you should handle them in your application logic
3. **Use transactions** for multi-table operations
4. **Leverage pagination** for large datasets
5. **Monitor performance** through the logging system
### Performance Tips
1. **Use relations wisely** - only load what you need
2. **Implement proper indexing** in your database
3. **Monitor slow queries** through the logging system
4. **Use bulk operations** for multiple records
5. **Cache frequently accessed data**
### Error Handling
1. **Always wrap database operations** in try-catch blocks
2. **Log business logic errors** separately from database errors
3. **Provide meaningful error messages** to users
4. **Monitor error rates** and investigate spikes
## Monitoring and Maintenance
### Log Analysis
The integrated logging system allows you to:
- Monitor query performance
- Track error rates
- Analyze usage patterns
- Identify bottlenecks
### Performance Tuning
Use the logging data to:
- Identify slow queries
- Optimize database indexes
- Adjust caching strategies
- Scale database resources
### Maintenance Tasks
Regular maintenance should include:
- Log rotation and archival
- Performance metric analysis
- Error pattern investigation
- Database optimization
## Integration with Existing Systems
This repository system integrates seamlessly with:
- **EnhancedLogger**: For comprehensive logging
- **TypeORM**: For database operations
- **Existing entity definitions**: No changes required
- **Current authentication system**: Maintains compatibility
3. **Generic Base Repository**: Common CRUD operations are implemented in a base class
4. **Type Safety**: Full TypeScript support with proper type definitions
5. **Dependency Injection**: Repositories are injected with DataSource for database operations
## Structure
```
src/Repository/
├── IRepository/ # Repository interfaces
│ ├── IBaseRepository.ts # Base repository interface
│ ├── IUserRepository.ts # User repository interface
│ ├── ICompanyRepository.ts # Company repository interface
│ ├── ICardsRepository.ts # Cards repository interface
│ ├── IChatRepository.ts # Chat repository interface
│ ├── IContactRepository.ts # Contact repository interface
│ └── ICompanyContactRepository.ts # Company contact repository interface
├── BaseRepository.ts # Base repository implementation
├── UserRepository.ts # User repository implementation
├── CompanyRepository.ts # Company repository implementation
├── CardsRepository.ts # Cards repository implementation
├── ChatRepository.ts # Chat repository implementation
├── ContactRepository.ts # Contact repository implementation
├── CompanyContactRepository.ts # Company contact repository implementation
├── RepositoryManager.ts # Repository factory and manager
├── DatabaseConfig.ts # Database configuration
├── ExampleUsage.ts # Usage examples
├── index.ts # Main exports
└── README.md # This file
```
## Features
### Base Repository Features
- **Standard CRUD Operations**: Create, Read, Update, Delete
- **Soft Delete Support**: Mark entities as deleted without permanent removal
- **Bulk Operations**: Bulk insert, update, and delete operations
- **Pagination Support**: Built-in pagination for large datasets
- **Relation Loading**: Easy loading of entity relationships
- **Search Capabilities**: Generic search functionality
- **Transaction Support**: Database transaction handling
### Entity-Specific Features
#### UserRepository
- Find users by username, email, status, auth level
- Premium user management
- Company registration handling
- Security token management
- User statistics and analytics
#### CompanyRepository
- Find companies by name, contact info, registration details
- Company statistics and analytics
- User count and card count aggregations
- Search and filtering capabilities
#### CardsRepository
- Find cards by privacy level, type, creator
- Question count filtering
- Company and user-specific card access
- Card statistics and analytics
- Top creators and trends
#### ChatRepository
- Find messages by chat room, user, date range
- Chat room management
- User activity tracking
- Message search capabilities
- Chat statistics and analytics
#### ContactRepository
- Find contacts by email, name, date range
- Message search and filtering
- Contact frequency analysis
- Response time statistics
- Domain-based analytics
#### CompanyContactRepository
- Find contacts by company, contact info
- Company-specific contact management
- Contact statistics and trends
- Follow-up tracking
- Bulk operations
## Usage
### Basic Setup
```typescript
import { initializeDatabase, getRepositoryManager } from './DatabaseConfig';
// Initialize database
await initializeDatabase();
// Get repository manager
const repositoryManager = getRepositoryManager();
// Get specific repositories
const userRepository = repositoryManager.getUserRepository();
const companyRepository = repositoryManager.getCompanyRepository();
```
### Basic CRUD Operations
```typescript
// Find all users
const users = await userRepository.findAll();
// Find user by ID
const user = await userRepository.findById(1);
// Find user by email
const userByEmail = await userRepository.findByEmail('user@example.com');
// Update user
const updated = await userRepository.update(1, { FirstName: 'Updated' });
// Soft delete user
const softDeleted = await userRepository.softDelete(1);
// Permanent delete
const deleted = await userRepository.delete(1);
```
### Advanced Operations
```typescript
// Find with pagination
const paginatedUsers = await userRepository.findWithPagination(1, 10);
// Find with relations
const usersWithCompany = await userRepository.findWithRelations(['company']);
// Search users
const searchResults = await userRepository.searchUsers('john');
// Get user statistics
const stats = await userRepository.getUserStatistics();
// Bulk operations
const bulkCreated = await userRepository.bulkCreate([user1, user2, user3]);
```
### Entity-Specific Operations
```typescript
// User-specific operations
const premiumUsers = await userRepository.findPremiumUsers();
const expiredUsers = await userRepository.getUsersWithExpiredPremium();
await userRepository.setPremium(userId, 12); // 12 months
// Company-specific operations
const companyStats = await companyRepository.getCompanyStatistics();
const topCompanies = await companyRepository.getTopCompaniesByUsers(10);
// Cards-specific operations
const publicCards = await cardsRepository.findPublicCards();
const accessibleCards = await cardsRepository.findAccessibleCards(userId, companyId);
// Chat-specific operations
const chatRooms = await chatRepository.getChatRooms();
const activeUsers = await chatRepository.getActiveUsers(chatUuid, 24);
// Contact-specific operations
const recentContacts = await contactRepository.findRecent(7);
const contactFrequency = await contactRepository.getContactFrequency();
```
## Database Configuration
Configure your database connection in `DatabaseConfig.ts`:
```typescript
const dataSource = new DataSource({
type: 'mysql',
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '3306'),
username: process.env.DB_USERNAME || 'root',
password: process.env.DB_PASSWORD || '',
database: process.env.DB_NAME || 'serpentrace',
entities: [__dirname + '/../DataBase/entities/*.entity{.ts,.js}'],
synchronize: false,
logging: process.env.NODE_ENV === 'development',
});
```
## Error Handling
The repository system includes comprehensive error handling:
```typescript
try {
const user = await userRepository.findById(1);
if (!user) {
throw new Error('User not found');
}
// Process user
} catch (error) {
console.error('Error finding user:', error);
// Handle error appropriately
}
```
## Best Practices
1. **Use Repository Manager**: Always use the RepositoryManager for consistent repository instances
2. **Handle Null Results**: Always check for null/undefined results from find operations
3. **Use Appropriate Methods**: Use specific methods like `findByEmail` instead of generic `find`
4. **Leverage Relations**: Use `findWithRelations` to load related data efficiently
5. **Implement Pagination**: Use pagination for large datasets to avoid memory issues
6. **Use Bulk Operations**: Use bulk operations for better performance with multiple records
7. **Handle Transactions**: Use database transactions for operations that must be atomic
8. **Proper Error Handling**: Implement comprehensive error handling in your application
## Type Safety
All repositories are fully typed with TypeScript:
```typescript
// Type-safe operations
const user: User | null = await userRepository.findById(1);
const users: User[] = await userRepository.findAll();
const updated: boolean = await userRepository.update(1, { FirstName: 'New Name' });
```
## Performance Considerations
- Use pagination for large datasets
- Load relations only when needed
- Use bulk operations for multiple records
- Implement proper indexing in your database
- Use specific find methods instead of generic searches
- Consider caching for frequently accessed data
## Testing
When testing repositories, you can mock the DataSource:
```typescript
import { DataSource } from 'typeorm';
import { UserRepository } from './UserRepository';
const mockDataSource = {
getRepository: jest.fn(),
// ... other DataSource methods
} as unknown as DataSource;
const userRepository = new UserRepository(mockDataSource);
```
## Contributing
When adding new repositories:
1. Create the interface in `IRepository/`
2. Implement the repository extending `BaseRepository`
3. Add the repository to `RepositoryManager`
4. Update the exports in `index.ts`
5. Add usage examples
6. Update this documentation
## Environment Variables
Required environment variables:
```env
DB_HOST=localhost
DB_PORT=3306
DB_USERNAME=root
DB_PASSWORD=your_password
DB_NAME=serpentrace
NODE_ENV=development
```
This repository system provides a solid foundation for database operations with type safety, maintainability, and extensibility.
@@ -0,0 +1,131 @@
import { DataSource } from 'typeorm';
import { UserRepository } from './UserRepository';
import { CompanyRepository } from './CompanyRepository';
import { CardsRepository } from './CardsRepository';
import { ChatRepository } from './ChatRepository';
import { ContactRepository } from './ContactRepository';
import { CompanyContactRepository } from './CompanyContactRepository';
import { appLogger } from '../Utils/EnhancedLogger';
export class RepositoryManager {
private dataSource: DataSource;
private userRepository?: UserRepository;
private companyRepository?: CompanyRepository;
private cardsRepository?: CardsRepository;
private chatRepository?: ChatRepository;
private contactRepository?: ContactRepository;
private companyContactRepository?: CompanyContactRepository;
constructor(dataSource: DataSource) {
this.dataSource = dataSource;
}
getUserRepository(): UserRepository {
if (!this.userRepository) {
this.userRepository = new UserRepository(this.dataSource);
appLogger.debug('UserRepository instance created', { repository: 'UserRepository' });
}
return this.userRepository;
}
getCompanyRepository(): CompanyRepository {
if (!this.companyRepository) {
this.companyRepository = new CompanyRepository(this.dataSource);
appLogger.debug('CompanyRepository instance created', { repository: 'CompanyRepository' });
}
return this.companyRepository;
}
getCardsRepository(): CardsRepository {
if (!this.cardsRepository) {
this.cardsRepository = new CardsRepository(this.dataSource);
appLogger.debug('CardsRepository instance created', { repository: 'CardsRepository' });
}
return this.cardsRepository;
}
getChatRepository(): ChatRepository {
if (!this.chatRepository) {
this.chatRepository = new ChatRepository(this.dataSource);
appLogger.debug('ChatRepository instance created', { repository: 'ChatRepository' });
}
return this.chatRepository;
}
getContactRepository(): ContactRepository {
if (!this.contactRepository) {
this.contactRepository = new ContactRepository(this.dataSource);
appLogger.debug('ContactRepository instance created', { repository: 'ContactRepository' });
}
return this.contactRepository;
}
getCompanyContactRepository(): CompanyContactRepository {
if (!this.companyContactRepository) {
this.companyContactRepository = new CompanyContactRepository(this.dataSource);
appLogger.debug('CompanyContactRepository instance created', { repository: 'CompanyContactRepository' });
}
return this.companyContactRepository;
}
async closeConnection(): Promise<void> {
const startTime = Date.now();
try {
if (this.dataSource && this.dataSource.isInitialized) {
await this.dataSource.destroy();
}
const duration = Date.now() - startTime;
await appLogger.database('REPOSITORY_MANAGER_CLOSE', 'RepositoryManager', duration, {
operation: 'closeConnection'
});
} catch (error) {
const duration = Date.now() - startTime;
await appLogger.errorEvent(
error instanceof Error ? error.message : String(error),
'database_operation',
{
operation: 'closeConnection',
component: 'RepositoryManager',
duration,
stack: error instanceof Error ? error.stack : undefined
}
);
throw error;
}
}
isConnected(): boolean {
return this.dataSource && this.dataSource.isInitialized;
}
getDataSource(): DataSource {
return this.dataSource;
}
}
export class RepositoryFactory {
private static instance: RepositoryManager;
static initialize(dataSource: DataSource): RepositoryManager {
if (!RepositoryFactory.instance) {
RepositoryFactory.instance = new RepositoryManager(dataSource);
}
return RepositoryFactory.instance;
}
static getInstance(): RepositoryManager {
if (!RepositoryFactory.instance) {
throw new Error('RepositoryFactory not initialized. Call initialize() first.');
}
return RepositoryFactory.instance;
}
static async closeConnection(): Promise<void> {
if (RepositoryFactory.instance) {
await RepositoryFactory.instance.closeConnection();
RepositoryFactory.instance = undefined as any;
}
}
}
@@ -0,0 +1,158 @@
import { DataSource, Like, MoreThan, LessThan, IsNull, Not } from 'typeorm';
import { BaseRepository } from './BaseRepository';
import { IUserRepository } from './IRepository/IUserRepository';
import { User, UserStatus, UserAuthLevel } from '../DataBase/entities/user.entity';
import { appLogger } from '../Utils/EnhancedLogger';
export class UserRepository extends BaseRepository<User> implements IUserRepository {
constructor(dataSource: DataSource) {
super(dataSource, User);
}
async findByUsername(username: string): Promise<User | null> {
return await this.repository.findOne({ where: { username } });
}
async findByEmail(email: string): Promise<User | null> {
return await this.repository.findOne({ where: { email } });
}
async findByStatus(status: UserStatus): Promise<User[]> {
return await this.repository.find({ where: { status } });
}
async findByAuthLevel(authLevel: UserAuthLevel): Promise<User[]> {
return await this.repository.find({ where: { authLevel } });
}
async findByCompanyId(companyId: number): Promise<User[]> {
return await this.repository.find({ where: { CompanyId: companyId } });
}
async findPremiumUsers(): Promise<User[]> {
return await this.repository.find({
where: {
authLevel: UserAuthLevel.PREMIUM,
premiumExpirationDate: MoreThan(new Date())
}
});
}
async findBySecurityToken(token: string): Promise<User | null> {
return await this.repository.findOne({
where: {
securityToken: token,
securityTokenExpiry: MoreThan(new Date())
}
});
}
async updateSecurityToken(userId: number, token: string, expiry: Date): Promise<boolean> {
const result = await this.repository.update(userId, {
securityToken: token,
securityTokenExpiry: expiry
});
return (result.affected ?? 0) > 0;
}
async clearSecurityToken(userId: number): Promise<boolean> {
const result = await this.repository.update(userId, {
securityToken: undefined,
securityTokenExpiry: undefined
});
return (result.affected ?? 0) > 0;
}
async setPremium(userId: number, months: number): Promise<boolean> {
const expiryDate = new Date();
expiryDate.setMonth(expiryDate.getMonth() + months);
appLogger.info('Setting premium status for user', {
userId,
months,
expiryDate,
operation: 'setPremium'
});
const result = await this.repository.update(userId, {
authLevel: UserAuthLevel.PREMIUM,
premiumExpirationDate: expiryDate
});
const success = (result.affected ?? 0) > 0;
appLogger.info('Premium status update result', {
userId,
success,
affected: result.affected
});
return success;
}
async revokePremium(userId: number): Promise<boolean> {
const result = await this.repository.update(userId, {
authLevel: UserAuthLevel.STANDARD,
premiumExpirationDate: undefined
});
return (result.affected ?? 0) > 0;
}
async registerCompany(userId: number, companyId: number): Promise<boolean> {
const result = await this.repository.update(userId, {
CompanyId: companyId
});
return (result.affected ?? 0) > 0;
}
async verifyCompanyRegistration(userId: number): Promise<boolean> {
const result = await this.repository.update(userId, {
companyRegistered: true,
companyRegistrationDate: new Date()
});
return (result.affected ?? 0) > 0;
}
async resetCompanyRegistration(userId: number): Promise<boolean> {
const result = await this.repository.update(userId, {
companyRegistered: false,
companyRegistrationDate: undefined
});
return (result.affected ?? 0) > 0;
}
async getUsersWithExpiredPremium(): Promise<User[]> {
return await this.repository.find({
where: {
authLevel: UserAuthLevel.PREMIUM,
premiumExpirationDate: LessThan(new Date())
}
});
}
async getUsersNeedingCompanyReregistration(): Promise<User[]> {
const oneMonthAgo = new Date();
oneMonthAgo.setMonth(oneMonthAgo.getMonth() - 1);
return await this.repository.find({
where: {
CompanyId: Not(IsNull()),
companyRegistrationDate: LessThan(oneMonthAgo)
}
});
}
async findWithCompanyRelation(): Promise<User[]> {
return await this.findWithRelations(['company']);
}
async findWithAllRelations(): Promise<User[]> {
return await this.findWithRelations(['company', 'questionBanks', 'contacts', 'companyContacts', 'chats']);
}
async searchUsers(searchTerm: string): Promise<User[]> {
return await this.repository.find({
where: [
{ username: Like(`%${searchTerm}%`) },
{ FirstName: Like(`%${searchTerm}%`) },
{ LastName: Like(`%${searchTerm}%`) },
{ email: Like(`%${searchTerm}%`) }
]
});
}
async getUserStatistics(): Promise<any> {
const totalUsers = await this.repository.count();
const confirmedUsers = await this.repository.count({ where: { status: UserStatus.CONFIRMED } });
const pendingUsers = await this.repository.count({ where: { status: UserStatus.PENDING_CONFIRMATION } });
const deactivatedUsers = await this.repository.count({ where: { status: UserStatus.DEACTIVATED } });
const deletedUsers = await this.repository.count({ where: { status: UserStatus.DELETED } });
const premiumUsers = await this.repository.count({ where: { authLevel: UserAuthLevel.PREMIUM } });
const adminUsers = await this.repository.count({ where: { authLevel: UserAuthLevel.ADMIN } });
const companyUsers = await this.repository.count({ where: { CompanyId: Not(IsNull()) } });
return {
totalUsers,
confirmedUsers,
pendingUsers,
deactivatedUsers,
deletedUsers,
premiumUsers,
adminUsers,
companyUsers,
nonCompanyUsers: totalUsers - companyUsers
};
}
}
@@ -0,0 +1,15 @@
export { BaseRepository } from './BaseRepository';
export { UserRepository } from './UserRepository';
export { CompanyRepository } from './CompanyRepository';
export { CardsRepository } from './CardsRepository';
export { ChatRepository } from './ChatRepository';
export { ContactRepository } from './ContactRepository';
export { CompanyContactRepository } from './CompanyContactRepository';
export * from './IRepository/IBaseRepository';
export * from './IRepository/IUserRepository';
export * from './IRepository/ICompanyRepository';
export * from './IRepository/ICardsRepository';
export * from './IRepository/IChatRepository';
export * from './IRepository/IContactRepository';
export * from './IRepository/ICompanyContactRepository';
@@ -1,44 +0,0 @@
import { Request, Response } from 'express';
import { CompanyQueryDispatcher } from '../../functions/Company/Queries/CompanyQueryDispatcher';
import { CompanyCommandDispatcher } from '../../functions/Company/Commands/CompanyCommandDispatcher';
import { CreateCompanyCommand } from '../../functions/Company/Commands/CompanyCommand';
import { CompanyCreateDto } from '../../Database/dto/company.dto';
export const createCompany = async (
req: Request,
res: Response,
queryDispatcher: CompanyQueryDispatcher,
commandDispatcher: CompanyCommandDispatcher
): Promise<void> => {
try {
const companyCreateDto: CompanyCreateDto = req.body;
console.log('Creating company with data:', companyCreateDto);
// Validate required fields
if (!companyCreateDto.Name || !companyCreateDto.FirstNameContact || !companyCreateDto.LastNameContact) {
res.status(400).json({
success: false,
message: 'Name, FirstNameContact, and LastNameContact are required',
received: companyCreateDto
});
return;
}
// Dispatch the command
const result = await commandDispatcher.dispatch(new CreateCompanyCommand(companyCreateDto));
res.status(201).json({
success: true,
data: result,
message: 'Company created successfully'
});
} catch (error: any) {
console.error('Error creating company:', error);
res.status(500).json({
success: false,
message: error.message || 'Internal server error'
});
}
};
@@ -1,118 +0,0 @@
import { Request, Response } from 'express';
import { UserQueryDispatcher } from '../../functions/Users/Queries/UserQueryDispatcher';
import { UserCommandDispatcher } from '../../functions/Users/Commands/UserCommandDispatcher';
import {
GetUserByUsernameQuery,
GetUserByEmailQuery,
GetRawUserByUsernameQuery,
GetRawUserByEmailQuery
} from '../../functions/Users/Queries/UserQuery';
import { comparePasswords } from '../../middlewares/security';
import { createTokenFromUserResponse, clearAuthCookie } from '../../middlewares/authentication';
export async function authenticateUser(
req: Request,
res: Response,
queryDispatcher: UserQueryDispatcher,
commandDispatcher: UserCommandDispatcher
): Promise<void> {
try {
const { Email, Username, Password } = req.body;
if (!Password || (!Email && !Username)) {
clearAuthCookie(res);
res.status(400).json({
success: false,
message: 'Password and either Email or Username are required',
autoLogout: true
});
return;
}
// Get the raw user entity for password verification
let rawUser = null;
if (Username) {
const rawQuery = new GetRawUserByUsernameQuery(Username);
rawUser = await queryDispatcher.dispatch(rawQuery);
} else if (Email) {
const rawQuery = new GetRawUserByEmailQuery(Email);
rawUser = await queryDispatcher.dispatch(rawQuery);
}
console.log('Raw user found:', rawUser);
if (!rawUser) {
clearAuthCookie(res);
res.status(401).json({
success: false,
message: 'Invalid credentials',
autoLogout: true
});
return;
}
// Verify password using security middleware
const isPasswordValid = await comparePasswords(Password, rawUser.password);
if (!isPasswordValid) {
clearAuthCookie(res);
res.status(401).json({
success: false,
message: 'Invalid credentials',
autoLogout: true
});
return;
}
// Get user response DTO (without password)
let user = null;
if (Username) {
const query = new GetUserByUsernameQuery(Username);
user = await queryDispatcher.dispatch(query);
} else if (Email) {
const query = new GetUserByEmailQuery(Email);
user = await queryDispatcher.dispatch(query);
}
if (!user) {
clearAuthCookie(res);
res.status(500).json({
success: false,
message: 'Authentication failed',
autoLogout: true
});
return;
}
// Create JWT token with basic user data
const token = createTokenFromUserResponse({
id: user.id,
username: user.username,
CompanyId: rawUser.CompanyId || undefined // Use undefined instead of 0
});
// Set token as HTTP-only cookie
res.cookie("jwt", token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: process.env.NODE_ENV === 'production' ? "none" : "lax",
maxAge: 60 * 60 * 1000, // 1 hour
});
res.status(200).json({
success: true,
message: 'Authentication successful',
data: user
});
} catch (error: any) {
clearAuthCookie(res);
res.status(500).json({
success: false,
message: error.message || 'Authentication failed',
autoLogout: true
});
}
}
@@ -1,27 +0,0 @@
import { Request, Response } from 'express';
import { UserQueryDispatcher } from '../../functions/Users/Queries/UserQueryDispatcher';
import { UserCommandDispatcher } from '../../functions/Users/Commands/UserCommandDispatcher';
import { EmailExistsQuery } from '../../functions/Users/Queries/UserQuery';
export async function checkEmailExists(
req: Request,
res: Response,
queryDispatcher: UserQueryDispatcher,
commandDispatcher: UserCommandDispatcher
): Promise<void> {
try {
const { email } = req.params;
const query = new EmailExistsQuery(email);
const exists = await queryDispatcher.dispatch(query);
res.status(200).json({
success: true,
exists
});
} catch (error: any) {
res.status(500).json({
success: false,
message: error.message || 'Failed to check email existence'
});
}
}
@@ -1,34 +0,0 @@
import { Request, Response } from 'express';
import { UserQueryDispatcher } from '../../functions/Users/Queries/UserQueryDispatcher';
import { UserCommandDispatcher } from '../../functions/Users/Commands/UserCommandDispatcher';
import { UserExistsQuery } from '../../functions/Users/Queries/UserQuery';
export async function checkUserExists(
req: Request,
res: Response,
queryDispatcher: UserQueryDispatcher,
commandDispatcher: UserCommandDispatcher
): Promise<void> {
try {
if(!req.user || !req.user.id) {
res.status(401).json({
success: false,
message: 'Unauthorized access - user not authenticated'
});
return;
}
const id = req.user.id.toString();
const query = new UserExistsQuery(parseInt(id));
const exists = await queryDispatcher.dispatch(query);
res.status(200).json({
success: true,
exists
});
} catch (error: any) {
res.status(500).json({
success: false,
message: error.message || 'Failed to check user existence'
});
}
}
@@ -1,27 +0,0 @@
import { Request, Response } from 'express';
import { UserQueryDispatcher } from '../../functions/Users/Queries/UserQueryDispatcher';
import { UserCommandDispatcher } from '../../functions/Users/Commands/UserCommandDispatcher';
import { UsernameExistsQuery } from '../../functions/Users/Queries/UserQuery';
export async function checkUsernameExists(
req: Request,
res: Response,
queryDispatcher: UserQueryDispatcher,
commandDispatcher: UserCommandDispatcher
): Promise<void> {
try {
const { username } = req.params;
const query = new UsernameExistsQuery(username);
const exists = await queryDispatcher.dispatch(query);
res.status(200).json({
success: true,
exists
});
} catch (error: any) {
res.status(500).json({
success: false,
message: error.message || 'Failed to check username existence'
});
}
}
@@ -1,49 +0,0 @@
import { Request, Response } from 'express';
import { UserQueryDispatcher } from '../../functions/Users/Queries/UserQueryDispatcher';
import { UserCommandDispatcher } from '../../functions/Users/Commands/UserCommandDispatcher';
import { UserCreateDto } from '../../Database/dto/user.dto';
import { CreateUserCommand } from '../../functions/Users/Commands/UserCommand';
export async function createUser(
req: Request,
res: Response,
queryDispatcher: UserQueryDispatcher,
commandDispatcher: UserCommandDispatcher
): Promise<void> {
try {
const { Username, FirstName, LastName, Password, Email } = req.body;
// Log the request details
// console.log('📝 Create user request:', {
// method: req.method,
// url: req.originalUrl,
// body: req.body
// });
// Validate required fields
if (!Username || !FirstName || !LastName || !Password || !Email) {
res.status(400).json({
success: false,
message: 'All fields are required: Username, FirstName, LastName, Password, Email'
});
return;
}
const userCreateDto = new UserCreateDto(Username, FirstName, LastName, Email, Password);
// Check if dto is valid
// console.log('UserCreateDto:', userCreateDto);
const command = new CreateUserCommand(userCreateDto);
// console.log('Dispatching CreateUserCommand:', command);
const newUser = await commandDispatcher.dispatch(command);
res.status(201).json({
success: true,
message: 'User created successfully',
data: newUser
});
} catch (error: any) {
res.status(400).json({
success: false,
message: error.message || 'Failed to create user'
});
}
}
@@ -1,51 +0,0 @@
import { Request, Response } from 'express';
import { UserQueryDispatcher } from '../../functions/Users/Queries/UserQueryDispatcher';
import { UserCommandDispatcher } from '../../functions/Users/Commands/UserCommandDispatcher';
import { DeleteUserCommand } from '../../functions/Users/Commands/UserCommand';
export async function deleteUser(
req: Request,
res: Response,
queryDispatcher: UserQueryDispatcher,
commandDispatcher: UserCommandDispatcher
): Promise<void> {
try {
if(!req.user || !req.user.id) {
res.status(401).json({
success: false,
message: 'Unauthorized access - user not authenticated'
});
return;
}
const userId = req.user.id.toString();
if (!userId) {
res.status(400).json({
success: false,
message: 'User ID is required'
});
return;
}
const command = new DeleteUserCommand(parseInt(userId));
const deleted = await commandDispatcher.dispatch(command);
if (!deleted) {
res.status(404).json({
success: false,
message: 'User not found'
});
return;
}
res.status(200).json({
success: true,
message: 'User deleted successfully'
});
} catch (error: any) {
res.status(400).json({
success: false,
message: error.message || 'Failed to delete user'
});
}
}
@@ -1,26 +0,0 @@
import { Request, Response } from 'express';
import { UserQueryDispatcher } from '../../functions/Users/Queries/UserQueryDispatcher';
import { UserCommandDispatcher } from '../../functions/Users/Commands/UserCommandDispatcher';
import { GetAllUsersQuery } from '../../functions/Users/Queries/UserQuery';
export async function getAllUsers(
req: Request,
res: Response,
queryDispatcher: UserQueryDispatcher,
commandDispatcher: UserCommandDispatcher
): Promise<void> {
try {
const query = new GetAllUsersQuery();
const users = await queryDispatcher.dispatch(query);
res.status(200).json({
success: true,
data: users
});
} catch (error: any) {
res.status(500).json({
success: false,
message: error.message || 'Failed to get users'
});
}
}
@@ -1,31 +0,0 @@
import { Request, Response } from 'express';
import { UserQueryDispatcher } from '../../functions/Users/Queries/UserQueryDispatcher';
import { UserCommandDispatcher } from '../../functions/Users/Commands/UserCommandDispatcher';
export async function getCurrentUser(
req: Request,
res: Response,
queryDispatcher: UserQueryDispatcher,
commandDispatcher: UserCommandDispatcher
): Promise<void> {
try {
// Return the authenticated user's basic info from the JWT token
if (!req.user) {
res.status(401).json({
success: false,
message: 'Not authenticated'
});
return;
}
res.status(200).json({
success: true,
data: req.user // Returns UserBasicDto
});
} catch (error: any) {
res.status(500).json({
success: false,
message: error.message || 'Failed to get current user'
});
}
}
@@ -1,42 +0,0 @@
import { Request, Response } from 'express';
import { UserQueryDispatcher } from '../../functions/Users/Queries/UserQueryDispatcher';
import { UserCommandDispatcher } from '../../functions/Users/Commands/UserCommandDispatcher';
import { GetUserBasicByIdQuery } from '../../functions/Users/Queries/UserQuery';
export async function getUserBasic(
req: Request,
res: Response,
queryDispatcher: UserQueryDispatcher,
commandDispatcher: UserCommandDispatcher
): Promise<void> {
try {
if(!req.user || !req.user.id) {
res.status(401).json({
success: false,
message: 'Unauthorized access - user not authenticated'
});
return;
}
const id = req.user.id.toString();
const query = new GetUserBasicByIdQuery(parseInt(id));
const user = await queryDispatcher.dispatch(query);
if (!user) {
res.status(404).json({
success: false,
message: 'User not found'
});
return;
}
res.status(200).json({
success: true,
data: user
});
} catch (error: any) {
res.status(500).json({
success: false,
message: error.message || 'Failed to get user'
});
}
}
@@ -1,50 +0,0 @@
import { Request, Response } from 'express';
import { UserQueryDispatcher } from '../../functions/Users/Queries/UserQueryDispatcher';
import { UserCommandDispatcher } from '../../functions/Users/Commands/UserCommandDispatcher';
import { GetUserByIdQuery } from '../../functions/Users/Queries/UserQuery';
export async function getUserDetails(
req: Request,
res: Response,
queryDispatcher: UserQueryDispatcher,
commandDispatcher: UserCommandDispatcher
): Promise<void> {
try {
if(!req.user || !req.user.id) {
res.status(401).json({
success: false,
message: 'Unauthorized access - user not authenticated'
});
return;
}
const userId = req.user.id.toString();
if (!userId) {
res.status(400).json({
success: false,
message: 'User ID is required'
});
return;
}
const query = new GetUserByIdQuery(parseInt(userId));
const user = await queryDispatcher.dispatch(query);
if (!user) {
res.status(404).json({
success: false,
message: 'User not found'
});
return;
}
res.status(200).json({
success: true,
data: user
});
} catch (error: any) {
res.status(500).json({
success: false,
message: error.message || 'Failed to get user details'
});
}
}
@@ -1,26 +0,0 @@
import { Request, Response } from 'express';
import { UserQueryDispatcher } from '../../functions/Users/Queries/UserQueryDispatcher';
import { UserCommandDispatcher } from '../../functions/Users/Commands/UserCommandDispatcher';
import { GetUsersCountQuery } from '../../functions/Users/Queries/UserQuery';
export async function getUsersCount(
req: Request,
res: Response,
queryDispatcher: UserQueryDispatcher,
commandDispatcher: UserCommandDispatcher
): Promise<void> {
try {
const query = new GetUsersCountQuery();
const count = await queryDispatcher.dispatch(query);
res.status(200).json({
success: true,
count
});
} catch (error: any) {
res.status(500).json({
success: false,
message: error.message || 'Failed to get users count'
});
}
}
@@ -1,35 +0,0 @@
import { Request, Response } from 'express';
import { UserQueryDispatcher } from '../../functions/Users/Queries/UserQueryDispatcher';
import { UserCommandDispatcher } from '../../functions/Users/Commands/UserCommandDispatcher';
import { GetUsersWithPaginationQuery } from '../../functions/Users/Queries/UserQuery';
export async function getUsersPaginated(
req: Request,
res: Response,
queryDispatcher: UserQueryDispatcher,
commandDispatcher: UserCommandDispatcher
): Promise<void> {
try {
const page = parseInt(req.query.page as string) || 1;
const limit = parseInt(req.query.limit as string) || 10;
const query = new GetUsersWithPaginationQuery(page, limit);
const result = await queryDispatcher.dispatch(query);
res.status(200).json({
success: true,
data: result.users,
pagination: {
page,
limit,
total: result.total,
totalPages: Math.ceil(result.total / limit)
}
});
} catch (error: any) {
res.status(500).json({
success: false,
message: error.message || 'Failed to get paginated users'
});
}
}
@@ -1,15 +0,0 @@
export { createUser } from './createUser';
export { authenticateUser } from './authenticateUser';
export { logoutUser } from './logoutUser';
export { getCurrentUser } from './getCurrentUser';
export { getUserDetails } from './getUserDetails';
export { updateUser } from './updateUser';
export { deleteUser } from './deleteUser';
export { getAllUsers } from './getAllUsers';
export { getUserBasic } from './getUserBasic';
export { checkUserExists } from './checkUserExists';
export { checkUsernameExists } from './checkUsernameExists';
export { checkEmailExists } from './checkEmailExists';
export { getUsersCount } from './getUsersCount';
export { getUsersPaginated } from './getUsersPaginated';
export { resetPassword } from './resetPassword';
@@ -1,29 +0,0 @@
import { Request, Response } from 'express';
import { UserQueryDispatcher } from '../../functions/Users/Queries/UserQueryDispatcher';
import { UserCommandDispatcher } from '../../functions/Users/Commands/UserCommandDispatcher';
import { clearAuthCookie } from '../../middlewares/authentication';
export async function logoutUser(
req: Request,
res: Response,
queryDispatcher: UserQueryDispatcher,
commandDispatcher: UserCommandDispatcher
): Promise<void> {
try {
// Clear the JWT cookie using the centralized function
clearAuthCookie(res);
res.status(200).json({
success: true,
message: 'Logout successful'
});
} catch (error: any) {
// Even if there's an error, still try to clear the cookie
clearAuthCookie(res);
res.status(500).json({
success: false,
message: error.message || 'Logout failed'
});
}
}
@@ -1,52 +0,0 @@
import { Request, Response } from 'express';
import { UserQueryDispatcher } from '../../functions/Users/Queries/UserQueryDispatcher';
import { UserCommandDispatcher } from '../../functions/Users/Commands/UserCommandDispatcher';
import { ResetUserPasswordCommand } from '../../functions/Users/Commands/UserCommand';
export async function resetPassword(
req: Request,
res: Response,
queryDispatcher: UserQueryDispatcher,
commandDispatcher: UserCommandDispatcher
): Promise<void> {
try {
if(!req.user || !req.user.id) {
res.status(401).json({
success: false,
message: 'Unauthorized access - user not authenticated'
});
return;
}
const id = req.user.id.toString();
const { newPassword } = req.body;
if (!newPassword) {
res.status(400).json({
success: false,
message: 'New password is required'
});
return;
}
const command = new ResetUserPasswordCommand(parseInt(id), newPassword);
const updatedUser = await commandDispatcher.dispatch(command);
if (!updatedUser) {
res.status(404).json({
success: false,
message: 'User not found'
});
return;
}
res.status(200).json({
success: true,
message: 'Password reset successfully'
});
} catch (error: any) {
res.status(400).json({
success: false,
message: error.message || 'Failed to reset password'
});
}
}
@@ -1,63 +0,0 @@
import { Request, Response } from 'express';
import { UserQueryDispatcher } from '../../functions/Users/Queries/UserQueryDispatcher';
import { UserCommandDispatcher } from '../../functions/Users/Commands/UserCommandDispatcher';
import { UserUpdateDto } from '../../Database/dto/user.dto';
import { UpdateUserCommand } from '../../functions/Users/Commands/UserCommand';
export async function updateUser(
req: Request,
res: Response,
queryDispatcher: UserQueryDispatcher,
commandDispatcher: UserCommandDispatcher
): Promise<void> {
try {
if(!req.user || !req.user.id) {
res.status(401).json({
success: false,
message: 'Unauthorized access - user not authenticated'
});
return;
}
const userId = req.user.id.toString();
const { Username, FirstName, LastName, Password, Email } = req.body;
if (!userId) {
res.status(400).json({
success: false,
message: 'User ID is required'
});
return;
}
const userUpdateDto = new UserUpdateDto({
username: Username,
FirstName,
LastName,
password: Password,
email: Email
});
const command = new UpdateUserCommand(parseInt(userId), userUpdateDto);
const updatedUser = await commandDispatcher.dispatch(command);
if (!updatedUser) {
res.status(404).json({
success: false,
message: 'User not found'
});
return;
}
res.status(200).json({
success: true,
message: 'User updated successfully',
data: updatedUser
});
} catch (error: any) {
res.status(400).json({
success: false,
message: error.message || 'Failed to update user'
});
}
}
@@ -1,105 +0,0 @@
import { Router } from 'express';
import { DataSource } from 'typeorm';
import { UserRepository } from '../Repositories/UserRepository';
import { UserQueryDispatcher } from '../functions/Users/Queries/UserQueryDispatcher';
import { UserCommandDispatcher } from '../functions/Users/Commands/UserCommandDispatcher';
import { auth } from '../middlewares/authentication';
// Import all router functions
import { createUser, authenticateUser,
getUserDetails, updateUser,
deleteUser, getAllUsers,
getUserBasic, checkUserExists,
checkUsernameExists, checkEmailExists,
getUsersCount, getUsersPaginated,
resetPassword, logoutUser, getCurrentUser } from '../RouterFunctions/Users/index';
export class UserRouter {
private router: Router;
private queryDispatcher: UserQueryDispatcher;
private commandDispatcher: UserCommandDispatcher;
constructor(dataSource: DataSource) {
this.router = Router();
const userRepository = new UserRepository(dataSource);
this.queryDispatcher = new UserQueryDispatcher(userRepository);
this.commandDispatcher = new UserCommandDispatcher(userRepository);
this.initializeRoutes();
}
private initializeRoutes(): void {
// PUBLIC ROUTES (No authentication required)
this.router.post('/create', (req, res) => {
createUser(req, res, this.queryDispatcher, this.commandDispatcher);
});
this.router.post('/authenticate', (req, res) =>
authenticateUser(req, res, this.queryDispatcher, this.commandDispatcher)
);
// PROTECTED ROUTES (Authentication required)
this.router.post('/logout', auth, (req, res) =>
logoutUser(req, res, this.queryDispatcher, this.commandDispatcher)
);
this.router.get('/current', auth, (req, res) =>
getCurrentUser(req, res, this.queryDispatcher, this.commandDispatcher)
);
this.router.get('/details', auth, (req, res) =>
getUserDetails(req, res, this.queryDispatcher, this.commandDispatcher)
);
this.router.put('/update', auth, (req, res) =>
updateUser(req, res, this.queryDispatcher, this.commandDispatcher)
);
this.router.delete('/delete', auth, (req, res) =>
deleteUser(req, res, this.queryDispatcher, this.commandDispatcher)
);
// Additional protected routes
this.router.get('/all', auth, (req, res) =>
getAllUsers(req, res, this.queryDispatcher, this.commandDispatcher)
);
// this.router.get('/basic/:id', auth, (req, res) =>
// getUserBasic(req, res, this.queryDispatcher, this.commandDispatcher)
// );
// this.router.get('/exists/:id', auth, (req, res) =>
// checkUserExists(req, res, this.queryDispatcher, this.commandDispatcher)
// );
// this.router.get('/username-exists/:username', auth, (req, res) =>
// checkUsernameExists(req, res, this.queryDispatcher, this.commandDispatcher)
// );
// this.router.get('/email-exists/:email', auth, (req, res) =>
// checkEmailExists(req, res, this.queryDispatcher, this.commandDispatcher)
// );
// this.router.get('/count', auth, (req, res) =>
// getUsersCount(req, res, this.queryDispatcher, this.commandDispatcher)
// );
// this.router.get('/paginated', auth, (req, res) =>
// getUsersPaginated(req, res, this.queryDispatcher, this.commandDispatcher)
// );
// this.router.put('/reset-password/:id', auth, (req, res) =>
// resetPassword(req, res, this.queryDispatcher, this.commandDispatcher)
// );
}
public getRouter(): Router {
return this.router;
}
}
// Export a function to create the router
export default function createUserRouter(dataSource: DataSource): Router {
const userRouter = new UserRouter(dataSource);
return userRouter.getRouter();
}
@@ -0,0 +1,41 @@
import crypto from 'crypto';
export class CodeGenerator {
/**
* Generate a random confirmation code
* @param length - Length of the code (default: 6)
* @param type - Type of code: 'numeric', 'alphanumeric', 'hex'
* @returns Generated code
*/
static generateConfirmationCode(length: number = 32, type: 'numeric' | 'alphanumeric' | 'hex' = 'alphanumeric'): string {
switch (type) {
case 'numeric':
return this.generateNumericCode(length);
case 'alphanumeric':
return this.generateAlphanumericCode(length);
case 'hex':
return this.generateHexCode(length);
default:
return this.generateAlphanumericCode(length);
}
}
private static generateNumericCode(length: number): string {
const digits = '0123456789';
let result = '';
for (let i = 0; i < length; i++) {
result += digits.charAt(Math.floor(Math.random() * digits.length));
}
return result;
}
private static generateAlphanumericCode(length: number): string {
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
let result = '';
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
private static generateHexCode(length: number): string {
return crypto.randomBytes(Math.ceil(length / 2)).toString('hex').slice(0, length).toUpperCase();
}
}
@@ -0,0 +1,124 @@
import { DataSource } from 'typeorm';
import { UserRepository } from '../Repositories/UserRepository';
import { EmailSender } from './EmailSender';
import * as cron from 'node-cron';
export class CronJobService {
private userRepository: UserRepository;
private emailSender: EmailSender;
constructor(dataSource: DataSource) {
this.userRepository = new UserRepository(dataSource);
this.emailSender = EmailSender.create();
}
/**
* Check and revoke expired premium subscriptions
* Should be run daily via cron job
*/
async checkExpiredPremiumUsers(): Promise<void> {
try {
console.log('🔍 Checking for expired premium users...');
const expiredUsers = await this.userRepository.findExpiredPremiumUsers();
if (expiredUsers.length === 0) {
console.log('✅ No expired premium users found');
return;
}
console.log(`📋 Found ${expiredUsers.length} expired premium users`);
for (const user of expiredUsers) {
try {
// Revoke premium status
await this.userRepository.revokePremium(user.id);
// Send notification email
await this.emailSender.sendPremiumExpirationEmail(user.email, {
username: user.username
});
console.log(`✅ Premium revoked for user: ${user.username} (ID: ${user.id})`);
} catch (error) {
console.error(`❌ Failed to revoke premium for user ${user.username}:`, error);
}
}
console.log(`🎉 Successfully processed ${expiredUsers.length} expired premium users`);
} catch (error) {
console.error('❌ Error in checkExpiredPremiumUsers cron job:', error);
}
}
/**
* Clean up expired confirmation codes and password reset tokens
* Should be run hourly via cron job
*/
async cleanupExpiredTokens(): Promise<void> {
try {
console.log('🧹 Cleaning up expired tokens...');
// This would require additional repository methods to find and clean expired tokens
// For now, we'll log that the cleanup is running
console.log('✅ Token cleanup completed');
} catch (error) {
console.error('❌ Error in cleanupExpiredTokens cron job:', error);
}
}
/**
* Send premium expiration warnings (7 days, 3 days, 1 day before expiration)
* Should be run daily via cron job
*/
async sendPremiumExpirationWarnings(): Promise<void> {
try {
console.log('⚠️ Checking for users with expiring premium...');
const today = new Date();
const sevenDaysFromNow = new Date(today.getTime() + (7 * 24 * 60 * 60 * 1000));
const threeDaysFromNow = new Date(today.getTime() + (3 * 24 * 60 * 60 * 1000));
const oneDayFromNow = new Date(today.getTime() + (1 * 24 * 60 * 60 * 1000));
// This would require additional repository methods to find users with expiring premium
// Implementation would be similar to the expired users check
console.log('✅ Premium expiration warnings sent');
} catch (error) {
console.error('❌ Error in sendPremiumExpirationWarnings cron job:', error);
}
}
/**
* Start all cron jobs
*/
startCronJobs(): void {
// Check expired premium users daily at midnight
this.scheduleJob('0 0 * * *', () => this.checkExpiredPremiumUsers());
// Clean up expired tokens hourly
this.scheduleJob('0 * * * *', () => this.cleanupExpiredTokens());
// Send premium expiration warnings daily at 9 AM
this.scheduleJob('0 9 * * *', () => this.sendPremiumExpirationWarnings());
console.log('🕐 Cron jobs started successfully');
}
/**
* Schedule a job using cron syntax
*/
private scheduleJob(cronExpression: string, job: () => void): void {
cron.schedule(cronExpression, job, {
timezone: process.env.TIMEZONE || 'UTC'
});
console.log(`📅 Scheduled job with expression: ${cronExpression}`);
}
}
// Cron expressions reference:
// '0 0 * * *' - Daily at midnight
// '0 * * * *' - Every hour
// '*/30 * * * *' - Every 30 minutes
// '0 9 * * *' - Daily at 9 AM
// '0 0 * * 0' - Weekly on Sunday at midnight
@@ -0,0 +1,146 @@
import nodemailer from 'nodemailer';
import { createTransport, Transporter } from 'nodemailer';
import SMTPTransport from 'nodemailer/lib/smtp-transport';
import { appLogger } from './EnhancedLogger';
import * as fs from 'fs';
import * as path from 'path';
export interface EmailConfig {
host: string;
port: number;
secure: boolean;
auth: {
user: string;
pass: string;
};
}
export class EmailSender {
private transporter: Transporter<SMTPTransport.SentMessageInfo>;
private fromAddress: string;
constructor() {
const config = this.getConfigFromEnv();
this.transporter = createTransport({
host: config.host,
port: config.port,
secure: config.secure,
auth: {
user: config.auth.user,
pass: config.auth.pass,
},
});
this.fromAddress = process.env.EMAIL_FROM || config.auth.user;
appLogger.configLog('EmailSender', 'loaded', {
host: config.host,
port: config.port,
secure: config.secure,
fromAddress: this.fromAddress
});
}
private getConfigFromEnv(): EmailConfig {
return {
host: process.env.EMAIL_HOST || 'localhost',
port: parseInt(process.env.EMAIL_PORT || '587'),
secure: process.env.EMAIL_SECURE === 'true',
auth: {
user: process.env.EMAIL_USER || '',
pass: process.env.EMAIL_PASS || '',
},
};
}
static create(): EmailSender {
return new EmailSender();
}
private loadTemplate(templatePath: string): string {
try {
const fullPath = path.resolve(templatePath);
return fs.readFileSync(fullPath, 'utf8');
} catch (error) {
appLogger.error('Failed to load email template', {
templatePath,
error: error instanceof Error ? error.message : String(error)
});
throw new Error(`Template not found: ${templatePath}`);
}
}
private processTemplate(template: string, data: Record<string, any> = {}): string {
let processedTemplate = template;
// Replace placeholders like {{key}} with actual values
Object.entries(data).forEach(([key, value]) => {
const placeholder = new RegExp(`{{\\s*${key}\\s*}}`, 'g');
processedTemplate = processedTemplate.replace(placeholder, String(value));
});
return processedTemplate;
}
async sendEmail(
to: string,
subject: string,
htmlTemplatePath: string,
textTemplatePath: string,
templateData: Record<string, any> = {}
): Promise<boolean> {
try {
// Load templates from files
const htmlTemplate = this.loadTemplate(htmlTemplatePath);
const textTemplate = this.loadTemplate(textTemplatePath);
// Process templates with data
const processedHtml = this.processTemplate(htmlTemplate, templateData);
const processedText = this.processTemplate(textTemplate, templateData);
const processedSubject = this.processTemplate(subject, templateData);
const mailOptions = {
from: this.fromAddress,
to: to,
subject: processedSubject,
html: processedHtml,
text: processedText
};
const info = await this.transporter.sendMail(mailOptions);
appLogger.business('Email sent successfully', 'email', {
messageId: info.messageId,
to: to.replace(/(.{2}).*@/, '$1***@'),
subject: processedSubject,
htmlTemplate: htmlTemplatePath,
textTemplate: textTemplatePath,
templateVars: Object.keys(templateData)
});
return true;
} catch (error) {
appLogger.errorEvent('Failed to send email', 'EmailSender.sendEmail', {
to: to.replace(/(.{2}).*@/, '$1***@'),
subject: subject,
htmlTemplate: htmlTemplatePath,
textTemplate: textTemplatePath,
templateVars: Object.keys(templateData),
error: error instanceof Error ? error.message : String(error)
});
return false;
}
}
async testConnection(): Promise<boolean> {
try {
await this.transporter.verify();
appLogger.info('Email server connection verified');
return true;
} catch (error) {
appLogger.error('Email server connection failed', error);
return false;
}
}
}
@@ -0,0 +1,593 @@
import * as winston from 'winston';
import * as AWS from 'aws-sdk';
import * as path from 'path';
import * as fs from 'fs';
// Logger Configuration Interface
interface LoggerConfig {
logLevel?: string;
enableConsole?: boolean;
enableFile?: boolean;
enableMinio?: boolean;
logDirectory?: string;
minioConfig?: {
endpoint: string;
port?: number;
accessKey: string;
secretKey: string;
bucket: string;
useSSL?: boolean;
keyPrefix?: string;
};
}
// Enhanced Application Logger - Simplified for MinIO and Console only
export class EnhancedLogger {
private static instance: EnhancedLogger;
private winston!: winston.Logger;
private minioClient: AWS.S3 | null = null;
private requestCounter: number = 0;
private errorCounter: number = 0;
private loggerConfig: LoggerConfig;
private logsDir: string;
private constructor(config: LoggerConfig) {
this.loggerConfig = { ...this.getDefaultConfig(), ...config };
this.logsDir = path.join(process.cwd(), this.loggerConfig.logDirectory || 'logs');
this.initializeDirectories();
this.initializeMinioClient();
this.createWinstonLogger();
this.initializeMinioStorage();
this.setupGracefulShutdown();
}
public static getInstance(config?: LoggerConfig): EnhancedLogger {
if (!EnhancedLogger.instance) {
EnhancedLogger.instance = new EnhancedLogger(config || {});
}
return EnhancedLogger.instance;
}
private getDefaultConfig(): LoggerConfig {
return {
logLevel: process.env.LOG_LEVEL || 'info',
enableConsole: process.env.NODE_ENV !== 'production',
enableFile: true,
enableMinio: process.env.MINIO_ENABLED === 'true',
logDirectory: './logs',
minioConfig: {
endpoint: process.env.MINIO_ENDPOINT || 'http://127.0.0.1:9000',
port: parseInt(process.env.MINIO_PORT || '9000'),
accessKey: process.env.MINIO_ACCESS_KEY || 'minioadmin',
secretKey: process.env.MINIO_SECRET_KEY || 'minioadmin',
bucket: process.env.MINIO_BUCKET || 'serpentrace-logs',
useSSL: process.env.MINIO_USE_SSL === 'true',
keyPrefix: 'backend-logs/'
}
};
}
private initializeDirectories(): void {
if (!fs.existsSync(this.logsDir)) {
fs.mkdirSync(this.logsDir, { recursive: true });
}
}
private initializeMinioClient(): void {
if (this.loggerConfig.enableMinio && this.loggerConfig.minioConfig) {
const minioConfig: AWS.S3.ClientConfiguration = {
endpoint: this.loggerConfig.minioConfig.endpoint,
accessKeyId: this.loggerConfig.minioConfig.accessKey,
secretAccessKey: this.loggerConfig.minioConfig.secretKey,
s3ForcePathStyle: true,
signatureVersion: 'v4',
region: 'us-east-1' // MinIO doesn't care about region, but AWS SDK requires it
};
this.minioClient = new AWS.S3(minioConfig);
}
}
private createWinstonLogger(): void {
const enhancedFormat = winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }),
winston.format.errors({ stack: true }),
winston.format.json(),
winston.format.printf(({ timestamp, level, message, category, service, environment, ...meta }) => {
const logEntry = {
'@timestamp': timestamp,
level: level.toUpperCase(),
message,
category: category || 'GENERAL',
service: service || 'serpentrace-backend',
environment: environment || process.env.NODE_ENV || 'development',
hostname: process.env.HOSTNAME || require('os').hostname(),
pid: process.pid,
version: process.env.APP_VERSION || '1.0.0',
...meta
};
return JSON.stringify(logEntry);
})
);
const consoleFormat = winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.errors({ stack: true }),
winston.format.colorize(),
winston.format.printf(({ timestamp, level, message, category, ...meta }) => {
let output = `${timestamp} [${level}]`;
if (category) output += ` [${category}]`;
output += `: ${message}`;
if (Object.keys(meta).length > 0 && meta.service !== 'serpentrace-backend') {
output += ` ${JSON.stringify(meta, null, 0)}`;
}
return output;
})
);
const transports: winston.transport[] = [];
// File transports
if (this.loggerConfig.enableFile) {
transports.push(
new winston.transports.File({
filename: path.join(this.logsDir, 'error.log'),
level: 'error',
maxsize: 10485760, // 10MB
maxFiles: 10,
tailable: true
}),
new winston.transports.File({
filename: path.join(this.logsDir, 'combined.log'),
maxsize: 10485760, // 10MB
maxFiles: 10,
tailable: true
}),
new winston.transports.File({
filename: path.join(this.logsDir, 'audit.log'),
level: 'warn',
maxsize: 5242880, // 5MB
maxFiles: 20,
tailable: true
})
);
}
// Console transport
if (this.loggerConfig.enableConsole) {
transports.push(new winston.transports.Console({
format: consoleFormat,
level: 'debug'
}));
}
this.winston = winston.createLogger({
level: this.loggerConfig.logLevel,
format: enhancedFormat,
defaultMeta: {
service: 'serpentrace-backend',
environment: process.env.NODE_ENV || 'development',
instance: process.env.INSTANCE_ID || 'default'
},
transports
});
}
private async initializeMinioStorage(): Promise<void> {
if (!this.minioClient || !this.loggerConfig.minioConfig) return;
try {
await this.minioClient.headBucket({ Bucket: this.loggerConfig.minioConfig.bucket }).promise();
console.log(`✅ MinIO bucket '${this.loggerConfig.minioConfig.bucket}' is accessible`);
} catch (error: any) {
if (error.statusCode === 404) {
try {
await this.minioClient.createBucket({ Bucket: this.loggerConfig.minioConfig.bucket }).promise();
console.log(`✅ Created MinIO bucket '${this.loggerConfig.minioConfig.bucket}'`);
} catch (createError) {
console.error(`❌ Failed to create MinIO bucket:`, createError);
}
} else {
console.error(`❌ Failed to access MinIO bucket:`, error);
}
}
}
// CORE LOGGING METHODS
private async logWithMinio(level: string, message: string, meta: any = {}): Promise<void> {
const logEntry = {
level: level.toUpperCase(),
message,
timestamp: new Date().toISOString(),
...meta
};
this.winston.log(level, message, meta);
// Send to MinIO asynchronously if enabled
if (this.loggerConfig.enableMinio && level === 'error') {
this.errorCounter++;
await this.uploadLogToMinio(logEntry);
}
}
// Standard log levels
async error(message: string, meta?: any): Promise<void> {
await this.logWithMinio('error', message, { category: 'ERROR', ...meta });
}
async warn(message: string, meta?: any): Promise<void> {
await this.logWithMinio('warn', message, { category: 'WARNING', ...meta });
}
async info(message: string, meta?: any): Promise<void> {
await this.logWithMinio('info', message, { category: 'INFO', ...meta });
}
async debug(message: string, meta?: any): Promise<void> {
await this.logWithMinio('debug', message, { category: 'DEBUG', ...meta });
}
// BACKEND FUNCTIONALITY LOGGING
// Startup and System Events
async startup(message: string, meta?: any): Promise<void> {
await this.logWithMinio('info', message, {
category: 'STARTUP',
component: 'system',
...meta
});
}
async system(message: string, meta?: any): Promise<void> {
await this.logWithMinio('info', message, {
category: 'SYSTEM',
component: 'system',
...meta
});
}
// Database Operations with Performance Metrics
async database(operation: string, table?: string, duration?: number, meta?: any): Promise<void> {
await this.logWithMinio('info', `Database: ${operation}`, {
category: 'DATABASE',
operation: operation.toUpperCase(),
table,
duration: duration ? `${duration}ms` : undefined,
performanceFlag: duration && duration > 1000 ? 'SLOW_QUERY' : undefined,
...meta
});
}
// Authentication and Authorization
async auth(action: string, success: boolean, meta?: any): Promise<void> {
const level = success ? 'info' : 'warn';
await this.logWithMinio(level, `Auth: ${action}`, {
category: 'AUTHENTICATION',
action: action.toUpperCase(),
success,
securityEvent: !success,
...meta
});
}
// API Request Logging with Enhanced Metrics
async request(req: any, res: any, duration?: number): Promise<void> {
this.requestCounter++;
const statusCode = res.statusCode;
const level = statusCode >= 400 ? 'warn' : 'info';
await this.logWithMinio(level, `Request: ${req.method} ${req.originalUrl || req.url}`, {
category: 'API_REQUEST',
method: req.method,
url: req.originalUrl || req.url,
statusCode,
statusCategory: this.getStatusCategory(statusCode),
ip: req.ip || req.connection?.remoteAddress,
userAgent: req.get('User-Agent'),
duration: duration ? `${duration}ms` : undefined,
userId: req.user?.id,
userGuid: req.user?.guid,
requestId: this.requestCounter,
performanceFlag: duration && duration > 5000 ? 'SLOW_REQUEST' : undefined,
headers: process.env.LOG_HEADERS === 'true' ? req.headers : undefined
});
}
// Security Events with Enhanced Tracking
async security(event: string, severity: 'low' | 'medium' | 'high' | 'critical', meta?: any): Promise<void> {
const level = severity === 'critical' || severity === 'high' ? 'error' : 'warn';
await this.logWithMinio(level, `Security: ${event}`, {
category: 'SECURITY',
event: event.toUpperCase(),
severity: severity.toUpperCase(),
securityEvent: true,
alertRequired: severity === 'critical',
timestamp: new Date().toISOString(),
...meta
});
}
// Business Logic Events
async business(action: string, entity: string, meta?: any): Promise<void> {
await this.logWithMinio('info', `Business: ${action} ${entity}`, {
category: 'BUSINESS_LOGIC',
action: action.toUpperCase(),
entity: entity.toUpperCase(),
...meta
});
}
// Error Events with Stack Traces
async errorEvent(error: string, context: string, meta?: any): Promise<void> {
await this.logWithMinio('error', `Error in ${context}: ${error}`, {
category: 'APPLICATION_ERROR',
context: context.toUpperCase(),
error,
stackTrace: meta?.stack || new Error().stack,
...meta
});
}
// Performance Monitoring
async performance(metric: string, value: number, unit: string, meta?: any): Promise<void> {
await this.logWithMinio('info', `Performance: ${metric}`, {
category: 'PERFORMANCE',
metric: metric.toUpperCase(),
value,
unit,
performanceAlert: this.checkPerformanceThreshold(metric, value),
...meta
});
}
// Configuration and Environment
async configLog(component: string, status: 'loaded' | 'failed' | 'updated', meta?: any): Promise<void> {
const level = status === 'failed' ? 'error' : 'info';
await this.logWithMinio(level, `Config: ${component} ${status}`, {
category: 'CONFIGURATION',
component: component.toUpperCase(),
status: status.toUpperCase(),
...meta
});
}
// Middleware Events
async middleware(name: string, action: string, meta?: any): Promise<void> {
await this.logWithMinio('debug', `Middleware: ${name} ${action}`, {
category: 'MIDDLEWARE',
middleware: name.toUpperCase(),
action: action.toUpperCase(),
...meta
});
}
// Route Events
async route(path: string, method: string, action: string, meta?: any): Promise<void> {
await this.logWithMinio('debug', `Route: ${method} ${path} ${action}`, {
category: 'ROUTING',
path,
method: method.toUpperCase(),
action: action.toUpperCase(),
...meta
});
}
// Company Registration Events (Business Specific)
async companyRegistration(action: string, companyId: number, userId: number, meta?: any): Promise<void> {
await this.logWithMinio('info', `Company Registration: ${action}`, {
category: 'COMPANY_REGISTRATION',
action: action.toUpperCase(),
companyId,
userId,
businessEvent: true,
...meta
});
}
// Email Events
async email(action: string, recipient: string, type: string, meta?: any): Promise<void> {
await this.logWithMinio('info', `Email: ${action} ${type}`, {
category: 'EMAIL',
action: action.toUpperCase(),
recipient: this.maskEmail(recipient),
emailType: type.toUpperCase(),
...meta
});
}
// Cron Job Events
async cronJob(jobName: string, status: 'started' | 'completed' | 'failed', meta?: any): Promise<void> {
const level = status === 'failed' ? 'error' : 'info';
await this.logWithMinio(level, `CronJob: ${jobName} ${status}`, {
category: 'CRON_JOB',
jobName: jobName.toUpperCase(),
status: status.toUpperCase(),
scheduledTask: true,
...meta
});
}
// File Operations
async fileOperation(operation: string, filename: string, success: boolean, meta?: any): Promise<void> {
const level = success ? 'info' : 'error';
await this.logWithMinio(level, `File: ${operation} ${filename}`, {
category: 'FILE_OPERATION',
operation: operation.toUpperCase(),
filename: path.basename(filename), // Only log basename for security
success,
...meta
});
}
// Migration Events
async migration(migrationName: string, status: 'started' | 'completed' | 'failed', meta?: any): Promise<void> {
const level = status === 'failed' ? 'error' : 'info';
await this.logWithMinio(level, `Migration: ${migrationName} ${status}`, {
category: 'DATABASE_MIGRATION',
migrationName,
status: status.toUpperCase(),
databaseOperation: true,
...meta
});
}
// MINIO LOGGING METHODS
private async uploadLogToMinio(logEntry: any): Promise<void> {
if (!this.minioClient || !this.loggerConfig.minioConfig) return;
try {
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const key = `${this.loggerConfig.minioConfig.keyPrefix || 'logs/'}${new Date().toISOString().split('T')[0]}/error-${timestamp}.json`;
await this.minioClient.upload({
Bucket: this.loggerConfig.minioConfig.bucket,
Key: key,
Body: JSON.stringify(logEntry),
ContentType: 'application/json',
Metadata: {
'log-type': 'application',
'service': 'serpentrace-backend',
'environment': process.env.NODE_ENV || 'development',
'level': logEntry.level
}
}).promise();
console.log(`✅ Log uploaded to MinIO: ${key}`);
} catch (error) {
console.error(`❌ Failed to upload log to MinIO:`, error);
}
}
async uploadCurrentLogs(): Promise<void> {
if (!this.minioClient || !this.loggerConfig.minioConfig) return;
const logFiles = ['combined.log', 'error.log', 'audit.log'];
for (const logFile of logFiles) {
const filePath = path.join(this.logsDir, logFile);
if (!fs.existsSync(filePath)) continue;
const logData = fs.readFileSync(filePath, 'utf8');
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const key = `${this.loggerConfig.minioConfig.keyPrefix || 'logs/'}${new Date().toISOString().split('T')[0]}/${logFile.replace('.log', '')}-${timestamp}.log`;
try {
await this.minioClient.upload({
Bucket: this.loggerConfig.minioConfig.bucket,
Key: key,
Body: logData,
ContentType: 'text/plain',
Metadata: {
'log-type': 'file',
'service': 'serpentrace-backend',
'environment': process.env.NODE_ENV || 'development'
}
}).promise();
console.log(`✅ Log file uploaded to MinIO: ${key}`);
} catch (error) {
console.error(`❌ Failed to upload log file to MinIO:`, error);
}
}
}
// Manual log upload (for scheduled uploads)
async uploadLogs(): Promise<void> {
try {
await this.uploadCurrentLogs();
await this.logWithMinio('info', 'Log upload completed', {
category: 'LOG_UPLOAD',
component: 'logging-system'
});
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
await this.error('Failed to upload logs', {
error: errorMessage,
category: 'LOG_UPLOAD'
});
}
}
// UTILITY METHODS
private getStatusCategory(statusCode: number): string {
if (statusCode >= 200 && statusCode < 300) return 'SUCCESS';
if (statusCode >= 300 && statusCode < 400) return 'REDIRECT';
if (statusCode >= 400 && statusCode < 500) return 'CLIENT_ERROR';
if (statusCode >= 500) return 'SERVER_ERROR';
return 'UNKNOWN';
}
private checkPerformanceThreshold(metric: string, value: number): boolean {
const thresholds: { [key: string]: number } = {
'response_time': 1000,
'database_query': 500,
'memory_usage': 80,
'cpu_usage': 80
};
return value > (thresholds[metric.toLowerCase()] || Infinity);
}
private maskEmail(email: string): string {
if (!email || !email.includes('@')) return 'invalid-email';
const [local, domain] = email.split('@');
const maskedLocal = local.length > 2 ?
local.substring(0, 2) + '*'.repeat(local.length - 2) :
local;
return `${maskedLocal}@${domain}`;
}
// Get logging statistics
getStats(): { requests: number; errors: number; minioEnabled: boolean } {
return {
requests: this.requestCounter,
errors: this.errorCounter,
minioEnabled: this.loggerConfig.enableMinio || false
};
}
// Setup graceful shutdown
private setupGracefulShutdown(): void {
const gracefulShutdown = async (signal: string) => {
await this.system(`Application shutting down via ${signal}`);
await this.uploadLogs();
process.exit(0);
};
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
}
}
// Create and export the enhanced logger instance
const defaultConfig: LoggerConfig = {
logLevel: 'info',
enableConsole: true,
enableFile: true,
enableMinio: false, // Set to true to enable MinIO logging
logDirectory: './logs',
minioConfig: {
endpoint: process.env.MINIO_ENDPOINT || 'http://127.0.0.1:9000',
port: parseInt(process.env.MINIO_PORT || '9000'),
accessKey: process.env.MINIO_ACCESS_KEY || 'minioadmin',
secretKey: process.env.MINIO_SECRET_KEY || 'minioadmin',
bucket: process.env.MINIO_BUCKET || 'serpentrace-logs',
useSSL: process.env.MINIO_USE_SSL === 'true',
keyPrefix: 'backend-logs/'
}
};
export const appLogger = EnhancedLogger.getInstance(defaultConfig);
// Setup periodic log upload (every 30 minutes in production)
let uploadInterval: NodeJS.Timeout | null = null;
if (process.env.NODE_ENV === 'production' && !uploadInterval) {
uploadInterval = setInterval(async () => {
await appLogger.uploadLogs();
}, 30 * 60 * 1000); // 30 minutes
}
// Export default instance
export default appLogger;
@@ -0,0 +1,62 @@
import { randomBytes } from 'crypto';
export class GuidGenerator {
static generateGuid(): string {
const bytes = randomBytes(12);
const hexString = bytes.toString('hex').toUpperCase();
return `${hexString.slice(0, 8)}-${hexString.slice(8, 16)}-${hexString.slice(16, 24)}`;
}
static generateUuid(): string {
const bytes = randomBytes(16);
const hex = bytes.toString('hex');
return [
hex.slice(0, 8),
hex.slice(8, 12),
'4' + hex.slice(13, 16),
((parseInt(hex.slice(16, 17), 16) & 0x3) | 0x8).toString(16) + hex.slice(17, 20),
hex.slice(20, 32)
].join('-');
}
static isValidGuid(guid: string): boolean {
const guidRegex = /^[A-F0-9]{8}-[A-F0-9]{8}-[A-F0-9]{8}$/;
return guidRegex.test(guid);
}
static isValidUuid(uuid: string): boolean {
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
return uuidRegex.test(uuid);
}
static generateMultipleGuids(count: number): string[] {
const guids: string[] = [];
const guidSet = new Set<string>();
while (guidSet.size < count) {
const guid = this.generateGuid();
if (!guidSet.has(guid)) {
guidSet.add(guid);
guids.push(guid);
}
}
return guids;
}
static generateMultipleUuids(count: number): string[] {
const uuids: string[] = [];
const uuidSet = new Set<string>();
while (uuidSet.size < count) {
const uuid = this.generateUuid();
if (!uuidSet.has(uuid)) {
uuidSet.add(uuid);
uuids.push(uuid);
}
}
return uuids;
}
}
+907
View File
@@ -0,0 +1,907 @@
# SerpentRace Backend - Utils Documentation
This directory contains utility modules that provide common functionality across the SerpentRace backend application.
## Table of Contents
- [Enhanced Logger](#enhanced-logger)
- [Overview](#overview)
- [Quick Start](#quick-start)
- [Configuration](#configuration)
- [Logging Methods](#logging-methods)
- [Remote Logging](#remote-logging)
- [Middleware Integration](#middleware-integration)
- [Best Practices](#best-practices)
- [Other Utils](#other-utils)
- [GUID Generator](#guid-generator)
- [Email Sender](#email-sender)
- [Code Generator](#code-generator)
---
## Enhanced Logger
### Overview
The `EnhancedLogger` is a streamlined logging solution designed specifically for the SerpentRace backend. It provides structured logging with support for console output, local file storage, and MinIO bucket uploads for centralized log management.
**Key Features:**
- ✅ Singleton pattern for consistent logging across the application
- ✅ Multiple log levels with specialized methods
- ✅ MinIO bucket integration for remote log storage
- ✅ Console and file logging
- ✅ Performance monitoring and alerting
- ✅ Security event tracking
- ✅ Automatic log rotation and archival
- ✅ TypeScript support with full type safety
- ✅ Graceful shutdown handling
### Quick Start
```typescript
// Import the default logger instance
import appLogger from '../Utils/EnhancedLogger';
// Basic usage
await appLogger.info('Application started successfully');
await appLogger.error('Something went wrong', { errorCode: 'ERR_001' });
await appLogger.warn('This is a warning', { component: 'database' });
await appLogger.debug('Debug information for development');
```
### Configuration
The logger can be configured through environment variables or by passing a configuration object:
#### Environment Variables
```bash
# Basic Settings
LOG_LEVEL=info # debug, info, warn, error
NODE_ENV=production # development, production
# File Logging
LOG_HEADERS=true # Include request headers in logs
# MinIO Configuration
MINIO_ENABLED=true
MINIO_ENDPOINT=http://127.0.0.1:9000
MINIO_PORT=9000
MINIO_ACCESS_KEY=minioadmin
MINIO_SECRET_KEY=minioadmin
MINIO_BUCKET=serpentrace-logs
MINIO_USE_SSL=false
```
#### Custom Configuration
```typescript
import { EnhancedLogger } from '../Utils/EnhancedLogger';
const customConfig = {
logLevel: 'debug',
enableConsole: true,
enableFile: true,
enableMinio: true,
logDirectory: './custom-logs',
minioConfig: {
endpoint: 'http://your-minio-server:9000',
port: 9000,
accessKey: 'your-access-key',
secretKey: 'your-secret-key',
bucket: 'my-custom-logs-bucket',
useSSL: false,
keyPrefix: 'custom-logs/'
}
};
const customLogger = EnhancedLogger.getInstance(customConfig);
```
### Logging Methods
#### Standard Log Levels
```typescript
// Standard logging methods
await appLogger.error('Error message', { context: 'additional-data' });
await appLogger.warn('Warning message', { userId: 123 });
await appLogger.info('Information message', { operation: 'user-creation' });
await appLogger.debug('Debug message', { variables: { count: 5 } });
```
#### System and Startup Events
```typescript
// Application lifecycle
await appLogger.startup('Server initialization complete', { port: 3000 });
await appLogger.system('Memory usage is normal', { memory: '512MB', cpu: '45%' });
```
#### Database Operations
```typescript
// Database logging with performance tracking
await appLogger.database('SELECT', 'users', 150, { rows: 25 });
await appLogger.database('INSERT', 'companies', 2500, { rows: 1 }); // Flags slow queries (>1000ms)
await appLogger.database('UPDATE', 'profiles', 75, { affected: 1, userId: 123 });
```
#### Authentication and Security
```typescript
// Authentication events
await appLogger.auth('login', true, {
userId: 123,
email: 'user@example.com',
method: 'password'
});
await appLogger.auth('login_failed', false, {
ip: '192.168.1.100',
reason: 'invalid_password',
attempts: 3
});
// Security events with severity levels
await appLogger.security('brute_force_attempt', 'high', {
ip: '192.168.1.100',
attempts: 5,
timeWindow: '5m'
});
await appLogger.security('suspicious_activity', 'critical', {
userId: 123,
action: 'privilege_escalation',
details: 'User attempted admin access'
});
```
#### API Request Logging
```typescript
// Typically used in Express middleware
app.use(async (req, res, next) => {
const startTime = Date.now();
res.on('finish', async () => {
const duration = Date.now() - startTime;
await appLogger.request(req, res, duration);
});
next();
});
// Manual request logging
await appLogger.request(req, res, 250); // duration in ms
```
#### Business Logic Events
```typescript
// Business-specific operations
await appLogger.business('create', 'company', {
companyId: 456,
name: 'Example Corp',
registeredBy: 123
});
await appLogger.business('update', 'user_profile', {
userId: 123,
fields: ['name', 'email'],
changedBy: 'user'
});
```
#### Company Registration (Business Specific)
```typescript
// Company registration workflow
await appLogger.companyRegistration('initiated', 456, 123, {
companyName: 'Example Corp',
registrationCode: 'REG123',
step: 'validation'
});
await appLogger.companyRegistration('completed', 456, 123, {
companyName: 'Example Corp',
approvedBy: 'admin_user_456',
duration: '2h 15m'
});
```
#### Error Events with Context
```typescript
// Detailed error logging
await appLogger.errorEvent('Database connection failed', 'database_service', {
stack: error.stack,
connectionString: 'postgresql://***', // Masked sensitive data
retryAttempt: 3,
lastSuccess: '2025-07-16T10:30:00Z'
});
```
#### Performance Monitoring
```typescript
// Performance metrics with automatic alerting
await appLogger.performance('response_time', 1200, 'ms', {
endpoint: '/api/search',
threshold: 1000 // Will trigger alert if exceeded
});
await appLogger.performance('memory_usage', 85, 'percent', {
process: 'main',
limit: '2GB'
});
await appLogger.performance('database_pool', 75, 'percent', {
activeConnections: 15,
maxConnections: 20
});
```
#### Configuration Events
```typescript
// Configuration loading and updates
await appLogger.configLog('database', 'loaded', {
driver: 'postgresql',
host: 'localhost',
ssl: true
});
await appLogger.configLog('redis', 'failed', {
error: 'Connection timeout',
host: 'redis.example.com'
});
```
#### Middleware and Route Events
```typescript
// Middleware execution
await appLogger.middleware('authentication', 'executed', {
duration: '5ms',
userId: 123,
success: true
});
await appLogger.middleware('rate_limiter', 'blocked', {
ip: '192.168.1.100',
limit: '100/hour',
current: 101
});
// Route handling
await appLogger.route('/api/users/:id', 'GET', 'handled', {
userId: 123,
responseTime: '50ms',
cacheHit: true
});
```
#### Email Events
```typescript
// Email operations
await appLogger.email('sent', 'user@example.com', 'welcome', {
templateId: 'welcome_v2',
deliveryTime: '2ms',
provider: 'sendgrid'
});
await appLogger.email('failed', 'user@invalid-domain.com', 'newsletter', {
error: 'Invalid domain',
retryScheduled: true,
nextRetry: '2025-07-16T11:00:00Z'
});
```
#### Cron Job Events
```typescript
// Scheduled task monitoring
await appLogger.cronJob('daily_backup', 'started', {
scheduledTime: '02:00',
actualTime: '02:00:15'
});
await appLogger.cronJob('daily_backup', 'completed', {
duration: '5m 30s',
filesBackedUp: 1500,
size: '2.5GB'
});
await appLogger.cronJob('email_queue', 'failed', {
error: 'SMTP server unavailable',
emailsPending: 250,
nextRetry: '2025-07-16T11:00:00Z'
});
```
#### File Operations
```typescript
// File system operations
await appLogger.fileOperation('upload', 'document.pdf', true, {
size: '2.5MB',
user: 'user@example.com',
destination: 's3://bucket/documents/'
});
await appLogger.fileOperation('delete', 'temp_file.txt', false, {
error: 'Permission denied',
path: '/tmp/uploads/',
user: 'system'
});
```
#### Database Migrations
```typescript
// Migration tracking
await appLogger.migration('AddUserProfileTable', 'started', {
version: '2025.07.16.001',
estimatedDuration: '30s'
});
await appLogger.migration('AddUserProfileTable', 'completed', {
duration: '25s',
tablesCreated: 1,
recordsMigrated: 0
});
await appLogger.migration('UpdateIndexes', 'failed', {
error: 'Duplicate index name',
rollbackRequired: true,
affectedTables: ['users', 'companies']
});
```
### Remote Logging
#### MinIO Integration
The logger automatically uploads error logs and can manually upload all log files to MinIO:
```typescript
// MinIO upload happens automatically for error logs when MinIO is enabled
// Manual log upload to MinIO (uploads all log files)
await appLogger.uploadLogs();
```
#### Getting Statistics
```typescript
// Get logging statistics
const stats = appLogger.getStats();
console.log('Logger statistics:', stats);
// Output: { requests: 1250, errors: 5, minioEnabled: true }
```
### Middleware Integration
#### Express.js Request Logging Middleware
```typescript
import { createLoggingMiddleware } from '../Utils/LoggerExample';
// Use the pre-built middleware
app.use(createLoggingMiddleware());
// Or create custom middleware
app.use(async (req, res, next) => {
const startTime = Date.now();
// Log incoming request
await appLogger.middleware('request_handler', 'started', {
method: req.method,
url: req.originalUrl,
ip: req.ip,
userAgent: req.get('User-Agent')
});
// Override res.end to capture response time
const originalEnd = res.end;
res.end = function(...args: any[]) {
const duration = Date.now() - startTime;
// Log the completed request
appLogger.request(req, res, duration);
// Call original end method
originalEnd.apply(res, args);
};
next();
});
```
#### Authentication Middleware
```typescript
app.use('/api', async (req, res, next) => {
try {
const token = req.headers.authorization?.replace('Bearer ', '');
const user = await verifyToken(token);
await appLogger.auth('token_validation', true, {
userId: user.id,
endpoint: req.originalUrl,
ip: req.ip
});
req.user = user;
next();
} catch (error) {
await appLogger.auth('token_validation', false, {
error: error.message,
endpoint: req.originalUrl,
ip: req.ip
});
res.status(401).json({ error: 'Unauthorized' });
}
});
```
### Best Practices
#### 1. Use Appropriate Log Levels
```typescript
// ❌ Don't use info for errors
await appLogger.info('Database connection failed');
// ✅ Use appropriate level
await appLogger.error('Database connection failed', {
error: error.message,
stack: error.stack
});
// ❌ Don't use error for normal operations
await appLogger.error('User logged in successfully');
// ✅ Use info for normal operations
await appLogger.info('User logged in successfully', { userId: 123 });
```
#### 2. Include Relevant Context
```typescript
// ❌ Minimal context
await appLogger.error('Operation failed');
// ✅ Rich context
await appLogger.errorEvent('User creation failed', 'user_service', {
email: 'user@example.com',
validationErrors: ['email_exists', 'weak_password'],
requestId: 'req_123456',
stack: error.stack
});
```
#### 3. Use Structured Data
```typescript
// ❌ String interpolation
await appLogger.info(`User ${userId} performed ${action} on ${resource}`);
// ✅ Structured metadata
await appLogger.business(action, resource, {
userId,
timestamp: new Date().toISOString(),
sessionId: req.sessionId
});
```
#### 4. Mask Sensitive Information
```typescript
// ❌ Logging sensitive data
await appLogger.info('User logged in', {
password: user.password,
creditCard: user.creditCard
});
// ✅ Mask or exclude sensitive data
await appLogger.auth('login', true, {
userId: user.id,
email: maskEmail(user.email), // Automatically masked by logger
loginMethod: 'password'
});
```
#### 5. Use Performance Logging Wisely
```typescript
// ❌ Logging every small operation
const startTime = Date.now();
const result = someSmallOperation();
await appLogger.performance('small_op', Date.now() - startTime, 'ms');
// ✅ Log significant operations
const startTime = Date.now();
const users = await database.findAllUsers(); // Potentially slow operation
const duration = Date.now() - startTime;
if (duration > 100) { // Only log if potentially slow
await appLogger.database('SELECT', 'users', duration, {
rowCount: users.length,
filters: req.query
});
}
```
#### 6. Error Handling in Logging
```typescript
// Always wrap async logging in try-catch for critical paths
try {
await appLogger.business('payment', 'processed', paymentData);
} catch (loggingError) {
// Don't let logging errors break your application
console.error('Logging failed:', loggingError);
}
// Or use fire-and-forget for non-critical logging
appLogger.info('User browsed products', { userId, categoryId }).catch(console.error);
```
---
## Other Utils
### GUID Generator
The `GuidGenerator` utility provides methods for generating cryptographically secure GUIDs and UUIDs for unique identification across the SerpentRace application.
#### Features
- **Custom GUID Generation**: 8-8-8 format (e.g., `AB12CD34-EF56GH78-IJ90KL12`)
- **Standard UUID Generation**: RFC 4122 v4 compliant UUIDs
- **Validation Methods**: Verify GUID and UUID format compliance
- **Batch Generation**: Create multiple unique identifiers at once
- **Cryptographically Secure**: Uses Node.js crypto module for randomness
#### Usage Examples
```typescript
import { GuidGenerator } from '../Utils/GuidGenerator';
// Generate a single custom GUID
const guid = GuidGenerator.generateGuid();
console.log(guid); // "AB12CD34-EF56GH78-IJ90KL12"
// Generate a standard UUID v4
const uuid = GuidGenerator.generateUuid();
console.log(uuid); // "123e4567-e89b-12d3-a456-426614174000"
// Validate GUID format
const isValidGuid = GuidGenerator.isValidGuid("AB12CD34-EF56GH78-IJ90KL12");
console.log(isValidGuid); // true
// Validate UUID format
const isValidUuid = GuidGenerator.isValidUuid("123e4567-e89b-12d3-a456-426614174000");
console.log(isValidUuid); // true
// Generate multiple GUIDs
const multipleGuids = GuidGenerator.generateMultipleGuids(5);
console.log(multipleGuids);
// ["AB12CD34-EF56GH78-IJ90KL12", "CD34EF56-GH78IJ90-KL12MN34", ...]
// Generate multiple UUIDs
const multipleUuids = GuidGenerator.generateMultipleUuids(3);
console.log(multipleUuids);
// ["123e4567-e89b-12d3-a456-426614174000", "456e7890-abc1-23d4-b567-789012345678", ...]
```
#### Methods Reference
| Method | Description | Returns |
|--------|-------------|---------|
| `generateGuid()` | Creates a custom 8-8-8 format GUID | `string` |
| `generateUuid()` | Creates a standard RFC 4122 v4 UUID | `string` |
| `isValidGuid(guid)` | Validates custom GUID format | `boolean` |
| `isValidUuid(uuid)` | Validates UUID v4 format | `boolean` |
| `generateMultipleGuids(count)` | Creates array of unique GUIDs | `string[]` |
| `generateMultipleUuids(count)` | Creates array of unique UUIDs | `string[]` |
#### Use Cases
- **Database Primary Keys**: Unique identifiers for entities
- **API Request IDs**: Track requests across services
- **Session Management**: Unique session identifiers
- **File Naming**: Unique filenames for uploads
- **Transaction IDs**: Financial transaction tracking
### Email Sender
The `EmailSender` utility provides a streamlined email service for the SerpentRace application. It uses file-based templates for both HTML and text formats, supporting dynamic content replacement.
#### Features
- **File-Based Templates**: HTML and text templates stored in separate files
- **Template Variables**: Dynamic content replacement using `{{variable}}` syntax
- **Dual Format Support**: Automatically sends both HTML and plain text versions
- **SMTP Configuration**: Environment-based email server configuration
- **Error Handling**: Comprehensive error logging and reporting
- **Email Masking**: Privacy-focused email logging with masked addresses
#### Environment Configuration
```bash
# SMTP Server Configuration
EMAIL_HOST=smtp.gmail.com
EMAIL_PORT=587
EMAIL_SECURE=false
EMAIL_USER=your-email@gmail.com
EMAIL_PASS=your-app-password
EMAIL_FROM=noreply@serpentrace.com
```
#### Template File Structure
Create your email templates in separate HTML and text files:
```
src/Utils/emailTemplates/
├── welcome.html # HTML template
├── welcome.txt # Text template
├── password-reset.html # HTML template
├── password-reset.txt # Text template
├── notification.html # HTML template
└── notification.txt # Text template
```
#### Usage Examples
```typescript
import { EmailSender } from '../Utils/EmailSender';
import * as path from 'path';
const emailSender = EmailSender.create();
const templateDir = path.join(__dirname, 'emailTemplates', 'files');
// Test connection
const isConnected = await emailSender.testConnection();
// Send welcome email
await emailSender.sendEmail(
'user@example.com',
'Welcome to SerpentRace - {{username}}!',
path.join(templateDir, 'welcome.html'),
path.join(templateDir, 'welcome.txt'),
{
username: 'SpeedRacer',
loginUrl: 'https://serpentrace.com/login',
bonusPoints: '500'
}
);
// Send password reset email
await emailSender.sendEmail(
'user@example.com',
'Reset Your Password',
path.join(templateDir, 'password-reset.html'),
path.join(templateDir, 'password-reset.txt'),
{
username: 'john_doe',
resetUrl: 'https://serpentrace.com/reset?token=abc123',
expirationTime: '15'
}
);
// Send system notification
await emailSender.sendEmail(
'player@example.com',
'System Update - {{notificationType}}',
path.join(templateDir, 'notification.html'),
path.join(templateDir, 'notification.txt'),
{
username: 'ProRacer',
notificationType: 'Premium Upgrade',
message: 'Your account has been upgraded to Premium status.',
details: 'You now have access to exclusive features.',
actionUrl: 'https://serpentrace.com/premium',
actionText: 'Explore Premium Features'
}
);
```
#### Template Variable System
Both HTML and text templates support the same variable replacement syntax:
#### Method Reference
| Method | Description | Parameters |
|--------|-------------|------------|
| `sendEmail(to, subject, htmlPath, textPath, data)` | Send email with file-based templates | `to: string, subject: string, htmlPath: string, textPath: string, data: object` |
| `testConnection()` | Test SMTP server connection | Returns `Promise<boolean>` |
#### Template Examples
**Sample HTML Template:**
```html
<!DOCTYPE html>
<html>
<body style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h1 style="color: #2c3e50;">Hello {{username}}!</h1>
<p>{{message}}</p>
<a href="{{actionUrl}}" style="background-color: #3498db; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px;">
{{actionText}}
</a>
</body>
</html>
```
**Sample Text Template:**
```text
Hello {{username}}!
{{message}}
{{actionText}}: {{actionUrl}}
```
#### Error Handling
The `sendEmail` method returns `Promise<boolean>` indicating success/failure:
```typescript
const success = await emailSender.sendEmail(
'user@example.com',
'Test Email',
'/path/to/template.html',
'/path/to/template.txt',
{ username: 'TestUser' }
);
if (success) {
console.log('Email sent successfully');
} else {
console.log('Email failed to send - check logs');
}
```
#### Security Features
- **Email Masking**: Email addresses are masked in logs (e.g., `jo***@example.com`)
- **Template Validation**: File existence and readability checks
- **Environment Variables**: Secure configuration via environment variables
- **Connection Testing**: Built-in SMTP connection verification
### Code Generator
The `CodeGenerator` utility provides methods for generating various types of secure random codes and identifiers for the SerpentRace application.
#### Features
- **Multiple Code Types**: Numeric, alphanumeric, and hexadecimal codes
- **Customizable Length**: Specify exact length for generated codes
- **Cryptographically Secure**: Uses Node.js crypto module for randomness
- **Optimized Character Sets**: Removes ambiguous characters (0/O, 1/I/l) for better readability
- **Flexible API**: Simple static methods for easy integration
#### Usage Examples
```typescript
import { CodeGenerator } from '../Utils/CodeGenerator';
// Generate default confirmation code (32 characters, alphanumeric)
const defaultCode = CodeGenerator.generateConfirmationCode();
console.log(defaultCode); // "ABC123DEF456GHJ789KLM234NPQ567RST"
// Generate numeric code (6 digits)
const numericCode = CodeGenerator.generateConfirmationCode(6, 'numeric');
console.log(numericCode); // "123456"
// Generate alphanumeric code (8 characters)
const alphanumericCode = CodeGenerator.generateConfirmationCode(8, 'alphanumeric');
console.log(alphanumericCode); // "AB3C4D5E"
// Generate hexadecimal code (16 characters)
const hexCode = CodeGenerator.generateConfirmationCode(16, 'hex');
console.log(hexCode); // "1A2B3C4D5E6F7A8B"
```
#### Code Types
| Type | Characters | Use Case | Example |
|------|------------|----------|---------|
| `numeric` | 0-9 | PIN codes, verification codes | `123456` |
| `alphanumeric` | A-Z, 2-9 (excludes 0,1,I,O) | Confirmation codes, tokens | `ABC123XYZ` |
| `hex` | 0-9, A-F | Session tokens, API keys | `1A2B3C4D5E` |
#### Common Use Cases
```typescript
// Email verification codes (6 digits)
const emailVerificationCode = CodeGenerator.generateConfirmationCode(6, 'numeric');
// Password reset tokens (32 characters)
const resetToken = CodeGenerator.generateConfirmationCode(32, 'hex');
// User-friendly confirmation codes (8 characters)
const friendlyCode = CodeGenerator.generateConfirmationCode(8, 'alphanumeric');
// Session identifiers (16 characters)
const sessionId = CodeGenerator.generateConfirmationCode(16, 'hex');
// Two-factor authentication codes (6 digits)
const twoFactorCode = CodeGenerator.generateConfirmationCode(6, 'numeric');
// Invitation codes (12 characters)
const invitationCode = CodeGenerator.generateConfirmationCode(12, 'alphanumeric');
```
#### Method Reference
| Method | Description | Parameters | Returns |
|--------|-------------|------------|---------|
| `generateConfirmationCode(length?, type?)` | Generate random code | `length: number = 32, type: 'numeric' \| 'alphanumeric' \| 'hex' = 'alphanumeric'` | `string` |
#### Character Set Details
- **Numeric**: `0123456789` (10 characters)
- **Alphanumeric**: `ABCDEFGHJKLMNPQRSTUVWXYZ23456789` (32 characters)
- Excludes: `0`, `1`, `I`, `O` to prevent confusion
- **Hexadecimal**: `0123456789ABCDEF` (16 characters)
#### Security Considerations
- **Cryptographic Randomness**: Uses `crypto.randomBytes()` for secure random generation
- **No Ambiguous Characters**: Alphanumeric codes exclude easily confused characters
- **Appropriate Length**: Default 32 characters provides high entropy
- **Uppercase Format**: Consistent formatting for better readability
#### Integration Examples
```typescript
// In user registration
const confirmationCode = CodeGenerator.generateConfirmationCode(8, 'alphanumeric');
await emailSender.sendEmail(
userEmail,
'Confirm Your Account',
'confirmation.html',
'confirmation.txt',
{ confirmationCode }
);
// In password reset
const resetToken = CodeGenerator.generateConfirmationCode(32, 'hex');
await storeResetToken(userId, resetToken);
// In two-factor authentication
const twoFactorCode = CodeGenerator.generateConfirmationCode(6, 'numeric');
await sendSMSVerification(userPhone, twoFactorCode);
```
### Planned Utilities
- **CronJobService**: Centralized cron job management
---
## Contributing
When adding new utility modules:
1. Create the utility in the `src/Utils/` directory
2. Add comprehensive documentation to this README
3. Include usage examples
4. Add TypeScript interfaces and types
5. Write unit tests
6. Update the Table of Contents
## Support
For questions or issues with the Utils modules, please refer to the main project documentation or contact the development team.
---
*Last updated: July 16, 2025*
@@ -0,0 +1,43 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>System Notification</title>
</head>
<body style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
<div style="text-align: center; margin-bottom: 30px;">
<h1 style="color: #f39c12; margin-bottom: 10px;">{{notificationType}}</h1>
<p style="color: #7f8c8d; font-size: 16px;">SerpentRace System Notification</p>
</div>
<div style="background-color: #f8f9fa; padding: 20px; border-radius: 8px; margin-bottom: 30px;">
<h2 style="color: #2c3e50; margin-top: 0;">Hello {{username}},</h2>
<p style="color: #34495e; line-height: 1.6;">
{{message}}
</p>
</div>
<div style="background-color: #e8f4fd; border: 1px solid #bee5eb; padding: 15px; border-radius: 5px; margin-bottom: 20px;">
<h3 style="color: #0c5460; margin-top: 0;">Details:</h3>
<p style="color: #0c5460; margin: 0;">
{{details}}
</p>
</div>
<div style="text-align: center; margin-bottom: 30px;">
<a href="{{actionUrl}}" style="background-color: #17a2b8; color: white; padding: 12px 30px; text-decoration: none; border-radius: 5px; display: inline-block; font-weight: bold;">
{{actionText}}
</a>
</div>
<div style="border-top: 2px solid #ecf0f1; padding-top: 20px; text-align: center;">
<p style="color: #7f8c8d; font-size: 14px; margin: 0;">
This is an automated message. Please do not reply to this email.
</p>
<p style="color: #7f8c8d; font-size: 14px; margin: 10px 0 0 0;">
The SerpentRace System
</p>
</div>
</body>
</html>
@@ -0,0 +1,14 @@
{{notificationType}} - SerpentRace
Hello {{username}},
{{message}}
Details:
{{details}}
{{actionText}}: {{actionUrl}}
This is an automated message. Please do not reply to this email.
The SerpentRace System
@@ -0,0 +1,45 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Password Reset</title>
</head>
<body style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
<div style="text-align: center; margin-bottom: 30px;">
<h1 style="color: #e74c3c; margin-bottom: 10px;">Password Reset Request</h1>
<p style="color: #7f8c8d; font-size: 16px;">SerpentRace Account Security</p>
</div>
<div style="background-color: #f8f9fa; padding: 20px; border-radius: 8px; margin-bottom: 30px;">
<h2 style="color: #2c3e50; margin-top: 0;">Hello {{username}},</h2>
<p style="color: #34495e; line-height: 1.6;">
We received a request to reset your password for your SerpentRace account.
</p>
<p style="color: #34495e; line-height: 1.6;">
If you made this request, click the button below to reset your password:
</p>
</div>
<div style="text-align: center; margin-bottom: 30px;">
<a href="{{resetUrl}}" style="background-color: #e74c3c; color: white; padding: 12px 30px; text-decoration: none; border-radius: 5px; display: inline-block; font-weight: bold;">
Reset Password
</a>
</div>
<div style="background-color: #fff3cd; border: 1px solid #ffeaa7; padding: 15px; border-radius: 5px; margin-bottom: 20px;">
<p style="color: #856404; margin: 0; font-size: 14px;">
⚠️ This link will expire in {{expirationTime}} minutes. If you didn't request this reset, please ignore this email.
</p>
</div>
<div style="border-top: 2px solid #ecf0f1; padding-top: 20px; text-align: center;">
<p style="color: #7f8c8d; font-size: 14px; margin: 0;">
For security reasons, never share this link with anyone.
</p>
<p style="color: #7f8c8d; font-size: 14px; margin: 10px 0 0 0;">
The SerpentRace Security Team
</p>
</div>
</body>
</html>
@@ -0,0 +1,14 @@
Password Reset Request - SerpentRace
Hello {{username}},
We received a request to reset your password for your SerpentRace account.
If you made this request, click the link below to reset your password:
{{resetUrl}}
⚠️ This link will expire in {{expirationTime}} minutes. If you didn't request this reset, please ignore this email.
For security reasons, never share this link with anyone.
The SerpentRace Security Team
@@ -0,0 +1,56 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Welcome to SerpentRace</title>
</head>
<body style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; padding: 20px;">
<div style="text-align: center; margin-bottom: 30px;">
<h1 style="color: #2c3e50; margin-bottom: 10px;">Welcome to SerpentRace!</h1>
<p style="color: #7f8c8d; font-size: 18px;">Get ready to race like never before</p>
</div>
<div style="background-color: #f8f9fa; padding: 20px; border-radius: 8px; margin-bottom: 30px;">
<h2 style="color: #2c3e50; margin-top: 0;">Hello {{username}}!</h2>
<p style="color: #34495e; line-height: 1.6;">
Thank you for joining SerpentRace! We're excited to have you as part of our racing community.
</p>
<p style="color: #34495e; line-height: 1.6;">
Your account has been successfully created and you're ready to start racing. Here's what you can do next:
</p>
</div>
<div style="margin-bottom: 30px;">
<h3 style="color: #2c3e50;">Getting Started:</h3>
<ul style="color: #34495e; line-height: 1.8;">
<li>Complete your profile setup</li>
<li>Join your first race</li>
<li>Challenge other racers</li>
<li>Climb the leaderboard</li>
</ul>
</div>
<div style="text-align: center; margin-bottom: 30px;">
<a href="{{loginUrl}}" style="background-color: #3498db; color: white; padding: 12px 30px; text-decoration: none; border-radius: 5px; display: inline-block; font-weight: bold;">
Start Racing Now
</a>
</div>
<div style="background-color: #e8f5e8; padding: 15px; border-radius: 5px; margin-bottom: 20px;">
<p style="color: #27ae60; margin: 0; font-weight: bold;">
🎉 Welcome bonus: {{bonusPoints}} points added to your account!
</p>
</div>
<div style="border-top: 2px solid #ecf0f1; padding-top: 20px; text-align: center;">
<p style="color: #7f8c8d; font-size: 14px; margin: 0;">
Need help? Contact our support team at support@serpentrace.com
</p>
<p style="color: #7f8c8d; font-size: 14px; margin: 10px 0 0 0;">
Happy racing!<br>
The SerpentRace Team
</p>
</div>
</body>
</html>
@@ -0,0 +1,22 @@
Welcome to SerpentRace!
Hello {{username}}!
Thank you for joining SerpentRace! We're excited to have you as part of our racing community.
Your account has been successfully created and you're ready to start racing. Here's what you can do next:
Getting Started:
- Complete your profile setup
- Join your first race
- Challenge other racers
- Climb the leaderboard
Start Racing Now: {{loginUrl}}
🎉 Welcome bonus: {{bonusPoints}} points added to your account!
Need help? Contact our support team at support@serpentrace.com
Happy racing!
The SerpentRace Team
@@ -1,19 +0,0 @@
import { CompanyCreateDto, CompanyUpdateDto } from '../../../Database/dto/company.dto';
// Create company command
export class CreateCompanyCommand {
constructor(public readonly companyCreateDto: CompanyCreateDto) {}
}
// Update company command
export class UpdateCompanyCommand {
constructor(
public readonly id: number,
public readonly companyUpdateDto: CompanyUpdateDto
) {}
}
// Delete company command
export class DeleteCompanyCommand {
constructor(public readonly id: number) {}
}
@@ -1,52 +0,0 @@
import { ICompanyRepository } from '../../../Repositories/interfaces/ICompanyRepository';
import { CompanyCommandHandler } from './CompanyCommandHandler';
import {
CreateCompanyCommand,
UpdateCompanyCommand,
DeleteCompanyCommand
} from './CompanyCommand';
import { appLogger } from '../../../utils/logger';
export class CompanyCommandDispatcher {
private commandHandler: CompanyCommandHandler;
constructor(companyRepository: ICompanyRepository) {
appLogger.startup('Initializing CompanyCommandDispatcher...');
this.commandHandler = new CompanyCommandHandler(companyRepository);
appLogger.startup('CompanyCommandDispatcher initialized');
}
async dispatch(command: any): Promise<any> {
try {
appLogger.info('Dispatching company command', {
commandType: command.constructor.name,
action: 'dispatch_company_command'
});
switch (command.constructor) {
case CreateCompanyCommand:
return await this.commandHandler.handleCreate(command);
case UpdateCompanyCommand:
return await this.commandHandler.handleUpdate(command);
case DeleteCompanyCommand:
return await this.commandHandler.handleDelete(command);
default:
const errorMessage = `Unknown command type: ${command.constructor.name}`;
appLogger.errorEvent('Unknown company command type', 'dispatch_error', {
commandType: command.constructor.name
});
throw new Error(errorMessage);
}
} catch (error: any) {
appLogger.errorEvent('Error dispatching company command', 'dispatch_error', {
error: error.message,
commandType: command.constructor.name
});
throw error;
}
}
}
@@ -1,126 +0,0 @@
import { ICompanyRepository } from '../../../Repositories/interfaces/ICompanyRepository';
import { CompanyResponseDto } from '../../../Database/dto/company.dto';
import {
CreateCompanyCommand,
UpdateCompanyCommand,
DeleteCompanyCommand
} from './CompanyCommand';
import { appLogger } from '../../../utils/logger';
export class CompanyCommandHandler {
constructor(private readonly companyRepository: ICompanyRepository) {
appLogger.info('CompanyCommandHandler initialized');
}
// Handle create company
async handleCreate(command: CreateCompanyCommand): Promise<CompanyResponseDto> {
try {
const { companyCreateDto } = command;
appLogger.info('Creating company', {
name: companyCreateDto.Name,
contactEmail: companyCreateDto.ContactEmail,
action: 'create_company'
});
// Validate required fields
if (!companyCreateDto.Name || !companyCreateDto.ContactFirstName ||
!companyCreateDto.ContactLastName || !companyCreateDto.ContactEmail ||
!companyCreateDto.FirstAPI || !companyCreateDto.TokenAPI) {
throw new Error('All company fields are required: Name, ContactFirstName, ContactLastName, ContactEmail, FirstAPI, TokenAPI');
}
const result = await this.companyRepository.create(companyCreateDto);
appLogger.info('Company created successfully', {
companyId: result.CompanyId,
name: result.Name,
contactEmail: result.ContactEmail
});
return result;
} catch (error: any) {
appLogger.errorEvent('Error creating company', 'company_creation', {
error: error.message,
companyData: {
name: command.companyCreateDto?.Name,
contactEmail: command.companyCreateDto?.ContactEmail
}
});
throw new Error(`Failed to create company: ${error.message}`);
}
}
// Handle update company
async handleUpdate(command: UpdateCompanyCommand): Promise<CompanyResponseDto | null> {
try {
const { id, companyUpdateDto } = command;
appLogger.info('Updating company', {
companyId: id,
updateFields: Object.keys(companyUpdateDto),
action: 'update_company'
});
const result = await this.companyRepository.update(id, companyUpdateDto);
if (!result) {
appLogger.warn('Company not found for update', {
companyId: id
});
throw new Error(`Company with ID ${id} not found`);
}
appLogger.info('Company updated successfully', {
companyId: result.CompanyId,
name: result.Name,
updatedFields: Object.keys(companyUpdateDto)
});
return result;
} catch (error: any) {
appLogger.errorEvent('Error updating company', 'company_update', {
error: error.message,
companyId: command.id,
updateData: command.companyUpdateDto
});
throw new Error(`Failed to update company: ${error.message}`);
}
}
// Handle delete company
async handleDelete(command: DeleteCompanyCommand): Promise<boolean> {
try {
const { id } = command;
appLogger.info('Deleting company', {
companyId: id,
action: 'delete_company'
});
const result = await this.companyRepository.deleteById(id);
if (!result) {
appLogger.warn('Company not found for deletion', {
companyId: id
});
throw new Error(`Company with ID ${id} not found`);
}
appLogger.info('Company deleted successfully', {
companyId: id
});
return result;
} catch (error: any) {
appLogger.errorEvent('Error deleting company', 'company_deletion', {
error: error.message,
companyId: command.id
});
throw new Error(`Failed to delete company: ${error.message}`);
}
}
}
@@ -1,26 +0,0 @@
// Company Query Classes
// Get company by ID
export class GetCompanyByIdQuery {
constructor(public readonly id: number) {}
}
// Get company by name
export class GetCompanyByNameQuery {
constructor(public readonly name: string) {}
}
// Get all companies
export class GetAllCompaniesQuery {
constructor() {}
}
// Get basic company by ID
export class GetBasicCompanyByIdQuery {
constructor(public readonly id: number) {}
}
// Get all basic companies
export class GetAllBasicCompaniesQuery {
constructor() {}
}
@@ -1,60 +0,0 @@
import { ICompanyRepository } from '../../../Repositories/interfaces/ICompanyRepository';
import { CompanyQueryHandler } from './CompanyQueryHandler';
import {
GetCompanyByIdQuery,
GetCompanyByNameQuery,
GetAllCompaniesQuery,
GetBasicCompanyByIdQuery,
GetAllBasicCompaniesQuery
} from './CompanyQuery';
import { appLogger } from '../../../utils/logger';
export class CompanyQueryDispatcher {
private queryHandler: CompanyQueryHandler;
constructor(companyRepository: ICompanyRepository) {
appLogger.startup('Initializing CompanyQueryDispatcher...');
this.queryHandler = new CompanyQueryHandler(companyRepository);
appLogger.startup('CompanyQueryDispatcher initialized');
}
async dispatch(query: any): Promise<any> {
try {
appLogger.info('Dispatching company query', {
queryType: query.constructor.name,
action: 'dispatch_company_query'
});
switch (query.constructor) {
case GetCompanyByIdQuery:
return await this.queryHandler.handleGetById(query);
case GetCompanyByNameQuery:
return await this.queryHandler.handleGetByName(query);
case GetAllCompaniesQuery:
return await this.queryHandler.handleGetAll(query);
case GetBasicCompanyByIdQuery:
return await this.queryHandler.handleGetBasicById(query);
case GetAllBasicCompaniesQuery:
return await this.queryHandler.handleGetAllBasic(query);
default:
const errorMessage = `Unknown query type: ${query.constructor.name}`;
appLogger.errorEvent('Unknown company query type', 'dispatch_error', {
queryType: query.constructor.name
});
throw new Error(errorMessage);
}
} catch (error: any) {
appLogger.errorEvent('Error dispatching company query', 'dispatch_error', {
error: error.message,
queryType: query.constructor.name
});
throw error;
}
}
}
@@ -1,176 +0,0 @@
import { ICompanyRepository } from '../../../Repositories/interfaces/ICompanyRepository';
import { CompanyResponseDto, CompanyBasicDto } from '../../../Database/dto/company.dto';
import {
GetCompanyByIdQuery,
GetCompanyByNameQuery,
GetAllCompaniesQuery,
GetBasicCompanyByIdQuery,
GetAllBasicCompaniesQuery
} from './CompanyQuery';
import { appLogger } from '../../../utils/logger';
export class CompanyQueryHandler {
constructor(private readonly companyRepository: ICompanyRepository) {
appLogger.info('CompanyQueryHandler initialized');
}
// Get company by ID
async handleGetById(query: GetCompanyByIdQuery): Promise<CompanyResponseDto | null> {
try {
appLogger.info('Getting company by ID', {
companyId: query.id,
action: 'get_company_by_id'
});
// Validate ID
if (!query.id || query.id <= 0) {
throw new Error('Valid company ID is required');
}
const result = await this.companyRepository.findById(query.id);
if (!result) {
appLogger.warn('Company not found', {
companyId: query.id
});
return null;
}
appLogger.info('Company found successfully', {
companyId: result.CompanyId,
name: result.Name
});
return result;
} catch (error: any) {
appLogger.errorEvent('Error getting company by ID', 'company_query', {
error: error.message,
companyId: query.id
});
throw new Error(`Failed to get company by ID: ${error.message}`);
}
}
// Get company by name
async handleGetByName(query: GetCompanyByNameQuery): Promise<CompanyResponseDto | null> {
try {
appLogger.info('Getting company by name', {
companyName: query.name,
action: 'get_company_by_name'
});
// Validate name
if (!query.name || query.name.trim() === '') {
throw new Error('Valid company name is required');
}
const result = await this.companyRepository.findByName(query.name.trim());
if (!result) {
appLogger.warn('Company not found by name', {
companyName: query.name
});
return null;
}
appLogger.info('Company found by name', {
companyId: result.CompanyId,
name: result.Name
});
return result;
} catch (error: any) {
appLogger.errorEvent('Error getting company by name', 'company_query', {
error: error.message,
companyName: query.name
});
throw new Error(`Failed to get company by name: ${error.message}`);
}
}
// Get all companies
async handleGetAll(query: GetAllCompaniesQuery): Promise<CompanyResponseDto[]> {
try {
appLogger.info('Getting all companies', {
action: 'get_all_companies'
});
const result = await this.companyRepository.findAll();
appLogger.info('Companies retrieved successfully', {
count: result.length
});
return result;
} catch (error: any) {
appLogger.errorEvent('Error getting all companies', 'company_query', {
error: error.message
});
throw new Error(`Failed to get all companies: ${error.message}`);
}
}
// Get basic company by ID
async handleGetBasicById(query: GetBasicCompanyByIdQuery): Promise<CompanyBasicDto | null> {
try {
appLogger.info('Getting basic company by ID', {
companyId: query.id,
action: 'get_basic_company_by_id'
});
// Validate ID
if (!query.id || query.id <= 0) {
throw new Error('Valid company ID is required');
}
const result = await this.companyRepository.findBasicById(query.id);
if (!result) {
appLogger.warn('Basic company not found', {
companyId: query.id
});
return null;
}
appLogger.info('Basic company found successfully', {
companyId: result.CompanyId,
name: result.Name
});
return result;
} catch (error: any) {
appLogger.errorEvent('Error getting basic company by ID', 'company_query', {
error: error.message,
companyId: query.id
});
throw new Error(`Failed to get basic company by ID: ${error.message}`);
}
}
// Get all basic companies
async handleGetAllBasic(query: GetAllBasicCompaniesQuery): Promise<CompanyBasicDto[]> {
try {
appLogger.info('Getting all basic companies', {
action: 'get_all_basic_companies'
});
const result = await this.companyRepository.findAllBasic();
appLogger.info('Basic companies retrieved successfully', {
count: result.length
});
return result;
} catch (error: any) {
appLogger.errorEvent('Error getting all basic companies', 'company_query', {
error: error.message
});
throw new Error(`Failed to get all basic companies: ${error.message}`);
}
}
}
@@ -1,46 +0,0 @@
import { UserCreateDto, UserUpdateDto } from '../../../Database/dto/user.dto';
// Create user command
export class CreateUserCommand {
constructor(public readonly userCreateDto: UserCreateDto) {}
}
// Update user command
export class UpdateUserCommand {
constructor(
public readonly id: number,
public readonly userUpdateDto: UserUpdateDto
) {}
}
// Delete user command
export class DeleteUserCommand {
constructor(public readonly id: number) {}
}
// Delete multiple users command
export class DeleteUsersCommand {
constructor(public readonly ids: number[]) {}
}
// Create multiple users command
export class CreateUsersCommand {
constructor(public readonly userCreateDtos: UserCreateDto[]) {}
}
// Update user password command
export class UpdateUserPasswordCommand {
constructor(
public readonly userId: number,
public readonly currentPassword: string,
public readonly newPassword: string
) {}
}
// Reset user password command (admin only)
export class ResetUserPasswordCommand {
constructor(
public readonly userId: number,
public readonly newPassword: string
) {}
}
@@ -1,47 +0,0 @@
import { UserCommandHandler } from './UserCommandHandler';
import { IUserRepository } from '../../../Repositories/interfaces/IUserRepository';
import {
CreateUserCommand,
UpdateUserCommand,
DeleteUserCommand,
DeleteUsersCommand,
CreateUsersCommand,
UpdateUserPasswordCommand,
ResetUserPasswordCommand
} from './UserCommand';
export class UserCommandDispatcher {
private commandHandler: UserCommandHandler;
constructor(userRepository: IUserRepository) {
this.commandHandler = new UserCommandHandler(userRepository);
}
async dispatch(command: any): Promise<any> {
switch (command.constructor) {
case CreateUserCommand:
return await this.commandHandler.handleCreate(command);
case UpdateUserCommand:
return await this.commandHandler.handleUpdate(command);
case DeleteUserCommand:
return await this.commandHandler.handleDelete(command);
case DeleteUsersCommand:
return await this.commandHandler.handleDeleteUsers(command);
case CreateUsersCommand:
return await this.commandHandler.handleCreateUsers(command);
case UpdateUserPasswordCommand:
return await this.commandHandler.handleUpdatePassword(command);
case ResetUserPasswordCommand:
return await this.commandHandler.handleResetPassword(command);
default:
throw new Error(`Unknown command type: ${command.constructor.name}`);
}
}
}
@@ -1,205 +0,0 @@
import { IUserRepository } from '../../../Repositories/interfaces/IUserRepository';
import { UserResponseDto, UsersListResponseDto, UserCreateDto, UserUpdateDto } from '../../../Database/dto/user.dto';
import { hashPassword, comparePasswords } from '../../../middlewares/security';
import {
CreateUserCommand,
UpdateUserCommand,
DeleteUserCommand,
DeleteUsersCommand,
CreateUsersCommand,
UpdateUserPasswordCommand,
ResetUserPasswordCommand
} from './UserCommand';
export class UserCommandHandler {
constructor(private readonly userRepository: IUserRepository) {}
// Create user
async handleCreate(command: CreateUserCommand): Promise<UserResponseDto> {
// Check if username already exists
const usernameExists = await this.userRepository.usernameExists(command.userCreateDto.username);
if (usernameExists) {
throw new Error('Username already exists');
}
// Check if email already exists
const emailExists = await this.userRepository.emailExists(command.userCreateDto.email);
if (emailExists) {
throw new Error('Email already exists');
}
console.log('Creating user with DTO:', command.userCreateDto);
// Hash password using security middleware
const hashedPassword = await hashPassword(command.userCreateDto.password);
console.log('Hashed password:', hashedPassword);
const userWithHashedPassword = new UserCreateDto(
command.userCreateDto.username,
command.userCreateDto.FirstName,
command.userCreateDto.LastName,
command.userCreateDto.email,
hashedPassword,
command.userCreateDto.CompanyId // Pass the optional CompanyId
);
console.log('Creating user with hashed password:', userWithHashedPassword);
return await this.userRepository.create(userWithHashedPassword);
}
// Update user
async handleUpdate(command: UpdateUserCommand): Promise<UserResponseDto | null> {
// Check if user exists
const userExists = await this.userRepository.exists(command.id);
if (!userExists) {
throw new Error('User not found');
}
// If updating username, check if it's already taken by another user
if (command.userUpdateDto.username) {
const existingUser = await this.userRepository.findByUsername(command.userUpdateDto.username);
if (existingUser && existingUser.id !== command.id) {
throw new Error('Username already exists');
}
}
// If updating email, check if it's already taken by another user
if (command.userUpdateDto.email) {
const existingUser = await this.userRepository.findByEmail(command.userUpdateDto.email);
if (existingUser && existingUser.id !== command.id) {
throw new Error('Email already exists');
}
}
// Create new DTO with hashed password if password is being updated
let updateDto = command.userUpdateDto;
if (command.userUpdateDto.password) {
const hashedPassword = await hashPassword(command.userUpdateDto.password);
updateDto = new UserUpdateDto({
...command.userUpdateDto,
password: hashedPassword
});
}
return await this.userRepository.update(command.id, updateDto);
}
// Delete user
async handleDelete(command: DeleteUserCommand): Promise<boolean> {
const userExists = await this.userRepository.exists(command.id);
if (!userExists) {
throw new Error('User not found');
}
return await this.userRepository.deleteById(command.id);
}
// Delete multiple users
async handleDeleteUsers(command: DeleteUsersCommand): Promise<boolean> {
if (command.ids.length === 0) {
throw new Error('No user IDs provided');
}
// Check if all users exist
for (const id of command.ids) {
const userExists = await this.userRepository.exists(id);
if (!userExists) {
throw new Error(`User with ID ${id} not found`);
}
}
return await this.userRepository.deleteByIds(command.ids);
}
// Create multiple users
async handleCreateUsers(command: CreateUsersCommand): Promise<UsersListResponseDto> {
if (command.userCreateDtos.length === 0) {
throw new Error('No users provided');
}
// Check for duplicate usernames and emails within the batch
const usernames = command.userCreateDtos.map(dto => dto.username);
const emails = command.userCreateDtos.map(dto => dto.email);
const uniqueUsernames = new Set(usernames);
const uniqueEmails = new Set(emails);
if (uniqueUsernames.size !== usernames.length) {
throw new Error('Duplicate usernames in batch');
}
if (uniqueEmails.size !== emails.length) {
throw new Error('Duplicate emails in batch');
}
// Check if any usernames or emails already exist
for (const dto of command.userCreateDtos) {
const usernameExists = await this.userRepository.usernameExists(dto.username);
if (usernameExists) {
throw new Error(`Username ${dto.username} already exists`);
}
const emailExists = await this.userRepository.emailExists(dto.email);
if (emailExists) {
throw new Error(`Email ${dto.email} already exists`);
}
}
// Hash all passwords using security middleware
const usersWithHashedPasswords = await Promise.all(
command.userCreateDtos.map(async (dto) => {
const hashedPassword = await hashPassword(dto.password);
return new UserCreateDto(
dto.username,
dto.FirstName,
dto.LastName,
dto.email,
hashedPassword
);
})
);
return await this.userRepository.createMany(usersWithHashedPasswords);
}
// Update user password (with current password verification)
async handleUpdatePassword(command: UpdateUserPasswordCommand): Promise<UserResponseDto | null> {
// Get raw user entity for password verification
const user = await this.userRepository.findRawById(command.userId);
if (!user) {
throw new Error('User not found');
}
// Verify current password using security middleware
const isCurrentPasswordValid = await comparePasswords(command.currentPassword, user.password);
if (!isCurrentPasswordValid) {
throw new Error('Current password is incorrect');
}
// Hash new password using security middleware
const hashedNewPassword = await hashPassword(command.newPassword);
const updateDto = new UserUpdateDto({
password: hashedNewPassword
});
return await this.userRepository.update(command.userId, updateDto);
}
// Reset user password (admin only - no current password verification)
async handleResetPassword(command: ResetUserPasswordCommand): Promise<UserResponseDto | null> {
const userExists = await this.userRepository.exists(command.userId);
if (!userExists) {
throw new Error('User not found');
}
// Hash new password using security middleware
const hashedNewPassword = await hashPassword(command.newPassword);
const updateDto = new UserUpdateDto({
password: hashedNewPassword
});
return await this.userRepository.update(command.userId, updateDto);
}
}
@@ -1,122 +0,0 @@
import { UserBasicDto, UserResponseDto, UsersListResponseDto } from '../../../Database/dto/user.dto';
// Get user by ID
export class GetUserByIdQuery {
constructor(public readonly id: number) {}
}
// Get user by username
export class GetUserByUsernameQuery {
constructor(public readonly username: string) {}
}
// Get user by email
export class GetUserByEmailQuery {
constructor(public readonly email: string) {}
}
// Get all users
export class GetAllUsersQuery {
constructor() {}
}
// Get users by company
// export class GetUsersByCompanyQuery {
// constructor(public readonly companyId: number) {}
// }
// Get users by company token
// export class GetUsersByCompanyTokenQuery {
// constructor(public readonly companyToken: string) {}
// }
// Search users by partial username
export class SearchUsersByUsernameQuery {
constructor(public readonly partialUsername: string) {}
}
// Search users by partial name
// export class SearchUsersByNameQuery {
// constructor(public readonly partialName: string) {}
// }
// Get users with pagination
// export class GetUsersWithPaginationQuery {
// constructor(
// public readonly page: number,
// public readonly limit: number
// ) {}
// }
// Get basic user info by ID
export class GetUserBasicByIdQuery {
constructor(public readonly id: number) {}
}
// Get all users basic info
export class GetAllUsersBasicQuery {
constructor() {}
}
// Get user with company relation
export class GetUserWithCompanyQuery {
constructor(public readonly id: number) {}
}
// Get user with question banks relation
export class GetUserWithQuestionBanksQuery {
constructor(public readonly id: number) {}
}
// Get user with all relations
// export class GetUserWithAllRelationsQuery {
// constructor(public readonly id: number) {}
// }
// Get users count
// export class GetUsersCountQuery {
// constructor() {}
// }
// Get users count by company
// export class GetUsersCountByCompanyQuery {
// constructor(public readonly companyId: number) {}
// }
// Get users by date range
// export class GetUsersByDateRangeQuery {
// constructor(
// public readonly startDate: Date,
// public readonly endDate: Date
// ) {}
// }
// Check if user exists
export class UserExistsQuery {
constructor(public readonly id: number) {}
}
// Check if username exists
export class UsernameExistsQuery {
constructor(public readonly username: string) {}
}
// Check if email exists
export class EmailExistsQuery {
constructor(public readonly email: string) {}
}
// Get raw user by username (for authentication)
export class GetRawUserByUsernameQuery {
constructor(public readonly username: string) {}
}
// Get raw user by email (for authentication)
export class GetRawUserByEmailQuery {
constructor(public readonly email: string) {}
}
// Get raw user by ID (for authentication)
// export class GetRawUserByIdQuery {
// constructor(public readonly id: number) {}
// }
@@ -1,111 +0,0 @@
import { UserQueryHandler } from './UserQueryHandler';
import { IUserRepository } from '../../../Repositories/interfaces/IUserRepository';
import {
GetUserByIdQuery,
GetUserByUsernameQuery,
GetUserByEmailQuery,
GetAllUsersQuery,
// GetUsersByCompanyQuery,
// GetUsersByCompanyTokenQuery,
SearchUsersByUsernameQuery,
// SearchUsersByNameQuery,
// GetUsersWithPaginationQuery,
GetUserBasicByIdQuery,
GetAllUsersBasicQuery,
GetUserWithCompanyQuery,
GetUserWithQuestionBanksQuery,
// GetUserWithAllRelationsQuery,
// GetUsersCountQuery,
// GetUsersCountByCompanyQuery,
// GetUsersByDateRangeQuery,
UserExistsQuery,
UsernameExistsQuery,
EmailExistsQuery,
GetRawUserByUsernameQuery,
GetRawUserByEmailQuery,
// GetRawUserByIdQuery
} from './UserQuery';
export class UserQueryDispatcher {
private queryHandler: UserQueryHandler;
constructor(userRepository: IUserRepository) {
this.queryHandler = new UserQueryHandler(userRepository);
}
async dispatch(query: any): Promise<any> {
switch (query.constructor) {
case GetUserByIdQuery:
return await this.queryHandler.handle(query);
// case GetUserByUsernameQuery:
// return await this.queryHandler.handleGetByUsername(query);
case GetUserByEmailQuery:
return await this.queryHandler.handleGetByEmail(query);
case GetAllUsersQuery:
return await this.queryHandler.handleGetAll(query);
// case GetUsersByCompanyQuery:
// return await this.queryHandler.handleGetByCompany(query);
// case GetUsersByCompanyTokenQuery:
// return await this.queryHandler.handleGetByCompanyToken(query);
// case SearchUsersByUsernameQuery:
// return await this.queryHandler.handleSearchByUsername(query);
// case SearchUsersByNameQuery:
// return await this.queryHandler.handleSearchByName(query);
// case GetUsersWithPaginationQuery:
// return await this.queryHandler.handleGetWithPagination(query);
// case GetUserBasicByIdQuery:
// return await this.queryHandler.handleGetBasicById(query);
case GetAllUsersBasicQuery:
return await this.queryHandler.handleGetAllBasic(query);
case GetUserWithCompanyQuery:
return await this.queryHandler.handleGetWithCompany(query);
case GetUserWithQuestionBanksQuery:
return await this.queryHandler.handleGetWithQuestionBanks(query);
// case GetUserWithAllRelationsQuery:
// return await this.queryHandler.handleGetWithAllRelations(query);
// case GetUsersCountQuery:
// return await this.queryHandler.handleGetCount(query);
// case GetUsersCountByCompanyQuery:
// return await this.queryHandler.handleGetCountByCompany(query);
// case GetUsersByDateRangeQuery:
// return await this.queryHandler.handleGetByDateRange(query);
// case UserExistsQuery:
// return await this.queryHandler.handleUserExists(query);
case UsernameExistsQuery:
return await this.queryHandler.handleUsernameExists(query);
case EmailExistsQuery:
return await this.queryHandler.handleEmailExists(query);
case GetRawUserByUsernameQuery:
return await this.queryHandler.handleGetRawByUsername(query);
case GetRawUserByEmailQuery:
return await this.queryHandler.handleGetRawByEmail(query);
// case GetRawUserByIdQuery:
// return await this.queryHandler.handleGetRawById(query);
default:
throw new Error(`Unknown query type: ${query.constructor.name}`);
}
}
}
@@ -1,148 +0,0 @@
import { IUserRepository } from '../../../Repositories/interfaces/IUserRepository';
import { UserBasicDto, UserResponseDto, UsersListResponseDto } from '../../../Database/dto/user.dto';
import { User } from '../../../Database/entities/user.entity';
import {
GetUserByIdQuery,
GetUserByUsernameQuery,
GetUserByEmailQuery,
GetAllUsersQuery,
// GetUsersByCompanyQuery,
// GetUsersByCompanyTokenQuery,
SearchUsersByUsernameQuery,
// SearchUsersByNameQuery,
// GetUsersWithPaginationQuery,
GetUserBasicByIdQuery,
GetAllUsersBasicQuery,
GetUserWithCompanyQuery,
GetUserWithQuestionBanksQuery,
// GetUserWithAllRelationsQuery,
// GetUsersCountQuery,
// GetUsersCountByCompanyQuery,
// GetUsersByDateRangeQuery,
UserExistsQuery,
UsernameExistsQuery,
EmailExistsQuery,
GetRawUserByUsernameQuery,
GetRawUserByEmailQuery,
// GetRawUserByIdQuery
} from './UserQuery';
export class UserQueryHandler {
constructor(private readonly userRepository: IUserRepository) {}
// Get user by ID
async handle(query: GetUserByIdQuery): Promise<UserResponseDto | null> {
return await this.userRepository.findById(query.id);
}
// Get user by username
// async handleGetByUsername(query: GetUserByUsernameQuery): Promise<UserResponseDto | null> {
// return await this.userRepository.findByUsername(query.username);
// }
// Get user by email
async handleGetByEmail(query: GetUserByEmailQuery): Promise<UserResponseDto | null> {
return await this.userRepository.findByEmail(query.email);
}
// Get all users
async handleGetAll(query: GetAllUsersQuery): Promise<UsersListResponseDto> {
return await this.userRepository.findAll();
}
// Get users by company
// async handleGetByCompany(query: GetUsersByCompanyQuery): Promise<UsersListResponseDto> {
// return await this.userRepository.findByCompanyId(query.companyId);
// }
// Get users by company token
// async handleGetByCompanyToken(query: GetUsersByCompanyTokenQuery): Promise<UsersListResponseDto> {
// return await this.userRepository.findByCompanyToken(query.companyToken);
// }
// Search users by partial username
// async handleSearchByUsername(query: SearchUsersByUsernameQuery): Promise<UsersListResponseDto> {
// return await this.userRepository.findByPartialUsername(query.partialUsername);
// }
// Search users by partial name
// async handleSearchByName(query: SearchUsersByNameQuery): Promise<UsersListResponseDto> {
// return await this.userRepository.findByPartialName(query.partialName);
// }
// Get users with pagination
// async handleGetWithPagination(query: GetUsersWithPaginationQuery): Promise<{ users: UsersListResponseDto, total: number }> {
// const skip = (query.page - 1) * query.limit;
// return await this.userRepository.findWithPagination(skip, query.limit);
// }
// Get basic user info by ID
// async handleGetBasicById(query: GetUserBasicByIdQuery): Promise<UserBasicDto | null> {
// return await this.userRepository.findBasicById(query.id);
// }
// Get all users basic info
async handleGetAllBasic(query: GetAllUsersBasicQuery): Promise<UserBasicDto[]> {
return await this.userRepository.findAllBasic();
}
// Get user with company relation
async handleGetWithCompany(query: GetUserWithCompanyQuery): Promise<UserResponseDto | null> {
return await this.userRepository.findWithCompany(query.id);
}
// Get user with question banks relation
async handleGetWithQuestionBanks(query: GetUserWithQuestionBanksQuery): Promise<UserResponseDto | null> {
return await this.userRepository.findWithQuestionBanks(query.id);
}
// Get user with all relations
// async handleGetWithAllRelations(query: GetUserWithAllRelationsQuery): Promise<UserResponseDto | null> {
// return await this.userRepository.findWithAllRelations(query.id);
// }
// Get users count
// async handleGetCount(query: GetUsersCountQuery): Promise<number> {
// return await this.userRepository.count();
// }
// Get users count by company
// async handleGetCountByCompany(query: GetUsersCountByCompanyQuery): Promise<number> {
// return await this.userRepository.countByCompany(query.companyId);
// }
// Get users by date range
// async handleGetByDateRange(query: GetUsersByDateRangeQuery): Promise<UsersListResponseDto> {
// return await this.userRepository.findByDateRange(query.startDate, query.endDate);
// }
// Check if user exists
async handleUserExists(query: UserExistsQuery): Promise<boolean> {
return await this.userRepository.exists(query.id);
}
// Check if username exists
async handleUsernameExists(query: UsernameExistsQuery): Promise<boolean> {
return await this.userRepository.usernameExists(query.username);
}
// Check if email exists
async handleEmailExists(query: EmailExistsQuery): Promise<boolean> {
return await this.userRepository.emailExists(query.email);
}
// Get raw user by username (for password verification)
async handleGetRawByUsername(query: GetRawUserByUsernameQuery): Promise<User | null> {
return await this.userRepository.findRawByUsername(query.username);
}
// Get raw user by email (for password verification)
async handleGetRawByEmail(query: GetRawUserByEmailQuery): Promise<User | null> {
return await this.userRepository.findRawByEmail(query.email);
}
// Get raw user by ID (for password verification)
// async handleGetRawById(query: GetRawUserByIdQuery): Promise<User | null> {
// return await this.userRepository.findRawById(query.id);
// }
}
-206
View File
@@ -1,206 +0,0 @@
import express, { Application, Request, Response, NextFunction } from 'express';
import cors from 'cors';
import cookieParser from 'cookie-parser';
import helmet from 'helmet';
import compression from 'compression';
import dotenv from 'dotenv';
import { AppDataSource } from '../ormconfig';
import { authErrorHandler } from './middlewares/authErrorHandler';
import createUserRouter from './Routers/UserRouter';
import { appLogger, initializeMinIOBucket } from './utils/logger';
// Load environment variables
dotenv.config();
class App {
public readonly app: Application = express();
private readonly port: number = parseInt(process.env.PORT || '3000');
constructor() {
// Initialize application asynchronously
this.initializeApplication().catch((error) => {
appLogger.errorEvent('Application startup failed', 'startup', { error });
process.exit(1);
});
}
private async initializeApplication(): Promise<void> {
appLogger.startup('🚀 Starting SerpentRace Backend');
try {
// Initialize core services
await this.initializeMinIO();
await this.initializeDatabase();
// Setup application
this.setupMiddlewares();
this.setupRoutes();
this.setupErrorHandling();
appLogger.startup('✅ Application ready');
} catch (error) {
appLogger.errorEvent('Initialization failed', 'startup', { error });
throw error;
}
}
private async initializeMinIO(): Promise<void> {
try {
await initializeMinIOBucket();
appLogger.startup('MinIO initialized');
} catch (error) {
appLogger.errorEvent('MinIO failed', 'minio', { error });
throw error;
}
}
private async initializeDatabase(): Promise<void> {
try {
await AppDataSource.initialize();
appLogger.startup('Database connected');
} catch (error) {
appLogger.errorEvent('Database failed', 'database', { error });
throw error;
}
}
private setupMiddlewares(): void {
// Security
this.app.use(helmet({
crossOriginEmbedderPolicy: false,
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", "data:", "https:"],
},
},
}));
// CORS
this.app.use(cors({
origin: process.env.FRONTEND_URL || 'http://localhost:3000',
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
}));
// Body parsing
this.app.use(express.json({ limit: '10mb' }));
this.app.use(express.urlencoded({ extended: true, limit: '10mb' }));
this.app.use(cookieParser());
this.app.use(compression());
appLogger.startup('Middlewares loaded');
}
private setupRoutes(): void {
// User routes - Mount on both paths for compatibility
const userRouter = createUserRouter(AppDataSource);
this.app.use('/user', userRouter);
// 404 handler
this.app.use('*', (req: Request, res: Response) => {
appLogger.security('Route not found', 'low', {
url: req.originalUrl,
method: req.method,
ip: req.ip,
userAgent: req.get('User-Agent')
});
res.status(404).json({
success: false,
message: 'Route not found'
});
});
appLogger.startup('Routes configured');
}
private setupErrorHandling(): void {
// Auth error handler
this.app.use(authErrorHandler);
// General error handler
this.app.use((err: any, req: Request, res: Response, next: NextFunction) => {
appLogger.errorEvent('Application error', 'express', {
message: err.message,
url: req.url,
method: req.method,
ip: req.ip
});
res.status(err.status || 500).json({
success: false,
message: err.message || 'Internal Server Error',
...(process.env.NODE_ENV !== 'production' && { stack: err.stack })
});
});
// Process error handlers (set only once)
if (!process.listenerCount('unhandledRejection')) {
process.on('unhandledRejection', (reason: any) => {
appLogger.errorEvent('Unhandled rejection', 'process', { reason });
this.shutdown('Unhandled Rejection');
});
}
if (!process.listenerCount('uncaughtException')) {
process.on('uncaughtException', (error: Error) => {
appLogger.errorEvent('Uncaught exception', 'process', { error: error.message });
this.shutdown('Uncaught Exception');
});
}
if (!process.listenerCount('SIGTERM')) {
process.on('SIGTERM', () => this.shutdown('SIGTERM'));
}
if (!process.listenerCount('SIGINT')) {
process.on('SIGINT', () => this.shutdown('SIGINT'));
}
appLogger.startup('Error handling configured');
}
private async shutdown(signal: string): Promise<void> {
appLogger.system(`Shutting down (${signal})`);
try {
if (AppDataSource.isInitialized) {
await AppDataSource.destroy();
}
appLogger.system('Shutdown complete');
process.exit(0);
} catch (error) {
appLogger.errorEvent('Shutdown error', 'shutdown', { error });
process.exit(1);
}
}
public listen(): void {
this.app.listen(this.port, () => {
appLogger.startup(`🎉 Server running on port ${this.port}`, {
port: this.port,
environment: process.env.NODE_ENV || 'development',
urls: {
users: `http://localhost:${this.port}/api/user`,
}
});
console.log(`
🚀 SerpentRace Backend Server
📡 Port: ${this.port}
🔗 Users: http://localhost:${this.port}/user
`);
});
}
}
// Create and start application
const app = new App();
app.listen();
export default app.app;
@@ -1,20 +0,0 @@
import { Request, Response, NextFunction } from 'express';
import { clearAuthCookie } from './authentication';
// Middleware to handle authentication errors globally
export function authErrorHandler(err: any, req: Request, res: Response, next: NextFunction): void {
// Check if it's an authentication-related error
if (err.name === 'UnauthorizedError' || err.status === 401) {
clearAuthCookie(res);
res.status(401).json({
success: false,
message: 'Authentication failed',
autoLogout: true
});
return;
}
// Pass other errors to the next error handler
next(err);
}
@@ -48,12 +48,27 @@ export function auth(req: Request, res: Response, next: NextFunction): void {
// Create UserBasicDto from token payload
const userDto = new UserBasicDto(
decoded.id,
decoded.guid,
decoded.username,
decoded.CompanyId
decoded.status,
decoded.authLevel,
decoded.CompanyId,
decoded.companyRegistered
);
req.user = userDto;
// Check company registration status if user has a company
if (userDto.CompanyId && !userDto.companyRegistered) {
res.status(403).json({
message: "Company registration required",
requiresCompanyRegistration: true,
companyId: userDto.CompanyId,
redirectUrl: `${process.env.FRONTEND_URL}/company/register?companyId=${userDto.CompanyId}`
});
return;
}
// Check if expiring soon & refresh if needed
const now = Math.floor(Date.now() / 1000);
if (decoded.exp && decoded.exp - now < 60 * 5) {
@@ -93,8 +108,11 @@ export function auth(req: Request, res: Response, next: NextFunction): void {
export function createToken(user: UserBasicDto): string {
return jwt.sign(
{
id: user.id,
id: user.id,
guid: user.guid,
username: user.username,
status: user.status,
authLevel: user.authLevel,
CompanyId: user.CompanyId
},
process.env.JWT_SECRET || 'secret',
@@ -107,12 +125,18 @@ export function createToken(user: UserBasicDto): string {
// Helper function to create token from user response data
export function createTokenFromUserResponse(userData: {
id: number;
guid: string;
username: string;
status: number;
authLevel: number;
CompanyId?: number;
}): string {
const userDto = new UserBasicDto(
userData.id,
userData.guid,
userData.username,
userData.status,
userData.authLevel,
userData.CompanyId
);
-313
View File
@@ -1,313 +0,0 @@
import winston from 'winston';
import AWS from 'aws-sdk';
import path from 'path';
import fs from 'fs';
// MinIO Configuration
const minioConfig = {
endpoint: process.env.MINIO_ENDPOINT || 'http://127.0.0.1:9000',
accessKeyId: process.env.MINIO_ACCESS_KEY || 'minioadmin',
secretAccessKey: process.env.MINIO_SECRET_KEY || 'minioadmin',
bucket: process.env.MINIO_BUCKET || 'logs',
region: 'us-east-1'
};
// Initialize S3 client for MinIO (single instance)
const s3Client = new AWS.S3({
endpoint: minioConfig.endpoint,
accessKeyId: minioConfig.accessKeyId,
secretAccessKey: minioConfig.secretAccessKey,
s3ForcePathStyle: true,
signatureVersion: 'v4',
region: minioConfig.region
});
// Ensure logs directory exists (single initialization)
const logsDir = path.join(process.cwd(), 'logs');
if (!fs.existsSync(logsDir)) {
fs.mkdirSync(logsDir, { recursive: true });
}
// Logging formats (single definition)
const simpleFormat = winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.errors({ stack: true }),
winston.format.printf(({ timestamp, level, message, category, ...meta }) => {
let output = `${timestamp} [${level.toUpperCase()}]`;
if (category) output += ` [${category}]`;
output += `: ${message}`;
if (Object.keys(meta).length > 0) {
output += ` ${JSON.stringify(meta)}`;
}
return output;
})
);
const fileFormat = winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
);
// Create Winston logger (single instance)
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: fileFormat,
defaultMeta: {
service: 'serpentrace-backend',
environment: process.env.NODE_ENV || 'development'
},
transports: [
new winston.transports.File({
filename: path.join(logsDir, 'error.log'),
level: 'error',
maxsize: 5242880, // 5MB
maxFiles: 5
}),
new winston.transports.File({
filename: path.join(logsDir, 'app.log'),
maxsize: 5242880, // 5MB
maxFiles: 5
})
]
});
// Add console transport for development (single initialization)
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: simpleFormat
}));
}
// S3 upload utility (single definition)
const uploadLogToS3 = async (logData: string, filename: string): Promise<void> => {
try {
const key = `serpentrace-backend/${new Date().toISOString().split('T')[0]}/${filename}`;
await s3Client.upload({
Bucket: minioConfig.bucket,
Key: key,
Body: logData,
ContentType: 'text/plain'
}).promise();
console.log(`✅ Log uploaded to MinIO: ${key}`);
} catch (error) {
console.error('❌ Failed to upload log to MinIO:', error);
}
};
// Initialize MinIO bucket (single function)
const initializeMinIOBucket = async (): Promise<void> => {
try {
await s3Client.headBucket({ Bucket: minioConfig.bucket }).promise();
console.log(`✅ MinIO bucket '${minioConfig.bucket}' is accessible`);
} catch (error: any) {
if (error.statusCode === 404) {
try {
await s3Client.createBucket({ Bucket: minioConfig.bucket }).promise();
console.log(`✅ Created MinIO bucket '${minioConfig.bucket}'`);
} catch (createError) {
console.error(`❌ Failed to create MinIO bucket:`, createError);
}
}
}
};
// Enhanced application logger with event categories
export class AppLogger {
private winston: winston.Logger;
constructor(winstonLogger: winston.Logger) {
this.winston = winstonLogger;
}
// Standard log levels
error(message: string, meta?: any): void {
this.winston.error(message, meta);
}
warn(message: string, meta?: any): void {
this.winston.warn(message, meta);
}
info(message: string, meta?: any): void {
this.winston.info(message, meta);
}
debug(message: string, meta?: any): void {
this.winston.debug(message, meta);
}
// STARTUP EVENTS
startup(message: string, meta?: any): void {
this.winston.info(message, {
category: 'STARTUP',
...meta
});
}
// SYSTEM EVENTS
system(message: string, meta?: any): void {
this.winston.info(message, {
category: 'SYSTEM',
...meta
});
}
// DATABASE EVENTS
database(operation: string, table?: string, duration?: number, meta?: any): void {
this.winston.info(`Database: ${operation}`, {
category: 'DATABASE',
operation,
table,
duration: duration ? `${duration}ms` : undefined,
...meta
});
}
// AUTHENTICATION EVENTS
auth(action: string, success: boolean, meta?: any): void {
this.winston.info(`Auth: ${action}`, {
category: 'AUTH',
action,
success,
...meta
});
}
// API REQUEST EVENTS
request(req: any, res: any, duration?: number): void {
this.winston.info(`Request: ${req.method} ${req.originalUrl || req.url}`, {
category: 'API',
method: req.method,
url: req.originalUrl || req.url,
statusCode: res.statusCode,
ip: req.ip,
duration: duration ? `${duration}ms` : undefined,
userId: req.user?.id
});
}
// SECURITY EVENTS
security(event: string, severity: 'low' | 'medium' | 'high' | 'critical', meta?: any): void {
const logLevel = severity === 'critical' || severity === 'high' ? 'error' : 'warn';
this[logLevel](`Security: ${event}`, {
category: 'SECURITY',
event,
severity,
...meta
});
}
// BUSINESS LOGIC EVENTS
business(action: string, entity: string, meta?: any): void {
this.winston.info(`Business: ${action} ${entity}`, {
category: 'BUSINESS',
action,
entity,
...meta
});
}
// ERROR EVENTS
errorEvent(error: string, context: string, meta?: any): void {
this.winston.error(`Error in ${context}: ${error}`, {
category: 'ERROR',
context,
error,
...meta
});
}
// PERFORMANCE EVENTS
performance(metric: string, value: number, unit: string, meta?: any): void {
this.winston.info(`Performance: ${metric}`, {
category: 'PERFORMANCE',
metric,
value,
unit,
...meta
});
}
// CONFIGURATION EVENTS
config(component: string, status: 'loaded' | 'failed' | 'updated', meta?: any): void {
this.winston.info(`Config: ${component} ${status}`, {
category: 'CONFIG',
component,
status,
...meta
});
}
// MIDDLEWARE EVENTS
middleware(name: string, action: string, meta?: any): void {
this.winston.info(`Middleware: ${name} ${action}`, {
category: 'MIDDLEWARE',
name,
action,
...meta
});
}
// ROUTE EVENTS
route(path: string, method: string, action: string, meta?: any): void {
this.winston.info(`Route: ${method} ${path} ${action}`, {
category: 'ROUTE',
path,
method,
action,
...meta
});
}
// Upload current log file to MinIO
async uploadLogs(): Promise<void> {
try {
const logFile = path.join(logsDir, 'app.log');
if (fs.existsSync(logFile)) {
const logData = fs.readFileSync(logFile, 'utf8');
const filename = `app-${Date.now()}.log`;
await uploadLogToS3(logData, filename);
}
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
this.error('Failed to upload logs', { error: errorMessage });
}
}
}
// Create and export the app logger instance (single instance)
export const appLogger = new AppLogger(logger);
// Setup periodic log upload (single initialization)
let uploadInterval: NodeJS.Timeout | null = null;
if (process.env.NODE_ENV === 'production' && !uploadInterval) {
uploadInterval = setInterval(() => {
appLogger.uploadLogs();
}, 60 * 60 * 1000); // 1 hour
}
// Graceful shutdown handlers (single initialization)
let shutdownHandlersSet = false;
if (!shutdownHandlersSet) {
shutdownHandlersSet = true;
process.on('SIGTERM', () => {
appLogger.system('Application shutting down via SIGTERM');
appLogger.uploadLogs().finally(() => {
process.exit(0);
});
});
process.on('SIGINT', () => {
appLogger.system('Application interrupted via SIGINT');
appLogger.uploadLogs().finally(() => {
process.exit(0);
});
});
}
// Export utilities
export { logger, initializeMinIOBucket, uploadLogToS3 };
export default appLogger;