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..8a8f2f38d --- /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.String(), + 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/roles.resolver.go b/apps/api-gql/internal/delivery/gql/resolvers/roles.resolver.go index 5044feac7..296c982b2 100644 --- a/apps/api-gql/internal/delivery/gql/resolvers/roles.resolver.go +++ b/apps/api-gql/internal/delivery/gql/resolvers/roles.resolver.go @@ -7,13 +7,9 @@ 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" @@ -21,7 +17,6 @@ import ( "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" - "gorm.io/gorm" ) // RolesCreate is the resolver for the rolesCreate field. @@ -51,7 +46,7 @@ func (r *mutationResolver) RolesCreate( } } - r.rolesWithUsersService.Create( + err = r.rolesWithUsersService.Create( ctx, roles_with_roles_users.CreateInput{ Role: roles.CreateInput{ ChannelID: dashboardId, @@ -66,51 +61,10 @@ func (r *mutationResolver) RolesCreate( Users: users, }, ) - - permissions := make([]string, 0, len(opts.Permissions)) - for _, permission := range opts.Permissions { - permissions = append(permissions, 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 { + if err != nil { return false, err } - 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, - }, - ) - return true, nil } @@ -130,88 +84,43 @@ func (r *mutationResolver) RolesUpdate( 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") + parsedID, err := uuid.Parse(id) + if err != nil { + return false, fmt.Errorf("failed to parse role id: %w", 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) + permissions := make([]string, len(opts.Permissions)) + for i, permission := range opts.Permissions { + permissions[i] = permission.String() } - var entityCopy model.ChannelRole - if err := utils.DeepCopy(entity, &entityCopy); 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, + } } - 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()) - } - 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: parsedID, + 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 } @@ -227,38 +136,22 @@ 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") + parsedID, err := uuid.Parse(id) + if err != nil { + return false, fmt.Errorf("failed to parse role id: %w", err) } - 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: parsedID, + }, + ); 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 } @@ -269,49 +162,17 @@ 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. @@ -319,13 +180,18 @@ 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) + if obj == nil { + return nil, nil + } + + parsedID, err := uuid.Parse(obj.ID) + if err != nil { + return nil, fmt.Errorf("failed to parse role id: %w", err) + } + + users, err := r.rolesUsersService.GetManyByRoleID(ctx, parsedID) + if err != nil { + return nil, err } ids := make([]string, 0, len(users)) diff --git a/apps/api-gql/internal/services/roles/roles.go b/apps/api-gql/internal/services/roles/roles.go index a2191e718..353e09b53 100644 --- a/apps/api-gql/internal/services/roles/roles.go +++ b/apps/api-gql/internal/services/roles/roles.go @@ -39,6 +39,8 @@ type Service struct { logger logger.Logger } +var maxRoles = 20 + func (c *Service) modelToEntity(m model.Role) entity.ChannelRole { return entity.ChannelRole{ ID: m.ID, @@ -80,6 +82,15 @@ 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(gqlmodel.AllRoleTypeEnum, a.Type) + // + // return typeIdx - lo.IndexOf(gqlmodel.AllRoleTypeEnum, b.Type) + // }, + // ) + return entities, nil } @@ -96,6 +107,15 @@ type CreateInput struct { } 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, @@ -131,7 +151,6 @@ type UpdateInput struct { ActorID string Name *string - Type *entity.ChannelRoleEnum Permissions []string RequiredWatchTime *int64 RequiredMessages *int32 @@ -153,18 +172,12 @@ func (c *Service) Update(ctx context.Context, id uuid.UUID, input UpdateInput) ( updateInput := roles.UpdateInput{ Name: input.Name, - Type: nil, Permissions: input.Permissions, RequiredWatchTime: input.RequiredWatchTime, RequiredMessages: input.RequiredMessages, RequiredUsedChannelPoints: input.RequiredUsedChannelPoints, } - if input.Type != nil { - newType := model.ChannelRoleEnum(input.Type.String()) - updateInput.Type = &newType - } - newRole, err := c.rolesRepository.Update(ctx, id, updateInput) if err != nil { return entity.ChannelRole{}, err @@ -224,3 +237,12 @@ func (c *Service) Delete(ctx context.Context, input DeleteInput) error { 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 index 50a07e088..722900a8e 100644 --- a/apps/api-gql/internal/services/roles_users/roles_users.go +++ b/apps/api-gql/internal/services/roles_users/roles_users.go @@ -58,6 +58,10 @@ 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{ @@ -78,3 +82,28 @@ func (c *Service) CreateMany(ctx context.Context, inputs []CreateInput) ( 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 index 401c10daa..009586bd4 100644 --- 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 @@ -5,11 +5,8 @@ import ( "fmt" "github.com/avito-tech/go-transaction-manager/trm/v2" - "github.com/samber/lo" + "github.com/google/uuid" "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/apps/api-gql/internal/services/roles" "github.com/twirapp/twir/apps/api-gql/internal/services/roles_users" @@ -51,8 +48,6 @@ type CreateInputUser struct { } func (c *Service) Create(ctx context.Context, input CreateInput) error { - var newRole entity.ChannelRole - err := c.trmManager.Do( ctx, func(txCtx context.Context) error { role, err := c.rolesService.Create(txCtx, input.Role) @@ -60,8 +55,6 @@ func (c *Service) Create(ctx context.Context, input CreateInput) error { return err } - newRole = role - usersInputs := make([]roles_users.CreateInput, 0, len(input.Users)) for _, user := range input.Users { usersInputs = append( @@ -72,7 +65,7 @@ func (c *Service) Create(ctx context.Context, input CreateInput) error { ) } - _, err = c.rolesUsersService.CreateMany(ctx, usersInputs) + _, err = c.rolesUsersService.CreateMany(txCtx, usersInputs) if err != nil { return err } @@ -85,17 +78,77 @@ func (c *Service) Create(ctx context.Context, input CreateInput) error { return fmt.Errorf("failed to create role with users: %w", err) } - c.logger.Audit( - "Role create", - audit.Fields{ - NewValue: newRole, - ActorID: &input.Role.ActorID, - ChannelID: &input.Role.ChannelID, - System: mappers.AuditSystemToTableName(gqlmodel.AuditLogSystemChannelRoles), - OperationType: audit.OperationCreate, - ObjectID: lo.ToPtr(newRole.ID.String()), + 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/libs/repositories/roles/pgx/pgx.go b/libs/repositories/roles/pgx/pgx.go index 567151932..7b6445f5f 100644 --- a/libs/repositories/roles/pgx/pgx.go +++ b/libs/repositories/roles/pgx/pgx.go @@ -143,7 +143,6 @@ func (c *Pgx) Update(ctx context.Context, id uuid.UUID, input roles.UpdateInput) updateBuilder, map[string]any{ "name": input.Name, - "type": input.Type, "permissions": input.Permissions, "required_messages": input.RequiredMessages, "required_used_channel_points": input.RequiredUsedChannelPoints, diff --git a/libs/repositories/roles/roles.go b/libs/repositories/roles/roles.go index 023fb09b7..7f1c4024d 100644 --- a/libs/repositories/roles/roles.go +++ b/libs/repositories/roles/roles.go @@ -28,7 +28,6 @@ type CreateInput struct { type UpdateInput struct { Name *string - Type *model.ChannelRoleEnum Permissions []string RequiredWatchTime *int64 RequiredMessages *int32 diff --git a/libs/repositories/roles_users/pgx/pgx.go b/libs/repositories/roles_users/pgx/pgx.go index 45a0e532a..2bae48171 100644 --- a/libs/repositories/roles_users/pgx/pgx.go +++ b/libs/repositories/roles_users/pgx/pgx.go @@ -104,15 +104,11 @@ WHERE "roleId" = $1 ` conn := c.getter.DefaultTrOrDB(ctx, c.pool) - rows, err := conn.Exec(ctx, query, roleID) + _, err := conn.Exec(ctx, query, roleID) if err != nil { return fmt.Errorf("failed to execute delete query: %w", err) } - if rows.RowsAffected() == 0 { - return fmt.Errorf("role not found") - } - return nil }