feat: implement sanitization for enumerated field values to remove stale options and add a cleanup script for existing data.
This commit is contained in:
@@ -966,8 +966,35 @@ export class AgentsService {
|
|||||||
candidateFields.find((f) => f.section.isGlobal) ||
|
candidateFields.find((f) => f.section.isGlobal) ||
|
||||||
candidateFields[0];
|
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
|
// 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<string, unknown> | null,
|
||||||
|
);
|
||||||
|
|
||||||
// Upsert the field value
|
// Upsert the field value
|
||||||
const result = await this.prisma.agentProfileFieldValue.upsert({
|
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)
|
// 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 => {
|
const resolveLabel = (rawValue: any, options: any): any => {
|
||||||
if (!options || !Array.isArray(options) || options.length === 0) return rawValue;
|
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));
|
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);
|
if (Array.isArray(rawValue)) return rawValue.map(lookup);
|
||||||
return lookup(rawValue);
|
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 transformedValues = fieldValues.map((fv) => {
|
||||||
const rawValue = this.extractValue(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 {
|
return {
|
||||||
fieldSlug: fv.field.slug,
|
fieldSlug: fv.field.slug,
|
||||||
fieldName: fv.field.name,
|
fieldName: fv.field.name,
|
||||||
fieldType: fv.field.fieldType,
|
fieldType: fv.field.fieldType,
|
||||||
sectionSlug: fv.field.section.slug,
|
sectionSlug: fv.field.section.slug,
|
||||||
sectionName: fv.field.section.name,
|
sectionName: fv.field.section.name,
|
||||||
value: rawValue,
|
value,
|
||||||
valueLabel: resolveLabel(rawValue, fv.field.options),
|
valueLabel,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1123,30 +1165,42 @@ export class AgentsService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Helper to resolve a raw value to its option label (for SELECT/RADIO/MULTI_SELECT fields)
|
// 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 => {
|
const resolveLabel = (rawValue: any, options: any): any => {
|
||||||
if (!options || !Array.isArray(options) || options.length === 0) return rawValue;
|
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));
|
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);
|
if (Array.isArray(rawValue)) return rawValue.map(lookup);
|
||||||
return lookup(rawValue);
|
return lookup(rawValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Transform to a more usable format
|
// Transform to a more usable format. For enumerated field types, drop any array entry
|
||||||
// - value: raw value (backward-compatible — for clients that map values themselves)
|
// whose value no longer resolves to a current option so stale selections never leak through.
|
||||||
// - valueLabel: option label resolved from field options (for clients that need display strings)
|
|
||||||
const transformedValues = fieldValues.map((fv) => {
|
const transformedValues = fieldValues.map((fv) => {
|
||||||
const rawValue = this.extractValue(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 {
|
return {
|
||||||
fieldSlug: fv.field.slug,
|
fieldSlug: fv.field.slug,
|
||||||
fieldName: fv.field.name,
|
fieldName: fv.field.name,
|
||||||
fieldType: fv.field.fieldType,
|
fieldType: fv.field.fieldType,
|
||||||
sectionSlug: fv.field.section.slug,
|
sectionSlug: fv.field.section.slug,
|
||||||
sectionName: fv.field.section.name,
|
sectionName: fv.field.section.name,
|
||||||
value: rawValue,
|
value,
|
||||||
valueLabel: labelValue,
|
valueLabel,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user