Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(SPV-1289): standardize building and processing search queries - part 1. #45

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions admin_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ func (a *AdminAPI) CreateXPub(ctx context.Context, cmd *commands.CreateUserXpub)
//
// The API response is unmarshaled into a *queries.XPubPage struct.
// Returns an error if the API request fails or the response cannot be decoded.
func (a *AdminAPI) XPubs(ctx context.Context, opts ...queries.XPubQueryOption) (*queries.XPubPage, error) {
func (a *AdminAPI) XPubs(ctx context.Context, opts ...queries.QueryOption[filter.XpubFilter]) (*queries.XPubPage, error) {
res, err := a.xpubsAPI.XPubs(ctx, opts...)
if err != nil {
return nil, xpubs.HTTPErrorFormatter("retrieve XPubs page", err).FormatGetErr()
Expand All @@ -83,9 +83,9 @@ func (a *AdminAPI) XPubs(ctx context.Context, opts ...queries.XPubQueryOption) (
//
// The response includes contact data along with pagination details, such as the
// current page, sort order, and sortBy field. Optional query parameters can be
// provided using query options. The result is unmarshaled into a *queries.UserContactsPage.
// provided using query options. The result is unmarshaled into a *queries.ContactsPage.
// Returns an error if the API request fails or the response cannot be decoded.
func (a *AdminAPI) Contacts(ctx context.Context, opts ...queries.ContactQueryOption) (*queries.UserContactsPage, error) {
func (a *AdminAPI) Contacts(ctx context.Context, opts ...queries.QueryOption[filter.ContactFilter]) (*queries.ContactsPage, error) {
res, err := a.contactsAPI.Contacts(ctx, opts...)
if err != nil {
return nil, contacts.HTTPErrorFormatter("retrieve user contacts page", err).FormatGetErr()
Expand Down Expand Up @@ -167,7 +167,7 @@ func (a *AdminAPI) RejectInvitation(ctx context.Context, ID string) error {
// This method allows optional query parameters to be applied via the provided query options.
// The response is expected to be to unmarshal into a *queries.TransactionPage struct.
// Returns an error if the request fails or the response cannot be decoded.
func (a *AdminAPI) Transactions(ctx context.Context, opts ...queries.TransactionsQueryOption) (*queries.TransactionPage, error) {
func (a *AdminAPI) Transactions(ctx context.Context, opts ...queries.QueryOption[filter.AdminTransactionFilter]) (*queries.TransactionPage, error) {
res, err := a.transactionsAPI.Transactions(ctx, opts...)
if err != nil {
return nil, transactions.HTTPErrorFormatter("retrieve transactions page", err).FormatGetErr()
Expand Down Expand Up @@ -196,8 +196,8 @@ func (a *AdminAPI) Transaction(ctx context.Context, ID string) (*response.Transa
// This method allows optional query parameters to be applied via the provided query options.
// The response is expected to unmarshal into a *queries.AccessKeyPage struct.
// Returns an error if the request fails or the response cannot be decoded.
func (a *AdminAPI) AccessKeys(ctx context.Context, accessKeyOpts ...queries.AdminAccessKeyQueryOption) (*queries.AccessKeyPage, error) {
res, err := a.accessKeyAPI.AccessKeys(ctx, accessKeyOpts...)
func (a *AdminAPI) AccessKeys(ctx context.Context, opts ...queries.QueryOption[filter.AdminAccessKeyFilter]) (*queries.AccessKeyPage, error) {
res, err := a.accessKeyAPI.AccessKeys(ctx, opts...)
if err != nil {
return nil, accesskeys.HTTPErrorFormatter("retrieve access keys page ", err).FormatGetErr()
}
Expand Down Expand Up @@ -241,7 +241,7 @@ func (a *AdminAPI) UnsubscribeWebhook(ctx context.Context, cmd *commands.CancelW
// Optional query parameters can be applied using the provided query options.
// The response is unmarshaled into a *queries.UtxosPage struct.
// Returns an error if the request fails or the response cannot be decoded.
func (a *AdminAPI) UTXOs(ctx context.Context, opts ...queries.AdminUtxoQueryOption) (*queries.UtxosPage, error) {
func (a *AdminAPI) UTXOs(ctx context.Context, opts ...queries.QueryOption[filter.AdminUtxoFilter]) (*queries.UtxosPage, error) {
res, err := a.utxosAPI.UTXOs(ctx, opts...)
if err != nil {
return nil, utxos.HTTPErrorFormatter("retrieve utxos page ", err).FormatGetErr()
Expand All @@ -257,9 +257,9 @@ func (a *AdminAPI) UTXOs(ctx context.Context, opts ...queries.AdminUtxoQueryOpti
// Query parameters can be configured using optional query options. These options allow
// filtering based on metadata, pagination settings, or specific paymail attributes.
//
// The API response is unmarshaled into a *queries.PaymailAddressPage struct.
// The API response is unmarshaled into a *queries.PaymailsPage struct.
// Returns an error if the API request fails or the response cannot be decoded.
func (a *AdminAPI) Paymails(ctx context.Context, opts ...queries.PaymailQueryOption[filter.AdminPaymailFilter]) (*queries.PaymailAddressPage, error) {
func (a *AdminAPI) Paymails(ctx context.Context, opts ...queries.QueryOption[filter.AdminPaymailFilter]) (*queries.PaymailsPage, error) {
res, err := a.paymailsAPI.Paymails(ctx, opts...)
if err != nil {
return nil, paymails.HTTPErrorFormatter("retrieve paymail addresses page", err).FormatGetErr()
Expand Down
4 changes: 1 addition & 3 deletions examples/fetch_paymails_as_admin/fetch_paymails_as_admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@ func main() {
log.Fatal(err)
}

page, err := adminAPI.Paymails(context.Background(), queries.PaymailQueryWithPageFilter[filter.AdminPaymailFilter](filter.Page{
Size: 3,
}))
page, err := adminAPI.Paymails(context.Background(), queries.QueryWithPageFilter[filter.AdminPaymailFilter](filter.Page{Size: 1}))
if err != nil {
log.Fatal(err)
}
Expand Down
4 changes: 1 addition & 3 deletions examples/fetch_xpubs_as_admin/fetch_xpubs_as_admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@ func main() {
log.Fatal(err)
}

page, err := adminAPI.XPubs(context.Background(), queries.XPubQueryWithPageFilter(filter.Page{
Size: 1,
}))
page, err := adminAPI.XPubs(context.Background(), queries.QueryWithPageFilter[filter.XpubFilter](filter.Page{Size: 1}))
if err != nil {
log.Fatal(err)
}
Expand Down
11 changes: 4 additions & 7 deletions internal/api/v1/admin/accesskeys/access_keys_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/bitcoin-sv/spv-wallet-go-client/internal/api/v1/errutil"
"github.com/bitcoin-sv/spv-wallet-go-client/internal/api/v1/querybuilders"
"github.com/bitcoin-sv/spv-wallet-go-client/queries"
"github.com/bitcoin-sv/spv-wallet/models/filter"
"github.com/go-resty/resty/v2"
)

Expand All @@ -21,16 +22,12 @@ type API struct {
url *url.URL
}

func (a *API) AccessKeys(ctx context.Context, opts ...queries.AdminAccessKeyQueryOption) (*queries.AccessKeyPage, error) {
var query queries.AdminAccessKeyQuery
for _, o := range opts {
o(&query)
}

func (a *API) AccessKeys(ctx context.Context, opts ...queries.QueryOption[filter.AdminAccessKeyFilter]) (*queries.AccessKeyPage, error) {
query := queries.NewQuery(opts...)
queryBuilder := querybuilders.NewQueryBuilder(
querybuilders.WithMetadataFilter(query.Metadata),
querybuilders.WithPageFilter(query.PageFilter),
querybuilders.WithFilterQueryBuilder(&adminAccessKeyFilterQueryBuilder{adminAccessKeyFilter: query.AdminAccessKeyFilter}),
querybuilders.WithFilterQueryBuilder(&adminAccessKeyFilterQueryBuilder{adminAccessKeyFilter: query.Filter}),
)
params, err := queryBuilder.Build()
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion internal/api/v1/admin/accesskeys/access_keys_api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func TestAccessKeyAPI_AccessKeys(t *testing.T) {
transport.RegisterResponder(http.MethodGet, url, tc.responder)

// when:
got, err := wallet.AccessKeys(context.Background(), queries.AdminAccessKeyQueryWithPageFilter(filter.Page{Size: 1}))
got, err := wallet.AccessKeys(context.Background(), queries.QueryWithPageFilter[filter.AdminAccessKeyFilter](filter.Page{Size: 1}))

// then:
require.ErrorIs(t, err, tc.expectedErr)
Expand Down
15 changes: 6 additions & 9 deletions internal/api/v1/admin/contacts/contacts_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/bitcoin-sv/spv-wallet-go-client/internal/api/v1/querybuilders"
"github.com/bitcoin-sv/spv-wallet-go-client/internal/api/v1/user/contacts"
"github.com/bitcoin-sv/spv-wallet-go-client/queries"
"github.com/bitcoin-sv/spv-wallet/models/filter"
"github.com/bitcoin-sv/spv-wallet/models/response"
"github.com/go-resty/resty/v2"
)
Expand All @@ -24,19 +25,15 @@ type API struct {
url *url.URL
}

func (a *API) Contacts(ctx context.Context, opts ...queries.ContactQueryOption) (*queries.UserContactsPage, error) {
var query queries.ContactQuery
for _, o := range opts {
o(&query)
}

func (a *API) Contacts(ctx context.Context, opts ...queries.QueryOption[filter.ContactFilter]) (*queries.ContactsPage, error) {
query := queries.NewQuery(opts...)
queryBuilder := querybuilders.NewQueryBuilder(
querybuilders.WithMetadataFilter(query.Metadata),
querybuilders.WithPageFilter(query.PageFilter),
querybuilders.WithFilterQueryBuilder(&contacts.ContactFilterQueryBuilder{
ContactFilter: query.ContactFilter,
ContactFilter: query.Filter,
ModelFilterBuilder: querybuilders.ModelFilterBuilder{
ModelFilter: query.ContactFilter.ModelFilter,
ModelFilter: query.Filter.ModelFilter,
},
}),
)
Expand All @@ -45,7 +42,7 @@ func (a *API) Contacts(ctx context.Context, opts ...queries.ContactQueryOption)
return nil, fmt.Errorf("failed to build admin contacts query params: %w", err)
}

var result queries.UserContactsPage
var result queries.ContactsPage
_, err = a.httpClient.
R().
SetContext(ctx).
Expand Down
6 changes: 3 additions & 3 deletions internal/api/v1/admin/contacts/contacts_api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ const (
func TestContactsAPI_Contacts(t *testing.T) {
tests := map[string]struct {
responder httpmock.Responder
expectedResponse *queries.UserContactsPage
expectedResponse *queries.ContactsPage
expectedErr error
}{
"HTTP GET /api/v1/admin/contacts response: 200": {
expectedResponse: contactstest.ExpectedUserContactsPage(t),
expectedResponse: contactstest.ExpectedContactsPage(t),
responder: testutils.NewJSONFileResponderWithStatusOK("contactstest/get_contacts_200.json"),
},
"HTTP GET /api/v1/admin/contacts response: 400": {
Expand All @@ -54,7 +54,7 @@ func TestContactsAPI_Contacts(t *testing.T) {
transport.RegisterResponder(http.MethodGet, url, tc.responder)

// then:
got, err := wallet.Contacts(context.Background(), queries.ContactQueryWithPageFilter(filter.Page{Size: 1}))
got, err := wallet.Contacts(context.Background(), queries.QueryWithPageFilter[filter.ContactFilter](filter.Page{Size: 1}))
require.ErrorIs(t, err, tc.expectedErr)
require.EqualValues(t, tc.expectedResponse, got)
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ func ExpectedUpdatedUserContact(t *testing.T) *response.Contact {
}
}

func ExpectedUserContactsPage(t *testing.T) *queries.UserContactsPage {
return &queries.UserContactsPage{
func ExpectedContactsPage(t *testing.T) *queries.ContactsPage {
return &queries.ContactsPage{
Content: []*response.Contact{
{
Model: response.Model{
Expand Down
12 changes: 4 additions & 8 deletions internal/api/v1/admin/paymails/paymails_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,23 +70,19 @@ func (a *API) Paymail(ctx context.Context, ID string) (*response.PaymailAddress,
return &result, nil
}

func (a *API) Paymails(ctx context.Context, opts ...queries.PaymailQueryOption[filter.AdminPaymailFilter]) (*queries.PaymailAddressPage, error) {
var query queries.PaymailQuery[filter.AdminPaymailFilter]
for _, o := range opts {
o(&query)
}

func (a *API) Paymails(ctx context.Context, opts ...queries.QueryOption[filter.AdminPaymailFilter]) (*queries.PaymailsPage, error) {
query := queries.NewQuery(opts...)
queryBuilder := querybuilders.NewQueryBuilder(
querybuilders.WithMetadataFilter(query.Metadata),
querybuilders.WithPageFilter(query.PageFilter),
querybuilders.WithFilterQueryBuilder(&adminPaymailFilterBuilder{paymailFilter: query.PaymailFilter}),
querybuilders.WithFilterQueryBuilder(&adminPaymailFilterBuilder{paymailFilter: query.Filter}),
)
params, err := queryBuilder.Build()
if err != nil {
return nil, fmt.Errorf("failed to build paymail address query params: %w", err)
}

var result queries.PaymailAddressPage
var result queries.PaymailsPage
_, err = a.httpClient.
R().
SetContext(ctx).
Expand Down
4 changes: 2 additions & 2 deletions internal/api/v1/admin/paymails/paymails_api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ func TestPaymailsAPI_CreatePaymail(t *testing.T) {
func TestPaymailsAPI_Paymails(t *testing.T) {
tests := map[string]struct {
responder httpmock.Responder
expectedResponse *queries.PaymailAddressPage
expectedResponse *queries.PaymailsPage
expectedErr error
}{
"HTTP GET /api/v1/admin/paymails response: 200": {
Expand Down Expand Up @@ -134,7 +134,7 @@ func TestPaymailsAPI_Paymails(t *testing.T) {
transport.RegisterResponder(http.MethodGet, url, tc.responder)

// then:
got, err := wallet.Paymails(context.Background(), queries.PaymailQueryWithPageFilter[filter.AdminPaymailFilter](filter.Page{Size: 1}))
got, err := wallet.Paymails(context.Background(), queries.QueryWithPageFilter[filter.AdminPaymailFilter](filter.Page{Size: 1}))
require.ErrorIs(t, err, tc.expectedErr)
require.EqualValues(t, tc.expectedResponse, got)
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ func ExpectedPaymail(t *testing.T) *response.PaymailAddress {
}
}

func ExpectedPaymailsPage(t *testing.T) *queries.PaymailAddressPage {
return &queries.PaymailAddressPage{
func ExpectedPaymailsPage(t *testing.T) *queries.PaymailsPage {
return &queries.PaymailsPage{
Content: []*response.PaymailAddress{
{
Model: response.Model{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package transactions

import (
"fmt"
"net/url"

"github.com/bitcoin-sv/spv-wallet-go-client/internal/api/v1/querybuilders"
"github.com/bitcoin-sv/spv-wallet-go-client/internal/api/v1/user/transactions"
"github.com/bitcoin-sv/spv-wallet/models/filter"
)

type adminTransactionFilterQueryBuilder struct {
transactionFilter filter.AdminTransactionFilter
}

func (a *adminTransactionFilterQueryBuilder) Build() (url.Values, error) {
builder := transactions.TransactionFilterBuilder{
TransactionFilter: a.transactionFilter.TransactionFilter,
ModelFilterBuilder: querybuilders.ModelFilterBuilder{ModelFilter: a.transactionFilter.ModelFilter},
}
params, err := builder.BuildExtendedURLValues()
if err != nil {
return nil, fmt.Errorf("failed to build extended URL values: %w", err)
}

params.AddPair("xpubid", a.transactionFilter.XPubID) // xpubid should be replaced by xpubId in filter model.
mgosek-4chain marked this conversation as resolved.
Show resolved Hide resolved
return params.Values, nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package transactions

import (
"net/url"
"testing"
"time"

"github.com/bitcoin-sv/spv-wallet/models/filter"
"github.com/stretchr/testify/require"
)

func TestAdminTransactionFilterQueryBuilder_Build(t *testing.T) {
tests := map[string]struct {
filter filter.AdminTransactionFilter
expectedParams url.Values
expectedErr error
}{
"admin transaction filter: zero values": {
expectedParams: make(url.Values),
},
"admin transaction: filter with only 'xPubId' field set": {
filter: filter.AdminTransactionFilter{
XPubID: ptr("9b496655-616a-48cd-a3f8-89608473a5f1"),
},
expectedParams: url.Values{
"xpubid": []string{"9b496655-616a-48cd-a3f8-89608473a5f1"},
},
},
"admin transaction filter: all fields set": {
filter: filter.AdminTransactionFilter{
XPubID: ptr("9b496655-616a-48cd-a3f8-89608473a5f1"),
TransactionFilter: filter.TransactionFilter{
Id: ptr("d425432e0d10a46af1ec6d00f380e9581ebf7907f3486572b3cd561a4c326e14"),
Hex: ptr("001290b87619e679aaf6b8aadd30c778726c89fc4442110feb6d8265a190386beb8311a31e7e97a1c9ff2c84f3993283078965eb81f6fa64f3d7ba7fdd09678d"),
BlockHash: ptr("0000000000000000031928c28075a82d7a00c2c90b489d1d66dc0afa3f8d26f8"),
BlockHeight: ptr(uint64(839376)),
Fee: ptr(uint64(1)),
NumberOfInputs: ptr(uint32(10)),
NumberOfOutputs: ptr(uint32(20)),
DraftID: ptr("d425432e0d10a46af1ec6d00f380e9581ebf7907f3486572b3cd561a4c326e14"),
TotalValue: ptr(uint64(100000000)),
Status: ptr("RECEIVED"),
ModelFilter: filter.ModelFilter{
IncludeDeleted: ptr(true),
CreatedRange: &filter.TimeRange{
From: ptr(time.Date(2021, 1, 1, 0, 0, 0, 0, time.UTC)),
To: ptr(time.Date(2021, 1, 2, 0, 0, 0, 0, time.UTC)),
},
UpdatedRange: &filter.TimeRange{
From: ptr(time.Date(2021, 2, 1, 0, 0, 0, 0, time.UTC)),
To: ptr(time.Date(2021, 2, 2, 0, 0, 0, 0, time.UTC)),
},
},
},
},
expectedParams: url.Values{
"xpubid": []string{"9b496655-616a-48cd-a3f8-89608473a5f1"},
"id": []string{"d425432e0d10a46af1ec6d00f380e9581ebf7907f3486572b3cd561a4c326e14"},
"hex": []string{"001290b87619e679aaf6b8aadd30c778726c89fc4442110feb6d8265a190386beb8311a31e7e97a1c9ff2c84f3993283078965eb81f6fa64f3d7ba7fdd09678d"},
"blockHash": []string{"0000000000000000031928c28075a82d7a00c2c90b489d1d66dc0afa3f8d26f8"},
"blockHeight": []string{"839376"},
"fee": []string{"1"},
"numberOfInputs": []string{"10"},
"numberOfOutputs": []string{"20"},
"draftId": []string{"d425432e0d10a46af1ec6d00f380e9581ebf7907f3486572b3cd561a4c326e14"},
"totalValue": []string{"100000000"},
"status": []string{"RECEIVED"},
"includeDeleted": []string{"true"},
"createdRange[from]": []string{"2021-01-01T00:00:00Z"},
"createdRange[to]": []string{"2021-01-02T00:00:00Z"},
"updatedRange[from]": []string{"2021-02-01T00:00:00Z"},
"updatedRange[to]": []string{"2021-02-02T00:00:00Z"},
},
},
}

for name, tc := range tests {
t.Run(name, func(t *testing.T) {
// when:
queryBuilder := adminTransactionFilterQueryBuilder{transactionFilter: tc.filter}

// then:
got, err := queryBuilder.Build()
require.ErrorIs(t, tc.expectedErr, err)
require.Equal(t, tc.expectedParams, got)
})
}
}

func ptr[T any](value T) *T { return &value }
Loading
Loading