Skip to content

FAC-96 refactor: search filter construction duplicated across all three service methods #211

@y4nder

Description

@y4nder

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']); // courses

Acceptance Criteria

  • Search filter logic consolidated into a single private method
  • All three list methods use the shared helper
  • All existing tests still pass

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions