Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 17 additions & 0 deletions backend/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Kopier denne fila til .env og fyll inn verdiene. .env skal ALDRI committes.

# Adressen til databasen: postgres://bruker:passord@vert:port/databasenavn
DATABASE_URL=postgres://localhost:5432/framerate

# Porten GraphQL-serveren lytter på
PORT=3001

# TMDB "API Read Access Token" (lang tekst), fra https://www.themoviedb.org/settings/api
# Brukes av importskriptet og av serveren (filmdetaljer). Sendes aldri til nettleseren.
TMDB_READ_TOKEN=

# Hvor mange filmer importskriptet henter (maks 10000)
IMPORT_MOVIE_COUNT=5000

# Sett til "production" på VM-en, så skjuler Apollo interne feildetaljer
NODE_ENV=development
3 changes: 3 additions & 0 deletions backend/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
.env
*.log
78 changes: 78 additions & 0 deletions backend/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Frame Rate – backend

GraphQL-server (Apollo Server) med egen PostgreSQL-database.

- **Søk, filtrering og sortering** går mot vår egen database. Filmene importeres én gang fra TMDB med et skript, og vi lagrer bare feltene vi trenger til dette (tittel, dato, plakat, popularitet, sjangre).
- **Detaljer** (beskrivelse, regissør, spilletid, aldersgrense) lagres ikke. De hentes fra TMDB med film-ID-en når detaljsiden ber om `details`, slik [TMDB anbefaler](https://developer.themoviedb.org/docs/search-and-query-for-details). Serveren husker svaret i 24 timer.
- **Anmeldelser, likes og lister** er våre egne data og lagres bare i databasen.

## Kom i gang

Krever Node 24.6+ og PostgreSQL.

```bash
cd backend
npm install
cp .env.example .env # fyll inn DATABASE_URL og TMDB_READ_TOKEN
createdb framerate # lager en tom database (første gang)
npm run db:migrate # lager tabellene
npm run import:movies # henter ~5000 filmer fra TMDB (under ett minutt)
npm run dev # starter serveren på http://localhost:3001
```

Åpne http://localhost:3001 i nettleseren for å teste spørringer i Apollo Sandbox.

## Skript

| Kommando | Hva den gjør |
| ----------------------- | -------------------------------------------------------------------------- |
| `npm run dev` | Starter serveren og restarter den når du lagrer en fil |
| `npm start` | Starter serveren (bruk denne på VM-en) |
| `npm run typecheck` | Sjekker TypeScript-typer |
| `npm run db:migrate` | Lager/oppdaterer tabellene fra `db/schema.sql`. Trygt å kjøre flere ganger |
| `npm run import:movies` | Henter filmer fra TMDB. Kan kjøres på nytt uten duplikater |

## Eksempel

```graphql
query {
searchMovies(search: "star", genreIds: [878], yearFrom: 1970, sort: RATING_DESC, limit: 20, offset: 0) {
totalCount
movies {
id
title
releaseYear
posterPath
averageRating
genres { name }
}
}
movie(id: 11) {
title
averageRating
details { # hentes fra TMDB, bruk bare på detaljsiden
overview
director
runtime
ageRating
}
}
}
```

Plakater: `https://image.tmdb.org/t/p/w185{posterPath}` (liste) eller `w500` (detaljside).

## Mappestruktur

```
backend/
├── db/schema.sql Tabeller: movies, genres, movie_genres, users, reviews, review_likes, saved_movies
├── scripts/migrate.ts Kjører schema.sql
├── scripts/import-movies.ts Engangsimport fra TMDB
└── src/
├── index.ts Starter serveren
├── schema.ts GraphQL-typene (hva API-et tilbyr)
├── resolvers.ts Henter data fra databasen
├── tmdb.ts Kall mot TMDB (filmdetaljer og import)
└── db.ts Tilkobling til PostgreSQL
```
90 changes: 90 additions & 0 deletions backend/db/schema.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
-- Databaseskjema for Frame Rate (PostgreSQL).
-- Kjøres med `npm run db:migrate`. Trygt å kjøre flere ganger (IF NOT EXISTS).

-- pg_trgm gjør at søk med ILIKE '%tekst%' kan bruke en indeks.
CREATE EXTENSION IF NOT EXISTS pg_trgm;

CREATE TABLE IF NOT EXISTS genres (
id INTEGER PRIMARY KEY, -- TMDB sin sjanger-ID
name TEXT NOT NULL
);

-- Bare feltene vi trenger for å søke, filtrere, sortere og vise filmkort.
-- Detaljer (beskrivelse, regissør, spilletid, aldersgrense ...) hentes fra TMDB
-- med film-ID-en når detaljsiden åpnes, se src/tmdb.ts.
CREATE TABLE IF NOT EXISTS movies (
id INTEGER PRIMARY KEY, -- TMDB sin film-ID, brukes til å hente detaljer
title TEXT NOT NULL,
release_date DATE,
release_year INTEGER GENERATED ALWAYS AS (EXTRACT(YEAR FROM release_date)::INTEGER) STORED,
poster_path TEXT,
popularity REAL NOT NULL DEFAULT 0,
-- Regnes ut fra våre egne anmeldelser, ikke fra TMDB
average_rating NUMERIC(3, 2),
review_count INTEGER NOT NULL DEFAULT 0
);

CREATE INDEX IF NOT EXISTS movies_title_trgm_idx ON movies USING gin (title gin_trgm_ops);
CREATE INDEX IF NOT EXISTS movies_release_year_idx ON movies (release_year);
CREATE INDEX IF NOT EXISTS movies_popularity_idx ON movies (popularity DESC);
CREATE INDEX IF NOT EXISTS movies_average_rating_idx ON movies (average_rating DESC NULLS LAST);

CREATE TABLE IF NOT EXISTS movie_genres (
movie_id INTEGER NOT NULL REFERENCES movies (id) ON DELETE CASCADE,
genre_id INTEGER NOT NULL REFERENCES genres (id) ON DELETE CASCADE,
PRIMARY KEY (movie_id, genre_id)
);

CREATE INDEX IF NOT EXISTS movie_genres_genre_idx ON movie_genres (genre_id);

-- Brukere: innlogging bygges senere, men tabellen finnes så anmeldelser og lister kan peke hit.
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
avatar_url TEXT, -- profilbilde, null = standardbilde
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE TABLE IF NOT EXISTS reviews (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
movie_id INTEGER NOT NULL REFERENCES movies (id) ON DELETE CASCADE,
rating SMALLINT NOT NULL CHECK (rating BETWEEN 1 AND 6), -- terningkast
text TEXT CHECK (char_length(text) <= 2000),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (user_id, movie_id) -- én anmeldelse per bruker per film
);

CREATE INDEX IF NOT EXISTS reviews_movie_idx ON reviews (movie_id, created_at DESC);
CREATE INDEX IF NOT EXISTS reviews_user_idx ON reviews (user_id, created_at DESC);

-- "Liker" på anmeldelser. Én like per bruker per anmeldelse.
CREATE TABLE IF NOT EXISTS review_likes (
user_id INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
review_id INTEGER NOT NULL REFERENCES reviews (id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (user_id, review_id)
);

CREATE INDEX IF NOT EXISTS review_likes_review_idx ON review_likes (review_id);

DO $$
BEGIN
CREATE TYPE list_type AS ENUM ('WATCHLIST', 'SEEN', 'FAVORITE');
EXCEPTION
WHEN duplicate_object THEN NULL;
END
$$;

-- Én tabell for alle tre listene; list_type sier hvilken liste filmen ligger i.
CREATE TABLE IF NOT EXISTS saved_movies (
user_id INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE,
movie_id INTEGER NOT NULL REFERENCES movies (id) ON DELETE CASCADE,
list_type list_type NOT NULL,
added_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (user_id, movie_id, list_type)
);

CREATE INDEX IF NOT EXISTS saved_movies_user_list_idx ON saved_movies (user_id, list_type, added_at DESC);
Loading