chore: sync service
This commit is contained in:
@@ -1,39 +1,79 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
|
||||
export class QuotaStatisticsDto {
|
||||
@ApiProperty({ description: "Total number of email users", example: 150 })
|
||||
totalUsers: number;
|
||||
|
||||
@ApiProperty({ description: "Total quota used by all users in bytes", example: 5368709120 })
|
||||
totalQuotaUsed: number;
|
||||
|
||||
@ApiProperty({ description: "Total quota allowed for all users in bytes", example: 21474836480 })
|
||||
totalQuotaAllowed: number;
|
||||
|
||||
@ApiProperty({ description: "Average quota usage percentage", example: 25.0 })
|
||||
averageUsagePercent: number;
|
||||
|
||||
@ApiPropertyOptional({
|
||||
description: "Last time quota was synced",
|
||||
example: "2024-01-15T10:30:00Z",
|
||||
type: String,
|
||||
format: "date-time",
|
||||
})
|
||||
lastSyncTime: Date | null;
|
||||
}
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
|
||||
export class QuotaInfoDto {
|
||||
@ApiProperty({ description: "Quota used in bytes", example: 1073741824 })
|
||||
@ApiProperty({ description: "Used quota in bytes", example: 67108864 })
|
||||
used: number;
|
||||
|
||||
@ApiProperty({ description: "Quota allowed in bytes", example: 5368709120 })
|
||||
@ApiProperty({ description: "Allowed quota in bytes", example: 134217728 })
|
||||
allowed: number;
|
||||
}
|
||||
|
||||
export class SyncResultDto {
|
||||
@ApiProperty({ description: "Number of successfully synced users", example: 145 })
|
||||
export class BusinessQuotaInfoDto {
|
||||
@ApiProperty({ description: "Business ID", example: "12345678-1234-1234-1234-123456789012" })
|
||||
businessId: string;
|
||||
|
||||
@ApiProperty({ description: "Business name", example: "Acme Corp" })
|
||||
businessName: string;
|
||||
|
||||
@ApiProperty({ description: "Total quota allocated in bytes", example: 1073741824 })
|
||||
totalQuota: number;
|
||||
|
||||
@ApiProperty({ description: "Used quota in bytes", example: 536870912 })
|
||||
usedQuota: number;
|
||||
|
||||
@ApiProperty({ description: "Remaining quota in bytes", example: 536870912 })
|
||||
remainingQuota: number;
|
||||
|
||||
@ApiProperty({ description: "Number of users in this business", example: 5 })
|
||||
userCount: number;
|
||||
}
|
||||
|
||||
export class UserQuotaStatisticsDto {
|
||||
@ApiProperty({ description: "Total number of users", example: 150 })
|
||||
totalUsers: number;
|
||||
|
||||
@ApiProperty({ description: "Total quota used across all users in bytes", example: 10737418240 })
|
||||
totalQuotaUsed: number;
|
||||
|
||||
@ApiProperty({ description: "Total quota allowed across all users in bytes", example: 21474836480 })
|
||||
totalQuotaAllowed: number;
|
||||
|
||||
@ApiProperty({ description: "Average usage percentage", example: 50.0 })
|
||||
averageUsagePercent: number;
|
||||
|
||||
@ApiProperty({ description: "Last sync time", example: "2024-01-15T10:30:00Z", nullable: true })
|
||||
lastSyncTime: Date | null;
|
||||
}
|
||||
|
||||
export class BusinessQuotaStatisticsDto {
|
||||
@ApiProperty({ description: "Total number of businesses", example: 10 })
|
||||
totalBusinesses: number;
|
||||
|
||||
@ApiProperty({ description: "Total quota allocated across all businesses in bytes", example: 10737418240 })
|
||||
totalQuotaAllocated: number;
|
||||
|
||||
@ApiProperty({ description: "Total quota used across all businesses in bytes", example: 5368709120 })
|
||||
totalQuotaUsed: number;
|
||||
|
||||
@ApiProperty({ description: "Average usage percentage", example: 50.0 })
|
||||
averageUsagePercent: number;
|
||||
|
||||
@ApiProperty({ description: "Detailed information for each business", type: [BusinessQuotaInfoDto] })
|
||||
businesses: BusinessQuotaInfoDto[];
|
||||
}
|
||||
|
||||
export class QuotaSyncResultDto {
|
||||
@ApiProperty({ description: "Number of successful syncs", example: 100 })
|
||||
success: number;
|
||||
|
||||
@ApiProperty({ description: "Number of users with sync errors", example: 5 })
|
||||
@ApiProperty({ description: "Number of failed syncs", example: 2 })
|
||||
errors: number;
|
||||
}
|
||||
|
||||
export class ComprehensiveQuotaSyncResultDto {
|
||||
@ApiProperty({ description: "User sync results", type: QuotaSyncResultDto })
|
||||
userSync: QuotaSyncResultDto;
|
||||
|
||||
@ApiProperty({ description: "Business sync results", type: QuotaSyncResultDto })
|
||||
businessSync: QuotaSyncResultDto;
|
||||
}
|
||||
|
||||
@@ -3,13 +3,17 @@ import { Injectable, Logger } from "@nestjs/common";
|
||||
import { Cron, CronExpression } from "@nestjs/schedule";
|
||||
import { firstValueFrom } from "rxjs";
|
||||
|
||||
import { Business } from "../../businesses/entities/business.entity";
|
||||
import { WildDuckUsersService } from "../../mail-server/services/wildduck-users.service";
|
||||
import { User } from "../../users/entities/user.entity";
|
||||
|
||||
interface QuotaInfo {
|
||||
used: number;
|
||||
allowed: number;
|
||||
}
|
||||
import {
|
||||
BusinessQuotaInfoDto,
|
||||
BusinessQuotaStatisticsDto,
|
||||
ComprehensiveQuotaSyncResultDto,
|
||||
QuotaInfoDto,
|
||||
QuotaSyncResultDto,
|
||||
UserQuotaStatisticsDto,
|
||||
} from "../DTO/quota-statistics.dto";
|
||||
|
||||
@Injectable()
|
||||
export class QuotaSyncService {
|
||||
@@ -23,7 +27,7 @@ export class QuotaSyncService {
|
||||
/**
|
||||
* Sync quota usage every 15 minutes
|
||||
*/
|
||||
@Cron(CronExpression.EVERY_2_HOURS)
|
||||
@Cron(CronExpression.EVERY_10_SECONDS)
|
||||
// @Cron("0 */15 * * * *")
|
||||
async syncAllUsersQuota(): Promise<void> {
|
||||
const em = this.em.fork();
|
||||
@@ -60,6 +64,43 @@ export class QuotaSyncService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync business quota usage every 4 hours
|
||||
*/
|
||||
@Cron(CronExpression.EVERY_10_SECONDS)
|
||||
async syncAllBusinessQuotas(): Promise<void> {
|
||||
const em = this.em.fork();
|
||||
this.logger.log("Starting business quota synchronization");
|
||||
|
||||
try {
|
||||
const businesses = await em.find(Business, {
|
||||
deletedAt: null,
|
||||
});
|
||||
|
||||
this.logger.log(`Found ${businesses.length} businesses to sync quota for`);
|
||||
|
||||
let successCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
for (const business of businesses) {
|
||||
try {
|
||||
await this.syncBusinessQuotaWithEntityManager(business, em);
|
||||
successCount++;
|
||||
} catch (error) {
|
||||
errorCount++;
|
||||
this.logger.error(`Failed to sync quota for business ${business.id}: ${error instanceof Error ? error.message : "Unknown error"}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Flush all changes at once
|
||||
await em.flush();
|
||||
|
||||
this.logger.log(`Business quota sync completed: ${successCount} success, ${errorCount} errors`);
|
||||
} catch (error) {
|
||||
this.logger.error("Failed to sync quota for businesses", `${error instanceof Error ? error.message : "Unknown error"}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync quota for a specific user with provided entity manager
|
||||
*/
|
||||
@@ -77,7 +118,7 @@ export class QuotaSyncService {
|
||||
return;
|
||||
}
|
||||
|
||||
const quotaInfo: QuotaInfo = {
|
||||
const quotaInfo: QuotaInfoDto = {
|
||||
used: wildduckUser.limits?.quota?.used || 0,
|
||||
allowed: wildduckUser.limits?.quota?.allowed || 0,
|
||||
};
|
||||
@@ -113,7 +154,7 @@ export class QuotaSyncService {
|
||||
/**
|
||||
* Manually trigger quota sync for a specific user
|
||||
*/
|
||||
async syncUserQuotaById(userId: string): Promise<QuotaInfo> {
|
||||
async syncUserQuotaById(userId: string): Promise<QuotaInfoDto> {
|
||||
const em = this.em.fork();
|
||||
const user = await em.findOne(User, {
|
||||
id: userId,
|
||||
@@ -137,13 +178,7 @@ export class QuotaSyncService {
|
||||
/**
|
||||
* Get quota statistics for all users
|
||||
*/
|
||||
async getQuotaStatistics(): Promise<{
|
||||
totalUsers: number;
|
||||
totalQuotaUsed: number;
|
||||
totalQuotaAllowed: number;
|
||||
averageUsagePercent: number;
|
||||
lastSyncTime: Date | null;
|
||||
}> {
|
||||
async getQuotaStatistics(): Promise<UserQuotaStatisticsDto> {
|
||||
const em = this.em.fork();
|
||||
const users = await em.find(User, {
|
||||
emailEnabled: true,
|
||||
@@ -177,10 +212,131 @@ export class QuotaSyncService {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync quota for a specific business with provided entity manager
|
||||
*/
|
||||
private async syncBusinessQuotaWithEntityManager(business: Business, em: EntityManager): Promise<void> {
|
||||
try {
|
||||
// Get all active users for this business
|
||||
const users = await em.find(User, {
|
||||
business: business.id,
|
||||
emailEnabled: true,
|
||||
deletedAt: null,
|
||||
emailQuotaUsed: { $ne: null },
|
||||
});
|
||||
|
||||
// Calculate total used quota by summing all users' usage
|
||||
const totalUsedQuota = users.reduce((sum, user) => sum + Number(user.emailQuotaUsed || 0), 0);
|
||||
|
||||
// Convert business quota to number for calculations
|
||||
const businessQuota = Number(business.quota);
|
||||
|
||||
// Update business quota usage
|
||||
business.usedQuota = totalUsedQuota;
|
||||
business.remainingQuota = Math.max(0, businessQuota - totalUsedQuota);
|
||||
|
||||
this.logger.debug(`Synced business quota for ${business.name}: ${totalUsedQuota}/${businessQuota} bytes (${users.length} users)`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error syncing business quota for ${business.id}:`, `${error instanceof Error ? error.message : "Unknown error"}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync quota for a specific business
|
||||
*/
|
||||
async syncBusinessQuota(business: Business): Promise<void> {
|
||||
const em = this.em.fork();
|
||||
|
||||
// Re-fetch the business with the current entity manager
|
||||
const managedBusiness = await em.findOne(Business, { id: business.id });
|
||||
if (!managedBusiness) {
|
||||
throw new Error(`Business ${business.id} not found`);
|
||||
}
|
||||
|
||||
await this.syncBusinessQuotaWithEntityManager(managedBusiness, em);
|
||||
await em.flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually trigger quota sync for a specific business
|
||||
*/
|
||||
async syncBusinessQuotaById(businessId: string): Promise<BusinessQuotaInfoDto> {
|
||||
const em = this.em.fork();
|
||||
const business = await em.findOne(Business, {
|
||||
id: businessId,
|
||||
deletedAt: null,
|
||||
});
|
||||
|
||||
if (!business) {
|
||||
throw new Error(`Business ${businessId} not found`);
|
||||
}
|
||||
|
||||
await this.syncBusinessQuotaWithEntityManager(business, em);
|
||||
await em.flush();
|
||||
|
||||
// Get user count for this business
|
||||
const userCount = await em.count(User, {
|
||||
business: businessId,
|
||||
emailEnabled: true,
|
||||
deletedAt: null,
|
||||
});
|
||||
|
||||
return {
|
||||
businessId: business.id,
|
||||
businessName: business.name,
|
||||
totalQuota: Number(business.quota),
|
||||
usedQuota: Number(business.usedQuota),
|
||||
remainingQuota: Number(business.remainingQuota),
|
||||
userCount,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get quota statistics for all businesses
|
||||
*/
|
||||
async getBusinessQuotaStatistics(): Promise<BusinessQuotaStatisticsDto> {
|
||||
const em = this.em.fork();
|
||||
const businesses = await em.find(Business, {
|
||||
deletedAt: null,
|
||||
});
|
||||
|
||||
const totalQuotaAllocated = businesses.reduce((sum, business) => sum + Number(business.quota), 0);
|
||||
const totalQuotaUsed = businesses.reduce((sum, business) => sum + Number(business.usedQuota), 0);
|
||||
const averageUsagePercent = totalQuotaAllocated > 0 ? (totalQuotaUsed / totalQuotaAllocated) * 100 : 0;
|
||||
|
||||
// Get detailed info for each business
|
||||
const businessInfos: BusinessQuotaInfoDto[] = [];
|
||||
for (const business of businesses) {
|
||||
const userCount = await em.count(User, {
|
||||
business: business.id,
|
||||
emailEnabled: true,
|
||||
deletedAt: null,
|
||||
});
|
||||
|
||||
businessInfos.push({
|
||||
businessId: business.id,
|
||||
businessName: business.name,
|
||||
totalQuota: Number(business.quota),
|
||||
usedQuota: Number(business.usedQuota),
|
||||
remainingQuota: Number(business.remainingQuota),
|
||||
userCount,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
totalBusinesses: businesses.length,
|
||||
totalQuotaAllocated,
|
||||
totalQuotaUsed,
|
||||
averageUsagePercent: Math.round(averageUsagePercent * 100) / 100,
|
||||
businesses: businessInfos,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Manual sync for all users (useful for admin endpoints)
|
||||
*/
|
||||
async forceSyncAllUsers(): Promise<{ success: number; errors: number }> {
|
||||
async forceSyncAllUsers(): Promise<QuotaSyncResultDto> {
|
||||
this.logger.log("Manual quota sync triggered");
|
||||
await this.syncAllUsersQuota();
|
||||
|
||||
@@ -203,4 +359,71 @@ export class QuotaSyncService {
|
||||
errors: users.length - recentSyncs,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Manual sync for all businesses (useful for admin endpoints)
|
||||
*/
|
||||
async forceSyncAllBusinesses(): Promise<QuotaSyncResultDto> {
|
||||
this.logger.log("Manual business quota sync triggered");
|
||||
await this.syncAllBusinessQuotas();
|
||||
|
||||
// Return basic statistics
|
||||
const em = this.em.fork();
|
||||
const businesses = await em.find(Business, {
|
||||
deletedAt: null,
|
||||
});
|
||||
|
||||
// Count businesses that were recently updated (assuming sync was successful)
|
||||
const recentSyncs = businesses.filter((business) => business.updatedAt && business.updatedAt > new Date(Date.now() - 300000)).length; // Last 5 minutes
|
||||
|
||||
return {
|
||||
success: recentSyncs,
|
||||
errors: businesses.length - recentSyncs,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Comprehensive sync for both users and businesses
|
||||
*/
|
||||
async syncAllQuotas(): Promise<ComprehensiveQuotaSyncResultDto> {
|
||||
this.logger.log("Starting comprehensive quota sync for users and businesses");
|
||||
|
||||
// First sync all users
|
||||
await this.syncAllUsersQuota();
|
||||
|
||||
// Then sync all businesses based on updated user data
|
||||
await this.syncAllBusinessQuotas();
|
||||
|
||||
// Get statistics for both
|
||||
const em = this.em.fork();
|
||||
|
||||
const users = await em.find(User, {
|
||||
emailEnabled: true,
|
||||
deletedAt: null,
|
||||
wildduckUserId: { $ne: null },
|
||||
});
|
||||
|
||||
const businesses = await em.find(Business, {
|
||||
deletedAt: null,
|
||||
});
|
||||
|
||||
const recentUserSyncs = await em.count(User, {
|
||||
emailEnabled: true,
|
||||
deletedAt: null,
|
||||
lastQuotaSync: { $gte: new Date(Date.now() - 60000) }, // Last minute
|
||||
});
|
||||
|
||||
const recentBusinessSyncs = businesses.filter((business) => business.updatedAt && business.updatedAt > new Date(Date.now() - 300000)).length; // Last 5 minutes
|
||||
|
||||
return {
|
||||
userSync: {
|
||||
success: recentUserSyncs,
|
||||
errors: users.length - recentUserSyncs,
|
||||
},
|
||||
businessSync: {
|
||||
success: recentBusinessSyncs,
|
||||
errors: businesses.length - recentBusinessSyncs,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user