diff --git a/backend/internal/interface/rest/domain/dto.go b/backend/internal/interface/rest/domain/dto.go index e649eb7..471de17 100644 --- a/backend/internal/interface/rest/domain/dto.go +++ b/backend/internal/interface/rest/domain/dto.go @@ -3,11 +3,14 @@ package domain import ( "encoding/json" "fmt" + + "github.com/emochka2007/block-accounting/internal/interface/rest/domain/hal" ) // Generic type Collection[T any] struct { + *hal.Resource Items []T `json:"items,omitempty"` Pagination Pagination `json:"pagination,omitempty"` } @@ -50,20 +53,12 @@ type NewOrganizationRequest struct { 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) { var req T diff --git a/backend/internal/interface/rest/domain/hal/hal.go b/backend/internal/interface/rest/domain/hal/hal.go new file mode 100644 index 0000000..2d428d8 --- /dev/null +++ b/backend/internal/interface/rest/domain/hal/hal.go @@ -0,0 +1,69 @@ +package hal + +type Link struct { + Href string `json:"href"` + Title string `json:"title,omitempty"` + Method string `json:"method,omitempty"` +} + +type Resource struct { + Type string `json:"_type,omitempty"` + Links map[string]Link `json:"_links"` + Embedded map[string]any `json:"_embedded,omitempty"` +} + +type Payload interface{} + +type NewResourceOption func(r *Resource) + +func NewResource(selfLink string, opts ...NewResourceOption) *Resource { + r := &Resource{ + Links: map[string]Link{ + "self": { + Href: selfLink, + }, + }, + } + + for _, o := range opts { + o(r) + } + + return r +} + +func WithType(t string) NewResourceOption { + return func(r *Resource) { + r.Type = t + } +} + +func WithSelfTitle(t string) NewResourceOption { + return func(r *Resource) { + if l, ok := r.Links["self"]; ok { + l.Title = t + + r.Links["self"] = l + } + } +} + +func (r *Resource) Embed(relation string, resource *Resource) { + if r.Embedded == nil { + r.Embedded = make(map[string]any, 1) + } + + r.Embedded[relation] = resource +} + +func (r *Resource) AddLink(relation string, link Link) { + if r.Links == nil { + r.Links = make(map[string]Link, 1) + } + + r.Links[relation] = link +} + +func (r *Resource) SetType(t string) { + r.Type = t +} diff --git a/backend/internal/interface/rest/domain/organization.go b/backend/internal/interface/rest/domain/organization.go index d61926a..6896d9a 100644 --- a/backend/internal/interface/rest/domain/organization.go +++ b/backend/internal/interface/rest/domain/organization.go @@ -1,6 +1,11 @@ package domain +import ( + "github.com/emochka2007/block-accounting/internal/interface/rest/domain/hal" +) + type Organization struct { + *hal.Resource Id string `json:"id"` Name string `json:"name"` Address string `json:"address"` diff --git a/backend/internal/interface/rest/presenters/organizations.go b/backend/internal/interface/rest/presenters/organizations.go index 6b24f3e..c1251ad 100644 --- a/backend/internal/interface/rest/presenters/organizations.go +++ b/backend/internal/interface/rest/presenters/organizations.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/emochka2007/block-accounting/internal/interface/rest/domain" + "github.com/emochka2007/block-accounting/internal/interface/rest/domain/hal" "github.com/emochka2007/block-accounting/internal/pkg/models" ) @@ -22,17 +23,20 @@ func NewOrganizationsPresenter() 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()), - }, + org := 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) + r := hal.NewResource( + "/organizations/"+org.Id, + hal.WithType("organization"), + ) + + out, err := json.Marshal(r) if err != nil { return nil, fmt.Errorf("error marshal organization create response. %w", err) } @@ -41,17 +45,19 @@ func (p *organizationsPresenter) ResponseCreate(o *models.Organization) ([]byte, } 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)), - }, + dtoOrgs := domain.Collection[domain.Organization]{ + Resource: hal.NewResource( + "/organizations", + hal.WithType("organizations"), + ), + Items: p.Organizations(orgs), + Pagination: domain.Pagination{ + NextCursor: nextCursor, + TotalItems: uint32(len(orgs)), }, } - out, err := json.Marshal(resp) + out, err := json.Marshal(dtoOrgs) if err != nil { return nil, fmt.Errorf("error marshal organizations list response. %w", err) } @@ -63,13 +69,16 @@ func (p *organizationsPresenter) Organizations(orgs []*models.Organization) []do out := make([]domain.Organization, len(orgs)) for i, o := range orgs { - out[i] = domain.Organization{ + org := domain.Organization{ + Resource: hal.NewResource("/organizations/" + o.ID.String()), Id: o.ID.String(), Name: o.Name, Address: o.Address, CreatedAt: uint64(o.CreatedAt.UnixMilli()), UpdatedAt: uint64(o.UpdatedAt.UnixMilli()), } + + out[i] = org } return out diff --git a/backend/migrations/blockd.sql b/backend/migrations/blockd.sql index faf8349..52c195f 100644 --- a/backend/migrations/blockd.sql +++ b/backend/migrations/blockd.sql @@ -113,4 +113,16 @@ create table transactions_confirmations ( ); create index if not exists index_transactions_confirmations_tx_id_user_id_organization_id - on transactions_confirmations (tx_id, user_id, organization_id); \ No newline at end of file + on transactions_confirmations (tx_id, user_id, organization_id); + +create table contracts ( + id uuid primary key, + title varchar(250) default 'New Contract', + description text not null, + + created_by uuid not null references users(id), + organization_id uuid not null references organizations(id), + + created_at timestamp default current_timestamp, + updated_at timestamp default current_timestamp +);