diff --git a/prisma/migrations/20240929000950_add_game_data/migration.sql b/prisma/migrations/20240929000950_add_game_data/migration.sql new file mode 100644 index 0000000..1c5b1fc --- /dev/null +++ b/prisma/migrations/20240929000950_add_game_data/migration.sql @@ -0,0 +1,76 @@ +-- CreateEnum +CREATE TYPE "MetadataSource" AS ENUM ('Custom', 'GiantBomb'); + +-- CreateTable +CREATE TABLE "Game" ( + "id" TEXT NOT NULL, + "metadataSource" "MetadataSource" NOT NULL, + "metadataId" TEXT NOT NULL, + "mName" TEXT NOT NULL, + "mShortDescription" TEXT NOT NULL, + "mDescription" TEXT NOT NULL, + "mReviewCount" INTEGER NOT NULL, + "mReviewRating" DOUBLE PRECISION NOT NULL, + "mIconId" TEXT NOT NULL, + "mBannerId" TEXT NOT NULL, + "mArt" TEXT[], + "mScreenshots" TEXT[], + + CONSTRAINT "Game_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Developer" ( + "id" TEXT NOT NULL, + "metadataSource" "MetadataSource" NOT NULL, + "metadataId" TEXT NOT NULL, + "mName" TEXT NOT NULL, + + CONSTRAINT "Developer_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Publisher" ( + "id" TEXT NOT NULL, + "metadataSource" "MetadataSource" NOT NULL, + "metadataId" TEXT NOT NULL, + "mName" TEXT NOT NULL, + + CONSTRAINT "Publisher_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "_GameToPublisher" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL +); + +-- CreateTable +CREATE TABLE "_DeveloperToGame" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "_GameToPublisher_AB_unique" ON "_GameToPublisher"("A", "B"); + +-- CreateIndex +CREATE INDEX "_GameToPublisher_B_index" ON "_GameToPublisher"("B"); + +-- CreateIndex +CREATE UNIQUE INDEX "_DeveloperToGame_AB_unique" ON "_DeveloperToGame"("A", "B"); + +-- CreateIndex +CREATE INDEX "_DeveloperToGame_B_index" ON "_DeveloperToGame"("B"); + +-- AddForeignKey +ALTER TABLE "_GameToPublisher" ADD CONSTRAINT "_GameToPublisher_A_fkey" FOREIGN KEY ("A") REFERENCES "Game"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_GameToPublisher" ADD CONSTRAINT "_GameToPublisher_B_fkey" FOREIGN KEY ("B") REFERENCES "Publisher"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_DeveloperToGame" ADD CONSTRAINT "_DeveloperToGame_A_fkey" FOREIGN KEY ("A") REFERENCES "Developer"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_DeveloperToGame" ADD CONSTRAINT "_DeveloperToGame_B_fkey" FOREIGN KEY ("B") REFERENCES "Game"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20240929010842_updates_to_metadata_schema/migration.sql b/prisma/migrations/20240929010842_updates_to_metadata_schema/migration.sql new file mode 100644 index 0000000..913d4c1 --- /dev/null +++ b/prisma/migrations/20240929010842_updates_to_metadata_schema/migration.sql @@ -0,0 +1,24 @@ +/* + Warnings: + + - Added the required column `mBanner` to the `Developer` table without a default value. This is not possible if the table is not empty. + - Added the required column `mDescription` to the `Developer` table without a default value. This is not possible if the table is not empty. + - Added the required column `mLogo` to the `Developer` table without a default value. This is not possible if the table is not empty. + - Added the required column `mShortDescription` to the `Developer` table without a default value. This is not possible if the table is not empty. + - Added the required column `mBanner` to the `Publisher` table without a default value. This is not possible if the table is not empty. + - Added the required column `mDescription` to the `Publisher` table without a default value. This is not possible if the table is not empty. + - Added the required column `mLogo` to the `Publisher` table without a default value. This is not possible if the table is not empty. + - Added the required column `mShortDescription` to the `Publisher` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "Developer" ADD COLUMN "mBanner" TEXT NOT NULL, +ADD COLUMN "mDescription" TEXT NOT NULL, +ADD COLUMN "mLogo" TEXT NOT NULL, +ADD COLUMN "mShortDescription" TEXT NOT NULL; + +-- AlterTable +ALTER TABLE "Publisher" ADD COLUMN "mBanner" TEXT NOT NULL, +ADD COLUMN "mDescription" TEXT NOT NULL, +ADD COLUMN "mLogo" TEXT NOT NULL, +ADD COLUMN "mShortDescription" TEXT NOT NULL; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e753047..4f9c820 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -34,3 +34,61 @@ model LinkedAuthMec { @@id([userId, mec]) } + +enum MetadataSource { + Custom + GiantBomb +} + +model Game { + id String @id @default(uuid()) + + metadataSource MetadataSource + metadataId String + + // Any field prefixed with m is filled in from metadata + // Acts as a cache so we can search and filter it + mName String // Name of game + mShortDescription String // Short description + mDescription String // Supports markdown + mDevelopers Developer[] + mPublishers Publisher[] + + mReviewCount Int + mReviewRating Float + + mIconId String // linked to objects in s3 + mBannerId String // linked to objects in s3 + mArt String[] // linked to objects in s3 + mScreenshots String[] // linked to objects in s3 +} + +model Developer { + id String @id @default(uuid()) + + metadataSource MetadataSource + metadataId String + + mName String + mShortDescription String + mDescription String + mLogo String + mBanner String + + games Game[] +} + +model Publisher { + id String @id @default(uuid()) + + metadataSource MetadataSource + metadataId String + + mName String + mShortDescription String + mDescription String + mLogo String + mBanner String + + games Game[] +} diff --git a/server/internal/metadata/index.ts b/server/internal/metadata/index.ts new file mode 100644 index 0000000..7721879 --- /dev/null +++ b/server/internal/metadata/index.ts @@ -0,0 +1,56 @@ +import { _FetchDeveloperMetadataParams, _FetchGameMetadataParams, _FetchPublisherMetadataParams, DeveloperMetadata, GameMetadata, GameMetadataSearchResult, InternalGameMetadataResult, PublisherMetadata } from "./types"; + + +export abstract class MetadataProvider { + abstract id(): string; + abstract name(): string; + + abstract search(query: string): Promise; + abstract fetchGame(params: _FetchGameMetadataParams): Promise; + abstract fetchPublisher(params: _FetchPublisherMetadataParams): Promise; + abstract fetchDeveloper(params: _FetchDeveloperMetadataParams): Promise; +} + +class MetadataHandler { + // Ordered by priority + private providers: Map = new Map(); + private createObject: (url: string) => Promise; + + constructor() { + this.createObject = async () => ""; + } + + async search(query: string) { + const promises: Promise[] = []; + for (const provider of this.providers.values()) { + const queryTransformationPromise = new Promise(async (resolve, reject) => { + const results = await provider.search(query); + const mappedResults: InternalGameMetadataResult[] = results.map((result) => Object.assign( + {}, + result, + { + sourceId: provider.id(), + sourceName: provider.name() + } + )); + resolve(mappedResults); + }); + promises.push(queryTransformationPromise); + } + + const results = await Promise.allSettled(promises); + const successfulResults = results.filter((result) => result.status === 'fulfilled').map((result) => result.value).flat(); + + return successfulResults; + } + + async fetchGame(game: InternalGameMetadataResult) { + + } + + async fetchDeveloper(query: string) { + + } +} + +export default new MetadataHandler(); \ No newline at end of file diff --git a/server/internal/metadata/types.d.ts b/server/internal/metadata/types.d.ts new file mode 100644 index 0000000..30ef3c8 --- /dev/null +++ b/server/internal/metadata/types.d.ts @@ -0,0 +1,64 @@ +import { Developer, Publisher } from "@prisma/client"; + +export interface GameMetadataSearchResult { + id: string; + name: string; + icon: string; + description: string; + year: number; +} + +export interface GameMetadataSource { + sourceId: string; + sourceName: string; +} + +export type InternalGameMetadataResult = GameMetadataSearchResult & GameMetadataSource; +export type RemoteObject = string; + +export interface GameMetadata { + name: string; + shortDescription: string; + description: string; + + // These are created using utility functions passed to the metadata loader + // (that then call back into the metadata provider chain) + publishers: Publisher[] + developers: Developer[] + + reviewCount: number; + reviewRating: number; + + // Created with another utility function + icon: RemoteObject, + banner: RemoteObject, + art: RemoteObject[], + screenshots: RemoteObject[], +} + +export interface PublisherMetadata { + name: string; + shortDescription: string; + description: string; + + logo: RemoteObject; + banner: RemoteObject; +} + +export type DeveloperMetadata = PublisherMetadata; + +export interface _FetchGameMetadataParams { + id: string, + + publisher: (query: string) => Promise + developer: (query: string) => Promise + + createObject: (url: string) => Promise +} + +export interface _FetchPublisherMetadataParams { + query: string; + createObject: (url: string) => Promise; +} + +export type _FetchDeveloperMetadataParams = _FetchPublisherMetadataParams; \ No newline at end of file