feat: Implement Game Image Support with Metadata & Schema Updates (#277)

## Description
<!-- Briefly describe the purpose and scope of your changes -->


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Introduced support for associating rich image metadata (color,
dimensions, file size) with games, organized by categories like
screenshots, box art, posters, hero art, backgrounds, logos, and icons.
- Game and library listings now include related image collections for
enhanced browsing and detail views.

- **Improvements**
- Updated game library management to use a consistent base game
identifier, improving data consistency and reliability.
- Enhanced data schemas and access permissions to allow public viewing
of game images and refined access control for game libraries.
- Added comprehensive database schema updates for games, categories,
images, and libraries to support new features and ensure data integrity.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Wanjohi
2025-05-10 22:47:28 +03:00
committed by GitHub
parent 38ad74d14a
commit 5806dc6e86
13 changed files with 2933 additions and 54 deletions

View File

@@ -1,7 +1,7 @@
import { timestamps } from "../drizzle/types";
import { baseGamesTable } from "../base-game/base-game.sql";
import { categoriesTable } from "../categories/categories.sql";
import { index, pgTable, primaryKey, varchar } from "drizzle-orm/pg-core";
import { categoriesTable, CategoryTypeEnum } from "../categories/categories.sql";
import { foreignKey, index, pgTable, primaryKey, varchar } from "drizzle-orm/pg-core";
export const gamesTable = pgTable(
'games',
@@ -13,21 +13,23 @@ export const gamesTable = pgTable(
{ onDelete: "cascade" }
),
categorySlug: varchar('category_slug', { length: 255 })
.notNull()
.references(() => categoriesTable.slug,
{ onDelete: "cascade" }
),
categoryType: varchar('category_type', { length: 255 })
.notNull()
.references(() => categoriesTable.type,
{ onDelete: "cascade" }
),
.notNull(),
categoryType: CategoryTypeEnum("type").notNull()
},
(table) => [
primaryKey({
columns: [table.baseGameID, table.categorySlug, table.categoryType]
}),
foreignKey({
name: "games_categories_fkey",
columns: [table.categorySlug, table.categoryType],
foreignColumns: [categoriesTable.slug, categoriesTable.type],
}).onDelete("cascade"),
index("idx_games_category_slug").on(table.categorySlug),
index("idx_games_category_type").on(table.categoryType),
index("idx_games_category_slug_type").on(
table.categorySlug,
table.categoryType
)
]
);

View File

@@ -1,11 +1,13 @@
import { z } from "zod";
import { fn } from "../utils";
import { Images } from "../images";
import { Examples } from "../examples";
import { BaseGame } from "../base-game";
import { gamesTable } from "./game.sql";
import { Categories } from "../categories";
import { eq, and, isNull } from "drizzle-orm";
import { createSelectSchema } from "drizzle-zod";
import { imagesTable } from "../images/images.sql";
import { groupBy, map, pipe, uniqueBy, values } from "remeda";
import { baseGamesTable } from "../base-game/base-game.sql";
import { categoriesTable } from "../categories/categories.sql";
@@ -13,7 +15,7 @@ import { createTransaction, useTransaction } from "../drizzle/transaction";
export namespace Game {
export const Info = z
.intersection(BaseGame.Info, Categories.Info)
.intersection(BaseGame.Info, Categories.Info, Images.Info)
.openapi({
ref: "Game",
description: "Detailed information about a game available in the Nestri library, including technical specifications, categories and metadata",
@@ -65,6 +67,7 @@ export namespace Game {
.select({
games: baseGamesTable,
categories: categoriesTable,
images: imagesTable
})
.from(gamesTable)
.innerJoin(baseGamesTable,
@@ -76,6 +79,12 @@ export namespace Game {
eq(categoriesTable.type, gamesTable.categoryType),
)
)
.leftJoin(imagesTable,
and(
eq(imagesTable.baseGameID, gamesTable.baseGameID),
isNull(imagesTable.timeDeleted),
)
)
.where(
and(
eq(gamesTable.baseGameID, gameID),
@@ -88,7 +97,7 @@ export namespace Game {
)
export function serialize(
input: { games: typeof baseGamesTable.$inferSelect; categories: typeof categoriesTable.$inferSelect | null }[],
input: { games: typeof baseGamesTable.$inferSelect; categories: typeof categoriesTable.$inferSelect | null; images: typeof imagesTable.$inferSelect | null }[],
): z.infer<typeof Info>[] {
return pipe(
input,
@@ -100,10 +109,16 @@ export namespace Game {
group.map(r => r.categories).filter((c): c is typeof categoriesTable.$inferSelect => Boolean(c)),
(c) => `${c.slug}:${c.type}`
)
const imgs = uniqueBy(
group.map(r => r.images).filter((c): c is typeof imagesTable.$inferSelect => Boolean(c)),
(c) => `${c.type}:${c.imageHash}:${c.position}`
)
const byType = Categories.serialize(cats)
const byImg = Images.serialize(imgs)
return {
...game,
...byType,
...byImg
}
})
)