From 1807ff17c6fa9aee2c4e70b57514806fc276b584 Mon Sep 17 00:00:00 2001 From: Grady Ward Date: Sun, 10 Sep 2023 18:15:33 -0600 Subject: [PATCH] Working! --- cmd/server/main.go | 3 +- cmd/server/pactasrv/BUILD.bazel | 4 + cmd/server/pactasrv/conv_oapi_to_pacta.go | 58 ++++ cmd/server/pactasrv/conv_pacta_to_oapi.go | 45 +++ cmd/server/pactasrv/error.go | 163 +++++++++ cmd/server/pactasrv/initiative.go | 164 +++++++++ cmd/server/pactasrv/pacta_version.go | 144 +++++++- cmd/server/pactasrv/pactasrv.go | 31 +- cmd/server/pactasrv/user.go | 46 ++- frontend/components/standard/Content.vue | 2 +- frontend/components/standard/Footer.vue | 16 +- frontend/composables/useURLParams.ts | 56 ++++ frontend/layouts/default.vue | 2 +- frontend/openapi/generated/pacta/index.ts | 3 + .../generated/pacta/models/Initiative.ts | 67 ++++ .../pacta/models/InitiativeChanges.ts | 59 ++++ .../pacta/models/InitiativeCreate.ts | 63 ++++ .../pacta/models/PactaVersionChanges.ts | 4 - .../pacta/services/DefaultService.ts | 145 ++++++-- frontend/pages/admin/pacta-version/[id].vue | 110 ++++++ frontend/pages/admin/pacta-version/index.vue | 42 ++- frontend/pages/admin/pacta-version/new.vue | 2 +- openapi/pacta.yaml | 315 +++++++++++++++--- 23 files changed, 1447 insertions(+), 97 deletions(-) create mode 100644 cmd/server/pactasrv/conv_oapi_to_pacta.go create mode 100644 cmd/server/pactasrv/conv_pacta_to_oapi.go create mode 100644 cmd/server/pactasrv/error.go create mode 100644 cmd/server/pactasrv/initiative.go create mode 100644 frontend/composables/useURLParams.ts create mode 100644 frontend/openapi/generated/pacta/models/Initiative.ts create mode 100644 frontend/openapi/generated/pacta/models/InitiativeChanges.ts create mode 100644 frontend/openapi/generated/pacta/models/InitiativeCreate.ts create mode 100644 frontend/pages/admin/pacta-version/[id].vue diff --git a/cmd/server/main.go b/cmd/server/main.go index bb2248c..5f0f34a 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -169,7 +169,8 @@ func run(args []string) error { AllowCredentials: true, AllowedHeaders: []string{"Authorization", "Content-Type"}, // Enable Debugging for testing, consider disabling in production - Debug: true, + Debug: true, + AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}, }).Handler(r) } else { handler = r diff --git a/cmd/server/pactasrv/BUILD.bazel b/cmd/server/pactasrv/BUILD.bazel index 13c3ea2..bda5d99 100644 --- a/cmd/server/pactasrv/BUILD.bazel +++ b/cmd/server/pactasrv/BUILD.bazel @@ -3,6 +3,10 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library") go_library( name = "pactasrv", srcs = [ + "conv_oapi_to_pacta.go", + "conv_pacta_to_oapi.go", + "error.go", + "initiative.go", "pacta_version.go", "pactasrv.go", "user.go", diff --git a/cmd/server/pactasrv/conv_oapi_to_pacta.go b/cmd/server/pactasrv/conv_oapi_to_pacta.go new file mode 100644 index 0000000..eb25585 --- /dev/null +++ b/cmd/server/pactasrv/conv_oapi_to_pacta.go @@ -0,0 +1,58 @@ +package pactasrv + +import ( + "fmt" + "regexp" + + api "github.com/RMI/pacta/openapi/pacta" + "github.com/RMI/pacta/pacta" +) + +var initiativeIDRegex = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`) + +func initiativeCreateToPACTA(i *api.InitiativeCreate) (*pacta.Initiative, error) { + if i == nil { + return nil, errorBadRequest("InitiativeCreate", "cannot be nil") + } + if !initiativeIDRegex.MatchString(i.Id) { + return nil, errorBadRequest("id", "must contain only alphanumeric characters, underscores, and dashes") + } + lang, err := pacta.ParseLanguage(string(i.Language)) + if err != nil { + return nil, errorBadRequest("language", err.Error()) + } + var pv *pacta.PACTAVersion + if i.PactaVersion != nil { + pv = &pacta.PACTAVersion{ID: pacta.PACTAVersionID(*i.PactaVersion)} + } + return &pacta.Initiative{ + Affiliation: ifNil(i.Affiliation, ""), + ID: pacta.InitiativeID(i.Id), + InternalDescription: ifNil(i.InternalDescription, ""), + IsAcceptingNewMembers: ifNil(i.IsAcceptingNewMembers, false), + IsAcceptingNewPortfolios: ifNil(i.IsAcceptingNewPortfolios, false), + Language: lang, + Name: i.Name, + PACTAVersion: pv, + PublicDescription: ifNil(i.PublicDescription, ""), + RequiresInvitationToJoin: ifNil(i.RequiresInvitationToJoin, false), + }, nil +} + +func pactaVersionCreateToPACTA(p *api.PactaVersionCreate) (*pacta.PACTAVersion, error) { + if p == nil { + return nil, fmt.Errorf("pactaVersionCreateToPACTA: nil pointer") + } + return &pacta.PACTAVersion{ + Name: p.Name, + Digest: p.Digest, + Description: p.Description, + }, nil +} + +func ifNil[T any](t *T, or T) T { + if t == nil { + return or + } + return *t +} diff --git a/cmd/server/pactasrv/conv_pacta_to_oapi.go b/cmd/server/pactasrv/conv_pacta_to_oapi.go new file mode 100644 index 0000000..95fecd7 --- /dev/null +++ b/cmd/server/pactasrv/conv_pacta_to_oapi.go @@ -0,0 +1,45 @@ +package pactasrv + +import ( + "fmt" + + api "github.com/RMI/pacta/openapi/pacta" + "github.com/RMI/pacta/pacta" +) + +func initiativeToOAPI(i *pacta.Initiative) (*api.Initiative, error) { + if i == nil { + return nil, errorInternal(fmt.Errorf("initiativeToOAPI: can't convert nil pointer")) + } + return &api.Initiative{ + Affiliation: i.Affiliation, + CreatedAt: i.CreatedAt, + Id: string(i.ID), + InternalDescription: i.InternalDescription, + IsAcceptingNewMembers: i.IsAcceptingNewMembers, + IsAcceptingNewPortfolios: i.IsAcceptingNewPortfolios, + Language: api.InitiativeLanguage(i.Language), + Name: i.Name, + PactaVersion: ptr(string(i.PACTAVersion.ID)), + PublicDescription: i.PublicDescription, + RequiresInvitationToJoin: i.RequiresInvitationToJoin, + }, nil +} + +func pactaVersionToOAPI(pv *pacta.PACTAVersion) (*api.PactaVersion, error) { + if pv == nil { + return nil, errorInternal(fmt.Errorf("pactaVersionToOAPI: can't convert nil pointer")) + } + return &api.PactaVersion{ + Id: string(pv.ID), + Name: pv.Name, + IsDefault: pv.IsDefault, + Digest: pv.Digest, + Description: pv.Description, + CreatedAt: pv.CreatedAt, + }, nil +} + +func ptr[T any](t T) *T { + return &t +} diff --git a/cmd/server/pactasrv/error.go b/cmd/server/pactasrv/error.go new file mode 100644 index 0000000..f55e735 --- /dev/null +++ b/cmd/server/pactasrv/error.go @@ -0,0 +1,163 @@ +package pactasrv + +import ( + "fmt" + + api "github.com/RMI/pacta/openapi/pacta" +) + +type errBadRequest struct { + Field string + Message string +} + +func (e *errBadRequest) Code() int32 { + return 400 +} + +func (e *errBadRequest) Error() string { + return fmt.Sprintf("bad request: field %q: %s", e.Field, e.Message) +} + +func (e *errBadRequest) Is(target error) bool { + _, ok := target.(*errBadRequest) + return ok +} + +func errorBadRequest(field string, message string) error { + return &errBadRequest{Field: field, Message: message} +} + +type errUnauthorized struct { + Action string + Resource string +} + +func (e *errUnauthorized) Code() int32 { + return 401 +} + +func (e *errUnauthorized) Error() string { + return fmt.Sprintf("unauthorized to %s %s", e.Action, e.Resource) +} + +func (e *errUnauthorized) Is(target error) bool { + _, ok := target.(*errUnauthorized) + return ok +} + +func errorUnauthorized(action string, resource string) error { + return &errUnauthorized{Action: action, Resource: resource} +} + +type errForbidden struct { + Action string + Resource string +} + +func (e *errForbidden) Code() int32 { + return 403 +} + +func (e *errForbidden) Error() string { + return fmt.Sprintf("user is not allowed to %s %s", e.Action, e.Resource) +} + +func (e *errForbidden) Is(target error) bool { + _, ok := target.(*errForbidden) + return ok +} + +func errorForbidden(action string, resource string) error { + return &errForbidden{Action: action, Resource: resource} +} + +type errNotFound struct { + What string + With string +} + +func (e *errNotFound) Code() int32 { + return 404 +} + +func (e *errNotFound) Error() string { + return fmt.Sprintf("not found: %s with %s", e.What, e.With) +} + +func (e *errNotFound) Is(target error) bool { + _, ok := target.(*errNotFound) + return ok +} + +func errorNotFound(what string, with string) error { + return &errNotFound{What: what, With: with} +} + +type errInternal struct { + What string +} + +func (e *errInternal) Code() int32 { + return 500 +} + +func (e *errInternal) Error() string { + return fmt.Sprintf("internal error: %s", e.What) +} + +func (e *errInternal) Is(target error) bool { + _, ok := target.(*errInternal) + return ok +} + +func errorInternal(err error) error { + return &errInternal{What: err.Error()} +} + +type errNotImplemented struct { + What string +} + +func (e *errNotImplemented) Code() int32 { + return 501 +} + +func (e *errNotImplemented) Error() string { + return fmt.Sprintf("not implemented: %s", e.What) +} + +func (e *errNotImplemented) Is(target error) bool { + _, ok := target.(*errNotImplemented) + return ok +} + +func errorNotImplemented(what string) error { + return &errNotImplemented{What: what} +} + +func errToAPIError(err error) api.Error { + if e, ok := err.(*errBadRequest); ok { + return api.Error{Code: e.Code(), Message: e.Error()} + } + if e, ok := err.(*errUnauthorized); ok { + return api.Error{Code: e.Code(), Message: e.Error()} + } + if e, ok := err.(*errForbidden); ok { + return api.Error{Code: e.Code(), Message: e.Error()} + } + if e, ok := err.(*errNotFound); ok { + return api.Error{Code: e.Code(), Message: e.Error()} + } + if e, ok := err.(*errInternal); ok { + return api.Error{Code: e.Code(), Message: e.Error()} + } + if e, ok := err.(*errNotImplemented); ok { + return api.Error{Code: e.Code(), Message: e.Error()} + } + // TODO: log here for an unexpected error condition. + return api.Error{ + Code: 500, + Message: "an unexpected error occurred", + } +} diff --git a/cmd/server/pactasrv/initiative.go b/cmd/server/pactasrv/initiative.go new file mode 100644 index 0000000..588d361 --- /dev/null +++ b/cmd/server/pactasrv/initiative.go @@ -0,0 +1,164 @@ +package pactasrv + +import ( + "context" + + "github.com/RMI/pacta/db" + api "github.com/RMI/pacta/openapi/pacta" + "github.com/RMI/pacta/pacta" +) + +// Creates a initiative +// (POST /initiatives) +func (s *Server) CreateInitiative(ctx context.Context, request api.CreateInitiativeRequestObject) (api.CreateInitiativeResponseObject, error) { + err := s.createInitiative(ctx, request) + if err != nil { + e := errToAPIError(err) + return api.CreateInitiativedefaultJSONResponse{ + Body: e, + StatusCode: int(e.Code), + }, nil + } + return api.CreateInitiative200JSONResponse(emptySuccess), nil +} + +func (s *Server) createInitiative(ctx context.Context, request api.CreateInitiativeRequestObject) error { + // TODO(#12) Implement Authorization + i, err := initiativeCreateToPACTA(request.Body) + if err != nil { + return err + } + err = s.DB.CreateInitiative(s.DB.NoTxn(ctx), i) + if err != nil { + return errorInternal(err) + } + return nil +} + +// Updates an initiative +// (PATCH /initiative/{id}) +func (s *Server) UpdateInitiative(ctx context.Context, request api.UpdateInitiativeRequestObject) (api.UpdateInitiativeResponseObject, error) { + err := s.updateInitiative(ctx, request) + if err != nil { + e := errToAPIError(err) + return api.UpdateInitiativedefaultJSONResponse{ + Body: e, + StatusCode: int(e.Code), + }, nil + } + return api.UpdateInitiative200JSONResponse(emptySuccess), nil +} + +func (s *Server) updateInitiative(ctx context.Context, request api.UpdateInitiativeRequestObject) error { + // TODO(#12) Implement Authorization + id := pacta.InitiativeID(request.Id) + mutations := []db.UpdateInitiativeFn{} + b := request.Params.Body + if b.Affiliation != nil { + mutations = append(mutations, db.SetInitiativeAffiliation(*b.Affiliation)) + } + if b.InternalDescription != nil { + mutations = append(mutations, db.SetInitiativeInternalDescription(*b.InternalDescription)) + } + if b.IsAcceptingNewMembers != nil { + mutations = append(mutations, db.SetInitiativeIsAcceptingNewMembers(*b.IsAcceptingNewMembers)) + } + if b.IsAcceptingNewPortfolios != nil { + mutations = append(mutations, db.SetInitiativeIsAcceptingNewPortfolios(*b.IsAcceptingNewPortfolios)) + } + if b.Language != nil { + lang, err := pacta.ParseLanguage(string(*b.Language)) + if err != nil { + return errorBadRequest("language", err.Error()) + } + mutations = append(mutations, db.SetInitiativeLanguage(lang)) + } + if b.Name != nil { + mutations = append(mutations, db.SetInitiativeName(*b.Name)) + } + if b.PactaVersion != nil { + mutations = append(mutations, db.SetInitiativePACTAVersion(pacta.PACTAVersionID(*b.PactaVersion))) + } + if b.PublicDescription != nil { + mutations = append(mutations, db.SetInitiativePublicDescription(*b.PublicDescription)) + } + if b.RequiresInvitationToJoin != nil { + mutations = append(mutations, db.SetInitiativeRequiresInvitationToJoin(*b.RequiresInvitationToJoin)) + } + err := s.DB.UpdateInitiative(s.DB.NoTxn(ctx), id, mutations...) + if err != nil { + return errorInternal(err) + } + return nil +} + +// Deletes an initiative by id +// (DELETE /initiative/{id}) +func (s *Server) DeleteInitiative(ctx context.Context, request api.DeleteInitiativeRequestObject) (api.DeleteInitiativeResponseObject, error) { + err := s.deleteInitiative(ctx, request) + if err != nil { + e := errToAPIError(err) + return api.DeleteInitiativedefaultJSONResponse{ + Body: e, + StatusCode: int(e.Code), + }, nil + } + return api.DeleteInitiative200JSONResponse(emptySuccess), nil +} + +func (s *Server) deleteInitiative(ctx context.Context, request api.DeleteInitiativeRequestObject) error { + // TODO(#12) Implement Authorization + err := s.DB.DeleteInitiative(s.DB.NoTxn(ctx), pacta.InitiativeID(request.Id)) + if err != nil { + return errorInternal(err) + } + return nil +} + +// Returns an initiative by ID +// (GET /initiative/{id}) +func (s *Server) FindInitiativeById(ctx context.Context, request api.FindInitiativeByIdRequestObject) (api.FindInitiativeByIdResponseObject, error) { + result, err := s.findInitiativeById(ctx, request) + if err != nil { + e := errToAPIError(err) + return api.FindInitiativeByIddefaultJSONResponse{ + Body: e, + StatusCode: int(e.Code), + }, nil + } + return api.FindInitiativeById200JSONResponse(*result), nil +} + +func (s *Server) findInitiativeById(ctx context.Context, request api.FindInitiativeByIdRequestObject) (*api.Initiative, error) { + // TODO(#12) Implement Authorization + i, err := s.DB.Initiative(s.DB.NoTxn(ctx), pacta.InitiativeID(request.Id)) + if err != nil { + if db.IsNotFound(err) { + return nil, errorNotFound("initiative", request.Id) + } + return nil, errorInternal(err) + } + return initiativeToOAPI(i) +} + +// Returns all initiatives +// (GET /initiatives) +func (s *Server) ListInitiatives(ctx context.Context, request api.ListInitiativesRequestObject) (api.ListInitiativesResponseObject, error) { + result, err := s.listInitiatives(ctx, request) + if err != nil { + e := errToAPIError(err) + return api.ListInitiativesdefaultJSONResponse{ + Body: e, + StatusCode: int(e.Code), + }, nil + } + return api.ListInitiatives200JSONResponse(result), nil +} + +func (s *Server) listInitiatives(ctx context.Context, request api.ListInitiativesRequestObject) ([]api.Initiative, error) { + is, err := s.DB.AllInitiatives(s.DB.NoTxn(ctx)) + if err != nil { + return nil, errorInternal(err) + } + return dereference(mapAll(is, initiativeToOAPI)) +} diff --git a/cmd/server/pactasrv/pacta_version.go b/cmd/server/pactasrv/pacta_version.go index 48f2f64..3f0a464 100644 --- a/cmd/server/pactasrv/pacta_version.go +++ b/cmd/server/pactasrv/pacta_version.go @@ -2,48 +2,164 @@ package pactasrv import ( "context" - "fmt" + "github.com/RMI/pacta/db" api "github.com/RMI/pacta/openapi/pacta" "github.com/RMI/pacta/pacta" - "go.uber.org/zap" ) // Returns a version of the PACTA model by ID // (GET /pacta-version/{id}) func (s *Server) FindPactaVersionById(ctx context.Context, request api.FindPactaVersionByIdRequestObject) (api.FindPactaVersionByIdResponseObject, error) { - return nil, fmt.Errorf("not implemented") + pv, err := s.findPactaVersionById(ctx, request) + if err != nil { + e := errToAPIError(err) + return api.FindPactaVersionByIddefaultJSONResponse{ + Body: e, + StatusCode: int(e.Code), + }, nil + } + return api.FindPactaVersionById200JSONResponse(*pv), nil +} + +func (s *Server) findPactaVersionById(ctx context.Context, request api.FindPactaVersionByIdRequestObject) (*api.PactaVersion, error) { + // TODO(#12) Implement Authorization + pv, err := s.DB.PACTAVersion(s.DB.NoTxn(ctx), pacta.PACTAVersionID(request.Id)) + if err != nil { + return nil, errorInternal(err) + } + return pactaVersionToOAPI(pv) } // Returns all versions of the PACTA model // (GET /pacta-versions) func (s *Server) ListPactaVersions(ctx context.Context, request api.ListPactaVersionsRequestObject) (api.ListPactaVersionsResponseObject, error) { - return nil, fmt.Errorf("not implemented") + pvs, err := s.listPactaVersions(ctx, request) + if err != nil { + e := errToAPIError(err) + return api.ListPactaVersionsdefaultJSONResponse{ + Body: e, + StatusCode: int(e.Code), + }, nil + } + return api.ListPactaVersions200JSONResponse(pvs), nil +} + +func (s *Server) listPactaVersions(ctx context.Context, _request api.ListPactaVersionsRequestObject) ([]api.PactaVersion, error) { + // TODO(#12) Implement Authorization + pvs, err := s.DB.PACTAVersions(s.DB.NoTxn(ctx)) + if err != nil { + return nil, errorInternal(err) + } + return dereference(mapAll(pvs, pactaVersionToOAPI)) } // Creates a PACTA version // (POST /pacta-versions) func (s *Server) CreatePactaVersion(ctx context.Context, request api.CreatePactaVersionRequestObject) (api.CreatePactaVersionResponseObject, error) { - // TODO(grady) Authz - _, err := s.DB.CreatePACTAVersion(s.DB.NoTxn(ctx), &pacta.PACTAVersion{ - Name: request.Body.Name, - Description: request.Body.Description, - Digest: request.Body.Digest, - }) + err := s.createPactaVersion(ctx, request) + if err != nil { + e := errToAPIError(err) + return api.CreatePactaVersiondefaultJSONResponse{ + Body: e, + StatusCode: int(e.Code), + }, nil + } + return api.CreatePactaVersion200JSONResponse(emptySuccess), nil +} + +func (s *Server) createPactaVersion(ctx context.Context, request api.CreatePactaVersionRequestObject) error { + // TODO(#12) Implement Authorization + pv, err := pactaVersionCreateToPACTA(request.Body) if err != nil { - return nil, zap.Error(ctx, "failed to create PACTA version", zap.Error(err)) + return errorBadRequest("body", err.Error()) } - return nil, nil + if _, err := s.DB.CreatePACTAVersion(s.DB.NoTxn(ctx), pv); err != nil { + return errorInternal(err) + } + return nil } // Updates a PACTA version // (PATCH /pacta-version/{id}) func (s *Server) UpdatePactaVersion(ctx context.Context, request api.UpdatePactaVersionRequestObject) (api.UpdatePactaVersionResponseObject, error) { - return nil, fmt.Errorf("not implemented") + err := s.updatePactaVersion(ctx, request) + if err != nil { + e := errToAPIError(err) + return api.UpdatePactaVersiondefaultJSONResponse{ + Body: e, + StatusCode: int(e.Code), + }, nil + } + return api.UpdatePactaVersion200JSONResponse(emptySuccess), nil +} + +func (s *Server) updatePactaVersion(ctx context.Context, request api.UpdatePactaVersionRequestObject) error { + // TODO(#12) Implement Authorization + id := pacta.PACTAVersionID(request.Id) + mutations := []db.UpdatePACTAVersionFn{} + b := request.Body + if b.Description != nil { + mutations = append(mutations, db.SetPACTAVersionDescription(*b.Description)) + } + if b.Digest != nil { + mutations = append(mutations, db.SetPACTAVersionDigest(*b.Digest)) + } + if b.Name != nil { + mutations = append(mutations, db.SetPACTAVersionName(*b.Name)) + } + err := s.DB.UpdatePACTAVersion(s.DB.NoTxn(ctx), id, mutations...) + if err != nil { + return errorInternal(err) + } + return nil + } // Deletes a pacta version by ID // (DELETE /pacta-version/{id}) func (s *Server) DeletePactaVersion(ctx context.Context, request api.DeletePactaVersionRequestObject) (api.DeletePactaVersionResponseObject, error) { - return nil, fmt.Errorf("not implemented") + err := s.deletePactaVersion(ctx, request) + if err != nil { + e := errToAPIError(err) + return api.DeletePactaVersiondefaultJSONResponse{ + Body: e, + StatusCode: int(e.Code), + }, nil + } + return api.DeletePactaVersion200JSONResponse(emptySuccess), nil +} + +func (s *Server) deletePactaVersion(ctx context.Context, request api.DeletePactaVersionRequestObject) error { + // TODO(#12) Implement Authorization + id := pacta.PACTAVersionID(request.Id) + err := s.DB.DeletePACTAVersion(s.DB.NoTxn(ctx), id) + if err != nil { + return errorInternal(err) + } + return nil +} + +// Marks this version of the PACTA model as the default +// (POST /pacta-version/{id}/set-default) +func (s *Server) MarkPactaVersionAsDefault(ctx context.Context, request api.MarkPactaVersionAsDefaultRequestObject) (api.MarkPactaVersionAsDefaultResponseObject, error) { + err := s.markPactaVersionAsDefault(ctx, request) + if err != nil { + e := errToAPIError(err) + return api.MarkPactaVersionAsDefaultdefaultJSONResponse{ + Body: e, + StatusCode: int(e.Code), + }, nil + } + return api.MarkPactaVersionAsDefault200JSONResponse(emptySuccess), nil +} + +func (s *Server) markPactaVersionAsDefault(ctx context.Context, request api.MarkPactaVersionAsDefaultRequestObject) error { + // TODO(#12) Implement Authorization + id := pacta.PACTAVersionID(request.Id) + err := s.DB.SetDefaultPACTAVersion(s.DB.NoTxn(ctx), id) + if err != nil { + return errorInternal(err) + } + return nil } diff --git a/cmd/server/pactasrv/pactasrv.go b/cmd/server/pactasrv/pactasrv.go index 94b8092..f179e60 100644 --- a/cmd/server/pactasrv/pactasrv.go +++ b/cmd/server/pactasrv/pactasrv.go @@ -2,9 +2,10 @@ package pactasrv import ( "context" + "fmt" "github.com/RMI/pacta/db" - + api "github.com/RMI/pacta/openapi/pacta" "github.com/RMI/pacta/pacta" ) @@ -62,3 +63,31 @@ type DB interface { type Server struct { DB DB } + +var emptySuccess api.EmptySuccess = api.EmptySuccess{} + +func mapAll[I any, O any](is []I, f func(I) (O, error)) ([]O, error) { + os := make([]O, len(is)) + for i, v := range is { + o, err := f(v) + if err != nil { + return nil, err + } + os[i] = o + } + return os, nil +} + +func dereference[T any](ts []*T, e error) ([]T, error) { + if e != nil { + return nil, e + } + result := make([]T, len(ts)) + for i, t := range ts { + if t == nil { + return nil, errorInternal(fmt.Errorf("dereference: nil pointer for %T at index %d", t, i)) + } + result[i] = *t + } + return result, nil +} diff --git a/cmd/server/pactasrv/user.go b/cmd/server/pactasrv/user.go index 74f8589..9d78bb8 100644 --- a/cmd/server/pactasrv/user.go +++ b/cmd/server/pactasrv/user.go @@ -2,7 +2,6 @@ package pactasrv import ( "context" - "fmt" api "github.com/RMI/pacta/openapi/pacta" ) @@ -10,17 +9,56 @@ import ( // Returns a user by ID // (GET /user/{id}) func (s *Server) FindUserById(ctx context.Context, request api.FindUserByIdRequestObject) (api.FindUserByIdResponseObject, error) { - return nil, fmt.Errorf("not implemented") + u, err := s.findUserById(ctx, request) + if err != nil { + e := errToAPIError(err) + return api.FindUserByIddefaultJSONResponse{ + Body: e, + StatusCode: int(e.Code), + }, nil + } + return api.FindUserById200JSONResponse(*u), nil +} + +func (s *Server) findUserById(ctx context.Context, request api.FindUserByIdRequestObject) (*api.User, error) { + // TODO(#12) Implement Authorization + return nil, errorNotImplemented("findUserById") } // Updates user properties // (PATCH /user/{id}) func (s *Server) UpdateUser(ctx context.Context, request api.UpdateUserRequestObject) (api.UpdateUserResponseObject, error) { - return nil, fmt.Errorf("not implemented") + err := s.updateUser(ctx, request) + if err != nil { + e := errToAPIError(err) + return api.UpdateUserdefaultJSONResponse{ + Body: e, + StatusCode: int(e.Code), + }, nil + } + return api.UpdateUser200JSONResponse(emptySuccess), nil +} + +func (s *Server) updateUser(ctx context.Context, request api.UpdateUserRequestObject) error { + // TODO(#12) Implement Authorization + return errorNotImplemented("updateUser") } // Deletes a user by ID // (DELETE /user/{id}) func (s *Server) DeleteUser(ctx context.Context, request api.DeleteUserRequestObject) (api.DeleteUserResponseObject, error) { - return nil, fmt.Errorf("not implemented") + err := s.deleteUser(ctx, request) + if err != nil { + e := errToAPIError(err) + return api.DeleteUserdefaultJSONResponse{ + Body: e, + StatusCode: int(e.Code), + }, nil + } + return api.DeleteUser200JSONResponse(emptySuccess), nil +} + +func (s *Server) deleteUser(ctx context.Context, request api.DeleteUserRequestObject) error { + // TODO(#12) Implement Authorization + return errorNotImplemented("deleteUser") } diff --git a/frontend/components/standard/Content.vue b/frontend/components/standard/Content.vue index baa3327..60d9b53 100644 --- a/frontend/components/standard/Content.vue +++ b/frontend/components/standard/Content.vue @@ -28,7 +28,7 @@ color: rgb(0 0 0 / 85%); } - a { + a & :not(.p-button) { font-weight: 600; color: rgb(0 0 0 / 85%); } diff --git a/frontend/components/standard/Footer.vue b/frontend/components/standard/Footer.vue index 8d7c8b0..9d21574 100644 --- a/frontend/components/standard/Footer.vue +++ b/frontend/components/standard/Footer.vue @@ -1,6 +1,18 @@ + + diff --git a/frontend/composables/useURLParams.ts b/frontend/composables/useURLParams.ts new file mode 100644 index 0000000..95692e6 --- /dev/null +++ b/frontend/composables/useURLParams.ts @@ -0,0 +1,56 @@ +import type { RouteParams, LocationQuery } from 'vue-router' +import { useRoute, stringifyQuery } from 'vue-router' +import { computed, type WritableComputedRef } from 'vue' + +export const useURLParams = () => { + const route = useRoute() + const router = useRouter() + + const getVal = (src: RouteParams | LocationQuery, key: string): string | undefined => { + const val = src[key] + if (!val) { + return undefined + } + + if (Array.isArray(val)) { + if (val.length === 0) { + return undefined + } + if (!val[0]) { + return undefined + } + return val[0] + } + + return val + } + + const setVal = (key: string, val: string | undefined) => { + const query = new URLSearchParams(stringifyQuery(router.currentRoute.value.query)) + if (val) { + query.set(key, val) + } else { + query.delete(key) + } + let qs = query.toString() + if (qs) { + qs = '?' + qs + } + void router.replace(qs) + } + + return { + fromQuery: (key: string): string | undefined => { + return getVal(route.query, key) + }, + fromQueryReactive: (key: string): WritableComputedRef => { + return computed({ + get: () => getVal(router.currentRoute.value.query, key), + set: (val: string | undefined) => { setVal(key, val) } + }) + }, + fromParams: (key: string): string | undefined => { + return getVal(route.params, key) + } + } +} diff --git a/frontend/layouts/default.vue b/frontend/layouts/default.vue index e7843f2..bb29859 100644 --- a/frontend/layouts/default.vue +++ b/frontend/layouts/default.vue @@ -33,7 +33,7 @@ onMounted(() => { {{ setError(error) }} {{ clearError() }} - + diff --git a/frontend/openapi/generated/pacta/index.ts b/frontend/openapi/generated/pacta/index.ts index a8fbfd5..d98730b 100644 --- a/frontend/openapi/generated/pacta/index.ts +++ b/frontend/openapi/generated/pacta/index.ts @@ -12,6 +12,9 @@ export type { OpenAPIConfig } from './core/OpenAPI'; export type { EmptySuccess } from './models/EmptySuccess'; export type { Error } from './models/Error'; +export { Initiative } from './models/Initiative'; +export { InitiativeChanges } from './models/InitiativeChanges'; +export { InitiativeCreate } from './models/InitiativeCreate'; export { Language } from './models/Language'; export type { PactaVersion } from './models/PactaVersion'; export type { PactaVersionChanges } from './models/PactaVersionChanges'; diff --git a/frontend/openapi/generated/pacta/models/Initiative.ts b/frontend/openapi/generated/pacta/models/Initiative.ts new file mode 100644 index 0000000..a04fcfc --- /dev/null +++ b/frontend/openapi/generated/pacta/models/Initiative.ts @@ -0,0 +1,67 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type Initiative = { + /** + * the human readable identifier for the initiative, can only include alphanumeric characters, dashes and underscores + */ + id: string; + /** + * the human meaningful name of the version of the initiative + */ + name: string; + /** + * the group that sponsors/created/owns this initiative + */ + affiliation: string; + /** + * Additional information about the initiative + */ + publicDescription: string; + /** + * Additional information about the initiative, for participants only + */ + internalDescription: string; + /** + * If set, only users who have been invited to join this initiative can join it, otherwise, anyone can join it. Defaults to false. + */ + requiresInvitationToJoin: boolean; + /** + * If set, new users can join the initiative. Defaults to false. + */ + isAcceptingNewMembers: boolean; + /** + * If set, users that are members of this initiative can add portfolios to it. + */ + isAcceptingNewPortfolios: boolean; + /** + * The language this initiative should be conducted in. + */ + language: Initiative.language; + /** + * The pacta model that this initiative should use, if not specified, the default pacta model will be used. + */ + pactaVersion?: string; + /** + * The time at which this initiative was created. + */ + createdAt: string; +}; + +export namespace Initiative { + + /** + * The language this initiative should be conducted in. + */ + export enum language { + EN = 'en', + FR = 'fr', + ES = 'es', + DE = 'de', + } + + +} + diff --git a/frontend/openapi/generated/pacta/models/InitiativeChanges.ts b/frontend/openapi/generated/pacta/models/InitiativeChanges.ts new file mode 100644 index 0000000..aeb4bd5 --- /dev/null +++ b/frontend/openapi/generated/pacta/models/InitiativeChanges.ts @@ -0,0 +1,59 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type InitiativeChanges = { + /** + * the human meaningful name of the version of the initiative + */ + name?: string; + /** + * the group that sponsors/created/owns this initiative + */ + affiliation?: string; + /** + * Additional information about the initiative + */ + publicDescription?: string; + /** + * Additional information about the initiative, for participants only + */ + internalDescription?: string; + /** + * If set, only users who have been invited to join this initiative can join it, otherwise, anyone can join it. Defaults to false. + */ + requiresInvitationToJoin?: boolean; + /** + * If set, new users can join the initiative. Defaults to false. + */ + isAcceptingNewMembers?: boolean; + /** + * If set, users that are members of this initiative can add portfolios to it. + */ + isAcceptingNewPortfolios?: boolean; + /** + * The language this initiative should be conducted in. + */ + language?: InitiativeChanges.language; + /** + * The pacta model that this initiative should use, if not specified, the default pacta model will be used. + */ + pactaVersion?: string; +}; + +export namespace InitiativeChanges { + + /** + * The language this initiative should be conducted in. + */ + export enum language { + EN = 'en', + FR = 'fr', + ES = 'es', + DE = 'de', + } + + +} + diff --git a/frontend/openapi/generated/pacta/models/InitiativeCreate.ts b/frontend/openapi/generated/pacta/models/InitiativeCreate.ts new file mode 100644 index 0000000..2d1335f --- /dev/null +++ b/frontend/openapi/generated/pacta/models/InitiativeCreate.ts @@ -0,0 +1,63 @@ +/* generated using openapi-typescript-codegen -- do no edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type InitiativeCreate = { + /** + * the human readable identifier for the initiative, can only include alphanumeric characters, dashes and underscores + */ + id: string; + /** + * the human meaningful name of the version of the initiative + */ + name: string; + /** + * the group that sponsors/created/owns this initiative + */ + affiliation?: string; + /** + * Additional information about the initiative + */ + publicDescription?: string; + /** + * Additional information about the initiative, for participants only + */ + internalDescription?: string; + /** + * If set, only users who have been invited to join this initiative can join it, otherwise, anyone can join it. Defaults to false. + */ + requiresInvitationToJoin?: boolean; + /** + * If set, new users can join the initiative. Defaults to false. + */ + isAcceptingNewMembers?: boolean; + /** + * If set, users that are members of this initiative can add portfolios to it. + */ + isAcceptingNewPortfolios?: boolean; + /** + * The language this initiative should be conducted in. + */ + language: InitiativeCreate.language; + /** + * The id of the PACTA model that this initiative should use, if not specified, the default PACTA model will be used. + */ + pactaVersion?: string; +}; + +export namespace InitiativeCreate { + + /** + * The language this initiative should be conducted in. + */ + export enum language { + EN = 'en', + FR = 'fr', + ES = 'es', + DE = 'de', + } + + +} + diff --git a/frontend/openapi/generated/pacta/models/PactaVersionChanges.ts b/frontend/openapi/generated/pacta/models/PactaVersionChanges.ts index 2e04be2..43277ea 100644 --- a/frontend/openapi/generated/pacta/models/PactaVersionChanges.ts +++ b/frontend/openapi/generated/pacta/models/PactaVersionChanges.ts @@ -16,9 +16,5 @@ export type PactaVersionChanges = { * The hash (typically SHA256) that uniquely identifies this version of the PACTA model. */ digest?: string; - /** - * Whether this version of the PACTA model is the default version - */ - isDefault?: boolean; }; diff --git a/frontend/openapi/generated/pacta/services/DefaultService.ts b/frontend/openapi/generated/pacta/services/DefaultService.ts index d8e32d4..126c405 100644 --- a/frontend/openapi/generated/pacta/services/DefaultService.ts +++ b/frontend/openapi/generated/pacta/services/DefaultService.ts @@ -4,6 +4,9 @@ /* eslint-disable */ import type { EmptySuccess } from '../models/EmptySuccess'; import type { Error } from '../models/Error'; +import type { Initiative } from '../models/Initiative'; +import type { InitiativeChanges } from '../models/InitiativeChanges'; +import type { InitiativeCreate } from '../models/InitiativeCreate'; import type { PactaVersion } from '../models/PactaVersion'; import type { PactaVersionChanges } from '../models/PactaVersionChanges'; import type { PactaVersionCreate } from '../models/PactaVersionCreate'; @@ -40,14 +43,14 @@ export class DefaultService { * Updates a PACTA version * Updates a PACTA version's settable properties * @param id ID of PACTA version to update - * @param body PACTA Version object properties to update + * @param requestBody PACTA Version object properties to update * @returns EmptySuccess pacta version updated successfully * @returns Error unexpected error * @throws ApiError */ public updatePactaVersion( id: string, - body: PactaVersionChanges, + requestBody: PactaVersionChanges, ): CancelablePromise { return this.httpRequest.request({ method: 'PATCH', @@ -55,12 +58,8 @@ export class DefaultService { path: { 'id': id, }, - query: { - 'body': body, - }, - errors: { - 403: `caller does not have access or PACTA version does not exist`, - }, + body: requestBody, + mediaType: 'application/json', }); } @@ -81,8 +80,24 @@ export class DefaultService { path: { 'id': id, }, - errors: { - 403: `caller does not have access or pacta version does not exist`, + }); + } + + /** + * Marks this version of the PACTA model as the default + * @param id ID of pacta version to fetch + * @returns EmptySuccess updated successfully + * @returns Error unexpected error + * @throws ApiError + */ + public markPactaVersionAsDefault( + id: string, + ): CancelablePromise { + return this.httpRequest.request({ + method: 'POST', + url: '/pacta-version/{id}/set-default', + path: { + 'id': id, }, }); } @@ -116,12 +131,105 @@ export class DefaultService { url: '/pacta-versions', body: requestBody, mediaType: 'application/json', - errors: { - 403: `caller does not have access to create PACTA versions`, + }); + } + + /** + * Returns an initiative by ID + * @param id ID of the initiative to fetch + * @returns Initiative the initiative requested + * @returns Error unexpected error + * @throws ApiError + */ + public findInitiativeById( + id: string, + ): CancelablePromise { + return this.httpRequest.request({ + method: 'GET', + url: '/initiative/{id}', + path: { + 'id': id, + }, + }); + } + + /** + * Updates an initiative + * Updates an initiative's settable properties + * @param id ID of the initiative to update + * @param body initiative object properties to update + * @returns EmptySuccess initiative updated successfully + * @returns Error unexpected error + * @throws ApiError + */ + public updateInitiative( + id: string, + body: InitiativeChanges, + ): CancelablePromise { + return this.httpRequest.request({ + method: 'PATCH', + url: '/initiative/{id}', + path: { + 'id': id, + }, + query: { + 'body': body, }, }); } + /** + * Deletes an initiative by id + * deletes an initiative based on the ID supplied + * @param id ID of initiative to delete + * @returns EmptySuccess initiative deleted successfully + * @returns Error unexpected error + * @throws ApiError + */ + public deleteInitiative( + id: string, + ): CancelablePromise { + return this.httpRequest.request({ + method: 'DELETE', + url: '/initiative/{id}', + path: { + 'id': id, + }, + }); + } + + /** + * Returns all initiatives + * @returns Initiative gets all initiatives + * @returns Error unexpected error + * @throws ApiError + */ + public listInitiatives(): CancelablePromise | Error> { + return this.httpRequest.request({ + method: 'GET', + url: '/initiatives', + }); + } + + /** + * Creates a initiative + * Creates a new initiative + * @param requestBody Initiative object properties to update + * @returns EmptySuccess initiative created successfully + * @returns Error unexpected error + * @throws ApiError + */ + public createInitiative( + requestBody: InitiativeCreate, + ): CancelablePromise { + return this.httpRequest.request({ + method: 'POST', + url: '/initiatives', + body: requestBody, + mediaType: 'application/json', + }); + } + /** * Returns a user by ID * Returns a user based on a single ID @@ -139,9 +247,6 @@ export class DefaultService { path: { 'id': id, }, - errors: { - 403: `caller does not have access or user does not exist`, - }, }); } @@ -150,14 +255,14 @@ export class DefaultService { * Updates a user's settable properties * @param id ID of user to update * @param requestBody User object properties to update - * @returns User the new user object + * @returns EmptySuccess the new user object * @returns Error unexpected error * @throws ApiError */ public updateUser( id: string, requestBody: UserChanges, - ): CancelablePromise { + ): CancelablePromise { return this.httpRequest.request({ method: 'PATCH', url: '/user/{id}', @@ -166,9 +271,6 @@ export class DefaultService { }, body: requestBody, mediaType: 'application/json', - errors: { - 403: `caller does not have access or user does not exist`, - }, }); } @@ -189,9 +291,6 @@ export class DefaultService { path: { 'id': id, }, - errors: { - 403: `caller does not have access or user does not exist`, - }, }); } diff --git a/frontend/pages/admin/pacta-version/[id].vue b/frontend/pages/admin/pacta-version/[id].vue new file mode 100644 index 0000000..4586865 --- /dev/null +++ b/frontend/pages/admin/pacta-version/[id].vue @@ -0,0 +1,110 @@ + + + diff --git a/frontend/pages/admin/pacta-version/index.vue b/frontend/pages/admin/pacta-version/index.vue index d3686bd..af0749d 100644 --- a/frontend/pages/admin/pacta-version/index.vue +++ b/frontend/pages/admin/pacta-version/index.vue @@ -8,13 +8,29 @@ const { error: { withLoadingAndErrorHandling, handleOAPIError } } = useModal() const prefix = 'admin/pacta-version' const pactaVersions = useState(`${prefix}.pactaVersions`, () => []) +const newPV = () => router.push('/admin/pacta-version/new') +const markDefault = (id: string) => withLoadingAndErrorHandling( + () => pactaClient.markPactaVersionAsDefault(id) + .then(handleOAPIError) + .then(() => { pactaVersions.value = pactaVersions.value.map(pv => ({ ...pv, isDefault: id === pv.id })) }), + `${prefix}.markPactaVersionAsDefault` +) const deletePV = (id: string) => withLoadingAndErrorHandling( () => pactaClient.deletePactaVersion(id) .then(handleOAPIError) .then(() => { pactaVersions.value = pactaVersions.value.filter(pv => pv.id !== id) }), `${prefix}.deletePactaVersion` ) -const newPV = () => router.push('/admin/pacta-version/new') + +// TODO(#13) Remove this from the on-mounted hook +onMounted(async () => { + await withLoadingAndErrorHandling( + () => pactaClient.listPactaVersions() + .then(handleOAPIError) + .then(pvs => { pactaVersions.value = pvs }), + `${prefix}.getPactaVersions` + ) +})