From 90277653cbf6370e342a4709642661508af834a5 Mon Sep 17 00:00:00 2001 From: DecDuck Date: Sat, 10 May 2025 11:59:56 +1000 Subject: [PATCH] feat: rework developer/publisher system --- .../migration.sql | 76 ++++++++++++ prisma/models/content.prisma | 66 ++--------- server/api/v1/store/developers.ts | 21 ---- server/api/v1/store/publishers.ts | 21 ---- server/api/v1/store/recent.get.ts | 4 +- server/internal/library/index.ts | 2 - server/internal/metadata/giantbomb.ts | 18 +-- server/internal/metadata/igdb.ts | 18 +-- server/internal/metadata/index.ts | 108 +++--------------- server/internal/metadata/manual.ts | 12 +- server/internal/metadata/pcgamingwiki.ts | 19 +-- server/internal/metadata/types.d.ts | 14 +-- 12 files changed, 132 insertions(+), 247 deletions(-) create mode 100644 prisma/migrations/20250510013650_remove_devlopers_and_publishers/migration.sql delete mode 100644 server/api/v1/store/developers.ts delete mode 100644 server/api/v1/store/publishers.ts diff --git a/prisma/migrations/20250510013650_remove_devlopers_and_publishers/migration.sql b/prisma/migrations/20250510013650_remove_devlopers_and_publishers/migration.sql new file mode 100644 index 0000000..00fb46d --- /dev/null +++ b/prisma/migrations/20250510013650_remove_devlopers_and_publishers/migration.sql @@ -0,0 +1,76 @@ +/* + Warnings: + + - You are about to drop the `CompanyGameRelation` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `Developer` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `Publisher` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `_DeveloperToGame` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `_GameToPublisher` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "CompanyGameRelation" DROP CONSTRAINT "CompanyGameRelation_companyId_fkey"; + +-- DropForeignKey +ALTER TABLE "CompanyGameRelation" DROP CONSTRAINT "CompanyGameRelation_gameId_fkey"; + +-- DropForeignKey +ALTER TABLE "_DeveloperToGame" DROP CONSTRAINT "_DeveloperToGame_A_fkey"; + +-- DropForeignKey +ALTER TABLE "_DeveloperToGame" DROP CONSTRAINT "_DeveloperToGame_B_fkey"; + +-- DropForeignKey +ALTER TABLE "_GameToPublisher" DROP CONSTRAINT "_GameToPublisher_A_fkey"; + +-- DropForeignKey +ALTER TABLE "_GameToPublisher" DROP CONSTRAINT "_GameToPublisher_B_fkey"; + +-- DropTable +DROP TABLE "CompanyGameRelation"; + +-- DropTable +DROP TABLE "Developer"; + +-- DropTable +DROP TABLE "Publisher"; + +-- DropTable +DROP TABLE "_DeveloperToGame"; + +-- DropTable +DROP TABLE "_GameToPublisher"; + +-- CreateTable +CREATE TABLE "_developers" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL, + + CONSTRAINT "_developers_AB_pkey" PRIMARY KEY ("A","B") +); + +-- CreateTable +CREATE TABLE "_publishers" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL, + + CONSTRAINT "_publishers_AB_pkey" PRIMARY KEY ("A","B") +); + +-- CreateIndex +CREATE INDEX "_developers_B_index" ON "_developers"("B"); + +-- CreateIndex +CREATE INDEX "_publishers_B_index" ON "_publishers"("B"); + +-- AddForeignKey +ALTER TABLE "_developers" ADD CONSTRAINT "_developers_A_fkey" FOREIGN KEY ("A") REFERENCES "Company"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_developers" ADD CONSTRAINT "_developers_B_fkey" FOREIGN KEY ("B") REFERENCES "Game"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_publishers" ADD CONSTRAINT "_publishers_A_fkey" FOREIGN KEY ("A") REFERENCES "Company"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_publishers" ADD CONSTRAINT "_publishers_B_fkey" FOREIGN KEY ("B") REFERENCES "Game"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/models/content.prisma b/prisma/models/content.prisma index b60a0a3..daaada3 100644 --- a/prisma/models/content.prisma +++ b/prisma/models/content.prisma @@ -17,8 +17,6 @@ model Game { mName String // Name of game mShortDescription String // Short description mDescription String // Supports markdown - mDevelopers Developer[] - mPublishers Publisher[] mReleased DateTime // When the game was released mReviewCount Int @@ -33,10 +31,12 @@ model Game { versions GameVersion[] libraryBasePath String @unique // Base dir for all the game versions - collections CollectionEntry[] - saves SaveSlot[] - screenshots Screenshot[] - companyRelations CompanyGameRelation[] + collections CollectionEntry[] + saves SaveSlot[] + screenshots Screenshot[] + + developers Company[] @relation(name: "developers") + publishers Company[] @relation(name: "publishers") @@unique([metadataSource, metadataId], name: "metadataKey") } @@ -117,62 +117,12 @@ model Company { mBannerObjectId String mWebsite String - gameRelations CompanyGameRelation[] + developed Game[] @relation(name: "developers") + published Game[] @relation(name: "publishers") @@unique([metadataSource, metadataId], name: "metadataKey") } -model CompanyGameRelation { - companyId String - company Company @relation(fields: [companyId], references: [id], onDelete: Cascade) - gameId String - game Game @relation(fields: [gameId], references: [id], onDelete: Cascade) - - // what the company did for the game - developer Boolean @default(false) - publisher Boolean @default(false) - - @@unique([companyId, gameId], name: "companyGame") -} - -model Developer { - id String @id @default(uuid()) - - metadataSource MetadataSource - metadataId String - metadataOriginalQuery String - - mName String - mShortDescription String - mDescription String - mLogo String - mBanner String - mWebsite String - - games Game[] - - @@unique([metadataSource, metadataId, metadataOriginalQuery], name: "metadataKey") -} - -model Publisher { - id String @id @default(uuid()) - - metadataSource MetadataSource - metadataId String - metadataOriginalQuery String - - mName String - mShortDescription String - mDescription String - mLogo String - mBanner String - mWebsite String - - games Game[] - - @@unique([metadataSource, metadataId, metadataOriginalQuery], name: "metadataKey") -} - model ObjectHash { id String @id hash String diff --git a/server/api/v1/store/developers.ts b/server/api/v1/store/developers.ts deleted file mode 100644 index 79e3124..0000000 --- a/server/api/v1/store/developers.ts +++ /dev/null @@ -1,21 +0,0 @@ -import aclManager from "~/server/internal/acls"; -import prisma from "~/server/internal/db/database"; - -export default defineEventHandler(async (h3) => { - const userId = await aclManager.getUserACL(h3, ["store:read"]); - if (!userId) throw createError({ statusCode: 403 }); - - const developers = await prisma.developer.findMany({ - include: { - games: true, - }, - orderBy: { - games: { - _count: "desc", - }, - }, - take: 3, - }); - - return developers; -}); diff --git a/server/api/v1/store/publishers.ts b/server/api/v1/store/publishers.ts deleted file mode 100644 index 9fef6ff..0000000 --- a/server/api/v1/store/publishers.ts +++ /dev/null @@ -1,21 +0,0 @@ -import aclManager from "~/server/internal/acls"; -import prisma from "~/server/internal/db/database"; - -export default defineEventHandler(async (h3) => { - const userId = await aclManager.getUserACL(h3, ["store:read"]); - if (!userId) throw createError({ statusCode: 403 }); - - const publishers = await prisma.publisher.findMany({ - include: { - games: true, - }, - orderBy: { - games: { - _count: "desc", - }, - }, - take: 4, - }); - - return publishers; -}); diff --git a/server/api/v1/store/recent.get.ts b/server/api/v1/store/recent.get.ts index 3d11231..f4bb39a 100644 --- a/server/api/v1/store/recent.get.ts +++ b/server/api/v1/store/recent.get.ts @@ -12,13 +12,13 @@ export default defineEventHandler(async (h3) => { mShortDescription: true, mCoverObjectId: true, mBannerObjectId: true, - mDevelopers: { + developers: { select: { id: true, mName: true, }, }, - mPublishers: { + publishers: { select: { id: true, mName: true, diff --git a/server/internal/library/index.ts b/server/internal/library/index.ts index 11de0a1..91d3b21 100644 --- a/server/internal/library/index.ts +++ b/server/internal/library/index.ts @@ -71,8 +71,6 @@ class LibraryManager { mName: true, mShortDescription: true, metadataSource: true, - mDevelopers: true, - mPublishers: true, mIconObjectId: true, libraryBasePath: true, }, diff --git a/server/internal/metadata/giantbomb.ts b/server/internal/metadata/giantbomb.ts index af7f415..d617f0c 100644 --- a/server/internal/metadata/giantbomb.ts +++ b/server/internal/metadata/giantbomb.ts @@ -1,4 +1,4 @@ -import type { Developer, Publisher } from "~/prisma/client"; +import type { Company } from "~/prisma/client"; import { MetadataSource } from "~/prisma/client"; import type { MetadataProvider } from "."; import { MissingMetadataProviderConfig } from "."; @@ -6,9 +6,8 @@ import type { GameMetadataSearchResult, _FetchGameMetadataParams, GameMetadata, - _FetchPublisherMetadataParams, + _FetchCompanyMetadataParams, CompanyMetadata, - _FetchDeveloperMetadataParams, } from "./types"; import type { AxiosRequestConfig } from "axios"; import axios from "axios"; @@ -168,7 +167,7 @@ export class GiantBombProvider implements MetadataProvider { ? this.turndown.turndown(gameData.description) : gameData.deck; - const publishers: Publisher[] = []; + const publishers: Company[] = []; if (gameData.publishers) { for (const pub of gameData.publishers) { const res = await publisher(pub.name); @@ -177,7 +176,7 @@ export class GiantBombProvider implements MetadataProvider { } } - const developers: Developer[] = []; + const developers: Company[] = []; if (gameData.developers) { for (const dev of gameData.developers) { const res = await developer(dev.name); @@ -222,10 +221,10 @@ export class GiantBombProvider implements MetadataProvider { return metadata; } - async fetchPublisher({ + async fetchCompany({ query, createObject, - }: _FetchPublisherMetadataParams): Promise { + }: _FetchCompanyMetadataParams): Promise { const results = await this.request>( "search", "", @@ -255,9 +254,4 @@ export class GiantBombProvider implements MetadataProvider { return metadata; } - async fetchDeveloper( - params: _FetchDeveloperMetadataParams, - ): Promise { - return await this.fetchPublisher(params); - } } diff --git a/server/internal/metadata/igdb.ts b/server/internal/metadata/igdb.ts index 8136be5..1970d89 100644 --- a/server/internal/metadata/igdb.ts +++ b/server/internal/metadata/igdb.ts @@ -1,4 +1,4 @@ -import type { Developer, Publisher } from "~/prisma/client"; +import type { Company } from "~/prisma/client"; import { MetadataSource } from "~/prisma/client"; import type { MetadataProvider } from "."; import { MissingMetadataProviderConfig } from "."; @@ -6,9 +6,8 @@ import type { GameMetadataSearchResult, _FetchGameMetadataParams, GameMetadata, - _FetchPublisherMetadataParams, + _FetchCompanyMetadataParams, CompanyMetadata, - _FetchDeveloperMetadataParams, } from "./types"; import type { AxiosRequestConfig } from "axios"; import axios from "axios"; @@ -312,8 +311,8 @@ export class IGDBProvider implements MetadataProvider { } } - const publishers: Publisher[] = []; - const developers: Developer[] = []; + const publishers: Company[] = []; + const developers: Company[] = []; for (const involvedCompany of response[i]?.involved_companies ?? []) { // get details about the involved company const involved_company_response = @@ -368,10 +367,10 @@ export class IGDBProvider implements MetadataProvider { throw new Error("No game found on igdb with that id"); } - async fetchPublisher({ + async fetchCompany({ query, createObject, - }: _FetchPublisherMetadataParams): Promise { + }: _FetchCompanyMetadataParams): Promise { const response = await this.request( "companies", `where name = "${query}"; fields *; limit 1;`, @@ -407,9 +406,4 @@ export class IGDBProvider implements MetadataProvider { throw new Error(`igdb failed to find publisher/developer ${query}`); } - async fetchDeveloper( - params: _FetchDeveloperMetadataParams, - ): Promise { - return await this.fetchPublisher(params); - } } diff --git a/server/internal/metadata/index.ts b/server/internal/metadata/index.ts index bc1f40e..9c1f03f 100644 --- a/server/internal/metadata/index.ts +++ b/server/internal/metadata/index.ts @@ -1,10 +1,8 @@ -import type { Developer, Publisher } from "~/prisma/client"; import { MetadataSource } from "~/prisma/client"; import prisma from "../db/database"; import type { - _FetchDeveloperMetadataParams, _FetchGameMetadataParams, - _FetchPublisherMetadataParams, + _FetchCompanyMetadataParams, GameMetadata, GameMetadataSearchResult, InternalGameMetadataResult, @@ -35,11 +33,8 @@ export abstract class MetadataProvider { abstract search(query: string): Promise; abstract fetchGame(params: _FetchGameMetadataParams): Promise; - abstract fetchPublisher( - params: _FetchPublisherMetadataParams, - ): Promise; - abstract fetchDeveloper( - params: _FetchDeveloperMetadataParams, + abstract fetchCompany( + params: _FetchCompanyMetadataParams, ): Promise; } @@ -138,14 +133,14 @@ export class MetadataHandler { ["internal:read"], ); - let metadata; + let metadata: GameMetadata | undefined = undefined; try { metadata = await provider.fetchGame({ id: result.id, name: result.name, // wrap in anonymous functions to keep references to this - publisher: (name: string) => this.fetchPublisher(name), - developer: (name: string) => this.fetchDeveloper(name), + publisher: (name: string) => this.fetchCompany(name), + developer: (name: string) => this.fetchCompany(name), createObject, }); } catch (e) { @@ -171,77 +166,27 @@ export class MetadataHandler { mCoverObjectId: metadata.coverId, mImageLibraryObjectIds: metadata.images, + publishers: { + connect: metadata.publishers, + }, + developers: { + connect: metadata.developers, + }, + libraryBasePath, }, }); // relate companies to game - for (const pub of metadata.publishers) { - await prisma.companyGameRelation.upsert({ - where: { - companyGame: { - gameId: game.id, - companyId: pub.id, - }, - }, - create: { - gameId: game.id, - companyId: pub.id, - publisher: true, - }, - update: { - publisher: true, - }, - }); - } - for (const dev of metadata.developers) { - await prisma.companyGameRelation.upsert({ - where: { - companyGame: { - gameId: game.id, - companyId: dev.id, - }, - }, - create: { - gameId: game.id, - companyId: dev.id, - developer: true, - }, - update: { - developer: true, - }, - }); - } await pullObjects(); return game; } - async fetchDeveloper(query: string) { - return (await this.fetchDeveloperPublisher( - query, - "fetchDeveloper", - "developer", - )) as Developer | undefined; - } - - async fetchPublisher(query: string) { - return (await this.fetchDeveloperPublisher( - query, - "fetchPublisher", - "publisher", - )) as Publisher | undefined; - } - // Careful with this function, it has no typechecking // Type-checking this thing is impossible - private async fetchDeveloperPublisher( - query: string, - functionName: "fetchDeveloper" | "fetchPublisher", - type: "developer" | "publisher", - ) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const existing = await (prisma as any)[type].findFirst({ + private async fetchCompany(query: string) { + const existing = await prisma.company.findFirst({ where: { metadataOriginalQuery: query, }, @@ -258,10 +203,10 @@ export class MetadataHandler { ); let result: CompanyMetadata | undefined; try { - result = await provider[functionName]({ query, createObject }); + result = await provider.fetchCompany({ query, createObject }); if (result === undefined) { throw new Error( - `${provider.source()} failed to find a ${type} for "${query}`, + `${provider.source()} failed to find a company for "${query}`, ); } } catch (e) { @@ -273,15 +218,8 @@ export class MetadataHandler { // If we're successful await pullObjects(); - // TODO: dedupe metadata in event that a company with same source and id appears - const object = await prisma.company.upsert({ - where: { - metadataKey: { - metadataId: result.id, - metadataSource: provider.source(), - }, - }, - create: { + const object = await prisma.company.create({ + data: { metadataSource: provider.source(), metadataId: result.id, metadataOriginalQuery: query, @@ -293,14 +231,6 @@ export class MetadataHandler { mBannerObjectId: result.banner, mWebsite: result.website, }, - update: { - mName: result.name, - mShortDescription: result.shortDescription, - mDescription: result.description, - mLogoObjectId: result.logo, - mBannerObjectId: result.banner, - mWebsite: result.website, - }, }); return object; diff --git a/server/internal/metadata/manual.ts b/server/internal/metadata/manual.ts index b51c16f..fb3786c 100644 --- a/server/internal/metadata/manual.ts +++ b/server/internal/metadata/manual.ts @@ -3,9 +3,8 @@ import type { MetadataProvider } from "."; import type { _FetchGameMetadataParams, GameMetadata, - _FetchPublisherMetadataParams, + _FetchCompanyMetadataParams, CompanyMetadata, - _FetchDeveloperMetadataParams, } from "./types"; import * as jdenticon from "jdenticon"; @@ -43,13 +42,8 @@ export class ManualMetadataProvider implements MetadataProvider { images: [iconId], }; } - async fetchPublisher( - _params: _FetchPublisherMetadataParams, - ): Promise { - throw new Error("Method not implemented."); - } - async fetchDeveloper( - _params: _FetchDeveloperMetadataParams, + async fetchCompany( + _params: _FetchCompanyMetadataParams, ): Promise { throw new Error("Method not implemented."); } diff --git a/server/internal/metadata/pcgamingwiki.ts b/server/internal/metadata/pcgamingwiki.ts index 16f28ba..5ebce0c 100644 --- a/server/internal/metadata/pcgamingwiki.ts +++ b/server/internal/metadata/pcgamingwiki.ts @@ -1,13 +1,12 @@ -import type { Developer, Publisher } from "~/prisma/client"; +import type { Company } from "~/prisma/client"; import { MetadataSource } from "~/prisma/client"; import type { MetadataProvider } from "."; import type { GameMetadataSearchResult, _FetchGameMetadataParams, GameMetadata, - _FetchPublisherMetadataParams, + _FetchCompanyMetadataParams, CompanyMetadata, - _FetchDeveloperMetadataParams, } from "./types"; import type { AxiosRequestConfig } from "axios"; import axios from "axios"; @@ -179,7 +178,7 @@ export class PCGamingWikiProvider implements MetadataProvider { const game = res.data.cargoquery[0].title; - const publishers: Publisher[] = []; + const publishers: Company[] = []; if (game.Publishers !== null) { const pubListClean = this.parseCompanyStr(game.Publishers); for (const pub of pubListClean) { @@ -189,7 +188,7 @@ export class PCGamingWikiProvider implements MetadataProvider { } } - const developers: Developer[] = []; + const developers: Company[] = []; if (game.Developers !== null) { const devListClean = this.parseCompanyStr(game.Developers); for (const dev of devListClean) { @@ -228,10 +227,10 @@ export class PCGamingWikiProvider implements MetadataProvider { return metadata; } - async fetchPublisher({ + async fetchCompany({ query, createObject, - }: _FetchPublisherMetadataParams): Promise { + }: _FetchCompanyMetadataParams): Promise { const searchParams = new URLSearchParams({ action: "cargoquery", tables: "Company", @@ -267,10 +266,4 @@ export class PCGamingWikiProvider implements MetadataProvider { throw new Error(`pcgamingwiki failed to find publisher/developer ${query}`); } - - async fetchDeveloper( - params: _FetchDeveloperMetadataParams, - ): Promise { - return await this.fetchPublisher(params); - } } diff --git a/server/internal/metadata/types.d.ts b/server/internal/metadata/types.d.ts index 93bd407..22f30f8 100644 --- a/server/internal/metadata/types.d.ts +++ b/server/internal/metadata/types.d.ts @@ -1,4 +1,4 @@ -import type { Developer, Publisher } from "~/prisma/client"; +import type { Company } from "~/prisma/client"; import type { TransactionDataType } from "../objects/transactional"; import type { ObjectReference } from "../objects/objectHandler"; @@ -27,8 +27,8 @@ export interface GameMetadata { // These are created using utility functions passed to the metadata loader // (that then call back into the metadata provider chain) - publishers: Publisher[]; - developers: Developer[]; + publishers: Company[]; + developers: Company[]; reviewCount: number; reviewRating: number; @@ -55,15 +55,13 @@ export interface _FetchGameMetadataParams { id: string; name: string; - publisher: (query: string) => Promise; - developer: (query: string) => Promise; + publisher: (query: string) => Promise; + developer: (query: string) => Promise; createObject: (data: TransactionDataType) => ObjectReference; } -export interface _FetchPublisherMetadataParams { +export interface _FetchCompanyMetadataParams { query: string; createObject: (data: TransactionDataType) => ObjectReference; } - -export type _FetchDeveloperMetadataParams = _FetchPublisherMetadataParams;