From afaaaf2eb5fae118c878f90bf3567ef78b61feb7 Mon Sep 17 00:00:00 2001 From: Huskydog9988 <39809509+Huskydog9988@users.noreply.github.com> Date: Thu, 8 May 2025 20:35:15 -0400 Subject: [PATCH] feat: unified company metadata store still need to migrate users from old developer and publisher tables --- .../migration.sql | 35 +++++++ prisma/models/content.prisma | 39 +++++++- server/internal/metadata/giantbomb.ts | 12 +-- server/internal/metadata/igdb.ts | 12 +-- server/internal/metadata/index.ts | 96 ++++++++++++++----- server/internal/metadata/manual.ts | 10 +- server/internal/metadata/pcgamingwiki.ts | 14 +-- server/internal/metadata/types.d.ts | 4 +- server/plugins/03.metadata-init.ts | 2 +- 9 files changed, 159 insertions(+), 65 deletions(-) create mode 100644 prisma/migrations/20250509003340_init_unified_company_metadata/migration.sql diff --git a/prisma/migrations/20250509003340_init_unified_company_metadata/migration.sql b/prisma/migrations/20250509003340_init_unified_company_metadata/migration.sql new file mode 100644 index 0000000..ac49e82 --- /dev/null +++ b/prisma/migrations/20250509003340_init_unified_company_metadata/migration.sql @@ -0,0 +1,35 @@ +-- CreateTable +CREATE TABLE "Company" ( + "id" TEXT NOT NULL, + "metadataSource" "MetadataSource" NOT NULL, + "metadataId" TEXT NOT NULL, + "metadataOriginalQuery" TEXT NOT NULL, + "mName" TEXT NOT NULL, + "mShortDescription" TEXT NOT NULL, + "mDescription" TEXT NOT NULL, + "mLogoObjectId" TEXT NOT NULL, + "mBannerObjectId" TEXT NOT NULL, + "mWebsite" TEXT NOT NULL, + + CONSTRAINT "Company_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "CompanyGameRelation" ( + "companyId" TEXT NOT NULL, + "gameId" TEXT NOT NULL, + "developer" BOOLEAN NOT NULL DEFAULT false, + "publisher" BOOLEAN NOT NULL DEFAULT false +); + +-- CreateIndex +CREATE UNIQUE INDEX "Company_metadataSource_metadataId_key" ON "Company"("metadataSource", "metadataId"); + +-- CreateIndex +CREATE UNIQUE INDEX "CompanyGameRelation_companyId_gameId_key" ON "CompanyGameRelation"("companyId", "gameId"); + +-- AddForeignKey +ALTER TABLE "CompanyGameRelation" ADD CONSTRAINT "CompanyGameRelation_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CompanyGameRelation" ADD CONSTRAINT "CompanyGameRelation_gameId_fkey" FOREIGN KEY ("gameId") REFERENCES "Game"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/models/content.prisma b/prisma/models/content.prisma index 969cbfb..b60a0a3 100644 --- a/prisma/models/content.prisma +++ b/prisma/models/content.prisma @@ -33,9 +33,10 @@ model Game { versions GameVersion[] libraryBasePath String @unique // Base dir for all the game versions - collections CollectionEntry[] - saves SaveSlot[] - screenshots Screenshot[] + collections CollectionEntry[] + saves SaveSlot[] + screenshots Screenshot[] + companyRelations CompanyGameRelation[] @@unique([metadataSource, metadataId], name: "metadataKey") } @@ -102,6 +103,38 @@ model Screenshot { @@index([gameId, userId]) } +model Company { + id String @id @default(uuid()) + + metadataSource MetadataSource + metadataId String + metadataOriginalQuery String + + mName String + mShortDescription String + mDescription String + mLogoObjectId String + mBannerObjectId String + mWebsite String + + gameRelations CompanyGameRelation[] + + @@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()) diff --git a/server/internal/metadata/giantbomb.ts b/server/internal/metadata/giantbomb.ts index 2fa4d09..af7f415 100644 --- a/server/internal/metadata/giantbomb.ts +++ b/server/internal/metadata/giantbomb.ts @@ -7,9 +7,8 @@ import type { _FetchGameMetadataParams, GameMetadata, _FetchPublisherMetadataParams, - PublisherMetadata, + CompanyMetadata, _FetchDeveloperMetadataParams, - DeveloperMetadata, } from "./types"; import type { AxiosRequestConfig } from "axios"; import axios from "axios"; @@ -125,9 +124,6 @@ export class GiantBombProvider implements MetadataProvider { return response; } - id() { - return "giantbomb"; - } name() { return "GiantBomb"; } @@ -229,7 +225,7 @@ export class GiantBombProvider implements MetadataProvider { async fetchPublisher({ query, createObject, - }: _FetchPublisherMetadataParams): Promise { + }: _FetchPublisherMetadataParams): Promise { const results = await this.request>( "search", "", @@ -246,7 +242,7 @@ export class GiantBombProvider implements MetadataProvider { ? this.turndown.turndown(company.description) : company.deck; - const metadata: PublisherMetadata = { + const metadata: CompanyMetadata = { id: company.guid, name: company.name, shortDescription: company.deck ?? "", @@ -261,7 +257,7 @@ export class GiantBombProvider implements MetadataProvider { } async fetchDeveloper( params: _FetchDeveloperMetadataParams, - ): Promise { + ): Promise { return await this.fetchPublisher(params); } } diff --git a/server/internal/metadata/igdb.ts b/server/internal/metadata/igdb.ts index ed7718a..8136be5 100644 --- a/server/internal/metadata/igdb.ts +++ b/server/internal/metadata/igdb.ts @@ -7,9 +7,8 @@ import type { _FetchGameMetadataParams, GameMetadata, _FetchPublisherMetadataParams, - PublisherMetadata, + CompanyMetadata, _FetchDeveloperMetadataParams, - DeveloperMetadata, } from "./types"; import type { AxiosRequestConfig } from "axios"; import axios from "axios"; @@ -265,9 +264,6 @@ export class IGDBProvider implements MetadataProvider { return msg.length > len ? msg.substring(0, 280) + "..." : msg; } - id() { - return "igdb"; - } name() { return "IGDB"; } @@ -375,7 +371,7 @@ export class IGDBProvider implements MetadataProvider { async fetchPublisher({ query, createObject, - }: _FetchPublisherMetadataParams): Promise { + }: _FetchPublisherMetadataParams): Promise { const response = await this.request( "companies", `where name = "${query}"; fields *; limit 1;`, @@ -395,7 +391,7 @@ export class IGDBProvider implements MetadataProvider { if (company_url.length <= 0) company_url = site.url; } } - const metadata: PublisherMetadata = { + const metadata: CompanyMetadata = { id: "" + company.id, name: company.name, shortDescription: this.trimMessage(company.description, 280), @@ -413,7 +409,7 @@ export class IGDBProvider implements MetadataProvider { } async fetchDeveloper( params: _FetchDeveloperMetadataParams, - ): Promise { + ): Promise { return await this.fetchPublisher(params); } } diff --git a/server/internal/metadata/index.ts b/server/internal/metadata/index.ts index a4f129c..bc1f40e 100644 --- a/server/internal/metadata/index.ts +++ b/server/internal/metadata/index.ts @@ -5,11 +5,10 @@ import type { _FetchDeveloperMetadataParams, _FetchGameMetadataParams, _FetchPublisherMetadataParams, - DeveloperMetadata, GameMetadata, GameMetadataSearchResult, InternalGameMetadataResult, - PublisherMetadata, + CompanyMetadata, } from "./types"; import { ObjectTransactionalHandler } from "../objects/transactional"; import { PriorityListIndexed } from "../utils/prioritylist"; @@ -31,7 +30,6 @@ export class MissingMetadataProviderConfig extends Error { export const DropUserAgent = "Drop/0.2"; export abstract class MetadataProvider { - abstract id(): string; abstract name(): string; abstract source(): MetadataSource; @@ -39,16 +37,16 @@ export abstract class MetadataProvider { abstract fetchGame(params: _FetchGameMetadataParams): Promise; abstract fetchPublisher( params: _FetchPublisherMetadataParams, - ): Promise; + ): Promise; abstract fetchDeveloper( params: _FetchDeveloperMetadataParams, - ): Promise; + ): Promise; } export class MetadataHandler { // Ordered by priority private providers: PriorityListIndexed = - new PriorityListIndexed("id"); + new PriorityListIndexed("source"); private objectHandler: ObjectTransactionalHandler = new ObjectTransactionalHandler(); @@ -63,8 +61,8 @@ export class MetadataHandler { fetchProviderIdsInOrder() { return this.providers .values() - .map((e) => e.id()) - .filter((e) => e !== "manual"); + .map((e) => e.source()) + .filter((e) => e !== "Manual"); } async search(query: string) { @@ -80,7 +78,7 @@ export class MetadataHandler { const mappedResults: InternalGameMetadataResult[] = results.map( (result) => Object.assign({}, result, { - sourceId: provider.id(), + sourceId: provider.source(), sourceName: provider.name(), }), ); @@ -129,7 +127,7 @@ export class MetadataHandler { where: { metadataKey: { metadataSource: provider.source(), - metadataId: provider.id(), + metadataId: result.id, }, }, }); @@ -163,12 +161,6 @@ export class MetadataHandler { mName: metadata.name, mShortDescription: metadata.shortDescription, mDescription: metadata.description, - mDevelopers: { - connect: metadata.developers, - }, - mPublishers: { - connect: metadata.publishers, - }, mReviewCount: metadata.reviewCount, mReviewRating: metadata.reviewRating, @@ -182,6 +174,44 @@ export class MetadataHandler { 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; @@ -208,10 +238,10 @@ export class MetadataHandler { private async fetchDeveloperPublisher( query: string, functionName: "fetchDeveloper" | "fetchPublisher", - databaseName: "developer" | "publisher", + type: "developer" | "publisher", ) { // eslint-disable-next-line @typescript-eslint/no-explicit-any - const existing = await (prisma as any)[databaseName].findFirst({ + const existing = await (prisma as any)[type].findFirst({ where: { metadataOriginalQuery: query, }, @@ -226,12 +256,12 @@ export class MetadataHandler { {}, ["internal:read"], ); - let result: PublisherMetadata | undefined; + let result: CompanyMetadata | undefined; try { result = await provider[functionName]({ query, createObject }); if (result === undefined) { throw new Error( - `${provider.source()} failed to find a ${databaseName} for "${query}`, + `${provider.source()} failed to find a ${type} for "${query}`, ); } } catch (e) { @@ -243,18 +273,32 @@ export class MetadataHandler { // If we're successful await pullObjects(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const object = await (prisma as any)[databaseName].create({ - data: { + // 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: { metadataSource: provider.source(), - metadataId: provider.id(), + metadataId: result.id, metadataOriginalQuery: query, mName: result.name, mShortDescription: result.shortDescription, mDescription: result.description, - mLogo: result.logo, - mBanner: result.banner, + mLogoObjectId: result.logo, + 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, }, }); diff --git a/server/internal/metadata/manual.ts b/server/internal/metadata/manual.ts index 9802270..b51c16f 100644 --- a/server/internal/metadata/manual.ts +++ b/server/internal/metadata/manual.ts @@ -4,16 +4,12 @@ import type { _FetchGameMetadataParams, GameMetadata, _FetchPublisherMetadataParams, - PublisherMetadata, + CompanyMetadata, _FetchDeveloperMetadataParams, - DeveloperMetadata, } from "./types"; import * as jdenticon from "jdenticon"; export class ManualMetadataProvider implements MetadataProvider { - id() { - return "manual"; - } name() { return "Manual"; } @@ -49,12 +45,12 @@ export class ManualMetadataProvider implements MetadataProvider { } async fetchPublisher( _params: _FetchPublisherMetadataParams, - ): Promise { + ): Promise { throw new Error("Method not implemented."); } async fetchDeveloper( _params: _FetchDeveloperMetadataParams, - ): Promise { + ): Promise { throw new Error("Method not implemented."); } } diff --git a/server/internal/metadata/pcgamingwiki.ts b/server/internal/metadata/pcgamingwiki.ts index e2511ec..6e7850f 100644 --- a/server/internal/metadata/pcgamingwiki.ts +++ b/server/internal/metadata/pcgamingwiki.ts @@ -6,9 +6,8 @@ import type { _FetchGameMetadataParams, GameMetadata, _FetchPublisherMetadataParams, - PublisherMetadata, + CompanyMetadata, _FetchDeveloperMetadataParams, - DeveloperMetadata, } from "./types"; import type { AxiosRequestConfig } from "axios"; import axios from "axios"; @@ -60,9 +59,6 @@ interface PCGamingWikiCargoResult { // Api Docs: https://www.pcgamingwiki.com/wiki/PCGamingWiki:API // Good tool for helping build cargo queries: https://www.pcgamingwiki.com/wiki/Special:CargoQuery export class PCGamingWikiProvider implements MetadataProvider { - id() { - return "pcgamingwiki"; - } name() { return "PCGamingWiki"; } @@ -219,12 +215,12 @@ export class PCGamingWikiProvider implements MetadataProvider { async fetchPublisher({ query, createObject, - }: _FetchPublisherMetadataParams): Promise { + }: _FetchPublisherMetadataParams): Promise { const searchParams = new URLSearchParams({ action: "cargoquery", tables: "Company", fields: - "Company.Parent,Company.Founded,Company.Defunct,Company.Website,Company._pageName=PageName,Company._pageID=pageID", + "Company.Parent,Company.Founded,Company.Defunct,Company.Website,Company._pageName=PageName,Company._pageID=PageID", where: `Company._pageName="Company:${query}"`, format: "json", }); @@ -240,7 +236,7 @@ export class PCGamingWikiProvider implements MetadataProvider { const fixedCompanyName = company.PageName.split("Company:").at(1) ?? company.PageName; - const metadata: PublisherMetadata = { + const metadata: CompanyMetadata = { id: company.PageID, name: fixedCompanyName, shortDescription: "", @@ -258,7 +254,7 @@ export class PCGamingWikiProvider implements MetadataProvider { async fetchDeveloper( params: _FetchDeveloperMetadataParams, - ): Promise { + ): Promise { return await this.fetchPublisher(params); } } diff --git a/server/internal/metadata/types.d.ts b/server/internal/metadata/types.d.ts index 251060a..93bd407 100644 --- a/server/internal/metadata/types.d.ts +++ b/server/internal/metadata/types.d.ts @@ -40,7 +40,7 @@ export interface GameMetadata { images: ObjectReference[]; } -export interface PublisherMetadata { +export interface CompanyMetadata { id: string; name: string; shortDescription: string; @@ -51,8 +51,6 @@ export interface PublisherMetadata { website: string; } -export type DeveloperMetadata = PublisherMetadata; - export interface _FetchGameMetadataParams { id: string; name: string; diff --git a/server/plugins/03.metadata-init.ts b/server/plugins/03.metadata-init.ts index b320184..aa47bf3 100644 --- a/server/plugins/03.metadata-init.ts +++ b/server/plugins/03.metadata-init.ts @@ -18,7 +18,7 @@ export default defineNitroPlugin(async (_nitro) => { for (const provider of metadataProviders) { try { const prov = new provider(); - const id = prov.id(); + const id = prov.source(); providers.set(id, prov); console.log(`enabled metadata provider: ${prov.name()}`);