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
+22
View File
@@ -102,6 +102,8 @@ paths:
- $ref: "#/components/parameters/Client"
- $ref: "#/components/parameters/Group"
- $ref: "#/components/parameters/User"
- $ref: "#/components/parameters/CreatedFrom"
- $ref: "#/components/parameters/CreatedTo"
responses:
"200":
$ref: "#/components/responses/ChannelPageRes"
@@ -944,6 +946,26 @@ components:
required: false
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
CreatedFrom:
name: created_from
description: Filter channels created from this date (inclusive).
in: query
schema:
type: string
format: date-time
required: false
example: "2023-01-01T00:00:00Z"
CreatedTo:
name: created_to
description: Filter channels created up to this date (inclusive).
in: query
schema:
type: string
format: date-time
required: false
example: "2023-12-31T23:59:59Z"
requestBodies:
ChannelCreateReq:
description: JSON-formatted document describing the new channel to be registered
+22
View File
@@ -100,6 +100,8 @@ paths:
- $ref: "#/components/parameters/ConnectionType"
- $ref: "#/components/parameters/Group"
- $ref: "#/components/parameters/User"
- $ref: "#/components/parameters/CreatedFrom"
- $ref: "#/components/parameters/CreatedTo"
security:
- bearerAuth: []
responses:
@@ -1418,6 +1420,26 @@ components:
minLength: 36
required: false
example: bb7edb32-2eac-4aad-aebe-ed96fe073879
CreatedFrom:
name: created_from
description: Filter clients created from this date (inclusive).
in: query
schema:
type: string
format: date-time
required: false
example: "2023-01-01T00:00:00Z"
CreatedTo:
name: created_to
description: Filter clients created up to this date (inclusive).
in: query
schema:
type: string
format: date-time
required: false
example: "2023-12-31T23:59:59Z"
requestBodies:
ClientCreateReq:
+22
View File
@@ -84,6 +84,8 @@ paths:
- $ref: "./schemas/roles.yaml#/components/parameters/RoleNameQuery"
- $ref: "#/components/parameters/AccessType"
- $ref: "#/components/parameters/OnlyTotal"
- $ref: "#/components/parameters/CreatedFrom"
- $ref: "#/components/parameters/CreatedTo"
tags:
- Domains
security:
@@ -1291,6 +1293,26 @@ components:
default: false
required: false
CreatedFrom:
name: created_from
description: Filter domains created from this date (inclusive).
in: query
schema:
type: string
format: date-time
required: false
example: "2023-01-01T00:00:00Z"
CreatedTo:
name: created_to
description: Filter domains created up to this date (inclusive).
in: query
schema:
type: string
format: date-time
required: false
example: "2023-12-31T23:59:59Z"
requestBodies:
DomainCreateReq:
description: JSON-formatted document describing the new domain to be registered
+24
View File
@@ -103,6 +103,8 @@ paths:
- $ref: "./schemas/roles.yaml#/components/parameters/RoleNameQuery"
- $ref: "#/components/parameters/AccessType"
- $ref: "#/components/parameters/OnlyTotal"
- $ref: "#/components/parameters/CreatedFrom"
- $ref: "#/components/parameters/CreatedTo"
responses:
"200":
$ref: "#/components/responses/GroupPageRes"
@@ -329,6 +331,8 @@ paths:
- $ref: "#/components/parameters/Level"
- $ref: "#/components/parameters/Tree"
- $ref: "#/components/parameters/Direction"
- $ref: "#/components/parameters/CreatedFrom"
- $ref: "#/components/parameters/CreatedTo"
responses:
"200":
$ref: "#/components/responses/GroupsHierarchyPageRes"
@@ -1599,6 +1603,26 @@ components:
default: false
required: false
CreatedFrom:
name: created_from
description: Filter groups created from this date (inclusive).
in: query
schema:
type: string
format: date-time
required: false
example: "2023-01-01T00:00:00Z"
CreatedTo:
name: created_to
description: Filter groups created up to this date (inclusive).
in: query
schema:
type: string
format: date-time
required: false
example: "2023-12-31T23:59:59Z"
User:
name: user
description: If provided lists groups associated with a user with the provided ID. Only available for admin users.
+22
View File
@@ -85,6 +85,8 @@ paths:
- $ref: "#/components/parameters/Email"
- $ref: "#/components/parameters/Tags"
- $ref: "#/components/parameters/OnlyTotal"
- $ref: "#/components/parameters/CreatedFrom"
- $ref: "#/components/parameters/CreatedTo"
security:
- bearerAuth: []
responses:
@@ -1403,6 +1405,26 @@ components:
default: false
required: false
CreatedFrom:
name: created_from
description: Filter users created from this date (inclusive).
in: query
schema:
type: string
format: date-time
required: false
example: "2023-01-01T00:00:00Z"
CreatedTo:
name: created_to
description: Filter users created up to this date (inclusive).
in: query
schema:
type: string
format: date-time
required: false
example: "2023-12-31T23:59:59Z"
VerificationToken:
name: token
description: Verification token.
+24
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"
@@ -158,6 +159,27 @@ func decodeListChannels(_ context.Context, r *http.Request) (any, error) {
return listChannelsReq{}, errors.Wrap(apiutil.ErrValidation, err)
}
cfrom, err := apiutil.ReadStringQuery(r, "created_from", "")
if err != nil {
return listChannelsReq{}, errors.Wrap(apiutil.ErrValidation, err)
}
cto, err := apiutil.ReadStringQuery(r, "created_to", "")
if err != nil {
return listChannelsReq{}, errors.Wrap(apiutil.ErrValidation, err)
}
var createdFrom, createdTo time.Time
if cfrom != "" {
if createdFrom, err = time.Parse(time.RFC3339, cfrom); err != nil {
return listChannelsReq{}, errors.Wrap(apiutil.ErrInvalidQueryParams, err)
}
}
if cto != "" {
if createdTo, err = time.Parse(time.RFC3339, cto); err != nil {
return listChannelsReq{}, errors.Wrap(apiutil.ErrInvalidQueryParams, err)
}
}
req := listChannelsReq{
Page: channels.Page{
Name: name,
@@ -177,6 +199,8 @@ func decodeListChannels(_ context.Context, r *http.Request) (any, error) {
ConnectionType: connectionType,
ID: id,
OnlyTotal: ot,
CreatedFrom: createdFrom,
CreatedTo: createdTo,
},
userID: userID,
}
+104 -4
View File
@@ -47,10 +47,11 @@ var (
UpdatedBy: testsutil.GenerateUUID(&testing.T{}),
Status: channels.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 newChannelsServer() (*httptest.Server, *mocks.Service, *authnmocks.Authentication) {
@@ -893,6 +894,105 @@ func TestListChannels(t *testing.T) {
status: http.StatusBadRequest,
err: apiutil.ErrInvalidQueryParams,
},
{
desc: "list channels with created_from",
domainID: validID,
token: validToken,
pageMeta: channels.Page{
Offset: 0,
Limit: 10,
Order: api.DefOrder,
Dir: api.DefDir,
Actions: []string{},
CreatedFrom: validTimeStamp,
},
listChannelsResponse: channels.ChannelsPage{
Page: channels.Page{
Total: 1,
},
Channels: []channels.Channel{validChannelResp},
},
query: "created_from=2024-01-01T00:00:00Z",
status: http.StatusOK,
err: nil,
},
{
desc: "list channels with created_to",
domainID: validID,
token: validToken,
pageMeta: channels.Page{
Offset: 0,
Limit: 10,
Order: api.DefOrder,
Dir: api.DefDir,
Actions: []string{},
CreatedTo: validTimeStamp,
},
listChannelsResponse: channels.ChannelsPage{
Page: channels.Page{
Total: 1,
},
Channels: []channels.Channel{validChannelResp},
},
query: "created_to=2024-01-01T00:00:00Z",
status: http.StatusOK,
err: nil,
},
{
desc: "list channels with both created_from and created_to",
domainID: validID,
token: validToken,
pageMeta: channels.Page{
Offset: 0,
Limit: 10,
Order: api.DefOrder,
Dir: api.DefDir,
Actions: []string{},
CreatedFrom: validTimeStamp,
CreatedTo: validTimeStamp,
},
listChannelsResponse: channels.ChannelsPage{
Page: channels.Page{
Total: 1,
},
Channels: []channels.Channel{validChannelResp},
},
query: "created_from=2024-01-01T00:00:00Z&created_to=2024-01-01T00:00:00Z",
status: http.StatusOK,
err: nil,
},
{
desc: "list channels with invalid created_from",
domainID: validID,
token: validToken,
query: "created_from=invalid-timestamp",
status: http.StatusBadRequest,
err: apiutil.ErrInvalidQueryParams,
},
{
desc: "list channels with duplicate created_from",
domainID: validID,
token: validToken,
query: "created_from=2024-01-01T00:00:00Z&created_from=2024-01-02T00:00:00Z",
status: http.StatusBadRequest,
err: apiutil.ErrInvalidQueryParams,
},
{
desc: "list channels with invalid created_to",
domainID: validID,
token: validToken,
query: "created_to=invalid-timestamp",
status: http.StatusBadRequest,
err: apiutil.ErrInvalidQueryParams,
},
{
desc: "list channels with duplicate created_to",
domainID: validID,
token: validToken,
query: "created_to=2024-12-31T23:59:59Z&created_to=2024-12-30T23:59:59Z",
status: http.StatusBadRequest,
err: apiutil.ErrInvalidQueryParams,
},
}
for _, tc := range cases {
+2
View File
@@ -99,6 +99,8 @@ type Page struct {
Actions []string `json:"actions,omitempty"`
AccessType string `json:"access_type,omitempty"`
IDs []string `json:"-"`
CreatedFrom time.Time `json:"created_from,omitempty"`
CreatedTo time.Time `json:"created_to,omitempty"`
}
// ChannelsPage contains page related metadata as well as list of channels that
+41 -30
View File
@@ -1342,6 +1342,13 @@ func PageQuery(pm channels.Page) (string, error) {
query = append(query, "c.metadata @> :metadata")
}
if !pm.CreatedFrom.IsZero() {
query = append(query, "c.created_at >= :created_from")
}
if !pm.CreatedTo.IsZero() {
query = append(query, "c.created_at <= :created_to")
}
var emq string
if len(query) > 0 {
emq = fmt.Sprintf("WHERE %s", strings.Join(query, " AND "))
@@ -1393,40 +1400,44 @@ func toDBChannelsPage(pm channels.Page) (dbChannelsPage, error) {
}
return dbChannelsPage{
Limit: pm.Limit,
Offset: pm.Offset,
Name: pm.Name,
Id: pm.ID,
Domain: pm.Domain,
Metadata: data,
Tags: tags,
Status: pm.Status,
GroupID: sql.NullString{Valid: pm.Group.Valid, String: pm.Group.Value},
ClientID: pm.Client,
ConnType: connType,
RoleName: pm.RoleName,
RoleID: pm.RoleID,
Actions: pm.Actions,
AccessType: pm.AccessType,
Limit: pm.Limit,
Offset: pm.Offset,
Name: pm.Name,
Id: pm.ID,
Domain: pm.Domain,
Metadata: data,
Tags: tags,
Status: pm.Status,
GroupID: sql.NullString{Valid: pm.Group.Valid, String: pm.Group.Value},
ClientID: pm.Client,
ConnType: connType,
RoleName: pm.RoleName,
RoleID: pm.RoleID,
Actions: pm.Actions,
AccessType: pm.AccessType,
CreatedFrom: pm.CreatedFrom,
CreatedTo: pm.CreatedTo,
}, nil
}
type dbChannelsPage struct {
Limit uint64 `db:"limit"`
Offset uint64 `db:"offset"`
Name string `db:"name"`
Id string `db:"id"`
Domain string `db:"domain_id"`
Metadata []byte `db:"metadata"`
Tags pgtype.TextArray `db:"tags"`
Status channels.Status `db:"status"`
GroupID sql.NullString `db:"group_id"`
ClientID string `db:"client_id"`
ConnType uint8 `db:"conn_type"`
RoleName string `db:"role_name"`
RoleID string `db:"role_id"`
Actions pq.StringArray `db:"actions"`
AccessType string `db:"access_type"`
Limit uint64 `db:"limit"`
Offset uint64 `db:"offset"`
Name string `db:"name"`
Id string `db:"id"`
Domain string `db:"domain_id"`
Metadata []byte `db:"metadata"`
Tags pgtype.TextArray `db:"tags"`
Status channels.Status `db:"status"`
GroupID sql.NullString `db:"group_id"`
ClientID string `db:"client_id"`
ConnType uint8 `db:"conn_type"`
RoleName string `db:"role_name"`
RoleID string `db:"role_id"`
Actions pq.StringArray `db:"actions"`
AccessType string `db:"access_type"`
CreatedFrom time.Time `db:"created_from"`
CreatedTo time.Time `db:"created_to"`
}
type dbConnection struct {
+109 -3
View File
@@ -604,7 +604,7 @@ func TestRetrieveAll(t *testing.T) {
var items []channels.Channel
parentID := ""
baseTime := time.Now().UTC().Truncate(time.Microsecond)
baseTime := time.Now().UTC().Truncate(time.Millisecond)
for i := 0; i < num; i++ {
name := namegen.Generate()
channel := channels.Channel{
@@ -1139,6 +1139,112 @@ func TestRetrieveAll(t *testing.T) {
},
err: nil,
},
{
desc: "retrieve channels with created_from",
page: channels.ChannelsPage{
Page: channels.Page{
Offset: 0,
Limit: 200,
Order: "created_at",
Dir: ascDir,
CreatedFrom: baseTime.Add(100 * time.Millisecond),
},
},
response: channels.ChannelsPage{
Page: channels.Page{
Total: 100,
Offset: 0,
Limit: 200,
},
Channels: items[100:],
},
err: nil,
},
{
desc: "retrieve channels with created_to",
page: channels.ChannelsPage{
Page: channels.Page{
Offset: 0,
Limit: 200,
Order: "created_at",
Dir: ascDir,
CreatedTo: baseTime.Add(99 * time.Millisecond),
},
},
response: channels.ChannelsPage{
Page: channels.Page{
Total: 100,
Offset: 0,
Limit: 200,
},
Channels: items[:100],
},
err: nil,
},
{
desc: "retrieve channels with both created_from and created_to",
page: channels.ChannelsPage{
Page: channels.Page{
Offset: 0,
Limit: 200,
Order: "created_at",
Dir: ascDir,
CreatedFrom: baseTime.Add(50 * time.Millisecond),
CreatedTo: baseTime.Add(149 * time.Millisecond),
},
},
response: channels.ChannelsPage{
Page: channels.Page{
Total: 100,
Offset: 0,
Limit: 200,
},
Channels: items[50:150],
},
err: nil,
},
{
desc: "retrieve channels with created_from returning no results",
page: channels.ChannelsPage{
Page: channels.Page{
Offset: 0,
Limit: 10,
Order: "created_at",
Dir: ascDir,
CreatedFrom: baseTime.Add(1000 * time.Millisecond),
},
},
response: channels.ChannelsPage{
Page: channels.Page{
Total: 0,
Offset: 0,
Limit: 10,
},
Channels: []channels.Channel{},
},
err: nil,
},
{
desc: "retrieve channels with created_to returning no results",
page: channels.ChannelsPage{
Page: channels.Page{
Offset: 0,
Limit: 10,
Order: "created_at",
Dir: ascDir,
CreatedTo: baseTime.Add(-1 * time.Millisecond),
},
},
response: channels.ChannelsPage{
Page: channels.Page{
Total: 0,
Offset: 0,
Limit: 10,
},
Channels: []channels.Channel{},
},
err: nil,
},
}
for _, tc := range cases {
@@ -2728,7 +2834,7 @@ func TestRetrieveUserChannels(t *testing.T) {
},
},
{
desc: "retrieve channels with actions filter",
desc: "retrieve channels with actions",
domainID: domain.ID,
userID: userID,
pm: channels.Page{
@@ -2749,7 +2855,7 @@ func TestRetrieveUserChannels(t *testing.T) {
},
},
{
desc: "retrieve channels with non-matching actions filter",
desc: "retrieve channels with non-matching actions",
domainID: domain.ID,
userID: userID,
pm: channels.Page{
+24
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"
@@ -140,6 +141,27 @@ func decodeListClients(_ context.Context, r *http.Request) (any, error) {
return listClientsReq{}, errors.Wrap(apiutil.ErrValidation, err)
}
cfrom, err := apiutil.ReadStringQuery(r, "created_from", "")
if err != nil {
return listClientsReq{}, errors.Wrap(apiutil.ErrValidation, err)
}
cto, err := apiutil.ReadStringQuery(r, "created_to", "")
if err != nil {
return listClientsReq{}, errors.Wrap(apiutil.ErrValidation, err)
}
var createdFrom, createdTo time.Time
if cfrom != "" {
if createdFrom, err = time.Parse(time.RFC3339, cfrom); err != nil {
return listClientsReq{}, errors.Wrap(apiutil.ErrInvalidQueryParams, err)
}
}
if cto != "" {
if createdTo, err = time.Parse(time.RFC3339, cto); err != nil {
return listClientsReq{}, errors.Wrap(apiutil.ErrInvalidQueryParams, err)
}
}
req := listClientsReq{
Page: clients.Page{
Name: name,
@@ -159,6 +181,8 @@ func decodeListClients(_ context.Context, r *http.Request) (any, error) {
ConnectionType: connType,
ID: id,
OnlyTotal: ot,
CreatedFrom: createdFrom,
CreatedTo: createdTo,
},
userID: userID,
}
+116 -8
View File
@@ -12,6 +12,7 @@ import (
"net/url"
"strings"
"testing"
"time"
"github.com/0x6flab/namegenerator"
api "github.com/absmach/supermq/api/http"
@@ -32,6 +33,8 @@ import (
"github.com/stretchr/testify/mock"
)
const contentType = "application/json"
var (
secret = "strongsecret"
validMetadata = clients.Metadata{"role": "client"}
@@ -45,16 +48,15 @@ var (
Metadata: validMetadata,
Status: clients.EnabledStatus,
}
validToken = "token"
inValidToken = "invalid"
inValid = "invalid"
validID = testsutil.GenerateUUID(&testing.T{})
domainID = testsutil.GenerateUUID(&testing.T{})
namesgen = namegenerator.NewGenerator()
validToken = "token"
inValidToken = "invalid"
inValid = "invalid"
validID = testsutil.GenerateUUID(&testing.T{})
domainID = testsutil.GenerateUUID(&testing.T{})
namesgen = namegenerator.NewGenerator()
validTimeStamp = time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
)
const contentType = "application/json"
type testRequest struct {
client *http.Client
method string
@@ -759,6 +761,112 @@ func TestListClients(t *testing.T) {
status: http.StatusBadRequest,
err: apiutil.ErrInvalidQueryParams,
},
{
desc: "list clients with created_from parameter",
domainID: domainID,
token: validToken,
authnRes: smqauthn.Session{UserID: validID, DomainID: domainID, DomainUserID: domainID + "_" + validID, SuperAdmin: false},
pageMeta: clients.Page{
Offset: 0,
Limit: 10,
Order: api.DefOrder,
Dir: api.DefDir,
Actions: []string{},
CreatedFrom: validTimeStamp,
},
listClientsResponse: clients.ClientsPage{
Page: clients.Page{
Total: 1,
},
Clients: []clients.Client{client},
},
query: "created_from=2024-01-01T00:00:00Z",
status: http.StatusOK,
err: nil,
},
{
desc: "list clients with created_to parameter",
domainID: domainID,
token: validToken,
authnRes: smqauthn.Session{UserID: validID, DomainID: domainID, DomainUserID: domainID + "_" + validID, SuperAdmin: false},
pageMeta: clients.Page{
Offset: 0,
Limit: 10,
Order: api.DefOrder,
Dir: api.DefDir,
Actions: []string{},
CreatedTo: validTimeStamp,
},
listClientsResponse: clients.ClientsPage{
Page: clients.Page{
Total: 1,
},
Clients: []clients.Client{client},
},
query: "created_to=2024-01-01T00:00:00Z",
status: http.StatusOK,
err: nil,
},
{
desc: "list clients with both created_from and created_to parameters",
domainID: domainID,
token: validToken,
authnRes: smqauthn.Session{UserID: validID, DomainID: domainID, DomainUserID: domainID + "_" + validID, SuperAdmin: false},
pageMeta: clients.Page{
Offset: 0,
Limit: 10,
Order: api.DefOrder,
Dir: api.DefDir,
Actions: []string{},
CreatedFrom: validTimeStamp,
CreatedTo: validTimeStamp,
},
listClientsResponse: clients.ClientsPage{
Page: clients.Page{
Total: 1,
},
Clients: []clients.Client{client},
},
query: "created_from=2024-01-01T00:00:00Z&created_to=2024-01-01T00:00:00Z",
status: http.StatusOK,
err: nil,
},
{
desc: "list clients with invalid created_from",
domainID: domainID,
token: validToken,
authnRes: smqauthn.Session{UserID: validID, DomainID: domainID, DomainUserID: domainID + "_" + validID, SuperAdmin: false},
query: "created_from=invalid-timestamp",
status: http.StatusBadRequest,
err: apiutil.ErrInvalidQueryParams,
},
{
desc: "list clients with duplicate created_from",
domainID: domainID,
token: validToken,
authnRes: smqauthn.Session{UserID: validID, DomainID: domainID, DomainUserID: domainID + "_" + validID, SuperAdmin: false},
query: "created_from=2024-01-01T00:00:00Z&created_from=2024-01-01T00:00:00Z",
status: http.StatusBadRequest,
err: apiutil.ErrInvalidQueryParams,
},
{
desc: "list clients with invalid created_to",
domainID: domainID,
token: validToken,
authnRes: smqauthn.Session{UserID: validID, DomainID: domainID, DomainUserID: domainID + "_" + validID, SuperAdmin: false},
query: "created_to=invalid-timestamp",
status: http.StatusBadRequest,
err: apiutil.ErrInvalidQueryParams,
},
{
desc: "list clients with duplicate created_to",
domainID: domainID,
token: validToken,
authnRes: smqauthn.Session{UserID: validID, DomainID: domainID, DomainUserID: domainID + "_" + validID, SuperAdmin: false},
query: "created_to=2024-12-31T23:59:59Z&created_to=2024-12-31T23:59:59Z",
status: http.StatusBadRequest,
err: apiutil.ErrInvalidQueryParams,
},
}
for _, tc := range cases {
+2
View File
@@ -251,6 +251,8 @@ type Page struct {
Actions []string `json:"actions,omitempty"`
AccessType string `json:"access_type,omitempty"`
IDs []string `json:"-"`
CreatedFrom time.Time `json:"created_from,omitempty"`
CreatedTo time.Time `json:"created_to,omitempty"`
}
// Metadata represents arbitrary JSON.
+43 -32
View File
@@ -1190,42 +1190,46 @@ func ToDBClientsPage(pm clients.Page) (dbClientsPage, error) {
return dbClientsPage{}, errors.Wrap(repoerr.ErrViewEntity, err)
}
return dbClientsPage{
Offset: pm.Offset,
Limit: pm.Limit,
Name: pm.Name,
Identity: pm.Identity,
Id: pm.ID,
Metadata: data,
Domain: pm.Domain,
Status: pm.Status,
Tags: tags,
GroupID: pm.Group,
ChannelID: pm.Channel,
RoleName: pm.RoleName,
ConnType: pm.ConnectionType,
RoleID: pm.RoleID,
Actions: pm.Actions,
AccessType: pm.AccessType,
Offset: pm.Offset,
Limit: pm.Limit,
Name: pm.Name,
Identity: pm.Identity,
Id: pm.ID,
Metadata: data,
Domain: pm.Domain,
Status: pm.Status,
Tags: tags,
GroupID: pm.Group,
ChannelID: pm.Channel,
RoleName: pm.RoleName,
ConnType: pm.ConnectionType,
RoleID: pm.RoleID,
Actions: pm.Actions,
AccessType: pm.AccessType,
CreatedFrom: pm.CreatedFrom,
CreatedTo: pm.CreatedTo,
}, nil
}
type dbClientsPage struct {
Limit uint64 `db:"limit"`
Offset uint64 `db:"offset"`
Name string `db:"name"`
Id string `db:"id"`
Domain string `db:"domain_id"`
Identity string `db:"identity"`
Metadata []byte `db:"metadata"`
Tags pgtype.TextArray `db:"tags"`
Status clients.Status `db:"status"`
GroupID *string `db:"group_id"`
ChannelID string `db:"channel_id"`
ConnType string `db:"type"`
RoleName string `db:"role_name"`
RoleID string `db:"role_id"`
Actions pq.StringArray `db:"actions"`
AccessType string `db:"access_type"`
Limit uint64 `db:"limit"`
Offset uint64 `db:"offset"`
Name string `db:"name"`
Id string `db:"id"`
Domain string `db:"domain_id"`
Identity string `db:"identity"`
Metadata []byte `db:"metadata"`
Tags pgtype.TextArray `db:"tags"`
Status clients.Status `db:"status"`
GroupID *string `db:"group_id"`
ChannelID string `db:"channel_id"`
ConnType string `db:"type"`
RoleName string `db:"role_name"`
RoleID string `db:"role_id"`
Actions pq.StringArray `db:"actions"`
AccessType string `db:"access_type"`
CreatedFrom time.Time `db:"created_from"`
CreatedTo time.Time `db:"created_to"`
}
func PageQuery(pm clients.Page) (string, error) {
@@ -1289,6 +1293,13 @@ func PageQuery(pm clients.Page) (string, error) {
query = append(query, "c.metadata @> :metadata")
}
if !pm.CreatedFrom.IsZero() {
query = append(query, "c.created_at >= :created_from")
}
if !pm.CreatedTo.IsZero() {
query = append(query, "c.created_at <= :created_to")
}
var emq string
if len(query) > 0 {
emq = fmt.Sprintf("WHERE %s", strings.Join(query, " AND "))
+99 -3
View File
@@ -1109,7 +1109,7 @@ func TestRetrieveAll(t *testing.T) {
expectedClients := []clients.Client{}
disabledClients := []clients.Client{}
reversedClients := []clients.Client{}
baseTime := time.Now().UTC().Truncate(time.Microsecond)
baseTime := time.Now().UTC().Truncate(time.Millisecond)
for i := uint64(0); i < nClients; i++ {
client := clients.Client{
ID: testsutil.GenerateUUID(t),
@@ -1124,8 +1124,8 @@ func TestRetrieveAll(t *testing.T) {
"department": namegen.Generate(),
},
Status: clients.EnabledStatus,
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),
}
if i%50 == 0 {
client.Status = clients.DisabledStatus
@@ -1809,6 +1809,102 @@ func TestRetrieveAll(t *testing.T) {
},
},
},
{
desc: "with created_from",
pm: clients.Page{
Offset: 0,
Limit: nClients,
Status: clients.AllStatus,
CreatedFrom: baseTime.Add(100 * time.Millisecond),
Order: defOrder,
Dir: ascDir,
},
response: clients.ClientsPage{
Page: clients.Page{
Total: 100,
Offset: 0,
Limit: nClients,
},
Clients: expectedClients[100:],
},
},
{
desc: "with created_to",
pm: clients.Page{
Offset: 0,
Limit: nClients,
Status: clients.AllStatus,
CreatedTo: baseTime.Add(99 * time.Millisecond),
Order: defOrder,
Dir: ascDir,
},
response: clients.ClientsPage{
Page: clients.Page{
Total: 100,
Offset: 0,
Limit: nClients,
},
Clients: expectedClients[:100],
},
},
{
desc: "with both created_from and created_to",
pm: clients.Page{
Offset: 0,
Limit: nClients,
Status: clients.AllStatus,
CreatedFrom: baseTime.Add(50 * time.Millisecond),
CreatedTo: baseTime.Add(149 * time.Millisecond),
Order: defOrder,
Dir: ascDir,
},
response: clients.ClientsPage{
Page: clients.Page{
Total: 100,
Offset: 0,
Limit: nClients,
},
Clients: expectedClients[50:150],
},
},
{
desc: "with created_from returning no results",
pm: clients.Page{
Offset: 0,
Limit: nClients,
Status: clients.AllStatus,
CreatedFrom: baseTime.Add(500 * time.Millisecond),
Order: defOrder,
Dir: ascDir,
},
response: clients.ClientsPage{
Page: clients.Page{
Total: 0,
Offset: 0,
Limit: nClients,
},
Clients: []clients.Client(nil),
},
},
{
desc: "with created_to returning no results",
pm: clients.Page{
Offset: 0,
Limit: nClients,
Status: clients.AllStatus,
CreatedTo: baseTime.Add(-10 * time.Millisecond),
Order: defOrder,
Dir: ascDir,
},
response: clients.ClientsPage{
Page: clients.Page{
Total: 0,
Offset: 0,
Limit: nClients,
},
Clients: []clients.Client(nil),
},
},
}
for _, c := range cases {
t.Run(c.desc, func(t *testing.T) {
+37 -13
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"
@@ -170,20 +171,43 @@ func decodePageRequest(_ context.Context, r *http.Request) (domains.Page, error)
return domains.Page{}, errors.Wrap(apiutil.ErrValidation, err)
}
cfrom, err := apiutil.ReadStringQuery(r, "created_from", "")
if err != nil {
return domains.Page{}, errors.Wrap(apiutil.ErrValidation, err)
}
cto, err := apiutil.ReadStringQuery(r, "created_to", "")
if err != nil {
return domains.Page{}, errors.Wrap(apiutil.ErrValidation, err)
}
var createdFrom, createdTo time.Time
if cfrom != "" {
if createdFrom, err = time.Parse(time.RFC3339, cfrom); err != nil {
return domains.Page{}, errors.Wrap(apiutil.ErrInvalidQueryParams, err)
}
}
if cto != "" {
if createdTo, err = time.Parse(time.RFC3339, cto); err != nil {
return domains.Page{}, errors.Wrap(apiutil.ErrInvalidQueryParams, err)
}
}
return domains.Page{
Offset: o,
Order: or,
Dir: dir,
Limit: l,
Name: n,
Metadata: m,
Tags: tq,
RoleID: roleID,
RoleName: roleName,
Actions: actions,
Status: st,
ID: id,
OnlyTotal: ot,
Offset: o,
Order: or,
Dir: dir,
Limit: l,
Name: n,
Metadata: m,
Tags: tq,
RoleID: roleID,
RoleName: roleName,
Actions: actions,
Status: st,
ID: id,
OnlyTotal: ot,
CreatedFrom: createdFrom,
CreatedTo: createdTo,
}, nil
}
+85 -6
View File
@@ -32,6 +32,8 @@ import (
"github.com/stretchr/testify/mock"
)
const contentType = "application/json"
var (
validMetadata = domains.Metadata{"role": "client"}
ID = testsutil.GenerateUUID(&testing.T{})
@@ -51,12 +53,6 @@ var (
domainID = testsutil.GenerateUUID(&testing.T{})
)
const (
contentType = "application/json"
refreshDuration = 24 * time.Hour
invalidDuration = 7 * 24 * time.Hour
)
type testRequest struct {
client *http.Client
method string
@@ -619,6 +615,89 @@ func TestListDomains(t *testing.T) {
svcErr: svcerr.ErrViewEntity,
err: svcerr.ErrViewEntity,
},
{
desc: "list domains with created_from parameter",
token: validToken,
query: "created_from=2024-01-01T00:00:00Z",
page: domains.Page{
Offset: api.DefOffset,
Limit: api.DefLimit,
Order: api.DefOrder,
Dir: api.DefDir,
CreatedFrom: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
},
listDomainsResp: domains.DomainsPage{
Total: 1,
Domains: []domains.Domain{domain},
},
status: http.StatusOK,
err: nil,
},
{
desc: "list domains with created_to parameter",
token: validToken,
query: "created_to=2024-12-31T23:59:59Z",
page: domains.Page{
Offset: api.DefOffset,
Limit: api.DefLimit,
Order: api.DefOrder,
Dir: api.DefDir,
CreatedTo: time.Date(2024, 12, 31, 23, 59, 59, 0, time.UTC),
},
listDomainsResp: domains.DomainsPage{
Total: 1,
Domains: []domains.Domain{domain},
},
status: http.StatusOK,
err: nil,
},
{
desc: "list domains with both created_from and created_to parameters",
token: validToken,
query: "created_from=2024-01-01T00:00:00Z&created_to=2024-12-31T23:59:59Z",
page: domains.Page{
Offset: api.DefOffset,
Limit: api.DefLimit,
Order: api.DefOrder,
Dir: api.DefDir,
CreatedFrom: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
CreatedTo: time.Date(2024, 12, 31, 23, 59, 59, 0, time.UTC),
},
listDomainsResp: domains.DomainsPage{
Total: 1,
Domains: []domains.Domain{domain},
},
status: http.StatusOK,
err: nil,
},
{
desc: "list domains with invalid created_from",
token: validToken,
query: "created_from=invalid-timestamp",
status: http.StatusBadRequest,
err: apiutil.ErrInvalidQueryParams,
},
{
desc: "list domains with duplicate created_from",
token: validToken,
query: "created_from=2024-01-01T00:00:00Z&created_from=2024-01-02T00:00:00Z",
status: http.StatusBadRequest,
err: apiutil.ErrInvalidQueryParams,
},
{
desc: "list domains with invalid created_to",
token: validToken,
query: "created_to=invalid-timestamp",
status: http.StatusBadRequest,
err: apiutil.ErrInvalidQueryParams,
},
{
desc: "list domains with duplicate created_to",
token: validToken,
query: "created_to=2024-12-31T23:59:59Z&created_to=2025-01-01T00:00:00Z",
status: http.StatusBadRequest,
err: apiutil.ErrInvalidQueryParams,
},
}
for _, tc := range cases {
+19 -17
View File
@@ -155,23 +155,25 @@ func ToTagsQuery(s string) TagsQuery {
}
type Page struct {
Total uint64 `json:"total"`
Offset uint64 `json:"offset"`
Limit uint64 `json:"limit"`
OnlyTotal bool `json:"only_total"`
Name string `json:"name,omitempty"`
Order string `json:"-"`
Dir string `json:"-"`
Metadata Metadata `json:"metadata,omitempty"`
Tags TagsQuery `json:"tags,omitempty"`
RoleName string `json:"role_name,omitempty"`
RoleID string `json:"role_id,omitempty"`
Actions []string `json:"actions,omitempty"`
Status Status `json:"status,omitempty"`
ID string `json:"id,omitempty"`
IDs []string `json:"-"`
Identity string `json:"identity,omitempty"`
UserID string `json:"user_id,omitempty"`
Total uint64 `json:"total"`
Offset uint64 `json:"offset"`
Limit uint64 `json:"limit"`
OnlyTotal bool `json:"only_total"`
Name string `json:"name,omitempty"`
Order string `json:"-"`
Dir string `json:"-"`
Metadata Metadata `json:"metadata,omitempty"`
Tags TagsQuery `json:"tags,omitempty"`
RoleName string `json:"role_name,omitempty"`
RoleID string `json:"role_id,omitempty"`
Actions []string `json:"actions,omitempty"`
Status Status `json:"status,omitempty"`
ID string `json:"id,omitempty"`
IDs []string `json:"-"`
Identity string `json:"identity,omitempty"`
UserID string `json:"user_id,omitempty"`
CreatedFrom time.Time `json:"created_from,omitempty"`
CreatedTo time.Time `json:"created_to,omitempty"`
}
type DomainsPage struct {
+41 -30
View File
@@ -660,21 +660,23 @@ func toDomain(d dbDomain) (domains.Domain, error) {
}
type dbDomainsPage struct {
Total uint64 `db:"total"`
Limit uint64 `db:"limit"`
Offset uint64 `db:"offset"`
Order string `db:"order"`
Dir string `db:"dir"`
Name string `db:"name"`
RoleID string `db:"role_id"`
RoleName string `db:"role_name"`
Actions pq.StringArray `db:"actions"`
ID string `db:"id"`
IDs []string `db:"ids"`
Metadata []byte `db:"metadata"`
Tags pgtype.TextArray `db:"tags"`
Status domains.Status `db:"status"`
UserID string `db:"member_id"`
Total uint64 `db:"total"`
Limit uint64 `db:"limit"`
Offset uint64 `db:"offset"`
Order string `db:"order"`
Dir string `db:"dir"`
Name string `db:"name"`
RoleID string `db:"role_id"`
RoleName string `db:"role_name"`
Actions pq.StringArray `db:"actions"`
ID string `db:"id"`
IDs []string `db:"ids"`
Metadata []byte `db:"metadata"`
Tags pgtype.TextArray `db:"tags"`
Status domains.Status `db:"status"`
UserID string `db:"member_id"`
CreatedFrom time.Time `db:"created_from"`
CreatedTo time.Time `db:"created_to"`
}
func toDBDomainsPage(pm domains.Page) (dbDomainsPage, error) {
@@ -687,21 +689,23 @@ func toDBDomainsPage(pm domains.Page) (dbDomainsPage, error) {
return dbDomainsPage{}, errors.Wrap(repoerr.ErrViewEntity, err)
}
return dbDomainsPage{
Total: pm.Total,
Limit: pm.Limit,
Offset: pm.Offset,
Order: pm.Order,
Dir: pm.Dir,
Name: pm.Name,
RoleID: pm.RoleID,
RoleName: pm.RoleName,
Actions: pm.Actions,
ID: pm.ID,
IDs: pm.IDs,
Metadata: data,
Tags: tags,
Status: pm.Status,
UserID: pm.UserID,
Total: pm.Total,
Limit: pm.Limit,
Offset: pm.Offset,
Order: pm.Order,
Dir: pm.Dir,
Name: pm.Name,
RoleID: pm.RoleID,
RoleName: pm.RoleName,
Actions: pm.Actions,
ID: pm.ID,
IDs: pm.IDs,
Metadata: data,
Tags: tags,
Status: pm.Status,
UserID: pm.UserID,
CreatedFrom: pm.CreatedFrom,
CreatedTo: pm.CreatedTo,
}, nil
}
@@ -758,6 +762,13 @@ func buildPageQuery(pm domains.Page) (string, error) {
query = append(query, mq)
}
if !pm.CreatedFrom.IsZero() {
query = append(query, "d.created_at >= :created_from")
}
if !pm.CreatedTo.IsZero() {
query = append(query, "d.created_at <= :created_to")
}
if len(query) > 0 {
emq = fmt.Sprintf("WHERE %s", strings.Join(query, " AND "))
}
+91
View File
@@ -1063,6 +1063,97 @@ func TestListDomains(t *testing.T) {
},
err: nil,
},
{
desc: "list domains with created_from filter",
pm: domains.Page{
Offset: 0,
Limit: 10,
Status: domains.AllStatus,
CreatedFrom: baseTime.Add(5 * time.Millisecond),
Order: "created_at",
Dir: ascDir,
},
response: domains.DomainsPage{
Total: 5,
Offset: 0,
Limit: 10,
Domains: []domains.Domain{items[5], items[6], items[7], items[8], items[9]},
},
err: nil,
},
{
desc: "list domains with created_to filter",
pm: domains.Page{
Offset: 0,
Limit: 10,
Status: domains.AllStatus,
CreatedTo: baseTime.Add(4 * time.Millisecond),
Order: "created_at",
Dir: ascDir,
},
response: domains.DomainsPage{
Total: 5,
Offset: 0,
Limit: 10,
Domains: []domains.Domain{items[0], items[1], items[2], items[3], items[4]},
},
err: nil,
},
{
desc: "list domains with both created_from and created_to filters",
pm: domains.Page{
Offset: 0,
Limit: 10,
Status: domains.AllStatus,
CreatedFrom: baseTime.Add(2 * time.Millisecond),
CreatedTo: baseTime.Add(7 * time.Millisecond),
Order: "created_at",
Dir: ascDir,
},
response: domains.DomainsPage{
Total: 6,
Offset: 0,
Limit: 10,
Domains: []domains.Domain{items[2], items[3], items[4], items[5], items[6], items[7]},
},
err: nil,
},
{
desc: "list domains with created_from filter returning no results",
pm: domains.Page{
Offset: 0,
Limit: 10,
Status: domains.AllStatus,
CreatedFrom: baseTime.Add(20 * time.Millisecond),
Order: "created_at",
Dir: ascDir,
},
response: domains.DomainsPage{
Total: 0,
Offset: 0,
Limit: 10,
Domains: []domains.Domain(nil),
},
err: nil,
},
{
desc: "list domains with created_to filter returning no results",
pm: domains.Page{
Offset: 0,
Limit: 10,
Status: domains.AllStatus,
CreatedTo: baseTime.Add(-10 * time.Millisecond),
Order: "created_at",
Dir: ascDir,
},
response: domains.DomainsPage{
Total: 0,
Offset: 0,
Limit: 10,
Domains: []domains.Domain(nil),
},
err: nil,
},
}
for _, tc := range cases {
+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 {
+8
View File
@@ -162,6 +162,8 @@ type PageMetadata struct {
Tree bool `json:"tree,omitempty"`
StartLevel int64 `json:"start_level,omitempty"`
EndLevel int64 `json:"end_level,omitempty"`
CreatedFrom time.Time `json:"created_from,omitempty"`
CreatedTo time.Time `json:"created_to,omitempty"`
}
type Role struct {
@@ -1702,6 +1704,12 @@ func (pm PageMetadata) query() (string, error) {
if pm.To != 0 {
q.Add("to", strconv.FormatInt(pm.To, 10))
}
if !pm.CreatedFrom.IsZero() {
q.Add("created_from", pm.CreatedFrom.Format(time.RFC3339))
}
if !pm.CreatedTo.IsZero() {
q.Add("created_to", pm.CreatedTo.Format(time.RFC3339))
}
q.Add("with_attributes", strconv.FormatBool(pm.WithAttributes))
q.Add("with_metadata", strconv.FormatBool(pm.WithMetadata))
+93
View File
@@ -10,6 +10,7 @@ import (
"net/http/httptest"
"strings"
"testing"
"time"
grpcTokenV1 "github.com/absmach/supermq/api/grpc/token/v1"
api "github.com/absmach/supermq/api/http"
@@ -509,6 +510,98 @@ func TestListUsers(t *testing.T) {
},
err: nil,
},
{
desc: "list users with CreatedFrom",
token: validToken,
pageMeta: sdk.PageMetadata{
Offset: offset,
Limit: limit,
CreatedFrom: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
},
svcReq: users.Page{
Offset: offset,
Limit: limit,
CreatedFrom: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
Order: api.DefOrder,
Dir: api.DefDir,
},
svcRes: users.UsersPage{
Page: users.Page{
Total: uint64(len(cls[offset:limit])),
},
Users: convertUsers(cls[offset:limit]),
},
svcErr: nil,
response: sdk.UsersPage{
PageRes: sdk.PageRes{
Total: uint64(len(cls[offset:limit])),
},
Users: cls[offset:limit],
},
err: nil,
},
{
desc: "list users with CreatedTo",
token: validToken,
pageMeta: sdk.PageMetadata{
Offset: offset,
Limit: limit,
CreatedTo: time.Date(2025, 12, 31, 23, 59, 59, 0, time.UTC),
},
svcReq: users.Page{
Offset: offset,
Limit: limit,
CreatedTo: time.Date(2025, 12, 31, 23, 59, 59, 0, time.UTC),
Order: api.DefOrder,
Dir: api.DefDir,
},
svcRes: users.UsersPage{
Page: users.Page{
Total: uint64(len(cls[offset:limit])),
},
Users: convertUsers(cls[offset:limit]),
},
svcErr: nil,
response: sdk.UsersPage{
PageRes: sdk.PageRes{
Total: uint64(len(cls[offset:limit])),
},
Users: cls[offset:limit],
},
err: nil,
},
{
desc: "list users with both CreatedFrom and CreatedTo",
token: validToken,
pageMeta: sdk.PageMetadata{
Offset: offset,
Limit: limit,
CreatedFrom: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
CreatedTo: time.Date(2025, 12, 31, 23, 59, 59, 0, time.UTC),
},
svcReq: users.Page{
Offset: offset,
Limit: limit,
CreatedFrom: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
CreatedTo: time.Date(2025, 12, 31, 23, 59, 59, 0, time.UTC),
Order: api.DefOrder,
Dir: api.DefDir,
},
svcRes: users.UsersPage{
Page: users.Page{
Total: 2,
},
Users: []users.User{convertUser(cls[10]), convertUser(cls[20])},
},
svcErr: nil,
response: sdk.UsersPage{
PageRes: sdk.PageRes{
Total: 2,
},
Users: []sdk.User{cls[10], cls[20]},
},
err: nil,
},
{
desc: "list users with request that can't be marshalled",
token: validToken,
+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