diff --git a/apps/api-gql/cmd/main.go b/apps/api-gql/cmd/main.go index c32b6e271..2c9a1c900 100644 --- a/apps/api-gql/cmd/main.go +++ b/apps/api-gql/cmd/main.go @@ -28,6 +28,8 @@ import ( "github.com/twirapp/twir/apps/api-gql/internal/services/greetings" "github.com/twirapp/twir/apps/api-gql/internal/services/keywords" "github.com/twirapp/twir/apps/api-gql/internal/services/roles" + "github.com/twirapp/twir/apps/api-gql/internal/services/roles_users" + "github.com/twirapp/twir/apps/api-gql/internal/services/roles_with_roles_users" "github.com/twirapp/twir/apps/api-gql/internal/services/timers" "github.com/twirapp/twir/apps/api-gql/internal/services/twir-users" "github.com/twirapp/twir/apps/api-gql/internal/services/twitch-channels" @@ -71,8 +73,8 @@ import ( alertsrepository "github.com/twirapp/twir/libs/repositories/alerts" alertsrepositorypgx "github.com/twirapp/twir/libs/repositories/alerts/pgx" - commandswithgroupsandreponsesrepository "github.com/twirapp/twir/libs/repositories/commands_with_groups_and_responses" - commandswithgroupsandreponsesrepositorypgx "github.com/twirapp/twir/libs/repositories/commands_with_groups_and_responses/pgx" + commandswithgroupsandresponsesrepository "github.com/twirapp/twir/libs/repositories/commands_with_groups_and_responses" + commandswithgroupsandresponsesrepositorypgx "github.com/twirapp/twir/libs/repositories/commands_with_groups_and_responses/pgx" commandsgroupsrepository "github.com/twirapp/twir/libs/repositories/commands_group" commandsgroupsrepositorypgx "github.com/twirapp/twir/libs/repositories/commands_group/pgx" @@ -86,6 +88,9 @@ import ( rolesrepository "github.com/twirapp/twir/libs/repositories/roles" rolesrepositorypgx "github.com/twirapp/twir/libs/repositories/roles/pgx" + rolesusersrepository "github.com/twirapp/twir/libs/repositories/roles_users" + rolesusersrepositorypgx "github.com/twirapp/twir/libs/repositories/roles_users/pgx" + greetingsrepository "github.com/twirapp/twir/libs/repositories/greetings" greetingsrepositorypgx "github.com/twirapp/twir/libs/repositories/greetings/pgx" ) @@ -119,8 +124,10 @@ func main() { commands_groups.New, commands_responses.New, commands.New, - roles.New, greetings.New, + roles.New, + roles_users.New, + roles_with_roles_users.New, ), // repositories fx.Provide( @@ -161,8 +168,8 @@ func main() { fx.As(new(alertsrepository.Repository)), ), fx.Annotate( - commandswithgroupsandreponsesrepositorypgx.NewFx, - fx.As(new(commandswithgroupsandreponsesrepository.Repository)), + commandswithgroupsandresponsesrepositorypgx.NewFx, + fx.As(new(commandswithgroupsandresponsesrepository.Repository)), ), fx.Annotate( commandsgroupsrepositorypgx.NewFx, @@ -180,6 +187,10 @@ func main() { rolesrepositorypgx.NewFx, fx.As(new(rolesrepository.Repository)), ), + fx.Annotate( + rolesusersrepositorypgx.NewFx, + fx.As(new(rolesusersrepository.Repository)), + ), fx.Annotate( greetingsrepositorypgx.NewFx, fx.As(new(greetingsrepository.Repository)), diff --git a/apps/api-gql/internal/delivery/gql/mappers/roles.go b/apps/api-gql/internal/delivery/gql/mappers/roles.go new file mode 100644 index 000000000..fd52c75d0 --- /dev/null +++ b/apps/api-gql/internal/delivery/gql/mappers/roles.go @@ -0,0 +1,26 @@ +package mappers + +import ( + "github.com/twirapp/twir/apps/api-gql/internal/delivery/gql/gqlmodel" + "github.com/twirapp/twir/apps/api-gql/internal/entity" +) + +func RolesToGql(m entity.ChannelRole) gqlmodel.Role { + permissions := make([]gqlmodel.ChannelRolePermissionEnum, len(m.Permissions)) + for i, permission := range m.Permissions { + permissions[i] = gqlmodel.ChannelRolePermissionEnum(permission) + } + + return gqlmodel.Role{ + ID: m.ID, + ChannelID: m.ChannelID, + Name: m.Name, + Type: gqlmodel.RoleTypeEnum(m.Type.String()), + Permissions: permissions, + Settings: &gqlmodel.RoleSettings{ + RequiredWatchTime: int(m.RequiredWatchTime), + RequiredMessages: int(m.RequiredMessages), + RequiredUserChannelPoints: int(m.RequiredUsedChannelPoints), + }, + } +} diff --git a/apps/api-gql/internal/delivery/gql/resolvers/commands.resolver.go b/apps/api-gql/internal/delivery/gql/resolvers/commands.resolver.go index bd27b0a8e..b0a97e6b8 100644 --- a/apps/api-gql/internal/delivery/gql/resolvers/commands.resolver.go +++ b/apps/api-gql/internal/delivery/gql/resolvers/commands.resolver.go @@ -22,10 +22,7 @@ import ( ) // Responses is the resolver for the responses field. -func (r *commandResolver) Responses( - ctx context.Context, - obj *gqlmodel.Command, -) ([]gqlmodel.CommandResponse, error) { +func (r *commandResolver) Responses(ctx context.Context, obj *gqlmodel.Command) ([]gqlmodel.CommandResponse, error) { if obj == nil || obj.Default { return []gqlmodel.CommandResponse{}, nil } @@ -39,10 +36,7 @@ func (r *commandResolver) Responses( } // Group is the resolver for the group field. -func (r *commandResolver) Group(ctx context.Context, obj *gqlmodel.Command) ( - *gqlmodel.CommandGroup, - error, -) { +func (r *commandResolver) Group(ctx context.Context, obj *gqlmodel.Command) (*gqlmodel.CommandGroup, error) { if obj == nil || obj.GroupID == nil { return nil, nil } @@ -61,10 +55,7 @@ func (r *commandResolver) Group(ctx context.Context, obj *gqlmodel.Command) ( } // TwitchCategories is the resolver for the twitchCategories field. -func (r *commandResponseResolver) TwitchCategories( - ctx context.Context, - obj *gqlmodel.CommandResponse, -) ([]gqlmodel.TwitchCategory, error) { +func (r *commandResponseResolver) TwitchCategories(ctx context.Context, obj *gqlmodel.CommandResponse) ([]gqlmodel.TwitchCategory, error) { categories, err := data_loader.GetTwitchCategoriesByIDs(ctx, obj.TwitchCategoriesIds) if err != nil { return nil, err @@ -79,10 +70,7 @@ func (r *commandResponseResolver) TwitchCategories( } // CommandsCreate is the resolver for the commandsCreate field -func (r *mutationResolver) CommandsCreate( - ctx context.Context, - opts gqlmodel.CommandsCreateOpts, -) (*gqlmodel.CommandCreatePayload, error) { +func (r *mutationResolver) CommandsCreate(ctx context.Context, opts gqlmodel.CommandsCreateOpts) (*gqlmodel.CommandCreatePayload, error) { dashboardId, err := r.sessions.GetSelectedDashboard(ctx) if err != nil { return nil, err @@ -153,11 +141,7 @@ func (r *mutationResolver) CommandsCreate( } // CommandsUpdate is the resolver for the commandsUpdate field. -func (r *mutationResolver) CommandsUpdate( - ctx context.Context, - id string, - opts gqlmodel.CommandsUpdateOpts, -) (bool, error) { +func (r *mutationResolver) CommandsUpdate(ctx context.Context, id string, opts gqlmodel.CommandsUpdateOpts) (bool, error) { dashboardId, err := r.sessions.GetSelectedDashboard(ctx) if err != nil { return false, err @@ -305,10 +289,7 @@ func (r *queryResolver) Commands(ctx context.Context) ([]gqlmodel.Command, error } // CommandsPublic is the resolver for the commandsPublic field. -func (r *queryResolver) CommandsPublic( - ctx context.Context, - channelID string, -) ([]gqlmodel.PublicCommand, error) { +func (r *queryResolver) CommandsPublic(ctx context.Context, channelID string) ([]gqlmodel.PublicCommand, error) { if channelID == "" { return nil, fmt.Errorf("channelID is required") } diff --git a/apps/api-gql/internal/delivery/gql/resolvers/greetings.resolver.go b/apps/api-gql/internal/delivery/gql/resolvers/greetings.resolver.go index 220fc91ac..1d764536a 100644 --- a/apps/api-gql/internal/delivery/gql/resolvers/greetings.resolver.go +++ b/apps/api-gql/internal/delivery/gql/resolvers/greetings.resolver.go @@ -17,18 +17,12 @@ import ( ) // TwitchProfile is the resolver for the twitchProfile field. -func (r *greetingResolver) TwitchProfile( - ctx context.Context, - obj *gqlmodel.Greeting, -) (*gqlmodel.TwirUserTwitchInfo, error) { +func (r *greetingResolver) TwitchProfile(ctx context.Context, obj *gqlmodel.Greeting) (*gqlmodel.TwirUserTwitchInfo, error) { return data_loader.GetHelixUserById(ctx, obj.UserID) } // GreetingsCreate is the resolver for the greetingsCreate field. -func (r *mutationResolver) GreetingsCreate( - ctx context.Context, - opts gqlmodel.GreetingsCreateInput, -) (*gqlmodel.Greeting, error) { +func (r *mutationResolver) GreetingsCreate(ctx context.Context, opts gqlmodel.GreetingsCreateInput) (*gqlmodel.Greeting, error) { dashboardId, err := r.sessions.GetSelectedDashboard(ctx) if err != nil { return nil, err @@ -60,11 +54,7 @@ func (r *mutationResolver) GreetingsCreate( } // GreetingsUpdate is the resolver for the greetingsUpdate field. -func (r *mutationResolver) GreetingsUpdate( - ctx context.Context, - id uuid.UUID, - opts gqlmodel.GreetingsUpdateInput, -) (*gqlmodel.Greeting, error) { +func (r *mutationResolver) GreetingsUpdate(ctx context.Context, id uuid.UUID, opts gqlmodel.GreetingsUpdateInput) (*gqlmodel.Greeting, error) { dashboardId, err := r.sessions.GetSelectedDashboard(ctx) if err != nil { return nil, err diff --git a/apps/api-gql/internal/delivery/gql/resolvers/resolver.go b/apps/api-gql/internal/delivery/gql/resolvers/resolver.go index 55be8d9c0..b9878f8f2 100644 --- a/apps/api-gql/internal/delivery/gql/resolvers/resolver.go +++ b/apps/api-gql/internal/delivery/gql/resolvers/resolver.go @@ -25,6 +25,8 @@ import ( "github.com/twirapp/twir/apps/api-gql/internal/services/greetings" "github.com/twirapp/twir/apps/api-gql/internal/services/keywords" "github.com/twirapp/twir/apps/api-gql/internal/services/roles" + "github.com/twirapp/twir/apps/api-gql/internal/services/roles_users" + "github.com/twirapp/twir/apps/api-gql/internal/services/roles_with_roles_users" "github.com/twirapp/twir/apps/api-gql/internal/services/timers" twir_users "github.com/twirapp/twir/apps/api-gql/internal/services/twir-users" "github.com/twirapp/twir/apps/api-gql/internal/services/users" @@ -71,8 +73,10 @@ type Resolver struct { commandsService *commands.Service commandsWithGroupsAndResponsesService *commands_with_groups_and_responses.Service commandsResponsesService *commands_responses.Service - rolesService *roles.Service greetingsService *greetings.Service + rolesService *roles.Service + rolesUsersService *roles_users.Service + rolesWithUsersService *roles_with_roles_users.Service } type Opts struct { @@ -105,8 +109,10 @@ type Opts struct { CommandsService *commands.Service CommandsWithGroupsAndResponsesService *commands_with_groups_and_responses.Service CommandsResponsesService *commands_responses.Service - RolesService *roles.Service GreetingsService *greetings.Service + RolesService *roles.Service + RolesUsersService *roles_users.Service + RolesWithUsersService *roles_with_roles_users.Service } func New(opts Opts) (*Resolver, error) { @@ -121,11 +127,11 @@ func New(opts Opts) (*Resolver, error) { gorm: opts.Gorm, twitchClient: twitchClient, cachedTwitchClient: opts.CachedTwitchClient, + cachedCommandsClient: opts.CachedCommandsClient, minioClient: opts.Minio, twirBus: opts.TwirBus, logger: opts.Logger, redis: opts.Redis, - cachedCommandsClient: opts.CachedCommandsClient, tokensClient: opts.TokensGrpc, wsRouter: opts.WsRouter, twirStats: opts.TwirStats, @@ -143,8 +149,10 @@ func New(opts Opts) (*Resolver, error) { commandsService: opts.CommandsService, commandsWithGroupsAndResponsesService: opts.CommandsWithGroupsAndResponsesService, commandsResponsesService: opts.CommandsResponsesService, - rolesService: opts.RolesService, greetingsService: opts.GreetingsService, + rolesService: opts.RolesService, + rolesUsersService: opts.RolesUsersService, + rolesWithUsersService: opts.RolesWithUsersService, }, nil } diff --git a/apps/api-gql/internal/delivery/gql/resolvers/roles.resolver.go b/apps/api-gql/internal/delivery/gql/resolvers/roles.resolver.go index 70ac44a97..3c600d0e4 100644 --- a/apps/api-gql/internal/delivery/gql/resolvers/roles.resolver.go +++ b/apps/api-gql/internal/delivery/gql/resolvers/roles.resolver.go @@ -7,22 +7,23 @@ package resolvers import ( "context" "fmt" - "slices" "github.com/google/uuid" "github.com/samber/lo" - model "github.com/satont/twir/libs/gomodels" - "github.com/satont/twir/libs/logger/audit" - "github.com/satont/twir/libs/utils" data_loader "github.com/twirapp/twir/apps/api-gql/internal/delivery/gql/data-loader" "github.com/twirapp/twir/apps/api-gql/internal/delivery/gql/gqlmodel" "github.com/twirapp/twir/apps/api-gql/internal/delivery/gql/graph" "github.com/twirapp/twir/apps/api-gql/internal/delivery/gql/mappers" - "gorm.io/gorm" + "github.com/twirapp/twir/apps/api-gql/internal/entity" + "github.com/twirapp/twir/apps/api-gql/internal/services/roles" + "github.com/twirapp/twir/apps/api-gql/internal/services/roles_with_roles_users" ) // RolesCreate is the resolver for the rolesCreate field. -func (r *mutationResolver) RolesCreate(ctx context.Context, opts gqlmodel.RolesCreateOrUpdateOpts) (bool, error) { +func (r *mutationResolver) RolesCreate( + ctx context.Context, + opts gqlmodel.RolesCreateOrUpdateOpts, +) (bool, error) { dashboardId, err := r.sessions.GetSelectedDashboard(ctx) if err != nil { return false, err @@ -33,55 +34,46 @@ func (r *mutationResolver) RolesCreate(ctx context.Context, opts gqlmodel.RolesC return false, err } - permissions := make([]string, 0, len(opts.Permissions)) - for _, permission := range opts.Permissions { - permissions = append(permissions, permission.String()) + permissions := make([]string, len(opts.Permissions)) + for i, permission := range opts.Permissions { + permissions[i] = permission.String() } - users := make([]*model.ChannelRoleUser, 0, len(opts.Users)) - for _, userId := range opts.Users { - users = append( - users, - &model.ChannelRoleUser{ - ID: uuid.New().String(), - UserID: userId, - }, - ) - } - - entity := &model.ChannelRole{ - ID: uuid.NewString(), - ChannelID: dashboardId, - Name: opts.Name, - Type: model.ChannelRoleEnum(gqlmodel.RoleTypeEnumCustom.String()), - Permissions: permissions, - RequiredWatchTime: int64(opts.Settings.RequiredWatchTime), - RequiredMessages: int32(opts.Settings.RequiredMessages), - RequiredUsedChannelPoints: int64(opts.Settings.RequiredUserChannelPoints), - Users: users, - } - - if err := r.gorm.WithContext(ctx).Create(entity).Error; err != nil { - return false, err + users := make([]roles_with_roles_users.CreateInputUser, len(opts.Users)) + for idx, userId := range opts.Users { + users[idx] = roles_with_roles_users.CreateInputUser{ + UserID: userId, + } } - r.logger.Audit( - "Role create", - audit.Fields{ - NewValue: entity, - ActorID: lo.ToPtr(user.ID), - ChannelID: lo.ToPtr(dashboardId), - System: mappers.AuditSystemToTableName(gqlmodel.AuditLogSystemChannelRoles), - OperationType: audit.OperationCreate, - ObjectID: &entity.ID, + err = r.rolesWithUsersService.Create( + ctx, roles_with_roles_users.CreateInput{ + Role: roles.CreateInput{ + ChannelID: dashboardId, + ActorID: user.ID, + Name: opts.Name, + Type: entity.ChannelRoleTypeCustom, + Permissions: permissions, + RequiredWatchTime: int64(opts.Settings.RequiredWatchTime), + RequiredMessages: int32(opts.Settings.RequiredMessages), + RequiredUsedChannelPoints: int64(opts.Settings.RequiredUserChannelPoints), + }, + Users: users, }, ) + if err != nil { + return false, err + } return true, nil } // RolesUpdate is the resolver for the rolesUpdate field. -func (r *mutationResolver) RolesUpdate(ctx context.Context, id string, opts gqlmodel.RolesCreateOrUpdateOpts) (bool, error) { +func (r *mutationResolver) RolesUpdate( + ctx context.Context, + id uuid.UUID, + opts gqlmodel.RolesCreateOrUpdateOpts, +) (bool, error) { dashboardId, err := r.sessions.GetSelectedDashboard(ctx) if err != nil { return false, err @@ -92,93 +84,43 @@ func (r *mutationResolver) RolesUpdate(ctx context.Context, id string, opts gqlm return false, err } - var rolesCount int64 - if err := r.gorm. - WithContext(ctx). - Model(&model.ChannelRole{}). - Where(`"channelId" = ?`, dashboardId). - Count(&rolesCount). - Error; err != nil { - return false, fmt.Errorf("failed to count roles: %w", err) - } - - if rolesCount >= 20 { - return false, fmt.Errorf("maximum number of roles reached") + permissions := make([]string, len(opts.Permissions)) + for i, permission := range opts.Permissions { + permissions[i] = permission.String() } - entity := &model.ChannelRole{} - if err := r.gorm. - WithContext(ctx). - Where(`"id" = ? AND "channelId" = ?`, id, dashboardId). - First(entity). - Error; err != nil { - return false, fmt.Errorf("failed to find role: %w", err) - } - - var entityCopy model.ChannelRole - if err := utils.DeepCopy(entity, &entityCopy); err != nil { - return false, err - } - - entity.Name = opts.Name - entity.RequiredWatchTime = int64(opts.Settings.RequiredWatchTime) - entity.RequiredMessages = int32(opts.Settings.RequiredMessages) - entity.RequiredUsedChannelPoints = int64(opts.Settings.RequiredUserChannelPoints) - - permissions := make([]string, 0, len(opts.Permissions)) - for _, permission := range opts.Permissions { - permissions = append(permissions, permission.String()) + users := make([]roles_with_roles_users.CreateInputUser, len(opts.Users)) + for idx, userId := range opts.Users { + users[idx] = roles_with_roles_users.CreateInputUser{ + UserID: userId, + } } - entity.Permissions = permissions - users := make([]*model.ChannelRoleUser, 0, len(opts.Users)) - for _, userId := range opts.Users { - users = append( - users, - &model.ChannelRoleUser{ - ID: uuid.New().String(), - UserID: userId, - RoleID: entity.ID, + if err := r.rolesWithUsersService.Update( + ctx, roles_with_roles_users.UpdateInput{ + ID: id, + ChannelID: dashboardId, + ActorID: user.ID, + Role: roles.UpdateInput{ + ChannelID: dashboardId, + ActorID: user.ID, + Name: &opts.Name, + Permissions: permissions, + RequiredWatchTime: lo.ToPtr(int64(opts.Settings.RequiredMessages)), + RequiredMessages: lo.ToPtr(int32(opts.Settings.RequiredMessages)), + RequiredUsedChannelPoints: lo.ToPtr(int64(opts.Settings.RequiredUserChannelPoints)), }, - ) - } - entity.Users = users - - txErr := r.gorm.WithContext(ctx).Transaction( - func(tx *gorm.DB) error { - if err := tx.Where( - `"roleId" = ?`, - entity.ID, - ).Delete(&model.ChannelRoleUser{}).Error; err != nil { - return err - } - - err := tx.WithContext(ctx).Save(entity).Error - return err + Users: users, }, - ) - if txErr != nil { - return false, fmt.Errorf("failed to update role: %w", txErr) + ); err != nil { + return false, err } - r.logger.Audit( - "Role update", - audit.Fields{ - OldValue: entityCopy, - NewValue: entity, - ActorID: lo.ToPtr(user.ID), - ChannelID: lo.ToPtr(dashboardId), - System: mappers.AuditSystemToTableName(gqlmodel.AuditLogSystemChannelRoles), - OperationType: audit.OperationUpdate, - ObjectID: &entity.ID, - }, - ) - return true, nil } // RolesRemove is the resolver for the rolesRemove field. -func (r *mutationResolver) RolesRemove(ctx context.Context, id string) (bool, error) { +func (r *mutationResolver) RolesRemove(ctx context.Context, id uuid.UUID) (bool, error) { dashboardId, err := r.sessions.GetSelectedDashboard(ctx) if err != nil { return false, err @@ -189,38 +131,17 @@ func (r *mutationResolver) RolesRemove(ctx context.Context, id string) (bool, er return false, err } - entity := &model.ChannelRole{} - if err := r.gorm. - WithContext(ctx). - Where(`"id" = ? AND "channelId" = ?`, id, dashboardId). - First(entity). - Error; err != nil { - return false, fmt.Errorf("failed to find role: %w", err) - } - - if entity.Type.String() != model.ChannelRoleTypeCustom.String() { - return false, fmt.Errorf("cannot remove default roles") - } - - if err := r.gorm. - WithContext(ctx). - Delete(entity). - Error; err != nil { + if err := r.rolesService.Delete( + ctx, + roles.DeleteInput{ + ChannelID: dashboardId, + ActorID: user.ID, + ID: id, + }, + ); err != nil { return false, err } - r.logger.Audit( - "Role remove", - audit.Fields{ - OldValue: entity, - ActorID: lo.ToPtr(user.ID), - ChannelID: lo.ToPtr(dashboardId), - System: mappers.AuditSystemToTableName(gqlmodel.AuditLogSystemChannelRoles), - OperationType: audit.OperationDelete, - ObjectID: &entity.ID, - }, - ) - return true, nil } @@ -231,60 +152,31 @@ func (r *queryResolver) Roles(ctx context.Context) ([]gqlmodel.Role, error) { return nil, err } - var entities []model.ChannelRole - if err := r.gorm. - WithContext(ctx). - Where(`"channelId" = ?`, dashboardId). - Group(`"id"`). - Find(&entities). - Error; err != nil { + entities, err := r.rolesService.GetManyByChannelID(ctx, dashboardId) + if err != nil { return nil, err } - res := make([]gqlmodel.Role, 0, len(entities)) - for _, entity := range entities { - permissions := make([]gqlmodel.ChannelRolePermissionEnum, 0, len(entity.Permissions)) - for _, permission := range entity.Permissions { - permissions = append(permissions, gqlmodel.ChannelRolePermissionEnum(permission)) - } - - res = append( - res, - gqlmodel.Role{ - ID: entity.ID, - ChannelID: entity.ChannelID, - Name: entity.Name, - Type: gqlmodel.RoleTypeEnum(entity.Type.String()), - Permissions: permissions, - Settings: &gqlmodel.RoleSettings{ - RequiredWatchTime: int(entity.RequiredWatchTime), - RequiredMessages: int(entity.RequiredMessages), - RequiredUserChannelPoints: int(entity.RequiredUsedChannelPoints), - }, - }, - ) + result := make([]gqlmodel.Role, len(entities)) + for i, role := range entities { + result[i] = mappers.RolesToGql(role) } - slices.SortFunc( - res, func(a, b gqlmodel.Role) int { - typeIdx := lo.IndexOf(gqlmodel.AllRoleTypeEnum, a.Type) - - return typeIdx - lo.IndexOf(gqlmodel.AllRoleTypeEnum, b.Type) - }, - ) - - return res, nil + return result, nil } // Users is the resolver for the users field. -func (r *roleResolver) Users(ctx context.Context, obj *gqlmodel.Role) ([]gqlmodel.TwirUserTwitchInfo, error) { - var users []model.ChannelRoleUser - if err := r.gorm. - WithContext(ctx). - Where(`"roleId" = ?`, obj.ID). - Find(&users). - Error; err != nil { - return nil, fmt.Errorf("failed to fetch users: %w", err) +func (r *roleResolver) Users( + ctx context.Context, + obj *gqlmodel.Role, +) ([]gqlmodel.TwirUserTwitchInfo, error) { + if obj == nil { + return nil, nil + } + + users, err := r.rolesUsersService.GetManyByRoleID(ctx, obj.ID) + if err != nil { + return nil, err } ids := make([]string, 0, len(users)) diff --git a/apps/api-gql/internal/entity/role.go b/apps/api-gql/internal/entity/role.go index c96075430..be9f2b13d 100644 --- a/apps/api-gql/internal/entity/role.go +++ b/apps/api-gql/internal/entity/role.go @@ -15,6 +15,8 @@ type ChannelRole struct { RequiredUsedChannelPoints int64 } +var ChannelRoleNil = ChannelRole{} + type ChannelRoleEnum string func (c ChannelRoleEnum) String() string { @@ -28,3 +30,19 @@ const ( ChannelRoleTypeVip ChannelRoleEnum = "VIP" ChannelRoleTypeCustom ChannelRoleEnum = "CUSTOM" ) + +var AllChannelRoleTypeEnum = []ChannelRoleEnum{ + ChannelRoleTypeBroadcaster, + ChannelRoleTypeModerator, + ChannelRoleTypeSubscriber, + ChannelRoleTypeVip, + ChannelRoleTypeCustom, +} + +type ChannelRoleUser struct { + ID uuid.UUID + UserID string + RoleID uuid.UUID +} + +var ChannelRoleUserNil = ChannelRoleUser{} diff --git a/apps/api-gql/internal/services/roles/roles.go b/apps/api-gql/internal/services/roles/roles.go index c49b6a94c..5572b3b2b 100644 --- a/apps/api-gql/internal/services/roles/roles.go +++ b/apps/api-gql/internal/services/roles/roles.go @@ -2,30 +2,46 @@ package roles import ( "context" + "fmt" + "slices" "github.com/google/uuid" + "github.com/samber/lo" + "github.com/satont/twir/libs/logger" + "github.com/satont/twir/libs/logger/audit" + "github.com/twirapp/twir/apps/api-gql/internal/delivery/gql/gqlmodel" + "github.com/twirapp/twir/apps/api-gql/internal/delivery/gql/mappers" "github.com/twirapp/twir/apps/api-gql/internal/entity" "github.com/twirapp/twir/libs/repositories/roles" "github.com/twirapp/twir/libs/repositories/roles/model" + "github.com/twirapp/twir/libs/repositories/roles_users" "go.uber.org/fx" ) type Opts struct { fx.In - RolesRepository roles.Repository + RolesRepository roles.Repository + RolesUsersRepository roles_users.Repository + Logger logger.Logger } func New(opts Opts) *Service { return &Service{ - rolesRepository: opts.RolesRepository, + rolesRepository: opts.RolesRepository, + rolesUsersRepository: opts.RolesUsersRepository, + logger: opts.Logger, } } type Service struct { - rolesRepository roles.Repository + rolesRepository roles.Repository + rolesUsersRepository roles_users.Repository + logger logger.Logger } +var maxRoles = 20 + func (c *Service) modelToEntity(m model.Role) entity.ChannelRole { return entity.ChannelRole{ ID: m.ID, @@ -67,5 +83,167 @@ func (c *Service) GetManyByChannelID(ctx context.Context, channelID string) ( entities = append(entities, c.modelToEntity(dbRole)) } + slices.SortFunc( + entities, + func(a, b entity.ChannelRole) int { + typeIdx := lo.IndexOf(entity.AllChannelRoleTypeEnum, a.Type) + + return typeIdx - lo.IndexOf(entity.AllChannelRoleTypeEnum, b.Type) + }, + ) + return entities, nil } + +type CreateInput struct { + ChannelID string + ActorID string + + Name string + Type entity.ChannelRoleEnum + Permissions []string + RequiredWatchTime int64 + RequiredMessages int32 + RequiredUsedChannelPoints int64 +} + +func (c *Service) Create(ctx context.Context, input CreateInput) (entity.ChannelRole, error) { + dbRoles, err := c.rolesRepository.GetManyByChannelID(ctx, input.ChannelID) + if err != nil { + return entity.ChannelRoleNil, err + } + + if len(dbRoles) >= maxRoles { + return entity.ChannelRoleNil, fmt.Errorf("maximum number of roles reached") + } + + dbRole, err := c.rolesRepository.Create( + ctx, roles.CreateInput{ + ChannelID: input.ChannelID, + Name: input.Name, + Type: model.ChannelRoleEnum(input.Type.String()), + Permissions: input.Permissions, + RequiredWatchTime: input.RequiredWatchTime, + RequiredMessages: input.RequiredMessages, + RequiredUsedChannelPoints: input.RequiredUsedChannelPoints, + }, + ) + if err != nil { + return entity.ChannelRole{}, err + } + + c.logger.Audit( + "Role create", + audit.Fields{ + NewValue: dbRole, + ActorID: &input.ActorID, + ChannelID: &input.ChannelID, + System: mappers.AuditSystemToTableName(gqlmodel.AuditLogSystemChannelRoles), + OperationType: audit.OperationCreate, + ObjectID: lo.ToPtr(dbRole.ID.String()), + }, + ) + + return c.modelToEntity(dbRole), nil +} + +type UpdateInput struct { + ChannelID string + ActorID string + + Name *string + Permissions []string + RequiredWatchTime *int64 + RequiredMessages *int32 + RequiredUsedChannelPoints *int64 +} + +func (c *Service) Update(ctx context.Context, id uuid.UUID, input UpdateInput) ( + entity.ChannelRole, + error, +) { + dbRole, err := c.rolesRepository.GetByID(ctx, id) + if err != nil { + return entity.ChannelRoleNil, err + } + + if dbRole.ChannelID != input.ChannelID { + return entity.ChannelRoleNil, fmt.Errorf("role doesn't belong to the channel") + } + + updateInput := roles.UpdateInput{ + Name: input.Name, + Permissions: input.Permissions, + RequiredWatchTime: input.RequiredWatchTime, + RequiredMessages: input.RequiredMessages, + RequiredUsedChannelPoints: input.RequiredUsedChannelPoints, + } + + newRole, err := c.rolesRepository.Update(ctx, id, updateInput) + if err != nil { + return entity.ChannelRole{}, err + } + + c.logger.Audit( + "Role update", + audit.Fields{ + OldValue: dbRole, + NewValue: newRole, + ActorID: &input.ActorID, + ChannelID: &input.ChannelID, + System: mappers.AuditSystemToTableName(gqlmodel.AuditLogSystemChannelRoles), + OperationType: audit.OperationUpdate, + ObjectID: lo.ToPtr(newRole.ID.String()), + }, + ) + + return c.modelToEntity(newRole), nil +} + +type DeleteInput struct { + ChannelID string + ActorID string + ID uuid.UUID +} + +func (c *Service) Delete(ctx context.Context, input DeleteInput) error { + dbRole, err := c.rolesRepository.GetByID(ctx, input.ID) + if err != nil { + return err + } + + if dbRole.ChannelID != input.ChannelID { + return fmt.Errorf("role doesn't belong to the channel") + } + + if dbRole.Type != model.ChannelRoleTypeCustom { + return fmt.Errorf("cannot remove default roles") + } + + if err := c.rolesRepository.Delete(ctx, input.ID); err != nil { + return err + } + + c.logger.Audit( + "Role remove", + audit.Fields{ + OldValue: dbRole, + ActorID: &input.ActorID, + ChannelID: &input.ChannelID, + System: mappers.AuditSystemToTableName(gqlmodel.AuditLogSystemChannelRoles), + OperationType: audit.OperationDelete, + ObjectID: lo.ToPtr(dbRole.ID.String()), + }, + ) + + return nil +} + +func (c *Service) GetByID(ctx context.Context, id uuid.UUID) (entity.ChannelRole, error) { + dbRole, err := c.rolesRepository.GetByID(ctx, id) + if err != nil { + return entity.ChannelRoleNil, err + } + + return c.modelToEntity(dbRole), nil +} diff --git a/apps/api-gql/internal/services/roles_users/roles_users.go b/apps/api-gql/internal/services/roles_users/roles_users.go new file mode 100644 index 000000000..722900a8e --- /dev/null +++ b/apps/api-gql/internal/services/roles_users/roles_users.go @@ -0,0 +1,109 @@ +package roles_users + +import ( + "context" + "fmt" + + "github.com/google/uuid" + "github.com/twirapp/twir/apps/api-gql/internal/entity" + "github.com/twirapp/twir/libs/repositories/roles_users" + "github.com/twirapp/twir/libs/repositories/roles_users/model" + "go.uber.org/fx" +) + +type Opts struct { + fx.In + + RolesUsersRepository roles_users.Repository +} + +func New(opts Opts) *Service { + return &Service{ + rolesUsersRepository: opts.RolesUsersRepository, + } +} + +type Service struct { + rolesUsersRepository roles_users.Repository +} + +type CreateInput struct { + UserID string + RoleID uuid.UUID +} + +func (c *Service) mapToEntity(m model.RoleUser) entity.ChannelRoleUser { + return entity.ChannelRoleUser{ + ID: m.ID, + UserID: m.UserID, + RoleID: m.RoleID, + } +} + +func (c *Service) Create(ctx context.Context, input CreateInput) (entity.ChannelRoleUser, error) { + user, err := c.rolesUsersRepository.Create( + ctx, roles_users.CreateInput{ + UserID: input.UserID, + RoleID: input.RoleID, + }, + ) + if err != nil { + return entity.ChannelRoleUserNil, fmt.Errorf("cannot create role user: %w", err) + } + + return c.mapToEntity(user), nil +} + +func (c *Service) CreateMany(ctx context.Context, inputs []CreateInput) ( + []entity.ChannelRoleUser, + error, +) { + if len(inputs) == 0 { + return nil, nil + } + + convertedInputs := make([]roles_users.CreateInput, len(inputs)) + for i, input := range inputs { + convertedInputs[i] = roles_users.CreateInput{ + UserID: input.UserID, + RoleID: input.RoleID, + } + } + + users, err := c.rolesUsersRepository.CreateMany(ctx, convertedInputs) + if err != nil { + return nil, fmt.Errorf("cannot create role users: %w", err) + } + + result := make([]entity.ChannelRoleUser, len(users)) + for i, u := range users { + result[i] = c.mapToEntity(u) + } + + return result, nil +} + +func (c *Service) DeleteManyByRoleID(ctx context.Context, roleID uuid.UUID) error { + if err := c.rolesUsersRepository.DeleteManyByRoleID(ctx, roleID); err != nil { + return fmt.Errorf("cannot delete role users by role ID: %w", err) + } + + return nil +} + +func (c *Service) GetManyByRoleID(ctx context.Context, roleID uuid.UUID) ( + []entity.ChannelRoleUser, + error, +) { + users, err := c.rolesUsersRepository.GetManyByRoleID(ctx, roleID) + if err != nil { + return nil, fmt.Errorf("cannot get role users by role ID: %w", err) + } + + result := make([]entity.ChannelRoleUser, len(users)) + for i, u := range users { + result[i] = c.mapToEntity(u) + } + + return result, nil +} diff --git a/apps/api-gql/internal/services/roles_with_roles_users/roles_with_roles_users.go b/apps/api-gql/internal/services/roles_with_roles_users/roles_with_roles_users.go new file mode 100644 index 000000000..009586bd4 --- /dev/null +++ b/apps/api-gql/internal/services/roles_with_roles_users/roles_with_roles_users.go @@ -0,0 +1,154 @@ +package roles_with_roles_users + +import ( + "context" + "fmt" + + "github.com/avito-tech/go-transaction-manager/trm/v2" + "github.com/google/uuid" + "github.com/satont/twir/libs/logger" + "github.com/twirapp/twir/apps/api-gql/internal/entity" + "github.com/twirapp/twir/apps/api-gql/internal/services/roles" + "github.com/twirapp/twir/apps/api-gql/internal/services/roles_users" + "go.uber.org/fx" +) + +type Opts struct { + fx.In + + TrmManager trm.Manager + RolesService *roles.Service + RolesUsersService *roles_users.Service + Logger logger.Logger +} + +func New(opts Opts) *Service { + return &Service{ + trmManager: opts.TrmManager, + rolesService: opts.RolesService, + rolesUsersService: opts.RolesUsersService, + logger: opts.Logger, + } +} + +type Service struct { + trmManager trm.Manager + rolesService *roles.Service + rolesUsersService *roles_users.Service + logger logger.Logger +} + +type CreateInput struct { + Role roles.CreateInput + Users []CreateInputUser +} + +type CreateInputUser struct { + UserID string +} + +func (c *Service) Create(ctx context.Context, input CreateInput) error { + err := c.trmManager.Do( + ctx, func(txCtx context.Context) error { + role, err := c.rolesService.Create(txCtx, input.Role) + if err != nil { + return err + } + + usersInputs := make([]roles_users.CreateInput, 0, len(input.Users)) + for _, user := range input.Users { + usersInputs = append( + usersInputs, roles_users.CreateInput{ + UserID: user.UserID, + RoleID: role.ID, + }, + ) + } + + _, err = c.rolesUsersService.CreateMany(txCtx, usersInputs) + if err != nil { + return err + } + + return nil + }, + ) + + if err != nil { + return fmt.Errorf("failed to create role with users: %w", err) + } + + return nil +} + +type UpdateInput struct { + ID uuid.UUID + ChannelID string + ActorID string + + Role roles.UpdateInput + Users []CreateInputUser +} + +func (c *Service) Update(ctx context.Context, input UpdateInput) error { + dbRole, err := c.rolesService.GetByID(ctx, input.ID) + if err != nil { + return fmt.Errorf("failed to get role: %w", err) + } + + if dbRole.ChannelID != input.ChannelID { + return fmt.Errorf("role doesn't belong to the channel") + } + + var newRole entity.ChannelRole + err = c.trmManager.Do( + ctx, + func(txCtx context.Context) error { + newDbRole, err := c.rolesService.Update( + txCtx, + input.ID, + roles.UpdateInput{ + ChannelID: input.ChannelID, + ActorID: input.ActorID, + Name: input.Role.Name, + Permissions: input.Role.Permissions, + RequiredWatchTime: input.Role.RequiredWatchTime, + RequiredMessages: input.Role.RequiredMessages, + RequiredUsedChannelPoints: input.Role.RequiredUsedChannelPoints, + }, + ) + if err != nil { + return err + } + + newRole = newDbRole + + err = c.rolesUsersService.DeleteManyByRoleID(txCtx, newRole.ID) + if err != nil { + return err + } + + usersInputs := make([]roles_users.CreateInput, 0, len(input.Users)) + for _, user := range input.Users { + usersInputs = append( + usersInputs, roles_users.CreateInput{ + UserID: user.UserID, + RoleID: newRole.ID, + }, + ) + } + + _, err = c.rolesUsersService.CreateMany(txCtx, usersInputs) + if err != nil { + return err + } + + return nil + }, + ) + if err != nil { + return fmt.Errorf("failed to update role with users: %w", err) + } + + return nil +} diff --git a/apps/api-gql/schema/roles.graphqls b/apps/api-gql/schema/roles.graphqls index a0edad029..96196f179 100644 --- a/apps/api-gql/schema/roles.graphqls +++ b/apps/api-gql/schema/roles.graphqls @@ -4,12 +4,12 @@ extend type Query { extend type Mutation { rolesCreate(opts: RolesCreateOrUpdateOpts!): Boolean! @isAuthenticated @hasChannelRolesDashboardPermission(permission: MANAGE_ROLES) - rolesUpdate(id: ID!, opts: RolesCreateOrUpdateOpts!): Boolean! @isAuthenticated @hasChannelRolesDashboardPermission(permission: MANAGE_ROLES) - rolesRemove(id: ID!): Boolean! @isAuthenticated @hasChannelRolesDashboardPermission(permission: MANAGE_ROLES) + rolesUpdate(id: UUID!, opts: RolesCreateOrUpdateOpts!): Boolean! @isAuthenticated @hasChannelRolesDashboardPermission(permission: MANAGE_ROLES) + rolesRemove(id: UUID!): Boolean! @isAuthenticated @hasChannelRolesDashboardPermission(permission: MANAGE_ROLES) } type Role { - id: ID! + id: UUID! channelId: String! name: String! type: RoleTypeEnum! diff --git a/frontend/dashboard/src/api/roles.ts b/frontend/dashboard/src/api/roles.ts index 6ffda2c3d..a82f71910 100644 --- a/frontend/dashboard/src/api/roles.ts +++ b/frontend/dashboard/src/api/roles.ts @@ -37,7 +37,7 @@ export const useRoles = createGlobalState(() => { const useRolesDeleteMutation = () => useMutation( graphql(` - mutation DeleteRole($id: ID!) { + mutation DeleteRole($id: UUID!) { rolesRemove(id: $id) } `), @@ -55,7 +55,7 @@ export const useRoles = createGlobalState(() => { const useRolesUpdateMutation = () => useMutation( graphql(` - mutation RolesUpdate($id: ID!, $opts: RolesCreateOrUpdateOpts!) { + mutation RolesUpdate($id: UUID!, $opts: RolesCreateOrUpdateOpts!) { rolesUpdate(id: $id, opts: $opts) } `), diff --git a/libs/repositories/roles/model/model.go b/libs/repositories/roles/model/model.go index 6c655134d..38cf66d64 100644 --- a/libs/repositories/roles/model/model.go +++ b/libs/repositories/roles/model/model.go @@ -15,6 +15,8 @@ type Role struct { RequiredUsedChannelPoints int64 } +var RoleNil = Role{} + type ChannelRoleEnum string func (c ChannelRoleEnum) String() string { diff --git a/libs/repositories/roles/pgx/pgx.go b/libs/repositories/roles/pgx/pgx.go index 74e3fd37c..7b6445f5f 100644 --- a/libs/repositories/roles/pgx/pgx.go +++ b/libs/repositories/roles/pgx/pgx.go @@ -9,6 +9,7 @@ import ( "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" + "github.com/twirapp/twir/libs/repositories" "github.com/twirapp/twir/libs/repositories/roles" "github.com/twirapp/twir/libs/repositories/roles/model" ) @@ -36,6 +37,27 @@ type Pgx struct { getter *trmpgx.CtxGetter } +func (c *Pgx) GetByID(ctx context.Context, id uuid.UUID) (model.Role, error) { + query := ` +SELECT id, "channelId", name, type, permissions, required_messages, required_used_channel_points, required_watch_time +FROM channels_roles +WHERE id = $1 +` + + conn := c.getter.DefaultTrOrDB(ctx, c.pool) + rows, err := conn.Query(ctx, query, id) + if err != nil { + return model.RoleNil, fmt.Errorf("GetByID: failed to execute select query: %w", err) + } + + result, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[model.Role]) + if err != nil { + return model.RoleNil, fmt.Errorf("GetByID: failed to collect rows: %w", err) + } + + return result, nil +} + func (c *Pgx) GetManyByIDS(ctx context.Context, ids []uuid.UUID) ([]model.Role, error) { query := ` SELECT id, "channelId", name, type, permissions, required_messages, required_used_channel_points, required_watch_time @@ -77,3 +99,91 @@ WHERE "channelId" = $1 return result, nil } + +func (c *Pgx) Create(ctx context.Context, input roles.CreateInput) (model.Role, error) { + query := ` +INSERT INTO channels_roles("channelId", name, type, permissions, required_messages, required_used_channel_points, required_watch_time) +VALUES ($1, $2, $3, $4, $5, $6, $7) +RETURNING id, "channelId", name, type, permissions, required_messages, required_used_channel_points, required_watch_time +` + + conn := c.getter.DefaultTrOrDB(ctx, c.pool) + rows, err := conn.Query( + ctx, + query, + input.ChannelID, + input.Name, + input.Type, + input.Permissions, + input.RequiredMessages, + input.RequiredUsedChannelPoints, + input.RequiredWatchTime, + ) + if err != nil { + return model.RoleNil, fmt.Errorf("cannot create role: %w", err) + } + + result, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[model.Role]) + if err != nil { + return model.RoleNil, fmt.Errorf("cannot create role: failed to collect rows: %w", err) + } + + return result, nil +} + +func (c *Pgx) Update(ctx context.Context, id uuid.UUID, input roles.UpdateInput) ( + model.Role, + error, +) { + updateBuilder := sq. + Update("channels_roles"). + Where(squirrel.Eq{"id": id}). + Suffix(`RETURNING id, "channelId", name, type, permissions, required_messages, required_used_channel_points, required_watch_time`) + updateBuilder = repositories.SquirrelApplyPatch( + updateBuilder, + map[string]any{ + "name": input.Name, + "permissions": input.Permissions, + "required_messages": input.RequiredMessages, + "required_used_channel_points": input.RequiredUsedChannelPoints, + "required_watch_time": input.RequiredWatchTime, + }, + ) + + query, args, err := updateBuilder.ToSql() + if err != nil { + return model.RoleNil, fmt.Errorf("cannot update role: failed to build query: %w", err) + } + + conn := c.getter.DefaultTrOrDB(ctx, c.pool) + rows, err := conn.Query(ctx, query, args...) + if err != nil { + return model.RoleNil, fmt.Errorf("cannot update role: failed to execute query: %w", err) + } + + result, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[model.Role]) + if err != nil { + return model.RoleNil, fmt.Errorf("cannot update role: failed to collect rows: %w", err) + } + + return result, nil +} + +func (c *Pgx) Delete(ctx context.Context, id uuid.UUID) error { + query := ` +DELETE FROM channels_roles +WHERE id = $1 AND type = $2 +` + + conn := c.getter.DefaultTrOrDB(ctx, c.pool) + rows, err := conn.Exec(ctx, query, id, model.ChannelRoleTypeCustom) + if err != nil { + return fmt.Errorf("cannot delete role: %w", err) + } + + if rows.RowsAffected() != 1 { + return fmt.Errorf("cannot delete role: role not found") + } + + return nil +} diff --git a/libs/repositories/roles/roles.go b/libs/repositories/roles/roles.go index 8f7a175b5..7f1c4024d 100644 --- a/libs/repositories/roles/roles.go +++ b/libs/repositories/roles/roles.go @@ -10,4 +10,26 @@ import ( type Repository interface { GetManyByIDS(ctx context.Context, ids []uuid.UUID) ([]model.Role, error) GetManyByChannelID(ctx context.Context, channelID string) ([]model.Role, error) + Create(ctx context.Context, input CreateInput) (model.Role, error) + Update(ctx context.Context, id uuid.UUID, input UpdateInput) (model.Role, error) + Delete(ctx context.Context, id uuid.UUID) error + GetByID(ctx context.Context, id uuid.UUID) (model.Role, error) +} + +type CreateInput struct { + ChannelID string + Name string + Type model.ChannelRoleEnum + Permissions []string + RequiredWatchTime int64 + RequiredMessages int32 + RequiredUsedChannelPoints int64 +} + +type UpdateInput struct { + Name *string + Permissions []string + RequiredWatchTime *int64 + RequiredMessages *int32 + RequiredUsedChannelPoints *int64 } diff --git a/libs/repositories/roles_users/model/model.go b/libs/repositories/roles_users/model/model.go new file mode 100644 index 000000000..8e4af0269 --- /dev/null +++ b/libs/repositories/roles_users/model/model.go @@ -0,0 +1,13 @@ +package model + +import ( + "github.com/google/uuid" +) + +type RoleUser struct { + ID uuid.UUID + UserID string + RoleID uuid.UUID +} + +var RoleUserNil = RoleUser{} diff --git a/libs/repositories/roles_users/pgx/pgx.go b/libs/repositories/roles_users/pgx/pgx.go new file mode 100644 index 000000000..2bae48171 --- /dev/null +++ b/libs/repositories/roles_users/pgx/pgx.go @@ -0,0 +1,143 @@ +package pgx + +import ( + "context" + "fmt" + + "github.com/Masterminds/squirrel" + trmpgx "github.com/avito-tech/go-transaction-manager/drivers/pgxv5/v2" + "github.com/google/uuid" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" + "github.com/twirapp/twir/libs/repositories/roles_users" + "github.com/twirapp/twir/libs/repositories/roles_users/model" +) + +type Opts struct { + PgxPool *pgxpool.Pool +} + +func New(opts Opts) *Pgx { + return &Pgx{ + pool: opts.PgxPool, + getter: trmpgx.DefaultCtxGetter, + } +} + +func NewFx(pool *pgxpool.Pool) *Pgx { + return New(Opts{PgxPool: pool}) +} + +var _ roles_users.Repository = (*Pgx)(nil) +var sq = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar) + +type Pgx struct { + pool *pgxpool.Pool + getter *trmpgx.CtxGetter +} + +func (c *Pgx) GetManyByRoleID(ctx context.Context, roleID uuid.UUID) ([]model.RoleUser, error) { + query := ` +SELECT id, "userId", "roleId" +FROM channels_roles_users +WHERE "roleId" = $1 +` + + conn := c.getter.DefaultTrOrDB(ctx, c.pool) + rows, err := conn.Query(ctx, query, roleID) + if err != nil { + return nil, fmt.Errorf("failed to execute select query: %w", err) + } + + result, err := pgx.CollectRows(rows, pgx.RowToStructByName[model.RoleUser]) + if err != nil { + return nil, fmt.Errorf("failed to collect rows: %w", err) + } + + return result, nil +} + +func (c *Pgx) Create(ctx context.Context, input roles_users.CreateInput) (model.RoleUser, error) { + query := ` +INSERT INTO channels_roles_users("userId", "roleId") +VALUES ($1, $2) +RETURNING id, "userId", "roleId" +` + + conn := c.getter.DefaultTrOrDB(ctx, c.pool) + rows, err := conn.Query(ctx, query, input.UserID, input.RoleID) + if err != nil { + return model.RoleUserNil, fmt.Errorf("failed to execute insert query: %w", err) + } + + result, err := pgx.CollectExactlyOneRow(rows, pgx.RowToStructByName[model.RoleUser]) + if err != nil { + return model.RoleUserNil, fmt.Errorf("failed to collect rows: %w", err) + } + + return result, nil +} + +func (c *Pgx) Delete(ctx context.Context, id uuid.UUID) error { + query := ` +DELETE FROM channels_roles_users +WHERE id = $1 +` + + conn := c.getter.DefaultTrOrDB(ctx, c.pool) + rows, err := conn.Exec(ctx, query, id) + if err != nil { + return fmt.Errorf("failed to execute delete query: %w", err) + } + + if rows.RowsAffected() != 1 { + return fmt.Errorf("role not found") + } + + return nil +} + +func (c *Pgx) DeleteManyByRoleID(ctx context.Context, roleID uuid.UUID) error { + query := ` +DELETE FROM channels_roles_users +WHERE "roleId" = $1 +` + + conn := c.getter.DefaultTrOrDB(ctx, c.pool) + _, err := conn.Exec(ctx, query, roleID) + if err != nil { + return fmt.Errorf("failed to execute delete query: %w", err) + } + + return nil +} + +func (c *Pgx) CreateMany(ctx context.Context, inputs []roles_users.CreateInput) ( + []model.RoleUser, + error, +) { + insertBuilder := sq.Insert("channels_roles_users"). + Columns(`"userId"`, `"roleId"`). + Suffix(`RETURNING id, "userId", "roleId"`) + for _, input := range inputs { + insertBuilder = insertBuilder.Values(input.UserID, input.RoleID) + } + + query, args, err := insertBuilder.ToSql() + if err != nil { + return nil, fmt.Errorf("failed to build insert query: %w", err) + } + + conn := c.getter.DefaultTrOrDB(ctx, c.pool) + rows, err := conn.Query(ctx, query, args...) + if err != nil { + return nil, fmt.Errorf("failed to execute insert query: %w", err) + } + + result, err := pgx.CollectRows(rows, pgx.RowToStructByName[model.RoleUser]) + if err != nil { + return nil, fmt.Errorf("failed to collect rows: %w", err) + } + + return result, nil +} diff --git a/libs/repositories/roles_users/roles_users.go b/libs/repositories/roles_users/roles_users.go new file mode 100644 index 000000000..352ce413b --- /dev/null +++ b/libs/repositories/roles_users/roles_users.go @@ -0,0 +1,21 @@ +package roles_users + +import ( + "context" + + "github.com/google/uuid" + "github.com/twirapp/twir/libs/repositories/roles_users/model" +) + +type Repository interface { + GetManyByRoleID(ctx context.Context, roleID uuid.UUID) ([]model.RoleUser, error) + Create(ctx context.Context, input CreateInput) (model.RoleUser, error) + CreateMany(ctx context.Context, inputs []CreateInput) ([]model.RoleUser, error) + Delete(ctx context.Context, id uuid.UUID) error + DeleteManyByRoleID(ctx context.Context, roleID uuid.UUID) error +} + +type CreateInput struct { + UserID string + RoleID uuid.UUID +}