-
Notifications
You must be signed in to change notification settings - Fork 0
FAC-95 refactor: unsafe type assertions bypass MikroORM filter type-checking #210
Description
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 assertionThe 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>andas FilterQuery<T>casts removed fromListProgramsandListCourses - Filter objects are typed as
FilterQuery<T>(or typed intermediate likeFilterQuery<Department>) from declaration - All existing tests still pass