Skip to content

Commit 556d9e5

Browse files
committed
Refactor year validation to use dynamic detection from database
- Remove hardcoded year bounds (1800-2030) from all three backends - Backend aggregations now only validate year field is numeric (: number) - Add fetchYearBounds() on client to dynamically detect min/max years from data - Move dataset warning above Year field label in FilterBar - Update tests to reflect removed year bound validation Backends modified: Java Spring, Express, Python FastAPI Client modified: FilterBar.tsx, api.ts
1 parent 1e9dc15 commit 556d9e5

9 files changed

Lines changed: 70 additions & 238 deletions

File tree

mflix/client/app/components/FilterBar/FilterBar.tsx

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,14 @@
22

33
import { useState, useCallback, useEffect, useRef } from 'react';
44
import styles from './FilterBar.module.css';
5-
import { fetchGenres, type MovieFilterParams } from '@/lib/api';
5+
import { fetchGenres, fetchYearBounds, type MovieFilterParams } from '@/lib/api';
66

77
const SORT_OPTIONS = [
88
{ value: 'title', label: 'Title' },
99
{ value: 'year', label: 'Year' },
1010
{ value: 'imdb.rating', label: 'IMDB Rating' },
1111
];
1212

13-
// The sample_mflix dataset only contains movies up to 2015
14-
const MAX_DATASET_YEAR = 2015;
15-
1613
interface FilterBarProps {
1714
onFilterChange: (filters: MovieFilterParams) => void;
1815
isLoading?: boolean;
@@ -42,6 +39,8 @@ export default function FilterBar({
4239
const [filters, setFilters] = useState<MovieFilterParams>(initialFilters);
4340
const [genres, setGenres] = useState<string[]>([]);
4441
const [isLoadingGenres, setIsLoadingGenres] = useState(true);
42+
const [maxDatasetYear, setMaxDatasetYear] = useState<number | null>(null);
43+
const [minDatasetYear, setMinDatasetYear] = useState<number | null>(null);
4544

4645
// Track previous initialFilters to detect changes
4746
const prevInitialFiltersRef = useRef<MovieFilterParams>(initialFilters);
@@ -57,6 +56,28 @@ export default function FilterBar({
5756
loadGenres();
5857
}, []);
5958

59+
// Fetch year bounds from the API on mount
60+
useEffect(() => {
61+
async function loadYearBounds() {
62+
console.log('FilterBar: Fetching year bounds...');
63+
const result = await fetchYearBounds();
64+
console.log('FilterBar: Year bounds result:', result);
65+
if (result.success) {
66+
if (result.maxYear) {
67+
console.log('FilterBar: Setting maxDatasetYear to', result.maxYear);
68+
setMaxDatasetYear(result.maxYear);
69+
}
70+
if (result.minYear) {
71+
console.log('FilterBar: Setting minDatasetYear to', result.minYear);
72+
setMinDatasetYear(result.minYear);
73+
}
74+
} else {
75+
console.warn('FilterBar: Failed to fetch year bounds:', result.error);
76+
}
77+
}
78+
loadYearBounds();
79+
}, []);
80+
6081
// Sync internal state when initialFilters changes (e.g. from URL navigation)
6182
useEffect(() => {
6283
if (!areFiltersEqual(prevInitialFiltersRef.current, initialFilters)) {
@@ -137,22 +158,27 @@ export default function FilterBar({
137158
</div>
138159

139160
<div className={styles.filterGroup}>
161+
{maxDatasetYear && filters.year && filters.year > maxDatasetYear && (
162+
<span className={styles.yearWarning}>
163+
Dataset only contains movies up to {maxDatasetYear}
164+
</span>
165+
)}
166+
{minDatasetYear && filters.year && filters.year < minDatasetYear && (
167+
<span className={styles.yearWarning}>
168+
Dataset only contains movies from {minDatasetYear} onwards
169+
</span>
170+
)}
140171
<label className={styles.filterLabel}>Year</label>
141172
<input
142173
type="number"
143-
className={`${styles.filterInput} ${filters.year && filters.year > MAX_DATASET_YEAR ? styles.inputWarning : ''}`}
174+
className={`${styles.filterInput} ${(maxDatasetYear && filters.year && filters.year > maxDatasetYear) || (minDatasetYear && filters.year && filters.year < minDatasetYear) ? styles.inputWarning : ''}`}
144175
placeholder="e.g. 2010"
145176
value={filters.year || ''}
146177
onChange={(e) => handleFilterChange('year', e.target.value ? parseInt(e.target.value) : undefined)}
147178
disabled={isLoading}
148-
min={1900}
149-
max={2030}
179+
min={minDatasetYear || undefined}
180+
max={maxDatasetYear || undefined}
150181
/>
151-
{filters.year && filters.year > MAX_DATASET_YEAR && (
152-
<span className={styles.yearWarning}>
153-
Dataset only contains movies up to {MAX_DATASET_YEAR}
154-
</span>
155-
)}
156182
</div>
157183

158184
<div className={styles.filterGroup}>

mflix/client/app/lib/api.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -585,13 +585,30 @@ export async function fetchMoviesByYear(): Promise<{ success: boolean; error?: s
585585
error: 'Request timed out after 15 seconds'
586586
};
587587
}
588-
return {
589-
success: false,
588+
return {
589+
success: false,
590590
error: error instanceof Error ? error.message : 'Network error occurred while fetching movies by year'
591591
};
592592
}
593593
}
594594

595+
/**
596+
* Fetch the min and max year bounds from the available movie data.
597+
* This allows dynamic detection of the dataset's year range.
598+
*/
599+
export async function fetchYearBounds(): Promise<{ success: boolean; minYear?: number; maxYear?: number; error?: string }> {
600+
const result = await fetchMoviesByYear();
601+
if (!result.success || !result.data || result.data.length === 0) {
602+
return { success: false, error: result.error || 'No year data available' };
603+
}
604+
const years = result.data.map(stat => stat.year);
605+
return {
606+
success: true,
607+
minYear: Math.min(...years),
608+
maxYear: Math.max(...years)
609+
};
610+
}
611+
595612
/**
596613
* Fetch directors with most movies and their statistics
597614
*/

mflix/server/java-spring/src/main/java/com/mongodb/samplemflix/controller/MovieControllerImpl.java

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package com.mongodb.samplemflix.controller;
22

3-
import com.mongodb.samplemflix.exception.ValidationException;
43
import com.mongodb.samplemflix.model.Movie;
54
import com.mongodb.samplemflix.model.dto.BatchInsertResponse;
65
import com.mongodb.samplemflix.model.dto.BatchUpdateResponse;
@@ -87,26 +86,6 @@ public ResponseEntity<SuccessResponse<List<Movie>>> getAllMovies(
8786
@Parameter(description = "Sort order: 'asc' or 'desc' (default: asc)")
8887
@RequestParam(defaultValue = "asc") String sortOrder) {
8988

90-
// The sample_mflix dataset only contains movies up to 2015
91-
final int MAX_DATASET_YEAR = 2015;
92-
final int MIN_VALID_YEAR = 1800;
93-
String yearWarning = null;
94-
95-
// Validate year if provided
96-
if (year != null) {
97-
if (year < MIN_VALID_YEAR) {
98-
throw new ValidationException(
99-
String.format("Invalid year: %d. Year must be %d or later.", year, MIN_VALID_YEAR)
100-
);
101-
}
102-
if (year > MAX_DATASET_YEAR) {
103-
yearWarning = String.format(
104-
"Note: The sample_mflix dataset only contains movies up to %d. Your search for year %d may return no results.",
105-
MAX_DATASET_YEAR, year
106-
);
107-
}
108-
}
109-
11089
MovieSearchQuery query = MovieSearchQuery.builder()
11190
.q(q)
11291
.genre(genre)
@@ -121,11 +100,7 @@ public ResponseEntity<SuccessResponse<List<Movie>>> getAllMovies(
121100

122101
List<Movie> movies = movieService.getAllMovies(query);
123102

124-
// Build response message, including year warning if applicable
125103
String message = "Found " + movies.size() + " movies";
126-
if (yearWarning != null) {
127-
message = message + ". " + yearWarning;
128-
}
129104

130105
SuccessResponse<List<Movie>> response = SuccessResponse.<List<Movie>>builder()
131106
.success(true)

mflix/server/java-spring/src/main/java/com/mongodb/samplemflix/service/MovieServiceImpl.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,7 @@ public List<MovieWithCommentsResult> getMoviesWithMostRecentComments(Integer lim
376376
int resultLimit = Math.clamp(limit != null ? limit : 10, 1, 50);
377377

378378
// Build match criteria
379-
Criteria matchCriteria = Criteria.where(Movie.Fields.YEAR).type(16).gte(1800).lte(2030);
379+
Criteria matchCriteria = Criteria.where(Movie.Fields.YEAR).type(16);
380380

381381
// Add movie ID filter if provided
382382
if (movieId != null && !movieId.trim().isEmpty()) {
@@ -461,7 +461,7 @@ public List<MoviesByYearResult> getMoviesByYearWithStats() {
461461
Aggregation aggregation = Aggregation.newAggregation(
462462
// STAGE 1: Match movies with valid year data
463463
Aggregation.match(
464-
Criteria.where(Movie.Fields.YEAR).type(16).gte(1800).lte(2030)
464+
Criteria.where(Movie.Fields.YEAR).type(16)
465465
),
466466

467467
// STAGE 2: Group by year and calculate statistics
@@ -512,7 +512,7 @@ public List<DirectorStatisticsResult> getDirectorsWithMostMovies(Integer limit)
512512
// STAGE 1: Match movies with directors and valid year
513513
Aggregation.match(
514514
Criteria.where(Movie.Fields.DIRECTORS).exists(true).ne(null).ne(List.of())
515-
.and(Movie.Fields.YEAR).type(16).gte(1800).lte(2030)
515+
.and(Movie.Fields.YEAR).type(16)
516516
),
517517

518518
// STAGE 2: Unwind directors array

mflix/server/java-spring/src/test/java/com/mongodb/samplemflix/controller/MovieControllerTest.java

Lines changed: 0 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -125,47 +125,6 @@ void testGetAllMovies_WithQueryParams() throws Exception {
125125
.andExpect(jsonPath("$.data").isArray());
126126
}
127127

128-
@Test
129-
@DisplayName("GET /api/movies - Should return 400 for year before 1800")
130-
void testGetAllMovies_InvalidYearBefore1800() throws Exception {
131-
// Act & Assert
132-
mockMvc.perform(get("/api/movies")
133-
.param("year", "1700"))
134-
.andExpect(status().isBadRequest())
135-
.andExpect(jsonPath("$.success").value(false))
136-
.andExpect(jsonPath("$.error.code").value("VALIDATION_ERROR"));
137-
}
138-
139-
@Test
140-
@DisplayName("GET /api/movies - Should include warning for year after 2015")
141-
void testGetAllMovies_YearAfter2015IncludesWarning() throws Exception {
142-
// Arrange
143-
List<Movie> movies = Arrays.asList();
144-
when(movieService.getAllMovies(any(MovieSearchQuery.class))).thenReturn(movies);
145-
146-
// Act & Assert
147-
mockMvc.perform(get("/api/movies")
148-
.param("year", "2020"))
149-
.andExpect(status().isOk())
150-
.andExpect(jsonPath("$.success").value(true))
151-
.andExpect(jsonPath("$.message", containsString("2015")));
152-
}
153-
154-
@Test
155-
@DisplayName("GET /api/movies - Should not include warning for year 2015 or earlier")
156-
void testGetAllMovies_Year2015NoWarning() throws Exception {
157-
// Arrange
158-
List<Movie> movies = Arrays.asList(testMovie);
159-
when(movieService.getAllMovies(any(MovieSearchQuery.class))).thenReturn(movies);
160-
161-
// Act & Assert
162-
mockMvc.perform(get("/api/movies")
163-
.param("year", "2015"))
164-
.andExpect(status().isOk())
165-
.andExpect(jsonPath("$.success").value(true))
166-
.andExpect(jsonPath("$.message", not(containsString("sample_mflix"))));
167-
}
168-
169128
// ==================== GET MOVIE BY ID TESTS ====================
170129

171130
@Test

mflix/server/js-express/src/controllers/movieController.ts

Lines changed: 6 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -87,32 +87,9 @@ export async function getAllMovies(req: Request, res: Response): Promise<void> {
8787
filter.genres = { $regex: new RegExp(genre, "i") };
8888
}
8989

90-
// Year filtering and validation
91-
// The sample_mflix dataset only contains movies up to 2015
92-
const MAX_DATASET_YEAR = 2015;
93-
const MIN_VALID_YEAR = 1800;
94-
let yearWarning: string | undefined;
95-
90+
// Year filtering
9691
if (year) {
97-
const yearNum = parseInt(year);
98-
99-
// Validate year is within reasonable bounds
100-
if (yearNum < MIN_VALID_YEAR) {
101-
res.status(400).json(
102-
createErrorResponse(
103-
`Invalid year: ${yearNum}. Year must be ${MIN_VALID_YEAR} or later.`,
104-
"INVALID_YEAR"
105-
)
106-
);
107-
return;
108-
}
109-
110-
// Warn if searching for years beyond the dataset's range
111-
if (yearNum > MAX_DATASET_YEAR) {
112-
yearWarning = `Note: The sample_mflix dataset only contains movies up to ${MAX_DATASET_YEAR}. Your search for year ${yearNum} may return no results.`;
113-
}
114-
115-
filter.year = yearNum;
92+
filter.year = parseInt(year);
11693
}
11794

11895
// Rating range filtering
@@ -154,14 +131,8 @@ export async function getAllMovies(req: Request, res: Response): Promise<void> {
154131
.skip(skipNum)
155132
.toArray();
156133

157-
// Build response message, including year warning if applicable
158-
let message = `Found ${movies.length} movies`;
159-
if (yearWarning) {
160-
message = `${message}. ${yearWarning}`;
161-
}
162-
163134
// Return successful response
164-
res.json(createSuccessResponse(movies, message));
135+
res.json(createSuccessResponse(movies, `Found ${movies.length} movies`));
165136
}
166137

167138
/**
@@ -996,7 +967,7 @@ export async function getMoviesWithMostRecentComments(
996967
// the collection
997968
{
998969
$match: {
999-
year: { $type: "number", $gte: 1800, $lte: 2030 },
970+
year: { $type: "number" },
1000971
},
1001972
},
1002973
];
@@ -1134,7 +1105,7 @@ export async function getMoviesByYearWithStats(
11341105
// STAGE 1: Data quality filter
11351106
{
11361107
$match: {
1137-
year: { $type: "number", $gte: 1800, $lte: 2030 },
1108+
year: { $type: "number" },
11381109
},
11391110
},
11401111
// STAGE 2: Group by year and calculate statistics
@@ -1238,7 +1209,7 @@ export async function getDirectorsWithMostMovies(
12381209
{
12391210
$match: {
12401211
directors: { $exists: true, $ne: null, $not: { $eq: [] } },
1241-
year: { $type: "number", $gte: 1800, $lte: 2030 },
1212+
year: { $type: "number" },
12421213
},
12431214
},
12441215
// STAGE 2: Unwind directors array

mflix/server/js-express/tests/controllers/movieController.test.ts

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -204,44 +204,6 @@ describe("Movie Controller Tests", () => {
204204
);
205205
});
206206

207-
it("should return 400 for year before 1800", async () => {
208-
mockRequest.query = { year: "1700" };
209-
210-
await getAllMovies(mockRequest as Request, mockResponse as Response);
211-
212-
expect(mockStatus).toHaveBeenCalledWith(400);
213-
expect(mockCreateErrorResponse).toHaveBeenCalledWith(
214-
"Invalid year: 1700. Year must be 1800 or later.",
215-
"INVALID_YEAR"
216-
);
217-
});
218-
219-
it("should include warning message for year after 2015", async () => {
220-
mockRequest.query = { year: "2020" };
221-
mockToArray.mockResolvedValue([]);
222-
223-
await getAllMovies(mockRequest as Request, mockResponse as Response);
224-
225-
expect(mockFind).toHaveBeenCalledWith({ year: 2020 });
226-
expect(mockCreateSuccessResponse).toHaveBeenCalledWith(
227-
[],
228-
"Found 0 movies. Note: The sample_mflix dataset only contains movies up to 2015. Your search for year 2020 may return no results."
229-
);
230-
});
231-
232-
it("should not include warning message for year 2015 or earlier", async () => {
233-
const testMovies = [{ _id: TEST_MOVIE_ID, title: "Old Movie" }];
234-
mockRequest.query = { year: "2015" };
235-
mockToArray.mockResolvedValue(testMovies);
236-
237-
await getAllMovies(mockRequest as Request, mockResponse as Response);
238-
239-
expect(mockFind).toHaveBeenCalledWith({ year: 2015 });
240-
expect(mockCreateSuccessResponse).toHaveBeenCalledWith(
241-
testMovies,
242-
"Found 1 movies"
243-
);
244-
});
245207
});
246208

247209
describe("getMovieById", () => {

0 commit comments

Comments
 (0)