Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
53325d8
feat: frontend api service
tGiech22 Oct 11, 2025
6f7a536
Merge branch 'main' of https://github.com/GenerateNU/cinecircle into …
tGiech22 Oct 11, 2025
d9a8814
chore: saving changes
tGiech22 Oct 15, 2025
1b14c1e
Chore: Merge branch 'main' of https://github.com/GenerateNU/cinecircl…
tGiech22 Oct 15, 2025
5550b4a
Ratings type
abby-stevenson Oct 1, 2025
8491ab1
Adding Rating to schema
abby-stevenson Oct 1, 2025
9b10360
Adding the create new rating
abby-stevenson Oct 1, 2025
fdd2d98
update to ratings
abby-stevenson Oct 1, 2025
98fe676
middleware
abby-stevenson Oct 2, 2025
6bd44c8
Deleting fake middleware
abby-stevenson Oct 10, 2025
af569d4
Adding tests
abby-stevenson Oct 10, 2025
676f665
Adding votes and removing double rating
abby-stevenson Oct 10, 2025
ef59c56
Rest of Crud
abby-stevenson Oct 13, 2025
8084d31
Fix: Add /api prefix in routes/index.ts
danctila Oct 13, 2025
62eb249
Fix: Check for Authorization header in auth mocks
danctila Oct 13, 2025
2eaa444
Fix: remove threadedComments from rating creation
danctila Oct 13, 2025
00fb691
fix: adding mapping functions
tGiech22 Oct 16, 2025
d0928ed
Merge branch 'main' of https://github.com/GenerateNU/cinecircle into …
tGiech22 Oct 16, 2025
45a944c
Merge branch 'main' of https://github.com/GenerateNU/cinecircle into …
tGiech22 Oct 16, 2025
00de2f8
saving changes
tGiech22 Oct 16, 2025
9f2b25e
chore: resolving merge conflicts
tGiech22 Oct 20, 2025
836b984
chore: Merge branch 'main' of https://github.com/GenerateNU/cinecircl…
tGiech22 Oct 29, 2025
6f2aefb
fix: adding mapping functions to tmdb.ts
tGiech22 Oct 30, 2025
a12d486
fix: this schema.prisma file seems to work
tGiech22 Oct 30, 2025
34fb614
chore: Merge branch 'main' of https://github.com/GenerateNU/cinecircl…
tGiech22 Oct 30, 2025
0a4aef3
fix: adding mapping functions for user.ts
tGiech22 Oct 30, 2025
aba6618
saving changes
tGiech22 Oct 31, 2025
0cf77f5
refactor: redoing mapping functions
tGiech22 Oct 31, 2025
8907c3e
fix: deleting unnecessary model
tGiech22 Nov 2, 2025
6ac0f03
fix: added mapping function for movies and tests
tGiech22 Nov 2, 2025
c5eca2a
fix: fixing import paths
tGiech22 Nov 2, 2025
dac6546
fix: adding mapping functions for user.ts and userFollows.ts
tGiech22 Nov 2, 2025
b70d20c
fix: changing imdbRating to BigInt for a test in movie.api.tests
tGiech22 Nov 2, 2025
6920c66
chore: Merge branch 'main' of https://github.com/GenerateNU/cinecircl…
tGiech22 Nov 2, 2025
e049fdb
fix: removing unnecessary mapper and tests for movie
tGiech22 Nov 2, 2025
e9e40e2
fix: fixing import for movie unit tests
tGiech22 Nov 2, 2025
4437b2a
fix: fixing mapping function usage
tGiech22 Nov 2, 2025
aa943b9
adding tests for userFollows
tGiech22 Nov 2, 2025
85e4c86
fix: fixing mapping function of tmdb
tGiech22 Nov 3, 2025
208c613
fix: fixing movie unit tests
tGiech22 Nov 3, 2025
82ee106
fix: fixing tests
tGiech22 Nov 3, 2025
2667012
fix: user schema relation names
danctila Nov 4, 2025
ae42136
fix: rm doc directory
danctila Nov 4, 2025
f76f3e4
Merge branch 'main' into frontend-api-service
danctila Nov 4, 2025
e8e9981
remove chat comments
danctila Nov 4, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
415 changes: 219 additions & 196 deletions backend/prisma/schema.prisma

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions backend/src/controllers/local-events.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Request, Response } from "express";
import { prisma } from "../services/db.js";
import { PrismaClient } from "@prisma/client";

type LocalEvent = {
id: string;
Expand Down
189 changes: 131 additions & 58 deletions backend/src/controllers/tmdb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Request, Response } from "express";
import { prisma } from "../services/db.js";
import { Prisma } from "@prisma/client";
import { randomUUID } from "crypto";
import type { Movie } from "../types/models";

type TmdbMovie = {
id: number;
Expand Down Expand Up @@ -78,23 +79,26 @@ export async function saveMovie(mapped: MovieInsert) {
});
}

// Express handler
// Gets a movie from TMDB and saves it to the local db
export const getMovie = async (req: Request, res: Response) => {
const { movieId: tmdbId } = req.params; // route: /movies/:movieId (TMDB id)

if (!tmdbId) {
return res.status(400).json({ message: "Movie ID is required" });
}
const { movieId: tmdbId } = req.params;
if (!tmdbId) return res.status(400).json({ message: "Movie ID is required" });

try {
const tmdb = await fetchTmdbMovie(tmdbId);
const mapped = mapTmdbToMovie(tmdb);
const saved = await saveMovie(mapped);
const mappedAppModel = mapTmdbToMovie(tmdb);
const prismaCreate = mappedAppModel;

// create or update if it already exists
const saved = await prisma.movie.upsert({
where: { movieId: prismaCreate.movieId },
create: prismaCreate,
update: prismaCreate,
});

// Convert BigInt to number for JSON serialization
const movieResponse = {
...saved,
imdbRating: saved.imdbRating ? Number(saved.imdbRating) : null,
imdbRating: saved.imdbRating != null ? Number(saved.imdbRating) : null,
};

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

if (!movieId) {
return res.status(400).json({ message: "Movie ID is required" });
}

const { title, description, languages, imdbRating, localRating, numRatings } =
req.body;

const updateData: Partial<Prisma.movieUpdateInput> = {};

if (title !== undefined) updateData.title = title;
if (description !== undefined) updateData.description = description;
if (languages !== undefined) updateData.languages = languages;
if (imdbRating !== undefined) updateData.imdbRating = imdbRating;
if (localRating !== undefined) updateData.localRating = String(localRating);
if (numRatings !== undefined) updateData.numRatings = String(numRatings);
const body = req.body as Partial<Movie>;
const updateData = mapMovieToPrismaUpdate(body);

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

try {
const updatedMovie = await prisma.movie.update({
where: { movieId: movieId },
where: { movieId },
data: updateData,
});

// Convert BigInt to number for JSON serialization
const movieResponse = {
...updatedMovie,
imdbRating: updatedMovie.imdbRating
? Number(updatedMovie.imdbRating)
: null,
imdbRating: updatedMovie.imdbRating != null ? Number(updatedMovie.imdbRating) : null,
};

res.json({
message: "Movie updated successfully",
data: movieResponse,
});
res.json({ message: "Movie updated successfully", data: movieResponse });
} catch (err) {
console.error("updateMovie error:", err);

if (
err instanceof Prisma.PrismaClientKnownRequestError &&
err.code === "P2025"
) {
if (err instanceof Prisma.PrismaClientKnownRequestError && err.code === "P2025") {
return res.status(404).json({ message: "Movie not found" });
}

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

export const getMovieById = async (req: Request, res: Response) => {
// Fetch movie from YOUR database using the local UUID
// No TMDB call needed
const { movieId } = req.params;
if (!movieId) {
return res.status(400).json({ message: "Movie ID is required" });
}
if (!movieId) return res.status(400).json({ message: "Movie ID is required" });

try {
const movie = await prisma.movie.findUnique({
where: { movieId: movieId },
});
if (!movie) {
return res.status(404).json({ message: "Movie not found." });
}
const movie = await prisma.movie.findUnique({ where: { movieId } });
if (!movie) return res.status(404).json({ message: "Movie not found." });

// Convert BigInt to number for JSON serialization
const movieResponse = {
...movie,
imdbRating: movie.imdbRating ? Number(movie.imdbRating) : null,
};
const movieResponse = mapMovieDbToApi(movie);

res.json({
message: "Movie found successfully",
data: movieResponse,
});
res.json({ message: "Movie found successfully", data: movieResponse });
} catch (err) {
console.error("getMovieById error:", err);
res.status(500).json({
Expand All @@ -233,3 +202,107 @@ export const getMovieById = async (req: Request, res: Response) => {
});
}
};

// -------- helpers --------
const isNonEmptyString = (v: unknown): v is string =>
typeof v === "string" && v.length > 0;

const toBigIntNullable = (
v: unknown,
{ forUpdate = false }: { forUpdate?: boolean } = {}
): bigint | null | undefined => {
if (v === undefined) return undefined; // skip
if (v === null) return forUpdate ? null : undefined; // update: explicit null, create: skip

if (typeof v === "bigint") return v;

if (typeof v === "number" && Number.isFinite(v)) {
return BigInt(Math.round(v));
}

if (typeof v === "string") {
const trimmed = v.trim();
if (!isNonEmptyString(trimmed)) return forUpdate ? null : undefined;

// Try strict BigInt first, then numeric parse fallback
try {
return BigInt(trimmed);
} catch {
const n = Number(trimmed);
if (Number.isFinite(n)) return BigInt(Math.round(n));
}
}

return forUpdate ? null : undefined;
};

const toJsonStringArray = (v: unknown): string[] | undefined => {
if (v == null) return undefined; // undefined or null => skip
if (Array.isArray(v)) return v.filter(x => x != null).map(String);
return undefined;
};

// -------- mappers --------
export function mapMovieDbToApi(dbMovie: Prisma.movieGetPayload<{}>): Movie {
return {
movieId: dbMovie.movieId,
title: dbMovie.title,
description: dbMovie.description,
languages: dbMovie.languages ? (dbMovie.languages as string[]) : null,
imdbRating: dbMovie.imdbRating != null ? Number(dbMovie.imdbRating) : null,
localRating: dbMovie.localRating,
numRatings: dbMovie.numRatings,
};
}

export function mapMoviesDbToApi(dbMovies: Prisma.movieGetPayload<{}>[]): Movie[] {
return dbMovies.map(mapMovieDbToApi);
}

export const mapMovieToPrismaUpdate = (
patch: Partial<Movie>
): Prisma.movieUpdateInput => {
const data: Prisma.movieUpdateInput = {};

if ("title" in patch) data.title = patch.title ?? null;
if ("description" in patch) data.description = patch.description ?? null;

if ("languages" in patch) {
if (patch.languages === null) {
data.languages = Prisma.DbNull; // clear JSON column
} else {
data.languages = toJsonStringArray(patch.languages); // string[] | undefined
}
}

if ("imdbRating" in patch) {
data.imdbRating = toBigIntNullable(patch.imdbRating, { forUpdate: true }) as
| bigint
| null
| undefined;
}

if ("localRating" in patch) {
if (patch.localRating === undefined) {
// leave undefined (no-op)
} else if (patch.localRating === null) {
data.localRating = null;
} else {
data.localRating = String(patch.localRating);
}
}

if ("numRatings" in patch) {
if (patch.numRatings === undefined) {
// no-op
} else if (patch.numRatings === null) {
data.numRatings = null;
} else {
data.numRatings = String(patch.numRatings);
}
}

return data;
};


Loading