From 29447de5b2a1b1f78a4ac5dd16e223979fcb87c6 Mon Sep 17 00:00:00 2001 From: pradeepkumar Date: Wed, 15 Apr 2026 15:46:33 +0530 Subject: [PATCH] feat: implement sanitization for enumerated field values to remove stale options and add a cleanup script for existing data. --- src/agents/agents.service.ts | 82 ++++++++++++++++++++++++++++++------ 1 file changed, 68 insertions(+), 14 deletions(-) diff --git a/src/agents/agents.service.ts b/src/agents/agents.service.ts index 62b3ef8..63618d6 100644 --- a/src/agents/agents.service.ts +++ b/src/agents/agents.service.ts @@ -966,8 +966,35 @@ export class AgentsService { candidateFields.find((f) => f.section.isGlobal) || candidateFields[0]; + // Sanitize enumerated fields: drop values that aren't in current options, + // and de-dup. Prevents stale option values from accumulating in the stored array. + let sanitizedValue: unknown = fieldValue.value; + const isEnumType = + field.fieldType === 'CHECKBOX_GROUP' || + field.fieldType === 'MULTI_SELECT' || + field.fieldType === 'SELECT' || + field.fieldType === 'RADIO'; + if (isEnumType && Array.isArray(field.options)) { + const validValues = new Set( + (field.options as Array<{ value: string }>).map((o) => String(o.value)), + ); + if (Array.isArray(sanitizedValue)) { + sanitizedValue = (sanitizedValue as unknown[]) + .filter((v) => validValues.has(String(v))) + .filter((v, i, arr) => arr.indexOf(v) === i); + } else if ( + typeof sanitizedValue === 'string' && + !validValues.has(sanitizedValue) + ) { + sanitizedValue = null; + } + } + // Determine which column to use based on value type and field type - const valueData = this.getValueData(field.fieldType, fieldValue.value); + const valueData = this.getValueData( + field.fieldType, + sanitizedValue as string | number | boolean | string[] | Record | null, + ); // Upsert the field value const result = await this.prisma.agentProfileFieldValue.upsert({ @@ -1059,27 +1086,42 @@ export class AgentsService { }); // Helper to resolve a raw value to its option label (for SELECT/RADIO/MULTI_SELECT fields) + // Returns `null` when no matching option is found — the caller can treat those as orphans. const resolveLabel = (rawValue: any, options: any): any => { if (!options || !Array.isArray(options) || options.length === 0) return rawValue; - const lookup = (val: any) => { + const lookup = (val: any): string | null => { const match = (options as any[]).find((o: any) => o.value === val || o.value === String(val)); - return match?.label || val; + return match?.label ?? null; }; if (Array.isArray(rawValue)) return rawValue.map(lookup); return lookup(rawValue); }; - // Transform to a more usable format + // Transform to a more usable format. For enumerated field types, drop any array entry + // whose value no longer resolves to a current option so stale selections never leak through. const transformedValues = fieldValues.map((fv) => { const rawValue = this.extractValue(fv); + let value: unknown = rawValue; + let valueLabel: unknown = resolveLabel(rawValue, fv.field.options); + const isEnum = + fv.field.fieldType === 'CHECKBOX_GROUP' || + fv.field.fieldType === 'MULTI_SELECT' || + fv.field.fieldType === 'SELECT' || + fv.field.fieldType === 'RADIO'; + if (isEnum && Array.isArray(value) && Array.isArray(valueLabel)) { + const pairs = (value as unknown[]).map((v, i) => ({ v, l: (valueLabel as unknown[])[i] })) + .filter((p) => p.l !== null && p.l !== undefined); + value = pairs.map((p) => p.v); + valueLabel = pairs.map((p) => p.l); + } return { fieldSlug: fv.field.slug, fieldName: fv.field.name, fieldType: fv.field.fieldType, sectionSlug: fv.field.section.slug, sectionName: fv.field.section.name, - value: rawValue, - valueLabel: resolveLabel(rawValue, fv.field.options), + value, + valueLabel, }; }); @@ -1123,30 +1165,42 @@ export class AgentsService { }); // Helper to resolve a raw value to its option label (for SELECT/RADIO/MULTI_SELECT fields) + // Returns `null` when no matching option is found — the caller can treat those as orphans. const resolveLabel = (rawValue: any, options: any): any => { if (!options || !Array.isArray(options) || options.length === 0) return rawValue; - const lookup = (val: any) => { + const lookup = (val: any): string | null => { const match = (options as any[]).find((o: any) => o.value === val || o.value === String(val)); - return match?.label || val; + return match?.label ?? null; }; if (Array.isArray(rawValue)) return rawValue.map(lookup); return lookup(rawValue); }; - // Transform to a more usable format - // - value: raw value (backward-compatible — for clients that map values themselves) - // - valueLabel: option label resolved from field options (for clients that need display strings) + // Transform to a more usable format. For enumerated field types, drop any array entry + // whose value no longer resolves to a current option so stale selections never leak through. const transformedValues = fieldValues.map((fv) => { const rawValue = this.extractValue(fv); - const labelValue = resolveLabel(rawValue, fv.field.options); + let value: unknown = rawValue; + let valueLabel: unknown = resolveLabel(rawValue, fv.field.options); + const isEnum = + fv.field.fieldType === 'CHECKBOX_GROUP' || + fv.field.fieldType === 'MULTI_SELECT' || + fv.field.fieldType === 'SELECT' || + fv.field.fieldType === 'RADIO'; + if (isEnum && Array.isArray(value) && Array.isArray(valueLabel)) { + const pairs = (value as unknown[]).map((v, i) => ({ v, l: (valueLabel as unknown[])[i] })) + .filter((p) => p.l !== null && p.l !== undefined); + value = pairs.map((p) => p.v); + valueLabel = pairs.map((p) => p.l); + } return { fieldSlug: fv.field.slug, fieldName: fv.field.name, fieldType: fv.field.fieldType, sectionSlug: fv.field.section.slug, sectionName: fv.field.section.name, - value: rawValue, - valueLabel: labelValue, + value, + valueLabel, }; });