Skip to content

Instantly share code, notes, and snippets.

@romelgomez
Created January 3, 2025 00:53
Show Gist options
  • Save romelgomez/043fbb7007980c11c3339d25cba57834 to your computer and use it in GitHub Desktop.
Save romelgomez/043fbb7007980c11c3339d25cba57834 to your computer and use it in GitHub Desktop.
account domain
package account
import (
"go-store/dto"
"go-store/helper"
"go-store/prisma/db"
)
type Account struct {
*db.AccountModel
}
func (u *Account) Dto() *dto.Account {
return &dto.Account{
ID: u.ExternalID,
Object: u.Object,
Name: u.Name,
Email: u.Email,
CreatedAt: *helper.DateTimeToStr(&u.CreatedAt),
UpdatedAt: *helper.DateTimeToStr(&u.UpdatedAt),
}
}
func NewAccount(model *db.AccountModel) *Account {
if model == nil {
model = &db.AccountModel{}
}
return &Account{
AccountModel: model,
}
}
// domain/account/controller/account_controller.go
package account
import (
"fmt"
service "go-store/domain/account/service"
"go-store/dto"
"go-store/helper"
middleware "go-store/middleware"
"net/http"
"github.com/clerk/clerk-sdk-go/v2"
"github.com/julienschmidt/httprouter"
)
type Controller struct {
AccountService service.AccountService
}
func NewController(service service.AccountService) *Controller {
return &Controller{AccountService: service}
}
func (c *Controller) CreateMock(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
var accountData dto.AccountCreate
helper.ReadRequestBody(r, &accountData)
if err := accountData.Validate(); err != nil {
http.Error(w, "Validation error: "+err.Error(), http.StatusBadRequest)
return
}
result, err := c.AccountService.Create(r.Context(), accountData)
if err != nil {
helper.WriteErrorResponse(w, err.Error(), http.StatusInternalServerError)
return
}
helper.WriteResponseBody(w, dto.WebResponse{
Code: 200,
Status: dto.StatusOK,
Data: result,
})
}
func (c *Controller) Create(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
var clerkEvent dto.ClerkEvent
if err := helper.ReadRequestBody(r, &clerkEvent); err != nil {
http.Error(w, "Failed to read request body: "+err.Error(), http.StatusInternalServerError)
return
}
if clerkEvent.Type != "user.created" {
http.Error(w, "Invalid event type", http.StatusBadRequest)
return
}
var primaryEmail string
for _, email := range clerkEvent.Data.EmailAddresses {
if email.ID == clerkEvent.Data.PrimaryEmailAddressID {
primaryEmail = email.EmailAddress
break
}
}
accountData := dto.AccountCreate{
Name: clerkEvent.Data.FirstName,
Email: primaryEmail,
ClerkID: clerkEvent.Data.ID,
}
result, err := c.AccountService.Create(r.Context(), accountData)
if err != nil {
helper.WriteErrorResponse(w, err.Error(), http.StatusInternalServerError)
return
}
helper.WriteResponseBody(w, dto.WebResponse{
Code: 200,
Status: dto.StatusOK,
Data: result,
})
}
func (c *Controller) Update(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
var accountData dto.AccountUpdate
helper.ReadRequestBody(r, &accountData)
accountData.ID = params.ByName("id")
result, err := c.AccountService.Update(r.Context(), accountData)
if err != nil {
helper.WriteErrorResponse(w, err.Error(), http.StatusInternalServerError)
return
}
helper.WriteResponseBody(w, dto.WebResponse{
Code: 200,
Status: dto.StatusOK,
Data: result,
})
}
func (c *Controller) FindBy(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
id := params.ByName("id")
expand := r.URL.Query()["expand"]
paginationParamsCollection := &dto.PaginationParamsCollection{
AccountMedia: helper.ParsePaginationParams(r, "account_media"),
Listing: helper.ParsePaginationParams(r, "listing"),
ListingMedia: helper.ParsePaginationParams(r, "listing_media"),
}
result, err := c.AccountService.FindBy(
r.Context(),
id,
expand,
paginationParamsCollection,
)
if err != nil {
helper.WriteErrorResponse(w, err.Error(), http.StatusInternalServerError)
return
}
helper.WriteResponseBody(w, dto.WebResponse{
Code: 200,
Status: dto.StatusOK,
Data: result,
})
}
func (c *Controller) Delete(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
result, err := c.AccountService.Delete(r.Context(), params.ByName("id"))
if err != nil {
helper.WriteErrorResponse(w, err.Error(), http.StatusInternalServerError)
return
}
helper.WriteResponseBody(w, dto.WebResponse{
Code: 200,
Status: dto.StatusOK,
Data: result,
})
}
func (c *Controller) FindAll(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
result, err := c.AccountService.FindAll(r.Context())
if err != nil {
helper.WriteErrorResponse(w, err.Error(), http.StatusInternalServerError)
return
}
helper.WriteResponseBody(w, dto.WebResponse{
Code: 200,
Status: dto.StatusOK,
Data: result.Data,
Meta: result.Meta,
})
}
// TODO
func (c *Controller) ClerkCheck(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
claims, ok := r.Context().Value(middleware.JwtClaimsKey).(*clerk.SessionClaims)
if !ok || claims == nil {
fmt.Println("Unauthorized: unable to retrieve claims from context")
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
fmt.Printf("Raw Claims: \n\n%+v\n\n", claims)
helper.WriteResponseBody(w, dto.WebResponse{
Code: 200,
Status: dto.StatusOK,
Data: nil,
})
}
// domain/account/repository/account_repository.go
package account
import (
"context"
model "go-store/domain/account/model"
"go-store/prisma/db"
)
type AccountRepository interface {
Create(ctx context.Context, data model.Account) (*db.AccountModel, error)
Update(ctx context.Context, data model.Account) (*db.AccountModel, error)
Delete(ctx context.Context, id string) error
Exists(ctx context.Context, accountID string) bool
FindAll(ctx context.Context) ([]*db.AccountModel, error)
FindBy(
ctx context.Context,
value string,
) (*db.AccountModel, error)
}
// domain/account/repository/account_repository_impl.go
package account
import (
"context"
"errors"
model "go-store/domain/account/model"
"go-store/helper"
"go-store/prisma/db"
"strings"
)
type AccountRepositoryImpl struct {
Db *db.PrismaClient
}
func NewAccountRepository(dbClient *db.PrismaClient) AccountRepository {
return &AccountRepositoryImpl{Db: dbClient}
}
func (r *AccountRepositoryImpl) Create(ctx context.Context, data model.Account) (*db.AccountModel, error) {
id, err := helper.GeneratePrefixedID("acc_", 15)
if err != nil {
return nil, err
}
return r.Db.Account.CreateOne(
db.Account.ExternalID.Set(id),
db.Account.ClerkID.Set(data.ClerkID),
db.Account.Name.Set(data.Name),
db.Account.Email.Set(data.Email),
).Exec(ctx)
}
func (r *AccountRepositoryImpl) FindAll(ctx context.Context) ([]*db.AccountModel, error) {
accounts, err := r.Db.Account.FindMany().Exec(ctx)
if err != nil {
return nil, err
}
var result []*db.AccountModel
for _, account := range accounts {
result = append(result, &account)
}
return result, nil
}
func (r *AccountRepositoryImpl) FindBy(
ctx context.Context,
value string,
) (*db.AccountModel, error) {
switch {
case strings.HasPrefix(value, "user_"):
return r.Db.Account.FindFirst(
db.Account.ClerkID.Equals(value),
).Exec(ctx)
case strings.HasPrefix(value, "acc_"):
return r.Db.Account.FindFirst(
db.Account.ExternalID.Equals(value),
).Exec(ctx)
case helper.IsValidUUID(value):
return r.Db.Account.FindFirst(
db.Account.ID.Equals(value),
).Exec(ctx)
case helper.IsValidEmail(value):
return r.Db.Account.FindFirst(
db.Account.Email.Equals(value),
).Exec(ctx)
default:
return nil, errors.New("unable to infer field type for the given value")
}
}
func (r *AccountRepositoryImpl) Update(ctx context.Context, data model.Account) (*db.AccountModel, error) {
return r.Db.Account.FindUnique(db.Account.ExternalID.Equals(data.ID)).Update(
db.Account.Name.Set(data.Name),
).Exec(ctx)
}
func (r *AccountRepositoryImpl) Delete(ctx context.Context, id string) error {
_, err := r.Db.Account.FindUnique(db.Account.ExternalID.Equals(id)).Delete().Exec(ctx)
return err
}
func (r *AccountRepositoryImpl) Exists(ctx context.Context, accountID string) bool {
account, err := r.FindBy(ctx, accountID)
if err != nil || account == nil {
return false
}
return true
}
package v1
import (
controller "go-store/domain/account/controller"
"go-store/middleware"
"github.com/julienschmidt/httprouter"
)
func NewAccountRoutes(
router *httprouter.Router,
accountController *controller.Controller,
clerkMiddleware *middleware.ClerkMiddleware,
) {
router.GET("/api/v1/clerk/", clerkMiddleware.ValidateClerkSession(accountController.ClerkCheck))
router.POST("/api/v1/account", clerkMiddleware.ValidateClerkSession(accountController.Create))
router.DELETE("/api/v1/account/:id", clerkMiddleware.ValidateClerkSession(accountController.Delete))
router.POST("/api/v1/account/mock", clerkMiddleware.ValidateClerkSession(accountController.CreateMock))
router.GET("/api/v1/account", clerkMiddleware.ValidateClerkSession(accountController.FindAll))
router.GET("/api/v1/account/:id", clerkMiddleware.ValidateClerkSession(accountController.FindBy))
}
// domain/account/service/account_service.go
package account
import (
"context"
"go-store/dto"
)
type AccountService interface {
Create(ctx context.Context, request dto.AccountCreate) (*dto.Account, error)
Update(ctx context.Context, request dto.AccountUpdate) (*dto.Account, error)
Delete(ctx context.Context, id string) (*dto.DeleteResponse, error)
Exists(ctx context.Context, accountID string) bool
FindBy(
ctx context.Context,
value string,
expand []string,
paginationParamsCollection *dto.PaginationParamsCollection,
) (*dto.Account, error)
FindAll(ctx context.Context) (*dto.AccountCollection, error)
}
// domain/account/service/account_service_impl.go
package account
import (
"context"
"fmt"
model "go-store/domain/account/model"
account_repository "go-store/domain/account/repository"
listing_model "go-store/domain/listing/model"
listing_repository "go-store/domain/listing/repository"
"go-store/dto"
"go-store/helper"
"go-store/prisma/db"
"log"
"strings"
)
type AccountServiceImpl struct {
AccountRepository account_repository.AccountRepository
ListingRepository listing_repository.ListingRepository
}
func NewAccountServiceImpl(
accountRepository account_repository.AccountRepository,
listingRepository listing_repository.ListingRepository,
) AccountService {
return &AccountServiceImpl{
AccountRepository: accountRepository,
ListingRepository: listingRepository,
}
}
func (s *AccountServiceImpl) Create(ctx context.Context, req dto.AccountCreate) (*dto.Account, error) {
account, err := s.AccountRepository.Create(ctx, *model.NewAccount(&db.AccountModel{
InnerAccount: db.InnerAccount{
Email: req.Email,
Name: req.Name,
ClerkID: req.ClerkID,
},
}))
if err != nil {
if strings.Contains(err.Error(), "email") {
return nil, fmt.Errorf("an account with the email '%s' already exists", req.Email)
}
if strings.Contains(err.Error(), "clerk_id") {
return nil, fmt.Errorf("an account with the clerk_id '%s' already exists", req.ClerkID)
}
log.Printf("Error creating account: %v", err)
return nil, fmt.Errorf("an unexpected error occurred while creating the account")
}
model := model.NewAccount(account)
dto := model.Dto()
return dto, nil
}
func (s *AccountServiceImpl) FindBy(
ctx context.Context,
value string,
expand []string,
paginationParamsCollection *dto.PaginationParamsCollection,
) (*dto.Account, error) {
account, err := s.AccountRepository.FindBy(ctx, value)
if err != nil {
if strings.Contains(err.Error(), "ErrNotFound") {
return nil, fmt.Errorf("account with '%s' does not exist", value)
}
log.Printf("Error finding account by: %v", err)
return nil, fmt.Errorf("an unexpected error occurred while retrieving the account")
}
acc := model.NewAccount(account)
response := acc.Dto()
if helper.Contains(expand, "listing") {
listing, err := s.getListingsForAccount(ctx, account, paginationParamsCollection)
if err != nil {
return nil, err
}
if len(listing.Data) > 0 {
response.Listings = listing
}
}
return response, nil
}
func (s *AccountServiceImpl) getListingsForAccount(
ctx context.Context,
account *db.AccountModel,
paginationParamsCollection *dto.PaginationParamsCollection,
) (*dto.ListingCollection, error) {
listings, total, err := s.ListingRepository.FindAllByAccountID(ctx, account.ID, paginationParamsCollection)
if err != nil {
log.Printf("Error retrieving listings for account '%s': %v", account.ExternalID, err)
return nil, fmt.Errorf("an unexpected error occurred while retrieving listings for the account")
}
var listingCollection dto.ListingCollection
for _, listing := range listings {
list := listing_model.NewListing(listing)
dto := list.Dto()
listingCollection.Data = append(listingCollection.Data, dto)
}
page := paginationParamsCollection.Listing.Page
limit := paginationParamsCollection.Listing.Limit
listingCollection.Meta = &dto.Meta{
Pagination: dto.Pagination{
Page: page,
Limit: limit,
TotalItems: total,
TotalPages: (total + limit - 1) / limit,
},
}
return &listingCollection, nil
}
func (s *AccountServiceImpl) Update(ctx context.Context, req dto.AccountUpdate) (*dto.Account, error) {
account, err := s.AccountRepository.Update(ctx, *model.NewAccount(&db.AccountModel{
InnerAccount: db.InnerAccount{
ExternalID: req.ID,
Name: req.Name,
},
}))
if err != nil {
if strings.Contains(err.Error(), "ErrNotFound") {
return nil, fmt.Errorf("cannot update: account with ID '%s' does not exist", req.ID)
}
log.Printf("Error updating account: %v", err)
return nil, fmt.Errorf("an unexpected error occurred while updating the account")
}
accountModel := model.NewAccount(account)
accountModelDto := accountModel.Dto()
return accountModelDto, nil
}
func (s *AccountServiceImpl) Delete(ctx context.Context, id string) (*dto.DeleteResponse, error) {
err := s.AccountRepository.Delete(ctx, id)
if err != nil {
if strings.Contains(err.Error(), "ErrNotFound") {
return nil, fmt.Errorf("cannot delete: account with ID '%s' does not exist", id)
}
log.Printf("Error deleting account: %v", err)
return nil, fmt.Errorf("an unexpected error occurred while deleting the account")
}
return &dto.DeleteResponse{
Status: "ok",
Message: "Deleted successfully",
}, nil
}
func (s *AccountServiceImpl) FindAll(ctx context.Context) (*dto.AccountCollection, error) {
accountModels, err := s.AccountRepository.FindAll(ctx)
if err != nil {
log.Printf("Error retrieving accounts: %v", err)
return nil, fmt.Errorf("an unexpected error occurred while retrieving accounts")
}
var accounts dto.AccountCollection
for _, account := range accountModels {
response := model.NewAccount(account)
accounts.Data = append(accounts.Data, response.Dto())
}
return &accounts, nil
}
func (s *AccountServiceImpl) Exists(ctx context.Context, accountID string) bool {
exists := s.AccountRepository.Exists(ctx, accountID)
if !exists {
log.Printf("Account with ID '%s' does not exist", accountID)
}
return exists
}

Partial Tree

tree -I '.github|.trunk|prisma|all.js'

├── api
│   ├── v1
│   │   ├── account_routes.go
│   └── v2
├── auth
│   └── jwt_strategy.go
├── config
│   └── database.go
├── domain
│   ├── account
│   │   ├── controller
│   │   │   └── account_controller.go
│   │   ├── model
│   │   │   └── account.go
│   │   ├── repository
│   │   │   ├── account_repository.go
│   │   │   └── account_repository_impl.go
│   │   └── service
│   │       ├── account_service.go
│   │       └── account_service_impl.go
├── dto
│   ├── account.go
│   ├── account_test.go
├── file.md
├── main.go
├── middleware
│   └── clerk_middleware.go
├── router
│   └── router.go
@romelgomez
Copy link
Author

Api Pesponse for GET

{{endpoint}}/api/v1/account/user_2ol3e8LvCLTZAlc6a3OcSORMnP1?expand=listing

{
    "code": 200,
    "status": "OK",
    "data": {
        "id": "acc_kVESFvxRP4xI",
        "object": "account",
        "name": "Natalia",
        "email": "[email protected]",
        "created_at": "2024-12-24T17:53:49+00:00",
        "updated_at": "2024-12-24T17:53:49+00:00",
        "listings": {
            "data": [
                {
                    "id": "ltn_IDMc3f47zASAvFgR87SMiuZt",
                    "object": "listing",
                    "name": "Ullrich - Morissette",
                    "visibility": "PUBLIC",
                    "created_at": "2024-12-24T17:58:47+00:00",
                    "updated_at": "2024-12-24T17:58:47+00:00"
                },
                {
                    "id": "ltn_RZbkkvJiZUZWexfJi8T8zz7m",
                    "object": "listing",
                    "name": "Wisozk LLC",
                    "visibility": "PUBLIC",
                    "created_at": "2024-12-24T17:58:46+00:00",
                    "updated_at": "2024-12-24T17:58:46+00:00"
                },
                {
                    "id": "ltn_VWLlgZKMUNnfXoJ9YAaZTVsy",
                    "object": "listing",
                    "name": "Bechtelar - Altenwerth",
                    "visibility": "PUBLIC",
                    "created_at": "2024-12-24T17:58:47+00:00",
                    "updated_at": "2024-12-24T17:58:47+00:00"
                },
                {
                    "id": "ltn_9KDP0SAuJdvYFwafe5G3goFz",
                    "object": "listing",
                    "name": "Bradtke - Gerlach",
                    "visibility": "PUBLIC",
                    "created_at": "2024-12-24T17:58:48+00:00",
                    "updated_at": "2024-12-24T17:58:48+00:00"
                }
            ],
            "meta": {
                "pagination": {
                    "page": 1,
                    "total_pages": 1,
                    "total_items": 4,
                    "limit": 10
                }
            }
        }
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment