mirror of
https://github.com/absmach/magistrala.git
synced 2026-06-23 04:10:28 +00:00
SMQ-3338 - Add created at period filter to entities (#3339)
Signed-off-by: Felix Gateru <felix.gateru@gmail.com>
This commit is contained in:
+39
-15
@@ -8,6 +8,7 @@ import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
api "github.com/absmach/supermq/api/http"
|
||||
apiutil "github.com/absmach/supermq/api/http/util"
|
||||
@@ -301,22 +302,45 @@ func decodePageMeta(r *http.Request) (groups.PageMeta, error) {
|
||||
tq = groups.ToTagsQuery(tags)
|
||||
}
|
||||
|
||||
cfrom, err := apiutil.ReadStringQuery(r, "created_from", "")
|
||||
if err != nil {
|
||||
return groups.PageMeta{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
cto, err := apiutil.ReadStringQuery(r, "created_to", "")
|
||||
if err != nil {
|
||||
return groups.PageMeta{}, errors.Wrap(apiutil.ErrValidation, err)
|
||||
}
|
||||
|
||||
var createdFrom, createdTo time.Time
|
||||
if cfrom != "" {
|
||||
if createdFrom, err = time.Parse(time.RFC3339, cfrom); err != nil {
|
||||
return groups.PageMeta{}, errors.Wrap(apiutil.ErrInvalidQueryParams, err)
|
||||
}
|
||||
}
|
||||
if cto != "" {
|
||||
if createdTo, err = time.Parse(time.RFC3339, cto); err != nil {
|
||||
return groups.PageMeta{}, errors.Wrap(apiutil.ErrInvalidQueryParams, err)
|
||||
}
|
||||
}
|
||||
|
||||
ret := groups.PageMeta{
|
||||
Offset: offset,
|
||||
Limit: limit,
|
||||
Name: name,
|
||||
ID: id,
|
||||
Metadata: meta,
|
||||
Status: st,
|
||||
RoleName: roleName,
|
||||
RoleID: roleID,
|
||||
Actions: actions,
|
||||
AccessType: accessType,
|
||||
RootGroup: rootGroup,
|
||||
OnlyTotal: ot,
|
||||
Order: order,
|
||||
Dir: dir,
|
||||
Tags: tq,
|
||||
Offset: offset,
|
||||
Limit: limit,
|
||||
Name: name,
|
||||
ID: id,
|
||||
Metadata: meta,
|
||||
Status: st,
|
||||
RoleName: roleName,
|
||||
RoleID: roleID,
|
||||
Actions: actions,
|
||||
AccessType: accessType,
|
||||
RootGroup: rootGroup,
|
||||
OnlyTotal: ot,
|
||||
Order: order,
|
||||
Dir: dir,
|
||||
Tags: tq,
|
||||
CreatedFrom: createdFrom,
|
||||
CreatedTo: createdTo,
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
api "github.com/absmach/supermq/api/http"
|
||||
apiutil "github.com/absmach/supermq/api/http/util"
|
||||
@@ -68,6 +69,61 @@ func TestDecodeListGroupsRequest(t *testing.T) {
|
||||
resp: nil,
|
||||
err: apiutil.ErrValidation,
|
||||
},
|
||||
{
|
||||
desc: "valid request with created_from parameter",
|
||||
url: "http://localhost:8080?created_from=2024-01-01T00:00:00Z",
|
||||
resp: listGroupsReq{
|
||||
PageMeta: groups.PageMeta{
|
||||
Limit: 10,
|
||||
Actions: []string{},
|
||||
Dir: "desc",
|
||||
Order: "updated_at",
|
||||
CreatedFrom: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "valid request with created_to parameter",
|
||||
url: "http://localhost:8080?created_to=2024-12-31T23:59:59Z",
|
||||
resp: listGroupsReq{
|
||||
PageMeta: groups.PageMeta{
|
||||
Limit: 10,
|
||||
Actions: []string{},
|
||||
Dir: "desc",
|
||||
Order: "updated_at",
|
||||
CreatedTo: time.Date(2024, 12, 31, 23, 59, 59, 0, time.UTC),
|
||||
},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "valid request with both created_from and created_to parameters",
|
||||
url: "http://localhost:8080?created_from=2024-01-01T00:00:00Z&created_to=2024-12-31T23:59:59Z",
|
||||
resp: listGroupsReq{
|
||||
PageMeta: groups.PageMeta{
|
||||
Limit: 10,
|
||||
Actions: []string{},
|
||||
Dir: "desc",
|
||||
Order: "updated_at",
|
||||
CreatedFrom: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
|
||||
CreatedTo: time.Date(2024, 12, 31, 23, 59, 59, 0, time.UTC),
|
||||
},
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "invalid request with malformed created_from",
|
||||
url: "http://localhost:8080?created_from=invalid-timestamp",
|
||||
resp: nil,
|
||||
err: apiutil.ErrInvalidQueryParams,
|
||||
},
|
||||
{
|
||||
desc: "invalid request with malformed created_to",
|
||||
url: "http://localhost:8080?created_to=invalid-timestamp",
|
||||
resp: nil,
|
||||
err: apiutil.ErrInvalidQueryParams,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
|
||||
@@ -49,10 +49,11 @@ var (
|
||||
UpdatedBy: testsutil.GenerateUUID(&testing.T{}),
|
||||
Status: groups.EnabledStatus,
|
||||
}
|
||||
validID = testsutil.GenerateUUID(&testing.T{})
|
||||
validToken = "validToken"
|
||||
invalidToken = "invalidToken"
|
||||
contentType = "application/json"
|
||||
validID = testsutil.GenerateUUID(&testing.T{})
|
||||
validToken = "validToken"
|
||||
invalidToken = "invalidToken"
|
||||
contentType = "application/json"
|
||||
validTimeStamp = time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
|
||||
)
|
||||
|
||||
func newGroupsServer() (*httptest.Server, *mocks.Service, *authnmocks.Authentication) {
|
||||
@@ -1121,6 +1122,89 @@ func TestListGroups(t *testing.T) {
|
||||
status: http.StatusBadRequest,
|
||||
err: apiutil.ErrInvalidQueryParams,
|
||||
},
|
||||
{
|
||||
desc: "list groups with created_from",
|
||||
domainID: validID,
|
||||
token: validToken,
|
||||
pageMeta: groups.PageMeta{
|
||||
Offset: 0,
|
||||
Limit: 10,
|
||||
Order: api.DefOrder,
|
||||
Dir: api.DefDir,
|
||||
Actions: []string{},
|
||||
CreatedFrom: validTimeStamp,
|
||||
},
|
||||
listGroupsResponse: groups.Page{
|
||||
PageMeta: groups.PageMeta{
|
||||
Total: 1,
|
||||
},
|
||||
Groups: []groups.Group{validGroupResp},
|
||||
},
|
||||
query: "created_from=2024-01-01T00:00:00Z",
|
||||
status: http.StatusOK,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list groups with created_to",
|
||||
domainID: validID,
|
||||
token: validToken,
|
||||
pageMeta: groups.PageMeta{
|
||||
Offset: 0,
|
||||
Limit: 10,
|
||||
Order: api.DefOrder,
|
||||
Dir: api.DefDir,
|
||||
Actions: []string{},
|
||||
CreatedTo: validTimeStamp,
|
||||
},
|
||||
listGroupsResponse: groups.Page{
|
||||
PageMeta: groups.PageMeta{
|
||||
Total: 1,
|
||||
},
|
||||
Groups: []groups.Group{validGroupResp},
|
||||
},
|
||||
query: "created_to=2024-01-01T00:00:00Z",
|
||||
status: http.StatusOK,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list groups with both created_from and created_to",
|
||||
domainID: validID,
|
||||
token: validToken,
|
||||
pageMeta: groups.PageMeta{
|
||||
Offset: 0,
|
||||
Limit: 10,
|
||||
Order: api.DefOrder,
|
||||
Dir: api.DefDir,
|
||||
Actions: []string{},
|
||||
CreatedFrom: validTimeStamp,
|
||||
CreatedTo: validTimeStamp,
|
||||
},
|
||||
listGroupsResponse: groups.Page{
|
||||
PageMeta: groups.PageMeta{
|
||||
Total: 1,
|
||||
},
|
||||
Groups: []groups.Group{validGroupResp},
|
||||
},
|
||||
query: "created_from=2024-01-01T00:00:00Z&created_to=2024-01-01T00:00:00Z",
|
||||
status: http.StatusOK,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list groups with invalid created_from",
|
||||
domainID: validID,
|
||||
token: validToken,
|
||||
query: "created_from=invalid-timestamp",
|
||||
status: http.StatusBadRequest,
|
||||
err: apiutil.ErrInvalidQueryParams,
|
||||
},
|
||||
{
|
||||
desc: "list groups with invalid created_to",
|
||||
domainID: validID,
|
||||
token: validToken,
|
||||
query: "created_to=invalid-timestamp",
|
||||
status: http.StatusBadRequest,
|
||||
err: apiutil.ErrInvalidQueryParams,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
|
||||
+24
-19
@@ -3,7 +3,10 @@
|
||||
|
||||
package groups
|
||||
|
||||
import "strings"
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Operator uint8
|
||||
|
||||
@@ -38,22 +41,24 @@ func ToTagsQuery(s string) TagsQuery {
|
||||
|
||||
// PageMeta contains page metadata that helps navigation.
|
||||
type PageMeta struct {
|
||||
Total uint64 `json:"total"`
|
||||
Offset uint64 `json:"offset"`
|
||||
Limit uint64 `json:"limit"`
|
||||
OnlyTotal bool `json:"only_total"`
|
||||
Name string `json:"name,omitempty"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Dir string `json:"dir,omitempty"`
|
||||
Order string `json:"order,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
DomainID string `json:"domain_id,omitempty"`
|
||||
Tags TagsQuery `json:"tags,omitempty"`
|
||||
Metadata Metadata `json:"metadata,omitempty"`
|
||||
Status Status `json:"status,omitempty"`
|
||||
RoleName string `json:"role_name,omitempty"`
|
||||
RoleID string `json:"role_id,omitempty"`
|
||||
Actions []string `json:"actions,omitempty"`
|
||||
AccessType string `json:"access_type,omitempty"`
|
||||
RootGroup bool `json:"root_group,omitempty"`
|
||||
Total uint64 `json:"total"`
|
||||
Offset uint64 `json:"offset"`
|
||||
Limit uint64 `json:"limit"`
|
||||
OnlyTotal bool `json:"only_total"`
|
||||
Name string `json:"name,omitempty"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Dir string `json:"dir,omitempty"`
|
||||
Order string `json:"order,omitempty"`
|
||||
Path string `json:"path,omitempty"`
|
||||
DomainID string `json:"domain_id,omitempty"`
|
||||
Tags TagsQuery `json:"tags,omitempty"`
|
||||
Metadata Metadata `json:"metadata,omitempty"`
|
||||
Status Status `json:"status,omitempty"`
|
||||
RoleName string `json:"role_name,omitempty"`
|
||||
RoleID string `json:"role_id,omitempty"`
|
||||
Actions []string `json:"actions,omitempty"`
|
||||
AccessType string `json:"access_type,omitempty"`
|
||||
RootGroup bool `json:"root_group,omitempty"`
|
||||
CreatedFrom time.Time `json:"created_from,omitempty"`
|
||||
CreatedTo time.Time `json:"created_to,omitempty"`
|
||||
}
|
||||
|
||||
+40
-30
@@ -1196,6 +1196,12 @@ func buildQuery(gm groups.PageMeta, ids ...string) string {
|
||||
if len(gm.Metadata) > 0 {
|
||||
queries = append(queries, "g.metadata @> :metadata")
|
||||
}
|
||||
if !gm.CreatedFrom.IsZero() {
|
||||
queries = append(queries, "g.created_at >= :created_from")
|
||||
}
|
||||
if !gm.CreatedTo.IsZero() {
|
||||
queries = append(queries, "g.created_at <= :created_to")
|
||||
}
|
||||
if len(queries) > 0 {
|
||||
return fmt.Sprintf("WHERE %s", strings.Join(queries, " AND "))
|
||||
}
|
||||
@@ -1341,40 +1347,44 @@ func toDBGroupPageMeta(pm groups.PageMeta) (dbGroupPageMeta, error) {
|
||||
return dbGroupPageMeta{}, errors.Wrap(repoerr.ErrViewEntity, err)
|
||||
}
|
||||
return dbGroupPageMeta{
|
||||
ID: pm.ID,
|
||||
Name: pm.Name,
|
||||
Metadata: data,
|
||||
Tags: tags,
|
||||
Total: pm.Total,
|
||||
Offset: pm.Offset,
|
||||
Limit: pm.Limit,
|
||||
DomainID: pm.DomainID,
|
||||
Status: pm.Status,
|
||||
RoleName: pm.RoleName,
|
||||
RoleID: pm.RoleID,
|
||||
Actions: pm.Actions,
|
||||
AccessType: pm.AccessType,
|
||||
ID: pm.ID,
|
||||
Name: pm.Name,
|
||||
Metadata: data,
|
||||
Tags: tags,
|
||||
Total: pm.Total,
|
||||
Offset: pm.Offset,
|
||||
Limit: pm.Limit,
|
||||
DomainID: pm.DomainID,
|
||||
Status: pm.Status,
|
||||
RoleName: pm.RoleName,
|
||||
RoleID: pm.RoleID,
|
||||
Actions: pm.Actions,
|
||||
AccessType: pm.AccessType,
|
||||
CreatedFrom: pm.CreatedFrom,
|
||||
CreatedTo: pm.CreatedTo,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type dbGroupPageMeta struct {
|
||||
ID string `db:"id"`
|
||||
Name string `db:"name"`
|
||||
ParentID string `db:"parent_id"`
|
||||
DomainID string `db:"domain_id"`
|
||||
Metadata []byte `db:"metadata"`
|
||||
Path string `db:"path"`
|
||||
Level uint64 `db:"level"`
|
||||
Total uint64 `db:"total"`
|
||||
Limit uint64 `db:"limit"`
|
||||
Offset uint64 `db:"offset"`
|
||||
Subject string `db:"subject"`
|
||||
RoleName string `db:"role_name"`
|
||||
RoleID string `db:"role_id"`
|
||||
Actions pq.StringArray `db:"actions"`
|
||||
AccessType string `db:"access_type"`
|
||||
Status groups.Status `db:"status"`
|
||||
Tags pgtype.TextArray `db:"tags"`
|
||||
ID string `db:"id"`
|
||||
Name string `db:"name"`
|
||||
ParentID string `db:"parent_id"`
|
||||
DomainID string `db:"domain_id"`
|
||||
Metadata []byte `db:"metadata"`
|
||||
Path string `db:"path"`
|
||||
Level uint64 `db:"level"`
|
||||
Total uint64 `db:"total"`
|
||||
Limit uint64 `db:"limit"`
|
||||
Offset uint64 `db:"offset"`
|
||||
Subject string `db:"subject"`
|
||||
RoleName string `db:"role_name"`
|
||||
RoleID string `db:"role_id"`
|
||||
Actions pq.StringArray `db:"actions"`
|
||||
AccessType string `db:"access_type"`
|
||||
Status groups.Status `db:"status"`
|
||||
Tags pgtype.TextArray `db:"tags"`
|
||||
CreatedFrom time.Time `db:"created_from"`
|
||||
CreatedTo time.Time `db:"created_to"`
|
||||
}
|
||||
|
||||
func (repo groupRepository) processRows(rows *sqlx.Rows) ([]groups.Group, error) {
|
||||
|
||||
@@ -675,7 +675,7 @@ func TestRetrieveAll(t *testing.T) {
|
||||
|
||||
repo := postgres.New(database)
|
||||
num := 200
|
||||
baseTime := time.Now().UTC().Truncate(time.Microsecond)
|
||||
baseTime := time.Now().UTC().Truncate(time.Millisecond)
|
||||
|
||||
var items []groups.Group
|
||||
parentID := ""
|
||||
@@ -688,8 +688,8 @@ func TestRetrieveAll(t *testing.T) {
|
||||
Name: name,
|
||||
Description: desc,
|
||||
Metadata: map[string]any{"name": name},
|
||||
CreatedAt: baseTime.Add(time.Duration(i) * time.Microsecond),
|
||||
UpdatedAt: baseTime.Add(time.Duration(i) * time.Microsecond),
|
||||
CreatedAt: baseTime.Add(time.Duration(i) * time.Millisecond),
|
||||
UpdatedAt: baseTime.Add(time.Duration(i) * time.Millisecond),
|
||||
Status: groups.EnabledStatus,
|
||||
Tags: []string{"tag1", "tag2"},
|
||||
}
|
||||
@@ -1178,6 +1178,112 @@ func TestRetrieveAll(t *testing.T) {
|
||||
Groups: []groups.Group(nil),
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "retrieve groups with created_from",
|
||||
page: groups.Page{
|
||||
PageMeta: groups.PageMeta{
|
||||
Offset: 0,
|
||||
Limit: 200,
|
||||
Order: "created_at",
|
||||
Dir: ascDir,
|
||||
CreatedFrom: baseTime.Add(100 * time.Millisecond),
|
||||
},
|
||||
},
|
||||
response: groups.Page{
|
||||
PageMeta: groups.PageMeta{
|
||||
Total: 100,
|
||||
Offset: 0,
|
||||
Limit: 200,
|
||||
},
|
||||
Groups: items[100:],
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "retrieve groups with created_to",
|
||||
page: groups.Page{
|
||||
PageMeta: groups.PageMeta{
|
||||
Offset: 0,
|
||||
Limit: 200,
|
||||
Order: "created_at",
|
||||
Dir: ascDir,
|
||||
CreatedTo: baseTime.Add(99 * time.Millisecond),
|
||||
},
|
||||
},
|
||||
response: groups.Page{
|
||||
PageMeta: groups.PageMeta{
|
||||
Total: 100,
|
||||
Offset: 0,
|
||||
Limit: 200,
|
||||
},
|
||||
Groups: items[:100],
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "retrieve groups with both created_from and created_to",
|
||||
page: groups.Page{
|
||||
PageMeta: groups.PageMeta{
|
||||
Offset: 0,
|
||||
Limit: 200,
|
||||
Order: "created_at",
|
||||
Dir: ascDir,
|
||||
CreatedFrom: baseTime.Add(50 * time.Millisecond),
|
||||
CreatedTo: baseTime.Add(149 * time.Millisecond),
|
||||
},
|
||||
},
|
||||
response: groups.Page{
|
||||
PageMeta: groups.PageMeta{
|
||||
Total: 100,
|
||||
Offset: 0,
|
||||
Limit: 200,
|
||||
},
|
||||
Groups: items[50:150],
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "retrieve groups with created_from returning no results",
|
||||
page: groups.Page{
|
||||
PageMeta: groups.PageMeta{
|
||||
Offset: 0,
|
||||
Limit: 10,
|
||||
Order: "created_at",
|
||||
Dir: ascDir,
|
||||
CreatedFrom: baseTime.Add(1000 * time.Millisecond),
|
||||
},
|
||||
},
|
||||
response: groups.Page{
|
||||
PageMeta: groups.PageMeta{
|
||||
Total: 0,
|
||||
Offset: 0,
|
||||
Limit: 10,
|
||||
},
|
||||
Groups: []groups.Group(nil),
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "retrieve groups with created_to returning no results",
|
||||
page: groups.Page{
|
||||
PageMeta: groups.PageMeta{
|
||||
Offset: 0,
|
||||
Limit: 10,
|
||||
Order: "created_at",
|
||||
Dir: ascDir,
|
||||
CreatedTo: baseTime.Add(-1 * time.Millisecond),
|
||||
},
|
||||
},
|
||||
response: groups.Page{
|
||||
PageMeta: groups.PageMeta{
|
||||
Total: 0,
|
||||
Offset: 0,
|
||||
Limit: 10,
|
||||
},
|
||||
Groups: []groups.Group(nil),
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
|
||||
Reference in New Issue
Block a user