diff --git a/backend/.env.example b/backend/.env.example index 6aed5c0..68799a7 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,2 +1,9 @@ -DATABASE_URL=postgresql://gameapp:@localhost:5432/gamesdb +# tunnel script env vars +VM_USER= +VM_HOST=it2810-02.idi.ntnu.no +TUNNEL_PORT=5433 + +# backend server env vars +DATABASE_URL=postgresql://webapp:@localhost:5433/gamesdb PORT=3001 +JWT_TOKEN= \ No newline at end of file diff --git a/backend/package.json b/backend/package.json index e70e316..17b9e07 100644 --- a/backend/package.json +++ b/backend/package.json @@ -5,7 +5,7 @@ "main": "index.js", "type": "module", "scripts": { - "dev": "tsx watch src/index.ts", + "dev": "tsx watch --env-file=.env src/index.ts", "compile": "tsc", "build": "tsc", "start": "pnpm run build && node dist/index.js", diff --git a/backend/src/auth.ts b/backend/src/auth.ts new file mode 100644 index 0000000..7816c10 --- /dev/null +++ b/backend/src/auth.ts @@ -0,0 +1,3 @@ + +// server jwt secret +// const secret = process.env.JWT_SECRET; \ No newline at end of file diff --git a/backend/src/db.ts b/backend/src/db.ts new file mode 100644 index 0000000..ead91fa --- /dev/null +++ b/backend/src/db.ts @@ -0,0 +1,7 @@ +import pg from 'pg'; + +const url = process.env.DATABASE_URL; +if (!url) throw new Error("DATABASE_URL not set in .env ...") + +//pg pool +export const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL}) diff --git a/backend/src/index.ts b/backend/src/index.ts index d9926ee..ba22d18 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -1,47 +1,21 @@ import { ApolloServer } from "@apollo/server"; import { startStandaloneServer } from "@apollo/server/standalone"; -import { Query } from "pg"; -import { start } from "repl"; +import { pool } from './db.js'; +import { typeDefs } from './schema.js'; +import { resolvers } from './resolvers.js'; -const typeDefs = `#graphql - # comment - type Book { - title: String - author: String - } - - type Query { - books: [Book] - } -`; - -const books = [ - { - title: 'The awakening', - author: 'Kate Chopin', - }, - { - title: 'City of glass', - author: 'Paul Auster', - }, -]; - -const resolvers = { - Query: { - books: () => books, - }, -}; +// 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'); // apollo server instance -const server = new ApolloServer({ - typeDefs, - resolvers, -}); +const server = new ApolloServer({ typeDefs, resolvers }); // pass apolloserver to standaloneserver const { url } = await startStandaloneServer(server, { - listen: { port: 4000 }, + listen: { port }, }); console.log(`Server ready at: ${url}`); \ No newline at end of file diff --git a/backend/src/resolvers.ts b/backend/src/resolvers.ts new file mode 100644 index 0000000..bbbca36 --- /dev/null +++ b/backend/src/resolvers.ts @@ -0,0 +1,89 @@ +import { pool } from './db.js'; + +const VALID_ID_REGEX = /^\d{1,18}$/; +const MAX_PAGE_SIZE = 50; + +// definerer hvilke kolonner som skal hentes for en generell +// forespørsel om et spill, inkludert oversetting fra databasen +// sin navnekonvensjon til GraphQL, og konvertering av typer +const GAME_COLS = ` + id, + name, + description, + released::text AS "released", + website, + added, + metacritic, + esrb_rating AS "esrbRating", + dominant_color AS "dominantColor", + background_image AS "backgroundImage", + background_image_additional AS "backgroundImageAdditional", + COALESCE(screenshots, '{}') AS "screenshots" +`; + +// SQL queries +const SQL_COUNT = ` + SELECT count(*)::int AS count FROM games +`; + +const SQL_GAME = ` + SELECT ${GAME_COLS} FROM games + WHERE id = $1 +`; + +const SQL_GAMES = ` + SELECT ${GAME_COLS} FROM games + ORDER BY added DESC, id DESC + LIMIT $1 +`; + +// resolver for koblings-tabellene i databasen +const relation = (table: string, junc: string, fk: string) => { + const sql = ` + SELECT t.id, t.name + FROM ${junc} j + JOIN ${table} t ON t.id = j.${fk} + WHERE j.game_id = $1 + ORDER BY t.name + `; + return async (game: {id: string }) => { + const res = await pool.query(sql, [game.id]); + return res.rows; + }; +}; + +// resolvers for å hente kjøre spørringer mot databasen +export const resolvers = { + Query: { + // ett spill basert på id + game: async (_parent: unknown, args: { id: string }) => { + if (!VALID_ID_REGEX.test(args.id)) { return null; } + const res = await pool.query(SQL_GAME, [args.id]) + return res.rows[0] ?? null; + }, + + // liste med spill formattert som en GameConnection + games: async (_parent: unknown, args: { first: number }) => { + const first = Math.min(Math.max(args.first, 1), MAX_PAGE_SIZE); + const res = await pool.query(SQL_GAMES, [first]); + return { + edges: res.rows.map((row) => ({ node: row, cursor: '' })), + pageInfo: { hasNextPage: false, endCursor: null}, + }; + } + }, + + Game: { + genres: relation('genres', 'game_genres', 'genre_id'), + platforms: relation('platforms', 'game_platforms', 'platform_id'), + parentPlatforms: relation('parent_platforms', 'game_parent_platforms', 'parent_platform_id'), + stores: relation('stores', 'game_stores', 'store_id'), + developers: relation('developers', 'game_developers', 'developer_id'), + publishers: relation('publishers', 'game_publishers', 'publisher_id'), + tags: relation('tags', 'game_tags', 'tag_id'), + }, + + GameConnection: { + totalCount: async() => { return (await pool.query(SQL_COUNT)).rows[0].count; } + } +}; diff --git a/backend/src/schema.ts b/backend/src/schema.ts new file mode 100644 index 0000000..c18d3a3 --- /dev/null +++ b/backend/src/schema.ts @@ -0,0 +1,71 @@ + +export const typeDefs = `#graphql + """ + Generell type for attributter som sjanger, platform, etc. + """ + type Facet { + id: ID! + name: String! + } + + """ + Game holder all informasjon om ett enkelt spill. + """ + type Game { + id: ID!, + name: String!, + description: String, + released: String, + website: String, + added: Int!, + metacritic: Int, + esrbRating: String, + dominantColor: String, + backgroundImage: String, + backgroundImageAdditional: String, + screenshots: [String!]! + + genres: [Facet!]! + platforms: [Facet!]! + parentPlatforms: [Facet!]! + stores: [Facet!]! + developers: [Facet!]! + publishers: [Facet!]! + tags: [Facet!]! + } + + """ + Resultatet av en 'games' query, dette returnerer en 'page' + med mange 'edges' (spill) og kontroll-informasjon for pagination. + """ + type GameConnection { + edges: [GameEdge!]! + pageInfo: PageInfo! + totalCount: Int! + } + + """ + Én 'game-edge' fra GameConnection. Inneholder informasjon om spillet (Game) + og posisjonen til spillet i lista (cursor). + """ + type GameEdge { + node: Game! + cursor: String! + } + + """ + Info om nåværende page, kontrollinformasjon for neste forespørsel. + """ + type PageInfo { + hasNextPage: Boolean! + endCursor: String + } + + """ + Alle gyldige queries mot backend er definert her. + """ + type Query { + game(id: ID!): Game + games(first: Int=20): GameConnection! + } +`; \ No newline at end of file