Skip to content

Commit

Permalink
Add an endpoint returning the number of followers and followees
Browse files Browse the repository at this point in the history
The endpoint is:

  /public-keys/{hex-public-key}

The response looks like this:

  {
    "followees": 0,
    "followers": 0
  }
  • Loading branch information
boreq committed Nov 29, 2023
1 parent 3ee0956 commit f9136eb
Show file tree
Hide file tree
Showing 7 changed files with 272 additions and 4 deletions.
1 change: 1 addition & 0 deletions cmd/event-service/di/inject_application.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ var applicationSet = wire.NewSet(
app.NewUpdateMetricsHandler,
app.NewAddPublicKeyToMonitorHandler,
app.NewGetEventHandler,
app.NewGetPublicKeyInfoHandler,
)
2 changes: 2 additions & 0 deletions cmd/event-service/di/wire_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 34 additions & 0 deletions service/adapters/sqlite/contact_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,3 +181,37 @@ func (r *ContactRepository) IsFolloweeOfMonitoredPublicKey(ctx context.Context,
return true, nil

}

func (r *ContactRepository) CountFollowers(ctx context.Context, publicKey domain.PublicKey) (int, error) {
row := r.tx.QueryRow(`
SELECT COUNT(*)
FROM contacts_followees CF
INNER JOIN public_keys PK ON PK.id=CF.followee_id
WHERE PK.public_key=$1`,
publicKey.Hex(),
)

var count int
if err := row.Scan(&count); err != nil {
return 0, errors.Wrap(err, "scan err")
}

return count, nil
}

func (r *ContactRepository) CountFollowees(ctx context.Context, publicKey domain.PublicKey) (int, error) {
row := r.tx.QueryRow(`
SELECT COUNT(*)
FROM contacts_followees CF
INNER JOIN public_keys PK ON PK.id=CF.follower_id
WHERE PK.public_key=$1`,
publicKey.Hex(),
)

var count int
if err := row.Scan(&count); err != nil {
return 0, errors.Wrap(err, "scan err")
}

return count, nil
}
119 changes: 119 additions & 0 deletions service/adapters/sqlite/contact_repository_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -324,3 +324,122 @@ func BenchmarkContactRepository_GetCurrentContactsEvent(b *testing.B) {
require.NoError(b, err)
}
}

func TestContactRepository_CountFolloweesReturnsNumberOfFollowees(t *testing.T) {
ctx := fixtures.TestContext(t)
adapters := NewTestAdapters(ctx, t)

pk1, sk1 := fixtures.SomeKeyPair()
event1 := fixtures.SomeEventWithAuthor(sk1)
followee11 := fixtures.SomePublicKey()
followee12 := fixtures.SomePublicKey()

pk2, sk2 := fixtures.SomeKeyPair()
event2 := fixtures.SomeEventWithAuthor(sk2)

err := adapters.TransactionProvider.Transact(ctx, func(ctx context.Context, adapters sqlite.TestAdapters) error {
n, err := adapters.ContactRepository.CountFollowees(ctx, pk1)
require.NoError(t, err)
require.Equal(t, 0, n)

n, err = adapters.ContactRepository.CountFollowees(ctx, pk2)
require.NoError(t, err)
require.Equal(t, 0, n)

return nil
})
require.NoError(t, err)

err = adapters.TransactionProvider.Transact(ctx, func(ctx context.Context, adapters sqlite.TestAdapters) error {
err := adapters.EventRepository.Save(ctx, event1)
require.NoError(t, err)

err = adapters.EventRepository.Save(ctx, event2)
require.NoError(t, err)

err = adapters.ContactRepository.SetContacts(ctx, event1, []domain.PublicKey{followee11, followee12})
require.NoError(t, err)

return nil
})
require.NoError(t, err)

err = adapters.TransactionProvider.Transact(ctx, func(ctx context.Context, adapters sqlite.TestAdapters) error {
n, err := adapters.ContactRepository.CountFollowees(ctx, pk1)
require.NoError(t, err)
require.Equal(t, 2, n)

n, err = adapters.ContactRepository.CountFollowees(ctx, pk2)
require.NoError(t, err)
require.Equal(t, 0, n)

return nil
})
require.NoError(t, err)
}

func TestContactRepository_CountFollowersReturnsNumberOfFollowers(t *testing.T) {
ctx := fixtures.TestContext(t)
adapters := NewTestAdapters(ctx, t)

_, sk1 := fixtures.SomeKeyPair()
event1 := fixtures.SomeEventWithAuthor(sk1)

_, sk2 := fixtures.SomeKeyPair()
event2 := fixtures.SomeEventWithAuthor(sk2)

followee1 := fixtures.SomePublicKey()
followee2 := fixtures.SomePublicKey()
followee3 := fixtures.SomePublicKey()

err := adapters.TransactionProvider.Transact(ctx, func(ctx context.Context, adapters sqlite.TestAdapters) error {
n, err := adapters.ContactRepository.CountFollowers(ctx, followee1)
require.NoError(t, err)
require.Equal(t, 0, n)

n, err = adapters.ContactRepository.CountFollowers(ctx, followee2)
require.NoError(t, err)
require.Equal(t, 0, n)

n, err = adapters.ContactRepository.CountFollowers(ctx, followee3)
require.NoError(t, err)
require.Equal(t, 0, n)

return nil
})
require.NoError(t, err)

err = adapters.TransactionProvider.Transact(ctx, func(ctx context.Context, adapters sqlite.TestAdapters) error {
err := adapters.EventRepository.Save(ctx, event1)
require.NoError(t, err)

err = adapters.EventRepository.Save(ctx, event2)
require.NoError(t, err)

err = adapters.ContactRepository.SetContacts(ctx, event1, []domain.PublicKey{followee1, followee2})
require.NoError(t, err)

err = adapters.ContactRepository.SetContacts(ctx, event2, []domain.PublicKey{followee2})
require.NoError(t, err)

return nil
})
require.NoError(t, err)

err = adapters.TransactionProvider.Transact(ctx, func(ctx context.Context, adapters sqlite.TestAdapters) error {
n, err := adapters.ContactRepository.CountFollowers(ctx, followee1)
require.NoError(t, err)
require.Equal(t, 1, n)

n, err = adapters.ContactRepository.CountFollowers(ctx, followee2)
require.NoError(t, err)
require.Equal(t, 2, n)

n, err = adapters.ContactRepository.CountFollowers(ctx, followee3)
require.NoError(t, err)
require.Equal(t, 0, n)

return nil
})
require.NoError(t, err)
}
3 changes: 3 additions & 0 deletions service/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ type ContactRepository interface {
SetContacts(ctx context.Context, event domain.Event, contacts []domain.PublicKey) error
GetFollowees(ctx context.Context, publicKey domain.PublicKey) ([]domain.PublicKey, error)
IsFolloweeOfMonitoredPublicKey(ctx context.Context, publicKey domain.PublicKey) (bool, error)
CountFollowers(ctx context.Context, publicKey domain.PublicKey) (int, error)
CountFollowees(ctx context.Context, publicKey domain.PublicKey) (int, error)
}

type PublicKeysToMonitorRepository interface {
Expand All @@ -73,6 +75,7 @@ type Application struct {
UpdateMetrics *UpdateMetricsHandler
AddPublicKeyToMonitor *AddPublicKeyToMonitorHandler
GetEvent *GetEventHandler
GetPublicKeyInfo *GetPublicKeyInfoHandler
}

type ReceivedEvent struct {
Expand Down
78 changes: 78 additions & 0 deletions service/app/handler_get_public_key_info.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package app

import (
"context"

"github.com/boreq/errors"
"github.com/planetary-social/nos-event-service/internal/logging"
"github.com/planetary-social/nos-event-service/service/domain"
)

type GetPublicKeyInfo struct {
publicKey domain.PublicKey
}

func NewGetPublicKeyInfo(publicKey domain.PublicKey) GetPublicKeyInfo {
return GetPublicKeyInfo{publicKey: publicKey}
}

type PublicKeyInfo struct {
numberOfFollowees int
numberOfFollowers int
}

func NewPublicKeyInfo(numberOfFollowees int, numberOfFollowers int) PublicKeyInfo {
return PublicKeyInfo{numberOfFollowees: numberOfFollowees, numberOfFollowers: numberOfFollowers}
}

func (p PublicKeyInfo) NumberOfFollowees() int {
return p.numberOfFollowees
}

func (p PublicKeyInfo) NumberOfFollowers() int {
return p.numberOfFollowers
}

type GetPublicKeyInfoHandler struct {
transactionProvider TransactionProvider
logger logging.Logger
metrics Metrics
}

func NewGetPublicKeyInfoHandler(
transactionProvider TransactionProvider,
logger logging.Logger,
metrics Metrics,
) *GetPublicKeyInfoHandler {
return &GetPublicKeyInfoHandler{
transactionProvider: transactionProvider,
logger: logger.New("getPublicKeyInfoHandler"),
metrics: metrics,
}
}

func (h *GetPublicKeyInfoHandler) Handle(ctx context.Context, cmd GetPublicKeyInfo) (publicKeyInfo PublicKeyInfo, err error) {
defer h.metrics.StartApplicationCall("getPublicKeyInfo").End(&err)

var followeesCount, followersCount int

if err := h.transactionProvider.Transact(ctx, func(ctx context.Context, adapters Adapters) error {
tmp, err := adapters.Contacts.CountFollowees(ctx, cmd.publicKey)
if err != nil {
return errors.Wrap(err, "error counting followees")
}
followeesCount = tmp

tmp, err = adapters.Contacts.CountFollowers(ctx, cmd.publicKey)
if err != nil {
return errors.Wrap(err, "error counting followers")
}
followersCount = tmp

return nil
}); err != nil {
return PublicKeyInfo{}, errors.Wrap(err, "transaction error")
}

return NewPublicKeyInfo(followeesCount, followersCount), nil
}
39 changes: 35 additions & 4 deletions service/ports/http/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ func (s *Server) createMux() http.Handler {
r := mux.NewRouter()
r.Handle("/metrics", promhttp.HandlerFor(s.prometheus.Registry(), promhttp.HandlerOpts{}))
r.HandleFunc("/events/{id}", rest.Wrap(s.serveEvents))
r.HandleFunc("/public-keys/{hex}", rest.Wrap(s.servePublicKey))
r.HandleFunc("/", s.serveWs)
return r
}
Expand Down Expand Up @@ -116,18 +117,48 @@ func (s *Server) serveEvents(r *http.Request) rest.RestResponse {
return rest.ErrInternalServerError
}

return rest.NewResponse(newGetEventTransport(event))
return rest.NewResponse(newGetEventResponse(event))
default:
return rest.ErrMethodNotAllowed
}
}

type getEventTransport struct {
type getEventResponse struct {
Event json.RawMessage `json:"event"`
}

func newGetEventTransport(event domain.Event) getEventTransport {
return getEventTransport{Event: event.Raw()}
func newGetEventResponse(event domain.Event) getEventResponse {
return getEventResponse{Event: event.Raw()}
}

func (s *Server) servePublicKey(r *http.Request) rest.RestResponse {
vars := mux.Vars(r)
hexPublicKeyString := vars["hex"]

publicKey, err := domain.NewPublicKeyFromHex(hexPublicKeyString)
if err != nil {
return rest.ErrBadRequest.WithMessage("invalid hex public key")
}

publicKeyInfo, err := s.app.GetPublicKeyInfo.Handle(r.Context(), app.NewGetPublicKeyInfo(publicKey))
if err != nil {
s.logger.Error().WithError(err).Message("error getting public key info")
return rest.ErrInternalServerError
}

return rest.NewResponse(newGetPublicKeyResponse(publicKeyInfo))
}

type getPublicKeyResponse struct {
Followers int `json:"followers"`
Followees int `json:"followees"`
}

func newGetPublicKeyResponse(info app.PublicKeyInfo) *getPublicKeyResponse {
return &getPublicKeyResponse{
Followers: info.NumberOfFollowers(),
Followees: info.NumberOfFollowees(),
}
}

func (s *Server) handleConnection(ctx context.Context, conn *websocket.Conn) error {
Expand Down

0 comments on commit f9136eb

Please sign in to comment.