Assist_Design/apps/bff/src/modules/id-mappings/validation/mapping-validator.service.ts

215 lines
6.7 KiB
TypeScript

import { Injectable, Inject } from "@nestjs/common";
import { Logger } from "nestjs-pino";
import { z } from "zod";
// Simple Zod schemas for mapping validation (matching database types)
const createMappingRequestSchema = z.object({
userId: z.string().uuid(),
whmcsClientId: z.number().int().positive(),
sfAccountId: z.string().optional(),
});
const updateMappingRequestSchema = z.object({
whmcsClientId: z.number().int().positive().optional(),
sfAccountId: z.string().optional(),
});
const userIdMappingSchema = z.object({
id: z.string().uuid(),
userId: z.string().uuid(),
whmcsClientId: z.number().int().positive(),
sfAccountId: z.string().nullable(),
createdAt: z.date(),
updatedAt: z.date(),
});
export type CreateMappingRequest = z.infer<typeof createMappingRequestSchema>;
export type UpdateMappingRequest = z.infer<typeof updateMappingRequestSchema>;
export type UserIdMapping = z.infer<typeof userIdMappingSchema>;
// Legacy interface for backward compatibility
export interface MappingValidationResult {
isValid: boolean;
errors: string[];
warnings: string[];
}
@Injectable()
export class MappingValidatorService {
constructor(@Inject(Logger) private readonly logger: Logger) {}
validateCreateRequest(request: CreateMappingRequest): MappingValidationResult {
const validationResult = createMappingRequestSchema.safeParse(request);
if (validationResult.success) {
const warnings: string[] = [];
if (!request.sfAccountId) {
warnings.push("Salesforce account ID not provided - mapping will be incomplete");
}
return { isValid: true, errors: [], warnings };
}
const errors = validationResult.error.issues.map(issue => issue.message);
this.logger.warn({ request, errors }, "Create mapping request validation failed");
return { isValid: false, errors, warnings: [] };
}
validateUpdateRequest(userId: string, request: UpdateMappingRequest): MappingValidationResult {
// First validate userId
const userIdValidation = z.string().uuid().safeParse(userId);
if (!userIdValidation.success) {
return {
isValid: false,
errors: ["User ID must be a valid UUID"],
warnings: [],
};
}
// Then validate the update request
const validationResult = updateMappingRequestSchema.safeParse(request);
if (validationResult.success) {
return { isValid: true, errors: [], warnings: [] };
}
const errors = validationResult.error.issues.map(issue => issue.message);
this.logger.warn({ userId, request, errors }, "Update mapping request validation failed");
return { isValid: false, errors, warnings: [] };
}
validateExistingMapping(mapping: UserIdMapping): MappingValidationResult {
const validationResult = userIdMappingSchema.safeParse(mapping);
if (validationResult.success) {
const warnings: string[] = [];
if (!mapping.sfAccountId) {
warnings.push("Mapping is missing Salesforce account ID");
}
return { isValid: true, errors: [], warnings };
}
const errors = validationResult.error.issues.map(issue => issue.message);
this.logger.warn({ mapping, errors }, "Existing mapping validation failed");
return { isValid: false, errors, warnings: [] };
}
validateBulkMappings(
mappings: CreateMappingRequest[]
): Array<{ index: number; validation: MappingValidationResult }> {
return mappings.map((mapping, index) => ({
index,
validation: this.validateCreateRequest(mapping),
}));
}
validateNoConflicts(
request: CreateMappingRequest,
existingMappings: UserIdMapping[]
): MappingValidationResult {
const errors: string[] = [];
const warnings: string[] = [];
// First validate the request format
const formatValidation = this.validateCreateRequest(request);
if (!formatValidation.isValid) {
return formatValidation;
}
// Check for conflicts
const duplicateUser = existingMappings.find(m => m.userId === request.userId);
if (duplicateUser) {
errors.push(`User ${request.userId} already has a mapping`);
}
const duplicateWhmcs = existingMappings.find(m => m.whmcsClientId === request.whmcsClientId);
if (duplicateWhmcs) {
errors.push(
`WHMCS client ${request.whmcsClientId} is already mapped to user ${duplicateWhmcs.userId}`
);
}
if (request.sfAccountId) {
const duplicateSf = existingMappings.find(m => m.sfAccountId === request.sfAccountId);
if (duplicateSf) {
warnings.push(
`Salesforce account ${request.sfAccountId} is already mapped to user ${duplicateSf.userId}`
);
}
}
return { isValid: errors.length === 0, errors, warnings };
}
validateDeletion(mapping: UserIdMapping): MappingValidationResult {
const errors: string[] = [];
const warnings: string[] = [];
if (!mapping) {
errors.push("Cannot delete non-existent mapping");
return { isValid: false, errors, warnings };
}
// Validate the mapping format
const formatValidation = this.validateExistingMapping(mapping);
if (!formatValidation.isValid) {
return formatValidation;
}
warnings.push(
"Deleting this mapping will prevent access to WHMCS/Salesforce data for this user"
);
if (mapping.sfAccountId) {
warnings.push(
"This mapping includes Salesforce integration - deletion will affect case management"
);
}
return { isValid: true, errors, warnings };
}
sanitizeCreateRequest(request: CreateMappingRequest): CreateMappingRequest {
// Use Zod parsing to sanitize and validate
const validationResult = createMappingRequestSchema.safeParse({
userId: request.userId?.trim(),
whmcsClientId: request.whmcsClientId,
sfAccountId: request.sfAccountId?.trim() || undefined,
});
if (validationResult.success) {
return validationResult.data;
}
// Fallback to original behavior if validation fails
return {
userId: request.userId?.trim(),
whmcsClientId: request.whmcsClientId,
sfAccountId: request.sfAccountId?.trim() || undefined,
};
}
sanitizeUpdateRequest(request: UpdateMappingRequest): UpdateMappingRequest {
const sanitized: any = {};
if (request.whmcsClientId !== undefined) {
sanitized.whmcsClientId = request.whmcsClientId;
}
if (request.sfAccountId !== undefined) {
sanitized.sfAccountId = request.sfAccountId?.trim() || undefined;
}
// Use Zod parsing to validate the sanitized data
const validationResult = updateMappingRequestSchema.safeParse(sanitized);
if (validationResult.success) {
return validationResult.data;
}
// Fallback to sanitized data if validation fails
return sanitized;
}
}