Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 bare av importskriptet, ikke av serveren.
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
69 changes: 69 additions & 0 deletions backend/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
# Frame Rate – backend

GraphQL-server (Apollo Server) med egen PostgreSQL-database. Filmene importeres **én gang** fra TMDB med et skript; serveren spør bare vår egen database.

## 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 (tar noen minutter)
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
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, 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
└── db.ts Tilkobling til PostgreSQL
```
82 changes: 82 additions & 0 deletions backend/db/schema.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
-- 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
);

CREATE TABLE IF NOT EXISTS movies (
id INTEGER PRIMARY KEY, -- TMDB sin film-ID, hindrer duplikater ved ny import
title TEXT NOT NULL,
original_title TEXT,
overview TEXT,
release_date DATE,
release_year INTEGER GENERATED ALWAYS AS (EXTRACT(YEAR FROM release_date)::INTEGER) STORED,
runtime INTEGER, -- minutter
director TEXT,
age_rating TEXT, -- f.eks. "15" (NO) eller "PG-13" (US)
poster_path TEXT,
backdrop_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,
imported_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

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,
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);

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