From b02609f009380b1ea6277ee3aa0bd9a30eafcdff Mon Sep 17 00:00:00 2001 From: Bradley Shellnut Date: Sun, 25 Jun 2023 23:14:26 -0700 Subject: [PATCH] Update packages, add to game types and mapping values, adding slug to publisher, and creating things when searching. --- package.json | 6 +- pnpm-lock.yaml | 55 ++++----- prisma/schema.prisma | 83 +++++++------ src/lib/types.ts | 26 ++++ src/lib/util/gameMapper.ts | 28 +++-- src/routes/search/+page.server.ts | 181 +++++++++++++++++++++++----- src/routes/wishlist/+page.server.ts | 13 +- 7 files changed, 276 insertions(+), 116 deletions(-) diff --git a/package.json b/package.json index 5200391..7edb46c 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "@rgossiaux/svelte-heroicons": "^0.1.2", "@sveltejs/adapter-auto": "^1.0.3", "@sveltejs/adapter-vercel": "^1.0.6", - "@sveltejs/kit": "^1.20.4", + "@sveltejs/kit": "^1.20.5", "@types/cookie": "^0.5.1", "@types/node": "^18.16.18", "@typescript-eslint/eslint-plugin": "^5.60.0", @@ -34,7 +34,7 @@ "autoprefixer": "^10.4.14", "eslint": "^8.43.0", "eslint-config-prettier": "^8.8.0", - "eslint-plugin-svelte": "^2.31.0", + "eslint-plugin-svelte": "^2.31.1", "just-clone": "^6.2.0", "just-debounce-it": "^3.2.0", "postcss": "^8.4.24", @@ -48,7 +48,7 @@ "svelte": "^3.59.2", "svelte-check": "^2.10.3", "svelte-preprocess": "^5.0.4", - "sveltekit-superforms": "^1.1.1", + "sveltekit-superforms": "^1.1.2", "ts-node": "^10.9.1", "tslib": "^2.5.3", "typescript": "^4.9.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c8633f9..fcc778c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -70,7 +70,7 @@ dependencies: version: 0.2.1(svelte@3.59.2) sveltekit-flash-message: specifier: ^0.11.3 - version: 0.11.3(@sveltejs/kit@1.20.4)(svelte@3.59.2) + version: 0.11.3(@sveltejs/kit@1.20.5)(svelte@3.59.2) zod-to-json-schema: specifier: ^3.21.2 version: 3.21.2(zod@3.21.4) @@ -87,13 +87,13 @@ devDependencies: version: 0.1.2(svelte@3.59.2) '@sveltejs/adapter-auto': specifier: ^1.0.3 - version: 1.0.3(@sveltejs/kit@1.20.4) + version: 1.0.3(@sveltejs/kit@1.20.5) '@sveltejs/adapter-vercel': specifier: ^1.0.6 - version: 1.0.6(@sveltejs/kit@1.20.4) + version: 1.0.6(@sveltejs/kit@1.20.5) '@sveltejs/kit': - specifier: ^1.20.4 - version: 1.20.4(svelte@3.59.2)(vite@4.3.9) + specifier: ^1.20.5 + version: 1.20.5(svelte@3.59.2)(vite@4.3.9) '@types/cookie': specifier: ^0.5.1 version: 0.5.1 @@ -116,8 +116,8 @@ devDependencies: specifier: ^8.8.0 version: 8.8.0(eslint@8.43.0) eslint-plugin-svelte: - specifier: ^2.31.0 - version: 2.31.0(eslint@8.43.0)(svelte@3.59.2)(ts-node@10.9.1) + specifier: ^2.31.1 + version: 2.31.1(eslint@8.43.0)(svelte@3.59.2)(ts-node@10.9.1) just-clone: specifier: ^6.2.0 version: 6.2.0 @@ -158,8 +158,8 @@ devDependencies: specifier: ^5.0.4 version: 5.0.4(postcss-load-config@4.0.1)(postcss@8.4.24)(sass@1.63.6)(svelte@3.59.2)(typescript@4.9.5) sveltekit-superforms: - specifier: ^1.1.1 - version: 1.1.1(@sveltejs/kit@1.20.4)(svelte@3.59.2)(zod@3.21.4) + specifier: ^1.1.2 + version: 1.1.2(@sveltejs/kit@1.20.5)(svelte@3.59.2)(zod@3.21.4) ts-node: specifier: ^10.9.1 version: 10.9.1(@types/node@18.16.18)(typescript@4.9.5) @@ -1123,7 +1123,7 @@ packages: nopt: 5.0.0 npmlog: 5.0.1 rimraf: 3.0.2 - semver: 7.5.2 + semver: 7.5.3 tar: 6.1.13 transitivePeerDependencies: - encoding @@ -1211,21 +1211,21 @@ packages: picomatch: 2.3.1 dev: true - /@sveltejs/adapter-auto@1.0.3(@sveltejs/kit@1.20.4): + /@sveltejs/adapter-auto@1.0.3(@sveltejs/kit@1.20.5): resolution: {integrity: sha512-hc7O12YQqvZ1CD4fo1gMJuPzBZvuoG5kwxb2RRoz4fVoB8B2vuPO2cY751Ln0G6T/HMrAf8kCqw6Pg+wbxcstw==} peerDependencies: '@sveltejs/kit': ^1.0.0 dependencies: - '@sveltejs/kit': 1.20.4(svelte@3.59.2)(vite@4.3.9) + '@sveltejs/kit': 1.20.5(svelte@3.59.2)(vite@4.3.9) import-meta-resolve: 2.2.0 dev: true - /@sveltejs/adapter-vercel@1.0.6(@sveltejs/kit@1.20.4): + /@sveltejs/adapter-vercel@1.0.6(@sveltejs/kit@1.20.5): resolution: {integrity: sha512-fo6aaEygPd/6B5Jms4Ff7R4jbADnppuLvKOWBNTGe5MGB7ZRUkl+gxHWMQx2av2knyEZkA6V8y5M6R3ML5yN4g==} peerDependencies: '@sveltejs/kit': ^1.0.0 dependencies: - '@sveltejs/kit': 1.20.4(svelte@3.59.2)(vite@4.3.9) + '@sveltejs/kit': 1.20.5(svelte@3.59.2)(vite@4.3.9) '@vercel/nft': 0.22.6 esbuild: 0.16.8 transitivePeerDependencies: @@ -1233,8 +1233,8 @@ packages: - supports-color dev: true - /@sveltejs/kit@1.20.4(svelte@3.59.2)(vite@4.3.9): - resolution: {integrity: sha512-MmAzIuMrP7A+8fqDVbxm6ekGHRHL/+Fk8sQPAzPG4G2TxUDtHdn/WcIxeEqHzARMf0OtGSC+VPyOSFuw2Cy2Mg==} + /@sveltejs/kit@1.20.5(svelte@3.59.2)(vite@4.3.9): + resolution: {integrity: sha512-8rJYZ2boRlO75lwpbpB+DlSzIwmTuamXTpVlDtw4dBk86o3UaDe/+Ro4xCsV/4FtTw2U8xPHyV83edAWbQHG0w==} engines: {node: ^16.14 || >=18} hasBin: true requiresBuild: true @@ -1446,7 +1446,7 @@ packages: debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 - semver: 7.5.2 + semver: 7.5.3 tsutils: 3.21.0(typescript@4.9.5) typescript: 4.9.5 transitivePeerDependencies: @@ -2027,8 +2027,8 @@ packages: eslint: 8.43.0 dev: true - /eslint-plugin-svelte@2.31.0(eslint@8.43.0)(svelte@3.59.2)(ts-node@10.9.1): - resolution: {integrity: sha512-Q70jPFRraTkc/giPSfY7yuatmJcb5fPelWNplevqd45gfaJDjc3qXRtWQ6m9U5tWVVYERU9dcdUod294vwD8Gw==} + /eslint-plugin-svelte@2.31.1(eslint@8.43.0)(svelte@3.59.2)(ts-node@10.9.1): + resolution: {integrity: sha512-08v+DqzHiwIVEbi+266D7+BDhayp9OSqCwa/lHaZlZOlFY0vZLYs/h7SkkUPzA5fTVt8OUJBtvCxFiWEYOvvGg==} engines: {node: ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^7.0.0 || ^8.0.0-0 @@ -2047,6 +2047,7 @@ packages: postcss-load-config: 3.1.4(postcss@8.4.24)(ts-node@10.9.1) postcss-safe-parser: 6.0.0(postcss@8.4.24) postcss-selector-parser: 6.0.13 + semver: 7.5.3 svelte: 3.59.2 svelte-eslint-parser: 0.31.0(svelte@3.59.2) transitivePeerDependencies: @@ -3447,8 +3448,8 @@ packages: lru-cache: 6.0.0 dev: true - /semver@7.5.2: - resolution: {integrity: sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==} + /semver@7.5.3: + resolution: {integrity: sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==} engines: {node: '>=10'} hasBin: true dependencies: @@ -3764,24 +3765,24 @@ packages: resolution: {integrity: sha512-vzSyuGr3eEoAtT/A6bmajosJZIUWySzY2CzB3w2pgPvnkUjGqlDnsNnA0PMO+mMAhuyMul6C2uuZzY6ELSkzyA==} engines: {node: '>= 8'} - /sveltekit-flash-message@0.11.3(@sveltejs/kit@1.20.4)(svelte@3.59.2): + /sveltekit-flash-message@0.11.3(@sveltejs/kit@1.20.5)(svelte@3.59.2): resolution: {integrity: sha512-tMKBobVFLYth0z2Kq9M+pi7Ip2OBhOrzivzx64v9+D2bWRwnZ3pmdWStYfQYxlz5CayozRQsmtipNg1vM+JN9Q==} peerDependencies: '@sveltejs/kit': ^1.0.0 svelte: ^3 dependencies: - '@sveltejs/kit': 1.20.4(svelte@3.59.2)(vite@4.3.9) + '@sveltejs/kit': 1.20.5(svelte@3.59.2)(vite@4.3.9) svelte: 3.59.2 dev: false - /sveltekit-superforms@1.1.1(@sveltejs/kit@1.20.4)(svelte@3.59.2)(zod@3.21.4): - resolution: {integrity: sha512-lD3ov06C/O8hD7mCzEQZ7CTsbHrh6kWhRP3njI9ZSwvG5DW6vhfi6Re/cyYy//cS0oMONI0xBieWyr9s24Y9jA==} + /sveltekit-superforms@1.1.2(@sveltejs/kit@1.20.5)(svelte@3.59.2)(zod@3.21.4): + resolution: {integrity: sha512-oWW3+Phcs/CkUKGEx0IsCr6Dw3a22/MVs4Tvi8k/jh66pqwP/jOZu3/dT367pkbIh5T7NK20yrnbHu8nm2E7pQ==} peerDependencies: '@sveltejs/kit': 1.x - svelte: 3.x + svelte: 3.x || 4.x zod: 3.x dependencies: - '@sveltejs/kit': 1.20.4(svelte@3.59.2)(vite@4.3.9) + '@sveltejs/kit': 1.20.5(svelte@3.59.2)(vite@4.3.9) svelte: 3.59.2 zod: 3.21.4 dev: true diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b9a99d1..3806639 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -129,8 +129,8 @@ model Game { id String @id @default(cuid()) name String slug String - description String? - description_preview String? + description String? @db.LongText + description_preview String? @db.LongText year_published Int? min_players Int? max_players Int? @@ -143,6 +143,8 @@ model Game { rules_url String? primary_publisher_id String? primary_publisher Publisher? @relation("PrimaryPublishers", references: [id], fields: [primary_publisher_id]) + primary_designer_id String? + primary_designer Designer? @relation("PrimaryDesigners", references: [id], fields: [primary_designer_id]) categories Category[] mechanics Mechanic[] designers Designer[] @@ -162,37 +164,39 @@ model Game { } model GameName { - id String @id @default(cuid()) - name String - game_id String - game Game @relation(references: [id], fields: [game_id]) - created_at DateTime @default(now()) @db.Timestamp(6) - updated_at DateTime @updatedAt @db.Timestamp(6) + id String @id @default(cuid()) + name String + game_id String + game Game @relation(references: [id], fields: [game_id]) + created_at DateTime @default(now()) @db.Timestamp(6) + updated_at DateTime @updatedAt @db.Timestamp(6) @@index([game_id]) @@map("game_names") } model Publisher { - id String @id @default(cuid()) - name String - games Game[] - primary_publisher Game[] @relation("PrimaryPublishers") - created_at DateTime @default(now()) @db.Timestamp(6) - updated_at DateTime @updatedAt @db.Timestamp(6) + id String @id @default(cuid()) + name String + slug String + external_id String @unique + games Game[] + primary_publisher Game[] @relation("PrimaryPublishers") + created_at DateTime @default(now()) @db.Timestamp(6) + updated_at DateTime @updatedAt @db.Timestamp(6) @@fulltext([name]) @@map("publishers") } model Category { - id String @id @default(cuid()) - name String - slug String - games Game[] - external_id String @unique - created_at DateTime @default(now()) @db.Timestamp(6) - updated_at DateTime @updatedAt @db.Timestamp(6) + id String @id @default(cuid()) + name String + slug String + games Game[] + external_id String @unique + created_at DateTime @default(now()) @db.Timestamp(6) + updated_at DateTime @updatedAt @db.Timestamp(6) @@fulltext([name]) @@map("categories") @@ -212,36 +216,39 @@ model Mechanic { } model Designer { - id String @id @default(cuid()) - name String - games Game[] - created_at DateTime @default(now()) @db.Timestamp(6) - updated_at DateTime @updatedAt @db.Timestamp(6) + id String @id @default(cuid()) + name String + external_id String @unique + games Game[] + primary_designer Game[] @relation("PrimaryDesigners") + created_at DateTime @default(now()) @db.Timestamp(6) + updated_at DateTime @updatedAt @db.Timestamp(6) @@fulltext([name]) @@map("designers") } model Artist { - id String @id @default(cuid()) - name String - games Game[] - created_at DateTime @default(now()) @db.Timestamp(6) - updated_at DateTime @updatedAt @db.Timestamp(6) + id String @id @default(cuid()) + name String + external_id String @unique + games Game[] + created_at DateTime @default(now()) @db.Timestamp(6) + updated_at DateTime @updatedAt @db.Timestamp(6) @@fulltext([name]) @@map("artists") } model Expansion { - id String @id @default(cuid()) - name String - year_published Int? - baseGame Game? @relation(fields: [base_game_id], references: [id]) + id String @id @default(cuid()) + name String + year_published Int? + baseGame Game? @relation(fields: [base_game_id], references: [id]) base_game_id String? - external_id String @unique - created_at DateTime @default(now()) @db.Timestamp(6) - updated_at DateTime @updatedAt @db.Timestamp(6) + external_id String @unique + created_at DateTime @default(now()) @db.Timestamp(6) + updated_at DateTime @updatedAt @db.Timestamp(6) @@fulltext([name]) @@index([base_game_id]) diff --git a/src/lib/types.ts b/src/lib/types.ts index 13c3200..95fdc06 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -57,6 +57,24 @@ export type SavedGameType = { includeInRandom: boolean; }; +export type MechanicType = { + id: string; +}; + +export type CategoryType = { + id: string; +}; + +export type PublisherType = { + id: string; + name: string; +}; + +export type DesignerType = { + id: string; + name: string; +}; + export type GameType = { id: string; handle: string; @@ -71,6 +89,14 @@ export type GameType = { price_au: number; msrp: number; year_published: number; + categories: CategoryType[]; + mechanics: MechanicType[]; + primary_publisher: PublisherType; + publishers: PublisherType[]; + primary_designer: DesignerType; + designers: DesignerType[]; + developers: String[]; + artists: String[]; min_players: number; max_players: number; min_playtime: number; diff --git a/src/lib/util/gameMapper.ts b/src/lib/util/gameMapper.ts index 5939e6a..a0f81ba 100644 --- a/src/lib/util/gameMapper.ts +++ b/src/lib/util/gameMapper.ts @@ -41,22 +41,23 @@ export function mapSavedGameToGame(game: SavedGameType): GameType { }; } -// TODO: TYpe API response +// TODO: Type API response export function mapAPIGameToBoredGame(game: any): GameType { const { id, handle, name, url, - edit_url, thumb_url, image_url, - price, - price_ca, - price_uk, - price_au, - msrp, year_published, + categories, + mechanics, + primary_designer, + designers, + primary_publisher, + publishers, + artists, min_players, max_players, min_playtime, @@ -72,15 +73,16 @@ export function mapAPIGameToBoredGame(game: any): GameType { handle, name, url, - edit_url, thumb_url, image_url, - price, - price_ca, - price_uk, - price_au, - msrp, year_published, + categories, + mechanics, + primary_designer, + designers, + primary_publisher, + publishers, + artists, min_players, max_players, min_playtime, diff --git a/src/routes/search/+page.server.ts b/src/routes/search/+page.server.ts index d08a830..c0e81cf 100644 --- a/src/routes/search/+page.server.ts +++ b/src/routes/search/+page.server.ts @@ -1,46 +1,87 @@ -import { BOARD_GAME_ATLAS_CLIENT_ID } from '$env/static/private'; import { error } from '@sveltejs/kit'; import { superValidate } from 'sveltekit-superforms/server'; +import kebabCase from 'just-kebab-case'; +import { BOARD_GAME_ATLAS_CLIENT_ID } from '$env/static/private'; +import prisma from '$lib/prisma.js'; import type { GameType, SearchQuery } from '$lib/types'; import { mapAPIGameToBoredGame } from '$lib/util/gameMapper'; import { search_schema } from '$lib/zodValidation'; async function searchForGames(urlQueryParams: SearchQuery) { try { - const url = `https://api.boardgameatlas.com/api/search${ - urlQueryParams ? `?${urlQueryParams}` : '' - }`; - const response = await fetch(url, { - method: 'get', - headers: { - 'content-type': 'application/json' + let dbGames = await prisma.game.findMany({ + where: { + name: { + search: urlQueryParams?.name + }, + min_players: { + gte: urlQueryParams?.min_players || 0 + }, + max_players: { + lte: urlQueryParams?.max_players || 100 + }, + min_playtime: { + gte: urlQueryParams?.min_playtime || 0 + }, + max_playtime: { + lte: urlQueryParams?.max_playtime || 5000 + }, + min_age: { + gte: urlQueryParams?.min_age || 130 + } + }, + skip: urlQueryParams?.skip, + take: urlQueryParams?.limit, + orderBy: { + name: 'asc' } }); + console.log('dbGames', dbGames); - if (!response.ok) { - console.log('Status not 200', response.status); - throw error(response.status); - } + if (!dbGames || dbGames.length === 0) { + const url = new URL( + `https://api.boardgameatlas.com/api/search${urlQueryParams ? `?${urlQueryParams}` : ''}` + ); + const headers: HeadersInit = new Headers(); + headers.set('Content-Type', 'application/json'); + const requestInit: RequestInit = { + method: 'GET', + headers + }; + const response = await fetch(url, requestInit); - const games: GameType[] = []; - let totalCount = 0; - if (response.ok) { - const gameResponse = await response.json(); - const gameList: GameType[] = gameResponse?.games; - totalCount = gameResponse?.count; - console.log('totalCount', totalCount); - gameList.forEach((game) => { - if (game?.min_players && game?.max_players) { - game.players = `${game.min_players}-${game.max_players}`; - game.playtime = `${game.min_playtime}-${game.max_playtime}`; - } - games.push(mapAPIGameToBoredGame(game)); - }); + if (!response.ok) { + console.log('Status not 200', response.status); + throw error(response.status); + } + + const games: GameType[] = []; + let totalCount = 0; + if (response.ok) { + const gameResponse = await response.json(); + const gameList: GameType[] = gameResponse?.games; + totalCount = gameResponse?.count; + console.log('totalCount', totalCount); + gameList.forEach((game) => { + if (game?.min_players && game?.max_players) { + game.players = `${game.min_players}-${game.max_players}`; + game.playtime = `${game.min_playtime}-${game.max_playtime}`; + } + const boredGame = mapAPIGameToBoredGame(game); + createOrUpdateGame(boredGame); + games.push(boredGame); + }); + } + return { + totalCount, + games + }; + } else { + return { + totalCount: dbGames.length, + dbGames + }; } - return { - totalCount, - games - }; } catch (e) { console.log(`Error searching board games ${e}`); } @@ -50,7 +91,85 @@ async function searchForGames(urlQueryParams: SearchQuery) { }; } -export const load = async ({ fetch, url }) => { +async function createOrUpdateGame(game: GameType) { + const categoryIds = game.categories.map((category) => ({ + external_id: category.id + })); + const mechanicIds = game.mechanics.map((mechanic) => ({ + external_id: mechanic.id + })); + return await prisma.game.upsert({ + where: { + external_id: game.id + }, + create: { + name: game.name, + slug: kebabCase(game.name), + description: game.description, + description_preview: game.description_preview, + external_id: game.id, + thumb_url: game.thumb_url, + min_age: game.min_age, + min_players: game.min_players, + max_players: game.max_players, + min_playtime: game.min_playtime, + max_playtime: game.max_playtime, + year_published: game.year_published, + primary_publisher: { + connectOrCreate: { + where: { + external_id: game.primary_publisher.id + }, + create: { + external_id: game.primary_publisher.id, + name: game.primary_publisher.name, + slug: kebabCase(game.primary_publisher.name) + } + } + }, + categories: { + connect: categoryIds + }, + mechanics: { + connect: mechanicIds + } + }, + update: { + name: game.name, + slug: kebabCase(game.name), + description: game.description, + description_preview: game.description_preview, + external_id: game.id, + thumb_url: game.thumb_url, + min_age: game.min_age, + min_players: game.min_players, + max_players: game.max_players, + min_playtime: game.min_playtime, + max_playtime: game.max_playtime, + year_published: game.year_published, + primary_publisher: { + connectOrCreate: { + where: { + external_id: game.primary_publisher.id + }, + create: { + external_id: game.primary_publisher.id, + name: game.primary_publisher.name + } + } + }, + categories: { + connect: categoryIds + }, + mechanics: { + connect: mechanicIds + } + } + }); +} + +export const load = async (event) => { + const { params, locals, request, fetch, url } = event; const defaults = { limit: 10, skip: 0 diff --git a/src/routes/wishlist/+page.server.ts b/src/routes/wishlist/+page.server.ts index a7faf46..8b785a7 100644 --- a/src/routes/wishlist/+page.server.ts +++ b/src/routes/wishlist/+page.server.ts @@ -48,15 +48,20 @@ export const actions = { throw redirect(302, '/auth/signin'); } - const game = await prisma.game.findUnique({ + let game = await prisma.game.findUnique({ where: { id: form.id } }); - // if (!game) { - // throw redirect(302, '/404'); - // } + if (!game) { + game = await prisma.game.create({ + data: { + name: form.name + } + }); + throw redirect(302, '/404'); + } if (game) { const wishlist = await prisma.wishlist.create({