mirror of
https://github.com/absmach/magistrala.git
synced 2026-06-22 20:00:22 +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:
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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 "))
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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 "))
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user