Skip to content

FAC-95 refactor: unsafe type assertions bypass MikroORM filter type-checking #210

@y4nder

Description

@y4nder

title: 'Unsafe type assertions bypass MikroORM filter type-checking'
severity: Minor
category: Code Smell
labels: [tech-debt, refactor]

Summary

Filter objects are built as Record<string, unknown> then cast with as FilterQuery<T>, erasing MikroORM's compile-time type safety. A typo in a filter field name would compile but silently produce wrong queries at runtime.

Location

  • File(s): src/modules/curriculum/services/curriculum.service.ts (lines 86, 100–101, 180, 193, 201–203)
  • Function/Class: CurriculumService.ListPrograms, CurriculumService.ListCourses

Problem

The ListPrograms and ListCourses methods build nested relation filters using Record<string, unknown>, then cast the result to satisfy MikroORM's FilterQuery<T> type.

// ListPrograms — line 86
const departmentFilter: Record<string, unknown> = {
  semester: query.semesterId,
};

// ... dynamic mutations ...

const filter: FilterQuery<Program> = {
  department: departmentFilter,
} as FilterQuery<Program>;  // line 101 — type assertion
// ListCourses — lines 180, 193, 201–203
const departmentFilter: Record<string, unknown> = {
  semester: query.semesterId,
};

const programFilter: Record<string, unknown> = {
  department: departmentFilter,
};

const filter: FilterQuery<Course> = {
  program: programFilter,
} as FilterQuery<Course>;  // line 203 — type assertion

The as FilterQuery<T> cast tells TypeScript to trust the developer. If a field is misspelled (e.g., semesters instead of semester), no compile error occurs — the bug surfaces only at runtime.

Impact

  • Developer safety: Refactoring entity field names won't produce compile errors in these filters, increasing the risk of silent regressions.
  • Maintainability: Future developers may copy this pattern, spreading untyped filter building across the codebase.

Suggested Fix

Build filters incrementally using properly typed objects rather than Record<string, unknown>. MikroORM's FilterQuery supports nested relation typing when structured correctly.

// ListPrograms — typed filter building
const filter: FilterQuery<Program> = {
  department: {
    semester: query.semesterId,
    ...(query.departmentId
      ? { id: query.departmentId }
      : departmentIds !== null
        ? { id: { $in: departmentIds } }
        : {}),
  },
};

If the dynamic nature makes a single expression awkward, use a typed intermediate:

const departmentFilter: FilterQuery<Department> = {
  semester: query.semesterId,
};
if (query.departmentId) {
  departmentFilter.id = query.departmentId;
} else if (departmentIds !== null) {
  departmentFilter.id = { $in: departmentIds };
}

Acceptance Criteria

  • Record<string, unknown> and as FilterQuery<T> casts removed from ListPrograms and ListCourses
  • Filter objects are typed as FilterQuery<T> (or typed intermediate like FilterQuery<Department>) from declaration
  • 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