-
Notifications
You must be signed in to change notification settings - Fork 0
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
Frontend Build Out #41
Changes from all commits
0b80a20
bcfa77d
44021ba
c15a2f9
5ba55e0
7e3091b
9b5c911
63a29d5
42c03aa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -51,6 +51,22 @@ func PactaVersionCreateFromOAPI(p *api.PactaVersionCreate) (*pacta.PACTAVersion, | |
}, nil | ||
} | ||
|
||
func InitiativeInvitationFromOAPI(i *api.InitiativeInvitationCreate) (*pacta.InitiativeInvitation, error) { | ||
if i == nil { | ||
return nil, oapierr.Internal("initiativeInvitationToOAPI: can't convert nil pointer") | ||
} | ||
if !initiativeIDRegex.MatchString(i.Id) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I can't remember if I asked this before, but why are we validating IDs, aren't we the ones generating them? And if we aren't the ones generating them, let's chat. |
||
return nil, oapierr.BadRequest("id must contain only alphanumeric characters, underscores, and dashes") | ||
} | ||
if i.InitiativeId == "" { | ||
return nil, oapierr.BadRequest("initiative_id must not be empty") | ||
} | ||
return &pacta.InitiativeInvitation{ | ||
ID: pacta.InitiativeInvitationID(i.Id), | ||
Initiative: &pacta.Initiative{ID: pacta.InitiativeID(i.InitiativeId)}, | ||
}, nil | ||
} | ||
|
||
func ifNil[T any](t *T, fallback T) T { | ||
if t == nil { | ||
return fallback | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -54,6 +54,46 @@ func PactaVersionToOAPI(pv *pacta.PACTAVersion) (*api.PactaVersion, error) { | |
}, nil | ||
} | ||
|
||
func InitiativeInvitationToOAPI(i *pacta.InitiativeInvitation) (*api.InitiativeInvitation, error) { | ||
if i == nil { | ||
return nil, oapierr.Internal("initiativeToOAPI: can't convert nil pointer") | ||
} | ||
var usedAt *string | ||
if !i.UsedAt.IsZero() { | ||
usedAt = ptr(i.UsedAt.String()) | ||
} | ||
var usedBy *string | ||
if i.UsedBy != nil { | ||
usedBy = ptr(string(i.UsedBy.ID)) | ||
} | ||
Comment on lines
+66
to
+68
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you expect to be doing this a lot, you could make a generic |
||
return &api.InitiativeInvitation{ | ||
CreatedAt: i.CreatedAt, | ||
Id: string(i.ID), | ||
InitiativeId: string(i.Initiative.ID), | ||
UsedAt: usedAt, | ||
UsedByUserId: usedBy, | ||
}, nil | ||
} | ||
|
||
func InitiativeUserRelationshipToOAPI(i *pacta.InitiativeUserRelationship) (*api.InitiativeUserRelationship, error) { | ||
if i == nil { | ||
return nil, oapierr.Internal("initiativeUserRelationshipToOAPI: can't convert nil pointer") | ||
} | ||
if i.User == nil { | ||
return nil, oapierr.Internal("initiativeUserRelationshipToOAPI: can't convert nil user") | ||
} | ||
if i.Initiative == nil { | ||
return nil, oapierr.Internal("initiativeUserRelationshipToOAPI: can't convert nil initiative") | ||
} | ||
return &api.InitiativeUserRelationship{ | ||
UpdatedAt: i.UpdatedAt, | ||
InitiativeId: string(i.Initiative.ID), | ||
UserId: string(i.User.ID), | ||
Manager: i.Manager, | ||
Member: i.Member, | ||
}, nil | ||
} | ||
|
||
func ptr[T any](t T) *T { | ||
return &t | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,123 @@ | ||
package pactasrv | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"time" | ||
|
||
"github.com/RMI/pacta/cmd/server/pactasrv/conv" | ||
"github.com/RMI/pacta/db" | ||
"github.com/RMI/pacta/oapierr" | ||
api "github.com/RMI/pacta/openapi/pacta" | ||
"github.com/RMI/pacta/pacta" | ||
"go.uber.org/zap" | ||
) | ||
|
||
// Creates an initiative invitation | ||
// (POST /initiative-invitation) | ||
func (s *Server) CreateInitiativeInvitation(ctx context.Context, request api.CreateInitiativeInvitationRequestObject) (api.CreateInitiativeInvitationResponseObject, error) { | ||
// TODO(#12) Implement Authorization | ||
ii, err := conv.InitiativeInvitationFromOAPI(request.Body) | ||
if err != nil { | ||
return nil, err | ||
} | ||
id, err := s.DB.CreateInitiativeInvitation(s.DB.NoTxn(ctx), ii) | ||
if err != nil { | ||
return nil, oapierr.Internal("failed to create initiative invitation", zap.Error(err)) | ||
} | ||
if id != ii.ID { | ||
return nil, oapierr.Internal( | ||
"failed to create initiative invitation: ID mismatch", | ||
zap.String("requested_id", string(ii.ID)), | ||
zap.String("actual_id", string(id)), | ||
) | ||
} | ||
return api.CreateInitiativeInvitation204Response{}, nil | ||
} | ||
|
||
// Deletes an initiative invitation by id | ||
// (DELETE /initiative-invitation/{id}) | ||
func (s *Server) DeleteInitiativeInvitation(ctx context.Context, request api.DeleteInitiativeInvitationRequestObject) (api.DeleteInitiativeInvitationResponseObject, error) { | ||
// TODO(#12) Implement Authorization | ||
err := s.DB.DeleteInitiativeInvitation(s.DB.NoTxn(ctx), pacta.InitiativeInvitationID(request.Id)) | ||
if err != nil { | ||
return nil, oapierr.Internal("failed to delete initiative invitation", zap.Error(err)) | ||
} | ||
return api.DeleteInitiativeInvitation204Response{}, nil | ||
} | ||
|
||
// Returns the initiative invitation from this id, if it exists | ||
// (GET /initiative-invitation/{id}) | ||
func (s *Server) GetInitiativeInvitation(ctx context.Context, request api.GetInitiativeInvitationRequestObject) (api.GetInitiativeInvitationResponseObject, error) { | ||
// TODO(#12) Implement Authorization | ||
ii, err := s.DB.InitiativeInvitation(s.DB.NoTxn(ctx), pacta.InitiativeInvitationID(request.Id)) | ||
if err != nil { | ||
return nil, oapierr.Internal("failed to retrieve initiative invitation", zap.Error(err)) | ||
} | ||
result, err := conv.InitiativeInvitationToOAPI(ii) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return api.GetInitiativeInvitation200JSONResponse(*result), nil | ||
} | ||
|
||
// Claims this initiative invitation, if it exists | ||
// (POST /initiative-invitation/{id}:claim) | ||
func (s *Server) ClaimInitiativeInvitation(ctx context.Context, request api.ClaimInitiativeInvitationRequestObject) (api.ClaimInitiativeInvitationResponseObject, error) { | ||
userID, err := getUserID(ctx) | ||
if err != nil { | ||
return nil, err | ||
} | ||
var customErr api.ClaimInitiativeInvitationResponseObject | ||
err = s.DB.Transactional(ctx, func(tx db.Tx) error { | ||
ii, err := s.DB.InitiativeInvitation(tx, pacta.InitiativeInvitationID(request.Id)) | ||
if err != nil { | ||
return fmt.Errorf("looking up initiative invite: %w", err) | ||
} | ||
if !ii.UsedAt.IsZero() || ii.UsedBy != nil { | ||
if ii.UsedBy != nil && ii.UsedBy.ID == userID { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My usual grumbling about new paradigms I don't like: the whole "We use objects for everything and only populate the IDs" makes all of this logic one conditional comparision more than it otherwise would need to be. E.g. as compared to if ii.UsedByID == userID { ... } There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah I think in most cases you'll come to like this pattern. I've found it increasingly lovely for side projects. |
||
// We don't return an error if the same user tries to claim the same invitation twice, | ||
// which might happen by accident, but wouldn't impact the state of the initiative memberships. | ||
// We may want to log this, though. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd log at WARN |
||
return nil | ||
} else { | ||
customErr = api.ClaimInitiativeInvitation409Response{} | ||
return fmt.Errorf("initiative is already used: %+v", ii) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should be using oapierr.BadRequest (or similar) for this. For this case in particular, we likely want a custom error code so we can show a specific error to the end user. Then you'd just need to update the error logic below to: if errors.Is(err, oapierr.Error{}) { // or something like that, could also make oapierr.Is(err) or similar
return err
} else if err != nil {
return oapierr.Internal(...)
} You could even make a lil helper, like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done, but through different means, as an external var. |
||
} | ||
} | ||
err = s.DB.UpdateInitiativeInvitation(tx, ii.ID, | ||
db.SetInitiativeInvitationUsedAt(time.Now()), | ||
db.SetInitiativeInvitationUsedBy(userID)) | ||
if err != nil { | ||
return fmt.Errorf("updating initiative invite: %w", err) | ||
} | ||
err = s.DB.UpdateInitiativeUserRelationship(tx, ii.Initiative.ID, userID, | ||
db.SetInitiativeUserRelationshipMember(true)) | ||
if err != nil { | ||
return fmt.Errorf("creating initiative membership: %w", err) | ||
} | ||
return nil | ||
}) | ||
if err != nil { | ||
if customErr != nil { | ||
return customErr, nil | ||
} | ||
return nil, oapierr.Internal("failed to claim initiative invitation", zap.Error(err)) | ||
} | ||
return api.ClaimInitiativeInvitation204Response{}, nil | ||
} | ||
|
||
// Returns all initiative invitations associated with the initiative | ||
// (GET /initiative/{id}/invitations) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is getting the invitations associated with an initiative. I think this is the standard paradigm: resources that have a nested/ownership structure can be requested this way. The ID in this case is the initiative ID. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Following up on this, I'd just roll "listing invitations" into There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We won't want that for public access. I think keeping them distinct will make authz much easier. |
||
func (s *Server) ListInitiativeInvitations(ctx context.Context, request api.ListInitiativeInvitationsRequestObject) (api.ListInitiativeInvitationsResponseObject, error) { | ||
// TODO(#12) Implement Authorization | ||
iis, err := s.DB.InitiativeInvitationsByInitiative(s.DB.NoTxn(ctx), pacta.InitiativeID(request.Id)) | ||
if err != nil { | ||
return nil, oapierr.Internal("failed to list initiative invitations", zap.Error(err)) | ||
} | ||
result, err := dereference(mapAll(iis, conv.InitiativeInvitationToOAPI)) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return api.ListInitiativeInvitations200JSONResponse(result), nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
package pactasrv | ||
|
||
import ( | ||
"context" | ||
|
||
"github.com/RMI/pacta/cmd/server/pactasrv/conv" | ||
"github.com/RMI/pacta/db" | ||
"github.com/RMI/pacta/oapierr" | ||
api "github.com/RMI/pacta/openapi/pacta" | ||
"github.com/RMI/pacta/pacta" | ||
"go.uber.org/zap" | ||
) | ||
|
||
// Returns all initiative user relationships for the user that the user has access to view | ||
// (GET /initiative/{id}/user-relationships) | ||
func (s *Server) ListInitiativeUserRelationshipsByUser(ctx context.Context, request api.ListInitiativeUserRelationshipsByUserRequestObject) (api.ListInitiativeUserRelationshipsByUserResponseObject, error) { | ||
// TODO(#12) Implement Authorization | ||
iurs, err := s.DB.InitiativeUserRelationshipsByUser(s.DB.NoTxn(ctx), pacta.UserID(request.UserId)) | ||
if err != nil { | ||
return nil, oapierr.Internal("failed to retrieve initiative user relationships by user", zap.Error(err)) | ||
} | ||
result, err := dereference(mapAll(iurs, conv.InitiativeUserRelationshipToOAPI)) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return api.ListInitiativeUserRelationshipsByUser200JSONResponse(result), nil | ||
} | ||
|
||
// Returns all initiative user relationships for the initiative that the user has access to view | ||
// (GET /initiative/user-relationships/{id}) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This URL scheme is not intuitive to me, e.g. the difference between:
The benefit of RESTful stuff is that usually it's pretty obvious from the URL what the endpoint does, but I'm not getting that from these. How about something like:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ^^ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yep! Public access won't load relationships. I think this actually fits nicely into the "composite key exception" listed here |
||
func (s *Server) ListInitiativeUserRelationshipsByInitiative(ctx context.Context, request api.ListInitiativeUserRelationshipsByInitiativeRequestObject) (api.ListInitiativeUserRelationshipsByInitiativeResponseObject, error) { | ||
// TODO(#12) Implement Authorization | ||
iurs, err := s.DB.InitiativeUserRelationshipsByInitiatives(s.DB.NoTxn(ctx), pacta.InitiativeID(request.InitiativeId)) | ||
if err != nil { | ||
return nil, oapierr.Internal("failed to retrieve initiative user relationships by user", zap.Error(err)) | ||
} | ||
result, err := dereference(mapAll(iurs, conv.InitiativeUserRelationshipToOAPI)) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return api.ListInitiativeUserRelationshipsByInitiative200JSONResponse(result), nil | ||
} | ||
|
||
// Returns the initiative user relationship from this id, if it exists | ||
// (GET /initiative/{initiativeId}/user-relationship/{userId}) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This API surface seems really broad to me, what's the motivation for having three different endpoints that return a user's relationship to an initiative? Unless we expect the list of relationships to be exceptionally large (>1,000), I'd just let the client filter for the stuff they're looking for. More API surface means more stuff to secure, and more DB functions/tests/code to maintain overall. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ^^ There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, they will be large. There is a public initiative, which anyone will have the ability to join, and may be segmented by region (i.e. can't be handled as a standalone case). |
||
func (s *Server) GetInitiativeUserRelationship(ctx context.Context, request api.GetInitiativeUserRelationshipRequestObject) (api.GetInitiativeUserRelationshipResponseObject, error) { | ||
// TODO(#12) Implement Authorization | ||
iur, err := s.DB.InitiativeUserRelationship(s.DB.NoTxn(ctx), pacta.InitiativeID(request.InitiativeId), pacta.UserID(request.UserId)) | ||
if err != nil { | ||
return nil, oapierr.Internal("failed to retrieve initiative user relationship", zap.Error(err)) | ||
} | ||
result, err := conv.InitiativeUserRelationshipToOAPI(iur) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return api.GetInitiativeUserRelationship200JSONResponse(*result), nil | ||
} | ||
|
||
// Updates initiative user relationship properties | ||
// (PATCH /initiative/{initiativeId}/user-relationship/{userId}) | ||
func (s *Server) UpdateInitiativeUserRelationship(ctx context.Context, request api.UpdateInitiativeUserRelationshipRequestObject) (api.UpdateInitiativeUserRelationshipResponseObject, error) { | ||
// TODO(#12) Implement Authorization | ||
mutations := []db.UpdateInitiativeUserRelationshipFn{} | ||
if request.Body.Manager != nil { | ||
mutations = append(mutations, db.SetInitiativeUserRelationshipManager(*request.Body.Manager)) | ||
} | ||
if request.Body.Member != nil { | ||
mutations = append(mutations, db.SetInitiativeUserRelationshipMember(*request.Body.Member)) | ||
} | ||
err := s.DB.UpdateInitiativeUserRelationship(s.DB.NoTxn(ctx), pacta.InitiativeID(request.InitiativeId), pacta.UserID(request.UserId), mutations...) | ||
if err != nil { | ||
return nil, oapierr.Internal("failed to update initiative user relationship", zap.Error(err)) | ||
} | ||
return api.UpdateInitiativeUserRelationship204Response{}, nil | ||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -8,6 +8,7 @@ import ( | |||||
"github.com/RMI/pacta/db" | ||||||
"github.com/RMI/pacta/oapierr" | ||||||
"github.com/RMI/pacta/pacta" | ||||||
"github.com/RMI/pacta/session" | ||||||
"github.com/RMI/pacta/task" | ||||||
"go.uber.org/zap" | ||||||
) | ||||||
|
@@ -44,6 +45,7 @@ type DB interface { | |||||
InitiativeUserRelationshipsByUser(tx db.Tx, uid pacta.UserID) ([]*pacta.InitiativeUserRelationship, error) | ||||||
InitiativeUserRelationshipsByInitiatives(tx db.Tx, iid pacta.InitiativeID) ([]*pacta.InitiativeUserRelationship, error) | ||||||
PutInitiativeUserRelationship(tx db.Tx, iur *pacta.InitiativeUserRelationship) error | ||||||
UpdateInitiativeUserRelationship(tx db.Tx, iid pacta.InitiativeID, uid pacta.UserID, mutations ...db.UpdateInitiativeUserRelationshipFn) error | ||||||
|
||||||
Initiative(tx db.Tx, id pacta.InitiativeID) (*pacta.Initiative, error) | ||||||
Initiatives(tx db.Tx, ids []pacta.InitiativeID) (map[pacta.InitiativeID]*pacta.Initiative, error) | ||||||
|
@@ -65,7 +67,7 @@ type DB interface { | |||||
CreatePortfolioInitiativeMembership(tx db.Tx, pim *pacta.PortfolioInitiativeMembership) error | ||||||
DeletePortfolioInitiativeMembership(tx db.Tx, pid pacta.PortfolioID, iid pacta.InitiativeID) error | ||||||
|
||||||
GetOrCreateUserByAuthn(tx db.Tx, authnMechanism pacta.AuthnMechanism, authnID, enteredEmail, canonicalEmail string) (*pacta.User, 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) | ||||||
UpdateUser(tx db.Tx, id pacta.UserID, mutations ...db.UpdateUserFn) error | ||||||
|
@@ -82,10 +84,9 @@ type Blob interface { | |||||
} | ||||||
|
||||||
type Server struct { | ||||||
DB DB | ||||||
TaskRunner TaskRunner | ||||||
Logger *zap.Logger | ||||||
|
||||||
DB DB | ||||||
TaskRunner TaskRunner | ||||||
Logger *zap.Logger | ||||||
Blob Blob | ||||||
PorfolioUploadURI string | ||||||
} | ||||||
|
@@ -115,3 +116,11 @@ func dereference[T any](ts []*T, e error) ([]T, error) { | |||||
} | ||||||
return result, nil | ||||||
} | ||||||
|
||||||
func getUserID(ctx context.Context) (pacta.UserID, error) { | ||||||
userID, err := session.UserIDFromContext(ctx) | ||||||
if err != nil { | ||||||
return "", oapierr.Unauthorized("error getting authorization token", zap.Error(err)) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
} | ||||||
return pacta.UserID(userID), nil | ||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nothing needs to change here, but because auth in RMI-land is so different than AS-land, we really don't need a whole package and middleware for this, it could just be a server helper like
s.getUser
ors.getUserID
. It's no more or less code in the endpoints themselves, just the difference betweensession.UserID(ctx)
ands.getUserID(ctx)
.