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

Backend for Portfolio Groups #83

Merged
merged 2 commits into from
Dec 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ bazel run //scripts:run_server -- --use_azure_auth
bazel run //scripts:run_db

# In another terminal, run the PACTA server
bazel run //scripts:run_server
bazel run //scripts:run_server -- --with_public_endpoint=$USER

# In one last terminal, run the frontend
cd frontend
Expand Down
2 changes: 1 addition & 1 deletion cmd/runner/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ If you do want to actually run the full `runner` image on Azure, you can use:

```bash
# Run the backend, tell it to create tasks as real Azure Container Apps Jobs.
bazel run //scripts:run_apiserver -- --use_azure_runner
bazel run //scripts:run_server -- --use_azure_runner
```

### Creating a new docker image to run locally
Expand Down
2 changes: 2 additions & 0 deletions cmd/server/pactasrv/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ go_library(
"pacta_version.go",
"pactasrv.go",
"parallel.go",
"populate.go",
"portfolio.go",
"portfolio_group.go",
"upload.go",
"user.go",
],
Expand Down
17 changes: 17 additions & 0 deletions cmd/server/pactasrv/conv/oapi_to_pacta.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,20 @@ func HoldingsDateFromOAPI(hd *api.HoldingsDate) (*pacta.HoldingsDate, error) {
Time: hd.Time,
}, nil
}

func PortfolioGroupCreateFromOAPI(pg *api.PortfolioGroupCreate, ownerID pacta.OwnerID) (*pacta.PortfolioGroup, error) {
if pg == nil {
return nil, oapierr.Internal("portfolioGroupCreateFromOAPI: can't convert nil pointer")
}
if pg.Name == "" {
return nil, oapierr.BadRequest("name must not be empty")
}
if ownerID == "" {
return nil, oapierr.Internal("portfolioGroupCreateFromOAPI: ownerID must not be empty")
}
return &pacta.PortfolioGroup{
Name: pg.Name,
Description: pg.Description,
Owner: &pacta.Owner{ID: ownerID},
}, nil
}
40 changes: 40 additions & 0 deletions cmd/server/pactasrv/conv/pacta_to_oapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,17 @@ func PortfolioToOAPI(p *pacta.Portfolio) (*api.Portfolio, error) {
if err != nil {
return nil, oapierr.Internal("portfolioToOAPI: holdingsDateToOAPI failed", zap.Error(err))
}
memberOfs := []api.PortfolioGroupMembershipPortfolioGroup{}
for _, m := range p.MemberOf {
pg, err := PortfolioGroupToOAPI(m.PortfolioGroup)
if err != nil {
return nil, oapierr.Internal("portfolioToOAPI: portfolioGroupToOAPI failed", zap.Error(err))
}
memberOfs = append(memberOfs, api.PortfolioGroupMembershipPortfolioGroup{
CreatedAt: m.CreatedAt,
PortfolioGroup: *pg,
})
}
return &api.Portfolio{
Id: string(p.ID),
Name: p.Name,
Expand All @@ -150,5 +161,34 @@ func PortfolioToOAPI(p *pacta.Portfolio) (*api.Portfolio, error) {
CreatedAt: p.CreatedAt,
NumberOfRows: p.NumberOfRows,
AdminDebugEnabled: p.AdminDebugEnabled,
Groups: &memberOfs,
}, nil
}

func PortfolioGroupToOAPI(pg *pacta.PortfolioGroup) (*api.PortfolioGroup, error) {
if pg == nil {
return nil, oapierr.Internal("portfolioGroupToOAPI: can't convert nil pointer")
}
members := []api.PortfolioGroupMembershipPortfolio{}
for _, m := range pg.Members {
portfolio, err := PortfolioToOAPI(m.Portfolio)
if err != nil {
return nil, oapierr.Internal("portfolioGroupToOAPI: portfolioToOAPI failed", zap.Error(err))
}
members = append(members, api.PortfolioGroupMembershipPortfolio{
CreatedAt: m.CreatedAt,
Portfolio: *portfolio,
})
}
return &api.PortfolioGroup{
Id: string(pg.ID),
Name: pg.Name,
Description: pg.Description,
CreatedAt: pg.CreatedAt,
Members: &members,
}, nil
}

func PortfolioGroupsToOAPI(pgs []*pacta.PortfolioGroup) ([]*api.PortfolioGroup, error) {
return convAll(pgs, PortfolioGroupToOAPI)
}
10 changes: 10 additions & 0 deletions cmd/server/pactasrv/pactasrv.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ type DB interface {

Portfolio(tx db.Tx, id pacta.PortfolioID) (*pacta.Portfolio, error)
PortfoliosByOwner(tx db.Tx, owner pacta.OwnerID) ([]*pacta.Portfolio, error)
Portfolios(tx db.Tx, ids []pacta.PortfolioID) (map[pacta.PortfolioID]*pacta.Portfolio, error)
CreatePortfolio(tx db.Tx, i *pacta.Portfolio) (pacta.PortfolioID, error)
UpdatePortfolio(tx db.Tx, id pacta.PortfolioID, mutations ...db.UpdatePortfolioFn) error
DeletePortfolio(tx db.Tx, id pacta.PortfolioID) ([]pacta.BlobURI, error)
Expand All @@ -83,6 +84,15 @@ type DB interface {

GetOrCreateOwnerForUser(tx db.Tx, uID pacta.UserID) (pacta.OwnerID, error)

PortfolioGroup(tx db.Tx, id pacta.PortfolioGroupID) (*pacta.PortfolioGroup, error)
PortfolioGroupsByOwner(tx db.Tx, owner pacta.OwnerID) ([]*pacta.PortfolioGroup, error)
PortfolioGroups(tx db.Tx, ids []pacta.PortfolioGroupID) (map[pacta.PortfolioGroupID]*pacta.PortfolioGroup, error)
CreatePortfolioGroup(tx db.Tx, p *pacta.PortfolioGroup) (pacta.PortfolioGroupID, error)
UpdatePortfolioGroup(tx db.Tx, id pacta.PortfolioGroupID, mutations ...db.UpdatePortfolioGroupFn) error
DeletePortfolioGroup(tx db.Tx, id pacta.PortfolioGroupID) error
CreatePortfolioGroupMembership(tx db.Tx, pgID pacta.PortfolioGroupID, pID pacta.PortfolioID) error
DeletePortfolioGroupMembership(tx db.Tx, pgID pacta.PortfolioGroupID, pID pacta.PortfolioID) error

GetOrCreateUserByAuthn(tx db.Tx, mech pacta.AuthnMechanism, authnID, email, canonicalEmail string) (*pacta.User, error)
User(tx db.Tx, id pacta.UserID) (*pacta.User, error)
Users(tx db.Tx, ids []pacta.UserID) (map[pacta.UserID]*pacta.User, error)
Expand Down
109 changes: 109 additions & 0 deletions cmd/server/pactasrv/populate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package pactasrv

import (
"context"
"fmt"

"github.com/RMI/pacta/oapierr"
"github.com/RMI/pacta/pacta"
"go.uber.org/zap"
)

func (s *Server) populatePortfoliosInPortfolioGroups(
ctx context.Context,
ts []*pacta.PortfolioGroup,
) error {
getFn := func(pg *pacta.PortfolioGroup) ([]*pacta.Portfolio, error) {
result := []*pacta.Portfolio{}
for _, member := range pg.Members {
result = append(result, member.Portfolio)
}
return result, nil
}
lookupFn := func(ids []pacta.PortfolioID) (map[pacta.PortfolioID]*pacta.Portfolio, error) {
return s.DB.Portfolios(s.DB.NoTxn(ctx), ids)
}
getIDFn := func(p *pacta.Portfolio) pacta.PortfolioID {
return p.ID
}
if err := populateAll(ts, getFn, getIDFn, lookupFn); err != nil {
return oapierr.Internal("populating portfolios in portfolio groups failed", zap.Error(err))
}
return nil
}

func (s *Server) populatePortfolioGroupsInPortfolios(
ctx context.Context,
ts []*pacta.Portfolio,
) error {
getFn := func(pg *pacta.Portfolio) ([]*pacta.PortfolioGroup, error) {
result := []*pacta.PortfolioGroup{}
for _, member := range pg.MemberOf {
result = append(result, member.PortfolioGroup)
}
return result, nil
}
lookupFn := func(ids []pacta.PortfolioGroupID) (map[pacta.PortfolioGroupID]*pacta.PortfolioGroup, error) {
return s.DB.PortfolioGroups(s.DB.NoTxn(ctx), ids)
}
getIDFn := func(p *pacta.PortfolioGroup) pacta.PortfolioGroupID {
return p.ID
}
if err := populateAll(ts, getFn, getIDFn, lookupFn); err != nil {
return oapierr.Internal("populating portfolio groups in portfolios failed", zap.Error(err))
}
return nil
}

// This helper function populates the given targets in the given sources,
// to allow for generic population of nested data structures.
// sources = entities that you want to populate sub-entity references in.
// the sub-entities should be pointers to structs with an ID populated.
// getTargetsFn = function that takes a source and returns zero or more sub-entities to populate.
// getTargetIDFn = function that takes a sub-entity and returns its ID.
// lookupTargetsFn = function that takes a list of sub-entity IDs and returns a map of ID -> sub-entity.
func populateAll[Source any, TargetID ~string, Target any](
Copy link
Collaborator

Choose a reason for hiding this comment

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

This is a clever use of generics, but it's also hard to understand what it's doing, both here and where its called. I'd add a comment or similar.

Also, it looks like we're doing that "populate a pre-existing pointer" thing, which I begrudgingly accepted was useful in a GraphQL system for populating arbitrary recursive responses, but in a standard RESTful server, I can't see the motivation for such gymnastics when populating a static, non-recursive structure.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

GC on commenting this to make it clearer.

Not sure of the distinction you're drawing on that one: we have recursive structures in our data model here too: Portfolio.MemberOf is a PortfolioGroupMembership,which has a Portfolio field (same with PortfolioGroup). The value of these selsective population things is exactly that we can build a DB layer that is "dumb", and then populate the data we want selectively based on the context.

This is the ~the same pattern we used for Abound, ex, because the data model is actually really similar. Portfolio:Art, Gallery:PortfolioGroup, ex.

sources []*Source,
getTargetsFn func(*Source) ([]*Target, error),
getTargetIDFn func(*Target) TargetID,
lookupTargetsFn func([]TargetID) (map[TargetID]*Target, error),
) error {
allTargets := []*Target{}
for i, source := range sources {
targets, err := getTargetsFn(source)
if err != nil {
return fmt.Errorf("getting %d-th targets: %w", i, err)
}
allTargets = append(allTargets, targets...)
}

seen := map[TargetID]bool{}
uniqueIds := []TargetID{}
for _, target := range allTargets {
id := getTargetIDFn(target)
if _, ok := seen[id]; !ok {
uniqueIds = append(uniqueIds, id)
seen[id] = true
}
}

populatedTargets, err := lookupTargetsFn(uniqueIds)
if err != nil {
return fmt.Errorf("looking up populated: %w", err)
}
for i, source := range sources {
targets, err := getTargetsFn(source)
if err != nil {
return fmt.Errorf("re-getting %d-th targets: %w", i, err)
}
for _, target := range targets {
id := getTargetIDFn(target)
if populated, ok := populatedTargets[id]; ok {
*target = *populated
} else {
return fmt.Errorf("can't find populated target %s", id)
}
}
}
return nil
}
14 changes: 10 additions & 4 deletions cmd/server/pactasrv/portfolio.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,14 @@ func (s *Server) ListPortfolios(ctx context.Context, request api.ListPortfoliosR
if err != nil {
return nil, err
}
ius, err := s.DB.PortfoliosByOwner(s.DB.NoTxn(ctx), ownerID)
ps, err := s.DB.PortfoliosByOwner(s.DB.NoTxn(ctx), ownerID)
if err != nil {
return nil, oapierr.Internal("failed to query portfolios", zap.Error(err))
}
items, err := dereference(conv.PortfoliosToOAPI(ius))
if err := s.populatePortfolioGroupsInPortfolios(ctx, ps); err != nil {
return nil, err
}
items, err := dereference(conv.PortfoliosToOAPI(ps))
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -52,11 +55,14 @@ func (s *Server) DeletePortfolio(ctx context.Context, request api.DeletePortfoli
// Returns an portfolio by ID
// (GET /portfolio/{id})
func (s *Server) FindPortfolioById(ctx context.Context, request api.FindPortfolioByIdRequestObject) (api.FindPortfolioByIdResponseObject, error) {
iu, err := s.checkPortfolioAuthorization(ctx, pacta.PortfolioID(request.Id))
p, err := s.checkPortfolioAuthorization(ctx, pacta.PortfolioID(request.Id))
if err != nil {
return nil, err
}
converted, err := conv.PortfolioToOAPI(iu)
if err := s.populatePortfolioGroupsInPortfolios(ctx, []*pacta.Portfolio{p}); err != nil {
return nil, err
}
converted, err := conv.PortfolioToOAPI(p)
if err != nil {
return nil, err
}
Expand Down
Loading