Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(api): add emergency contacts database functions #1081

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
92 changes: 64 additions & 28 deletions database/redis/contact.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,22 @@ import (
// GetContact returns contact data by given id, if no value, return database.ErrNil error.
func (connector *DbConnector) GetContact(id string) (moira.ContactData, error) {
c := *connector.client
ctx := connector.context

var contact moira.ContactData

result := c.Get(connector.context, contactKey(id))
result := c.Get(ctx, contactKey(id))
if errors.Is(result.Err(), redis.Nil) {
return contact, database.ErrNil
}

contact, err := reply.Contact(result)
if err != nil {
return contact, err
return contact, fmt.Errorf("failed to deserialize contact '%s': %w", id, err)
}

contact.ID = id

return contact, nil
}

Expand All @@ -37,26 +41,30 @@ func (connector *DbConnector) GetContacts(contactIDs []string) ([]*moira.Contact
results := make([]*redis.StringCmd, 0, len(contactIDs))

c := *connector.client
ctx := connector.context

pipe := c.TxPipeline()
for _, id := range contactIDs {
result := pipe.Get(connector.context, contactKey(id))
result := pipe.Get(ctx, contactKey(id))
kissken marked this conversation as resolved.
Show resolved Hide resolved
results = append(results, result)
}
_, err := pipe.Exec(connector.context)

_, err := pipe.Exec(ctx)
if err != nil && !errors.Is(err, redis.Nil) {
return nil, err
return nil, fmt.Errorf("failed to get contacts by id: %w", err)
kissken marked this conversation as resolved.
Show resolved Hide resolved
}

contacts, err := reply.Contacts(results)
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to reply contacts: %w", err)
}

for i := range contacts {
if contacts[i] != nil {
contacts[i].ID = contactIDs[i]
}
}

return contacts, nil
}

Expand All @@ -79,6 +87,7 @@ func getContactsKeysOnRedisNode(ctx context.Context, client redis.UniversalClien
break
}
}

return keys, nil
}

Expand All @@ -91,6 +100,7 @@ func (connector *DbConnector) GetAllContacts() ([]*moira.ContactData, error) {
if err != nil {
return err
}

keys = append(keys, keysResult...)
return nil
})
Expand All @@ -102,81 +112,107 @@ func (connector *DbConnector) GetAllContacts() ([]*moira.ContactData, error) {
for _, key := range keys {
contactIDs = append(contactIDs, strings.TrimPrefix(key, contactKey("")))
}

return connector.GetContacts(contactIDs)
}

// SaveContact writes contact data and updates user contacts.
func (connector *DbConnector) SaveContact(contact *moira.ContactData) error {
existing, getContactErr := connector.GetContact(contact.ID)
if getContactErr != nil && !errors.Is(getContactErr, database.ErrNil) {
return getContactErr
return fmt.Errorf("failed to get contact '%s': %w", contact.ID, getContactErr)
}
contactString, err := json.Marshal(contact)

contactStr, err := json.Marshal(contact)
if err != nil {
return err
return fmt.Errorf("failed to marshal contact '%s': %w", contact.ID, err)
}

c := *connector.client
ctx := connector.context

pipe := c.TxPipeline()
pipe.Set(connector.context, contactKey(contact.ID), contactString, redis.KeepTTL)
pipe.Set(ctx, contactKey(contact.ID), contactStr, redis.KeepTTL)
if !errors.Is(getContactErr, database.ErrNil) && contact.User != existing.User {
pipe.SRem(connector.context, userContactsKey(existing.User), contact.ID)
pipe.SRem(ctx, userContactsKey(existing.User), contact.ID)
}

if !errors.Is(getContactErr, database.ErrNil) && contact.Team != existing.Team {
pipe.SRem(connector.context, teamContactsKey(existing.Team), contact.ID)
pipe.SRem(ctx, teamContactsKey(existing.Team), contact.ID)
}

if contact.User != "" {
pipe.SAdd(connector.context, userContactsKey(contact.User), contact.ID)
pipe.SAdd(ctx, userContactsKey(contact.User), contact.ID)
}

if contact.Team != "" {
pipe.SAdd(connector.context, teamContactsKey(contact.Team), contact.ID)
pipe.SAdd(ctx, teamContactsKey(contact.Team), contact.ID)
}
_, err = pipe.Exec(connector.context)

_, err = pipe.Exec(ctx)
if err != nil {
return fmt.Errorf("failed to EXEC: %s", err.Error())
return fmt.Errorf("failed to save contact '%s': %w", contact.ID, err)
}

return nil
}

// RemoveContact deletes contact data and contactID from user contacts.
func (connector *DbConnector) RemoveContact(contactID string) error {
existing, err := connector.GetContact(contactID)
if err != nil && !errors.Is(err, database.ErrNil) {
return err
return fmt.Errorf("failed to get contact '%s': %w", contactID, err)
}

emergencyContact, getEmergencyContactErr := connector.GetEmergencyContact(contactID)
if getEmergencyContactErr != nil && !errors.Is(getEmergencyContactErr, database.ErrNil) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Если есть эмёрдженси контакт -- мы удаляем и его? Может логика должна быть наоборот, как с подписками и контактами: сначала удалите все зависимости, а потом только удаляйте контакты

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Вопрос обсуждаемый и зависит от того, как это будет выглядеть в UI, если это будет галка внутри UI заполнения контакта, то логично было бы не просить пользователя отжимать галку, а просто удалять и то, и то. А если в UI будет отдельное окошко для этих контактов, то вариант с тем, чтобы пользователь сначала убирал эти контакты, а затем удалял основной имеет смысл быть. Но, честно, с учетом того, что связь 1-1, то удалять для меня выглядит более логичным вариантом

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Вынесу в общее обсуждение

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Просто в рамках логики апи это разные штуки. Это в рамках логики фронта надо запросы по очереди отправлять

return fmt.Errorf("failed to get emergency contact '%s': %w", contactID, err)
}

c := *connector.client
ctx := connector.context

pipe := c.TxPipeline()
pipe.Del(connector.context, contactKey(contactID))
pipe.SRem(connector.context, userContactsKey(existing.User), contactID)
pipe.SRem(connector.context, teamContactsKey(existing.Team), contactID)
_, err = pipe.Exec(connector.context)
pipe.Del(ctx, contactKey(contactID))
pipe.SRem(ctx, userContactsKey(existing.User), contactID)
pipe.SRem(ctx, teamContactsKey(existing.Team), contactID)

if !errors.Is(getEmergencyContactErr, database.ErrNil) {
addRemoveEmergencyContactToPipe(ctx, pipe, emergencyContact)
}

_, err = pipe.Exec(ctx)
if err != nil {
return fmt.Errorf("failed to EXEC: %s", err.Error())
return fmt.Errorf("failed to remove contact '%s': %w", contactID, err)
}

return nil
}

// GetUserContactIDs returns contacts ids by given login.
func (connector *DbConnector) GetUserContactIDs(login string) ([]string, error) {
c := *connector.client
ctx := connector.context

contacts, err := c.SMembers(connector.context, userContactsKey(login)).Result()
contactIDs, err := c.SMembers(ctx, userContactsKey(login)).Result()
if err != nil {
return nil, fmt.Errorf("failed to get contacts for user login %s: %s", login, err.Error())
return nil, fmt.Errorf("failed to get contact IDs for user login '%s': %w", login, err)
}
return contacts, nil

return contactIDs, nil
}

// GetTeamContactIDs returns contacts ids by given team.
func (connector *DbConnector) GetTeamContactIDs(login string) ([]string, error) {
c := *connector.client
contacts, err := c.SMembers(connector.context, teamContactsKey(login)).Result()
ctx := connector.context

contactIDs, err := c.SMembers(ctx, teamContactsKey(login)).Result()
if err != nil {
return nil, fmt.Errorf("failed to get contacts for team login %s: %s", login, err.Error())
return nil, fmt.Errorf("failed to get contact IDs for team login '%s': %w", login, err)
}
return contacts, nil

return contactIDs, nil
}

func contactKey(id string) string {
Expand Down
37 changes: 37 additions & 0 deletions database/redis/contact_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"testing"

"github.com/moira-alert/moira/database"
"github.com/moira-alert/moira/datatypes"

"github.com/moira-alert/moira"
logging "github.com/moira-alert/moira/logging/zerolog_adapter"
Expand Down Expand Up @@ -171,6 +172,9 @@ func TestContacts(t *testing.T) {
err := dataBase.SaveContact(contact2)
So(err, ShouldBeNil)

err = dataBase.SaveEmergencyContact(user2EmergencyContacts[0])
So(err, ShouldBeNil)

actual, err := dataBase.GetContact(contact2.ID)
So(err, ShouldBeNil)
So(actual, ShouldResemble, *contact2)
Expand All @@ -179,9 +183,17 @@ func TestContacts(t *testing.T) {
So(err, ShouldBeNil)
So(actual1, ShouldHaveLength, 1)

emergencyContact, err := dataBase.GetEmergencyContact(contact2.ID)
So(err, ShouldBeNil)
So(emergencyContact, ShouldResemble, user2EmergencyContacts[0])

err = dataBase.RemoveContact(contact2.ID)
So(err, ShouldBeNil)

emergencyContact, err = dataBase.GetEmergencyContact(contact2.ID)
So(err, ShouldResemble, database.ErrNil)
So(emergencyContact, ShouldResemble, datatypes.EmergencyContact{})

err = dataBase.SaveContact(contact1)
So(err, ShouldBeNil)

Expand Down Expand Up @@ -315,6 +327,9 @@ func TestContacts(t *testing.T) {
err := dataBase.SaveContact(contact2)
So(err, ShouldBeNil)

err = dataBase.SaveEmergencyContact(team2EmergencyContacts[0])
So(err, ShouldBeNil)

actual, err := dataBase.GetContact(contact2.ID)
So(err, ShouldBeNil)
So(actual, ShouldResemble, *contact2)
Expand All @@ -323,9 +338,17 @@ func TestContacts(t *testing.T) {
So(err, ShouldBeNil)
So(actual1, ShouldHaveLength, 1)

emergencyContact, err := dataBase.GetEmergencyContact(contact2.ID)
So(err, ShouldBeNil)
So(emergencyContact, ShouldResemble, team2EmergencyContacts[0])

err = dataBase.RemoveContact(contact2.ID)
So(err, ShouldBeNil)

emergencyContact, err = dataBase.GetEmergencyContact(contact2.ID)
So(err, ShouldResemble, database.ErrNil)
So(emergencyContact, ShouldResemble, datatypes.EmergencyContact{})

err = dataBase.SaveContact(contact1)
So(err, ShouldBeNil)

Expand Down Expand Up @@ -519,6 +542,13 @@ var user2Contacts = []*moira.ContactData{
},
}

var user2EmergencyContacts = []datatypes.EmergencyContact{
{
ContactID: "ContactID-000000000000003",
HeartbeatTypes: []datatypes.HeartbeatType{datatypes.HeartbeatNotifierOff},
},
}

var team1Contacts = []*moira.ContactData{
{
ID: "TeamContactID-000000000000001",
Expand Down Expand Up @@ -572,3 +602,10 @@ var team2Contacts = []*moira.ContactData{
Team: team2,
},
}

var team2EmergencyContacts = []datatypes.EmergencyContact{
{
ContactID: "TeamContactID-000000000000003",
HeartbeatTypes: []datatypes.HeartbeatType{datatypes.HeartbeatNotifierOff},
},
}
Loading
Loading