backend v4 half
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
// }
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user