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
+121
View File
@@ -13,6 +13,7 @@ import (
"regexp"
"strings"
"testing"
"time"
grpcTokenV1 "github.com/absmach/supermq/api/grpc/token/v1"
api "github.com/absmach/supermq/api/http"
@@ -56,6 +57,7 @@ var (
testReferer = "http://localhost"
domainID = testsutil.GenerateUUID(&testing.T{})
verifiedSession = smqauthn.Session{UserID: validID, DomainID: domainID, Verified: true}
validTimeStamp = time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
)
const contentType = "application/json"
@@ -846,6 +848,125 @@ func TestListUsers(t *testing.T) {
authnRes: verifiedSession,
err: apiutil.ErrInvalidQueryParams,
},
{
desc: "list users with created_from",
token: validToken,
query: "created_from=2024-01-01T00:00:00Z",
pageMeta: users.Page{
Offset: 0,
Limit: 10,
Dir: api.DefDir,
Order: api.DefOrder,
CreatedFrom: validTimeStamp,
},
listUsersResponse: users.UsersPage{
Page: users.Page{
Total: 1,
},
Users: []users.User{user},
},
status: http.StatusOK,
authnRes: verifiedSession,
err: nil,
},
{
desc: "list users with created_to",
token: validToken,
query: "created_to=2024-01-01T00:00:00Z",
pageMeta: users.Page{
Offset: 0,
Limit: 10,
Order: api.DefOrder,
Dir: api.DefDir,
CreatedTo: validTimeStamp,
},
listUsersResponse: users.UsersPage{
Page: users.Page{
Total: 1,
},
Users: []users.User{user},
},
status: http.StatusOK,
authnRes: verifiedSession,
err: nil,
},
{
desc: "list users with both created_from and created_to",
token: validToken,
query: "created_from=2024-01-01T00:00:00Z&created_to=2024-01-01T00:00:00Z",
pageMeta: users.Page{
Offset: 0,
Limit: 10,
Order: api.DefOrder,
Dir: api.DefDir,
CreatedFrom: validTimeStamp,
CreatedTo: validTimeStamp,
},
listUsersResponse: users.UsersPage{
Page: users.Page{
Total: 1,
},
Users: []users.User{user},
},
status: http.StatusOK,
authnRes: verifiedSession,
err: nil,
},
{
desc: "list users with invalid created_from format",
token: validToken,
query: "created_from=invalid-date",
status: http.StatusBadRequest,
authnRes: verifiedSession,
err: apiutil.ErrInvalidQueryParams,
},
{
desc: "list users with invalid created_to format",
token: validToken,
query: "created_to=invalid-date",
status: http.StatusBadRequest,
authnRes: verifiedSession,
err: apiutil.ErrInvalidQueryParams,
},
{
desc: "list users with duplicate created_from",
token: validToken,
query: "created_from=2024-01-01T00:00:00Z&created_from=2024-01-02T00:00:00Z",
status: http.StatusBadRequest,
authnRes: verifiedSession,
err: apiutil.ErrInvalidQueryParams,
},
{
desc: "list users with duplicate created_to",
token: validToken,
query: "created_to=2024-12-31T23:59:59Z&created_to=2024-12-30T23:59:59Z",
status: http.StatusBadRequest,
authnRes: verifiedSession,
err: apiutil.ErrInvalidQueryParams,
},
{
desc: "list users with created_from and others",
token: validToken,
query: "created_from=2024-01-01T00:00:00Z&status=enabled&limit=10",
pageMeta: users.Page{
Offset: 0,
Limit: 10,
Order: api.DefOrder,
Dir: api.DefDir,
Status: users.EnabledStatus,
CreatedFrom: validTimeStamp,
},
listUsersResponse: users.UsersPage{
Page: users.Page{
Total: 1,
Limit: 10,
},
Users: []users.User{user},
},
status: http.StatusOK,
authnRes: verifiedSession,
err: nil,
},
}
for _, tc := range cases {
+15 -13
View File
@@ -122,19 +122,21 @@ func listUsersEndpoint(svc users.Service) endpoint.Endpoint {
}
pm := users.Page{
Status: req.status,
Offset: req.offset,
Limit: req.limit,
OnlyTotal: req.onlyTotal,
Username: req.userName,
Tags: req.tags,
Metadata: req.metadata,
FirstName: req.firstName,
LastName: req.lastName,
Email: req.email,
Order: req.order,
Dir: req.dir,
Id: req.id,
Status: req.status,
Offset: req.offset,
Limit: req.limit,
OnlyTotal: req.onlyTotal,
Username: req.userName,
Tags: req.tags,
Metadata: req.metadata,
FirstName: req.firstName,
LastName: req.lastName,
Email: req.email,
Order: req.order,
Dir: req.dir,
Id: req.id,
CreatedFrom: req.createdFrom,
CreatedTo: req.createdTo,
}
page, err := svc.ListUsers(ctx, session, pm)
+16 -13
View File
@@ -5,6 +5,7 @@ package api
import (
"net/url"
"time"
api "github.com/absmach/supermq/api/http"
apiutil "github.com/absmach/supermq/api/http/util"
@@ -94,19 +95,21 @@ func (req viewUserReq) validate() error {
}
type listUsersReq struct {
status users.Status
offset uint64
limit uint64
onlyTotal bool
userName string
tags users.TagsQuery
firstName string
lastName string
email string
metadata users.Metadata
order string
dir string
id string
status users.Status
offset uint64
limit uint64
onlyTotal bool
userName string
tags users.TagsQuery
firstName string
lastName string
email string
metadata users.Metadata
order string
dir string
id string
createdFrom time.Time
createdTo time.Time
}
func (req listUsersReq) validate() error {
+37 -13
View File
@@ -10,6 +10,7 @@ import (
"net/http"
"regexp"
"strings"
"time"
"github.com/absmach/supermq"
grpcTokenV1 "github.com/absmach/supermq/api/grpc/token/v1"
@@ -317,20 +318,43 @@ func decodeListUsers(_ context.Context, r *http.Request) (any, error) {
return nil, errors.Wrap(apiutil.ErrValidation, err)
}
cfrom, err := apiutil.ReadStringQuery(r, "created_from", "")
if err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, err)
}
cto, err := apiutil.ReadStringQuery(r, "created_to", "")
if err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, err)
}
var createdFrom, createdTo time.Time
if cfrom != "" {
if createdFrom, err = time.Parse(time.RFC3339, cfrom); err != nil {
return nil, errors.Wrap(apiutil.ErrInvalidQueryParams, err)
}
}
if cto != "" {
if createdTo, err = time.Parse(time.RFC3339, cto); err != nil {
return nil, errors.Wrap(apiutil.ErrInvalidQueryParams, err)
}
}
req := listUsersReq{
status: st,
offset: o,
limit: l,
onlyTotal: ot,
metadata: m,
userName: n,
firstName: i,
lastName: f,
tags: tq,
order: order,
dir: dir,
id: id,
email: d,
status: st,
offset: o,
limit: l,
onlyTotal: ot,
metadata: m,
userName: n,
firstName: i,
lastName: f,
tags: tq,
order: order,
dir: dir,
id: id,
email: d,
createdFrom: createdFrom,
createdTo: createdTo,
}
return req, nil
+35 -26
View File
@@ -621,19 +621,21 @@ func ToUser(dbu DBUser) (users.User, error) {
}
type DBUsersPage struct {
Total uint64 `db:"total"`
Limit uint64 `db:"limit"`
Offset uint64 `db:"offset"`
FirstName string `db:"first_name"`
LastName string `db:"last_name"`
Username string `db:"username"`
Id string `db:"id"`
Email string `db:"email"`
Metadata []byte `db:"metadata"`
Tags pgtype.TextArray `db:"tags"`
GroupID string `db:"group_id"`
Role users.Role `db:"role"`
Status users.Status `db:"status"`
Total uint64 `db:"total"`
Limit uint64 `db:"limit"`
Offset uint64 `db:"offset"`
FirstName string `db:"first_name"`
LastName string `db:"last_name"`
Username string `db:"username"`
Id string `db:"id"`
Email string `db:"email"`
Metadata []byte `db:"metadata"`
Tags pgtype.TextArray `db:"tags"`
GroupID string `db:"group_id"`
Role users.Role `db:"role"`
Status users.Status `db:"status"`
CreatedFrom time.Time `db:"created_from"`
CreatedTo time.Time `db:"created_to"`
}
func ToDBUsersPage(pm users.Page) (DBUsersPage, error) {
@@ -648,18 +650,20 @@ func ToDBUsersPage(pm users.Page) (DBUsersPage, error) {
}
return DBUsersPage{
FirstName: pm.FirstName,
LastName: pm.LastName,
Username: pm.Username,
Email: pm.Email,
Id: pm.Id,
Metadata: data,
Total: pm.Total,
Offset: pm.Offset,
Limit: pm.Limit,
Status: pm.Status,
Tags: tags,
Role: pm.Role,
FirstName: pm.FirstName,
LastName: pm.LastName,
Username: pm.Username,
Email: pm.Email,
Id: pm.Id,
Metadata: data,
Total: pm.Total,
Offset: pm.Offset,
Limit: pm.Limit,
Status: pm.Status,
Tags: tags,
Role: pm.Role,
CreatedFrom: pm.CreatedFrom,
CreatedTo: pm.CreatedTo,
}, nil
}
@@ -694,13 +698,18 @@ func PageQuery(pm users.Page) (string, error) {
if len(pm.Metadata) > 0 {
query = append(query, "metadata @> :metadata")
}
if len(pm.IDs) != 0 {
query = append(query, fmt.Sprintf("id IN ('%s')", strings.Join(pm.IDs, "','")))
}
if pm.Status != users.AllStatus {
query = append(query, "u.status = :status")
}
if !pm.CreatedFrom.IsZero() {
query = append(query, "created_at >= :created_from")
}
if !pm.CreatedTo.IsZero() {
query = append(query, "created_at <= :created_to")
}
var emq string
if len(query) > 0 {
+84
View File
@@ -1034,6 +1034,90 @@ func TestRetrieveAll(t *testing.T) {
},
err: nil,
},
{
desc: "retrieve users created from specific time",
pageMeta: users.Page{
CreatedFrom: baseTime.Add(50 * time.Millisecond),
Offset: 0,
Limit: 200,
Role: users.AllRole,
Status: users.AllStatus,
Order: "created_at",
Dir: ascDir,
},
page: users.UsersPage{
Page: users.Page{
Total: 150,
Offset: 0,
Limit: 200,
},
Users: items[50:200],
},
err: nil,
},
{
desc: "retrieve users created to specific time",
pageMeta: users.Page{
CreatedTo: baseTime.Add(49 * time.Millisecond),
Offset: 0,
Limit: 200,
Role: users.AllRole,
Status: users.AllStatus,
Order: "created_at",
Dir: ascDir,
},
page: users.UsersPage{
Page: users.Page{
Total: 50,
Offset: 0,
Limit: 200,
},
Users: items[0:50],
},
err: nil,
},
{
desc: "retrieve users created within time range",
pageMeta: users.Page{
CreatedFrom: baseTime.Add(50 * time.Millisecond),
CreatedTo: baseTime.Add(99 * time.Millisecond),
Offset: 0,
Limit: 200,
Role: users.AllRole,
Status: users.AllStatus,
Order: "created_at",
Dir: ascDir,
},
page: users.UsersPage{
Page: users.Page{
Total: 50,
Offset: 0,
Limit: 200,
},
Users: items[50:100],
},
err: nil,
},
{
desc: "retrieve users with time range outside of all records",
pageMeta: users.Page{
CreatedFrom: baseTime.Add(300 * time.Millisecond),
CreatedTo: baseTime.Add(400 * time.Millisecond),
Offset: 0,
Limit: 200,
Role: users.AllRole,
Status: users.AllStatus,
},
page: users.UsersPage{
Page: users.Page{
Total: 0,
Offset: 0,
Limit: 200,
},
Users: []users.User{},
},
err: nil,
},
}
for _, tc := range cases {
+22 -20
View File
@@ -176,26 +176,28 @@ func ToTagsQuery(s string) TagsQuery {
// Page contains page metadata that helps navigation.
type Page struct {
Total uint64 `json:"total"`
Offset uint64 `json:"offset"`
Limit uint64 `json:"limit"`
OnlyTotal bool `json:"only_total"`
Id string `json:"id,omitempty"`
Order string `json:"order,omitempty"`
Dir string `json:"dir,omitempty"`
Metadata Metadata `json:"metadata,omitempty"`
Domain string `json:"domain,omitempty"`
Tags TagsQuery `json:"tag,omitempty"`
Permission string `json:"permission,omitempty"`
Status Status `json:"status,omitempty"`
IDs []string `json:"ids,omitempty"`
Role Role `json:"-"`
ListPerms bool `json:"-"`
Username string `json:"username,omitempty"`
FirstName string `json:"first_name,omitempty"`
LastName string `json:"last_name,omitempty"`
Email string `json:"email,omitempty"`
Verified bool `json:"verified,omitempty"`
Total uint64 `json:"total"`
Offset uint64 `json:"offset"`
Limit uint64 `json:"limit"`
OnlyTotal bool `json:"only_total"`
Id string `json:"id,omitempty"`
Order string `json:"order,omitempty"`
Dir string `json:"dir,omitempty"`
Metadata Metadata `json:"metadata,omitempty"`
Domain string `json:"domain,omitempty"`
Tags TagsQuery `json:"tag,omitempty"`
Permission string `json:"permission,omitempty"`
Status Status `json:"status,omitempty"`
IDs []string `json:"ids,omitempty"`
Role Role `json:"-"`
ListPerms bool `json:"-"`
Username string `json:"username,omitempty"`
FirstName string `json:"first_name,omitempty"`
LastName string `json:"last_name,omitempty"`
Email string `json:"email,omitempty"`
Verified bool `json:"verified,omitempty"`
CreatedFrom time.Time `json:"created_from,omitempty"`
CreatedTo time.Time `json:"created_to,omitempty"`
}
// Service specifies an API that must be fullfiled by the domain service