From c9fe3f62951f15819d8e04a39663661012d99104 Mon Sep 17 00:00:00 2001 From: Jakub Kowalski <155538368+jakubmkowalski@users.noreply.github.com> Date: Tue, 17 Dec 2024 14:36:45 +0100 Subject: [PATCH] feat(SPV-1214): add contact filter for admin (#815) --- actions/admin/contact.go | 10 +- actions/admin/contact_old.go | 8 +- docs/docs.go | 103 ++++++++++++++++++++- docs/swagger.json | 103 ++++++++++++++++++++- docs/swagger.yaml | 79 ++++++++++++++-- models/contact.go | 2 + models/filter/contact_admin_filter.go | 20 ++++ models/filter/contact_admin_filter_test.go | 73 +++++++++++++++ models/filter/models.go | 3 + 9 files changed, 376 insertions(+), 25 deletions(-) create mode 100644 models/filter/contact_admin_filter.go create mode 100644 models/filter/contact_admin_filter_test.go diff --git a/actions/admin/contact.go b/actions/admin/contact.go index 6a8a11615..f7811b30d 100644 --- a/actions/admin/contact.go +++ b/actions/admin/contact.go @@ -15,15 +15,15 @@ import ( "github.com/gin-gonic/gin" ) -// contactsSearch will fetch a list of contacts filtered by Metadata and ContactFilters -// Search for contacts filtering by metadata and ContactFilters godoc +// contactsSearch will fetch a list of contacts filtered by Metadata and AdminContactFilters +// Search for contacts filtering by metadata and AdminContactFilters godoc // @Summary Search for contacts // @Description Search for contacts // @Tags Admin // @Produce json -// @Param SearchContacts body filter.SearchContacts false "Supports targeted resource searches with filters and metadata, plus options for pagination and sorting to streamline data exploration and analysis" +// @Param AdminSearchContacts body filter.AdminContactFilter false "Supports targeted resource searches with filters and metadata, plus options for pagination and sorting to streamline data exploration and analysis" // @Success 200 {object} response.PageModel[response.Contact] "List of contacts" -// @Failure 400 "Bad request - Error while parsing SearchContacts from request body" +// @Failure 400 "Bad request - Error while parsing AdminSearchContacts from request body" // @Failure 500 "Internal server error - Error while searching for contacts" // @Router /api/v1/admin/contacts [get] // @Security x-auth-xpub @@ -31,7 +31,7 @@ func contactsSearch(c *gin.Context, _ *reqctx.AdminContext) { logger := reqctx.Logger(c) engine := reqctx.Engine(c) - searchParams, err := query.ParseSearchParams[filter.ContactFilter](c) + searchParams, err := query.ParseSearchParams[filter.AdminContactFilter](c) if err != nil { spverrors.ErrorResponse(c, spverrors.ErrCannotParseQueryParams.WithTrace(err), logger) return diff --git a/actions/admin/contact_old.go b/actions/admin/contact_old.go index a34bded13..e0459b87d 100644 --- a/actions/admin/contact_old.go +++ b/actions/admin/contact_old.go @@ -13,14 +13,14 @@ import ( "github.com/gin-gonic/gin" ) -// contactsSearchOld will fetch a list of contacts filtered by Metadata and ContactFilters -// Search for contacts filtering by metadata and ContactFilters godoc +// contactsSearchOld will fetch a list of contacts filtered by Metadata and AdminContactFilters +// Search for contacts filtering by metadata and AdminContactFilters godoc // @DeprecatedRouter /v1/admin/contact/search [post] // @Summary Search for contacts // @Description Search for contacts // @Tags Admin // @Produce json -// @Param SearchContacts body filter.SearchContacts false "Supports targeted resource searches with filters and metadata, plus options for pagination and sorting to streamline data exploration and analysis" +// @Param AdminSearchContacts body filter.AdminSearchContacts false "Supports targeted resource searches with filters and metadata, plus options for pagination and sorting to streamline data exploration and analysis" // @Success 200 {object} models.SearchContactsResponse "List of contacts" // @Failure 400 "Bad request - Error while parsing SearchContacts from request body" // @Failure 500 "Internal server error - Error while searching for contacts" @@ -29,7 +29,7 @@ import ( func contactsSearchOld(c *gin.Context, _ *reqctx.AdminContext) { logger := reqctx.Logger(c) engine := reqctx.Engine(c) - var reqParams filter.SearchContacts + var reqParams filter.AdminSearchContacts if err := c.Bind(&reqParams); err != nil { spverrors.ErrorResponse(c, spverrors.ErrCannotBindRequest, logger) return diff --git a/docs/docs.go b/docs/docs.go index 8fd1f7b26..b0caab248 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -78,10 +78,10 @@ const docTemplate = `{ "parameters": [ { "description": "Supports targeted resource searches with filters and metadata, plus options for pagination and sorting to streamline data exploration and analysis", - "name": "SearchContacts", + "name": "AdminSearchContacts", "in": "body", "schema": { - "$ref": "#/definitions/filter.SearchContacts" + "$ref": "#/definitions/filter.AdminContactFilter" } } ], @@ -93,7 +93,7 @@ const docTemplate = `{ } }, "400": { - "description": "Bad request - Error while parsing SearchContacts from request body" + "description": "Bad request - Error while parsing AdminSearchContacts from request body" }, "500": { "description": "Internal server error - Error while searching for contacts" @@ -2605,10 +2605,10 @@ const docTemplate = `{ "parameters": [ { "description": "Supports targeted resource searches with filters and metadata, plus options for pagination and sorting to streamline data exploration and analysis", - "name": "SearchContacts", + "name": "AdminSearchContacts", "in": "body", "schema": { - "$ref": "#/definitions/filter.SearchContacts" + "$ref": "#/definitions/filter.AdminSearchContacts" } } ], @@ -4864,6 +4864,62 @@ const docTemplate = `{ } } }, + "filter.AdminContactFilter": { + "type": "object", + "properties": { + "createdRange": { + "description": "CreatedRange specifies the time range when a record was created.", + "allOf": [ + { + "$ref": "#/definitions/filter.TimeRange" + } + ] + }, + "fullName": { + "type": "string", + "example": "Alice" + }, + "id": { + "type": "string", + "example": "ffdbe74e-0700-4710-aac5-611a1f877c7f" + }, + "includeDeleted": { + "description": "IncludeDeleted is a flag whether or not to include deleted items in the search results", + "type": "boolean", + "default": false, + "example": true + }, + "paymail": { + "type": "string", + "example": "alice@example.com" + }, + "pubKey": { + "type": "string", + "example": "0334f01ecb971e93db179e6fb320cd1466beb0c1ec6c1c6a37aa6cb02e53d5dd1a" + }, + "status": { + "type": "string", + "enum": [ + "unconfirmed", + "awaiting", + "confirmed", + "rejected" + ] + }, + "updatedRange": { + "description": "UpdatedRange specifies the time range when a record was updated.", + "allOf": [ + { + "$ref": "#/definitions/filter.TimeRange" + } + ] + }, + "xpubid": { + "type": "string", + "example": "623bc25ce1c0fc510dea72b5ee27b2e70384c099f1f3dce9e73dd987198c3486" + } + } + }, "filter.AdminCountAccessKeys": { "type": "object", "properties": { @@ -5020,6 +5076,43 @@ const docTemplate = `{ } } }, + "filter.AdminSearchContacts": { + "type": "object", + "properties": { + "conditions": { + "description": "Custom conditions used for filtering the search results. Every field within the object is optional.", + "allOf": [ + { + "$ref": "#/definitions/filter.AdminContactFilter" + } + ] + }, + "metadata": { + "description": "Accepts a JSON object for embedding custom metadata, enabling arbitrary additional information to be associated with the resource", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "example": { + "key": "value", + "key2": "value2" + } + }, + "params": { + "description": "Pagination and sorting options to streamline data exploration and analysis", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "example": { + "order_by_direction": "desc", + "order_by_field": "created_at", + "page": "1", + "page_size": "10" + } + } + } + }, "filter.AdminSearchPaymails": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 08adce9b9..7edbe4038 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -69,10 +69,10 @@ "parameters": [ { "description": "Supports targeted resource searches with filters and metadata, plus options for pagination and sorting to streamline data exploration and analysis", - "name": "SearchContacts", + "name": "AdminSearchContacts", "in": "body", "schema": { - "$ref": "#/definitions/filter.SearchContacts" + "$ref": "#/definitions/filter.AdminContactFilter" } } ], @@ -84,7 +84,7 @@ } }, "400": { - "description": "Bad request - Error while parsing SearchContacts from request body" + "description": "Bad request - Error while parsing AdminSearchContacts from request body" }, "500": { "description": "Internal server error - Error while searching for contacts" @@ -2596,10 +2596,10 @@ "parameters": [ { "description": "Supports targeted resource searches with filters and metadata, plus options for pagination and sorting to streamline data exploration and analysis", - "name": "SearchContacts", + "name": "AdminSearchContacts", "in": "body", "schema": { - "$ref": "#/definitions/filter.SearchContacts" + "$ref": "#/definitions/filter.AdminSearchContacts" } } ], @@ -4855,6 +4855,62 @@ } } }, + "filter.AdminContactFilter": { + "type": "object", + "properties": { + "createdRange": { + "description": "CreatedRange specifies the time range when a record was created.", + "allOf": [ + { + "$ref": "#/definitions/filter.TimeRange" + } + ] + }, + "fullName": { + "type": "string", + "example": "Alice" + }, + "id": { + "type": "string", + "example": "ffdbe74e-0700-4710-aac5-611a1f877c7f" + }, + "includeDeleted": { + "description": "IncludeDeleted is a flag whether or not to include deleted items in the search results", + "type": "boolean", + "default": false, + "example": true + }, + "paymail": { + "type": "string", + "example": "alice@example.com" + }, + "pubKey": { + "type": "string", + "example": "0334f01ecb971e93db179e6fb320cd1466beb0c1ec6c1c6a37aa6cb02e53d5dd1a" + }, + "status": { + "type": "string", + "enum": [ + "unconfirmed", + "awaiting", + "confirmed", + "rejected" + ] + }, + "updatedRange": { + "description": "UpdatedRange specifies the time range when a record was updated.", + "allOf": [ + { + "$ref": "#/definitions/filter.TimeRange" + } + ] + }, + "xpubid": { + "type": "string", + "example": "623bc25ce1c0fc510dea72b5ee27b2e70384c099f1f3dce9e73dd987198c3486" + } + } + }, "filter.AdminCountAccessKeys": { "type": "object", "properties": { @@ -5011,6 +5067,43 @@ } } }, + "filter.AdminSearchContacts": { + "type": "object", + "properties": { + "conditions": { + "description": "Custom conditions used for filtering the search results. Every field within the object is optional.", + "allOf": [ + { + "$ref": "#/definitions/filter.AdminContactFilter" + } + ] + }, + "metadata": { + "description": "Accepts a JSON object for embedding custom metadata, enabling arbitrary additional information to be associated with the resource", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "example": { + "key": "value", + "key2": "value2" + } + }, + "params": { + "description": "Pagination and sorting options to streamline data exploration and analysis", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "example": { + "order_by_direction": "desc", + "order_by_field": "created_at", + "page": "1", + "page_size": "10" + } + } + } + }, "filter.AdminSearchPaymails": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 7d72d8b33..ad54b1e20 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -188,6 +188,45 @@ definitions: xpubId: type: string type: object + filter.AdminContactFilter: + properties: + createdRange: + allOf: + - $ref: '#/definitions/filter.TimeRange' + description: CreatedRange specifies the time range when a record was created. + fullName: + example: Alice + type: string + id: + example: ffdbe74e-0700-4710-aac5-611a1f877c7f + type: string + includeDeleted: + default: false + description: IncludeDeleted is a flag whether or not to include deleted items + in the search results + example: true + type: boolean + paymail: + example: alice@example.com + type: string + pubKey: + example: 0334f01ecb971e93db179e6fb320cd1466beb0c1ec6c1c6a37aa6cb02e53d5dd1a + type: string + status: + enum: + - unconfirmed + - awaiting + - confirmed + - rejected + type: string + updatedRange: + allOf: + - $ref: '#/definitions/filter.TimeRange' + description: UpdatedRange specifies the time range when a record was updated. + xpubid: + example: 623bc25ce1c0fc510dea72b5ee27b2e70384c099f1f3dce9e73dd987198c3486 + type: string + type: object filter.AdminCountAccessKeys: properties: conditions: @@ -299,6 +338,34 @@ definitions: page_size: "10" type: object type: object + filter.AdminSearchContacts: + properties: + conditions: + allOf: + - $ref: '#/definitions/filter.AdminContactFilter' + description: Custom conditions used for filtering the search results. Every + field within the object is optional. + metadata: + additionalProperties: + type: string + description: Accepts a JSON object for embedding custom metadata, enabling + arbitrary additional information to be associated with the resource + example: + key: value + key2: value2 + type: object + params: + additionalProperties: + type: string + description: Pagination and sorting options to streamline data exploration + and analysis + example: + order_by_direction: desc + order_by_field: created_at + page: "1" + page_size: "10" + type: object + type: object filter.AdminSearchPaymails: properties: conditions: @@ -2718,9 +2785,9 @@ paths: plus options for pagination and sorting to streamline data exploration and analysis in: body - name: SearchContacts + name: AdminSearchContacts schema: - $ref: '#/definitions/filter.SearchContacts' + $ref: '#/definitions/filter.AdminContactFilter' produces: - application/json responses: @@ -2729,8 +2796,8 @@ paths: schema: $ref: '#/definitions/response.PageModel-response_Contact' "400": - description: Bad request - Error while parsing SearchContacts from request - body + description: Bad request - Error while parsing AdminSearchContacts from + request body "500": description: Internal server error - Error while searching for contacts security: @@ -4444,9 +4511,9 @@ paths: plus options for pagination and sorting to streamline data exploration and analysis in: body - name: SearchContacts + name: AdminSearchContacts schema: - $ref: '#/definitions/filter.SearchContacts' + $ref: '#/definitions/filter.AdminSearchContacts' produces: - application/json responses: diff --git a/models/contact.go b/models/contact.go index 06b17dcb2..7d7a8a0c2 100644 --- a/models/contact.go +++ b/models/contact.go @@ -26,6 +26,8 @@ type Contact struct { // Status is a contact's current status. Status response.ContactStatus `json:"status" example:"unconfirmed"` } + +// AdminConfirmContactPair is a model for request to confirm contact pair. type AdminConfirmContactPair struct { PaymailA string `json:"paymailA"` PaymailB string `json:"paymailB"` diff --git a/models/filter/contact_admin_filter.go b/models/filter/contact_admin_filter.go new file mode 100644 index 000000000..207178800 --- /dev/null +++ b/models/filter/contact_admin_filter.go @@ -0,0 +1,20 @@ +package filter + +// AdminContactFilter extends ContactFilter for admin-specific use, including xpubid filtering +type AdminContactFilter struct { + //nolint:staticcheck // SA5008 We want to reuse json tags also to mapstructure. + ContactFilter `json:",inline,squash"` + XPubID *string `json:"xpubid,omitempty" example:"623bc25ce1c0fc510dea72b5ee27b2e70384c099f1f3dce9e73dd987198c3486"` +} + +// ToDbConditions converts filter fields to the datastore conditions for admin-specific queries +func (f *AdminContactFilter) ToDbConditions() (map[string]interface{}, error) { + conditions, err := f.ContactFilter.ToDbConditions() + if err != nil { + return nil, err + } + + applyIfNotNil(conditions, "xpub_id", f.XPubID) + + return conditions, nil +} diff --git a/models/filter/contact_admin_filter_test.go b/models/filter/contact_admin_filter_test.go new file mode 100644 index 000000000..6084ae7a1 --- /dev/null +++ b/models/filter/contact_admin_filter_test.go @@ -0,0 +1,73 @@ +package filter + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAdminContactFilter(t *testing.T) { + tests := []struct { + name string + filter AdminContactFilter + want map[string]interface{} + wantErr bool + }{ + { + name: "Empty filter", + filter: AdminContactFilter{}, + want: map[string]interface{}{ + "deleted_at": nil, + }, + wantErr: false, + }, + { + name: "With XPubID", + filter: AdminContactFilter{ + XPubID: ptrString("623bc25ce1c0fc510dea72b5ee27b2e70384c099f1f3dce9e73dd987198c3486"), + }, + want: map[string]interface{}{ + "xpub_id": "623bc25ce1c0fc510dea72b5ee27b2e70384c099f1f3dce9e73dd987198c3486", + "deleted_at": nil, + }, + wantErr: false, + }, + { + name: "With ContactFilter conditions", + filter: AdminContactFilter{ContactFilter: ContactFilter{ + Paymail: ptrString("test@example.com"), + ModelFilter: ModelFilter{ + IncludeDeleted: ptrBool(true), + }, + }, + XPubID: ptrString("623bc25ce1c0fc510dea72b5ee27b2e70384c099f1f3dce9e73dd987198c3486"), + }, + want: map[string]interface{}{ + "paymail": "test@example.com", + "xpub_id": "623bc25ce1c0fc510dea72b5ee27b2e70384c099f1f3dce9e73dd987198c3486", + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := tt.filter.ToDbConditions() + if tt.wantErr { + assert.Error(t, err) + assert.Nil(t, got) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.want, got) + } + }) + } +} + +func ptrString(s string) *string { + return &s +} + +func ptrBool(b bool) *bool { + return &b +} diff --git a/models/filter/models.go b/models/filter/models.go index ff48818ed..c5b493e7f 100644 --- a/models/filter/models.go +++ b/models/filter/models.go @@ -9,6 +9,9 @@ type CountDestinations = ConditionsModel[DestinationFilter] // SearchContacts is a model for handling searching with filters and metadata type SearchContacts = SearchModel[ContactFilter] +// AdminSearchContacts is a model for handling searching with filters and metadata +type AdminSearchContacts = SearchModel[AdminContactFilter] + // AdminSearchPaymails is a model for handling searching with filters and metadata type AdminSearchPaymails = SearchModel[AdminPaymailFilter]