chore: sync service
This commit is contained in:
@@ -1,39 +1,79 @@
|
|||||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
import { ApiProperty } 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class QuotaInfoDto {
|
export class QuotaInfoDto {
|
||||||
@ApiProperty({ description: "Quota used in bytes", example: 1073741824 })
|
@ApiProperty({ description: "Used quota in bytes", example: 67108864 })
|
||||||
used: number;
|
used: number;
|
||||||
|
|
||||||
@ApiProperty({ description: "Quota allowed in bytes", example: 5368709120 })
|
@ApiProperty({ description: "Allowed quota in bytes", example: 134217728 })
|
||||||
allowed: number;
|
allowed: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SyncResultDto {
|
export class BusinessQuotaInfoDto {
|
||||||
@ApiProperty({ description: "Number of successfully synced users", example: 145 })
|
@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;
|
success: number;
|
||||||
|
|
||||||
@ApiProperty({ description: "Number of users with sync errors", example: 5 })
|
@ApiProperty({ description: "Number of failed syncs", example: 2 })
|
||||||
errors: number;
|
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 { Cron, CronExpression } from "@nestjs/schedule";
|
||||||
import { firstValueFrom } from "rxjs";
|
import { firstValueFrom } from "rxjs";
|
||||||
|
|
||||||
|
import { Business } from "../../businesses/entities/business.entity";
|
||||||
import { WildDuckUsersService } from "../../mail-server/services/wildduck-users.service";
|
import { WildDuckUsersService } from "../../mail-server/services/wildduck-users.service";
|
||||||
import { User } from "../../users/entities/user.entity";
|
import { User } from "../../users/entities/user.entity";
|
||||||
|
import {
|
||||||
interface QuotaInfo {
|
BusinessQuotaInfoDto,
|
||||||
used: number;
|
BusinessQuotaStatisticsDto,
|
||||||
allowed: number;
|
ComprehensiveQuotaSyncResultDto,
|
||||||
}
|
QuotaInfoDto,
|
||||||
|
QuotaSyncResultDto,
|
||||||
|
UserQuotaStatisticsDto,
|
||||||
|
} from "../DTO/quota-statistics.dto";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class QuotaSyncService {
|
export class QuotaSyncService {
|
||||||
@@ -23,7 +27,7 @@ export class QuotaSyncService {
|
|||||||
/**
|
/**
|
||||||
* Sync quota usage every 15 minutes
|
* Sync quota usage every 15 minutes
|
||||||
*/
|
*/
|
||||||
@Cron(CronExpression.EVERY_2_HOURS)
|
@Cron(CronExpression.EVERY_10_SECONDS)
|
||||||
// @Cron("0 */15 * * * *")
|
// @Cron("0 */15 * * * *")
|
||||||
async syncAllUsersQuota(): Promise<void> {
|
async syncAllUsersQuota(): Promise<void> {
|
||||||
const em = this.em.fork();
|
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
|
* Sync quota for a specific user with provided entity manager
|
||||||
*/
|
*/
|
||||||
@@ -77,7 +118,7 @@ export class QuotaSyncService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const quotaInfo: QuotaInfo = {
|
const quotaInfo: QuotaInfoDto = {
|
||||||
used: wildduckUser.limits?.quota?.used || 0,
|
used: wildduckUser.limits?.quota?.used || 0,
|
||||||
allowed: wildduckUser.limits?.quota?.allowed || 0,
|
allowed: wildduckUser.limits?.quota?.allowed || 0,
|
||||||
};
|
};
|
||||||
@@ -113,7 +154,7 @@ export class QuotaSyncService {
|
|||||||
/**
|
/**
|
||||||
* Manually trigger quota sync for a specific user
|
* 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 em = this.em.fork();
|
||||||
const user = await em.findOne(User, {
|
const user = await em.findOne(User, {
|
||||||
id: userId,
|
id: userId,
|
||||||
@@ -137,13 +178,7 @@ export class QuotaSyncService {
|
|||||||
/**
|
/**
|
||||||
* Get quota statistics for all users
|
* Get quota statistics for all users
|
||||||
*/
|
*/
|
||||||
async getQuotaStatistics(): Promise<{
|
async getQuotaStatistics(): Promise<UserQuotaStatisticsDto> {
|
||||||
totalUsers: number;
|
|
||||||
totalQuotaUsed: number;
|
|
||||||
totalQuotaAllowed: number;
|
|
||||||
averageUsagePercent: number;
|
|
||||||
lastSyncTime: Date | null;
|
|
||||||
}> {
|
|
||||||
const em = this.em.fork();
|
const em = this.em.fork();
|
||||||
const users = await em.find(User, {
|
const users = await em.find(User, {
|
||||||
emailEnabled: true,
|
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)
|
* 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");
|
this.logger.log("Manual quota sync triggered");
|
||||||
await this.syncAllUsersQuota();
|
await this.syncAllUsersQuota();
|
||||||
|
|
||||||
@@ -203,4 +359,71 @@ export class QuotaSyncService {
|
|||||||
errors: users.length - recentSyncs,
|
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