package adapter import ( "context" "database/sql" "errors" "fmt" "gitea.dwysokinski.me/twhelp/dcbot/internal/adapter/internal/bunmodel" "gitea.dwysokinski.me/twhelp/dcbot/internal/domain" "github.com/google/uuid" "github.com/jackc/pgerrcode" "github.com/uptrace/bun" "github.com/uptrace/bun/driver/pgdriver" ) type GroupBunRepository struct { db *bun.DB } func NewGroupBunRepository(db *bun.DB) *GroupBunRepository { return &GroupBunRepository{db: db} } func (g *GroupBunRepository) Create(ctx context.Context, params domain.CreateGroupParams) (domain.GroupWithMonitors, error) { group := bunmodel.Group{ ServerID: params.ServerID(), VersionCode: params.VersionCode(), ChannelGains: params.ChannelGains(), ChannelLosses: params.ChannelLosses(), ServerKey: params.ServerKey(), Barbarians: params.Barbarians(), Internals: params.Internals(), LanguageTag: params.LanguageTag(), } if _, err := g.db.NewInsert(). Model(&group). Returning("*"). Exec(ctx); err != nil { return domain.GroupWithMonitors{}, fmt.Errorf("something went wrong while inserting group into the db: %w", err) } return group.ToDomain(), nil } func (g *GroupBunRepository) Update(ctx context.Context, id string, params domain.UpdateGroupParams) (domain.GroupWithMonitors, error) { if params.IsZero() { return domain.GroupWithMonitors{}, domain.ErrNothingToUpdate } if _, err := uuid.Parse(id); err != nil { return domain.GroupWithMonitors{}, domain.GroupNotFoundError{ID: id} } var group bunmodel.Group res, err := g.db.NewUpdate(). Model(&group). Returning("NULL"). Where("id = ?", id). Apply(updateGroupsParamsApplier{params}.apply). Exec(ctx) if err != nil && !errors.Is(err, sql.ErrNoRows) { return domain.GroupWithMonitors{}, fmt.Errorf("couldn't update group (id=%s): %w", id, err) } if affected, _ := res.RowsAffected(); affected == 0 { return domain.GroupWithMonitors{}, domain.GroupNotFoundError{ID: id} } return g.Get(ctx, id) } func (g *GroupBunRepository) AddMonitor(ctx context.Context, id string, tribeID int64) (domain.GroupWithMonitors, error) { parsedID, err := uuid.Parse(id) if err != nil { return domain.GroupWithMonitors{}, domain.GroupDoesNotExistError{ID: id} } monitor := bunmodel.Monitor{ GroupID: parsedID, TribeID: tribeID, } if _, err = g.db.NewInsert(). Model(&monitor). Returning("NULL"). Exec(ctx); err != nil { return domain.GroupWithMonitors{}, fmt.Errorf( "something went wrong while inserting monitor into the db: %w", mapAddMonitorError(err, id, tribeID), ) } return g.Get(ctx, id) } func (g *GroupBunRepository) DeleteMonitors(ctx context.Context, id string, monitorIDs ...string) (domain.GroupWithMonitors, error) { if _, err := uuid.Parse(id); err != nil { return domain.GroupWithMonitors{}, domain.GroupNotFoundError{ID: id} } for _, monitorID := range monitorIDs { if _, err := uuid.Parse(monitorID); err != nil { return domain.GroupWithMonitors{}, domain.MonitorNotFoundError{ID: monitorID} } } _, err := g.db.NewDelete(). Model(&bunmodel.Monitor{}). Returning("NULL"). Where("id IN (?)", bun.In(monitorIDs)). Where("group_id = ?", id). Exec(ctx) if err != nil && !errors.Is(err, sql.ErrNoRows) { return domain.GroupWithMonitors{}, fmt.Errorf("couldn't delete monitors: %w", err) } return g.Get(ctx, id) } func (g *GroupBunRepository) List(ctx context.Context, params domain.ListGroupsParams) ([]domain.GroupWithMonitors, error) { var groups []bunmodel.Group if err := g.db.NewSelect(). Model(&groups). Order("created_at ASC"). Relation("Monitors", func(q *bun.SelectQuery) *bun.SelectQuery { return q.Order("created_at ASC") }). Apply(listGroupsParamsApplier{params}.apply). Scan(ctx); err != nil && !errors.Is(err, sql.ErrNoRows) { return nil, fmt.Errorf("couldn't select groups from the db: %w", err) } result := make([]domain.GroupWithMonitors, 0, len(groups)) for _, group := range groups { result = append(result, group.ToDomain()) } return result, nil } func (g *GroupBunRepository) Get(ctx context.Context, id string) (domain.GroupWithMonitors, error) { group, err := g.get(ctx, id, true) if err != nil { return domain.GroupWithMonitors{}, err } return group.ToDomain(), nil } func (g *GroupBunRepository) get(ctx context.Context, id string, withMonitors bool) (bunmodel.Group, error) { if _, err := uuid.Parse(id); err != nil { return bunmodel.Group{}, domain.GroupNotFoundError{ID: id} } var group bunmodel.Group q := g.db.NewSelect(). Model(&group). Where("id = ?", id) if withMonitors { q = q.Relation("Monitors", func(q *bun.SelectQuery) *bun.SelectQuery { return q.Order("created_at ASC") }) } if err := q.Scan(ctx); err != nil { if errors.Is(err, sql.ErrNoRows) { return bunmodel.Group{}, domain.GroupNotFoundError{ID: id} } return bunmodel.Group{}, fmt.Errorf("couldn't select group (id=%s) from the db: %w", id, err) } return group, nil } func (g *GroupBunRepository) Delete(ctx context.Context, id string) error { if _, err := uuid.Parse(id); err != nil { return domain.GroupNotFoundError{ID: id} } res, err := g.db.NewDelete(). Model(&bunmodel.Group{}). Returning("NULL"). Where("id = ?", id). Exec(ctx) if err != nil && !errors.Is(err, sql.ErrNoRows) { return fmt.Errorf("couldn't delete group (id=%s): %w", id, err) } if affected, _ := res.RowsAffected(); affected == 0 { return domain.GroupNotFoundError{ID: id} } return nil } func (g *GroupBunRepository) DeleteMany(ctx context.Context, ids ...string) error { if len(ids) == 0 { return nil } _, err := g.db.NewDelete(). Model(&bunmodel.Group{}). Returning("NULL"). Where("id IN (?)", bun.In(ids)). Exec(ctx) if err != nil && !errors.Is(err, sql.ErrNoRows) { return fmt.Errorf("couldn't delete groups: %w", err) } return nil } type updateGroupsParamsApplier struct { params domain.UpdateGroupParams } func (u updateGroupsParamsApplier) apply(q *bun.UpdateQuery) *bun.UpdateQuery { if u.params.ChannelGains.Valid { if u.params.ChannelGains.String != "" { q = q.Set("channel_gains = ?", u.params.ChannelGains.String) } else { q = q.Set("channel_gains = NULL") } } if u.params.ChannelLosses.Valid { if u.params.ChannelLosses.String != "" { q = q.Set("channel_losses = ?", u.params.ChannelLosses.String) } else { q = q.Set("channel_losses = NULL") } } if u.params.LanguageTag.Valid { if u.params.LanguageTag.String != "" { q = q.Set("language_tag = ?", u.params.LanguageTag.String) } else { q = q.Set("language_tag = NULL") } } if u.params.Barbarians.Valid { q = q.Set("barbarians = ?", u.params.Barbarians.Bool) } if u.params.Internals.Valid { q = q.Set("internals = ?", u.params.Internals.Bool) } return q } type listGroupsParamsApplier struct { params domain.ListGroupsParams } func (l listGroupsParamsApplier) apply(q *bun.SelectQuery) *bun.SelectQuery { if len(l.params.ServerIDs) > 0 { q = q.Where("server_id IN (?)", bun.In(l.params.ServerIDs)) } if l.params.EnabledNotifications.Valid { if l.params.EnabledNotifications.Bool { q = q.WhereGroup(" AND ", func(query *bun.SelectQuery) *bun.SelectQuery { return q.WhereOr("channel_gains IS NOT NULL"). WhereOr("channel_losses IS NOT NULL") }) } else { q = q.Where("channel_gains IS NULL").Where("channel_losses IS NULL") } } if l.params.VersionCode.Valid { q = q.Where("version_code = ?", l.params.VersionCode.String) } if len(l.params.ServerKeys) > 0 { q = q.Where("server_key IN (?)", bun.In(l.params.ServerKeys)) } if !l.params.CreatedAtLTE.IsZero() { q = q.Where("created_at <= ?", l.params.CreatedAtLTE) } return q } func mapAddMonitorError(err error, groupID string, tribeID int64) error { var pgError pgdriver.Error if !errors.As(err, &pgError) { return err } code := pgError.Field('C') constraint := pgError.Field('n') switch { case code == pgerrcode.ForeignKeyViolation && constraint == "monitors_group_id_fkey": return domain.GroupDoesNotExistError{ ID: groupID, } case code == pgerrcode.UniqueViolation && constraint == "monitors_group_id_tribe_id_key": return domain.MonitorAlreadyExistsError{ TribeID: tribeID, GroupID: groupID, } default: return err } }