diff --git a/apps/services/accounts-api/cmd/server/main.go b/apps/services/accounts-api/cmd/server/main.go index 27f13b3..5198532 100644 --- a/apps/services/accounts-api/cmd/server/main.go +++ b/apps/services/accounts-api/cmd/server/main.go @@ -88,7 +88,7 @@ func run() error { accountRepo := repositories.NewAccountRespository(params.Logger, params.DB) // Create services - registrationService := services.NewRegistrationService(params.Logger, accountRepo) + registrationService := services.NewAccountService(params.Logger, accountRepo) // Create Application app := app.NewApp( diff --git a/apps/services/accounts-api/fly.toml b/apps/services/accounts-api/fly.toml index d782cae..7962d18 100644 --- a/apps/services/accounts-api/fly.toml +++ b/apps/services/accounts-api/fly.toml @@ -1,7 +1,7 @@ app = 'accounts-api-prod' [[services]] -primary_region = 'ewr' +primary_region = 'iad' [build] dockerfile = "Dockerfile" diff --git a/apps/services/accounts-api/internal/adapters/connectrpc/account_service_handler.go b/apps/services/accounts-api/internal/adapters/connectrpc/account_service_handler.go index e08d885..db56222 100644 --- a/apps/services/accounts-api/internal/adapters/connectrpc/account_service_handler.go +++ b/apps/services/accounts-api/internal/adapters/connectrpc/account_service_handler.go @@ -6,7 +6,7 @@ import ( "fmt" "libs/backend/boot" "libs/backend/domain/user/entities" - "libs/backend/domain/user/valueobjects" + userValueObjects "libs/backend/domain/user/valueobjects" accountsapiv1 "libs/backend/proto-gen/go/accounts/accountsapi/v1" "libs/backend/proto-gen/go/accounts/accountsapi/v1/accountsapiv1connect" accountsDomain "libs/backend/proto-gen/go/accounts/domain" @@ -16,7 +16,7 @@ import ( ) // AuthHandler handles all gRPC endpoints for inbound webhooks -type RegistrationServiceHandler struct { +type AccountServiceHandler struct { accountsapiv1connect.UnimplementedAccountServiceHandler // Logger is the logger from the boot framework @@ -27,20 +27,20 @@ type RegistrationServiceHandler struct { } // NewRegistrationHandler will return a pointer to the inbound webhooks API server -func NewRegistrationHandler(logger boot.Logger, app app.App) *RegistrationServiceHandler { - return &RegistrationServiceHandler{ +func NewRegistrationHandler(logger boot.Logger, app app.App) *AccountServiceHandler { + return &AccountServiceHandler{ Logger: logger, App: app, } } // CreateAccount handles user creation and saves them in the database -func (r *RegistrationServiceHandler) CreateAccount( +func (r *AccountServiceHandler) CreateAccount( ctx context.Context, req *connect.Request[accountsapiv1.CreateAccountRequest], ) (*connect.Response[accountsapiv1.CreateAcountResponse], error) { - commonID := valueobjects.NewCommonIDFromString(req.Msg.CommonId) - emailAddress := valueobjects.NewEmailAddress(req.Msg.EmailAddress) + commonID := userValueObjects.NewCommonIDFromString(req.Msg.CommonId) + emailAddress := userValueObjects.NewEmailAddress(req.Msg.EmailAddress) // Convert to user domain type user := entities.NewUser( @@ -56,21 +56,21 @@ func (r *RegistrationServiceHandler) CreateAccount( } // GetAccount handles user creation and saves them in the database -func (r *RegistrationServiceHandler) GetAccount( +func (r *AccountServiceHandler) GetAccount( ctx context.Context, req *connect.Request[accountsapiv1.GetAccountRequest], ) (*connect.Response[accountsapiv1.GetAccountResponse], error) { // Parse the commonID vs emailAddress, depending on which one is passed - var commonID valueobjects.CommonID - var emailAddress valueobjects.EmailAddress + var commonID userValueObjects.CommonID + var emailAddress userValueObjects.EmailAddress if req.Msg.CommonId != nil { - commonID = valueobjects.NewCommonIDFromString(*req.Msg.CommonId) + commonID = userValueObjects.NewCommonIDFromString(*req.Msg.CommonId) } if req.Msg.EmailAddress != nil { - emailAddress = valueobjects.NewEmailAddress(*req.Msg.EmailAddress) + emailAddress = userValueObjects.NewEmailAddress(*req.Msg.EmailAddress) } // Get the user from the database @@ -91,3 +91,21 @@ func (r *RegistrationServiceHandler) GetAccount( return connect.NewResponse(resp), nil } + +// DeleteAccount handles account deletion +func (r *AccountServiceHandler) DeleteAccount( + ctx context.Context, + req *connect.Request[accountsapiv1.DeleteAccountRequest], +) (*connect.Response[accountsapiv1.DeleteAccountResponse], error) { + // Parse incoming request data + parsedCommonID := userValueObjects.NewCommonIDFromString(req.Msg.CommonId) + hardDelete := req.Msg.HardDelete + + // Perform deletion logic + deletedAt, err := r.App.RegistrationService.DeleteUser(ctx, parsedCommonID, hardDelete) + if err != nil { + return nil, connect.NewError(connect.CodeInternal, err) + } + + return connect.NewResponse(&accountsapiv1.DeleteAccountResponse{DeletedAt: timestamppb.New(deletedAt)}), nil +} diff --git a/apps/services/accounts-api/internal/adapters/database/repositories/account_repository.go b/apps/services/accounts-api/internal/adapters/database/repositories/account_repository.go index efa3621..cd53e9c 100644 --- a/apps/services/accounts-api/internal/adapters/database/repositories/account_repository.go +++ b/apps/services/accounts-api/internal/adapters/database/repositories/account_repository.go @@ -4,10 +4,12 @@ import ( "apps/services/accounts-api/internal/models" "context" "errors" + "fmt" "libs/backend/boot" userEntities "libs/backend/domain/user/entities" userValueObjects "libs/backend/domain/user/valueobjects" "log/slog" + "time" "github.com/google/uuid" "gorm.io/gorm" @@ -69,7 +71,7 @@ func (r AccountRepository) GetAccountByEmailAddress(ctx context.Context, emailAd r.Logger.Info("Getting account", slog.String("emailAdress", emailAdress.String())) account := &models.Account{} - r.Database.First(account, "email_address = ?", emailAdress.Value()) + r.Database.First(account, "email_address = ?", emailAdress) if account.ID == uuid.Nil { return userEntities.User{}, errors.New("account not found") @@ -78,6 +80,34 @@ func (r AccountRepository) GetAccountByEmailAddress(ctx context.Context, emailAd return r.convertAccountToUser(account), nil } +// SoftDeleteAccountByCommonID will mark the user as deleted in the database with a timestamp +func (r AccountRepository) SoftDeleteAccountByCommonID(ctx context.Context, commonID userValueObjects.CommonID) (time.Time, error) { + r.Logger.Info("Handling soft deletion of account by commonID", slog.String("commonID", commonID.String())) + + // Handle soft deletion in the database + deletedAccount := &models.Account{} + if err := r.Database.Delete(deletedAccount, "common_id = ?", commonID).Error; err != nil { + r.Logger.Error("Cannot soft delete the user by commonID", slog.String("commonID", commonID.String())) + return time.Time{}, fmt.Errorf("cannot soft delete account: %w", err) + } + + return deletedAccount.DeletedAt.Time, nil +} + +// HardDeleteAccountByCommonID will remove the user from the dataase +func (r AccountRepository) HardDeleteAccountByCommonID(ctx context.Context, commonID userValueObjects.CommonID) (time.Time, error) { + r.Logger.Info("Handling hard deletion of account by commonID", slog.Any("commonID", commonID.String())) + + // Handle soft deletion in the database + deletedAccount := &models.Account{} + if err := r.Database.Unscoped().Delete(deletedAccount, "common_id = ?", commonID).Error; err != nil { + r.Logger.Error("Cannot hard delete the user by commonID", slog.String("commonID", commonID.String())) + return time.Time{}, fmt.Errorf("cannot hard delete accoutn: %w", err) + } + + return deletedAccount.DeletedAt.Time, nil +} + // convertAccountToUser converts an account to a user func (r AccountRepository) convertAccountToUser(account *models.Account) userEntities.User { parsedCommonID := userValueObjects.NewCommonIDFromUUID(account.CommonID) diff --git a/apps/services/accounts-api/internal/app/ports/repository.go b/apps/services/accounts-api/internal/app/ports/repository.go index 53f0f63..003ec45 100644 --- a/apps/services/accounts-api/internal/app/ports/repository.go +++ b/apps/services/accounts-api/internal/app/ports/repository.go @@ -4,6 +4,7 @@ import ( "context" userEntities "libs/backend/domain/user/entities" userValueObjects "libs/backend/domain/user/valueobjects" + "time" ) // AccountRepository is the interface for the account repository @@ -16,4 +17,10 @@ type AccountRepository interface { // GetAccountByEmailAddress gets an account from the database by email address GetAccountByEmailAddress(context.Context, userValueObjects.EmailAddress) (userEntities.User, error) + + // SoftDeleteAccountByCommonID will soft delete the user + SoftDeleteAccountByCommonID(context.Context, userValueObjects.CommonID) (time.Time, error) + + // HardDeleteAccountByCommonID will hard delete the user + HardDeleteAccountByCommonID(context.Context, userValueObjects.CommonID) (time.Time, error) } diff --git a/apps/services/accounts-api/internal/app/ports/service.go b/apps/services/accounts-api/internal/app/ports/service.go index 728a736..fc2025d 100644 --- a/apps/services/accounts-api/internal/app/ports/service.go +++ b/apps/services/accounts-api/internal/app/ports/service.go @@ -4,6 +4,7 @@ import ( "context" userEntities "libs/backend/domain/user/entities" userValueObjects "libs/backend/domain/user/valueobjects" + "time" ) // AccountService is the interface for the registration service @@ -13,4 +14,7 @@ type AccountService interface { // GetUser gets a user from the system GetUser(ctx context.Context, commonID userValueObjects.CommonID, emailAddress userValueObjects.EmailAddress) (userEntities.User, error) + + // Delete user will delete the user from the system (hard or soft deletion) + DeleteUser(ctx context.Context, commonID userValueObjects.CommonID, hardDelete bool) (time.Time, error) } diff --git a/apps/services/accounts-api/internal/domain/generic.go b/apps/services/accounts-api/internal/domain/generic.go deleted file mode 100644 index cdcede9..0000000 --- a/apps/services/accounts-api/internal/domain/generic.go +++ /dev/null @@ -1,4 +0,0 @@ -package domain - -// GenericDomain is a placeholder -type GenericDomain struct{} diff --git a/apps/services/accounts-api/internal/domain/services/registration.go b/apps/services/accounts-api/internal/domain/services/account_service.go similarity index 54% rename from apps/services/accounts-api/internal/domain/services/registration.go rename to apps/services/accounts-api/internal/domain/services/account_service.go index 4ff1286..1f97188 100644 --- a/apps/services/accounts-api/internal/domain/services/registration.go +++ b/apps/services/accounts-api/internal/domain/services/account_service.go @@ -8,10 +8,11 @@ import ( userEntities "libs/backend/domain/user/entities" userValueObjects "libs/backend/domain/user/valueobjects" "log/slog" + "time" ) -// RegistrationService is the registration service -type RegistrationService struct { +// AccountService is the registration service +type AccountService struct { // Logger is the logger from the boot framework Logger boot.Logger @@ -19,16 +20,16 @@ type RegistrationService struct { AccountRepository ports.AccountRepository } -// NewRegistrationService creates a new registration service -func NewRegistrationService(logger boot.Logger, accountRepository ports.AccountRepository) RegistrationService { - return RegistrationService{ +// NewAccountService creates a new registration service +func NewAccountService(logger boot.Logger, accountRepository ports.AccountRepository) AccountService { + return AccountService{ Logger: logger, AccountRepository: accountRepository, } } // RegisterUser registers a user in the system and the database -func (s RegistrationService) RegisterUser(ctx context.Context, user userEntities.User) error { +func (s AccountService) RegisterUser(ctx context.Context, user userEntities.User) error { s.Logger.Info("Registering user", slog.String("commonID", user.CommonID.String())) // Create account @@ -41,7 +42,7 @@ func (s RegistrationService) RegisterUser(ctx context.Context, user userEntities } // GetUser gets a user from the system -func (s RegistrationService) GetUser(ctx context.Context, commonID userValueObjects.CommonID, emailAddress userValueObjects.EmailAddress) (userEntities.User, error) { +func (s AccountService) GetUser(ctx context.Context, commonID userValueObjects.CommonID, emailAddress userValueObjects.EmailAddress) (userEntities.User, error) { var err error s.Logger.Info("Getting user", slog.String("commonID", commonID.String())) @@ -66,3 +67,24 @@ func (s RegistrationService) GetUser(ctx context.Context, commonID userValueObje return user, nil } + +// Delete user will delete the user from the system (hard or soft deletion) +func (s AccountService) DeleteUser(ctx context.Context, commonID userValueObjects.CommonID, hardDelete bool) (time.Time, error) { + s.Logger.Info("Deleting user by commonID", slog.String("commonID", commonID.String()), slog.Bool("hardDelete", hardDelete)) + + switch { + case !hardDelete: + deletedAt, err := s.AccountRepository.SoftDeleteAccountByCommonID(ctx, commonID) + if err != nil { + return time.Time{}, err + } + return deletedAt, nil + default: + deletedAt, err := s.AccountRepository.HardDeleteAccountByCommonID(ctx, commonID) + if err != nil { + return time.Time{}, err + } + + return deletedAt, nil + } +} diff --git a/apps/services/accounts-graphql/cmd/server/main.go b/apps/services/accounts-graphql/cmd/server/main.go index 122f78c..60f98ff 100644 --- a/apps/services/accounts-graphql/cmd/server/main.go +++ b/apps/services/accounts-graphql/cmd/server/main.go @@ -82,7 +82,9 @@ func run() error { } // Set up all ConnectRPC Handlers - srv := handler.New(generated.NewExecutableSchema(generated.Config{Resolvers: resolvers.NewResolver(config)})) + srv := handler.New(generated.NewExecutableSchema(generated.Config{ + Resolvers: resolvers.NewResolver(params.Logger, config), + })) srv.AddTransport(transport.Options{}) srv.AddTransport(transport.GET{}) srv.AddTransport(transport.POST{}) diff --git a/apps/services/accounts-graphql/fly.toml b/apps/services/accounts-graphql/fly.toml index c37a742..4460a35 100644 --- a/apps/services/accounts-graphql/fly.toml +++ b/apps/services/accounts-graphql/fly.toml @@ -4,7 +4,7 @@ # app = 'accounts-graphql-prod' -primary_region = 'ewr' +primary_region = 'iad' [build] dockerfile = 'Dockerfile' diff --git a/apps/services/accounts-graphql/internal/graph/generated/accounts.generated.go b/apps/services/accounts-graphql/internal/graph/generated/accounts.generated.go index b8c2e6e..31b5e91 100644 --- a/apps/services/accounts-graphql/internal/graph/generated/accounts.generated.go +++ b/apps/services/accounts-graphql/internal/graph/generated/accounts.generated.go @@ -275,7 +275,7 @@ func (ec *executionContext) _AccountInterface(ctx context.Context, sel ast.Selec // region **************************** object.gotpl **************************** -var accountImplementors = []string{"Account", "AccountInterface"} +var accountImplementors = []string{"Account", "AccountInterface", "_Entity"} func (ec *executionContext) _Account(ctx context.Context, sel ast.SelectionSet, obj *models.Account) graphql.Marshaler { fields := graphql.CollectFields(ec.OperationContext, sel, accountImplementors) @@ -333,6 +333,20 @@ func (ec *executionContext) _Account(ctx context.Context, sel ast.SelectionSet, // region ***************************** type.gotpl ***************************** +func (ec *executionContext) marshalNAccount2appsᚋservicesᚋaccountsᚑgraphqlᚋinternalᚋgraphᚋmodelsᚐAccount(ctx context.Context, sel ast.SelectionSet, v models.Account) graphql.Marshaler { + return ec._Account(ctx, sel, &v) +} + +func (ec *executionContext) marshalNAccount2ᚖappsᚋservicesᚋaccountsᚑgraphqlᚋinternalᚋgraphᚋmodelsᚐAccount(ctx context.Context, sel ast.SelectionSet, v *models.Account) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + return ec._Account(ctx, sel, v) +} + func (ec *executionContext) unmarshalNRetrieveAccountInput2appsᚋservicesᚋaccountsᚑgraphqlᚋinternalᚋgraphᚋmodelsᚐRetrieveAccountInput(ctx context.Context, v any) (models.RetrieveAccountInput, error) { res, err := ec.unmarshalInputRetrieveAccountInput(ctx, v) return res, graphql.ErrorOnPath(ctx, err) diff --git a/apps/services/accounts-graphql/internal/graph/generated/directives.generated.go b/apps/services/accounts-graphql/internal/graph/generated/directives.generated.go index 88f21af..5a57ef6 100644 --- a/apps/services/accounts-graphql/internal/graph/generated/directives.generated.go +++ b/apps/services/accounts-graphql/internal/graph/generated/directives.generated.go @@ -54,6 +54,59 @@ func (ec *executionContext) marshalNFieldSet2string(ctx context.Context, sel ast return res } +func (ec *executionContext) unmarshalN_Any2map(ctx context.Context, v any) (map[string]any, error) { + res, err := graphql.UnmarshalMap(v) + return res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalN_Any2map(ctx context.Context, sel ast.SelectionSet, v map[string]any) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + res := graphql.MarshalMap(v) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res +} + +func (ec *executionContext) unmarshalN_Any2ᚕmapᚄ(ctx context.Context, v any) ([]map[string]any, error) { + var vSlice []any + if v != nil { + vSlice = graphql.CoerceList(v) + } + var err error + res := make([]map[string]any, len(vSlice)) + for i := range vSlice { + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithIndex(i)) + res[i], err = ec.unmarshalN_Any2map(ctx, vSlice[i]) + if err != nil { + return nil, err + } + } + return res, nil +} + +func (ec *executionContext) marshalN_Any2ᚕmapᚄ(ctx context.Context, sel ast.SelectionSet, v []map[string]any) graphql.Marshaler { + ret := make(graphql.Array, len(v)) + for i := range v { + ret[i] = ec.marshalN_Any2map(ctx, sel, v[i]) + } + + for _, e := range ret { + if e == graphql.Null { + return graphql.Null + } + } + + return ret +} + func (ec *executionContext) unmarshalNfederation__Policy2string(ctx context.Context, v any) (string, error) { res, err := graphql.UnmarshalString(v) return res, graphql.ErrorOnPath(ctx, err) diff --git a/apps/services/accounts-graphql/internal/graph/generated/entity.generated.go b/apps/services/accounts-graphql/internal/graph/generated/entity.generated.go index a729eb8..98c8b63 100644 --- a/apps/services/accounts-graphql/internal/graph/generated/entity.generated.go +++ b/apps/services/accounts-graphql/internal/graph/generated/entity.generated.go @@ -3,22 +3,53 @@ package generated import ( + "apps/services/accounts-graphql/internal/graph/models" "context" "errors" + "fmt" "strconv" + "sync" "sync/atomic" "github.com/99designs/gqlgen/graphql" "github.com/99designs/gqlgen/plugin/federation/fedruntime" + "github.com/google/uuid" "github.com/vektah/gqlparser/v2/ast" ) // region ************************** generated!.gotpl ************************** +type EntityResolver interface { + FindAccountByID(ctx context.Context, id uuid.UUID) (*models.Account, error) +} + // endregion ************************** generated!.gotpl ************************** // region ***************************** args.gotpl ***************************** +func (ec *executionContext) field_Entity_findAccountByID_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := ec.field_Entity_findAccountByID_argsID(ctx, rawArgs) + if err != nil { + return nil, err + } + args["id"] = arg0 + return args, nil +} +func (ec *executionContext) field_Entity_findAccountByID_argsID( + ctx context.Context, + rawArgs map[string]any, +) (uuid.UUID, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("id")) + if tmp, ok := rawArgs["id"]; ok { + return ec.unmarshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, tmp) + } + + var zeroVal uuid.UUID + return zeroVal, nil +} + // endregion ***************************** args.gotpl ***************************** // region ************************** directives.gotpl ************************** @@ -27,6 +58,71 @@ import ( // region **************************** field.gotpl ***************************** +func (ec *executionContext) _Entity_findAccountByID(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Entity_findAccountByID(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Entity().FindAccountByID(rctx, fc.Args["id"].(uuid.UUID)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*models.Account) + fc.Result = res + return ec.marshalNAccount2ᚖappsᚋservicesᚋaccountsᚑgraphqlᚋinternalᚋgraphᚋmodelsᚐAccount(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Entity_findAccountByID(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Entity", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + switch field.Name { + case "id": + return ec.fieldContext_Account_id(ctx, field) + case "emailAddress": + return ec.fieldContext_Account_emailAddress(ctx, field) + case "createdAt": + return ec.fieldContext_Account_createdAt(ctx, field) + case "updatedAt": + return ec.fieldContext_Account_updatedAt(ctx, field) + } + return nil, fmt.Errorf("no field named %q was found under type Account", field.Name) + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Entity_findAccountByID_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) __Service_sdl(ctx context.Context, field graphql.CollectedField, obj *fedruntime.Service) (ret graphql.Marshaler) { fc, err := ec.fieldContext__Service_sdl(ctx, field) if err != nil { @@ -76,10 +172,90 @@ func (ec *executionContext) fieldContext__Service_sdl(_ context.Context, field g // region ************************** interface.gotpl *************************** +func (ec *executionContext) __Entity(ctx context.Context, sel ast.SelectionSet, obj fedruntime.Entity) graphql.Marshaler { + switch obj := (obj).(type) { + case nil: + return graphql.Null + case models.Account: + return ec._Account(ctx, sel, &obj) + case *models.Account: + if obj == nil { + return graphql.Null + } + return ec._Account(ctx, sel, obj) + default: + panic(fmt.Errorf("unexpected type %T", obj)) + } +} + // endregion ************************** interface.gotpl *************************** // region **************************** object.gotpl **************************** +var entityImplementors = []string{"Entity"} + +func (ec *executionContext) _Entity(ctx context.Context, sel ast.SelectionSet) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, entityImplementors) + ctx = graphql.WithFieldContext(ctx, &graphql.FieldContext{ + Object: "Entity", + }) + + out := graphql.NewFieldSet(fields) + deferred := make(map[string]*graphql.FieldSet) + for i, field := range fields { + innerCtx := graphql.WithRootFieldContext(ctx, &graphql.RootFieldContext{ + Object: field.Name, + Field: field, + }) + + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("Entity") + case "findAccountByID": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Entity_findAccountByID(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch(ctx) + if out.Invalids > 0 { + return graphql.Null + } + + atomic.AddInt32(&ec.deferred, int32(len(deferred))) + + for label, dfs := range deferred { + ec.processDeferredGroup(graphql.DeferredGroup{ + Label: label, + Path: graphql.GetPath(ctx), + FieldSet: dfs, + Context: ctx, + }) + } + + return out +} + var _ServiceImplementors = []string{"_Service"} func (ec *executionContext) __Service(ctx context.Context, sel ast.SelectionSet, obj *fedruntime.Service) graphql.Marshaler { @@ -120,8 +296,53 @@ func (ec *executionContext) __Service(ctx context.Context, sel ast.SelectionSet, // region ***************************** type.gotpl ***************************** +func (ec *executionContext) marshalN_Entity2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋpluginᚋfederationᚋfedruntimeᚐEntity(ctx context.Context, sel ast.SelectionSet, v []fedruntime.Entity) graphql.Marshaler { + ret := make(graphql.Array, len(v)) + var wg sync.WaitGroup + isLen1 := len(v) == 1 + if !isLen1 { + wg.Add(len(v)) + } + for i := range v { + i := i + fc := &graphql.FieldContext{ + Index: &i, + Result: &v[i], + } + ctx := graphql.WithFieldContext(ctx, fc) + f := func(i int) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = nil + } + }() + if !isLen1 { + defer wg.Done() + } + ret[i] = ec.marshalO_Entity2githubᚗcomᚋ99designsᚋgqlgenᚋpluginᚋfederationᚋfedruntimeᚐEntity(ctx, sel, v[i]) + } + if isLen1 { + f(i) + } else { + go f(i) + } + + } + wg.Wait() + + return ret +} + func (ec *executionContext) marshalN_Service2githubᚗcomᚋ99designsᚋgqlgenᚋpluginᚋfederationᚋfedruntimeᚐService(ctx context.Context, sel ast.SelectionSet, v fedruntime.Service) graphql.Marshaler { return ec.__Service(ctx, sel, &v) } +func (ec *executionContext) marshalO_Entity2githubᚗcomᚋ99designsᚋgqlgenᚋpluginᚋfederationᚋfedruntimeᚐEntity(ctx context.Context, sel ast.SelectionSet, v fedruntime.Entity) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec.__Entity(ctx, sel, v) +} + // endregion ***************************** type.gotpl ***************************** diff --git a/apps/services/accounts-graphql/internal/graph/generated/federation.go b/apps/services/accounts-graphql/internal/graph/generated/federation.go index 1116a02..8d6f100 100644 --- a/apps/services/accounts-graphql/internal/graph/generated/federation.go +++ b/apps/services/accounts-graphql/internal/graph/generated/federation.go @@ -5,7 +5,9 @@ package generated import ( "context" "errors" + "fmt" "strings" + "sync" "github.com/99designs/gqlgen/plugin/federation/fedruntime" ) @@ -33,3 +35,200 @@ func (ec *executionContext) __resolve__service(ctx context.Context) (fedruntime. SDL: strings.Join(sdl, "\n"), }, nil } + +func (ec *executionContext) __resolve_entities(ctx context.Context, representations []map[string]any) []fedruntime.Entity { + list := make([]fedruntime.Entity, len(representations)) + + repsMap := ec.buildRepresentationGroups(ctx, representations) + + switch len(repsMap) { + case 0: + return list + case 1: + for typeName, reps := range repsMap { + ec.resolveEntityGroup(ctx, typeName, reps, list) + } + return list + default: + var g sync.WaitGroup + g.Add(len(repsMap)) + for typeName, reps := range repsMap { + go func(typeName string, reps []EntityWithIndex) { + ec.resolveEntityGroup(ctx, typeName, reps, list) + g.Done() + }(typeName, reps) + } + g.Wait() + return list + } +} + +type EntityWithIndex struct { + // The index in the original representation array + index int + entity EntityRepresentation +} + +// EntityRepresentation is the JSON representation of an entity sent by the Router +// used as the inputs for us to resolve. +// +// We make it a map because we know the top level JSON is always an object. +type EntityRepresentation map[string]any + +// We group entities by typename so that we can parallelize their resolution. +// This is particularly helpful when there are entity groups in multi mode. +func (ec *executionContext) buildRepresentationGroups( + ctx context.Context, + representations []map[string]any, +) map[string][]EntityWithIndex { + repsMap := make(map[string][]EntityWithIndex) + for i, rep := range representations { + typeName, ok := rep["__typename"].(string) + if !ok { + // If there is no __typename, we just skip the representation; + // we just won't be resolving these unknown types. + ec.Error(ctx, errors.New("__typename must be an existing string")) + continue + } + + repsMap[typeName] = append(repsMap[typeName], EntityWithIndex{ + index: i, + entity: rep, + }) + } + + return repsMap +} + +func (ec *executionContext) resolveEntityGroup( + ctx context.Context, + typeName string, + reps []EntityWithIndex, + list []fedruntime.Entity, +) { + if isMulti(typeName) { + err := ec.resolveManyEntities(ctx, typeName, reps, list) + if err != nil { + ec.Error(ctx, err) + } + } else { + // if there are multiple entities to resolve, parallelize (similar to + // graphql.FieldSet.Dispatch) + var e sync.WaitGroup + e.Add(len(reps)) + for i, rep := range reps { + i, rep := i, rep + go func(i int, rep EntityWithIndex) { + entity, err := ec.resolveEntity(ctx, typeName, rep.entity) + if err != nil { + ec.Error(ctx, err) + } else { + list[rep.index] = entity + } + e.Done() + }(i, rep) + } + e.Wait() + } +} + +func isMulti(typeName string) bool { + switch typeName { + default: + return false + } +} + +func (ec *executionContext) resolveEntity( + ctx context.Context, + typeName string, + rep EntityRepresentation, +) (e fedruntime.Entity, err error) { + // we need to do our own panic handling, because we may be called in a + // goroutine, where the usual panic handling can't catch us + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + } + }() + + switch typeName { + case "Account": + resolverName, err := entityResolverNameForAccount(ctx, rep) + if err != nil { + return nil, fmt.Errorf(`finding resolver for Entity "Account": %w`, err) + } + switch resolverName { + + case "findAccountByID": + id0, err := ec.unmarshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, rep["id"]) + if err != nil { + return nil, fmt.Errorf(`unmarshalling param 0 for findAccountByID(): %w`, err) + } + entity, err := ec.resolvers.Entity().FindAccountByID(ctx, id0) + if err != nil { + return nil, fmt.Errorf(`resolving Entity "Account": %w`, err) + } + + return entity, nil + } + + } + return nil, fmt.Errorf("%w: %s", ErrUnknownType, typeName) +} + +func (ec *executionContext) resolveManyEntities( + ctx context.Context, + typeName string, + reps []EntityWithIndex, + list []fedruntime.Entity, +) (err error) { + // we need to do our own panic handling, because we may be called in a + // goroutine, where the usual panic handling can't catch us + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + } + }() + + switch typeName { + + default: + return errors.New("unknown type: " + typeName) + } +} + +func entityResolverNameForAccount(ctx context.Context, rep EntityRepresentation) (string, error) { + // we collect errors because a later entity resolver may work fine + // when an entity has multiple keys + entityResolverErrs := []error{} + for { + var ( + m EntityRepresentation + val any + ok bool + ) + _ = val + // if all of the KeyFields values for this resolver are null, + // we shouldn't use use it + allNull := true + m = rep + val, ok = m["id"] + if !ok { + entityResolverErrs = append(entityResolverErrs, + fmt.Errorf("%w due to missing Key Field \"id\" for Account", ErrTypeNotFound)) + break + } + if allNull { + allNull = val == nil + } + if allNull { + entityResolverErrs = append(entityResolverErrs, + fmt.Errorf("%w due to all null value KeyFields for Account", ErrTypeNotFound)) + break + } + return "findAccountByID", nil + } + return "", fmt.Errorf("%w for Account due to %v", ErrTypeNotFound, + errors.Join(entityResolverErrs...).Error()) +} diff --git a/apps/services/accounts-graphql/internal/graph/generated/root_.generated.go b/apps/services/accounts-graphql/internal/graph/generated/root_.generated.go index 1ee436e..f6af46b 100644 --- a/apps/services/accounts-graphql/internal/graph/generated/root_.generated.go +++ b/apps/services/accounts-graphql/internal/graph/generated/root_.generated.go @@ -34,6 +34,7 @@ type Config struct { } type ResolverRoot interface { + Entity() EntityResolver Mutation() MutationResolver Query() QueryResolver } @@ -49,8 +50,13 @@ type ComplexityRoot struct { UpdatedAt func(childComplexity int) int } + Entity struct { + FindAccountByID func(childComplexity int, id uuid.UUID) int + } + Mutation struct { - Empty func(childComplexity int) int + DeleteAccount func(childComplexity int, commonID uuid.UUID) int + Empty func(childComplexity int) int } Query struct { @@ -58,6 +64,7 @@ type ComplexityRoot struct { Empty func(childComplexity int) int Viewer func(childComplexity int, commonID uuid.UUID) int __resolve__service func(childComplexity int) int + __resolve_entities func(childComplexity int, representations []map[string]any) int } Viewer struct { @@ -120,6 +127,30 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Account.UpdatedAt(childComplexity), true + case "Entity.findAccountByID": + if e.complexity.Entity.FindAccountByID == nil { + break + } + + args, err := ec.field_Entity_findAccountByID_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Entity.FindAccountByID(childComplexity, args["id"].(uuid.UUID)), true + + case "Mutation.deleteAccount": + if e.complexity.Mutation.DeleteAccount == nil { + break + } + + args, err := ec.field_Mutation_deleteAccount_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Mutation.DeleteAccount(childComplexity, args["commonID"].(uuid.UUID)), true + case "Mutation.empty": if e.complexity.Mutation.Empty == nil { break @@ -165,6 +196,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Query.__resolve__service(childComplexity), true + case "Query._entities": + if e.complexity.Query.__resolve_entities == nil { + break + } + + args, err := ec.field_Query__entities_args(context.TODO(), rawArgs) + if err != nil { + return 0, false + } + + return e.complexity.Query.__resolve_entities(childComplexity, args["representations"].([]map[string]any)), true + case "Viewer.createdAt": if e.complexity.Viewer.CreatedAt == nil { break @@ -340,7 +383,7 @@ interface AccountInterface { Account has default account interface with additional account properties """ -type Account implements AccountInterface { +type Account implements AccountInterface @key(fields: "id") { """ The unique identifier for the account """ @@ -394,8 +437,18 @@ input RetrieveAccountInput { } extend type Query { + """ + Obtains the account by commonID or email address + """ account(input: RetrieveAccountInput!): Account } + +extend type Mutation { + """ + Delete account by commonID + """ + deleteAccount(commonID: UUID!): Time! +} `, BuiltIn: false}, {Name: "../schemas/schema.graphql", Input: `# GraphQL schema example # @@ -500,11 +553,20 @@ type Mutation { scalar federation__Scope `, BuiltIn: true}, {Name: "../../../federation/entity.graphql", Input: ` +# a union of all types that use the @key directive +union _Entity = Account + +# fake type to build resolver interfaces for users to implement +type Entity { + findAccountByID(id: UUID!,): Account! +} + type _Service { sdl: String } extend type Query { + _entities(representations: [_Any!]!): [_Entity]! _service: _Service! } `, BuiltIn: true}, diff --git a/apps/services/accounts-graphql/internal/graph/generated/schema.generated.go b/apps/services/accounts-graphql/internal/graph/generated/schema.generated.go index de94bf0..98839fb 100644 --- a/apps/services/accounts-graphql/internal/graph/generated/schema.generated.go +++ b/apps/services/accounts-graphql/internal/graph/generated/schema.generated.go @@ -22,6 +22,7 @@ import ( type MutationResolver interface { Empty(ctx context.Context) (bool, error) + DeleteAccount(ctx context.Context, commonID uuid.UUID) (*time.Time, error) } type QueryResolver interface { Empty(ctx context.Context) (bool, error) @@ -33,6 +34,29 @@ type QueryResolver interface { // region ***************************** args.gotpl ***************************** +func (ec *executionContext) field_Mutation_deleteAccount_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := ec.field_Mutation_deleteAccount_argsCommonID(ctx, rawArgs) + if err != nil { + return nil, err + } + args["commonID"] = arg0 + return args, nil +} +func (ec *executionContext) field_Mutation_deleteAccount_argsCommonID( + ctx context.Context, + rawArgs map[string]any, +) (uuid.UUID, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("commonID")) + if tmp, ok := rawArgs["commonID"]; ok { + return ec.unmarshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx, tmp) + } + + var zeroVal uuid.UUID + return zeroVal, nil +} + func (ec *executionContext) field_Query___type_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -56,6 +80,29 @@ func (ec *executionContext) field_Query___type_argsName( return zeroVal, nil } +func (ec *executionContext) field_Query__entities_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { + var err error + args := map[string]any{} + arg0, err := ec.field_Query__entities_argsRepresentations(ctx, rawArgs) + if err != nil { + return nil, err + } + args["representations"] = arg0 + return args, nil +} +func (ec *executionContext) field_Query__entities_argsRepresentations( + ctx context.Context, + rawArgs map[string]any, +) ([]map[string]any, error) { + ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("representations")) + if tmp, ok := rawArgs["representations"]; ok { + return ec.unmarshalN_Any2ᚕmapᚄ(ctx, tmp) + } + + var zeroVal []map[string]any + return zeroVal, nil +} + func (ec *executionContext) field_Query_account_args(ctx context.Context, rawArgs map[string]any) (map[string]any, error) { var err error args := map[string]any{} @@ -154,6 +201,61 @@ func (ec *executionContext) fieldContext_Mutation_empty(_ context.Context, field return fc, nil } +func (ec *executionContext) _Mutation_deleteAccount(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Mutation_deleteAccount(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.resolvers.Mutation().DeleteAccount(rctx, fc.Args["commonID"].(uuid.UUID)) + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.(*time.Time) + fc.Result = res + return ec.marshalNTime2ᚖtimeᚐTime(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Mutation_deleteAccount(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Mutation", + Field: field, + IsMethod: true, + IsResolver: true, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type Time does not have child fields") + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Mutation_deleteAccount_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _Query_empty(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Query_empty(ctx, field) if err != nil { @@ -324,6 +426,61 @@ func (ec *executionContext) fieldContext_Query_account(ctx context.Context, fiel return fc, nil } +func (ec *executionContext) _Query__entities(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { + fc, err := ec.fieldContext_Query__entities(ctx, field) + if err != nil { + return graphql.Null + } + ctx = graphql.WithFieldContext(ctx, fc) + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (any, error) { + ctx = rctx // use context from middleware stack in children + return ec.__resolve_entities(ctx, fc.Args["representations"].([]map[string]any)), nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.([]fedruntime.Entity) + fc.Result = res + return ec.marshalN_Entity2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋpluginᚋfederationᚋfedruntimeᚐEntity(ctx, field.Selections, res) +} + +func (ec *executionContext) fieldContext_Query__entities(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + fc = &graphql.FieldContext{ + Object: "Query", + Field: field, + IsMethod: true, + IsResolver: false, + Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return nil, errors.New("field of type _Entity does not have child fields") + }, + } + defer func() { + if r := recover(); r != nil { + err = ec.Recover(ctx, r) + ec.Error(ctx, err) + } + }() + ctx = graphql.WithFieldContext(ctx, fc) + if fc.Args, err = ec.field_Query__entities_args(ctx, field.ArgumentMap(ec.Variables)); err != nil { + ec.Error(ctx, err) + return fc, err + } + return fc, nil +} + func (ec *executionContext) _Query__service(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { fc, err := ec.fieldContext_Query__service(ctx, field) if err != nil { @@ -759,6 +916,13 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet) if out.Values[i] == graphql.Null { out.Invalids++ } + case "deleteAccount": + out.Values[i] = ec.OperationContext.RootResolverMiddleware(innerCtx, func(ctx context.Context) (res graphql.Marshaler) { + return ec._Mutation_deleteAccount(ctx, field) + }) + if out.Values[i] == graphql.Null { + out.Invalids++ + } default: panic("unknown field " + strconv.Quote(field.Name)) } @@ -860,6 +1024,28 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) + case "_entities": + field := field + + innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + } + }() + res = ec._Query__entities(ctx, field) + if res == graphql.Null { + atomic.AddUint32(&fs.Invalids, 1) + } + return res + } + + rrm := func(ctx context.Context) graphql.Marshaler { + return ec.OperationContext.RootResolverMiddleware(ctx, + func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) }) + } + out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) }) case "_service": field := field @@ -992,6 +1178,27 @@ func (ec *executionContext) marshalNTime2timeᚐTime(ctx context.Context, sel as return res } +func (ec *executionContext) unmarshalNTime2ᚖtimeᚐTime(ctx context.Context, v any) (*time.Time, error) { + res, err := graphql.UnmarshalTime(v) + return &res, graphql.ErrorOnPath(ctx, err) +} + +func (ec *executionContext) marshalNTime2ᚖtimeᚐTime(ctx context.Context, sel ast.SelectionSet, v *time.Time) graphql.Marshaler { + if v == nil { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + return graphql.Null + } + res := graphql.MarshalTime(*v) + if res == graphql.Null { + if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) { + ec.Errorf(ctx, "the requested element is null which the schema does not allow") + } + } + return res +} + func (ec *executionContext) unmarshalNUUID2githubᚗcomᚋgoogleᚋuuidᚐUUID(ctx context.Context, v any) (uuid.UUID, error) { res, err := graphql.UnmarshalUUID(v) return res, graphql.ErrorOnPath(ctx, err) diff --git a/apps/services/accounts-graphql/internal/graph/models/models_gen.go b/apps/services/accounts-graphql/internal/graph/models/models_gen.go index 86152c6..28b25ae 100644 --- a/apps/services/accounts-graphql/internal/graph/models/models_gen.go +++ b/apps/services/accounts-graphql/internal/graph/models/models_gen.go @@ -49,6 +49,8 @@ func (this Account) GetCreatedAt() time.Time { return this.CreatedAt } // The updatedAt time of the account func (this Account) GetUpdatedAt() time.Time { return this.UpdatedAt } +func (Account) IsEntity() {} + type Mutation struct { } diff --git a/apps/services/accounts-graphql/internal/graph/resolvers/accounts.resolvers.go b/apps/services/accounts-graphql/internal/graph/resolvers/accounts.resolvers.go index 5ac2c08..740c6de 100644 --- a/apps/services/accounts-graphql/internal/graph/resolvers/accounts.resolvers.go +++ b/apps/services/accounts-graphql/internal/graph/resolvers/accounts.resolvers.go @@ -9,13 +9,36 @@ import ( "context" "fmt" accountsapiv1 "libs/backend/proto-gen/go/accounts/accountsapi/v1" + "log/slog" + "time" "connectrpc.com/connect" "github.com/google/uuid" ) +// DeleteAccount is the resolver for the deleteAccount field. +func (r *mutationResolver) DeleteAccount(ctx context.Context, commonID uuid.UUID) (*time.Time, error) { + r.Logger.Info("Deleting account", slog.String("commonID", commonID.String())) + + // Call Accounts API + const hardDelete = false + resp, err := r.AccountsAPIClient.DeleteAccount(ctx, connect.NewRequest(&accountsapiv1.DeleteAccountRequest{ + CommonId: commonID.String(), + HardDelete: hardDelete, + })) + + if err != nil { + return nil, fmt.Errorf("could not delete account: %w", err) + } + + deletedAtUTC := resp.Msg.DeletedAt.AsTime().UTC() + return &deletedAtUTC, nil +} + // Account is the resolver for the account field. func (r *queryResolver) Account(ctx context.Context, input models.RetrieveAccountInput) (*models.Account, error) { + loggerValues := make([]any, 0) + // Call the Accounts API commonID := input.CommonID emailAddress := input.EmailAddress @@ -27,10 +50,15 @@ func (r *queryResolver) Account(ctx context.Context, input models.RetrieveAccoun case commonID != nil: commonIDStr := commonID.String() commonIDForSearch = &commonIDStr + loggerValues = append(loggerValues, slog.String("commonID", commonIDStr)) case emailAddress != nil: emailAddressForSearch = emailAddress + loggerValues = append(loggerValues, slog.String("emailAddress", *emailAddress)) } + r.Logger.Info("Fetching account", loggerValues...) + + // Call the accounts API resp, err := r.AccountsAPIClient.GetAccount(ctx, connect.NewRequest(&accountsapiv1.GetAccountRequest{ CommonId: commonIDForSearch, EmailAddress: emailAddressForSearch, diff --git a/apps/services/accounts-graphql/internal/graph/resolvers/entity.resolvers.go b/apps/services/accounts-graphql/internal/graph/resolvers/entity.resolvers.go new file mode 100644 index 0000000..b49789b --- /dev/null +++ b/apps/services/accounts-graphql/internal/graph/resolvers/entity.resolvers.go @@ -0,0 +1,36 @@ +package resolvers + +// This file will be automatically regenerated based on the schema, any resolver implementations +// will be copied through when generating and any unknown code will be moved to the end. +// Code generated by github.com/99designs/gqlgen version v0.17.61 + +import ( + "apps/services/accounts-graphql/internal/graph/generated" + "apps/services/accounts-graphql/internal/graph/models" + "context" + "fmt" + + "github.com/google/uuid" +) + +// FindAccountByID is the resolver for the findAccountByID field. +func (r *entityResolver) FindAccountByID(ctx context.Context, id uuid.UUID) (*models.Account, error) { + panic(fmt.Errorf("not implemented: FindAccountByID - findAccountByID")) +} + +// Entity returns generated.EntityResolver implementation. +func (r *Resolver) Entity() generated.EntityResolver { return &entityResolver{r} } + +type entityResolver struct{ *Resolver } + +// !!! WARNING !!! +// The code below was going to be deleted when updating resolvers. It has been copied here so you have +// one last chance to move it out of harms way if you want. There are two reasons this happens: +// - When renaming or deleting a resolver the old code will be put in here. You can safely delete +// it when you're done. +// - You have helper methods in this file. Move them out to keep these resolver files clean. +/* + func (r *entityResolver) FindViewerByID(ctx context.Context, id uuid.UUID) (*models.Viewer, error) { + panic(fmt.Errorf("not implemented: FindViewerByID - findViewerByID")) +} +*/ diff --git a/apps/services/accounts-graphql/internal/graph/resolvers/resolver.go b/apps/services/accounts-graphql/internal/graph/resolvers/resolver.go index d7263b6..28ba4d7 100644 --- a/apps/services/accounts-graphql/internal/graph/resolvers/resolver.go +++ b/apps/services/accounts-graphql/internal/graph/resolvers/resolver.go @@ -2,6 +2,7 @@ package resolvers import ( "apps/services/accounts-graphql/internal/config" + "libs/backend/boot" "libs/backend/proto-gen/go/accounts/accountsapi/v1/accountsapiv1connect" "net/http" @@ -15,10 +16,11 @@ import ( // It serves as dependency injection for your app, add any dependencies you require here. type Resolver struct { + Logger boot.Logger AccountsAPIClient accountsapiv1connect.AccountServiceClient } -func NewResolver(config config.Config) *Resolver { +func NewResolver(logger boot.Logger, config config.Config) *Resolver { // Set up Accounts API Client accountsAPIClient := accountsapiv1connect.NewAccountServiceClient( http.DefaultClient, @@ -27,6 +29,7 @@ func NewResolver(config config.Config) *Resolver { ) return &Resolver{ + Logger: logger, AccountsAPIClient: accountsAPIClient, } } diff --git a/apps/services/accounts-graphql/internal/graph/schemas/accounts.graphql b/apps/services/accounts-graphql/internal/graph/schemas/accounts.graphql index 0eb1ec9..9cfc455 100644 --- a/apps/services/accounts-graphql/internal/graph/schemas/accounts.graphql +++ b/apps/services/accounts-graphql/internal/graph/schemas/accounts.graphql @@ -25,7 +25,7 @@ interface AccountInterface { Account has default account interface with additional account properties """ -type Account implements AccountInterface { +type Account implements AccountInterface @key(fields: "id") { """ The unique identifier for the account """ @@ -79,5 +79,15 @@ input RetrieveAccountInput { } extend type Query { + """ + Obtains the account by commonID or email address + """ account(input: RetrieveAccountInput!): Account } + +extend type Mutation { + """ + Delete account by commonID + """ + deleteAccount(commonID: UUID!): Time! +} diff --git a/apps/services/accounts-worker/fly.toml b/apps/services/accounts-worker/fly.toml index ed28c15..8b6fb4f 100644 --- a/apps/services/accounts-worker/fly.toml +++ b/apps/services/accounts-worker/fly.toml @@ -1,7 +1,7 @@ app = 'accounts-worker-prod' [[services]] -primary_region = 'ewr' +primary_region = 'iad' [build] dockerfile = "Dockerfile" diff --git a/apps/services/apollo-router/fly.toml b/apps/services/apollo-router/fly.toml index f7c2c08..45dc249 100644 --- a/apps/services/apollo-router/fly.toml +++ b/apps/services/apollo-router/fly.toml @@ -4,7 +4,7 @@ # app = 'apollo-router-prod' -primary_region = 'ewr' +primary_region = 'iad' [build] dockerfile = 'Dockerfile' diff --git a/apps/services/apollo-router/supergraph-prod.graphql b/apps/services/apollo-router/supergraph-prod.graphql index a27f55d..59d64b2 100644 --- a/apps/services/apollo-router/supergraph-prod.graphql +++ b/apps/services/apollo-router/supergraph-prod.graphql @@ -20,18 +20,21 @@ directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on IN Account has default account interface with additional account properties """ -type Account implements AccountInterface { +type Account implements AccountInterface + @join__owner(graph: ACCOUNTS) + @join__type(graph: ACCOUNTS, key: "id") +{ """The createdAt time of the account""" - createdAt: Time! + createdAt: Time! @join__field(graph: ACCOUNTS) """The email address of the account""" - emailAddress: String! + emailAddress: String! @join__field(graph: ACCOUNTS) """The unique identifier for the account""" - id: UUID! + id: UUID! @join__field(graph: ACCOUNTS) """The updatedAt time of the account""" - updatedAt: Time! + updatedAt: Time! @join__field(graph: ACCOUNTS) } """ @@ -53,10 +56,13 @@ interface AccountInterface { } type Mutation { + """Delete account by commonID""" + deleteAccount(commonID: UUID!): Time! @join__field(graph: ACCOUNTS) empty: Boolean! @join__field(graph: ACCOUNTS) } type Query { + """Obtains the account by commonID or email address""" account(input: RetrieveAccountInput!): Account @join__field(graph: ACCOUNTS) empty: Boolean! @join__field(graph: ACCOUNTS) diff --git a/apps/services/inbound-webhooks-api/fly.toml b/apps/services/inbound-webhooks-api/fly.toml index 598ff43..4f926fe 100644 --- a/apps/services/inbound-webhooks-api/fly.toml +++ b/apps/services/inbound-webhooks-api/fly.toml @@ -4,7 +4,7 @@ # app = 'inbound-webhooks-api-prod' -primary_region = 'ewr' +primary_region = 'iad' [build] dockerfile = "Dockerfile" diff --git a/graphql/supergraph-dev.graphql b/graphql/supergraph-dev.graphql index ef70776..564c51f 100644 --- a/graphql/supergraph-dev.graphql +++ b/graphql/supergraph-dev.graphql @@ -20,18 +20,21 @@ directive @join__type(graph: join__Graph!, key: join__FieldSet) repeatable on IN Account has default account interface with additional account properties """ -type Account implements AccountInterface { +type Account implements AccountInterface + @join__owner(graph: ACCOUNTS) + @join__type(graph: ACCOUNTS, key: "id") +{ """The createdAt time of the account""" - createdAt: Time! + createdAt: Time! @join__field(graph: ACCOUNTS) """The email address of the account""" - emailAddress: String! + emailAddress: String! @join__field(graph: ACCOUNTS) """The unique identifier for the account""" - id: UUID! + id: UUID! @join__field(graph: ACCOUNTS) """The updatedAt time of the account""" - updatedAt: Time! + updatedAt: Time! @join__field(graph: ACCOUNTS) } """ @@ -53,10 +56,13 @@ interface AccountInterface { } type Mutation { + """Delete account by commonID""" + deleteAccount(commonID: UUID!): Time! @join__field(graph: ACCOUNTS) empty: Boolean! @join__field(graph: ACCOUNTS) } type Query { + """Obtains the account by commonID or email address""" account(input: RetrieveAccountInput!): Account @join__field(graph: ACCOUNTS) empty: Boolean! @join__field(graph: ACCOUNTS) diff --git a/libs/backend/proto-gen/go/accounts/accountsapi/v1/accountsapiv1connect/api.connect.go b/libs/backend/proto-gen/go/accounts/accountsapi/v1/accountsapiv1connect/api.connect.go index c03e4ef..c8a955f 100644 --- a/libs/backend/proto-gen/go/accounts/accountsapi/v1/accountsapiv1connect/api.connect.go +++ b/libs/backend/proto-gen/go/accounts/accountsapi/v1/accountsapiv1connect/api.connect.go @@ -39,6 +39,9 @@ const ( // AccountServiceGetAccountProcedure is the fully-qualified name of the AccountService's GetAccount // RPC. AccountServiceGetAccountProcedure = "/accounts.accountsapi.v1.AccountService/GetAccount" + // AccountServiceDeleteAccountProcedure is the fully-qualified name of the AccountService's + // DeleteAccount RPC. + AccountServiceDeleteAccountProcedure = "/accounts.accountsapi.v1.AccountService/DeleteAccount" ) // These variables are the protoreflect.Descriptor objects for the RPCs defined in this package. @@ -46,6 +49,7 @@ var ( accountServiceServiceDescriptor = v1.File_accounts_accountsapi_v1_api_proto.Services().ByName("AccountService") accountServiceCreateAccountMethodDescriptor = accountServiceServiceDescriptor.Methods().ByName("CreateAccount") accountServiceGetAccountMethodDescriptor = accountServiceServiceDescriptor.Methods().ByName("GetAccount") + accountServiceDeleteAccountMethodDescriptor = accountServiceServiceDescriptor.Methods().ByName("DeleteAccount") ) // AccountServiceClient is a client for the accounts.accountsapi.v1.AccountService service. @@ -54,6 +58,8 @@ type AccountServiceClient interface { CreateAccount(context.Context, *connect.Request[v1.CreateAccountRequest]) (*connect.Response[v1.CreateAcountResponse], error) // GetAccount retrieves an account by its common id GetAccount(context.Context, *connect.Request[v1.GetAccountRequest]) (*connect.Response[v1.GetAccountResponse], error) + // Delete Account will soft/hard delete an account + DeleteAccount(context.Context, *connect.Request[v1.DeleteAccountRequest]) (*connect.Response[v1.DeleteAccountResponse], error) } // NewAccountServiceClient constructs a client for the accounts.accountsapi.v1.AccountService @@ -78,6 +84,12 @@ func NewAccountServiceClient(httpClient connect.HTTPClient, baseURL string, opts connect.WithSchema(accountServiceGetAccountMethodDescriptor), connect.WithClientOptions(opts...), ), + deleteAccount: connect.NewClient[v1.DeleteAccountRequest, v1.DeleteAccountResponse]( + httpClient, + baseURL+AccountServiceDeleteAccountProcedure, + connect.WithSchema(accountServiceDeleteAccountMethodDescriptor), + connect.WithClientOptions(opts...), + ), } } @@ -85,6 +97,7 @@ func NewAccountServiceClient(httpClient connect.HTTPClient, baseURL string, opts type accountServiceClient struct { createAccount *connect.Client[v1.CreateAccountRequest, v1.CreateAcountResponse] getAccount *connect.Client[v1.GetAccountRequest, v1.GetAccountResponse] + deleteAccount *connect.Client[v1.DeleteAccountRequest, v1.DeleteAccountResponse] } // CreateAccount calls accounts.accountsapi.v1.AccountService.CreateAccount. @@ -97,12 +110,19 @@ func (c *accountServiceClient) GetAccount(ctx context.Context, req *connect.Requ return c.getAccount.CallUnary(ctx, req) } +// DeleteAccount calls accounts.accountsapi.v1.AccountService.DeleteAccount. +func (c *accountServiceClient) DeleteAccount(ctx context.Context, req *connect.Request[v1.DeleteAccountRequest]) (*connect.Response[v1.DeleteAccountResponse], error) { + return c.deleteAccount.CallUnary(ctx, req) +} + // AccountServiceHandler is an implementation of the accounts.accountsapi.v1.AccountService service. type AccountServiceHandler interface { // CreateAccount creates a new account CreateAccount(context.Context, *connect.Request[v1.CreateAccountRequest]) (*connect.Response[v1.CreateAcountResponse], error) // GetAccount retrieves an account by its common id GetAccount(context.Context, *connect.Request[v1.GetAccountRequest]) (*connect.Response[v1.GetAccountResponse], error) + // Delete Account will soft/hard delete an account + DeleteAccount(context.Context, *connect.Request[v1.DeleteAccountRequest]) (*connect.Response[v1.DeleteAccountResponse], error) } // NewAccountServiceHandler builds an HTTP handler from the service implementation. It returns the @@ -123,12 +143,20 @@ func NewAccountServiceHandler(svc AccountServiceHandler, opts ...connect.Handler connect.WithSchema(accountServiceGetAccountMethodDescriptor), connect.WithHandlerOptions(opts...), ) + accountServiceDeleteAccountHandler := connect.NewUnaryHandler( + AccountServiceDeleteAccountProcedure, + svc.DeleteAccount, + connect.WithSchema(accountServiceDeleteAccountMethodDescriptor), + connect.WithHandlerOptions(opts...), + ) return "/accounts.accountsapi.v1.AccountService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case AccountServiceCreateAccountProcedure: accountServiceCreateAccountHandler.ServeHTTP(w, r) case AccountServiceGetAccountProcedure: accountServiceGetAccountHandler.ServeHTTP(w, r) + case AccountServiceDeleteAccountProcedure: + accountServiceDeleteAccountHandler.ServeHTTP(w, r) default: http.NotFound(w, r) } @@ -145,3 +173,7 @@ func (UnimplementedAccountServiceHandler) CreateAccount(context.Context, *connec func (UnimplementedAccountServiceHandler) GetAccount(context.Context, *connect.Request[v1.GetAccountRequest]) (*connect.Response[v1.GetAccountResponse], error) { return nil, connect.NewError(connect.CodeUnimplemented, errors.New("accounts.accountsapi.v1.AccountService.GetAccount is not implemented")) } + +func (UnimplementedAccountServiceHandler) DeleteAccount(context.Context, *connect.Request[v1.DeleteAccountRequest]) (*connect.Response[v1.DeleteAccountResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("accounts.accountsapi.v1.AccountService.DeleteAccount is not implemented")) +} diff --git a/libs/backend/proto-gen/go/accounts/accountsapi/v1/api.pb.go b/libs/backend/proto-gen/go/accounts/accountsapi/v1/api.pb.go index 5f29ada..d4384a9 100644 --- a/libs/backend/proto-gen/go/accounts/accountsapi/v1/api.pb.go +++ b/libs/backend/proto-gen/go/accounts/accountsapi/v1/api.pb.go @@ -12,6 +12,7 @@ import ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" _ "google.golang.org/protobuf/types/known/anypb" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" domain "libs/backend/proto-gen/go/accounts/domain" reflect "reflect" sync "sync" @@ -228,6 +229,104 @@ func (x *GetAccountResponse) GetAccount() *domain.Account { return nil } +// DeleteAccountRequest will hard/soft delete an account by common_id +type DeleteAccountRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + CommonId string `protobuf:"bytes,1,opt,name=common_id,json=commonId,proto3" json:"common_id,omitempty"` + HardDelete bool `protobuf:"varint,2,opt,name=hard_delete,json=hardDelete,proto3" json:"hard_delete,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteAccountRequest) Reset() { + *x = DeleteAccountRequest{} + mi := &file_accounts_accountsapi_v1_api_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteAccountRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteAccountRequest) ProtoMessage() {} + +func (x *DeleteAccountRequest) ProtoReflect() protoreflect.Message { + mi := &file_accounts_accountsapi_v1_api_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteAccountRequest.ProtoReflect.Descriptor instead. +func (*DeleteAccountRequest) Descriptor() ([]byte, []int) { + return file_accounts_accountsapi_v1_api_proto_rawDescGZIP(), []int{4} +} + +func (x *DeleteAccountRequest) GetCommonId() string { + if x != nil { + return x.CommonId + } + return "" +} + +func (x *DeleteAccountRequest) GetHardDelete() bool { + if x != nil { + return x.HardDelete + } + return false +} + +// DeleteAccountResponse will let the caller know if the server deleted the account +type DeleteAccountResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + DeletedAt *timestamppb.Timestamp `protobuf:"bytes,1,opt,name=deleted_at,json=deletedAt,proto3" json:"deleted_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteAccountResponse) Reset() { + *x = DeleteAccountResponse{} + mi := &file_accounts_accountsapi_v1_api_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteAccountResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteAccountResponse) ProtoMessage() {} + +func (x *DeleteAccountResponse) ProtoReflect() protoreflect.Message { + mi := &file_accounts_accountsapi_v1_api_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteAccountResponse.ProtoReflect.Descriptor instead. +func (*DeleteAccountResponse) Descriptor() ([]byte, []int) { + return file_accounts_accountsapi_v1_api_proto_rawDescGZIP(), []int{5} +} + +func (x *DeleteAccountResponse) GetDeletedAt() *timestamppb.Timestamp { + if x != nil { + return x.DeletedAt + } + return nil +} + var File_accounts_accountsapi_v1_api_proto protoreflect.FileDescriptor var file_accounts_accountsapi_v1_api_proto_rawDesc = []byte{ @@ -240,65 +339,85 @@ var file_accounts_accountsapi_v1_api_proto_rawDesc = []byte{ 0x65, 0x2f, 0x74, 0x79, 0x70, 0x65, 0x2f, 0x64, 0x61, 0x74, 0x65, 0x74, 0x69, 0x6d, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x19, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x61, 0x6e, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x1a, 0x1d, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x2f, 0x64, 0x6f, 0x6d, 0x61, 0x69, - 0x6e, 0x2f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, - 0x8f, 0x01, 0x0a, 0x14, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, - 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x23, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, - 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, - 0x02, 0x10, 0x01, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2c, 0x0a, - 0x0d, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x60, 0x01, 0x52, 0x0c, 0x65, - 0x6d, 0x61, 0x69, 0x6c, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x24, 0x0a, 0x09, 0x63, - 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, - 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x49, - 0x64, 0x22, 0x35, 0x0a, 0x14, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x63, 0x6f, 0x75, 0x6e, - 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x69, 0x73, 0x5f, - 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x69, - 0x73, 0x53, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x22, 0x94, 0x01, 0x0a, 0x11, 0x47, 0x65, 0x74, - 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x29, - 0x0a, 0x09, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x10, 0x01, 0x48, 0x00, 0x52, 0x08, 0x63, 0x6f, - 0x6d, 0x6d, 0x6f, 0x6e, 0x49, 0x64, 0x88, 0x01, 0x01, 0x12, 0x34, 0x0a, 0x0d, 0x65, 0x6d, 0x61, - 0x69, 0x6c, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x42, 0x0a, 0xba, 0x48, 0x07, 0xc8, 0x01, 0x00, 0x72, 0x02, 0x60, 0x01, 0x48, 0x01, 0x52, 0x0c, - 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x88, 0x01, 0x01, 0x42, - 0x0c, 0x0a, 0x0a, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x42, 0x10, 0x0a, - 0x0e, 0x5f, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, - 0x48, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x32, 0x0a, 0x07, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, - 0x73, 0x2e, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, - 0x52, 0x07, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x32, 0xea, 0x01, 0x0a, 0x0e, 0x41, 0x63, - 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x6f, 0x0a, 0x0d, - 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x2d, 0x2e, + 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x1a, 0x1d, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x2f, 0x64, 0x6f, 0x6d, 0x61, + 0x69, 0x6e, 0x2f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x22, 0x90, 0x01, 0x0a, 0x14, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x23, 0x0a, 0x08, 0x75, 0x73, 0x65, + 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, + 0x72, 0x02, 0x10, 0x01, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2c, + 0x0a, 0x0d, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x07, 0xba, 0x48, 0x04, 0x72, 0x02, 0x60, 0x01, 0x52, 0x0c, + 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x25, 0x0a, 0x09, + 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x42, + 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, + 0x6e, 0x49, 0x64, 0x22, 0x35, 0x0a, 0x14, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x69, + 0x73, 0x5f, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x09, 0x69, 0x73, 0x53, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x22, 0x95, 0x01, 0x0a, 0x11, 0x47, + 0x65, 0x74, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x2a, 0x0a, 0x09, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, 0x01, 0x48, 0x00, 0x52, + 0x08, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x49, 0x64, 0x88, 0x01, 0x01, 0x12, 0x34, 0x0a, 0x0d, + 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x42, 0x0a, 0xba, 0x48, 0x07, 0xc8, 0x01, 0x00, 0x72, 0x02, 0x60, 0x01, 0x48, + 0x01, 0x52, 0x0c, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x88, + 0x01, 0x01, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x5f, 0x69, 0x64, + 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, + 0x73, 0x73, 0x22, 0x48, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x32, 0x0a, 0x07, 0x61, 0x63, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x61, 0x63, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0x73, 0x2e, 0x64, 0x6f, 0x6d, 0x61, 0x69, 0x6e, 0x2e, 0x41, 0x63, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0x52, 0x07, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x5e, 0x0a, 0x14, + 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x25, 0x0a, 0x09, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x5f, 0x69, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x08, 0xba, 0x48, 0x05, 0x72, 0x03, 0xb0, 0x01, + 0x01, 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x68, + 0x61, 0x72, 0x64, 0x5f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, + 0x52, 0x0a, 0x68, 0x61, 0x72, 0x64, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x22, 0x52, 0x0a, 0x15, + 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, + 0x5f, 0x61, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x41, 0x74, + 0x32, 0xdc, 0x02, 0x0a, 0x0e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x53, 0x65, 0x72, 0x76, + 0x69, 0x63, 0x65, 0x12, 0x6f, 0x0a, 0x0d, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x63, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x2d, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x2e, + 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x43, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x2d, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x2e, 0x61, + 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x41, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x00, 0x12, 0x67, 0x0a, 0x0a, 0x47, 0x65, 0x74, 0x41, 0x63, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x12, 0x2a, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x2e, 0x61, 0x63, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, + 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, + 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, + 0x74, 0x73, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x63, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x70, 0x0a, + 0x0d, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x2d, + 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, + 0x74, 0x73, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, + 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, - 0x73, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x63, - 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2d, 0x2e, 0x61, - 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, - 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x63, 0x6f, - 0x75, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x67, 0x0a, - 0x0a, 0x47, 0x65, 0x74, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x2a, 0x2e, 0x61, 0x63, - 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x61, - 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, - 0x74, 0x73, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x61, 0x70, 0x69, 0x2e, 0x76, - 0x31, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0xe6, 0x01, 0x0a, 0x1b, 0x63, 0x6f, 0x6d, 0x2e, 0x61, - 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, - 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x42, 0x08, 0x41, 0x70, 0x69, 0x50, 0x72, 0x6f, 0x74, 0x6f, - 0x50, 0x01, 0x5a, 0x3f, 0x6c, 0x69, 0x62, 0x73, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, - 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2d, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x61, 0x63, - 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x2f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x61, - 0x70, 0x69, 0x2f, 0x76, 0x31, 0x3b, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x61, 0x70, - 0x69, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x41, 0x41, 0x58, 0xaa, 0x02, 0x17, 0x41, 0x63, 0x63, 0x6f, - 0x75, 0x6e, 0x74, 0x73, 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x61, 0x70, 0x69, - 0x2e, 0x56, 0x31, 0xca, 0x02, 0x17, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x5c, 0x41, - 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x61, 0x70, 0x69, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x23, - 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x5c, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, - 0x73, 0x61, 0x70, 0x69, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0xea, 0x02, 0x19, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x3a, 0x3a, - 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x61, 0x70, 0x69, 0x3a, 0x3a, 0x56, 0x31, 0x62, - 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x73, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x63, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, + 0xe6, 0x01, 0x0a, 0x1b, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, + 0x2e, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x42, + 0x08, 0x41, 0x70, 0x69, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x3f, 0x6c, 0x69, 0x62, + 0x73, 0x2f, 0x62, 0x61, 0x63, 0x6b, 0x65, 0x6e, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2d, + 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x6f, 0x2f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x2f, + 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x3b, 0x61, + 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x61, 0x70, 0x69, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x41, + 0x41, 0x58, 0xaa, 0x02, 0x17, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x2e, 0x41, 0x63, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x61, 0x70, 0x69, 0x2e, 0x56, 0x31, 0xca, 0x02, 0x17, 0x41, + 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x5c, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, + 0x61, 0x70, 0x69, 0x5c, 0x56, 0x31, 0xe2, 0x02, 0x23, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x73, 0x5c, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x61, 0x70, 0x69, 0x5c, 0x56, 0x31, + 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, 0x02, 0x19, 0x41, + 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x3a, 0x3a, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x73, 0x61, 0x70, 0x69, 0x3a, 0x3a, 0x56, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -313,25 +432,31 @@ func file_accounts_accountsapi_v1_api_proto_rawDescGZIP() []byte { return file_accounts_accountsapi_v1_api_proto_rawDescData } -var file_accounts_accountsapi_v1_api_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_accounts_accountsapi_v1_api_proto_msgTypes = make([]protoimpl.MessageInfo, 6) var file_accounts_accountsapi_v1_api_proto_goTypes = []any{ - (*CreateAccountRequest)(nil), // 0: accounts.accountsapi.v1.CreateAccountRequest - (*CreateAcountResponse)(nil), // 1: accounts.accountsapi.v1.CreateAcountResponse - (*GetAccountRequest)(nil), // 2: accounts.accountsapi.v1.GetAccountRequest - (*GetAccountResponse)(nil), // 3: accounts.accountsapi.v1.GetAccountResponse - (*domain.Account)(nil), // 4: accounts.domain.Account + (*CreateAccountRequest)(nil), // 0: accounts.accountsapi.v1.CreateAccountRequest + (*CreateAcountResponse)(nil), // 1: accounts.accountsapi.v1.CreateAcountResponse + (*GetAccountRequest)(nil), // 2: accounts.accountsapi.v1.GetAccountRequest + (*GetAccountResponse)(nil), // 3: accounts.accountsapi.v1.GetAccountResponse + (*DeleteAccountRequest)(nil), // 4: accounts.accountsapi.v1.DeleteAccountRequest + (*DeleteAccountResponse)(nil), // 5: accounts.accountsapi.v1.DeleteAccountResponse + (*domain.Account)(nil), // 6: accounts.domain.Account + (*timestamppb.Timestamp)(nil), // 7: google.protobuf.Timestamp } var file_accounts_accountsapi_v1_api_proto_depIdxs = []int32{ - 4, // 0: accounts.accountsapi.v1.GetAccountResponse.account:type_name -> accounts.domain.Account - 0, // 1: accounts.accountsapi.v1.AccountService.CreateAccount:input_type -> accounts.accountsapi.v1.CreateAccountRequest - 2, // 2: accounts.accountsapi.v1.AccountService.GetAccount:input_type -> accounts.accountsapi.v1.GetAccountRequest - 1, // 3: accounts.accountsapi.v1.AccountService.CreateAccount:output_type -> accounts.accountsapi.v1.CreateAcountResponse - 3, // 4: accounts.accountsapi.v1.AccountService.GetAccount:output_type -> accounts.accountsapi.v1.GetAccountResponse - 3, // [3:5] is the sub-list for method output_type - 1, // [1:3] is the sub-list for method input_type - 1, // [1:1] is the sub-list for extension type_name - 1, // [1:1] is the sub-list for extension extendee - 0, // [0:1] is the sub-list for field type_name + 6, // 0: accounts.accountsapi.v1.GetAccountResponse.account:type_name -> accounts.domain.Account + 7, // 1: accounts.accountsapi.v1.DeleteAccountResponse.deleted_at:type_name -> google.protobuf.Timestamp + 0, // 2: accounts.accountsapi.v1.AccountService.CreateAccount:input_type -> accounts.accountsapi.v1.CreateAccountRequest + 2, // 3: accounts.accountsapi.v1.AccountService.GetAccount:input_type -> accounts.accountsapi.v1.GetAccountRequest + 4, // 4: accounts.accountsapi.v1.AccountService.DeleteAccount:input_type -> accounts.accountsapi.v1.DeleteAccountRequest + 1, // 5: accounts.accountsapi.v1.AccountService.CreateAccount:output_type -> accounts.accountsapi.v1.CreateAcountResponse + 3, // 6: accounts.accountsapi.v1.AccountService.GetAccount:output_type -> accounts.accountsapi.v1.GetAccountResponse + 5, // 7: accounts.accountsapi.v1.AccountService.DeleteAccount:output_type -> accounts.accountsapi.v1.DeleteAccountResponse + 5, // [5:8] is the sub-list for method output_type + 2, // [2:5] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name } func init() { file_accounts_accountsapi_v1_api_proto_init() } @@ -346,7 +471,7 @@ func file_accounts_accountsapi_v1_api_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_accounts_accountsapi_v1_api_proto_rawDesc, NumEnums: 0, - NumMessages: 4, + NumMessages: 6, NumExtensions: 0, NumServices: 1, }, diff --git a/libs/backend/proto-gen/openapiv2/accounts/accountsapi/v1/api.swagger.json b/libs/backend/proto-gen/openapiv2/accounts/accountsapi/v1/api.swagger.json index f3e32bd..affab3a 100644 --- a/libs/backend/proto-gen/openapiv2/accounts/accountsapi/v1/api.swagger.json +++ b/libs/backend/proto-gen/openapiv2/accounts/accountsapi/v1/api.swagger.json @@ -79,6 +79,16 @@ }, "title": "CreateAcountResponse defines the response for the create account request" }, + "v1DeleteAccountResponse": { + "type": "object", + "properties": { + "deletedAt": { + "type": "string", + "format": "date-time" + } + }, + "title": "DeleteAccountResponse will let the caller know if the server deleted the account" + }, "v1GetAccountResponse": { "type": "object", "properties": { diff --git a/protos/accounts/accountsapi/v1/api.proto b/protos/accounts/accountsapi/v1/api.proto index d70a8cc..0cb368d 100644 --- a/protos/accounts/accountsapi/v1/api.proto +++ b/protos/accounts/accountsapi/v1/api.proto @@ -5,6 +5,7 @@ package accounts.accountsapi.v1; import "buf/validate/validate.proto"; import "google/type/datetime.proto"; import "google/protobuf/any.proto"; +import "google/protobuf/timestamp.proto"; import "accounts/domain/account.proto"; service AccountService { @@ -13,13 +14,16 @@ service AccountService { // GetAccount retrieves an account by its common id rpc GetAccount(GetAccountRequest) returns (GetAccountResponse) {} + + // Delete Account will soft/hard delete an account + rpc DeleteAccount(DeleteAccountRequest) returns (DeleteAccountResponse) {} } // CreateAccountRequest defines the incoming data for the create account request message CreateAccountRequest { string username = 1 [(buf.validate.field).string.min_len = 1]; string email_address = 2 [(buf.validate.field).string.email = true]; - string common_id = 3 [(buf.validate.field).string.min_len = 1]; + string common_id = 3 [(buf.validate.field).string.uuid = true]; } // CreateAcountResponse defines the response for the create account request @@ -29,7 +33,7 @@ message CreateAcountResponse { // GetAccountRequest defines the incoming data for the get account request message GetAccountRequest { - optional string common_id = 1 [(buf.validate.field).string.min_len = 1]; + optional string common_id = 1 [(buf.validate.field).string.uuid = true]; optional string email_address = 2 [ (buf.validate.field).string.email = true, (buf.validate.field).required = false @@ -40,3 +44,14 @@ message GetAccountRequest { message GetAccountResponse{ accounts.domain.Account account = 1; } + +// DeleteAccountRequest will hard/soft delete an account by common_id +message DeleteAccountRequest { + string common_id = 1 [(buf.validate.field).string.uuid = true]; + bool hard_delete = 2; +} + +// DeleteAccountResponse will let the caller know if the server deleted the account +message DeleteAccountResponse { + google.protobuf.Timestamp deleted_at = 1; +} diff --git a/tools/backend-service/src/generators/service-gen/api-worker-files/fly.toml.template b/tools/backend-service/src/generators/service-gen/api-worker-files/fly.toml.template index 4c76351..46e372e 100644 --- a/tools/backend-service/src/generators/service-gen/api-worker-files/fly.toml.template +++ b/tools/backend-service/src/generators/service-gen/api-worker-files/fly.toml.template @@ -1,7 +1,7 @@ app = '<%= serviceName %>-prod' [[services]] -primary_region = 'ewr' +primary_region = 'iad' [build] dockerfile = "Dockerfile" diff --git a/tools/backend-service/src/generators/service-gen/graphql-files/fly.toml.template b/tools/backend-service/src/generators/service-gen/graphql-files/fly.toml.template index dbe1254..554d90c 100644 --- a/tools/backend-service/src/generators/service-gen/graphql-files/fly.toml.template +++ b/tools/backend-service/src/generators/service-gen/graphql-files/fly.toml.template @@ -4,7 +4,7 @@ # app = '<%= serviceName %>-prod' -primary_region = 'ewr' +primary_region = 'iad' [build] dockerfile = 'Dockerfile'