-
Notifications
You must be signed in to change notification settings - Fork 0
FAC-96 refactor: search filter construction duplicated across all three service methods #211
Copy link
Copy link
Closed
Description
title: 'Search filter construction duplicated across all three service methods'
severity: Minor
category: Code Smell
labels: [tech-debt, refactor]
Summary
The $and/$or/$ilike search filter block is copy-pasted identically across ListDepartments, ListPrograms, and ListCourses, with only the field names varying. This means any change to search behavior (trimming, full-text search, pagination interaction) requires updating three places.
Location
- File(s):
src/modules/curriculum/services/curriculum.service.ts(lines 48–60, 103–115, 205–217) - Function/Class:
CurriculumService.ListDepartments,ListPrograms,ListCourses
Problem
Each method repeats the same 12-line block:
// ListDepartments — lines 48–60
if (query.search) {
const escaped = this.EscapeLikeWildcards(query.search);
Object.assign(filter, {
$and: [
{
$or: [
{ code: { $ilike: `%${escaped}%` } },
{ name: { $ilike: `%${escaped}%` } },
],
},
],
});
}
// ListPrograms — lines 103–115 (IDENTICAL to above)
if (query.search) {
const escaped = this.EscapeLikeWildcards(query.search);
Object.assign(filter, {
$and: [
{
$or: [
{ code: { $ilike: `%${escaped}%` } },
{ name: { $ilike: `%${escaped}%` } },
],
},
],
});
}
// ListCourses — lines 205–217 (same structure, different fields)
if (query.search) {
const escaped = this.EscapeLikeWildcards(query.search);
Object.assign(filter, {
$and: [
{
$or: [
{ shortname: { $ilike: `%${escaped}%` } },
{ fullname: { $ilike: `%${escaped}%` } },
],
},
],
});
}The ListDepartments and ListPrograms blocks are character-for-character identical. ListCourses differs only in field names.
Impact
- Shotgun surgery: Changing search behavior (e.g., adding input trimming, switching to full-text search, limiting results) requires modifying all three methods.
- Divergence risk: The identical blocks may drift apart over time if one is updated and others are missed.
Suggested Fix
Extract a private helper that accepts the filter object and an array of field names:
private ApplySearchFilter<T>(
filter: FilterQuery<T>,
search: string | undefined,
fields: string[],
): void {
if (!search) return;
const escaped = this.EscapeLikeWildcards(search);
Object.assign(filter, {
$and: [
{
$or: fields.map((field) => ({
[field]: { $ilike: `%${escaped}%` },
})),
},
],
});
}Usage:
this.ApplySearchFilter(filter, query.search, ['code', 'name']); // departments & programs
this.ApplySearchFilter(filter, query.search, ['shortname', 'fullname']); // coursesAcceptance Criteria
- Search filter logic consolidated into a single private method
- All three list methods use the shared helper
- All existing tests still pass
Reactions are currently unavailable