Skip to content

Commit

Permalink
FEAT: Accounts GraphQL Finalized (#33)
Browse files Browse the repository at this point in the history
* feat: adding graphql service for accounts

* feat: adding support for accounts-graphql schema

* fix: scale machines to 0

* fix: fly.toml

* fix: internal port 8080 in production

* fix: change back to port 3000

* fix: move graph into internal

* feat: code generation for gql

* fix: models

* fix: models

* feat: resolvers are fixed

* feat: updating resolver path

* feat: move schemas into their own directory

* fix: moving models to a package

* feat: adding proper generated files package

* feat: adding apollo router locally for dev

* feat: logged todos resolver is called

* fix: empty resolvers

* fix: moving schema configs to dev

* fix: moving schema configs to dev

* feat: adding apolo router

* feat: super graph dev and prod setup

* feat: remove the custom metrics

* fix: deployment for apollo router

* fix: delete older files

* feat: updating dockerfile for router

* fix: remove homepage

* fix: disable homepage

* fix: disable homepage

* fix: router config

* fix: config

* fix: apollo router config

* fix: dockerfile

* fix: dockerfile slashes

* fix: apollo router prod

* fix: prod

* fix: http service for accounts graphql

* fix: revert back to public prod url

* fix: graphql schema

* feat: account model generation

* feat: update prod graph

* fix: use internal routing

* feat: adding accounts internal

* feat: hide accounts-graphql behind VPC

* feat: graphql service generator

* feat: cleanup graphql files

* fix: file generation

* fix: further cleanup

* fix: remove unwanted package in go workspace

* feat: calling accounts api from accounts graph

* feat: updating supergraph schema with prod

* refactor: change name to account_service handler

* feat: adding uuid validation for account

* fix: models to query by commonID

* feat: abstracting lru cache and other caches to their own package

* fix: move data center deployments closer to US-East

* feat: supporting get account by commonID or Email gRPC

* feat: updating graphs

* feat: adding federation support

* feat: adding protos for delete account

* feat: handling delete user from db not auth0

* feat: handling delete account mutation

* feat: updating the fly default primary region to iad (virginia
  • Loading branch information
ericzorn93 authored Jan 4, 2025
1 parent 553d1bd commit 41bdab8
Show file tree
Hide file tree
Showing 32 changed files with 1,233 additions and 125 deletions.
2 changes: 1 addition & 1 deletion apps/services/accounts-api/cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion apps/services/accounts-api/fly.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
app = 'accounts-api-prod'

[[services]]
primary_region = 'ewr'
primary_region = 'iad'

[build]
dockerfile = "Dockerfile"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand All @@ -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(
Expand All @@ -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
Expand All @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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")
Expand All @@ -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)
Expand Down
7 changes: 7 additions & 0 deletions apps/services/accounts-api/internal/app/ports/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
4 changes: 4 additions & 0 deletions apps/services/accounts-api/internal/app/ports/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
4 changes: 0 additions & 4 deletions apps/services/accounts-api/internal/domain/generic.go

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -8,27 +8,28 @@ 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

// AccountRepository is the account repository
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
Expand All @@ -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()))
Expand All @@ -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
}
}
4 changes: 3 additions & 1 deletion apps/services/accounts-graphql/cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{})
Expand Down
2 changes: 1 addition & 1 deletion apps/services/accounts-graphql/fly.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
#

app = 'accounts-graphql-prod'
primary_region = 'ewr'
primary_region = 'iad'

[build]
dockerfile = 'Dockerfile'
Expand Down

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

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

Loading

0 comments on commit 41bdab8

Please sign in to comment.