@@ -2,6 +2,7 @@ import type { Request, Response } from "express";
22import { prisma } from "../services/db.js" ;
33import { Prisma } from "@prisma/client" ;
44import { randomUUID } from "crypto" ;
5+ import type { Movie } from "../types/models" ;
56
67type 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
8283export 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
114118export 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
203186export 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