Skip to content

Commit cae3068

Browse files
authored
Merge pull request #35 from GenerateNU/frontend-api-service
FE API SERVICEEEEEEEE
2 parents 429eac4 + e8e9981 commit cae3068

30 files changed

Lines changed: 4030 additions & 614 deletions

backend/prisma/schema.prisma

Lines changed: 219 additions & 196 deletions
Large diffs are not rendered by default.

backend/src/controllers/local-events.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { Request, Response } from "express";
22
import { prisma } from "../services/db.js";
3+
import { PrismaClient } from "@prisma/client";
34

45
type LocalEvent = {
56
id: string;

backend/src/controllers/tmdb.ts

Lines changed: 131 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { Request, Response } from "express";
22
import { prisma } from "../services/db.js";
33
import { Prisma } from "@prisma/client";
44
import { randomUUID } from "crypto";
5+
import type { Movie } from "../types/models";
56

67
type TmdbMovie = {
78
id: number;
@@ -78,23 +79,26 @@ export async function saveMovie(mapped: MovieInsert) {
7879
});
7980
}
8081

81-
// Express handler
82+
// Gets a movie from TMDB and saves it to the local db
8283
export const getMovie = async (req: Request, res: Response) => {
83-
const { movieId: tmdbId } = req.params; // route: /movies/:movieId (TMDB id)
84-
85-
if (!tmdbId) {
86-
return res.status(400).json({ message: "Movie ID is required" });
87-
}
84+
const { movieId: tmdbId } = req.params;
85+
if (!tmdbId) return res.status(400).json({ message: "Movie ID is required" });
8886

8987
try {
9088
const tmdb = await fetchTmdbMovie(tmdbId);
91-
const mapped = mapTmdbToMovie(tmdb);
92-
const saved = await saveMovie(mapped);
89+
const mappedAppModel = mapTmdbToMovie(tmdb);
90+
const prismaCreate = mappedAppModel;
91+
92+
// create or update if it already exists
93+
const saved = await prisma.movie.upsert({
94+
where: { movieId: prismaCreate.movieId },
95+
create: prismaCreate,
96+
update: prismaCreate,
97+
});
9398

94-
// Convert BigInt to number for JSON serialization
9599
const movieResponse = {
96100
...saved,
97-
imdbRating: saved.imdbRating ? Number(saved.imdbRating) : null,
101+
imdbRating: saved.imdbRating != null ? Number(saved.imdbRating) : null,
98102
};
99103

100104
res.json({
@@ -113,52 +117,31 @@ export const getMovie = async (req: Request, res: Response) => {
113117
// PUT /movies/:movieId
114118
export const updateMovie = async (req: Request, res: Response) => {
115119
const { movieId } = req.params;
120+
if (!movieId) return res.status(400).json({ message: "Movie ID is required" });
116121

117-
if (!movieId) {
118-
return res.status(400).json({ message: "Movie ID is required" });
119-
}
120-
121-
const { title, description, languages, imdbRating, localRating, numRatings } =
122-
req.body;
123-
124-
const updateData: Partial<Prisma.movieUpdateInput> = {};
125-
126-
if (title !== undefined) updateData.title = title;
127-
if (description !== undefined) updateData.description = description;
128-
if (languages !== undefined) updateData.languages = languages;
129-
if (imdbRating !== undefined) updateData.imdbRating = imdbRating;
130-
if (localRating !== undefined) updateData.localRating = String(localRating);
131-
if (numRatings !== undefined) updateData.numRatings = String(numRatings);
122+
const body = req.body as Partial<Movie>;
123+
const updateData = mapMovieToPrismaUpdate(body);
132124

133125
if (Object.keys(updateData).length === 0) {
134126
return res.status(400).json({ message: "No fields to update" });
135127
}
136128

137129
try {
138130
const updatedMovie = await prisma.movie.update({
139-
where: { movieId: movieId },
131+
where: { movieId },
140132
data: updateData,
141133
});
142134

143-
// Convert BigInt to number for JSON serialization
144135
const movieResponse = {
145136
...updatedMovie,
146-
imdbRating: updatedMovie.imdbRating
147-
? Number(updatedMovie.imdbRating)
148-
: null,
137+
imdbRating: updatedMovie.imdbRating != null ? Number(updatedMovie.imdbRating) : null,
149138
};
150139

151-
res.json({
152-
message: "Movie updated successfully",
153-
data: movieResponse,
154-
});
140+
res.json({ message: "Movie updated successfully", data: movieResponse });
155141
} catch (err) {
156142
console.error("updateMovie error:", err);
157143

158-
if (
159-
err instanceof Prisma.PrismaClientKnownRequestError &&
160-
err.code === "P2025"
161-
) {
144+
if (err instanceof Prisma.PrismaClientKnownRequestError && err.code === "P2025") {
162145
return res.status(404).json({ message: "Movie not found" });
163146
}
164147

@@ -201,30 +184,16 @@ export const deleteMovie = async (req: Request, res: Response) => {
201184
};
202185

203186
export const getMovieById = async (req: Request, res: Response) => {
204-
// Fetch movie from YOUR database using the local UUID
205-
// No TMDB call needed
206187
const { movieId } = req.params;
207-
if (!movieId) {
208-
return res.status(400).json({ message: "Movie ID is required" });
209-
}
188+
if (!movieId) return res.status(400).json({ message: "Movie ID is required" });
189+
210190
try {
211-
const movie = await prisma.movie.findUnique({
212-
where: { movieId: movieId },
213-
});
214-
if (!movie) {
215-
return res.status(404).json({ message: "Movie not found." });
216-
}
191+
const movie = await prisma.movie.findUnique({ where: { movieId } });
192+
if (!movie) return res.status(404).json({ message: "Movie not found." });
217193

218-
// Convert BigInt to number for JSON serialization
219-
const movieResponse = {
220-
...movie,
221-
imdbRating: movie.imdbRating ? Number(movie.imdbRating) : null,
222-
};
194+
const movieResponse = mapMovieDbToApi(movie);
223195

224-
res.json({
225-
message: "Movie found successfully",
226-
data: movieResponse,
227-
});
196+
res.json({ message: "Movie found successfully", data: movieResponse });
228197
} catch (err) {
229198
console.error("getMovieById error:", err);
230199
res.status(500).json({
@@ -233,3 +202,107 @@ export const getMovieById = async (req: Request, res: Response) => {
233202
});
234203
}
235204
};
205+
206+
// -------- helpers --------
207+
const isNonEmptyString = (v: unknown): v is string =>
208+
typeof v === "string" && v.length > 0;
209+
210+
const toBigIntNullable = (
211+
v: unknown,
212+
{ forUpdate = false }: { forUpdate?: boolean } = {}
213+
): bigint | null | undefined => {
214+
if (v === undefined) return undefined; // skip
215+
if (v === null) return forUpdate ? null : undefined; // update: explicit null, create: skip
216+
217+
if (typeof v === "bigint") return v;
218+
219+
if (typeof v === "number" && Number.isFinite(v)) {
220+
return BigInt(Math.round(v));
221+
}
222+
223+
if (typeof v === "string") {
224+
const trimmed = v.trim();
225+
if (!isNonEmptyString(trimmed)) return forUpdate ? null : undefined;
226+
227+
// Try strict BigInt first, then numeric parse fallback
228+
try {
229+
return BigInt(trimmed);
230+
} catch {
231+
const n = Number(trimmed);
232+
if (Number.isFinite(n)) return BigInt(Math.round(n));
233+
}
234+
}
235+
236+
return forUpdate ? null : undefined;
237+
};
238+
239+
const toJsonStringArray = (v: unknown): string[] | undefined => {
240+
if (v == null) return undefined; // undefined or null => skip
241+
if (Array.isArray(v)) return v.filter(x => x != null).map(String);
242+
return undefined;
243+
};
244+
245+
// -------- mappers --------
246+
export function mapMovieDbToApi(dbMovie: Prisma.movieGetPayload<{}>): Movie {
247+
return {
248+
movieId: dbMovie.movieId,
249+
title: dbMovie.title,
250+
description: dbMovie.description,
251+
languages: dbMovie.languages ? (dbMovie.languages as string[]) : null,
252+
imdbRating: dbMovie.imdbRating != null ? Number(dbMovie.imdbRating) : null,
253+
localRating: dbMovie.localRating,
254+
numRatings: dbMovie.numRatings,
255+
};
256+
}
257+
258+
export function mapMoviesDbToApi(dbMovies: Prisma.movieGetPayload<{}>[]): Movie[] {
259+
return dbMovies.map(mapMovieDbToApi);
260+
}
261+
262+
export const mapMovieToPrismaUpdate = (
263+
patch: Partial<Movie>
264+
): Prisma.movieUpdateInput => {
265+
const data: Prisma.movieUpdateInput = {};
266+
267+
if ("title" in patch) data.title = patch.title ?? null;
268+
if ("description" in patch) data.description = patch.description ?? null;
269+
270+
if ("languages" in patch) {
271+
if (patch.languages === null) {
272+
data.languages = Prisma.DbNull; // clear JSON column
273+
} else {
274+
data.languages = toJsonStringArray(patch.languages); // string[] | undefined
275+
}
276+
}
277+
278+
if ("imdbRating" in patch) {
279+
data.imdbRating = toBigIntNullable(patch.imdbRating, { forUpdate: true }) as
280+
| bigint
281+
| null
282+
| undefined;
283+
}
284+
285+
if ("localRating" in patch) {
286+
if (patch.localRating === undefined) {
287+
// leave undefined (no-op)
288+
} else if (patch.localRating === null) {
289+
data.localRating = null;
290+
} else {
291+
data.localRating = String(patch.localRating);
292+
}
293+
}
294+
295+
if ("numRatings" in patch) {
296+
if (patch.numRatings === undefined) {
297+
// no-op
298+
} else if (patch.numRatings === null) {
299+
data.numRatings = null;
300+
} else {
301+
data.numRatings = String(patch.numRatings);
302+
}
303+
}
304+
305+
return data;
306+
};
307+
308+

0 commit comments

Comments
 (0)