feat: Add Games (#276)

## 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 comprehensive management of game libraries, including
adding, removing, and listing games in a user's Steam library.
- Added new API endpoints for retrieving detailed game information by ID
and listing all games in a user's library.
- Enabled friend-related API endpoints to list friends and fetch friend
details by SteamID.
- Added category and base game data structures with validation and
serialization for enriched game metadata.
- Introduced ownership update functionality for Steam accounts during
login.
- Added new game and category linking to support detailed game metadata
and categorization.
- Introduced member retrieval functions for enhanced team and user
management.

- **Improvements**
- Enhanced authentication to enforce team membership checks and provide
member-level access control.
- Improved Steam account ownership handling to ensure accurate user
association.
  - Added indexes to friend relationships for optimized querying.
  - Refined API routing structure with added game and friend routes.
- Improved friend listing queries for efficiency and data completeness.

- **Bug Fixes**
  - Fixed formatting issues in permissions related to Steam accounts.

- **Other**
- Refined event handling for user account refresh based on user ID
instead of email.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Wanjohi
2025-05-10 08:11:00 +03:00
committed by GitHub
parent d933c1e61d
commit 0b995fa540
23 changed files with 1120 additions and 142 deletions

View File

@@ -8,10 +8,10 @@ import { friendTable } from "./friend.sql";
import { userTable } from "../user/user.sql";
import { steamTable } from "../steam/steam.sql";
import { createSelectSchema } from "drizzle-zod";
import { and, eq, isNull, sql } from "drizzle-orm";
import { groupBy, map, pipe, values } from "remeda";
import { createTransaction, useTransaction } from "../drizzle/transaction";
import { ErrorCodes, VisibleError } from "../error";
import { and, eq, isNull, sql } from "drizzle-orm";
import { createTransaction, useTransaction } from "../drizzle/transaction";
export namespace Friend {
export const Info = Steam.Info
@@ -24,12 +24,14 @@ export namespace Friend {
.openapi({
ref: "Friend",
description: "Represents a friend's information stored on Nestri",
example: { ...Examples.SteamAccount, user: Examples.User },
example: Examples.Friend,
});
export const InputInfo = createSelectSchema(friendTable)
.omit({ timeCreated: true, timeDeleted: true, timeUpdated: true })
export type Info = z.infer<typeof Info>;
export type InputInfo = z.infer<typeof InputInfo>;
export const add = fn(
@@ -45,6 +47,21 @@ export namespace Friend {
);
}
const results =
await tx
.select()
.from(friendTable)
.where(
and(
eq(friendTable.steamID, steamID),
eq(friendTable.friendSteamID, input.friendSteamID),
isNull(friendTable.timeDeleted)
)
)
.execute()
if (results.length > 0) return null
await tx
.insert(friendTable)
.values({
@@ -76,35 +93,41 @@ export namespace Friend {
)
)
export const list = async () =>
useTransaction(async (tx) => {
const userSteamAccounts =
await tx
.select()
.from(steamTable)
.where(eq(steamTable.userID, Actor.userID()))
.execute();
if (userSteamAccounts.length === 0) {
return []; // User has no steam accounts
}
const friendPromises =
userSteamAccounts.map(async (steamAccount) => {
return await fromSteamID(steamAccount.id)
export const list = () =>
useTransaction(async (tx) =>
tx
.select({
steam: steamTable,
user: userTable,
})
.from(friendTable)
.innerJoin(
steamTable,
eq(friendTable.friendSteamID, steamTable.id)
)
.leftJoin(
userTable,
eq(steamTable.userID, userTable.id)
)
.where(
and(
eq(friendTable.steamID, Actor.steamID()),
isNull(friendTable.timeDeleted)
)
)
.limit(100)
.execute()
.then(rows => serialize(rows))
)
return (await Promise.all(friendPromises)).flat()
})
export const fromSteamID = fn(
InputInfo.shape.steamID,
(steamID) =>
export const fromFriendID = fn(
InputInfo.shape.friendSteamID,
(friendSteamID) =>
useTransaction(async (tx) =>
tx
.select({
steam: steamTable,
user: userTable
user: userTable,
})
.from(friendTable)
.innerJoin(
@@ -117,28 +140,29 @@ export namespace Friend {
)
.where(
and(
eq(friendTable.steamID, steamID),
eq(friendTable.steamID, Actor.steamID()),
eq(friendTable.friendSteamID, friendSteamID),
isNull(friendTable.timeDeleted)
)
)
.orderBy(friendTable.timeCreated)
.limit(100)
.limit(1)
.execute()
.then((rows) => serialize(rows))
.then(rows => serialize(rows).at(0))
)
)
export const areFriends = fn(
InputInfo,
(input) =>
InputInfo.shape.friendSteamID,
(friendSteamID) =>
useTransaction(async (tx) => {
const result = await tx
.select()
.from(friendTable)
.where(
and(
eq(friendTable.steamID, input.steamID),
eq(friendTable.friendSteamID, input.friendSteamID),
eq(friendTable.steamID, Actor.steamID()),
eq(friendTable.friendSteamID, friendSteamID),
isNull(friendTable.timeDeleted)
)
)
@@ -154,7 +178,7 @@ export namespace Friend {
): z.infer<typeof Info>[] {
return pipe(
input,
groupBy((row) => row.steam.id.toString()),
groupBy((row) => row.steam.id),
values(),
map((group) => ({
...Steam.serialize(group[0].steam),