mirror of
https://github.com/emo2007/block-accounting.git
synced 2025-04-12 08:56:28 +00:00
org creatin and listing with pagianation implemented with one small bug
This commit is contained in:
parent
d4430445a7
commit
9b1d371cb1
@ -18,10 +18,11 @@ import (
|
|||||||
|
|
||||||
var interfaceSet wire.ProviderSet = wire.NewSet(
|
var interfaceSet wire.ProviderSet = wire.NewSet(
|
||||||
provideAuthController,
|
provideAuthController,
|
||||||
provideOrganizationsCOntroller,
|
provideOrganizationsController,
|
||||||
provideControllers,
|
provideControllers,
|
||||||
|
|
||||||
provideAuthPresenter,
|
provideAuthPresenter,
|
||||||
|
provideOrganizationsPresenter,
|
||||||
)
|
)
|
||||||
|
|
||||||
func provideLogger(c config.Config) *slog.Logger {
|
func provideLogger(c config.Config) *slog.Logger {
|
||||||
@ -53,6 +54,10 @@ func provideAuthPresenter(
|
|||||||
return presenters.NewAuthPresenter(jwtInteractor)
|
return presenters.NewAuthPresenter(jwtInteractor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func provideOrganizationsPresenter() presenters.OrganizationsPresenter {
|
||||||
|
return presenters.NewOrganizationsPresenter()
|
||||||
|
}
|
||||||
|
|
||||||
func provideAuthController(
|
func provideAuthController(
|
||||||
log *slog.Logger,
|
log *slog.Logger,
|
||||||
usersInteractor users.UsersInteractor,
|
usersInteractor users.UsersInteractor,
|
||||||
@ -67,13 +72,15 @@ func provideAuthController(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func provideOrganizationsCOntroller(
|
func provideOrganizationsController(
|
||||||
log *slog.Logger,
|
log *slog.Logger,
|
||||||
organizationsInteractor organizations.OrganizationsInteractor,
|
organizationsInteractor organizations.OrganizationsInteractor,
|
||||||
|
presenter presenters.OrganizationsPresenter,
|
||||||
) controllers.OrganizationsController {
|
) controllers.OrganizationsController {
|
||||||
return controllers.NewOrganizationsController(
|
return controllers.NewOrganizationsController(
|
||||||
log.WithGroup("organizations-controller"),
|
log.WithGroup("organizations-controller"),
|
||||||
organizationsInteractor,
|
organizationsInteractor,
|
||||||
|
presenter,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,7 +27,8 @@ func ProvideService(c config.Config) (service.Service, func(), error) {
|
|||||||
authController := provideAuthController(logger, usersInteractor, authPresenter, jwtInteractor)
|
authController := provideAuthController(logger, usersInteractor, authPresenter, jwtInteractor)
|
||||||
organizationsRepository := provideOrganizationsRepository(db)
|
organizationsRepository := provideOrganizationsRepository(db)
|
||||||
organizationsInteractor := provideOrganizationsInteractor(logger, organizationsRepository)
|
organizationsInteractor := provideOrganizationsInteractor(logger, organizationsRepository)
|
||||||
organizationsController := provideOrganizationsCOntroller(logger, organizationsInteractor)
|
organizationsPresenter := provideOrganizationsPresenter()
|
||||||
|
organizationsController := provideOrganizationsController(logger, organizationsInteractor, organizationsPresenter)
|
||||||
rootController := provideControllers(logger, authController, organizationsController)
|
rootController := provideControllers(logger, authController, organizationsController)
|
||||||
server := provideRestServer(logger, rootController, c, jwtInteractor)
|
server := provideRestServer(logger, rootController, c, jwtInteractor)
|
||||||
serviceService := service.NewService(logger, server)
|
serviceService := service.NewService(logger, server)
|
||||||
|
@ -61,7 +61,7 @@ func (c *authController) Join(w http.ResponseWriter, req *http.Request) ([]byte,
|
|||||||
return nil, fmt.Errorf("error invalid mnemonic. %w", ErrorAuthInvalidMnemonic)
|
return nil, fmt.Errorf("error invalid mnemonic. %w", ErrorAuthInvalidMnemonic)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(req.Context(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(req.Context(), 3*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
user, err := c.usersInteractor.Create(ctx, users.CreateParams{
|
user, err := c.usersInteractor.Create(ctx, users.CreateParams{
|
||||||
@ -78,7 +78,7 @@ func (c *authController) Join(w http.ResponseWriter, req *http.Request) ([]byte,
|
|||||||
|
|
||||||
c.log.Debug("join request", slog.String("user id", user.ID.String()))
|
c.log.Debug("join request", slog.String("user id", user.ID.String()))
|
||||||
|
|
||||||
return c.presenter.ResponseJoin(w, user)
|
return c.presenter.ResponseJoin(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NIT: wrap with idempotent action handler
|
// NIT: wrap with idempotent action handler
|
||||||
@ -90,7 +90,7 @@ func (c *authController) Login(w http.ResponseWriter, req *http.Request) ([]byte
|
|||||||
|
|
||||||
c.log.Debug("login request", slog.String("mnemonic", request.Mnemonic))
|
c.log.Debug("login request", slog.String("mnemonic", request.Mnemonic))
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(req.Context(), 5*time.Second)
|
ctx, cancel := context.WithTimeout(req.Context(), 3*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
seed, err := hdwallet.NewSeedFromMnemonic(request.Mnemonic)
|
seed, err := hdwallet.NewSeedFromMnemonic(request.Mnemonic)
|
||||||
@ -111,7 +111,7 @@ func (c *authController) Login(w http.ResponseWriter, req *http.Request) ([]byte
|
|||||||
|
|
||||||
c.log.Debug("login request", slog.String("user id", users[0].ID.String()))
|
c.log.Debug("login request", slog.String("user id", users[0].ID.String()))
|
||||||
|
|
||||||
return c.presenter.ResponseLogin(w, users[0])
|
return c.presenter.ResponseLogin(users[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
// const mnemonicEntropyBitSize int = 256
|
// const mnemonicEntropyBitSize int = 256
|
||||||
|
@ -1,43 +1,85 @@
|
|||||||
package controllers
|
package controllers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/emochka2007/block-accounting/internal/interface/rest/domain"
|
"github.com/emochka2007/block-accounting/internal/interface/rest/domain"
|
||||||
"github.com/emochka2007/block-accounting/internal/interface/rest/presenters"
|
"github.com/emochka2007/block-accounting/internal/interface/rest/presenters"
|
||||||
|
"github.com/emochka2007/block-accounting/internal/pkg/ctxmeta"
|
||||||
"github.com/emochka2007/block-accounting/internal/usecase/interactors/organizations"
|
"github.com/emochka2007/block-accounting/internal/usecase/interactors/organizations"
|
||||||
)
|
)
|
||||||
|
|
||||||
type OrganizationsController interface {
|
type OrganizationsController interface {
|
||||||
NewOrganization(w http.ResponseWriter, r *http.Request) ([]byte, error)
|
NewOrganization(w http.ResponseWriter, r *http.Request) ([]byte, error)
|
||||||
|
ListOrganizations(w http.ResponseWriter, r *http.Request) ([]byte, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type organizationsController struct {
|
type organizationsController struct {
|
||||||
log *slog.Logger
|
log *slog.Logger
|
||||||
orgInteractor organizations.OrganizationsInteractor
|
orgInteractor organizations.OrganizationsInteractor
|
||||||
|
presenter presenters.OrganizationsPresenter
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewOrganizationsController(
|
func NewOrganizationsController(
|
||||||
log *slog.Logger,
|
log *slog.Logger,
|
||||||
orgInteractor organizations.OrganizationsInteractor,
|
orgInteractor organizations.OrganizationsInteractor,
|
||||||
|
presenter presenters.OrganizationsPresenter,
|
||||||
) OrganizationsController {
|
) OrganizationsController {
|
||||||
return &organizationsController{
|
return &organizationsController{
|
||||||
log: log,
|
log: log,
|
||||||
orgInteractor: orgInteractor,
|
orgInteractor: orgInteractor,
|
||||||
|
presenter: presenter,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *organizationsController) NewOrganization(w http.ResponseWriter, r *http.Request) ([]byte, error) {
|
func (c *organizationsController) NewOrganization(w http.ResponseWriter, r *http.Request) ([]byte, error) {
|
||||||
_, err := presenters.CreateRequest[domain.NewOrganizationRequest](r)
|
req, err := presenters.CreateRequest[domain.NewOrganizationRequest](r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error build request. %w", err)
|
return nil, fmt.Errorf("error build request. %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo call int.Create
|
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
// todo build response
|
org, err := c.orgInteractor.Create(ctx, organizations.CreateParams{
|
||||||
|
Name: req.Name,
|
||||||
|
Address: req.Address,
|
||||||
|
WalletMnemonic: req.WalletMnemonic,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error create new organization. %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil, nil
|
return c.presenter.ResponseCreate(org)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *organizationsController) ListOrganizations(w http.ResponseWriter, r *http.Request) ([]byte, error) {
|
||||||
|
req, err := presenters.CreateRequest[domain.ListOrganizationsRequest](r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error build request. %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
user, err := ctxmeta.User(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error fetch user from context. %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := c.orgInteractor.List(ctx, organizations.ListParams{
|
||||||
|
UserId: user.Id(),
|
||||||
|
Cursor: req.Cursor,
|
||||||
|
Limit: req.Limit,
|
||||||
|
OffsetDate: time.UnixMilli(req.OffsetDate),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error fetch organizations list. %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.presenter.ResponseList(resp.Organizations, resp.NextCursor)
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,20 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Generic
|
||||||
|
|
||||||
|
type Collection[T any] struct {
|
||||||
|
Items []T `json:"items,omitempty"`
|
||||||
|
Pagination Pagination `json:"pagination,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Pagination struct {
|
||||||
|
NextCursor string `json:"next_cursor,omitempty"`
|
||||||
|
TotalItems uint32 `json:"total_items,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auth related DTO's
|
||||||
|
|
||||||
type JoinRequest struct {
|
type JoinRequest struct {
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
Credentals struct {
|
Credentals struct {
|
||||||
@ -28,12 +42,28 @@ type LoginResponse struct {
|
|||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Organizations
|
||||||
|
|
||||||
type NewOrganizationRequest struct {
|
type NewOrganizationRequest struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Address string `json:"address"`
|
Address string `json:"address"`
|
||||||
WalletMnemonic string `json:"wallet_mnemonic,omitempty"`
|
WalletMnemonic string `json:"wallet_mnemonic,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type NewOrganizationResponse struct {
|
||||||
|
Organization Organization `json:"organization"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListOrganizationsRequest struct {
|
||||||
|
Cursor string `json:"cursor,omitempty"`
|
||||||
|
Limit uint8 `json:"limit,omitempty"` // Default: 50, Max: 50
|
||||||
|
OffsetDate int64 `json:"offset_date,omitempty"` // List organizations, updated since the date
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListOrganizationsResponse struct {
|
||||||
|
Collection[Organization]
|
||||||
|
}
|
||||||
|
|
||||||
func BuildRequest[T any](data []byte) (*T, error) {
|
func BuildRequest[T any](data []byte) (*T, error) {
|
||||||
var req T
|
var req T
|
||||||
|
|
||||||
|
9
backend/internal/interface/rest/domain/organization.go
Normal file
9
backend/internal/interface/rest/domain/organization.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package domain
|
||||||
|
|
||||||
|
type Organization struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Address string `json:"address"`
|
||||||
|
CreatedAt uint64 `json:"created_at"`
|
||||||
|
UpdatedAt uint64 `json:"updated_at"`
|
||||||
|
}
|
@ -3,7 +3,6 @@ package presenters
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/emochka2007/block-accounting/internal/interface/rest/domain"
|
"github.com/emochka2007/block-accounting/internal/interface/rest/domain"
|
||||||
@ -12,8 +11,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type AuthPresenter interface {
|
type AuthPresenter interface {
|
||||||
ResponseJoin(w http.ResponseWriter, user *models.User) ([]byte, error)
|
ResponseJoin(user *models.User) ([]byte, error)
|
||||||
ResponseLogin(w http.ResponseWriter, user *models.User) ([]byte, error)
|
ResponseLogin(user *models.User) ([]byte, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type authPresenter struct {
|
type authPresenter struct {
|
||||||
@ -28,7 +27,7 @@ func NewAuthPresenter(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *authPresenter) ResponseJoin(w http.ResponseWriter, user *models.User) ([]byte, error) {
|
func (p *authPresenter) ResponseJoin(user *models.User) ([]byte, error) {
|
||||||
resp := new(domain.JoinResponse)
|
resp := new(domain.JoinResponse)
|
||||||
|
|
||||||
token, err := p.jwtInteractor.NewToken(user, 24*time.Hour)
|
token, err := p.jwtInteractor.NewToken(user, 24*time.Hour)
|
||||||
@ -46,7 +45,7 @@ func (p *authPresenter) ResponseJoin(w http.ResponseWriter, user *models.User) (
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *authPresenter) ResponseLogin(w http.ResponseWriter, user *models.User) ([]byte, error) {
|
func (p *authPresenter) ResponseLogin(user *models.User) ([]byte, error) {
|
||||||
resp := new(domain.LoginResponse)
|
resp := new(domain.LoginResponse)
|
||||||
|
|
||||||
token, err := p.jwtInteractor.NewToken(user, 24*time.Hour)
|
token, err := p.jwtInteractor.NewToken(user, 24*time.Hour)
|
||||||
|
74
backend/internal/interface/rest/presenters/organizations.go
Normal file
74
backend/internal/interface/rest/presenters/organizations.go
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
package presenters
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/emochka2007/block-accounting/internal/interface/rest/domain"
|
||||||
|
"github.com/emochka2007/block-accounting/internal/pkg/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OrganizationsPresenter interface {
|
||||||
|
ResponseCreate(organization *models.Organization) ([]byte, error)
|
||||||
|
ResponseList(orgs []*models.Organization, nextCursor string) ([]byte, error)
|
||||||
|
Organizations(orgs []*models.Organization) []domain.Organization
|
||||||
|
}
|
||||||
|
|
||||||
|
type organizationsPresenter struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewOrganizationsPresenter() OrganizationsPresenter {
|
||||||
|
return &organizationsPresenter{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *organizationsPresenter) ResponseCreate(o *models.Organization) ([]byte, error) {
|
||||||
|
resp := &domain.NewOrganizationResponse{
|
||||||
|
Organization: domain.Organization{
|
||||||
|
Id: o.ID.String(),
|
||||||
|
Name: o.Name,
|
||||||
|
Address: o.Address,
|
||||||
|
CreatedAt: uint64(o.CreatedAt.UnixMilli()),
|
||||||
|
UpdatedAt: uint64(o.UpdatedAt.UnixMilli()),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := json.Marshal(resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error marshal organization create response. %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *organizationsPresenter) ResponseList(orgs []*models.Organization, nextCursor string) ([]byte, error) {
|
||||||
|
resp := &domain.ListOrganizationsResponse{
|
||||||
|
Collection: domain.Collection[domain.Organization]{
|
||||||
|
Items: p.Organizations(orgs),
|
||||||
|
Pagination: domain.Pagination{
|
||||||
|
NextCursor: nextCursor,
|
||||||
|
TotalItems: uint32(len(orgs)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := json.Marshal(resp)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error marshal organizations list response. %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *organizationsPresenter) Organizations(orgs []*models.Organization) []domain.Organization {
|
||||||
|
out := make([]domain.Organization, len(orgs))
|
||||||
|
|
||||||
|
for i, o := range orgs {
|
||||||
|
out[i] = domain.Organization{
|
||||||
|
Id: o.ID.String(),
|
||||||
|
Name: o.Name,
|
||||||
|
Address: o.Address,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
@ -100,7 +100,7 @@ func (s *Server) buildRouter() {
|
|||||||
router.Route("/organizations", func(r chi.Router) {
|
router.Route("/organizations", func(r chi.Router) {
|
||||||
r = r.With(s.withAuthorization)
|
r = r.With(s.withAuthorization)
|
||||||
|
|
||||||
// r.Get("/", s.handle(s.controllers.Auth.Invite, "list_organizations"))
|
r.Get("/", s.handle(s.controllers.Organizations.ListOrganizations, "list_organizations"))
|
||||||
r.Post("/", s.handle(s.controllers.Organizations.NewOrganization, "new_organization"))
|
r.Post("/", s.handle(s.controllers.Organizations.NewOrganization, "new_organization"))
|
||||||
|
|
||||||
r.Route("/{organization_id}", func(r chi.Router) {
|
r.Route("/{organization_id}", func(r chi.Router) {
|
||||||
@ -108,7 +108,7 @@ func (s *Server) buildRouter() {
|
|||||||
// r.Delete("/", s.handle(s.controllers.Organizations.NewOrganization, "delete_organization"))
|
// r.Delete("/", s.handle(s.controllers.Organizations.NewOrganization, "delete_organization"))
|
||||||
|
|
||||||
r.Route("/transactions", func(r chi.Router) {
|
r.Route("/transactions", func(r chi.Router) {
|
||||||
r.Get("/", nil) // list
|
r.Get("/", nil) // list todo add cache
|
||||||
r.Post("/", nil) // add
|
r.Post("/", nil) // add
|
||||||
r.Put("/{tx_id}", nil) // update / approve (or maybe body?)
|
r.Put("/{tx_id}", nil) // update / approve (or maybe body?)
|
||||||
r.Delete("/{tx_id}", nil) // remove
|
r.Delete("/{tx_id}", nil) // remove
|
||||||
@ -117,7 +117,7 @@ func (s *Server) buildRouter() {
|
|||||||
r.Post("/invite/{hash}", s.handle(s.controllers.Auth.Invite, "invite")) // create a new invite link
|
r.Post("/invite/{hash}", s.handle(s.controllers.Auth.Invite, "invite")) // create a new invite link
|
||||||
|
|
||||||
r.Route("/employees", func(r chi.Router) {
|
r.Route("/employees", func(r chi.Router) {
|
||||||
r.Get("/", nil) // list
|
r.Get("/", nil) // list. todo add cache
|
||||||
r.Post("/", nil) // add
|
r.Post("/", nil) // add
|
||||||
r.Put("/{employee_id}", nil) // update (or maybe body?)
|
r.Put("/{employee_id}", nil) // update (or maybe body?)
|
||||||
r.Delete("/{employee_id}", nil) // remove
|
r.Delete("/{employee_id}", nil) // remove
|
||||||
@ -154,6 +154,8 @@ func (s *Server) handle(
|
|||||||
)
|
)
|
||||||
|
|
||||||
s.responseError(w, err)
|
s.responseError(w, err)
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Add("Content-Type", "application/json")
|
w.Header().Add("Content-Type", "application/json")
|
||||||
@ -186,6 +188,8 @@ func (s *Server) responseError(w http.ResponseWriter, e error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleMw(next http.Handler) http.Handler {
|
func (s *Server) handleMw(next http.Handler) http.Handler {
|
||||||
|
// todo add rate limiter && cirquit braker
|
||||||
|
|
||||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||||
s.closeMu.RLock()
|
s.closeMu.RLock()
|
||||||
defer s.closeMu.RUnlock()
|
defer s.closeMu.RUnlock()
|
||||||
|
@ -2,6 +2,9 @@ package organizations
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"time"
|
"time"
|
||||||
@ -13,17 +16,34 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrorUnauthorizedAccess = errors.New("unauthorized access")
|
||||||
|
)
|
||||||
|
|
||||||
type CreateParams struct {
|
type CreateParams struct {
|
||||||
Name string
|
Name string
|
||||||
Address string
|
Address string
|
||||||
WalletMnemonic string
|
WalletMnemonic string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ListParams struct {
|
||||||
|
Ids uuid.UUIDs
|
||||||
|
UserId uuid.UUID
|
||||||
|
|
||||||
|
Cursor string
|
||||||
|
OffsetDate time.Time
|
||||||
|
Limit uint8 // Max limit is 50 (may change)
|
||||||
|
}
|
||||||
|
|
||||||
type OrganizationsInteractor interface {
|
type OrganizationsInteractor interface {
|
||||||
Create(
|
Create(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
params CreateParams,
|
params CreateParams,
|
||||||
) (*models.Organization, error)
|
) (*models.Organization, error)
|
||||||
|
List(
|
||||||
|
ctx context.Context,
|
||||||
|
params ListParams,
|
||||||
|
) (*ListResponse, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type organizationsInteractor struct {
|
type organizationsInteractor struct {
|
||||||
@ -41,6 +61,101 @@ func NewOrganizationsInteractor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type organizationsListCursor struct {
|
||||||
|
Id uuid.UUID `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func newOrganizationsListCursor(id ...uuid.UUID) *organizationsListCursor {
|
||||||
|
if len(id) > 0 {
|
||||||
|
return &organizationsListCursor{id[0]}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new(organizationsListCursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *organizationsListCursor) encode() (string, error) {
|
||||||
|
data, err := json.Marshal(c)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("couldn't marshal reaction id. %w", err)
|
||||||
|
}
|
||||||
|
return base64.StdEncoding.EncodeToString(data), nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *organizationsListCursor) decode(s string) error {
|
||||||
|
if c == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := base64.StdEncoding.DecodeString(s)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error decode token. %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Unmarshal(token, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListResponse struct {
|
||||||
|
Organizations []*models.Organization
|
||||||
|
NextCursor string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *organizationsInteractor) List(
|
||||||
|
ctx context.Context,
|
||||||
|
params ListParams,
|
||||||
|
) (*ListResponse, error) {
|
||||||
|
user, err := ctxmeta.User(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error fetch user from context. %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.UserId != uuid.Nil {
|
||||||
|
if params.UserId != user.Id() {
|
||||||
|
return nil, fmt.Errorf("error unauthorized organizations list access. %w", ErrorUnauthorizedAccess)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
params.UserId = user.Id()
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.Limit <= 0 || params.Limit > 50 {
|
||||||
|
params.Limit = 50
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor := newOrganizationsListCursor()
|
||||||
|
|
||||||
|
if params.Cursor != "" {
|
||||||
|
if err := cursor.decode(params.Cursor); err != nil {
|
||||||
|
return nil, fmt.Errorf("error decode cursor value. %w", err) // maybe just log error?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
orgs, err := i.orgRepository.Get(ctx, organizations.GetParams{
|
||||||
|
UserId: params.UserId,
|
||||||
|
Ids: params.Ids,
|
||||||
|
OffsetDate: params.OffsetDate,
|
||||||
|
Limit: int64(params.Limit),
|
||||||
|
CursorId: cursor.Id,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error fetch organizations. %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var nextCursor string
|
||||||
|
|
||||||
|
// BUG: pagination by cursor works with errors. empty set as return value
|
||||||
|
if len(orgs) >= 50 || len(orgs) >= int(params.Limit) {
|
||||||
|
cursor.Id = orgs[len(orgs)-1].ID
|
||||||
|
if nextCursor, err = cursor.encode(); err != nil {
|
||||||
|
return nil, fmt.Errorf("error encode next page token. %w", err) // maybe just log error?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ListResponse{
|
||||||
|
Organizations: orgs,
|
||||||
|
NextCursor: nextCursor,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (i *organizationsInteractor) Create(
|
func (i *organizationsInteractor) Create(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
params CreateParams,
|
params CreateParams,
|
||||||
|
@ -3,6 +3,7 @@ package organizations
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -13,7 +14,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type GetParams struct {
|
type GetParams struct {
|
||||||
Ids uuid.UUIDs
|
Ids uuid.UUIDs
|
||||||
|
UserId uuid.UUID
|
||||||
|
|
||||||
|
OffsetDate time.Time
|
||||||
|
CursorId uuid.UUID
|
||||||
|
Limit int64
|
||||||
}
|
}
|
||||||
|
|
||||||
type AddParticipantParams struct {
|
type AddParticipantParams struct {
|
||||||
@ -53,7 +59,12 @@ func (s *repositorySQL) Conn(ctx context.Context) sqltools.DBTX {
|
|||||||
func (r *repositorySQL) Create(ctx context.Context, org models.Organization) error {
|
func (r *repositorySQL) Create(ctx context.Context, org models.Organization) error {
|
||||||
if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) (err error) {
|
if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) (err error) {
|
||||||
query := sq.Insert("organizations").Columns(
|
query := sq.Insert("organizations").Columns(
|
||||||
"id, name, address, wallet_seed, created_at, updated_at",
|
"id",
|
||||||
|
"name",
|
||||||
|
"address",
|
||||||
|
"wallet_seed",
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
).Values(
|
).Values(
|
||||||
org.ID,
|
org.ID,
|
||||||
org.Name,
|
org.Name,
|
||||||
@ -61,7 +72,7 @@ func (r *repositorySQL) Create(ctx context.Context, org models.Organization) err
|
|||||||
org.WalletSeed,
|
org.WalletSeed,
|
||||||
org.CreatedAt,
|
org.CreatedAt,
|
||||||
org.UpdatedAt,
|
org.UpdatedAt,
|
||||||
)
|
).PlaceholderFormat(sq.Dollar)
|
||||||
|
|
||||||
if _, err := query.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil {
|
if _, err := query.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil {
|
||||||
return fmt.Errorf("error insert new organization. %w", err)
|
return fmt.Errorf("error insert new organization. %w", err)
|
||||||
@ -76,9 +87,95 @@ func (r *repositorySQL) Create(ctx context.Context, org models.Organization) err
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *repositorySQL) Get(ctx context.Context, params GetParams) ([]*models.Organization, error) {
|
func (r *repositorySQL) Get(ctx context.Context, params GetParams) ([]*models.Organization, error) {
|
||||||
panic("implement me!")
|
organizations := make([]*models.Organization, 0, params.Limit)
|
||||||
|
|
||||||
return nil, nil
|
if err := sqltools.Transaction(ctx, r.db, func(ctx context.Context) (err error) {
|
||||||
|
query := sq.Select(
|
||||||
|
"o.id",
|
||||||
|
"o.name",
|
||||||
|
"o.address",
|
||||||
|
"o.wallet_seed",
|
||||||
|
"o.created_at",
|
||||||
|
"o.updated_at",
|
||||||
|
).From("organizations as o").
|
||||||
|
Limit(uint64(params.Limit)).
|
||||||
|
PlaceholderFormat(sq.Dollar)
|
||||||
|
|
||||||
|
if params.UserId != uuid.Nil {
|
||||||
|
query = query.InnerJoin("organizations_users as ou on o.id = ou.organization_id").
|
||||||
|
Where(sq.Eq{
|
||||||
|
"ou.user_id": params.UserId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.CursorId != uuid.Nil {
|
||||||
|
query = query.Where(sq.Lt{
|
||||||
|
"o.id": params.CursorId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if params.Ids != nil {
|
||||||
|
query = query.Where(sq.Eq{
|
||||||
|
"o.id": params.Ids,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if !params.OffsetDate.IsZero() {
|
||||||
|
query = query.Where(sq.GtOrEq{
|
||||||
|
"o.updated_at": params.OffsetDate,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(query.ToSql())
|
||||||
|
|
||||||
|
rows, err := query.RunWith(r.Conn(ctx)).QueryContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error fetch organizations from database. %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if closeErr := rows.Close(); closeErr != nil {
|
||||||
|
err = errors.Join(fmt.Errorf("error close rows. %w", closeErr), err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var (
|
||||||
|
id uuid.UUID
|
||||||
|
name string
|
||||||
|
address string
|
||||||
|
walletSeed []byte
|
||||||
|
createdAt time.Time
|
||||||
|
updatedAt time.Time
|
||||||
|
)
|
||||||
|
|
||||||
|
if err = rows.Scan(
|
||||||
|
&id,
|
||||||
|
&name,
|
||||||
|
&address,
|
||||||
|
&walletSeed,
|
||||||
|
&createdAt,
|
||||||
|
&updatedAt,
|
||||||
|
); err != nil {
|
||||||
|
return fmt.Errorf("error scan row. %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
organizations = append(organizations, &models.Organization{
|
||||||
|
ID: id,
|
||||||
|
Name: name,
|
||||||
|
Address: address,
|
||||||
|
WalletSeed: walletSeed,
|
||||||
|
CreatedAt: createdAt,
|
||||||
|
UpdatedAt: updatedAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}); err != nil {
|
||||||
|
return nil, fmt.Errorf("error execute transactional operation. %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return organizations, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *repositorySQL) Update(ctx context.Context, org models.Organization) error {
|
func (r *repositorySQL) Update(ctx context.Context, org models.Organization) error {
|
||||||
@ -109,7 +206,7 @@ func (r *repositorySQL) AddParticipant(ctx context.Context, params AddParticipan
|
|||||||
time.Now(),
|
time.Now(),
|
||||||
time.Now(),
|
time.Now(),
|
||||||
params.IsAdmin,
|
params.IsAdmin,
|
||||||
)
|
).PlaceholderFormat(sq.Dollar)
|
||||||
|
|
||||||
if _, err := query.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil {
|
if _, err := query.RunWith(r.Conn(ctx)).ExecContext(ctx); err != nil {
|
||||||
return fmt.Errorf("error add new participant to organization. %w", err)
|
return fmt.Errorf("error add new participant to organization. %w", err)
|
||||||
|
@ -54,7 +54,7 @@ create index if not exists index_user_id_organization_id
|
|||||||
create table organizations_users (
|
create table organizations_users (
|
||||||
organization_id uuid not null references organizations(id),
|
organization_id uuid not null references organizations(id),
|
||||||
user_id uuid not null references users(id),
|
user_id uuid not null references users(id),
|
||||||
employee_id uuid references employees(id) default null,
|
employee_id uuid default null,
|
||||||
added_at timestamp default current_timestamp,
|
added_at timestamp default current_timestamp,
|
||||||
updated_at timestamp default current_timestamp,
|
updated_at timestamp default current_timestamp,
|
||||||
deleted_at timestamp default null,
|
deleted_at timestamp default null,
|
||||||
|
Loading…
Reference in New Issue
Block a user