diff --git a/client/src/lib/queryKeys.ts b/client/src/lib/queryKeys.ts index f663f5a..443a8b1 100644 --- a/client/src/lib/queryKeys.ts +++ b/client/src/lib/queryKeys.ts @@ -3,6 +3,8 @@ * Organized by domain for easy cache invalidation. */ +import { all } from "axios"; + export const queryKeys = { profiles: { all: ["profiles"], @@ -14,6 +16,9 @@ export const queryKeys = { items: { all: ["items"], }, + characters: { + all: ["characters"] + }, purchases: { byProfileId: (profileId: number) => ["purchases", "byProfileId", profileId], }, diff --git a/client/src/pages/admin/AdminPageContent.tsx b/client/src/pages/admin/AdminPageContent.tsx index 47e1bba..a5be868 100644 --- a/client/src/pages/admin/AdminPageContent.tsx +++ b/client/src/pages/admin/AdminPageContent.tsx @@ -25,7 +25,7 @@ export function AdminPageContent({ animReady = true }: { animReady?: boolean }) return ( <>
{useAnimations ? ( ({ - queryKey: queryKeys.items.all, + queryKey: queryKeys.characters.all, queryFn: async () => { const { data } = await apiClient.get("/characters"); return data.map(itemDTOToWebstoreItem); diff --git a/cmd/seeder/main.go b/cmd/seeder/main.go index 7ed4e47..755f174 100644 --- a/cmd/seeder/main.go +++ b/cmd/seeder/main.go @@ -32,9 +32,7 @@ func main() { seeder.WithContext(ctx), seeder.WithSeeder(seeder.NewRoleSeeder()), seeder.WithSeeder(seeder.NewUserSeeder()), - seeder.WithSeeder(seeder.NewRaritySeeder()), - seeder.WithSeeder(seeder.NewCategorySeeder()), - seeder.WithSeeder(seeder.NewCharacterSeeder()), + seeder.WithSeeder(seeder.NewCharacterSeeder(), seeder.NewCategorySeeder(), seeder.NewRaritySeeder()), ) go func() { diff --git a/internal/controllers/player_profiles_controller.go b/internal/controllers/player_profiles_controller.go index 40bca74..3b1ea74 100644 --- a/internal/controllers/player_profiles_controller.go +++ b/internal/controllers/player_profiles_controller.go @@ -2,6 +2,7 @@ package controllers import ( "errors" + "log" "net/http" "os" dtos "smaash-web/internal/DTOs" @@ -94,9 +95,13 @@ func (pc PlayerProfileController) Create(c *gin.Context) { path := c.Request.URL.Path var body dtos.PlayerProfileCreateDTO if err := c.ShouldBindJSON(&body); err != nil { + log.Printf("profiles.create bind error: %v", err) c.JSON(http.StatusUnprocessableEntity, dtos.NewErrResp(err.Error(), path)) + return } + log.Printf("profiles.create requested: user_id=%d display_name=%q", body.UserID, body.DisplayName) + currentProfiles, err := pc.profilesRepo.ReadByUserID(c.Request.Context(), body.UserID) if err != nil { c.JSON(http.StatusInternalServerError, dtos.NewErrResp(err.Error(), path)) @@ -109,7 +114,8 @@ func (pc PlayerProfileController) Create(c *gin.Context) { } newProfile := dtos.CreateDTOToPlayerProfile(body) - if err := pc.profilesRepo.CreateWithBaseCharacters(c, &newProfile); err != nil { + if err := pc.profilesRepo.CreateWithBaseCharacters(c.Request.Context(), &newProfile); err != nil { + log.Printf("profiles.create failed: user_id=%d display_name=%q err=%v", body.UserID, body.DisplayName, err) if errors.Is(err, gorm.ErrDuplicatedKey) { c.JSON(http.StatusConflict, dtos.NewErrResp("Display name already taken", path)) return @@ -118,6 +124,8 @@ func (pc PlayerProfileController) Create(c *gin.Context) { return } + log.Printf("profiles.create success: profile_id=%d user_id=%d", newProfile.ID, newProfile.UserID) + c.JSON(http.StatusCreated, newProfile) } @@ -330,7 +338,13 @@ func (pc PlayerProfileController) ReadNotOwnedCharacters(c *gin.Context) { return } - c.JSON(http.StatusOK, utils.Map(player.Characters, dtos.CharacterToDTO)) + characters, err := pc.profilesRepo.ReadNotOwnedCharacters(c.Request.Context(), player.ID) + if err != nil { + c.JSON(http.StatusInternalServerError, dtos.NewErrResp(err.Error(), path)) + return + } + + c.JSON(http.StatusOK, utils.Map(characters, dtos.CharacterToDTO)) } func (pc PlayerProfileController) MountRoutes(apiGroup *gin.RouterGroup) { @@ -344,4 +358,5 @@ func (pc PlayerProfileController) MountRoutes(apiGroup *gin.RouterGroup) { profiles.GET("/:id/pfp", middlewares.Authorize(middlewares.ANY), middlewares.ValidateUrl, pc.GetPFP) profiles.GET("/:id/purchases", middlewares.Authorize(middlewares.ANY), middlewares.ValidateUrl, pc.ReadPurchases) profiles.GET("/:id/characters", middlewares.Authorize(middlewares.ANY), middlewares.ValidateUrl, pc.ReadCharacters) + profiles.GET("/:id/unowned", middlewares.Authorize(middlewares.ANY), middlewares.ValidateUrl, pc.ReadNotOwnedCharacters) } diff --git a/internal/controllers/users_controller.go b/internal/controllers/users_controller.go index b8354aa..c99f426 100644 --- a/internal/controllers/users_controller.go +++ b/internal/controllers/users_controller.go @@ -200,7 +200,7 @@ func (uc *UserController) AddProfileToUser(c *gin.Context) { newProfile := dtos.AppendDTOToPlayerProfile(body) newProfile.UserID = id.(uint) - if err := uc.profilesRepo.Create(c.Request.Context(), &newProfile); err != nil { + if err := uc.profilesRepo.CreateWithBaseCharacters(c.Request.Context(), &newProfile); err != nil { if errors.Is(err, gorm.ErrDuplicatedKey) { c.JSON(http.StatusConflict, dtos.NewErrResp("Display name already taken", path)) return diff --git a/internal/repository/profiles_repository_actions.go b/internal/repository/profiles_repository_actions.go index 9fd21c0..3d72a74 100644 --- a/internal/repository/profiles_repository_actions.go +++ b/internal/repository/profiles_repository_actions.go @@ -2,6 +2,7 @@ package repository import ( "context" + "log" "smaash-web/internal/models" "gorm.io/gorm" @@ -38,10 +39,9 @@ func (pra ProfilesRepositoryActions) HardDelete(c context.Context, id uint) erro func (pra ProfilesRepositoryActions) ReadNotOwnedCharacters(c context.Context, playerID uint) ([]models.Character, error) { var result []models.Character err := pra.conn. - Select("characters.*"). - Table("characters"). - Joins("LEFT JOIN player_characters ON characters.id = player_characters.character_id AND player_characters.player_profile_id = ?", playerID). - Where("player_characters.character_id IS NULL"). + Model(&models.Character{}). + Joins("LEFT JOIN profile_character ON characters.id = profile_character.character_id AND profile_character.player_profile_id = ?", playerID). + Where("profile_character.character_id IS NULL"). Find(&result).Error return result, err @@ -49,18 +49,20 @@ func (pra ProfilesRepositoryActions) ReadNotOwnedCharacters(c context.Context, p func (pra ProfilesRepositoryActions) CreateWithBaseCharacters(c context.Context, profile *models.PlayerProfile) error { return pra.conn.Transaction(func(tx *gorm.DB) error { - if err := gorm.G[models.PlayerProfile](pra.conn).Create(c, profile); err != nil { + log.Printf("profiles.create tx start: user_id=%d display_name=%q", profile.UserID, profile.DisplayName) + + if err := gorm.G[models.PlayerProfile](tx).Create(c, profile); err != nil { + log.Printf("profiles.create tx profile insert failed: err=%v", err) return err } // These characters are seeded into the database, so they're guaranteed to have ids of 1 and 2 - if err := pra.conn. + characters, _ := gorm.G[models.Character](tx).Where("id IN (1, 2)").Find(c) + + if err := tx. Model(profile). Association("Characters"). - Append([]models.Character{ - {Model: gorm.Model{ID: 1}}, - {Model: gorm.Model{ID: 2}}, - }); err != nil { + Append(characters); err != nil { return err } diff --git a/internal/seeder/category_seeder.go b/internal/seeder/category_seeder.go index 6b16fc0..57edf78 100644 --- a/internal/seeder/category_seeder.go +++ b/internal/seeder/category_seeder.go @@ -6,18 +6,27 @@ import ( "errors" "log" "os" + "slices" "smaash-web/internal/models" "gorm.io/gorm" "gorm.io/gorm/logger" ) -type CategorySeeder struct{} +type CategorySeeder struct{ children []Seeder } -func NewCategorySeeder() *CategorySeeder { +func NewCategorySeeder() Seeder { return &CategorySeeder{} } +func (cs CategorySeeder) GetChildren() []Seeder { + return cs.children +} + +func (cs CategorySeeder) AppendChildren(children ...Seeder) { + cs.children = slices.Concat(cs.children, children) +} + type CategoryDataFormat struct { Name string } diff --git a/internal/seeder/character_seeder.go b/internal/seeder/character_seeder.go index 55f88e9..84c55ca 100644 --- a/internal/seeder/character_seeder.go +++ b/internal/seeder/character_seeder.go @@ -6,18 +6,27 @@ import ( "errors" "log" "os" + "slices" "smaash-web/internal/models" "gorm.io/gorm" "gorm.io/gorm/logger" ) -type CharacterSeeder struct{} +type CharacterSeeder struct{ children []Seeder } -func NewCharacterSeeder() *CharacterSeeder { +func NewCharacterSeeder() Seeder { return &CharacterSeeder{} } +func (cs *CharacterSeeder) GetChildren() []Seeder { + return cs.children +} + +func (cs *CharacterSeeder) AppendChildren(children ...Seeder) { + cs.children = slices.Concat(cs.children, children) +} + type CharacterDataFormat struct { Name string Description string @@ -54,12 +63,14 @@ func (rs CharacterSeeder) Seed(c context.Context, data_root_path string, db *gor return err } + newCharacter, _ := gorm.G[models.Character](tx).Where("name = ?", val.Name).First(c) + for _, category := range val.Categories { cat, err := gorm.G[models.Category](tx).Where("name = ?", category).First(c) if err != nil { return err } - if err := tx.Model(&val).Association("Categories").Append(cat); err != nil { + if err := tx.Model(&newCharacter).Association("Categories").Append(cat); err != nil { return err } } diff --git a/internal/seeder/rarity_seeder.go b/internal/seeder/rarity_seeder.go index b83bebe..bfd3980 100644 --- a/internal/seeder/rarity_seeder.go +++ b/internal/seeder/rarity_seeder.go @@ -6,18 +6,27 @@ import ( "errors" "log" "os" + "slices" "smaash-web/internal/models" "gorm.io/gorm" "gorm.io/gorm/logger" ) -type RaritySeeder struct{} +type RaritySeeder struct{ children []Seeder } -func NewRaritySeeder() *RaritySeeder { +func NewRaritySeeder() Seeder { return &RaritySeeder{} } +func (rs RaritySeeder) GetChildren() []Seeder { + return rs.children +} + +func (rs RaritySeeder) AppendChildren(children ...Seeder) { + rs.children = slices.Concat(rs.children, children) +} + type RarityDataFormat struct { Name string } diff --git a/internal/seeder/role_seeder.go b/internal/seeder/role_seeder.go index 5e7ce7f..7c68278 100644 --- a/internal/seeder/role_seeder.go +++ b/internal/seeder/role_seeder.go @@ -6,18 +6,27 @@ import ( "errors" "log" "os" + "slices" "smaash-web/internal/models" "gorm.io/gorm" "gorm.io/gorm/logger" ) -type RoleSeeder struct{} +type RoleSeeder struct{ children []Seeder } -func NewRoleSeeder() *RoleSeeder { +func NewRoleSeeder() Seeder { return &RoleSeeder{} } +func (rs RoleSeeder) GetChildren() []Seeder { + return rs.children +} + +func (rs RoleSeeder) AppendChildren(children ...Seeder) { + rs.children = slices.Concat(rs.children, children) +} + type RoleDataFormat struct { Name string } diff --git a/internal/seeder/seeder.go b/internal/seeder/seeder.go index 05bf0c9..bc80180 100644 --- a/internal/seeder/seeder.go +++ b/internal/seeder/seeder.go @@ -15,6 +15,8 @@ import ( type Seeder interface { Seed(ctx context.Context, seed_data_root string, conn *gorm.DB, errStream chan error, logger logger.Interface) + GetChildren() []Seeder + AppendChildren(...Seeder) } type SeederManager struct { @@ -48,8 +50,9 @@ func NewSeedManager(seed_data_root string, dbUrl string, errStream chan error, o return seederManager } -func WithSeeder(seeder Seeder) SeederOpt { +func WithSeeder(seeder Seeder, children ...Seeder) SeederOpt { return func(sm *SeederManager) { + seeder.AppendChildren(children...) sm.seeders = append(sm.seeders, seeder) } } @@ -82,6 +85,10 @@ func (sm *SeederManager) Seed() { for _, seeder := range sm.seeders { wg.Go(func() { + log.Println(seeder) + for _, child := range seeder.GetChildren() { + child.Seed(sm.ctx, sm.seed_data_root, conn, sm.errStream, sm.logger) + } seeder.Seed(sm.ctx, sm.seed_data_root, conn, sm.errStream, sm.logger) }) } diff --git a/internal/seeder/test_source/characters.json b/internal/seeder/test_source/characters.json new file mode 100644 index 0000000..b930b96 --- /dev/null +++ b/internal/seeder/test_source/characters.json @@ -0,0 +1,26 @@ +[ + { + "Name": "Samurai", + "Description": "A balanced melee character for new profiles.", + "RarityID": 1, + "Price": 0, + "Implemented": true, + "ImgURI": "/characters/samurai.png", + "Categories": [ + "Character", + "Melee" + ] + }, + { + "Name": "Slime", + "Description": "A ranged character with simple starter stats.", + "RarityID": 1, + "Price": 0, + "Implemented": true, + "ImgURI": "/characters/slime.png", + "Categories": [ + "Character", + "Ranged" + ] + } +] \ No newline at end of file diff --git a/internal/seeder/user_seeder.go b/internal/seeder/user_seeder.go index a731f7d..d7370eb 100644 --- a/internal/seeder/user_seeder.go +++ b/internal/seeder/user_seeder.go @@ -6,6 +6,7 @@ import ( "errors" "log" "os" + "slices" "smaash-web/internal/models" "time" @@ -14,12 +15,20 @@ import ( "gorm.io/gorm/logger" ) -type UserSeeder struct{} +type UserSeeder struct{ children []Seeder } -func NewUserSeeder() *UserSeeder { +func NewUserSeeder() Seeder { return &UserSeeder{} } +func (us UserSeeder) GetChildren() []Seeder { + return us.children +} + +func (us UserSeeder) AppendChildren(children ...Seeder) { + us.children = slices.Concat(us.children, children) +} + type UserDataFormat struct { Email string Password string