SMQ-3338 - Add created at period filter to entities (#3339)

Signed-off-by: Felix Gateru <felix.gateru@gmail.com>
This commit is contained in:
Felix Gateru
2026-03-04 14:37:35 +03:00
committed by GitHub
parent 2260293dfc
commit f8410b8940
35 changed files with 1736 additions and 302 deletions
+39 -15
View File
@@ -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
}
+56
View File
@@ -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 {
+88 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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) {
+109 -3
View File
@@ -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 {