From 54577cca3cdd37c8c8a2b4d22f86942607970745 Mon Sep 17 00:00:00 2001 From: albdert Date: Thu, 15 Oct 2026 11:06:10 +0200 Subject: [PATCH] =?UTF-8?q?formattering=20p=C3=A5=20backend?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/backend-api.md | 123 ++++++++++-------- backend/package.json | 4 +- backend/src/auth.ts | 3 +- backend/src/db.ts | 4 +- backend/src/index.ts | 12 +- backend/src/resolvers.ts | 36 ++--- backend/src/schema.ts | 3 +- db/database-connect.md | 12 +- db/database-layout.md | 2 - db/database-setup.md | 8 +- db/database-usage.md | 21 ++- frontend/src/App.css | 2 +- frontend/src/App.tsx | 7 +- frontend/src/components/GameCard/GameCard.css | 3 +- frontend/src/components/GameCard/GameCard.tsx | 1 - .../src/components/SearchBar/SearchBar.tsx | 2 +- frontend/src/graphql/games.ts | 2 +- frontend/src/hooks/useDebounce.ts | 2 +- 18 files changed, 140 insertions(+), 107 deletions(-) diff --git a/backend/backend-api.md b/backend/backend-api.md index 200bef0f..7dc50ea0 100644 --- a/backend/backend-api.md +++ b/backend/backend-api.md @@ -1,5 +1,7 @@ # Backend-API + ## Queries + ### game Henter informasjon om ett enkelt spill ved ID. @@ -25,7 +27,9 @@ game(id: ID!): Game game(id: "3498") { name esrbRating - genres { name } + genres { + name + } } } ``` @@ -54,11 +58,11 @@ games(search: String, filter: GameFilter, first: Int = 20): GameConnection! #### Argumenter -| Navn | Type | Default | Beskrivelse | -| -------- | --------------------------- | ------- | --------------------------------------------------------------------------------------------- | +| Navn | Type | Default | Beskrivelse | +| -------- | --------------------------- | ------- | --------------------------------------------------------------------------------------------------------- | | `search` | `String` | — | Søk i tittel og alternative navn. Skiller ikke mellom store og små bokstaver, og matcher deler av navnet. | -| `filter` | [`GameFilter`](#gamefilter) | — | Filtrering på sjanger, plattform og utgivelsesår. | -| `first` | `Int` | `20` | Antall spill på siden. Begrenset til 1–50. | +| `filter` | [`GameFilter`](#gamefilter) | — | Filtrering på sjanger, plattform og utgivelsesår. | +| `first` | `Int` | `20` | Antall spill på siden. Begrenset til 1–50. | Alle argumenter er valgfrie og kan kombineres. @@ -72,7 +76,11 @@ Alle argumenter er valgfrie og kan kombineres. { games(search: "zelda", first: 3) { totalCount - edges { node { name } } + edges { + node { + name + } + } } } ``` @@ -103,7 +111,12 @@ Action- og RPG-spill på PC, utgitt 2015–2020: first: 3 ) { totalCount - edges { node { name released } } + edges { + node { + name + released + } + } } } ``` @@ -145,7 +158,10 @@ Liste med [`Facet`](#facet). ```graphql { - genres { id name } + genres { + id + name + } } ``` @@ -185,7 +201,10 @@ Merk: ID-ene her er plattformfamilier og er ikke de samme som ID-ene i `Game.pla ```graphql { - platforms { id name } + platforms { + id + name + } } ``` @@ -202,36 +221,38 @@ Merk: ID-ene her er plattformfamilier og er ikke de samme som ID-ene i `Game.pla ``` ## Mutations + > Dette skal legges til når funksjonalitet for brukere blir lagt til ## Types + ### Game Et spill med detaljer og relasjoner. -| Felt | Type | Beskrivelse | -| --------------------------- | ------------------- | ----------------------------------------------------------------- | -| `id` | `ID!` | ID til spillet. | -| `name` | `String!` | Tittel. | -| `description` | `String` | Beskrivelse i ren tekst. | -| `released` | `String` | Utgivelsesdato, `YYYY-MM-DD`. | -| `website` | `String` | Offisiell nettside. | -| `added` | `Int!` | Antall RAWG-brukere som har lagt spillet til i en liste. Brukes som mål på popularitet. | -| `metacritic` | `Int` | Metacritic-score, 0–100. | -| `rating` | `Float` | RAWG-brukernes snittvurdering, 0–5. | -| `ratingsCount` | `Int!` | Antall vurderinger bak `rating`. | -| `playtime` | `Int` | Gjennomsnittlig spilletid i timer. | -| `esrbRating` | `String` | Amerikansk aldersgrense, f.eks. `Mature`. | -| `dominantColor` | `String` | Hovedfargen i forsidebildet som hex, f.eks. `0f0f0f`. | -| `backgroundImage` | `String` | URL til forsidebildet. | -| `backgroundImageAdditional` | `String` | URL til et ekstra bilde. | -| `screenshots` | `[String!]!` | URL-er til skjermbilder. Tom liste hvis ingen. | -| `genres` | [`[Facet!]!`](#facet) | Sjangre. | -| `platforms` | [`[Facet!]!`](#facet) | Enkeltkonsoller, f.eks. PlayStation 4. | -| `stores` | [`[Facet!]!`](#facet) | Butikker spillet selges i. | -| `developers` | [`[Facet!]!`](#facet) | Utviklere. | -| `publishers` | [`[Facet!]!`](#facet) | Utgivere. | -| `tags` | [`[Facet!]!`](#facet) | Tagger fra RAWG, f.eks. Open World. | +| Felt | Type | Beskrivelse | +| --------------------------- | --------------------- | --------------------------------------------------------------------------------------- | +| `id` | `ID!` | ID til spillet. | +| `name` | `String!` | Tittel. | +| `description` | `String` | Beskrivelse i ren tekst. | +| `released` | `String` | Utgivelsesdato, `YYYY-MM-DD`. | +| `website` | `String` | Offisiell nettside. | +| `added` | `Int!` | Antall RAWG-brukere som har lagt spillet til i en liste. Brukes som mål på popularitet. | +| `metacritic` | `Int` | Metacritic-score, 0–100. | +| `rating` | `Float` | RAWG-brukernes snittvurdering, 0–5. | +| `ratingsCount` | `Int!` | Antall vurderinger bak `rating`. | +| `playtime` | `Int` | Gjennomsnittlig spilletid i timer. | +| `esrbRating` | `String` | Amerikansk aldersgrense, f.eks. `Mature`. | +| `dominantColor` | `String` | Hovedfargen i forsidebildet som hex, f.eks. `0f0f0f`. | +| `backgroundImage` | `String` | URL til forsidebildet. | +| `backgroundImageAdditional` | `String` | URL til et ekstra bilde. | +| `screenshots` | `[String!]!` | URL-er til skjermbilder. Tom liste hvis ingen. | +| `genres` | [`[Facet!]!`](#facet) | Sjangre. | +| `platforms` | [`[Facet!]!`](#facet) | Enkeltkonsoller, f.eks. PlayStation 4. | +| `stores` | [`[Facet!]!`](#facet) | Butikker spillet selges i. | +| `developers` | [`[Facet!]!`](#facet) | Utviklere. | +| `publishers` | [`[Facet!]!`](#facet) | Utgivere. | +| `tags` | [`[Facet!]!`](#facet) | Tagger fra RAWG, f.eks. Open World. | Relasjonene returnerer en tom liste hvis spillet ikke har noen. @@ -252,11 +273,11 @@ Felles type for sjangre, plattformer, butikker, utviklere, utgivere og tagger. Resultatet av [`games`](#games). Følger Relay-mønsteret for paginering (se [dokumentasjon](https://graphql.org/learn/pagination/)). -| Felt | Type | Beskrivelse | -| ------------ | --------------------------- | --------------------------------------------------- | -| `edges` | [`[GameEdge!]!`](#gameedge) | Spillene på denne siden. | -| `pageInfo` | [`PageInfo!`](#pageinfo) | Informasjon for å hente neste side. | -| `totalCount` | `Int!` | Totalt antall spill som matcher søk og filtre. | +| Felt | Type | Beskrivelse | +| ------------ | --------------------------- | ---------------------------------------------- | +| `edges` | [`[GameEdge!]!`](#gameedge) | Spillene på denne siden. | +| `pageInfo` | [`PageInfo!`](#pageinfo) | Informasjon for å hente neste side. | +| `totalCount` | `Int!` | Totalt antall spill som matcher søk og filtre. | --- @@ -264,18 +285,18 @@ Resultatet av [`games`](#games). Følger Relay-mønsteret for paginering (se [do Ett spill i en [`GameConnection`](#gameconnection). -| Felt | Type | Beskrivelse | -| -------- | -------------------- | ------------------------------------ | -| `node` | [`Game!`](#game-1) | Spillet. | -| `cursor` | `String!` | Posisjonen til spillet i listen. | +| Felt | Type | Beskrivelse | +| -------- | ------------------ | -------------------------------- | +| `node` | [`Game!`](#game-1) | Spillet. | +| `cursor` | `String!` | Posisjonen til spillet i listen. | --- ### PageInfo -| Felt | Type | Beskrivelse | -| ------------- | ---------- | ------------------------------------------------------------- | -| `hasNextPage` | `Boolean!` | `true` hvis det finnes flere spill etter denne siden. | +| Felt | Type | Beskrivelse | +| ------------- | ---------- | ------------------------------------------------------------------- | +| `hasNextPage` | `Boolean!` | `true` hvis det finnes flere spill etter denne siden. | | `endCursor` | `String` | Cursor til siste spill på siden. Sendes som `after` for neste side. | --- @@ -284,13 +305,11 @@ Ett spill i en [`GameConnection`](#gameconnection). Input-type for filtrering i [`games`](#games). Alle felt er valgfrie. -| Felt | Type | Beskrivelse | -| -------------- | -------- | ------------------------------------------------------------------------------ | -| `genreIds` | `[ID!]` | Spill som har **alle** de valgte sjangrene. ID-er fra [`genres`](#genres). | -| `platformIds` | `[ID!]` | Spill på **minst én** av de valgte plattformfamiliene. ID-er fra [`platforms`](#platforms). | -| `releasedFrom` | `Int` | Utgitt dette året eller senere, f.eks. `2015`. | -| `releasedTo` | `Int` | Utgitt dette året eller tidligere, f.eks. `2020`. | +| Felt | Type | Beskrivelse | +| -------------- | ------- | ------------------------------------------------------------------------------------------- | +| `genreIds` | `[ID!]` | Spill som har **alle** de valgte sjangrene. ID-er fra [`genres`](#genres). | +| `platformIds` | `[ID!]` | Spill på **minst én** av de valgte plattformfamiliene. ID-er fra [`platforms`](#platforms). | +| `releasedFrom` | `Int` | Utgitt dette året eller senere, f.eks. `2015`. | +| `releasedTo` | `Int` | Utgitt dette året eller tidligere, f.eks. `2020`. | Merk: Spill uten utgivelsesdato utelates når `releasedFrom` eller `releasedTo` er satt. ID-er som ikke er tall ignoreres. - - diff --git a/backend/package.json b/backend/package.json index ec1a6342..ee7697e9 100644 --- a/backend/package.json +++ b/backend/package.json @@ -11,7 +11,9 @@ "start": "pnpm run build && node dist/index.js", "lint": "eslint ." }, - "files": ["dist"], + "files": [ + "dist" + ], "keywords": [], "author": "", "license": "ISC", diff --git a/backend/src/auth.ts b/backend/src/auth.ts index 7816c105..3e717471 100644 --- a/backend/src/auth.ts +++ b/backend/src/auth.ts @@ -1,3 +1,2 @@ - // server jwt secret -// const secret = process.env.JWT_SECRET; \ No newline at end of file +// const secret = process.env.JWT_SECRET; diff --git a/backend/src/db.ts b/backend/src/db.ts index ead91fad..4b81d4df 100644 --- a/backend/src/db.ts +++ b/backend/src/db.ts @@ -1,7 +1,7 @@ import pg from 'pg'; const url = process.env.DATABASE_URL; -if (!url) throw new Error("DATABASE_URL not set in .env ...") +if (!url) throw new Error('DATABASE_URL not set in .env ...'); //pg pool -export const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL}) +export const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL }); diff --git a/backend/src/index.ts b/backend/src/index.ts index ba22d187..514ffc78 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -1,11 +1,11 @@ -import { ApolloServer } from "@apollo/server"; -import { startStandaloneServer } from "@apollo/server/standalone"; +import { ApolloServer } from '@apollo/server'; +import { startStandaloneServer } from '@apollo/server/standalone'; import { pool } from './db.js'; import { typeDefs } from './schema.js'; import { resolvers } from './resolvers.js'; -// apollo-server port number [default port 3001] -const port = Number(process.env.PORT ?? 3001) +// apollo-server port number [default port 3001] +const port = Number(process.env.PORT ?? 3001); // tester db tilkobling med en query await pool.query('SELECT 1'); @@ -15,7 +15,7 @@ const server = new ApolloServer({ typeDefs, resolvers }); // pass apolloserver to standaloneserver const { url } = await startStandaloneServer(server, { - listen: { port }, + listen: { port }, }); -console.log(`Server ready at: ${url}`); \ No newline at end of file +console.log(`Server ready at: ${url}`); diff --git a/backend/src/resolvers.ts b/backend/src/resolvers.ts index 5537c4fc..757dc296 100644 --- a/backend/src/resolvers.ts +++ b/backend/src/resolvers.ts @@ -1,4 +1,3 @@ -import { assertName } from 'graphql'; import { pool } from './db.js'; // type definitions @@ -13,12 +12,12 @@ type GameFilter = { platformIds?: string[] | null; releasedFrom?: number | null; releasedTo?: number | null; -} +}; // const vars and func const VALID_ID_REGEX = /^\d{1,18}$/; const MAX_PAGE_SIZE = 50; -const escapeChars = (s: string) => s.replace(/[\\%_]/g, '\\$&') +const escapeChars = (s: string) => s.replace(/[\\%_]/g, '\\$&'); // definerer hvilke kolonner som skal hentes for en generell // forespørsel om et spill, inkludert oversetting fra databasen @@ -74,7 +73,6 @@ const GAMES_WHERE = ` AND ${WHERE_RELEASED_TO} `; - // SQL queries const SQL_COUNT = ` SELECT count(*)::int AS count FROM games @@ -101,18 +99,18 @@ const SQL_GAMES = ` // hjelpefunksjoner function toIds(ids?: string[] | null) { - const nums = (ids??[]).map(Number).filter(Number.isInteger); - return nums.length? nums : null; + const nums = (ids ?? []).map(Number).filter(Number.isInteger); + return nums.length ? nums : null; } // returnerer array med filter-verdiene function filterValues(args: GamesArgs) { const term = args.search?.trim(); return [ - term? `%${escapeChars(term)}%` : null, + term ? `%${escapeChars(term)}%` : null, toIds(args.filter?.genreIds), toIds(args.filter?.platformIds), - args.filter?.releasedFrom??null, - args.filter?.releasedTo??null, + args.filter?.releasedFrom ?? null, + args.filter?.releasedTo ?? null, ]; } // resolver for koblings-tabellene i databasen @@ -124,27 +122,29 @@ function relation(table: string, junc: string, fk: string) { WHERE j.game_id = $1 ORDER BY t.name `; - return async (game: {id: string }) => { + return async (game: { id: string }) => { const res = await pool.query(sql, [game.id]); return res.rows; }; -}; +} // resolver-funksjoner async function game(_parent: unknown, args: { id: string }) { - if (!VALID_ID_REGEX.test(args.id)) { return null; } - const res = await pool.query(SQL_GAME, [args.id]) + if (!VALID_ID_REGEX.test(args.id)) { + return null; + } + const res = await pool.query(SQL_GAME, [args.id]); return res.rows[0] ?? null; -}; +} -async function games(_parent: unknown, args: GamesArgs ) { +async function games(_parent: unknown, args: GamesArgs) { const values = filterValues(args); - const first = Math.min(Math.max(args.first, 1), MAX_PAGE_SIZE); + const first = Math.min(Math.max(args.first, 1), MAX_PAGE_SIZE); const res = await pool.query(SQL_GAMES, [...values, first]); return { edges: res.rows.map((row) => ({ node: row, cursor: '' })), - pageInfo: { hasNextPage: false, endCursor: null}, + pageInfo: { hasNextPage: false, endCursor: null }, values, }; } @@ -174,5 +174,5 @@ export const resolvers = { tags: relation('tags', 'game_tags', 'tag_id'), }, - GameConnection: { totalCount } + GameConnection: { totalCount }, }; diff --git a/backend/src/schema.ts b/backend/src/schema.ts index 5e1b1c01..9d2621e1 100644 --- a/backend/src/schema.ts +++ b/backend/src/schema.ts @@ -1,4 +1,3 @@ - export const typeDefs = `#graphql """ Definerer input argumentene som er gyldige mot serveren. @@ -83,4 +82,4 @@ export const typeDefs = `#graphql genres: [Facet!]! platforms: [Facet!]! } -`; \ No newline at end of file +`; diff --git a/db/database-connect.md b/db/database-connect.md index 22b178e7..627b09dc 100644 --- a/db/database-connect.md +++ b/db/database-connect.md @@ -1,10 +1,13 @@ # Database-connect + ## 1. Konfigurere `.env` + 1. Koble til NTNU VPN hvis du er utenfor campus. 2. Kopier `backend/.env.example` til `backend/.env` 3. Fyll inn verdien til `VM_USER`, og `PASSORD` i `DATABASE_URL`. `backend/.env` skal se ut slik: + ``` # tunnel script env vars VM_USER= @@ -17,9 +20,12 @@ PORT=3001 JWT_TOKEN= ``` -## 2. Database-tilkobling under utvikling +## 2. Database-tilkobling under utvikling + #### Linux/Mac + Gå til rot i repo (dvs. `T02-Project-2` mappen) og kjør kommandoen: + ```bash pnpm db:tunnel ``` @@ -27,10 +33,13 @@ pnpm db:tunnel La terminalen stå åpen, og start backend i en annen terminal med `pnpm --filter backend dev`. #### Windows + Start tunnelen i PowerShell: + ```powershell ssh -N -L 5433:localhost:5432 brukernavn@it2810-02.idi.ntnu.no ``` + Alternativt kan scriptet kjøres direkte i Git Bash eller WSL med ./scripts/db-tunnel.sh. Portsjekken i scriptet bruker lsof, som ikke finnes i Git Bash, men den hoppes da bare over. #### Verifiser @@ -50,6 +59,7 @@ await c.end(); Skal skrive ut `[ { count: '120067' } ]`. #### Mulige feil + 1. Connection refused på 5433 betyr at tunnelen ikke kjører. 2. Connection timed out mot VM-en betyr som regel at VPN mangler. 3. Password authentication failed betyr feil passord eller rolle i DATABASE_URL. diff --git a/db/database-layout.md b/db/database-layout.md index 58c91f4d..bd9345ed 100644 --- a/db/database-layout.md +++ b/db/database-layout.md @@ -1,4 +1,3 @@ - #### Database-layout Under er utskrift fra postgres lokalt på vm. Eksemplene viser henholdsvis alle tabellene i databasen vår `gamesdb`, og alle kolonnene som tabellen `games` inneholder. @@ -44,4 +43,3 @@ gamesdb-> \d games dominant_color | text | | | ``` - diff --git a/db/database-setup.md b/db/database-setup.md index b786ff44..2e3f26b5 100644 --- a/db/database-setup.md +++ b/db/database-setup.md @@ -60,11 +60,11 @@ psql "$DATABASE_URL" -f grants.sql `load.py` leser Parquet-filen med DuckDB, deler opp flerverdi-felter til oppslags- og koblingstabeller, laster alt med `COPY`, oppretter indekser etter lasting og kjører `ANALYZE`. Filstien kan overstyres med `PARQUET_PATH`. Finnes tabellene allerede, avbryter scriptet. `python load.py --force` sletter og laster alt på nytt, inkludert reviews. -| Fil | Innhold | -| --- | --- | -| `schema.sql` | Tabeller: `games`, oppslagstabeller, koblingstabeller, `reviews` | +| Fil | Innhold | +| ------------- | --------------------------------------------------------------------------------------- | +| `schema.sql` | Tabeller: `games`, oppslagstabeller, koblingstabeller, `reviews` | | `indexes.sql` | Trigram-indeks for søk, keyset-indekser for sortering, omvendte indekser for filtrering | -| `load.py` | Import fra Parquet til Postgres | +| `load.py` | Import fra Parquet til Postgres | ## 6. Verifiser diff --git a/db/database-usage.md b/db/database-usage.md index 142ddfa9..7795a683 100644 --- a/db/database-usage.md +++ b/db/database-usage.md @@ -1,13 +1,15 @@ ## Roller for databasen i postgres -|Rolle|Bruk|Rettigheter| -|-|-|-| -|postgres|Administrasjon|Superuser| -|gameapp|Import og schemaendringer|Eier av gamesdb| -|webapp|Backend|SELECT på alt, INSERT på reviews| +| Rolle | Bruk | Rettigheter | +| -------- | ------------------------- | -------------------------------- | +| postgres | Administrasjon | Superuser | +| gameapp | Import og schemaendringer | Eier av gamesdb | +| webapp | Backend | SELECT på alt, INSERT på reviews | ## Koble til postgres + #### Fra vm-en: + ```bash psql -h localhost -U webapp -d gamesdb psql -h localhost -U gameapp -d gamesdb @@ -15,11 +17,13 @@ sudo -u postgres psql -d gamesdb ``` #### Fra egen maskin (`pnpm db:tunnel`): + ```bash psql -h localhost -p 5433 -U webapp -d gamesdb ``` #### Backend bruker `DATABASE_URL` i `backend/.env`: + ``` postgresql://webapp:PASSORD@localhost:5433/gamesdb ``` @@ -46,6 +50,7 @@ Under er noen generelle kommandoer som kan brukes i postgres for navigere databa ``` Se på data i tabellen (spørringer): + ```SQL // viser første 5 spillene i games-tabellen SELECT * FROM games LIMIT 5; @@ -60,6 +65,7 @@ SELECT metacritic FROM games WHERE id = 4291; Filer i `backend/db/`: `schema.sql`, `indexes.sql`, `grants.sql`, `load.py`, `requirements.txt`. _Full reimport på VM-en (sletter også reviews)_: + ``` cd backend/db && source .venv/bin/activate export DATABASE_URL='postgresql://gameapp:PASSORD@localhost:5432/gamesdb' @@ -67,10 +73,11 @@ python load.py --force psql "$DATABASE_URL" -f grants.sql ``` ->[!NOTE] ->`grants.sql` må kjøres etter hver reimport, siden tabellene gjenopprettes og mister rettighetene. +> [!NOTE] +> `grants.sql` må kjøres etter hver reimport, siden tabellene gjenopprettes og mister rettighetene. _Sjekk status på tjenesten_: + ```bash sudo systemctl status postgresql sudo systemctl restart postgresql diff --git a/frontend/src/App.css b/frontend/src/App.css index 8a106e5a..2bf280f5 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -27,4 +27,4 @@ font-family: var(--mono); font-size: 13px; color: var(--text); -} \ No newline at end of file +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index bd67ad93..af1d58ba 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -45,7 +45,8 @@ function App() { // Teksten over lista. role="status" gjør at skjermlesere leser den opp når den endres let status = ''; if (loading && !result) status = 'Laster …'; - else if (result && !showFavorites) status = `${result.games.totalCount.toLocaleString('nb-NO')} treff`; + else if (result && !showFavorites) + status = `${result.games.totalCount.toLocaleString('nb-NO')} treff`; const toggleFavorite = (id: string) => { setFavorites((prev) => { @@ -70,7 +71,7 @@ function App() {

{showFavorites ? 'Favoritter' : 'Søkeresultater'}

-
+