mirror of
https://github.com/nestriness/nestri.git
synced 2025-12-12 16:55:37 +02:00
## 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** - Added AWS queue infrastructure and SQS handler for processing Steam game libraries and images. - Introduced event-driven handling for new credentials and game additions, including image uploads to S3. - Added client functions to fetch Steam user libraries, friends lists, app info, and related images. - Added new database columns and schema updates to track game acquisition, playtime, and family sharing. - Added utility function for chunking arrays. - Added new event notifications for library queue processing and game creation. - Added new lookup functions for categories and teams by slug. - Introduced a new Team API with endpoints to list and fetch teams by slug. - Added a new Steam library page displaying game images. - **Enhancements** - Improved game creation with event notifications and upsert logic. - Enhanced category and team retrieval with new lookup functions. - Renamed and refined image categories for clearer classification. - Expanded dependencies for image processing and AWS SDK integration. - Improved image processing utilities with caching, ranking, and metadata extraction. - Refined Steam client utilities for concurrency and error handling. - **Bug Fixes** - Fixed event publishing timing and removed deprecated credential retrieval methods. - **Chores** - Updated infrastructure configurations with increased timeouts, memory, and resource linking. - Added new dependencies for image processing, caching, and AWS SDK clients. - Refined internal code structure and imports for clarity. - Removed Steam provider and related UI components from the frontend. - Disabled authentication providers and Steam-related routes in the frontend. - Updated API fetch handler to accept environment bindings. - **Refactor** - Simplified query result handling and renamed functions for better clarity. - Removed outdated event handler in favor of consolidated event subscriber. - Consolidated and simplified database relationships and permission queries. - **Tests** - No explicit test changes included in this release. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
405 lines
13 KiB
TypeScript
405 lines
13 KiB
TypeScript
import { Size } from "@nestri/core/src/base-game/base-game.sql";
|
|
import { type Limitations } from "@nestri/core/src/steam/steam.sql";
|
|
import { ImageColor, ImageDimensions } from "@nestri/core/src/images/images.sql";
|
|
import {
|
|
json,
|
|
table,
|
|
number,
|
|
string,
|
|
boolean,
|
|
enumeration,
|
|
createSchema,
|
|
relationships,
|
|
definePermissions,
|
|
type ExpressionBuilder,
|
|
} from "@rocicorp/zero";
|
|
|
|
const timestamps = {
|
|
time_created: number(),
|
|
time_deleted: number().optional(),
|
|
} as const;
|
|
|
|
// Table Definitions
|
|
const users = table("users")
|
|
.columns({
|
|
id: string(),
|
|
name: string(),
|
|
email: string(),
|
|
last_login: number(),
|
|
avatar_url: string().optional(),
|
|
polar_customer_id: string().optional(),
|
|
...timestamps
|
|
})
|
|
.primaryKey("id");
|
|
|
|
const steam_accounts = table("steam_accounts")
|
|
.columns({
|
|
id: string(),
|
|
name: string(),
|
|
status: string(),
|
|
user_id: string(),
|
|
username: string(),
|
|
avatar_hash: string(),
|
|
member_since: number(),
|
|
last_synced_at: number(),
|
|
real_name: string().optional(),
|
|
profile_url: string().optional(),
|
|
limitations: json<Limitations>(),
|
|
...timestamps,
|
|
})
|
|
.primaryKey("id");
|
|
|
|
const teams = table("teams")
|
|
.columns({
|
|
id: string(),
|
|
name: string(),
|
|
slug: string(),
|
|
owner_id: string(),
|
|
invite_code: string(),
|
|
max_members: number(),
|
|
...timestamps,
|
|
})
|
|
.primaryKey("id");
|
|
|
|
const members = table("members")
|
|
.columns({
|
|
role: string(),
|
|
team_id: string(),
|
|
steam_id: string(),
|
|
user_id: string().optional(),
|
|
...timestamps,
|
|
})
|
|
.primaryKey("team_id", "steam_id");
|
|
|
|
const friends_list = table("friends_list")
|
|
.columns({
|
|
steam_id: string(),
|
|
friend_steam_id: string(),
|
|
...timestamps,
|
|
})
|
|
.primaryKey("steam_id", "friend_steam_id");
|
|
|
|
const games = table("games")
|
|
.columns({
|
|
base_game_id: string(),
|
|
category_slug: string(),
|
|
type: enumeration<"tag" | "genre" | "publisher" | "developer">(),
|
|
...timestamps
|
|
})
|
|
.primaryKey("category_slug", "base_game_id", "type")
|
|
|
|
const base_games = table("base_games")
|
|
.columns({
|
|
id: string(),
|
|
slug: string(),
|
|
name: string(),
|
|
release_date: number(),
|
|
size: json<Size>(),
|
|
description: string(),
|
|
primary_genre: string().optional(),
|
|
controller_support: enumeration<"full" | "partial" | "unknown">(),
|
|
compatibility: enumeration<"high" | "mid" | "low" | "unknown">(),
|
|
score: number(),
|
|
...timestamps
|
|
})
|
|
.primaryKey("id")
|
|
|
|
const categories = table("categories")
|
|
.columns({
|
|
slug: string(),
|
|
type: enumeration<"tag" | "genre" | "publisher" | "developer">(),
|
|
name: string(),
|
|
...timestamps
|
|
})
|
|
.primaryKey("slug", "type")
|
|
|
|
const game_libraries = table("game_libraries")
|
|
.columns({
|
|
base_game_id: string(),
|
|
owner_id: string(),
|
|
time_acquired: number(),
|
|
last_played: number(),
|
|
total_playtime: number(),
|
|
is_family_shared: boolean(),
|
|
...timestamps
|
|
}).primaryKey("base_game_id", "owner_id")
|
|
|
|
const images = table("images")
|
|
.columns({
|
|
image_hash: string(),
|
|
base_game_id: string(),
|
|
type: enumeration<"heroArt" | "icon" | "logo" | "superHeroArt" | "poster" | "boxArt" | "screenshot" | "background">(),
|
|
position: number(),
|
|
dimensions: json<ImageDimensions>(),
|
|
extracted_color: json<ImageColor>(),
|
|
...timestamps
|
|
}).primaryKey("image_hash", "type", "base_game_id", "position")
|
|
|
|
// Schema and Relationships
|
|
export const schema = createSchema({
|
|
tables: [users, steam_accounts, teams, members, friends_list, categories, base_games, games, game_libraries, images],
|
|
relationships: [
|
|
relationships(steam_accounts, (r) => ({
|
|
user: r.one({
|
|
sourceField: ["user_id"],
|
|
destSchema: users,
|
|
destField: ["id"],
|
|
}),
|
|
memberEntries: r.many({
|
|
sourceField: ["id"],
|
|
destSchema: members,
|
|
destField: ["steam_id"],
|
|
}),
|
|
friends: r.many(
|
|
{
|
|
sourceField: ["id"],
|
|
destSchema: friends_list,
|
|
destField: ["steam_id"],
|
|
},
|
|
{
|
|
sourceField: ["friend_steam_id"],
|
|
destSchema: steam_accounts,
|
|
destField: ["id"],
|
|
}
|
|
),
|
|
libraryGames: r.many(
|
|
{
|
|
sourceField: ["id"],
|
|
destSchema: game_libraries,
|
|
destField: ["owner_id"],
|
|
},
|
|
{
|
|
sourceField: ["base_game_id"],
|
|
destSchema: base_games,
|
|
destField: ["id"],
|
|
}
|
|
),
|
|
})),
|
|
relationships(users, (r) => ({
|
|
teams: r.many({
|
|
sourceField: ["id"],
|
|
destSchema: teams,
|
|
destField: ["owner_id"],
|
|
}),
|
|
members: r.many({
|
|
sourceField: ["id"],
|
|
destSchema: members,
|
|
destField: ["user_id"],
|
|
}),
|
|
steamAccounts: r.many({
|
|
sourceField: ["id"],
|
|
destSchema: steam_accounts,
|
|
destField: ["user_id"]
|
|
})
|
|
})),
|
|
relationships(teams, (r) => ({
|
|
owner: r.one({
|
|
sourceField: ["owner_id"],
|
|
destSchema: users,
|
|
destField: ["id"],
|
|
}),
|
|
members: r.many({
|
|
sourceField: ["id"],
|
|
destSchema: members,
|
|
destField: ["team_id"],
|
|
}),
|
|
})),
|
|
relationships(members, (r) => ({
|
|
team: r.one({
|
|
sourceField: ["team_id"],
|
|
destSchema: teams,
|
|
destField: ["id"],
|
|
}),
|
|
user: r.one({
|
|
sourceField: ["user_id"],
|
|
destSchema: users,
|
|
destField: ["id"],
|
|
}),
|
|
steamAccount: r.one({
|
|
sourceField: ["steam_id"],
|
|
destSchema: steam_accounts,
|
|
destField: ["id"],
|
|
}),
|
|
})),
|
|
relationships(base_games, (r) => ({
|
|
libraryOwners: r.many(
|
|
{
|
|
sourceField: ["id"],
|
|
destSchema: game_libraries,
|
|
destField: ["base_game_id"],
|
|
},
|
|
{
|
|
sourceField: ["owner_id"],
|
|
destSchema: steam_accounts,
|
|
destField: ["id"],
|
|
}
|
|
),
|
|
categories: r.many(
|
|
{
|
|
sourceField: ["id"],
|
|
destSchema: games,
|
|
destField: ["base_game_id"],
|
|
},
|
|
{
|
|
sourceField: ["category_slug", "type"],
|
|
destSchema: categories,
|
|
destField: ["slug", "type"],
|
|
}
|
|
),
|
|
images: r.many({
|
|
sourceField: ["id"],
|
|
destSchema: images,
|
|
destField: ["base_game_id"]
|
|
})
|
|
})),
|
|
relationships(categories, (r) => ({
|
|
baseGames: r.many(
|
|
{
|
|
sourceField: ["slug", "type"],
|
|
destSchema: games,
|
|
destField: ["category_slug", "type"],
|
|
},
|
|
{
|
|
sourceField: ["base_game_id"],
|
|
destSchema: base_games,
|
|
destField: ["id"],
|
|
}
|
|
),
|
|
})),
|
|
relationships(images, (r) => ({
|
|
base_game: r.one({
|
|
sourceField: ["base_game_id"],
|
|
destSchema: base_games,
|
|
destField: ["id"],
|
|
}),
|
|
})),
|
|
//Junction tables
|
|
relationships(friends_list, (r) => ({
|
|
steam: r.one({
|
|
sourceField: ["steam_id"],
|
|
destSchema: steam_accounts,
|
|
destField: ["id"]
|
|
}),
|
|
friend: r.one({
|
|
sourceField: ["friend_steam_id"],
|
|
destSchema: steam_accounts,
|
|
destField: ["id"]
|
|
}),
|
|
})),
|
|
|
|
relationships(game_libraries, (r) => ({
|
|
owner: r.one({
|
|
sourceField: ["owner_id"],
|
|
destSchema: steam_accounts,
|
|
destField: ["id"]
|
|
}),
|
|
baseGame: r.one({
|
|
sourceField: ["base_game_id"],
|
|
destSchema: base_games,
|
|
destField: ["id"]
|
|
}),
|
|
})),
|
|
|
|
relationships(games, (r) => ({
|
|
baseGame: r.one({
|
|
sourceField: ["base_game_id"],
|
|
destSchema: base_games,
|
|
destField: ["id"]
|
|
}),
|
|
category: r.one({
|
|
sourceField: ["category_slug", "type"],
|
|
destSchema: categories,
|
|
destField: ["slug", "type"]
|
|
}),
|
|
})),
|
|
],
|
|
});
|
|
|
|
export type Schema = typeof schema;
|
|
|
|
type Auth = {
|
|
sub: string;
|
|
properties: {
|
|
userID: string;
|
|
email: string;
|
|
};
|
|
};
|
|
|
|
export const permissions = definePermissions<Auth, Schema>(schema, () => {
|
|
return {
|
|
members: {
|
|
row: {
|
|
select: [
|
|
(auth: Auth, q: ExpressionBuilder<Schema, 'members'>) => q.exists("user", (u) => u.where("id", auth.sub)),
|
|
//allow other team members to view other members
|
|
(auth: Auth, q: ExpressionBuilder<Schema, 'members'>) => q.exists("team", (u) => u.related("members", (m) => m.where("user_id", auth.sub))),
|
|
]
|
|
},
|
|
},
|
|
teams: {
|
|
row: {
|
|
select: [
|
|
(auth: Auth, q: ExpressionBuilder<Schema, 'teams'>) => q.exists("members", (u) => u.where("user_id", auth.sub)),
|
|
]
|
|
},
|
|
},
|
|
steam_accounts: {
|
|
row: {
|
|
select: [
|
|
(auth: Auth, q: ExpressionBuilder<Schema, 'steam_accounts'>) => q.exists("user", (u) => u.where("id", auth.sub)),
|
|
//Allow friends to view friends steam accounts
|
|
(auth: Auth, q: ExpressionBuilder<Schema, 'steam_accounts'>) => q.exists("friends", (u) => u.where("user_id", auth.sub)),
|
|
//allow other team members to see a user's steam account
|
|
(auth: Auth, q: ExpressionBuilder<Schema, 'steam_accounts'>) => q.exists("memberEntries", (u) => u.related("team", (t) => t.related("members", (m) => m.where("user_id", auth.sub)))),
|
|
]
|
|
},
|
|
},
|
|
users: {
|
|
row: {
|
|
select: [
|
|
(auth: Auth, q: ExpressionBuilder<Schema, 'users'>) => q.cmp("id", "=", auth.sub),
|
|
]
|
|
},
|
|
},
|
|
friends_list: {
|
|
row: {
|
|
select: [
|
|
(auth: Auth, q: ExpressionBuilder<Schema, 'friends_list'>) => q.exists("steam", (u) => u.where("user_id", auth.sub)),
|
|
(auth: Auth, q: ExpressionBuilder<Schema, 'friends_list'>) => q.exists("friend", (u) => u.where("user_id", auth.sub)),
|
|
]
|
|
},
|
|
},
|
|
game_libraries: {
|
|
row: {
|
|
select: [
|
|
(auth: Auth, q: ExpressionBuilder<Schema, 'game_libraries'>) => q.exists("owner", (u) => u.where("user_id", auth.sub)),
|
|
//allow team members to see the other members' libraries
|
|
(auth: Auth, q: ExpressionBuilder<Schema, 'game_libraries'>) => q.exists("owner", (u) => u.related("memberEntries", (f) => f.where("user_id", auth.sub))),
|
|
//allow friends to see their friends libraries
|
|
(auth: Auth, q: ExpressionBuilder<Schema, 'game_libraries'>) => q.exists("owner", (u) => u.related("friends", (f) => f.where("user_id", auth.sub))),
|
|
]
|
|
}
|
|
},
|
|
// Games are publicly viewable - but only to logged in users
|
|
games: {
|
|
row: {
|
|
select: [(auth: Auth, q: ExpressionBuilder<Schema, 'games'>) => q.cmpLit(auth.sub, "IS NOT", null),]
|
|
}
|
|
},
|
|
base_games: {
|
|
row: {
|
|
select: [(auth: Auth, q: ExpressionBuilder<Schema, 'base_games'>) => q.cmpLit(auth.sub, "IS NOT", null),]
|
|
}
|
|
},
|
|
categories: {
|
|
row: {
|
|
select: [(auth: Auth, q: ExpressionBuilder<Schema, 'categories'>) => q.cmpLit(auth.sub, "IS NOT", null),]
|
|
}
|
|
},
|
|
images: {
|
|
row: {
|
|
select: [(auth: Auth, q: ExpressionBuilder<Schema, 'images'>) => q.cmpLit(auth.sub, "IS NOT", null),]
|
|
}
|
|
},
|
|
};
|
|
}); |