MG-136 - Move reports to a separate service (#152)

* initial implementation

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* initial implementation

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* add remove report from nats handler

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* add license header

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* fix failing linter

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* remove unused code

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* update docker compose

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* address comments

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* fix failing linter

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* move runinfo to pkg

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* update report handler

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* update reports handler

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* update handler in reports

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* update repo method from time to due

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* fix validation methods

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* address comments

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* update reports port to 9017

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* update nginx to support reports

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* fix reports location in nginx

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

* update env variable

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>

---------

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>
This commit is contained in:
Steve Munene
2025-06-16 13:10:50 +03:00
committed by GitHub
parent e57db52b34
commit dcd5ff914d
52 changed files with 5789 additions and 4152 deletions
+6
View File
@@ -0,0 +1,6 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
// Package api contains API-related concerns: endpoint definitions, middlewares
// and all resource representations.
package api
+238
View File
@@ -0,0 +1,238 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package api
import (
"context"
"github.com/absmach/magistrala/reports"
api "github.com/absmach/supermq/api/http"
"github.com/absmach/supermq/pkg/authn"
svcerr "github.com/absmach/supermq/pkg/errors/service"
"github.com/go-kit/kit/endpoint"
)
func generateReportEndpoint(svc reports.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
session, ok := ctx.Value(api.SessionKey).(authn.Session)
if !ok {
return nil, svcerr.ErrAuthorization
}
req := request.(generateReportReq)
if err := req.validate(); err != nil {
return generateReportResp{}, err
}
res, err := svc.GenerateReport(ctx, session, reports.ReportConfig{
Name: req.Name,
DomainID: req.DomainID,
Config: req.Config,
Metrics: req.Metrics,
Email: req.Email,
}, req.action)
if err != nil {
return generateReportResp{}, err
}
switch req.action {
case reports.DownloadReport:
return downloadReportResp{
File: res.File,
}, nil
case reports.EmailReport:
return emailReportResp{}, nil
default:
return generateReportResp{
Total: res.Total,
From: res.From,
To: res.To,
Aggregation: res.Aggregation,
Reports: res.Reports,
}, nil
}
}
}
func listReportsConfigEndpoint(svc reports.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
session, ok := ctx.Value(api.SessionKey).(authn.Session)
if !ok {
return nil, svcerr.ErrAuthorization
}
req := request.(listReportsConfigReq)
if err := req.validate(); err != nil {
return listReportsConfigRes{}, err
}
page, err := svc.ListReportsConfig(ctx, session, req.PageMeta)
if err != nil {
return listReportsConfigRes{}, err
}
return listReportsConfigRes{
pageRes: pageRes{
Limit: page.Limit,
Offset: page.Offset,
Total: page.Total,
},
ReportConfigs: page.ReportConfigs,
}, nil
}
}
func deleteReportConfigEndpoint(svc reports.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
session, ok := ctx.Value(api.SessionKey).(authn.Session)
if !ok {
return nil, svcerr.ErrAuthorization
}
req := request.(deleteReportConfigReq)
if err := req.validate(); err != nil {
return deleteReportConfigRes{}, err
}
err := svc.RemoveReportConfig(ctx, session, req.ID)
if err != nil {
return deleteReportConfigRes{false}, err
}
return deleteReportConfigRes{true}, nil
}
}
func updateReportConfigEndpoint(svc reports.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
session, ok := ctx.Value(api.SessionKey).(authn.Session)
if !ok {
return nil, svcerr.ErrAuthorization
}
req := request.(updateReportConfigReq)
if err := req.validate(); err != nil {
return updateReportConfigRes{}, err
}
cfg, err := svc.UpdateReportConfig(ctx, session, req.ReportConfig)
if err != nil {
return updateReportConfigRes{}, err
}
return updateReportConfigRes{ReportConfig: cfg}, nil
}
}
func updateReportScheduleEndpoint(s reports.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
session, ok := ctx.Value(api.SessionKey).(authn.Session)
if !ok {
return nil, svcerr.ErrAuthorization
}
req := request.(updateReportScheduleReq)
if err := req.validate(); err != nil {
return updateReportConfigRes{}, err
}
rpt := reports.ReportConfig{
ID: req.id,
Schedule: req.Schedule,
}
updatedReport, err := s.UpdateReportSchedule(ctx, session, rpt)
if err != nil {
return updateReportConfigRes{}, err
}
return updateReportConfigRes{ReportConfig: updatedReport}, nil
}
}
func viewReportConfigEndpoint(svc reports.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
session, ok := ctx.Value(api.SessionKey).(authn.Session)
if !ok {
return nil, svcerr.ErrAuthorization
}
req := request.(viewReportConfigReq)
if err := req.validate(); err != nil {
return viewReportConfigRes{}, err
}
cfg, err := svc.ViewReportConfig(ctx, session, req.ID)
if err != nil {
return viewReportConfigRes{}, err
}
return viewReportConfigRes{ReportConfig: cfg}, nil
}
}
func addReportConfigEndpoint(svc reports.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
session, ok := ctx.Value(api.SessionKey).(authn.Session)
if !ok {
return nil, svcerr.ErrAuthorization
}
req := request.(addReportConfigReq)
if err := req.validate(); err != nil {
return addReportConfigRes{}, err
}
cfg, err := svc.AddReportConfig(ctx, session, req.ReportConfig)
if err != nil {
return addReportConfigRes{}, err
}
return addReportConfigRes{
ReportConfig: cfg,
created: true,
}, nil
}
}
func enableReportConfigEndpoint(svc reports.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
session, ok := ctx.Value(api.SessionKey).(authn.Session)
if !ok {
return nil, svcerr.ErrAuthorization
}
req := request.(updateReportStatusReq)
if err := req.validate(); err != nil {
return updateReportConfigRes{}, err
}
cfg, err := svc.EnableReportConfig(ctx, session, req.id)
if err != nil {
return updateReportConfigRes{}, err
}
return updateReportConfigRes{ReportConfig: cfg}, nil
}
}
func disableReportConfigEndpoint(svc reports.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
session, ok := ctx.Value(api.SessionKey).(authn.Session)
if !ok {
return nil, svcerr.ErrAuthorization
}
req := request.(updateReportStatusReq)
if err := req.validate(); err != nil {
return updateReportConfigRes{}, err
}
cfg, err := svc.DisableReportConfig(ctx, session, req.id)
if err != nil {
return updateReportConfigRes{}, err
}
return updateReportConfigRes{ReportConfig: cfg}, nil
}
}
+813
View File
@@ -0,0 +1,813 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package api_test
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/0x6flab/namegenerator"
"github.com/absmach/magistrala/internal/testsutil"
pkgSch "github.com/absmach/magistrala/pkg/schedule"
"github.com/absmach/magistrala/reports"
"github.com/absmach/magistrala/reports/api"
"github.com/absmach/magistrala/reports/mocks"
apiutil "github.com/absmach/supermq/api/http/util"
"github.com/absmach/supermq/auth"
smqlog "github.com/absmach/supermq/logger"
smqauthn "github.com/absmach/supermq/pkg/authn"
authnmocks "github.com/absmach/supermq/pkg/authn/mocks"
"github.com/absmach/supermq/pkg/errors"
svcerr "github.com/absmach/supermq/pkg/errors/service"
"github.com/go-chi/chi/v5"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
const contentType = "application/json"
var (
namegen = namegenerator.NewGenerator()
domainID = testsutil.GenerateUUID(&testing.T{})
userID = testsutil.GenerateUUID(&testing.T{})
validID = testsutil.GenerateUUID(&testing.T{})
validToken = "valid"
invalidToken = "invalid"
now = time.Now().UTC().Truncate(time.Minute)
schedule = pkgSch.Schedule{
StartDateTime: now.Add(-1 * time.Hour),
Recurring: pkgSch.Daily,
RecurringPeriod: 1,
Time: now,
}
reportConfig = reports.ReportConfig{
ID: validID,
Name: namegen.Generate(),
DomainID: domainID,
Schedule: schedule,
Status: reports.EnabledStatus,
Metrics: []reports.ReqMetric{
{
ChannelID: "channel1",
ClientIDs: []string{"client1"},
Name: "metric_name",
},
},
Config: &reports.MetricConfig{
From: "now()-1h",
To: "now()",
Title: title,
Aggregation: reports.AggConfig{AggType: reports.AggregationAVG, Interval: "1h"},
},
Email: &reports.EmailSetting{
To: []string{"test@example.com"},
Subject: "Test Report",
},
}
title = "test_title"
)
type testRequest struct {
client *http.Client
method string
url string
contentType string
token string
body io.Reader
}
func (tr testRequest) make() (*http.Response, error) {
req, err := http.NewRequest(tr.method, tr.url, tr.body)
if err != nil {
return nil, err
}
if tr.token != "" {
req.Header.Set("Authorization", apiutil.BearerPrefix+tr.token)
}
if tr.contentType != "" {
req.Header.Set("Content-Type", tr.contentType)
}
req.Header.Set("Referer", "http://localhost")
return tr.client.Do(req)
}
func newReportsServer() (*httptest.Server, *mocks.Service, *authnmocks.Authentication) {
svc := new(mocks.Service)
authn := new(authnmocks.Authentication)
logger := smqlog.NewMock()
mux := chi.NewRouter()
api.MakeHandler(svc, authn, mux, logger, "")
return httptest.NewServer(mux), svc, authn
}
func toJSON(data any) string {
jsonData, err := json.Marshal(data)
if err != nil {
return ""
}
return string(jsonData)
}
func TestAddReportConfigEndpoint(t *testing.T) {
ts, svc, authn := newReportsServer()
defer ts.Close()
cases := []struct {
desc string
cfg reports.ReportConfig
domainID string
token string
contentType string
status int
authnRes smqauthn.Session
authnErr error
svcRes reports.ReportConfig
svcErr error
err error
}{
{
desc: "add report config successfully",
cfg: reportConfig,
token: validToken,
contentType: contentType,
domainID: domainID,
authnRes: smqauthn.Session{DomainUserID: auth.EncodeDomainUserID(domainID, userID), UserID: userID, DomainID: domainID},
status: http.StatusCreated,
svcRes: reportConfig,
},
{
desc: "add report config with invalid token",
cfg: reportConfig,
token: invalidToken,
authnRes: smqauthn.Session{},
domainID: domainID,
contentType: contentType,
authnErr: svcerr.ErrAuthentication,
status: http.StatusUnauthorized,
err: svcerr.ErrAuthentication,
},
{
desc: "add report config with empty token",
token: "",
authnRes: smqauthn.Session{},
domainID: domainID,
cfg: reportConfig,
contentType: contentType,
status: http.StatusUnauthorized,
err: apiutil.ErrBearerToken,
},
{
desc: "add report config with empty domainID",
token: validToken,
cfg: reportConfig,
contentType: contentType,
status: http.StatusBadRequest,
err: apiutil.ErrMissingDomainID,
},
{
desc: "add report config with invalid content type",
token: validToken,
domainID: domainID,
cfg: reportConfig,
contentType: "application/xml",
status: http.StatusUnsupportedMediaType,
err: apiutil.ErrUnsupportedContentType,
},
{
desc: "add report config with service error",
token: validToken,
domainID: domainID,
authnRes: smqauthn.Session{DomainUserID: auth.EncodeDomainUserID(domainID, userID), UserID: userID, DomainID: domainID},
cfg: reportConfig,
contentType: contentType,
svcErr: svcerr.ErrAuthorization,
status: http.StatusForbidden,
err: svcerr.ErrAuthorization,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
data := toJSON(tc.cfg)
req := testRequest{
client: ts.Client(),
method: http.MethodPost,
url: fmt.Sprintf("%s/%s/reports/configs", ts.URL, tc.domainID),
contentType: tc.contentType,
token: tc.token,
body: strings.NewReader(data),
}
authCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.authnRes, tc.authnErr)
svcCall := svc.On("AddReportConfig", mock.Anything, tc.authnRes, mock.Anything).Return(tc.svcRes, tc.svcErr)
res, err := req.make()
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
var errRes respBody
err = json.NewDecoder(res.Body).Decode(&errRes)
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err))
if errRes.Err != "" || errRes.Message != "" {
err = errors.Wrap(errors.New(errRes.Err), errors.New(errRes.Message))
}
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
svcCall.Unset()
authCall.Unset()
})
}
}
func TestViewReportConfigEndpoint(t *testing.T) {
ts, svc, authn := newReportsServer()
defer ts.Close()
cases := []struct {
desc string
id string
domainID string
token string
contentType string
status int
authnRes smqauthn.Session
authnErr error
svcRes reports.ReportConfig
svcErr error
err error
}{
{
desc: "view report config successfully",
id: validID,
token: validToken,
contentType: contentType,
domainID: domainID,
authnRes: smqauthn.Session{DomainUserID: auth.EncodeDomainUserID(domainID, userID), UserID: userID, DomainID: domainID},
status: http.StatusOK,
svcRes: reportConfig,
},
{
desc: "view report config with invalid token",
id: validID,
token: invalidToken,
authnRes: smqauthn.Session{},
domainID: domainID,
contentType: contentType,
authnErr: svcerr.ErrAuthentication,
status: http.StatusUnauthorized,
err: svcerr.ErrAuthentication,
},
{
desc: "view report config with empty token",
token: "",
authnRes: smqauthn.Session{},
domainID: domainID,
id: validID,
contentType: contentType,
status: http.StatusUnauthorized,
err: apiutil.ErrBearerToken,
},
{
desc: "view report config with empty domainID",
token: validToken,
id: validID,
contentType: contentType,
status: http.StatusBadRequest,
err: apiutil.ErrMissingDomainID,
},
{
desc: "view report config with service error",
token: validToken,
domainID: domainID,
authnRes: smqauthn.Session{DomainUserID: auth.EncodeDomainUserID(domainID, userID), UserID: userID, DomainID: domainID},
id: validID,
contentType: contentType,
svcErr: svcerr.ErrAuthorization,
status: http.StatusForbidden,
err: svcerr.ErrAuthorization,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
req := testRequest{
client: ts.Client(),
method: http.MethodGet,
url: fmt.Sprintf("%s/%s/reports/configs/%s", ts.URL, tc.domainID, tc.id),
contentType: tc.contentType,
token: tc.token,
}
authCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.authnRes, tc.authnErr)
svcCall := svc.On("ViewReportConfig", mock.Anything, tc.authnRes, tc.id).Return(tc.svcRes, tc.svcErr)
res, err := req.make()
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
var errRes respBody
err = json.NewDecoder(res.Body).Decode(&errRes)
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err))
if errRes.Err != "" || errRes.Message != "" {
err = errors.Wrap(errors.New(errRes.Err), errors.New(errRes.Message))
}
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
svcCall.Unset()
authCall.Unset()
})
}
}
func TestListReportsConfigEndpoint(t *testing.T) {
ts, svc, authn := newReportsServer()
defer ts.Close()
cases := []struct {
desc string
query string
domainID string
token string
session smqauthn.Session
listReportsResponse reports.ReportConfigPage
status int
authnErr error
err error
}{
{
desc: "list reports config successfully",
domainID: domainID,
token: validToken,
status: http.StatusOK,
listReportsResponse: reports.ReportConfigPage{
ReportConfigs: []reports.ReportConfig{reportConfig},
PageMeta: reports.PageMeta{Total: 1},
},
err: nil,
},
{
desc: "list reports config with empty token",
domainID: domainID,
token: "",
status: http.StatusUnauthorized,
err: apiutil.ErrBearerToken,
},
{
desc: "list reports config with invalid token",
domainID: domainID,
token: invalidToken,
status: http.StatusUnauthorized,
authnErr: svcerr.ErrAuthentication,
err: svcerr.ErrAuthentication,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
req := testRequest{
client: ts.Client(),
method: http.MethodGet,
url: ts.URL + "/" + tc.domainID + "/reports/configs?" + tc.query,
contentType: contentType,
token: tc.token,
}
if tc.token == validToken {
tc.session = smqauthn.Session{DomainUserID: auth.EncodeDomainUserID(domainID, userID), UserID: userID, DomainID: domainID}
}
authCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.session, tc.authnErr)
svcCall := svc.On("ListReportsConfig", mock.Anything, tc.session, mock.Anything).Return(tc.listReportsResponse, tc.err)
res, err := req.make()
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
var bodyRes respBody
err = json.NewDecoder(res.Body).Decode(&bodyRes)
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err))
if bodyRes.Err != "" || bodyRes.Message != "" {
err = errors.Wrap(errors.New(bodyRes.Err), errors.New(bodyRes.Message))
}
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
svcCall.Unset()
authCall.Unset()
})
}
}
func TestUpdateReportConfigEndpoint(t *testing.T) {
ts, svc, authn := newReportsServer()
defer ts.Close()
cases := []struct {
desc string
token string
id string
domainID string
updateReq reports.ReportConfig
contentType string
session smqauthn.Session
svcResp reports.ReportConfig
svcErr error
status int
authnErr error
err error
}{
{
desc: "update report config successfully",
token: validToken,
domainID: domainID,
id: validID,
updateReq: reportConfig,
contentType: contentType,
svcResp: reportConfig,
status: http.StatusOK,
err: nil,
},
{
desc: "update report config with invalid token",
token: invalidToken,
session: smqauthn.Session{},
domainID: domainID,
id: validID,
updateReq: reportConfig,
contentType: contentType,
authnErr: svcerr.ErrAuthentication,
status: http.StatusUnauthorized,
err: svcerr.ErrAuthentication,
},
{
desc: "update report config with empty token",
token: "",
session: smqauthn.Session{},
domainID: domainID,
id: validID,
updateReq: reportConfig,
contentType: contentType,
status: http.StatusUnauthorized,
err: apiutil.ErrBearerToken,
},
{
desc: "update report config with empty domainID",
token: validToken,
id: validID,
updateReq: reportConfig,
contentType: contentType,
status: http.StatusBadRequest,
err: apiutil.ErrMissingDomainID,
},
{
desc: "update report config with invalid content type",
token: validToken,
id: validID,
domainID: domainID,
updateReq: reportConfig,
contentType: "application/xml",
svcResp: reportConfig,
status: http.StatusUnsupportedMediaType,
err: apiutil.ErrUnsupportedContentType,
},
{
desc: "update report config with service error",
token: validToken,
id: validID,
domainID: domainID,
updateReq: reportConfig,
contentType: contentType,
svcResp: reports.ReportConfig{},
svcErr: svcerr.ErrAuthorization,
status: http.StatusForbidden,
err: svcerr.ErrAuthorization,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
data := toJSON(tc.updateReq)
req := testRequest{
client: ts.Client(),
method: http.MethodPatch,
url: fmt.Sprintf("%s/%s/reports/configs/%s", ts.URL, tc.domainID, tc.id),
contentType: tc.contentType,
token: tc.token,
body: strings.NewReader(data),
}
if tc.token == validToken {
tc.session = smqauthn.Session{DomainUserID: auth.EncodeDomainUserID(domainID, userID), UserID: userID, DomainID: domainID}
}
authCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.session, tc.authnErr)
svcCall := svc.On("UpdateReportConfig", mock.Anything, tc.session, mock.Anything).Return(tc.svcResp, tc.svcErr)
res, err := req.make()
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
var errRes respBody
err = json.NewDecoder(res.Body).Decode(&errRes)
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err))
if errRes.Err != "" || errRes.Message != "" {
err = errors.Wrap(errors.New(errRes.Err), errors.New(errRes.Message))
}
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
svcCall.Unset()
authCall.Unset()
})
}
}
func TestDeleteReportConfigEndpoint(t *testing.T) {
ts, svc, authn := newReportsServer()
defer ts.Close()
cases := []struct {
desc string
token string
id string
domainID string
session smqauthn.Session
svcErr error
status int
authnErr error
err error
}{
{
desc: "delete report config successfully",
token: validToken,
domainID: domainID,
id: validID,
svcErr: nil,
status: http.StatusNoContent,
err: nil,
},
{
desc: "delete report config with invalid token",
token: invalidToken,
session: smqauthn.Session{},
domainID: domainID,
id: validID,
authnErr: svcerr.ErrAuthentication,
status: http.StatusUnauthorized,
err: svcerr.ErrAuthentication,
},
{
desc: "delete report config with empty token",
token: "",
session: smqauthn.Session{},
domainID: domainID,
id: validID,
status: http.StatusUnauthorized,
err: apiutil.ErrBearerToken,
},
{
desc: "delete report config with empty domainID",
token: validToken,
id: validID,
status: http.StatusBadRequest,
err: apiutil.ErrMissingDomainID,
},
{
desc: "delete report config with service error",
token: validToken,
id: validID,
domainID: domainID,
svcErr: svcerr.ErrAuthorization,
status: http.StatusForbidden,
err: svcerr.ErrAuthorization,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
req := testRequest{
client: ts.Client(),
method: http.MethodDelete,
url: fmt.Sprintf("%s/%s/reports/configs/%s", ts.URL, tc.domainID, tc.id),
token: tc.token,
}
if tc.token == validToken {
tc.session = smqauthn.Session{DomainUserID: auth.EncodeDomainUserID(domainID, userID), UserID: userID, DomainID: domainID}
}
authCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.session, tc.authnErr)
svcCall := svc.On("RemoveReportConfig", mock.Anything, tc.session, tc.id).Return(tc.svcErr)
res, err := req.make()
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
svcCall.Unset()
authCall.Unset()
})
}
}
func TestEnableReportConfigEndpoint(t *testing.T) {
ts, svc, authn := newReportsServer()
defer ts.Close()
cases := []struct {
desc string
token string
id string
domainID string
session smqauthn.Session
svcResp reports.ReportConfig
svcErr error
status int
authnErr error
err error
}{
{
desc: "enable report config successfully",
token: validToken,
domainID: domainID,
id: validID,
svcResp: reportConfig,
svcErr: nil,
status: http.StatusOK,
err: nil,
},
{
desc: "enable report config with invalid token",
token: invalidToken,
session: smqauthn.Session{},
domainID: domainID,
id: validID,
authnErr: svcerr.ErrAuthentication,
status: http.StatusUnauthorized,
err: svcerr.ErrAuthentication,
},
{
desc: "enable report config with empty token",
token: "",
session: smqauthn.Session{},
domainID: domainID,
id: validID,
status: http.StatusUnauthorized,
err: apiutil.ErrBearerToken,
},
{
desc: "enable report config with empty domainID",
token: validToken,
id: validID,
status: http.StatusBadRequest,
err: apiutil.ErrMissingDomainID,
},
{
desc: "enable report config with service error",
token: validToken,
id: validID,
domainID: domainID,
svcResp: reports.ReportConfig{},
svcErr: svcerr.ErrAuthorization,
status: http.StatusForbidden,
err: svcerr.ErrAuthorization,
},
{
desc: "enable report config with empty id",
token: validToken,
id: "",
domainID: domainID,
status: http.StatusBadRequest,
err: apiutil.ErrMissingID,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
req := testRequest{
client: ts.Client(),
method: http.MethodPost,
url: fmt.Sprintf("%s/%s/reports/configs/%s/enable", ts.URL, tc.domainID, tc.id),
token: tc.token,
}
if tc.token == validToken {
tc.session = smqauthn.Session{DomainUserID: auth.EncodeDomainUserID(domainID, userID), UserID: userID, DomainID: domainID}
}
authCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.session, tc.authnErr)
svcCall := svc.On("EnableReportConfig", mock.Anything, tc.session, tc.id).Return(tc.svcResp, tc.svcErr)
res, err := req.make()
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
var errRes respBody
err = json.NewDecoder(res.Body).Decode(&errRes)
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err))
if errRes.Err != "" || errRes.Message != "" {
err = errors.Wrap(errors.New(errRes.Err), errors.New(errRes.Message))
}
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
svcCall.Unset()
authCall.Unset()
})
}
}
func TestDisableReportConfigEndpoint(t *testing.T) {
ts, svc, authn := newReportsServer()
defer ts.Close()
cases := []struct {
desc string
token string
id string
domainID string
session smqauthn.Session
svcResp reports.ReportConfig
svcErr error
status int
authnErr error
err error
}{
{
desc: "disable report config successfully",
token: validToken,
domainID: domainID,
id: validID,
svcResp: reportConfig,
svcErr: nil,
status: http.StatusOK,
err: nil,
},
{
desc: "disable report config with invalid token",
token: invalidToken,
session: smqauthn.Session{},
domainID: domainID,
id: validID,
authnErr: svcerr.ErrAuthentication,
status: http.StatusUnauthorized,
err: svcerr.ErrAuthentication,
},
{
desc: "disable report config with empty token",
token: "",
session: smqauthn.Session{},
domainID: domainID,
id: validID,
status: http.StatusUnauthorized,
err: apiutil.ErrBearerToken,
},
{
desc: "disable report config with empty domainID",
token: validToken,
id: validID,
status: http.StatusBadRequest,
err: apiutil.ErrMissingDomainID,
},
{
desc: "disable report config with service error",
token: validToken,
id: validID,
domainID: domainID,
svcResp: reports.ReportConfig{},
svcErr: svcerr.ErrAuthorization,
status: http.StatusForbidden,
err: svcerr.ErrAuthorization,
},
{
desc: "disable report config with empty id",
token: validToken,
id: "",
domainID: domainID,
status: http.StatusBadRequest,
err: apiutil.ErrMissingID,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
req := testRequest{
client: ts.Client(),
method: http.MethodPost,
url: fmt.Sprintf("%s/%s/reports/configs/%s/disable", ts.URL, tc.domainID, tc.id),
token: tc.token,
}
if tc.token == validToken {
tc.session = smqauthn.Session{DomainUserID: auth.EncodeDomainUserID(domainID, userID), UserID: userID, DomainID: domainID}
}
authCall := authn.On("Authenticate", mock.Anything, tc.token).Return(tc.session, tc.authnErr)
svcCall := svc.On("DisableReportConfig", mock.Anything, tc.session, tc.id).Return(tc.svcResp, tc.svcErr)
res, err := req.make()
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err))
var errRes respBody
err = json.NewDecoder(res.Body).Decode(&errRes)
assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err))
if errRes.Err != "" || errRes.Message != "" {
err = errors.Wrap(errors.New(errRes.Err), errors.New(errRes.Message))
}
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode))
svcCall.Unset()
authCall.Unset()
})
}
}
type respBody struct {
Err string `json:"error"`
Message string `json:"message"`
Total uint64 `json:"total"`
ID string `json:"id"`
Status reports.Status `json:"status"`
}
+171
View File
@@ -0,0 +1,171 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package api
import (
"fmt"
"github.com/absmach/magistrala/pkg/schedule"
"github.com/absmach/magistrala/reports"
apiutil "github.com/absmach/supermq/api/http/util"
"github.com/absmach/supermq/pkg/errors"
svcerr "github.com/absmach/supermq/pkg/errors/service"
)
const (
maxLimitSize = 1000
MaxNameSize = 1024
MaxTitleSize = 37
errInvalidMetric = "invalid metric[%d]: %w"
)
var (
errInvalidReportAction = errors.New("invalid report action")
errMetricsNotProvided = errors.New("metrics not provided")
errMissingReportConfig = errors.New("missing report config")
errMissingReportEmailConfig = errors.New("missing report email config")
errInvalidRecurringPeriod = errors.New("invalid recurring period")
errTitleSize = errors.New("invalid title size")
)
type addReportConfigReq struct {
reports.ReportConfig `json:",inline"`
}
func (req addReportConfigReq) validate() error {
if req.Name == "" {
return apiutil.ErrMissingName
}
return validateReportConfig(req.ReportConfig, false, false)
}
type viewReportConfigReq struct {
ID string `json:"id"`
}
func (req viewReportConfigReq) validate() error {
if req.ID == "" {
return apiutil.ErrMissingID
}
return nil
}
type listReportsConfigReq struct {
reports.PageMeta `json:",inline"`
}
func (req listReportsConfigReq) validate() error {
if req.Limit > maxLimitSize {
return svcerr.ErrMalformedEntity
}
return nil
}
type updateReportConfigReq struct {
reports.ReportConfig `json:",inline"`
}
func (req updateReportConfigReq) validate() error {
if req.ID == "" {
return apiutil.ErrMissingID
}
return validateReportConfig(req.ReportConfig, false, false)
}
type updateReportScheduleReq struct {
id string
Schedule schedule.Schedule `json:"schedule,omitempty"`
}
func (req updateReportScheduleReq) validate() error {
if req.id == "" {
return apiutil.ErrMissingID
}
return nil
}
type deleteReportConfigReq struct {
ID string `json:"id"`
}
func (req deleteReportConfigReq) validate() error {
if req.ID == "" {
return apiutil.ErrMissingID
}
return nil
}
type generateReportReq struct {
reports.ReportConfig
action reports.ReportAction
}
func (req generateReportReq) validate() error {
if len(req.Config.Title) > MaxTitleSize {
return errors.Wrap(apiutil.ErrValidation, errTitleSize)
}
switch req.action {
case reports.ViewReport, reports.DownloadReport:
return validateReportConfig(req.ReportConfig, true, true)
case reports.EmailReport:
return validateReportConfig(req.ReportConfig, false, true)
default:
return errors.Wrap(apiutil.ErrValidation, errInvalidReportAction)
}
}
type updateReportStatusReq struct {
id string
}
func (req updateReportStatusReq) validate() error {
if req.id == "" {
return apiutil.ErrMissingID
}
return nil
}
func validateReportConfig(req reports.ReportConfig, skipEmailValidation bool, skipSchedularValidation bool) error {
if len(req.Metrics) == 0 {
return errors.Wrap(apiutil.ErrValidation, errMetricsNotProvided)
}
for i, metric := range req.Metrics {
if err := metric.Validate(); err != nil {
return errors.Wrap(apiutil.ErrValidation, fmt.Errorf(errInvalidMetric, i+1, err))
}
}
if req.Config == nil {
return errors.Wrap(errMissingReportConfig, apiutil.ErrValidation)
}
if err := req.Config.Validate(); err != nil {
return errors.Wrap(err, apiutil.ErrValidation)
}
if skipEmailValidation {
return nil
}
if req.Email == nil {
return errors.Wrap(errMissingReportEmailConfig, apiutil.ErrValidation)
}
if err := req.Email.Validate(); err != nil {
return errors.Wrap(apiutil.ErrValidation, err)
}
if skipSchedularValidation {
return nil
}
return validateScheduler(req.Schedule)
}
func validateScheduler(sch schedule.Schedule) error {
if sch.Recurring != schedule.None && sch.RecurringPeriod < 1 {
return errors.Wrap(apiutil.ErrValidation, errInvalidRecurringPeriod)
}
return nil
}
+167
View File
@@ -0,0 +1,167 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package api
import (
"net/http"
"time"
"github.com/absmach/magistrala/reports"
"github.com/absmach/supermq"
)
var (
_ supermq.Response = (*addReportConfigRes)(nil)
_ supermq.Response = (*viewReportConfigRes)(nil)
_ supermq.Response = (*updateReportConfigRes)(nil)
_ supermq.Response = (*deleteReportConfigRes)(nil)
_ supermq.Response = (*listReportsConfigRes)(nil)
)
type pageRes struct {
Limit uint64 `json:"limit,omitempty"`
Offset uint64 `json:"offset"`
Total uint64 `json:"total"`
}
type generateReportResp struct {
Total uint64 `json:"total"`
From time.Time `json:"from,omitempty"`
To time.Time `json:"to,omitempty"`
Aggregation reports.AggConfig `json:"aggregation,omitempty"`
Reports []reports.Report `json:"reports,omitempty"`
}
func (res generateReportResp) Code() int {
return http.StatusCreated
}
func (res generateReportResp) Headers() map[string]string {
return map[string]string{}
}
func (res generateReportResp) Empty() bool {
return false
}
type addReportConfigRes struct {
reports.ReportConfig `json:",inline"`
created bool
}
func (res addReportConfigRes) Code() int {
if res.created {
return http.StatusCreated
}
return http.StatusOK
}
func (res addReportConfigRes) Headers() map[string]string {
if res.created {
return map[string]string{}
}
return map[string]string{}
}
func (res addReportConfigRes) Empty() bool {
return false
}
type viewReportConfigRes struct {
reports.ReportConfig `json:",inline"`
}
func (res viewReportConfigRes) Code() int {
return http.StatusOK
}
func (res viewReportConfigRes) Headers() map[string]string {
return map[string]string{}
}
func (res viewReportConfigRes) Empty() bool {
return false
}
type updateReportConfigRes struct {
reports.ReportConfig `json:",inline"`
}
func (res updateReportConfigRes) Code() int {
return http.StatusOK
}
func (res updateReportConfigRes) Headers() map[string]string {
return map[string]string{}
}
func (res updateReportConfigRes) Empty() bool {
return false
}
type deleteReportConfigRes struct {
deleted bool
}
func (res deleteReportConfigRes) Code() int {
if res.deleted {
return http.StatusNoContent
}
return http.StatusOK
}
func (res deleteReportConfigRes) Headers() map[string]string {
return map[string]string{}
}
func (res deleteReportConfigRes) Empty() bool {
return true
}
type listReportsConfigRes struct {
pageRes
ReportConfigs []reports.ReportConfig `json:"report_configs"`
}
func (res listReportsConfigRes) Code() int {
return http.StatusOK
}
func (res listReportsConfigRes) Headers() map[string]string {
return map[string]string{}
}
func (res listReportsConfigRes) Empty() bool {
return false
}
type downloadReportResp struct {
File reports.ReportFile
}
func (res downloadReportResp) Code() int {
return http.StatusOK
}
func (res downloadReportResp) Headers() map[string]string {
return map[string]string{}
}
func (res downloadReportResp) Empty() bool {
return false
}
type emailReportResp struct{}
func (res emailReportResp) Code() int {
return http.StatusOK
}
func (res emailReportResp) Headers() map[string]string {
return map[string]string{}
}
func (res emailReportResp) Empty() bool {
return true
}
+247
View File
@@ -0,0 +1,247 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package api
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"net/http"
"strings"
"github.com/absmach/magistrala/reports"
"github.com/absmach/supermq"
api "github.com/absmach/supermq/api/http"
apiutil "github.com/absmach/supermq/api/http/util"
mgauthn "github.com/absmach/supermq/pkg/authn"
"github.com/absmach/supermq/pkg/errors"
"github.com/go-chi/chi/v5"
kithttp "github.com/go-kit/kit/transport/http"
"github.com/prometheus/client_golang/prometheus/promhttp"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
)
const (
reportIdKey = "reportID"
statusKey = "status"
actionKey = "action"
defAction = "view"
)
// MakeHandler creates an HTTP handler for the service endpoints.
func MakeHandler(svc reports.Service, authn mgauthn.Authentication, mux *chi.Mux, logger *slog.Logger, instanceID string) http.Handler {
opts := []kithttp.ServerOption{
kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, api.EncodeError)),
}
mux.Group(func(r chi.Router) {
r.Use(api.AuthenticateMiddleware(authn, true))
r.Route("/{domainID}", func(r chi.Router) {
r.Route("/reports", func(r chi.Router) {
r.Post("/", otelhttp.NewHandler(kithttp.NewServer(
generateReportEndpoint(svc),
decodeGenerateReportRequest,
encodeFileDownloadResponse,
opts...,
), "generate_report").ServeHTTP)
r.Route("/configs", func(r chi.Router) {
r.Post("/", otelhttp.NewHandler(kithttp.NewServer(
addReportConfigEndpoint(svc),
decodeAddReportConfigRequest,
api.EncodeResponse,
opts...,
), "add_report_config").ServeHTTP)
r.Get("/{reportID}", otelhttp.NewHandler(kithttp.NewServer(
viewReportConfigEndpoint(svc),
decodeViewReportConfigRequest,
api.EncodeResponse,
opts...,
), "view_report_config").ServeHTTP)
r.Patch("/{reportID}", otelhttp.NewHandler(kithttp.NewServer(
updateReportConfigEndpoint(svc),
decodeUpdateReportConfigRequest,
api.EncodeResponse,
opts...,
), "update_report_config").ServeHTTP)
r.Patch("/{reportID}/schedule", otelhttp.NewHandler(kithttp.NewServer(
updateReportScheduleEndpoint(svc),
decodeUpdateReportScheduleRequest,
api.EncodeResponse,
opts...,
), "update_report_scheduler").ServeHTTP)
r.Delete("/{reportID}", otelhttp.NewHandler(kithttp.NewServer(
deleteReportConfigEndpoint(svc),
decodeDeleteReportConfigRequest,
api.EncodeResponse,
opts...,
), "delete_report_config").ServeHTTP)
r.Get("/", otelhttp.NewHandler(kithttp.NewServer(
listReportsConfigEndpoint(svc),
decodeListReportsConfigRequest,
api.EncodeResponse,
opts...,
), "list_reports_config").ServeHTTP)
r.Post("/{reportID}/enable", otelhttp.NewHandler(kithttp.NewServer(
enableReportConfigEndpoint(svc),
decodeUpdateReportStatusRequest,
api.EncodeResponse,
opts...,
), "enable_report_config").ServeHTTP)
r.Post("/{reportID}/disable", otelhttp.NewHandler(kithttp.NewServer(
disableReportConfigEndpoint(svc),
decodeUpdateReportStatusRequest,
api.EncodeResponse,
opts...,
), "disable_report_config").ServeHTTP)
})
})
})
})
mux.Get("/health", supermq.Health("rule_engine", instanceID))
mux.Handle("/metrics", promhttp.Handler())
return mux
}
func decodeGenerateReportRequest(_ context.Context, r *http.Request) (interface{}, error) {
if !strings.Contains(r.Header.Get("Content-Type"), api.ContentType) {
return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType)
}
a, err := apiutil.ReadStringQuery(r, actionKey, defAction)
if err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, err)
}
action, err := reports.ToReportAction(a)
if err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, err)
}
req := generateReportReq{
action: action,
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, errors.Wrap(err, apiutil.ErrValidation)
}
return req, nil
}
func decodeAddReportConfigRequest(_ context.Context, r *http.Request) (interface{}, error) {
if !strings.Contains(r.Header.Get("Content-Type"), api.ContentType) {
return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType)
}
var config reports.ReportConfig
if err := json.NewDecoder(r.Body).Decode(&config); err != nil {
return nil, errors.Wrap(err, apiutil.ErrValidation)
}
return addReportConfigReq{ReportConfig: config}, nil
}
func decodeViewReportConfigRequest(_ context.Context, r *http.Request) (interface{}, error) {
id := chi.URLParam(r, reportIdKey)
return viewReportConfigReq{ID: id}, nil
}
func decodeUpdateReportConfigRequest(_ context.Context, r *http.Request) (interface{}, error) {
if !strings.Contains(r.Header.Get("Content-Type"), api.ContentType) {
return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType)
}
var config reports.ReportConfig
if err := json.NewDecoder(r.Body).Decode(&config); err != nil {
return nil, errors.Wrap(err, apiutil.ErrValidation)
}
config.ID = chi.URLParam(r, reportIdKey)
return updateReportConfigReq{ReportConfig: config}, nil
}
func decodeUpdateReportScheduleRequest(_ context.Context, r *http.Request) (interface{}, error) {
if !strings.Contains(r.Header.Get("Content-Type"), api.ContentType) {
return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType)
}
req := updateReportScheduleReq{
id: chi.URLParam(r, reportIdKey),
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(errors.ErrMalformedEntity, err))
}
return req, nil
}
func decodeUpdateReportStatusRequest(_ context.Context, r *http.Request) (interface{}, error) {
req := updateReportStatusReq{
id: chi.URLParam(r, reportIdKey),
}
return req, nil
}
func decodeDeleteReportConfigRequest(_ context.Context, r *http.Request) (interface{}, error) {
id := chi.URLParam(r, reportIdKey)
return deleteReportConfigReq{ID: id}, nil
}
func decodeListReportsConfigRequest(_ context.Context, r *http.Request) (interface{}, error) {
offset, err := apiutil.ReadNumQuery[uint64](r, api.OffsetKey, api.DefOffset)
if err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, err)
}
limit, err := apiutil.ReadNumQuery[uint64](r, api.LimitKey, api.DefLimit)
if err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, err)
}
status, err := apiutil.ReadStringQuery(r, api.StatusKey, api.DefStatus)
if err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, err)
}
st, err := reports.ToStatus(status)
if err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, err)
}
name, err := apiutil.ReadStringQuery(r, api.NameKey, "")
if err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, err)
}
return listReportsConfigReq{
PageMeta: reports.PageMeta{
Offset: offset,
Limit: limit,
Status: st,
Name: name,
},
}, nil
}
func encodeFileDownloadResponse(_ context.Context, w http.ResponseWriter, response interface{}) error {
switch resp := response.(type) {
case downloadReportResp:
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", resp.File.Name))
w.Header().Set("Content-Type", resp.File.Format.ContentType())
_, err := w.Write(resp.File.Data)
return err
default:
if ar, ok := response.(supermq.Response); ok {
for k, v := range ar.Headers() {
w.Header().Set(k, v)
}
w.Header().Set("Content-Type", api.ContentType)
w.WriteHeader(ar.Code())
if ar.Empty() {
return nil
}
}
return json.NewEncoder(w).Encode(response)
}
}
+423
View File
@@ -0,0 +1,423 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package reports
import (
"bytes"
"encoding/csv"
"fmt"
"sort"
"time"
"github.com/absmach/supermq/pkg/errors"
svcerr "github.com/absmach/supermq/pkg/errors/service"
"github.com/absmach/supermq/pkg/transformers/senml"
"github.com/johnfercher/maroto/pkg/color"
"github.com/johnfercher/maroto/pkg/consts"
"github.com/johnfercher/maroto/pkg/pdf"
"github.com/johnfercher/maroto/pkg/props"
)
func generatePDFReport(title string, reports []Report) ([]byte, error) {
m := pdf.NewMaroto(consts.Portrait, consts.A4)
m.SetPageMargins(10, 15, 10)
primaryColor := color.Color{Red: 41, Green: 128, Blue: 185} // Blue
secondaryColor := color.Color{Red: 26, Green: 82, Blue: 118} // Darker blue
subtleColor := color.Color{Red: 189, Green: 195, Blue: 199} // Light gray
tableHeaderBg := color.Color{Red: 236, Green: 240, Blue: 241} // Very light gray
alternateRow := color.Color{Red: 245, Green: 247, Blue: 249} // Even lighter gray
textPrimary := color.Color{Red: 44, Green: 62, Blue: 80} // Dark blue-gray
textSecondary := color.Color{Red: 127, Green: 140, Blue: 141} // Medium gray
white := color.NewWhite()
m.RegisterHeader(func() {
m.SetBackgroundColor(primaryColor)
m.Row(2, func() { m.Col(12, func() {}) })
m.SetBackgroundColor(white)
m.Row(20, func() {
m.Col(2, func() {})
m.Col(8, func() {
m.Text(title, props.Text{
Size: 20,
Style: consts.Bold,
Color: primaryColor,
Align: consts.Center,
Top: 6,
})
})
m.Col(2, func() {
m.Text(time.Now().Format("02 Jan 2006"), props.Text{
Size: 10,
Style: consts.Italic,
Align: consts.Right,
Color: textSecondary,
Top: 8,
})
})
})
m.SetBackgroundColor(subtleColor)
m.Row(0.5, func() { m.Col(12, func() {}) })
m.SetBackgroundColor(white)
m.Row(0.25, func() {})
m.SetBackgroundColor(subtleColor)
m.Row(0.25, func() { m.Col(12, func() {}) })
m.SetBackgroundColor(white)
m.Row(5, func() {})
})
m.RegisterFooter(func() {
currentPage := m.GetCurrentPage()
m.Row(5, func() {})
m.SetBackgroundColor(subtleColor)
m.Row(0.25, func() { m.Col(12, func() {}) })
m.SetBackgroundColor(white)
m.Row(0.25, func() {})
m.SetBackgroundColor(subtleColor)
m.Row(0.5, func() { m.Col(12, func() {}) })
m.SetBackgroundColor(white)
m.Row(10, func() {
m.Col(4, func() {
m.Text("Generated: "+time.Now().Format("15:04:05"), props.Text{
Size: 8,
Style: consts.Italic,
Align: consts.Left,
Color: textSecondary,
Top: 3,
})
})
m.Col(4, func() {
m.Text(fmt.Sprintf("Page %d", currentPage+1), props.Text{
Size: 9,
Style: consts.Bold,
Align: consts.Center,
Color: textPrimary,
Top: 3,
})
})
})
})
headers := []string{"Time", "Value", "Unit", "Protocol", "Subtopic"}
widths := []uint{3, 2, 2, 2, 3}
for i, report := range reports {
if i > 0 {
m.AddPage()
}
m.Row(0.5, func() {
m.Col(1, func() {})
})
m.SetBackgroundColor(white)
m.Row(10, func() {
m.Col(12, func() {
m.Text("Metrics", props.Text{
Size: 16,
Style: consts.Bold,
Color: secondaryColor,
Top: 2,
})
})
})
m.SetBackgroundColor(alternateRow)
m.Row(0.5, func() { m.Col(12, func() {}) })
m.Row(8, func() {
m.Col(2, func() {
m.Text("Name: ", props.Text{
Size: 11,
Style: consts.Bold,
Align: consts.Left,
Color: textPrimary,
Top: 1,
})
})
m.Col(10, func() {
m.Text(report.Metric.Name, props.Text{
Size: 11,
Style: consts.Italic,
Color: textPrimary,
Top: 1,
})
})
})
if report.Metric.ClientID != "" {
m.Row(8, func() {
m.Col(2, func() {
m.Text("Device ID: ", props.Text{
Size: 11,
Style: consts.Bold,
Align: consts.Left,
Color: textPrimary,
Top: 1,
})
})
m.Col(10, func() {
m.Text(report.Metric.ClientID, props.Text{
Size: 11,
Style: consts.Italic,
Color: textPrimary,
Top: 1,
})
})
})
}
m.Row(8, func() {
m.Col(2, func() {
m.Text("Channel ID: ", props.Text{
Size: 11,
Style: consts.Bold,
Align: consts.Left,
Color: textPrimary,
Top: 1,
})
})
m.Col(10, func() {
m.Text(report.Metric.ChannelID, props.Text{
Size: 11,
Style: consts.Italic,
Color: textPrimary,
Top: 1,
})
})
})
m.SetBackgroundColor(alternateRow)
m.Row(0.5, func() { m.Col(12, func() {}) })
m.SetBackgroundColor(white)
m.Row(10, func() {
m.Col(12, func() {
m.Text(fmt.Sprintf("Total Records: %d", len(report.Messages)), props.Text{
Size: 10,
Style: consts.Italic,
Align: consts.Right,
Color: textSecondary,
Top: 2,
})
})
})
m.SetBackgroundColor(primaryColor)
m.Row(1, func() { m.Col(12, func() {}) })
m.SetBackgroundColor(tableHeaderBg)
m.Row(10, func() {
for i, header := range headers {
m.Col(widths[i], func() {
m.Text(header, props.Text{
Size: 11,
Style: consts.Bold,
Align: consts.Center,
Top: 2,
Color: secondaryColor,
})
})
}
})
m.SetBackgroundColor(subtleColor)
m.Row(0.5, func() { m.Col(12, func() {}) })
m.SetBackgroundColor(white)
useAlternateColor := false
for _, msg := range report.Messages {
if useAlternateColor {
m.SetBackgroundColor(alternateRow)
}
m.Row(9, func() {
m.Col(widths[0], func() {
m.Text(formatTime(msg.Time), props.Text{
Size: 10,
Align: consts.Center,
Top: 2,
Color: textPrimary,
})
})
m.Col(widths[1], func() {
m.Text(formatValue(msg), props.Text{
Size: 10,
Style: consts.Normal,
Align: consts.Center,
Top: 2,
Color: textPrimary,
})
})
m.Col(widths[2], func() {
m.Text(msg.Unit, props.Text{
Size: 10,
Style: consts.Italic,
Align: consts.Center,
Top: 2,
Color: textSecondary,
})
})
m.Col(widths[3], func() {
m.Text(msg.Protocol, props.Text{
Size: 10,
Align: consts.Center,
Top: 2,
Color: textPrimary,
})
})
m.Col(widths[4], func() {
m.Text(msg.Subtopic, props.Text{
Size: 10,
Align: consts.Center,
Top: 2,
Color: secondaryColor,
})
})
})
if !useAlternateColor {
m.Row(0.2, func() {
m.Col(12, func() {})
})
}
useAlternateColor = !useAlternateColor
m.SetBackgroundColor(white)
}
}
buf, err := m.Output()
if err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
return buf.Bytes(), nil
}
func formatTime(t float64) string {
if t > 9999999999 {
return time.Unix(0, int64(t)).Format("2006-01-02 15:04:05")
}
return time.Unix(int64(t), 0).Format("2006-01-02 15:04:05")
}
func formatValue(msg senml.Message) string {
switch {
case msg.Value != nil:
return fmt.Sprintf("%.2f", *msg.Value)
case msg.StringValue != nil:
return *msg.StringValue
case msg.BoolValue != nil:
return fmt.Sprintf("%t", *msg.BoolValue)
case msg.DataValue != nil:
return *msg.DataValue
default:
return "N/A"
}
}
func generateCSVReport(title string, reports []Report) ([]byte, error) {
var buf bytes.Buffer
writer := csv.NewWriter(&buf)
headers := []string{"Time", "Value", "Unit", "Protocol", "Subtopic"}
for i, report := range reports {
if i > 0 {
if err := writer.Write([]string{""}); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
if err := writer.Write([]string{"=== NEW REPORT ==="}); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
if err := writer.Write([]string{""}); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
} else {
if err := writer.Write([]string{title}); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
if err := writer.Write([]string{""}); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
}
if err := writer.Write([]string{"Report Information:"}); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
if err := writer.Write([]string{"Name", report.Metric.Name}); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
if report.Metric.ClientID != "" {
if err := writer.Write([]string{"Device ID", report.Metric.ClientID}); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
}
if err := writer.Write([]string{"Channel ID", report.Metric.ChannelID}); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
if err := writer.Write([]string{""}); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
if err := writer.Write(headers); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
sort.Slice(report.Messages, func(i, j int) bool {
return report.Messages[i].Time < report.Messages[j].Time
})
for _, msg := range report.Messages {
timeStr := formatTime(msg.Time)
var valueStr string
if msg.Value != nil {
valueStr = fmt.Sprintf("%.2f", *msg.Value)
} else if msg.StringValue != nil {
valueStr = *msg.StringValue
} else if msg.BoolValue != nil {
valueStr = fmt.Sprintf("%v", *msg.BoolValue)
} else if msg.DataValue != nil {
valueStr = *msg.DataValue
} else {
valueStr = "N/A"
}
row := []string{
timeStr,
valueStr,
msg.Unit,
msg.Protocol,
msg.Subtopic,
}
if err := writer.Write(row); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
}
}
writer.Flush()
if err := writer.Error(); err != nil {
return nil, errors.Wrap(svcerr.ErrCreateEntity, err)
}
return buf.Bytes(), nil
}
+64
View File
@@ -0,0 +1,64 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package reports
import (
"context"
"fmt"
"log/slog"
"time"
pkglog "github.com/absmach/magistrala/pkg/logger"
)
func (r *report) StartScheduler(ctx context.Context) error {
defer r.ticker.Stop()
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-r.ticker.Tick():
due := time.Now().UTC()
pm := PageMeta{
Status: EnabledStatus,
ScheduledBefore: &due,
}
reportConfigs, err := r.repo.ListReportsConfig(ctx, pm)
if err != nil {
r.runInfo <- pkglog.RunInfo{
Level: slog.LevelError,
Message: fmt.Sprintf("failed to list reports : %s", err),
Details: []slog.Attr{slog.Time("due", due)},
}
continue
}
for _, c := range reportConfigs.ReportConfigs {
go func(cfg ReportConfig) {
if _, err := r.repo.UpdateReportDue(ctx, cfg.ID, cfg.Schedule.NextDue()); err != nil {
r.runInfo <- pkglog.RunInfo{Level: slog.LevelError, Message: fmt.Sprintf("failed to update report: %s", err), Details: []slog.Attr{slog.Time("time", time.Now().UTC())}}
return
}
_, err := r.generateReport(ctx, cfg, EmailReport)
ret := pkglog.RunInfo{
Details: []slog.Attr{
slog.String("domain_id", cfg.DomainID),
slog.String("report_id", cfg.ID),
slog.String("report_name", cfg.Name),
slog.Time("exec_time", time.Now().UTC()),
},
}
if err != nil {
ret.Level = slog.LevelError
ret.Message = fmt.Sprintf("failed to generate report: %s", err)
}
r.runInfo <- ret
}(c)
}
}
}
}
+190
View File
@@ -0,0 +1,190 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package middleware
import (
"context"
"github.com/absmach/magistrala/reports"
"github.com/absmach/supermq/pkg/authn"
smqauthz "github.com/absmach/supermq/pkg/authz"
"github.com/absmach/supermq/pkg/errors"
"github.com/absmach/supermq/pkg/policies"
)
var (
errDomainCreateConfigs = errors.New("not authorized to create report configs in domain")
errDomainViewConfigs = errors.New("not authorized to view report configs in domain")
errDomainUpdateConfigs = errors.New("not authorized to update report configs in domain")
errDomainDeleteConfigs = errors.New("not authorized to delete report configs in domain")
errDomainGenerateReports = errors.New("not authorized to generate reports in domain")
)
type authorizationMiddleware struct {
svc reports.Service
authz smqauthz.Authorization
}
// AuthorizationMiddleware adds authorization to the reports service.
func AuthorizationMiddleware(svc reports.Service, authz smqauthz.Authorization) (reports.Service, error) {
return &authorizationMiddleware{
svc: svc,
authz: authz,
}, nil
}
func (am *authorizationMiddleware) AddReportConfig(ctx context.Context, session authn.Session, cfg reports.ReportConfig) (reports.ReportConfig, error) {
if err := am.authorize(ctx, smqauthz.PolicyReq{
Domain: session.DomainID,
SubjectType: policies.UserType,
SubjectKind: policies.UsersKind,
Subject: session.DomainUserID,
Object: session.DomainID,
ObjectType: policies.DomainType,
Permission: policies.MembershipPermission,
}); err != nil {
return reports.ReportConfig{}, errors.Wrap(errDomainCreateConfigs, err)
}
return am.svc.AddReportConfig(ctx, session, cfg)
}
func (am *authorizationMiddleware) ViewReportConfig(ctx context.Context, session authn.Session, id string) (reports.ReportConfig, error) {
if err := am.authorize(ctx, smqauthz.PolicyReq{
Domain: session.DomainID,
SubjectType: policies.UserType,
SubjectKind: policies.UsersKind,
Subject: session.DomainUserID,
Object: session.DomainID,
ObjectType: policies.DomainType,
Permission: policies.MembershipPermission,
}); err != nil {
return reports.ReportConfig{}, errors.Wrap(errDomainViewConfigs, err)
}
return am.svc.ViewReportConfig(ctx, session, id)
}
func (am *authorizationMiddleware) UpdateReportConfig(ctx context.Context, session authn.Session, cfg reports.ReportConfig) (reports.ReportConfig, error) {
if err := am.authorize(ctx, smqauthz.PolicyReq{
Domain: session.DomainID,
SubjectType: policies.UserType,
SubjectKind: policies.UsersKind,
Subject: session.DomainUserID,
Object: session.DomainID,
ObjectType: policies.DomainType,
Permission: policies.MembershipPermission,
}); err != nil {
return reports.ReportConfig{}, errors.Wrap(errDomainUpdateConfigs, err)
}
return am.svc.UpdateReportConfig(ctx, session, cfg)
}
func (am *authorizationMiddleware) UpdateReportSchedule(ctx context.Context, session authn.Session, cfg reports.ReportConfig) (reports.ReportConfig, error) {
if err := am.authorize(ctx, smqauthz.PolicyReq{
Domain: session.DomainID,
SubjectType: policies.UserType,
SubjectKind: policies.UsersKind,
Subject: session.DomainUserID,
Object: session.DomainID,
ObjectType: policies.DomainType,
Permission: policies.MembershipPermission,
}); err != nil {
return reports.ReportConfig{}, errors.Wrap(errDomainDeleteConfigs, err)
}
return am.svc.UpdateReportSchedule(ctx, session, cfg)
}
func (am *authorizationMiddleware) RemoveReportConfig(ctx context.Context, session authn.Session, id string) error {
if err := am.authorize(ctx, smqauthz.PolicyReq{
Domain: session.DomainID,
SubjectType: policies.UserType,
SubjectKind: policies.UsersKind,
Subject: session.DomainUserID,
Object: session.DomainID,
ObjectType: policies.DomainType,
Permission: policies.MembershipPermission,
}); err != nil {
return errors.Wrap(errDomainDeleteConfigs, err)
}
return am.svc.RemoveReportConfig(ctx, session, id)
}
func (am *authorizationMiddleware) ListReportsConfig(ctx context.Context, session authn.Session, pm reports.PageMeta) (reports.ReportConfigPage, error) {
if err := am.authorize(ctx, smqauthz.PolicyReq{
Domain: session.DomainID,
SubjectType: policies.UserType,
SubjectKind: policies.UsersKind,
Subject: session.DomainUserID,
Object: session.DomainID,
ObjectType: policies.DomainType,
Permission: policies.MembershipPermission,
}); err != nil {
return reports.ReportConfigPage{}, errors.Wrap(errDomainViewConfigs, err)
}
return am.svc.ListReportsConfig(ctx, session, pm)
}
func (am *authorizationMiddleware) EnableReportConfig(ctx context.Context, session authn.Session, id string) (reports.ReportConfig, error) {
if err := am.authorize(ctx, smqauthz.PolicyReq{
Domain: session.DomainID,
SubjectType: policies.UserType,
SubjectKind: policies.UsersKind,
Subject: session.DomainUserID,
Object: session.DomainID,
ObjectType: policies.DomainType,
Permission: policies.MembershipPermission,
}); err != nil {
return reports.ReportConfig{}, errors.Wrap(errDomainUpdateConfigs, err)
}
return am.svc.EnableReportConfig(ctx, session, id)
}
func (am *authorizationMiddleware) DisableReportConfig(ctx context.Context, session authn.Session, id string) (reports.ReportConfig, error) {
if err := am.authorize(ctx, smqauthz.PolicyReq{
Domain: session.DomainID,
SubjectType: policies.UserType,
SubjectKind: policies.UsersKind,
Subject: session.DomainUserID,
Object: session.DomainID,
ObjectType: policies.DomainType,
Permission: policies.MembershipPermission,
}); err != nil {
return reports.ReportConfig{}, errors.Wrap(errDomainUpdateConfigs, err)
}
return am.svc.DisableReportConfig(ctx, session, id)
}
func (am *authorizationMiddleware) GenerateReport(ctx context.Context, session authn.Session, config reports.ReportConfig, action reports.ReportAction) (reports.ReportPage, error) {
if err := am.authorize(ctx, smqauthz.PolicyReq{
Domain: session.DomainID,
SubjectType: policies.UserType,
SubjectKind: policies.UsersKind,
Subject: session.DomainUserID,
Object: session.DomainID,
ObjectType: policies.DomainType,
Permission: policies.MembershipPermission,
}); err != nil {
return reports.ReportPage{}, errors.Wrap(errDomainGenerateReports, err)
}
return am.svc.GenerateReport(ctx, session, config, action)
}
func (am *authorizationMiddleware) StartScheduler(ctx context.Context) error {
return am.svc.StartScheduler(ctx)
}
func (am *authorizationMiddleware) authorize(ctx context.Context, pr smqauthz.PolicyReq) error {
if err := am.authz.Authorize(ctx, pr); err != nil {
return err
}
return nil
}
+210
View File
@@ -0,0 +1,210 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package middleware
import (
"context"
"log/slog"
"time"
"github.com/absmach/magistrala/reports"
"github.com/absmach/supermq/pkg/authn"
)
var _ reports.Service = (*loggingMiddleware)(nil)
type loggingMiddleware struct {
logger *slog.Logger
svc reports.Service
}
func LoggingMiddleware(svc reports.Service, logger *slog.Logger) reports.Service {
return &loggingMiddleware{logger, svc}
}
func (lm *loggingMiddleware) StartScheduler(ctx context.Context) (err error) {
defer func(begin time.Time) {
args := []any{
slog.String("duration", time.Since(begin).String()),
}
if err != nil {
args = append(args, slog.String("error", err.Error()))
lm.logger.Warn("Start scheduler failed", args...)
return
}
lm.logger.Info("Start scheduler completed successfully", args...)
}(time.Now())
return lm.svc.StartScheduler(ctx)
}
func (lm *loggingMiddleware) GenerateReport(ctx context.Context, session authn.Session, config reports.ReportConfig, action reports.ReportAction) (page reports.ReportPage, err error) {
defer func(begin time.Time) {
args := []any{
slog.String("duration", time.Since(begin).String()),
}
if err != nil {
args = append(args, slog.String("error", err.Error()))
lm.logger.Warn("Generate report failed", args...)
return
}
lm.logger.Info("Generate report completed", args...)
}(time.Now())
return lm.svc.GenerateReport(ctx, session, config, action)
}
func (lm *loggingMiddleware) AddReportConfig(ctx context.Context, session authn.Session, config reports.ReportConfig) (res reports.ReportConfig, err error) {
defer func(begin time.Time) {
args := []any{
slog.String("duration", time.Since(begin).String()),
slog.String("domain_id", session.DomainID),
slog.String("report_name", config.Name),
}
if err != nil {
args = append(args, slog.String("error", err.Error()))
lm.logger.Warn("Add report config failed", args...)
return
}
lm.logger.Info("Add report config completed successfully", args...)
}(time.Now())
return lm.svc.AddReportConfig(ctx, session, config)
}
func (lm *loggingMiddleware) ViewReportConfig(ctx context.Context, session authn.Session, id string) (res reports.ReportConfig, err error) {
defer func(begin time.Time) {
args := []any{
slog.String("duration", time.Since(begin).String()),
slog.String("domain_id", session.DomainID),
slog.Group("report_config",
slog.String("id", res.ID),
slog.String("name", res.Name),
),
}
if err != nil {
args = append(args, slog.String("error", err.Error()))
lm.logger.Warn("View report config failed", args...)
return
}
lm.logger.Info("View report config completed successfully", args...)
}(time.Now())
return lm.svc.ViewReportConfig(ctx, session, id)
}
func (lm *loggingMiddleware) UpdateReportConfig(ctx context.Context, session authn.Session, config reports.ReportConfig) (res reports.ReportConfig, err error) {
defer func(begin time.Time) {
args := []any{
slog.String("duration", time.Since(begin).String()),
slog.String("domain_id", session.DomainID),
slog.Group("report_config",
slog.String("id", config.ID),
slog.String("name", config.Name),
),
}
if err != nil {
args = append(args, slog.String("error", err.Error()))
lm.logger.Warn("Update report config failed", args...)
return
}
lm.logger.Info("Update report config completed successfully", args...)
}(time.Now())
return lm.svc.UpdateReportConfig(ctx, session, config)
}
func (lm *loggingMiddleware) UpdateReportSchedule(ctx context.Context, session authn.Session, cfg reports.ReportConfig) (res reports.ReportConfig, err error) {
defer func(begin time.Time) {
args := []any{
slog.String("duration", time.Since(begin).String()),
slog.String("domain_id", session.DomainID),
slog.Group("report",
slog.String("id", cfg.ID),
slog.Any("schedule", cfg.Schedule),
),
}
if err != nil {
args = append(args, slog.String("error", err.Error()))
lm.logger.Warn("Update report schedule failed", args...)
return
}
lm.logger.Info("Update report schedule completed successfully", args...)
}(time.Now())
return lm.svc.UpdateReportSchedule(ctx, session, cfg)
}
func (lm *loggingMiddleware) ListReportsConfig(ctx context.Context, session authn.Session, pm reports.PageMeta) (pg reports.ReportConfigPage, err error) {
defer func(begin time.Time) {
args := []any{
slog.String("duration", time.Since(begin).String()),
slog.String("domain_id", session.DomainID),
slog.Group("page",
slog.Uint64("offset", pm.Offset),
slog.Uint64("limit", pm.Limit),
slog.Uint64("total", pg.Total),
),
}
if err != nil {
args = append(args, slog.String("error", err.Error()))
lm.logger.Warn("List reports config failed", args...)
return
}
lm.logger.Info("List reports config completed successfully", args...)
}(time.Now())
return lm.svc.ListReportsConfig(ctx, session, pm)
}
func (lm *loggingMiddleware) DisableReportConfig(ctx context.Context, session authn.Session, id string) (res reports.ReportConfig, err error) {
defer func(begin time.Time) {
args := []any{
slog.String("duration", time.Since(begin).String()),
slog.String("domain_id", session.DomainID),
slog.Group("report_config",
slog.String("id", res.ID),
slog.String("name", res.Name),
),
}
if err != nil {
args = append(args, slog.String("error", err.Error()))
lm.logger.Warn("Disable report config failed", args...)
return
}
lm.logger.Info("Disable report config completed successfully", args...)
}(time.Now())
return lm.svc.DisableReportConfig(ctx, session, id)
}
func (lm *loggingMiddleware) EnableReportConfig(ctx context.Context, session authn.Session, id string) (res reports.ReportConfig, err error) {
defer func(begin time.Time) {
args := []any{
slog.String("duration", time.Since(begin).String()),
slog.String("domain_id", session.DomainID),
slog.Group("report_config",
slog.String("id", res.ID),
slog.String("name", res.Name),
),
}
if err != nil {
args = append(args, slog.String("error", err.Error()))
lm.logger.Warn("Enable report config failed", args...)
return
}
lm.logger.Info("Enable report config completed successfully", args...)
}(time.Now())
return lm.svc.EnableReportConfig(ctx, session, id)
}
func (lm *loggingMiddleware) RemoveReportConfig(ctx context.Context, session authn.Session, id string) (err error) {
defer func(begin time.Time) {
args := []any{
slog.String("duration", time.Since(begin).String()),
slog.String("domain_id", session.DomainID),
slog.String("report_config_id", id),
}
if err != nil {
args = append(args, slog.String("error", err.Error()))
lm.logger.Warn("Remove report config failed", args...)
return
}
lm.logger.Info("Remove report config completed successfully", args...)
}(time.Now())
return lm.svc.RemoveReportConfig(ctx, session, id)
}
+475
View File
@@ -0,0 +1,475 @@
// Code generated by mockery; DO NOT EDIT.
// github.com/vektra/mockery
// template: testify
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package mocks
import (
"context"
"time"
"github.com/absmach/magistrala/reports"
mock "github.com/stretchr/testify/mock"
)
// NewRepository creates a new instance of Repository. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewRepository(t interface {
mock.TestingT
Cleanup(func())
}) *Repository {
mock := &Repository{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
// Repository is an autogenerated mock type for the Repository type
type Repository struct {
mock.Mock
}
type Repository_Expecter struct {
mock *mock.Mock
}
func (_m *Repository) EXPECT() *Repository_Expecter {
return &Repository_Expecter{mock: &_m.Mock}
}
// AddReportConfig provides a mock function for the type Repository
func (_mock *Repository) AddReportConfig(ctx context.Context, cfg reports.ReportConfig) (reports.ReportConfig, error) {
ret := _mock.Called(ctx, cfg)
if len(ret) == 0 {
panic("no return value specified for AddReportConfig")
}
var r0 reports.ReportConfig
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, reports.ReportConfig) (reports.ReportConfig, error)); ok {
return returnFunc(ctx, cfg)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, reports.ReportConfig) reports.ReportConfig); ok {
r0 = returnFunc(ctx, cfg)
} else {
r0 = ret.Get(0).(reports.ReportConfig)
}
if returnFunc, ok := ret.Get(1).(func(context.Context, reports.ReportConfig) error); ok {
r1 = returnFunc(ctx, cfg)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Repository_AddReportConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddReportConfig'
type Repository_AddReportConfig_Call struct {
*mock.Call
}
// AddReportConfig is a helper method to define mock.On call
// - ctx
// - cfg
func (_e *Repository_Expecter) AddReportConfig(ctx interface{}, cfg interface{}) *Repository_AddReportConfig_Call {
return &Repository_AddReportConfig_Call{Call: _e.mock.On("AddReportConfig", ctx, cfg)}
}
func (_c *Repository_AddReportConfig_Call) Run(run func(ctx context.Context, cfg reports.ReportConfig)) *Repository_AddReportConfig_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(reports.ReportConfig))
})
return _c
}
func (_c *Repository_AddReportConfig_Call) Return(reportConfig reports.ReportConfig, err error) *Repository_AddReportConfig_Call {
_c.Call.Return(reportConfig, err)
return _c
}
func (_c *Repository_AddReportConfig_Call) RunAndReturn(run func(ctx context.Context, cfg reports.ReportConfig) (reports.ReportConfig, error)) *Repository_AddReportConfig_Call {
_c.Call.Return(run)
return _c
}
// ListReportsConfig provides a mock function for the type Repository
func (_mock *Repository) ListReportsConfig(ctx context.Context, pm reports.PageMeta) (reports.ReportConfigPage, error) {
ret := _mock.Called(ctx, pm)
if len(ret) == 0 {
panic("no return value specified for ListReportsConfig")
}
var r0 reports.ReportConfigPage
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, reports.PageMeta) (reports.ReportConfigPage, error)); ok {
return returnFunc(ctx, pm)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, reports.PageMeta) reports.ReportConfigPage); ok {
r0 = returnFunc(ctx, pm)
} else {
r0 = ret.Get(0).(reports.ReportConfigPage)
}
if returnFunc, ok := ret.Get(1).(func(context.Context, reports.PageMeta) error); ok {
r1 = returnFunc(ctx, pm)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Repository_ListReportsConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListReportsConfig'
type Repository_ListReportsConfig_Call struct {
*mock.Call
}
// ListReportsConfig is a helper method to define mock.On call
// - ctx
// - pm
func (_e *Repository_Expecter) ListReportsConfig(ctx interface{}, pm interface{}) *Repository_ListReportsConfig_Call {
return &Repository_ListReportsConfig_Call{Call: _e.mock.On("ListReportsConfig", ctx, pm)}
}
func (_c *Repository_ListReportsConfig_Call) Run(run func(ctx context.Context, pm reports.PageMeta)) *Repository_ListReportsConfig_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(reports.PageMeta))
})
return _c
}
func (_c *Repository_ListReportsConfig_Call) Return(reportConfigPage reports.ReportConfigPage, err error) *Repository_ListReportsConfig_Call {
_c.Call.Return(reportConfigPage, err)
return _c
}
func (_c *Repository_ListReportsConfig_Call) RunAndReturn(run func(ctx context.Context, pm reports.PageMeta) (reports.ReportConfigPage, error)) *Repository_ListReportsConfig_Call {
_c.Call.Return(run)
return _c
}
// RemoveReportConfig provides a mock function for the type Repository
func (_mock *Repository) RemoveReportConfig(ctx context.Context, id string) error {
ret := _mock.Called(ctx, id)
if len(ret) == 0 {
panic("no return value specified for RemoveReportConfig")
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string) error); ok {
r0 = returnFunc(ctx, id)
} else {
r0 = ret.Error(0)
}
return r0
}
// Repository_RemoveReportConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoveReportConfig'
type Repository_RemoveReportConfig_Call struct {
*mock.Call
}
// RemoveReportConfig is a helper method to define mock.On call
// - ctx
// - id
func (_e *Repository_Expecter) RemoveReportConfig(ctx interface{}, id interface{}) *Repository_RemoveReportConfig_Call {
return &Repository_RemoveReportConfig_Call{Call: _e.mock.On("RemoveReportConfig", ctx, id)}
}
func (_c *Repository_RemoveReportConfig_Call) Run(run func(ctx context.Context, id string)) *Repository_RemoveReportConfig_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string))
})
return _c
}
func (_c *Repository_RemoveReportConfig_Call) Return(err error) *Repository_RemoveReportConfig_Call {
_c.Call.Return(err)
return _c
}
func (_c *Repository_RemoveReportConfig_Call) RunAndReturn(run func(ctx context.Context, id string) error) *Repository_RemoveReportConfig_Call {
_c.Call.Return(run)
return _c
}
// UpdateReportConfig provides a mock function for the type Repository
func (_mock *Repository) UpdateReportConfig(ctx context.Context, cfg reports.ReportConfig) (reports.ReportConfig, error) {
ret := _mock.Called(ctx, cfg)
if len(ret) == 0 {
panic("no return value specified for UpdateReportConfig")
}
var r0 reports.ReportConfig
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, reports.ReportConfig) (reports.ReportConfig, error)); ok {
return returnFunc(ctx, cfg)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, reports.ReportConfig) reports.ReportConfig); ok {
r0 = returnFunc(ctx, cfg)
} else {
r0 = ret.Get(0).(reports.ReportConfig)
}
if returnFunc, ok := ret.Get(1).(func(context.Context, reports.ReportConfig) error); ok {
r1 = returnFunc(ctx, cfg)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Repository_UpdateReportConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateReportConfig'
type Repository_UpdateReportConfig_Call struct {
*mock.Call
}
// UpdateReportConfig is a helper method to define mock.On call
// - ctx
// - cfg
func (_e *Repository_Expecter) UpdateReportConfig(ctx interface{}, cfg interface{}) *Repository_UpdateReportConfig_Call {
return &Repository_UpdateReportConfig_Call{Call: _e.mock.On("UpdateReportConfig", ctx, cfg)}
}
func (_c *Repository_UpdateReportConfig_Call) Run(run func(ctx context.Context, cfg reports.ReportConfig)) *Repository_UpdateReportConfig_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(reports.ReportConfig))
})
return _c
}
func (_c *Repository_UpdateReportConfig_Call) Return(reportConfig reports.ReportConfig, err error) *Repository_UpdateReportConfig_Call {
_c.Call.Return(reportConfig, err)
return _c
}
func (_c *Repository_UpdateReportConfig_Call) RunAndReturn(run func(ctx context.Context, cfg reports.ReportConfig) (reports.ReportConfig, error)) *Repository_UpdateReportConfig_Call {
_c.Call.Return(run)
return _c
}
// UpdateReportConfigStatus provides a mock function for the type Repository
func (_mock *Repository) UpdateReportConfigStatus(ctx context.Context, cfg reports.ReportConfig) (reports.ReportConfig, error) {
ret := _mock.Called(ctx, cfg)
if len(ret) == 0 {
panic("no return value specified for UpdateReportConfigStatus")
}
var r0 reports.ReportConfig
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, reports.ReportConfig) (reports.ReportConfig, error)); ok {
return returnFunc(ctx, cfg)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, reports.ReportConfig) reports.ReportConfig); ok {
r0 = returnFunc(ctx, cfg)
} else {
r0 = ret.Get(0).(reports.ReportConfig)
}
if returnFunc, ok := ret.Get(1).(func(context.Context, reports.ReportConfig) error); ok {
r1 = returnFunc(ctx, cfg)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Repository_UpdateReportConfigStatus_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateReportConfigStatus'
type Repository_UpdateReportConfigStatus_Call struct {
*mock.Call
}
// UpdateReportConfigStatus is a helper method to define mock.On call
// - ctx
// - cfg
func (_e *Repository_Expecter) UpdateReportConfigStatus(ctx interface{}, cfg interface{}) *Repository_UpdateReportConfigStatus_Call {
return &Repository_UpdateReportConfigStatus_Call{Call: _e.mock.On("UpdateReportConfigStatus", ctx, cfg)}
}
func (_c *Repository_UpdateReportConfigStatus_Call) Run(run func(ctx context.Context, cfg reports.ReportConfig)) *Repository_UpdateReportConfigStatus_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(reports.ReportConfig))
})
return _c
}
func (_c *Repository_UpdateReportConfigStatus_Call) Return(reportConfig reports.ReportConfig, err error) *Repository_UpdateReportConfigStatus_Call {
_c.Call.Return(reportConfig, err)
return _c
}
func (_c *Repository_UpdateReportConfigStatus_Call) RunAndReturn(run func(ctx context.Context, cfg reports.ReportConfig) (reports.ReportConfig, error)) *Repository_UpdateReportConfigStatus_Call {
_c.Call.Return(run)
return _c
}
// UpdateReportDue provides a mock function for the type Repository
func (_mock *Repository) UpdateReportDue(ctx context.Context, id string, due time.Time) (reports.ReportConfig, error) {
ret := _mock.Called(ctx, id, due)
if len(ret) == 0 {
panic("no return value specified for UpdateReportDue")
}
var r0 reports.ReportConfig
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string, time.Time) (reports.ReportConfig, error)); ok {
return returnFunc(ctx, id, due)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, string, time.Time) reports.ReportConfig); ok {
r0 = returnFunc(ctx, id, due)
} else {
r0 = ret.Get(0).(reports.ReportConfig)
}
if returnFunc, ok := ret.Get(1).(func(context.Context, string, time.Time) error); ok {
r1 = returnFunc(ctx, id, due)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Repository_UpdateReportDue_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateReportDue'
type Repository_UpdateReportDue_Call struct {
*mock.Call
}
// UpdateReportDue is a helper method to define mock.On call
// - ctx
// - id
// - due
func (_e *Repository_Expecter) UpdateReportDue(ctx interface{}, id interface{}, due interface{}) *Repository_UpdateReportDue_Call {
return &Repository_UpdateReportDue_Call{Call: _e.mock.On("UpdateReportDue", ctx, id, due)}
}
func (_c *Repository_UpdateReportDue_Call) Run(run func(ctx context.Context, id string, due time.Time)) *Repository_UpdateReportDue_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string), args[2].(time.Time))
})
return _c
}
func (_c *Repository_UpdateReportDue_Call) Return(reportConfig reports.ReportConfig, err error) *Repository_UpdateReportDue_Call {
_c.Call.Return(reportConfig, err)
return _c
}
func (_c *Repository_UpdateReportDue_Call) RunAndReturn(run func(ctx context.Context, id string, due time.Time) (reports.ReportConfig, error)) *Repository_UpdateReportDue_Call {
_c.Call.Return(run)
return _c
}
// UpdateReportSchedule provides a mock function for the type Repository
func (_mock *Repository) UpdateReportSchedule(ctx context.Context, cfg reports.ReportConfig) (reports.ReportConfig, error) {
ret := _mock.Called(ctx, cfg)
if len(ret) == 0 {
panic("no return value specified for UpdateReportSchedule")
}
var r0 reports.ReportConfig
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, reports.ReportConfig) (reports.ReportConfig, error)); ok {
return returnFunc(ctx, cfg)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, reports.ReportConfig) reports.ReportConfig); ok {
r0 = returnFunc(ctx, cfg)
} else {
r0 = ret.Get(0).(reports.ReportConfig)
}
if returnFunc, ok := ret.Get(1).(func(context.Context, reports.ReportConfig) error); ok {
r1 = returnFunc(ctx, cfg)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Repository_UpdateReportSchedule_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateReportSchedule'
type Repository_UpdateReportSchedule_Call struct {
*mock.Call
}
// UpdateReportSchedule is a helper method to define mock.On call
// - ctx
// - cfg
func (_e *Repository_Expecter) UpdateReportSchedule(ctx interface{}, cfg interface{}) *Repository_UpdateReportSchedule_Call {
return &Repository_UpdateReportSchedule_Call{Call: _e.mock.On("UpdateReportSchedule", ctx, cfg)}
}
func (_c *Repository_UpdateReportSchedule_Call) Run(run func(ctx context.Context, cfg reports.ReportConfig)) *Repository_UpdateReportSchedule_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(reports.ReportConfig))
})
return _c
}
func (_c *Repository_UpdateReportSchedule_Call) Return(reportConfig reports.ReportConfig, err error) *Repository_UpdateReportSchedule_Call {
_c.Call.Return(reportConfig, err)
return _c
}
func (_c *Repository_UpdateReportSchedule_Call) RunAndReturn(run func(ctx context.Context, cfg reports.ReportConfig) (reports.ReportConfig, error)) *Repository_UpdateReportSchedule_Call {
_c.Call.Return(run)
return _c
}
// ViewReportConfig provides a mock function for the type Repository
func (_mock *Repository) ViewReportConfig(ctx context.Context, id string) (reports.ReportConfig, error) {
ret := _mock.Called(ctx, id)
if len(ret) == 0 {
panic("no return value specified for ViewReportConfig")
}
var r0 reports.ReportConfig
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, string) (reports.ReportConfig, error)); ok {
return returnFunc(ctx, id)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, string) reports.ReportConfig); ok {
r0 = returnFunc(ctx, id)
} else {
r0 = ret.Get(0).(reports.ReportConfig)
}
if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok {
r1 = returnFunc(ctx, id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Repository_ViewReportConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ViewReportConfig'
type Repository_ViewReportConfig_Call struct {
*mock.Call
}
// ViewReportConfig is a helper method to define mock.On call
// - ctx
// - id
func (_e *Repository_Expecter) ViewReportConfig(ctx interface{}, id interface{}) *Repository_ViewReportConfig_Call {
return &Repository_ViewReportConfig_Call{Call: _e.mock.On("ViewReportConfig", ctx, id)}
}
func (_c *Repository_ViewReportConfig_Call) Run(run func(ctx context.Context, id string)) *Repository_ViewReportConfig_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(string))
})
return _c
}
func (_c *Repository_ViewReportConfig_Call) Return(reportConfig reports.ReportConfig, err error) *Repository_ViewReportConfig_Call {
_c.Call.Return(reportConfig, err)
return _c
}
func (_c *Repository_ViewReportConfig_Call) RunAndReturn(run func(ctx context.Context, id string) (reports.ReportConfig, error)) *Repository_ViewReportConfig_Call {
_c.Call.Return(run)
return _c
}
+584
View File
@@ -0,0 +1,584 @@
// Code generated by mockery; DO NOT EDIT.
// github.com/vektra/mockery
// template: testify
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package mocks
import (
"context"
"github.com/absmach/magistrala/reports"
"github.com/absmach/supermq/pkg/authn"
mock "github.com/stretchr/testify/mock"
)
// NewService creates a new instance of Service. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewService(t interface {
mock.TestingT
Cleanup(func())
}) *Service {
mock := &Service{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
// Service is an autogenerated mock type for the Service type
type Service struct {
mock.Mock
}
type Service_Expecter struct {
mock *mock.Mock
}
func (_m *Service) EXPECT() *Service_Expecter {
return &Service_Expecter{mock: &_m.Mock}
}
// AddReportConfig provides a mock function for the type Service
func (_mock *Service) AddReportConfig(ctx context.Context, session authn.Session, cfg reports.ReportConfig) (reports.ReportConfig, error) {
ret := _mock.Called(ctx, session, cfg)
if len(ret) == 0 {
panic("no return value specified for AddReportConfig")
}
var r0 reports.ReportConfig
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, reports.ReportConfig) (reports.ReportConfig, error)); ok {
return returnFunc(ctx, session, cfg)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, reports.ReportConfig) reports.ReportConfig); ok {
r0 = returnFunc(ctx, session, cfg)
} else {
r0 = ret.Get(0).(reports.ReportConfig)
}
if returnFunc, ok := ret.Get(1).(func(context.Context, authn.Session, reports.ReportConfig) error); ok {
r1 = returnFunc(ctx, session, cfg)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Service_AddReportConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddReportConfig'
type Service_AddReportConfig_Call struct {
*mock.Call
}
// AddReportConfig is a helper method to define mock.On call
// - ctx
// - session
// - cfg
func (_e *Service_Expecter) AddReportConfig(ctx interface{}, session interface{}, cfg interface{}) *Service_AddReportConfig_Call {
return &Service_AddReportConfig_Call{Call: _e.mock.On("AddReportConfig", ctx, session, cfg)}
}
func (_c *Service_AddReportConfig_Call) Run(run func(ctx context.Context, session authn.Session, cfg reports.ReportConfig)) *Service_AddReportConfig_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(authn.Session), args[2].(reports.ReportConfig))
})
return _c
}
func (_c *Service_AddReportConfig_Call) Return(reportConfig reports.ReportConfig, err error) *Service_AddReportConfig_Call {
_c.Call.Return(reportConfig, err)
return _c
}
func (_c *Service_AddReportConfig_Call) RunAndReturn(run func(ctx context.Context, session authn.Session, cfg reports.ReportConfig) (reports.ReportConfig, error)) *Service_AddReportConfig_Call {
_c.Call.Return(run)
return _c
}
// DisableReportConfig provides a mock function for the type Service
func (_mock *Service) DisableReportConfig(ctx context.Context, session authn.Session, id string) (reports.ReportConfig, error) {
ret := _mock.Called(ctx, session, id)
if len(ret) == 0 {
panic("no return value specified for DisableReportConfig")
}
var r0 reports.ReportConfig
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, string) (reports.ReportConfig, error)); ok {
return returnFunc(ctx, session, id)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, string) reports.ReportConfig); ok {
r0 = returnFunc(ctx, session, id)
} else {
r0 = ret.Get(0).(reports.ReportConfig)
}
if returnFunc, ok := ret.Get(1).(func(context.Context, authn.Session, string) error); ok {
r1 = returnFunc(ctx, session, id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Service_DisableReportConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DisableReportConfig'
type Service_DisableReportConfig_Call struct {
*mock.Call
}
// DisableReportConfig is a helper method to define mock.On call
// - ctx
// - session
// - id
func (_e *Service_Expecter) DisableReportConfig(ctx interface{}, session interface{}, id interface{}) *Service_DisableReportConfig_Call {
return &Service_DisableReportConfig_Call{Call: _e.mock.On("DisableReportConfig", ctx, session, id)}
}
func (_c *Service_DisableReportConfig_Call) Run(run func(ctx context.Context, session authn.Session, id string)) *Service_DisableReportConfig_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(authn.Session), args[2].(string))
})
return _c
}
func (_c *Service_DisableReportConfig_Call) Return(reportConfig reports.ReportConfig, err error) *Service_DisableReportConfig_Call {
_c.Call.Return(reportConfig, err)
return _c
}
func (_c *Service_DisableReportConfig_Call) RunAndReturn(run func(ctx context.Context, session authn.Session, id string) (reports.ReportConfig, error)) *Service_DisableReportConfig_Call {
_c.Call.Return(run)
return _c
}
// EnableReportConfig provides a mock function for the type Service
func (_mock *Service) EnableReportConfig(ctx context.Context, session authn.Session, id string) (reports.ReportConfig, error) {
ret := _mock.Called(ctx, session, id)
if len(ret) == 0 {
panic("no return value specified for EnableReportConfig")
}
var r0 reports.ReportConfig
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, string) (reports.ReportConfig, error)); ok {
return returnFunc(ctx, session, id)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, string) reports.ReportConfig); ok {
r0 = returnFunc(ctx, session, id)
} else {
r0 = ret.Get(0).(reports.ReportConfig)
}
if returnFunc, ok := ret.Get(1).(func(context.Context, authn.Session, string) error); ok {
r1 = returnFunc(ctx, session, id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Service_EnableReportConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'EnableReportConfig'
type Service_EnableReportConfig_Call struct {
*mock.Call
}
// EnableReportConfig is a helper method to define mock.On call
// - ctx
// - session
// - id
func (_e *Service_Expecter) EnableReportConfig(ctx interface{}, session interface{}, id interface{}) *Service_EnableReportConfig_Call {
return &Service_EnableReportConfig_Call{Call: _e.mock.On("EnableReportConfig", ctx, session, id)}
}
func (_c *Service_EnableReportConfig_Call) Run(run func(ctx context.Context, session authn.Session, id string)) *Service_EnableReportConfig_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(authn.Session), args[2].(string))
})
return _c
}
func (_c *Service_EnableReportConfig_Call) Return(reportConfig reports.ReportConfig, err error) *Service_EnableReportConfig_Call {
_c.Call.Return(reportConfig, err)
return _c
}
func (_c *Service_EnableReportConfig_Call) RunAndReturn(run func(ctx context.Context, session authn.Session, id string) (reports.ReportConfig, error)) *Service_EnableReportConfig_Call {
_c.Call.Return(run)
return _c
}
// GenerateReport provides a mock function for the type Service
func (_mock *Service) GenerateReport(ctx context.Context, session authn.Session, config reports.ReportConfig, action reports.ReportAction) (reports.ReportPage, error) {
ret := _mock.Called(ctx, session, config, action)
if len(ret) == 0 {
panic("no return value specified for GenerateReport")
}
var r0 reports.ReportPage
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, reports.ReportConfig, reports.ReportAction) (reports.ReportPage, error)); ok {
return returnFunc(ctx, session, config, action)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, reports.ReportConfig, reports.ReportAction) reports.ReportPage); ok {
r0 = returnFunc(ctx, session, config, action)
} else {
r0 = ret.Get(0).(reports.ReportPage)
}
if returnFunc, ok := ret.Get(1).(func(context.Context, authn.Session, reports.ReportConfig, reports.ReportAction) error); ok {
r1 = returnFunc(ctx, session, config, action)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Service_GenerateReport_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GenerateReport'
type Service_GenerateReport_Call struct {
*mock.Call
}
// GenerateReport is a helper method to define mock.On call
// - ctx
// - session
// - config
// - action
func (_e *Service_Expecter) GenerateReport(ctx interface{}, session interface{}, config interface{}, action interface{}) *Service_GenerateReport_Call {
return &Service_GenerateReport_Call{Call: _e.mock.On("GenerateReport", ctx, session, config, action)}
}
func (_c *Service_GenerateReport_Call) Run(run func(ctx context.Context, session authn.Session, config reports.ReportConfig, action reports.ReportAction)) *Service_GenerateReport_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(authn.Session), args[2].(reports.ReportConfig), args[3].(reports.ReportAction))
})
return _c
}
func (_c *Service_GenerateReport_Call) Return(reportPage reports.ReportPage, err error) *Service_GenerateReport_Call {
_c.Call.Return(reportPage, err)
return _c
}
func (_c *Service_GenerateReport_Call) RunAndReturn(run func(ctx context.Context, session authn.Session, config reports.ReportConfig, action reports.ReportAction) (reports.ReportPage, error)) *Service_GenerateReport_Call {
_c.Call.Return(run)
return _c
}
// ListReportsConfig provides a mock function for the type Service
func (_mock *Service) ListReportsConfig(ctx context.Context, session authn.Session, pm reports.PageMeta) (reports.ReportConfigPage, error) {
ret := _mock.Called(ctx, session, pm)
if len(ret) == 0 {
panic("no return value specified for ListReportsConfig")
}
var r0 reports.ReportConfigPage
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, reports.PageMeta) (reports.ReportConfigPage, error)); ok {
return returnFunc(ctx, session, pm)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, reports.PageMeta) reports.ReportConfigPage); ok {
r0 = returnFunc(ctx, session, pm)
} else {
r0 = ret.Get(0).(reports.ReportConfigPage)
}
if returnFunc, ok := ret.Get(1).(func(context.Context, authn.Session, reports.PageMeta) error); ok {
r1 = returnFunc(ctx, session, pm)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Service_ListReportsConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListReportsConfig'
type Service_ListReportsConfig_Call struct {
*mock.Call
}
// ListReportsConfig is a helper method to define mock.On call
// - ctx
// - session
// - pm
func (_e *Service_Expecter) ListReportsConfig(ctx interface{}, session interface{}, pm interface{}) *Service_ListReportsConfig_Call {
return &Service_ListReportsConfig_Call{Call: _e.mock.On("ListReportsConfig", ctx, session, pm)}
}
func (_c *Service_ListReportsConfig_Call) Run(run func(ctx context.Context, session authn.Session, pm reports.PageMeta)) *Service_ListReportsConfig_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(authn.Session), args[2].(reports.PageMeta))
})
return _c
}
func (_c *Service_ListReportsConfig_Call) Return(reportConfigPage reports.ReportConfigPage, err error) *Service_ListReportsConfig_Call {
_c.Call.Return(reportConfigPage, err)
return _c
}
func (_c *Service_ListReportsConfig_Call) RunAndReturn(run func(ctx context.Context, session authn.Session, pm reports.PageMeta) (reports.ReportConfigPage, error)) *Service_ListReportsConfig_Call {
_c.Call.Return(run)
return _c
}
// RemoveReportConfig provides a mock function for the type Service
func (_mock *Service) RemoveReportConfig(ctx context.Context, session authn.Session, id string) error {
ret := _mock.Called(ctx, session, id)
if len(ret) == 0 {
panic("no return value specified for RemoveReportConfig")
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, string) error); ok {
r0 = returnFunc(ctx, session, id)
} else {
r0 = ret.Error(0)
}
return r0
}
// Service_RemoveReportConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoveReportConfig'
type Service_RemoveReportConfig_Call struct {
*mock.Call
}
// RemoveReportConfig is a helper method to define mock.On call
// - ctx
// - session
// - id
func (_e *Service_Expecter) RemoveReportConfig(ctx interface{}, session interface{}, id interface{}) *Service_RemoveReportConfig_Call {
return &Service_RemoveReportConfig_Call{Call: _e.mock.On("RemoveReportConfig", ctx, session, id)}
}
func (_c *Service_RemoveReportConfig_Call) Run(run func(ctx context.Context, session authn.Session, id string)) *Service_RemoveReportConfig_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(authn.Session), args[2].(string))
})
return _c
}
func (_c *Service_RemoveReportConfig_Call) Return(err error) *Service_RemoveReportConfig_Call {
_c.Call.Return(err)
return _c
}
func (_c *Service_RemoveReportConfig_Call) RunAndReturn(run func(ctx context.Context, session authn.Session, id string) error) *Service_RemoveReportConfig_Call {
_c.Call.Return(run)
return _c
}
// StartScheduler provides a mock function for the type Service
func (_mock *Service) StartScheduler(ctx context.Context) error {
ret := _mock.Called(ctx)
if len(ret) == 0 {
panic("no return value specified for StartScheduler")
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(context.Context) error); ok {
r0 = returnFunc(ctx)
} else {
r0 = ret.Error(0)
}
return r0
}
// Service_StartScheduler_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'StartScheduler'
type Service_StartScheduler_Call struct {
*mock.Call
}
// StartScheduler is a helper method to define mock.On call
// - ctx
func (_e *Service_Expecter) StartScheduler(ctx interface{}) *Service_StartScheduler_Call {
return &Service_StartScheduler_Call{Call: _e.mock.On("StartScheduler", ctx)}
}
func (_c *Service_StartScheduler_Call) Run(run func(ctx context.Context)) *Service_StartScheduler_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context))
})
return _c
}
func (_c *Service_StartScheduler_Call) Return(err error) *Service_StartScheduler_Call {
_c.Call.Return(err)
return _c
}
func (_c *Service_StartScheduler_Call) RunAndReturn(run func(ctx context.Context) error) *Service_StartScheduler_Call {
_c.Call.Return(run)
return _c
}
// UpdateReportConfig provides a mock function for the type Service
func (_mock *Service) UpdateReportConfig(ctx context.Context, session authn.Session, cfg reports.ReportConfig) (reports.ReportConfig, error) {
ret := _mock.Called(ctx, session, cfg)
if len(ret) == 0 {
panic("no return value specified for UpdateReportConfig")
}
var r0 reports.ReportConfig
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, reports.ReportConfig) (reports.ReportConfig, error)); ok {
return returnFunc(ctx, session, cfg)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, reports.ReportConfig) reports.ReportConfig); ok {
r0 = returnFunc(ctx, session, cfg)
} else {
r0 = ret.Get(0).(reports.ReportConfig)
}
if returnFunc, ok := ret.Get(1).(func(context.Context, authn.Session, reports.ReportConfig) error); ok {
r1 = returnFunc(ctx, session, cfg)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Service_UpdateReportConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateReportConfig'
type Service_UpdateReportConfig_Call struct {
*mock.Call
}
// UpdateReportConfig is a helper method to define mock.On call
// - ctx
// - session
// - cfg
func (_e *Service_Expecter) UpdateReportConfig(ctx interface{}, session interface{}, cfg interface{}) *Service_UpdateReportConfig_Call {
return &Service_UpdateReportConfig_Call{Call: _e.mock.On("UpdateReportConfig", ctx, session, cfg)}
}
func (_c *Service_UpdateReportConfig_Call) Run(run func(ctx context.Context, session authn.Session, cfg reports.ReportConfig)) *Service_UpdateReportConfig_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(authn.Session), args[2].(reports.ReportConfig))
})
return _c
}
func (_c *Service_UpdateReportConfig_Call) Return(reportConfig reports.ReportConfig, err error) *Service_UpdateReportConfig_Call {
_c.Call.Return(reportConfig, err)
return _c
}
func (_c *Service_UpdateReportConfig_Call) RunAndReturn(run func(ctx context.Context, session authn.Session, cfg reports.ReportConfig) (reports.ReportConfig, error)) *Service_UpdateReportConfig_Call {
_c.Call.Return(run)
return _c
}
// UpdateReportSchedule provides a mock function for the type Service
func (_mock *Service) UpdateReportSchedule(ctx context.Context, session authn.Session, cfg reports.ReportConfig) (reports.ReportConfig, error) {
ret := _mock.Called(ctx, session, cfg)
if len(ret) == 0 {
panic("no return value specified for UpdateReportSchedule")
}
var r0 reports.ReportConfig
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, reports.ReportConfig) (reports.ReportConfig, error)); ok {
return returnFunc(ctx, session, cfg)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, reports.ReportConfig) reports.ReportConfig); ok {
r0 = returnFunc(ctx, session, cfg)
} else {
r0 = ret.Get(0).(reports.ReportConfig)
}
if returnFunc, ok := ret.Get(1).(func(context.Context, authn.Session, reports.ReportConfig) error); ok {
r1 = returnFunc(ctx, session, cfg)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Service_UpdateReportSchedule_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateReportSchedule'
type Service_UpdateReportSchedule_Call struct {
*mock.Call
}
// UpdateReportSchedule is a helper method to define mock.On call
// - ctx
// - session
// - cfg
func (_e *Service_Expecter) UpdateReportSchedule(ctx interface{}, session interface{}, cfg interface{}) *Service_UpdateReportSchedule_Call {
return &Service_UpdateReportSchedule_Call{Call: _e.mock.On("UpdateReportSchedule", ctx, session, cfg)}
}
func (_c *Service_UpdateReportSchedule_Call) Run(run func(ctx context.Context, session authn.Session, cfg reports.ReportConfig)) *Service_UpdateReportSchedule_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(authn.Session), args[2].(reports.ReportConfig))
})
return _c
}
func (_c *Service_UpdateReportSchedule_Call) Return(reportConfig reports.ReportConfig, err error) *Service_UpdateReportSchedule_Call {
_c.Call.Return(reportConfig, err)
return _c
}
func (_c *Service_UpdateReportSchedule_Call) RunAndReturn(run func(ctx context.Context, session authn.Session, cfg reports.ReportConfig) (reports.ReportConfig, error)) *Service_UpdateReportSchedule_Call {
_c.Call.Return(run)
return _c
}
// ViewReportConfig provides a mock function for the type Service
func (_mock *Service) ViewReportConfig(ctx context.Context, session authn.Session, id string) (reports.ReportConfig, error) {
ret := _mock.Called(ctx, session, id)
if len(ret) == 0 {
panic("no return value specified for ViewReportConfig")
}
var r0 reports.ReportConfig
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, string) (reports.ReportConfig, error)); ok {
return returnFunc(ctx, session, id)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, string) reports.ReportConfig); ok {
r0 = returnFunc(ctx, session, id)
} else {
r0 = ret.Get(0).(reports.ReportConfig)
}
if returnFunc, ok := ret.Get(1).(func(context.Context, authn.Session, string) error); ok {
r1 = returnFunc(ctx, session, id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// Service_ViewReportConfig_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ViewReportConfig'
type Service_ViewReportConfig_Call struct {
*mock.Call
}
// ViewReportConfig is a helper method to define mock.On call
// - ctx
// - session
// - id
func (_e *Service_Expecter) ViewReportConfig(ctx interface{}, session interface{}, id interface{}) *Service_ViewReportConfig_Call {
return &Service_ViewReportConfig_Call{Call: _e.mock.On("ViewReportConfig", ctx, session, id)}
}
func (_c *Service_ViewReportConfig_Call) Run(run func(ctx context.Context, session authn.Session, id string)) *Service_ViewReportConfig_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(context.Context), args[1].(authn.Session), args[2].(string))
})
return _c
}
func (_c *Service_ViewReportConfig_Call) Return(reportConfig reports.ReportConfig, err error) *Service_ViewReportConfig_Call {
_c.Call.Return(reportConfig, err)
return _c
}
func (_c *Service_ViewReportConfig_Call) RunAndReturn(run func(ctx context.Context, session authn.Session, id string) (reports.ReportConfig, error)) *Service_ViewReportConfig_Call {
_c.Call.Return(run)
return _c
}
+42
View File
@@ -0,0 +1,42 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package postgres
import (
_ "github.com/jackc/pgx/v5/stdlib" // required for SQL access
migrate "github.com/rubenv/sql-migrate"
)
func Migration() *migrate.MemoryMigrationSource {
return &migrate.MemoryMigrationSource{
Migrations: []*migrate.Migration{
{
Id: "reports_01",
Up: []string{
`CREATE TABLE IF NOT EXISTS report_config (
id VARCHAR(36) PRIMARY KEY,
name VARCHAR(1024),
description TEXT,
domain_id VARCHAR(36) NOT NULL,
status SMALLINT NOT NULL DEFAULT 0 CHECK (status >= 0),
created_at TIMESTAMP,
created_by VARCHAR(254),
updated_at TIMESTAMP,
updated_by VARCHAR(254),
due TIMESTAMPTZ,
recurring SMALLINT,
recurring_period SMALLINT,
start_datetime TIMESTAMP,
config JSONB,
email JSONB,
metrics JSONB
);`,
},
Down: []string{
`DROP TABLE IF EXISTS report_config;`,
},
},
},
}
}
+137
View File
@@ -0,0 +1,137 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package postgres
import (
"database/sql"
"encoding/json"
"time"
"github.com/absmach/magistrala/pkg/errors"
"github.com/absmach/magistrala/pkg/schedule"
"github.com/absmach/magistrala/reports"
)
// dbReport represents the database structure for a Report.
type dbReport struct {
ID string `db:"id"`
Name string `db:"name"`
Description string `db:"description"`
DomainID string `db:"domain_id"`
StartDateTime sql.NullTime `db:"start_datetime"`
Due sql.NullTime `db:"due"`
Recurring schedule.Recurring `db:"recurring"`
RecurringPeriod uint `db:"recurring_period"`
Status reports.Status `db:"status"`
CreatedAt time.Time `db:"created_at"`
CreatedBy string `db:"created_by"`
UpdatedAt time.Time `db:"updated_at"`
UpdatedBy string `db:"updated_by"`
Config []byte `db:"config,omitempty"`
Metrics []byte `db:"metrics"`
Email []byte `db:"email"`
}
func reportToDb(r reports.ReportConfig) (dbReport, error) {
config := []byte("{}")
if r.Config != nil {
b, err := json.Marshal(r.Config)
if err != nil {
return dbReport{}, errors.Wrap(errors.ErrMalformedEntity, err)
}
config = b
}
metrics := []byte("{}")
if r.Metrics != nil {
m, err := json.Marshal(r.Metrics)
if err != nil {
return dbReport{}, errors.Wrap(errors.ErrMalformedEntity, err)
}
metrics = m
}
email := []byte("{}")
if r.Email != nil {
e, err := json.Marshal(r.Email)
if err != nil {
return dbReport{}, errors.Wrap(errors.ErrMalformedEntity, err)
}
email = e
}
start := sql.NullTime{Time: r.Schedule.StartDateTime}
if !r.Schedule.StartDateTime.IsZero() {
start.Valid = true
}
t := sql.NullTime{Time: r.Schedule.Time}
if !r.Schedule.Time.IsZero() {
t.Valid = true
}
return dbReport{
ID: r.ID,
Name: r.Name,
Description: r.Description,
DomainID: r.DomainID,
StartDateTime: start,
Due: t,
Recurring: r.Schedule.Recurring,
RecurringPeriod: r.Schedule.RecurringPeriod,
Status: r.Status,
CreatedAt: r.CreatedAt,
CreatedBy: r.CreatedBy,
UpdatedAt: r.UpdatedAt,
UpdatedBy: r.UpdatedBy,
Config: config,
Metrics: metrics,
Email: email,
}, nil
}
func dbToReport(dto dbReport) (reports.ReportConfig, error) {
var config reports.MetricConfig
if dto.Config != nil {
if err := json.Unmarshal(dto.Config, &config); err != nil {
return reports.ReportConfig{}, errors.Wrap(errors.ErrMalformedEntity, err)
}
}
var email reports.EmailSetting
if dto.Email != nil {
if err := json.Unmarshal(dto.Email, &email); err != nil {
return reports.ReportConfig{}, errors.Wrap(errors.ErrMalformedEntity, err)
}
}
var metrics []reports.ReqMetric
if dto.Metrics != nil {
if err := json.Unmarshal(dto.Metrics, &metrics); err != nil {
return reports.ReportConfig{}, errors.Wrap(errors.ErrMalformedEntity, err)
}
}
rpt := reports.ReportConfig{
ID: dto.ID,
Name: dto.Name,
Description: dto.Description,
DomainID: dto.DomainID,
Config: &config,
Metrics: metrics,
Schedule: schedule.Schedule{
StartDateTime: dto.StartDateTime.Time,
Time: dto.Due.Time,
Recurring: dto.Recurring,
RecurringPeriod: dto.RecurringPeriod,
},
Email: &email,
Status: dto.Status,
CreatedAt: dto.CreatedAt,
CreatedBy: dto.CreatedBy,
UpdatedAt: dto.UpdatedAt,
UpdatedBy: dto.UpdatedBy,
}
return rpt, nil
}
+340
View File
@@ -0,0 +1,340 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package postgres
import (
"context"
"database/sql"
"fmt"
"strings"
"time"
"github.com/absmach/magistrala/pkg/errors"
"github.com/absmach/magistrala/reports"
repoerr "github.com/absmach/supermq/pkg/errors/repository"
"github.com/absmach/supermq/pkg/postgres"
)
type PostgresRepository struct {
DB postgres.Database
}
func NewRepository(db postgres.Database) reports.Repository {
return &PostgresRepository{DB: db}
}
func (repo *PostgresRepository) AddReportConfig(ctx context.Context, cfg reports.ReportConfig) (reports.ReportConfig, error) {
q := `
INSERT INTO report_config (id, name, description, domain_id, config, metrics,
email, start_datetime, due, recurring, recurring_period, created_at, created_by, updated_at, updated_by, status)
VALUES (:id, :name, :description, :domain_id, :config, :metrics,
:email, :start_datetime, :due, :recurring, :recurring_period, :created_at, :created_by, :updated_at, :updated_by, :status)
RETURNING id, name, description, domain_id, config, metrics,
email, start_datetime, due, recurring, recurring_period, created_at, created_by, updated_at, updated_by, status;
`
dbr, err := reportToDb(cfg)
if err != nil {
return reports.ReportConfig{}, err
}
row, err := repo.DB.NamedQueryContext(ctx, q, dbr)
if err != nil {
return reports.ReportConfig{}, err
}
defer row.Close()
var dbReport dbReport
if row.Next() {
if err := row.StructScan(&dbReport); err != nil {
return reports.ReportConfig{}, err
}
}
report, err := dbToReport(dbReport)
if err != nil {
return reports.ReportConfig{}, err
}
return report, nil
}
func (repo *PostgresRepository) ViewReportConfig(ctx context.Context, id string) (reports.ReportConfig, error) {
q := `
SELECT id, name, description, domain_id, config, metrics,
email, start_datetime, due, recurring, recurring_period, created_at, created_by, updated_at, updated_by, status
FROM report_config
WHERE id = $1;
`
row := repo.DB.QueryRowxContext(ctx, q, id)
if err := row.Err(); err != nil {
return reports.ReportConfig{}, err
}
var dbr dbReport
if err := row.StructScan(&dbr); err != nil {
return reports.ReportConfig{}, err
}
rpt, err := dbToReport(dbr)
if err != nil {
return reports.ReportConfig{}, err
}
return rpt, nil
}
func (repo *PostgresRepository) UpdateReportConfigStatus(ctx context.Context, cfg reports.ReportConfig) (reports.ReportConfig, error) {
q := `UPDATE report_config SET status = :status, updated_at = :updated_at, updated_by = :updated_by
WHERE id = :id
RETURNING id, name, description, domain_id, metrics, email, config,
start_datetime, due, recurring, recurring_period, created_at, created_by, updated_at, updated_by, status;`
dbRpt, err := reportToDb(cfg)
if err != nil {
return reports.ReportConfig{}, errors.Wrap(repoerr.ErrUpdateEntity, err)
}
row, err := repo.DB.NamedQueryContext(ctx, q, dbRpt)
if err != nil {
return reports.ReportConfig{}, postgres.HandleError(repoerr.ErrUpdateEntity, err)
}
defer row.Close()
dbr := dbReport{}
if row.Next() {
if err := row.StructScan(&dbr); err != nil {
return reports.ReportConfig{}, err
}
res, err := dbToReport(dbr)
if err != nil {
return reports.ReportConfig{}, err
}
return res, err
}
return reports.ReportConfig{}, repoerr.ErrNotFound
}
func (repo *PostgresRepository) UpdateReportConfig(ctx context.Context, cfg reports.ReportConfig) (reports.ReportConfig, error) {
var query []string
if cfg.Name != "" {
query = append(query, "name = :name")
}
if cfg.Description != "" {
query = append(query, "description = :description")
}
if len(cfg.Metrics) > 0 {
query = append(query, "metrics = :metrics")
}
if cfg.Email != nil {
query = append(query, "email = :email")
}
if cfg.Config != nil {
query = append(query, "config = :config")
}
var q string
if len(query) > 0 {
q = fmt.Sprintf("%s", strings.Join(query, ", "))
}
q = fmt.Sprintf(`
UPDATE report_config
SET %s,
updated_at = :updated_at, updated_by = :updated_by
WHERE id = :id
RETURNING id, name, description, domain_id, config, metrics,
email, start_datetime, due, recurring, recurring_period, created_at, created_by, updated_at, updated_by, status;
`, q)
dbr, err := reportToDb(cfg)
if err != nil {
return reports.ReportConfig{}, err
}
row, err := repo.DB.NamedQueryContext(ctx, q, dbr)
if err != nil {
return reports.ReportConfig{}, err
}
defer row.Close()
var dbReport dbReport
if row.Next() {
if err := row.StructScan(&dbReport); err != nil {
return reports.ReportConfig{}, err
}
}
rpt, err := dbToReport(dbReport)
if err != nil {
return reports.ReportConfig{}, err
}
return rpt, nil
}
func (repo *PostgresRepository) UpdateReportSchedule(ctx context.Context, cfg reports.ReportConfig) (reports.ReportConfig, error) {
q := `
UPDATE report_config
SET start_datetime = :start_datetime, due = :due, recurring = :recurring,
recurring_period = :recurring_period, updated_at = :updated_at, updated_by = :updated_by WHERE id = :id
RETURNING id, name, description, domain_id, config, metrics,
email, start_datetime, due, recurring, recurring_period, created_at, created_by, updated_at, updated_by, status;
`
dbr, err := reportToDb(cfg)
if err != nil {
return reports.ReportConfig{}, errors.Wrap(repoerr.ErrUpdateEntity, err)
}
row, err := repo.DB.NamedQueryContext(ctx, q, dbr)
if err != nil {
return reports.ReportConfig{}, postgres.HandleError(repoerr.ErrUpdateEntity, err)
}
defer row.Close()
var dbReport dbReport
if row.Next() {
if err := row.StructScan(&dbReport); err != nil {
return reports.ReportConfig{}, errors.Wrap(repoerr.ErrUpdateEntity, err)
}
}
report, err := dbToReport(dbReport)
if err != nil {
return reports.ReportConfig{}, errors.Wrap(repoerr.ErrUpdateEntity, err)
}
return report, nil
}
func (repo *PostgresRepository) RemoveReportConfig(ctx context.Context, id string) error {
q := `
DELETE FROM report_config
WHERE id = $1;
`
result, err := repo.DB.ExecContext(ctx, q, id)
if err != nil {
return err
}
if _, err := result.RowsAffected(); err != nil {
return repoerr.ErrNotFound
}
return nil
}
func (repo *PostgresRepository) ListReportsConfig(ctx context.Context, pm reports.PageMeta) (reports.ReportConfigPage, error) {
listReportsQuery := `
SELECT id, name, description, domain_id, metrics, email, config,
start_datetime, due, recurring, recurring_period, created_at, created_by, updated_at, updated_by, status
FROM report_config rc %s %s;
`
pgData := ""
if pm.Limit != 0 {
pgData = "LIMIT :limit"
}
if pm.Offset != 0 {
pgData += " OFFSET :offset"
}
pq := pageReportQuery(pm)
q := fmt.Sprintf(listReportsQuery, pq, pgData)
rows, err := repo.DB.NamedQueryContext(ctx, q, pm)
if err != nil {
return reports.ReportConfigPage{}, err
}
defer rows.Close()
cfgs := []reports.ReportConfig{}
for rows.Next() {
var r dbReport
if err := rows.StructScan(&r); err != nil {
return reports.ReportConfigPage{}, errors.Wrap(repoerr.ErrViewEntity, err)
}
rpt, err := dbToReport(r)
if err != nil {
return reports.ReportConfigPage{}, err
}
cfgs = append(cfgs, rpt)
}
cq := fmt.Sprintf(`SELECT COUNT(*) FROM report_config rc %s;`, pq)
total, err := postgres.Total(ctx, repo.DB, cq, pm)
if err != nil {
return reports.ReportConfigPage{}, errors.Wrap(repoerr.ErrViewEntity, err)
}
pm.Total = total
ret := reports.ReportConfigPage{
PageMeta: pm,
ReportConfigs: cfgs,
}
return ret, nil
}
func (repo *PostgresRepository) UpdateReportDue(ctx context.Context, id string, due time.Time) (reports.ReportConfig, error) {
q := `
UPDATE report_config
SET due = :due, updated_at = :updated_at WHERE id = :id
RETURNING id, name, description, domain_id, config, metrics,
email, start_datetime, due, recurring, recurring_period, created_at, created_by, updated_at, updated_by, status;
`
dbr := dbReport{
ID: id,
UpdatedAt: time.Now().UTC(),
Due: sql.NullTime{Time: due},
}
if !due.IsZero() {
dbr.Due.Valid = true
}
row, err := repo.DB.NamedQueryContext(ctx, q, dbr)
if err != nil {
return reports.ReportConfig{}, postgres.HandleError(repoerr.ErrUpdateEntity, err)
}
defer row.Close()
var dbReport dbReport
if row.Next() {
if err := row.StructScan(&dbReport); err != nil {
return reports.ReportConfig{}, errors.Wrap(repoerr.ErrUpdateEntity, err)
}
}
report, err := dbToReport(dbReport)
if err != nil {
return reports.ReportConfig{}, errors.Wrap(repoerr.ErrUpdateEntity, err)
}
return report, nil
}
func pageReportQuery(pm reports.PageMeta) string {
var query []string
if pm.Status != reports.AllStatus {
query = append(query, "rc.status = :status")
}
if pm.Domain != "" {
query = append(query, "rc.domain_id = :domain_id")
}
if pm.ScheduledBefore != nil {
query = append(query, "rc.due < :scheduled_before")
}
if pm.ScheduledAfter != nil {
query = append(query, "rc.due > :scheduled_after")
}
if pm.Name != "" {
query = append(query, "rc.name ILIKE '%' || :name || '%'")
}
var q string
if len(query) > 0 {
q = fmt.Sprintf("WHERE %s", strings.Join(query, " AND "))
}
return q
}
+411
View File
@@ -0,0 +1,411 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package reports
import (
"context"
"encoding/json"
"fmt"
"net/mail"
"strings"
"time"
"github.com/absmach/magistrala/pkg/reltime"
"github.com/absmach/magistrala/pkg/schedule"
"github.com/absmach/supermq/pkg/authn"
"github.com/absmach/supermq/pkg/errors"
"github.com/absmach/supermq/pkg/transformers/senml"
)
var (
errFromTimeNotProvided = errors.New("\"from time\" not provided")
errInvalidFromTime = errors.New("invalid \"from time\" ")
errToTimeNotProvided = errors.New("\"to time\" not provided")
errTitleNotProvided = errors.New("title not provided")
errInvalidToTime = errors.New("invalid \"to time\"")
errAggIntervalTimeNotProvided = errors.New("aggregation interval time not provided")
errInvalidAggInterval = errors.New("invalid aggregation interval time")
errNoToEmail = errors.New("no \"To\" email address found")
errChannelIDNotProvided = errors.New("channel id not provided")
errNameNotProvided = errors.New("name not provided")
)
const (
errInvalidFormatFmt = "invalid format %s"
errInvalidReportActionFmt = "invalid action %s"
errInvalidToEmail = "invalid \"To\" email %s"
errUnknownAggregationFmt = "unknown aggregation type %d"
errUnknownAggregationStringFmt = "unknown aggregation type %s"
)
type Report struct {
Metric Metric `json:"metric,omitempty"`
Messages []senml.Message `json:"messages,omitempty"`
}
type ReportPage struct {
Total uint64 `json:"total"`
From time.Time `json:"from,omitempty"`
To time.Time `json:"to,omitempty"`
Aggregation AggConfig `json:"aggregation,omitempty"`
Reports []Report `json:"reports,omitempty"`
File ReportFile `json:"file,omitempty"`
}
type ReportFile struct {
Name string `json:"name,omitempty"`
Data []byte `json:"data,omitempty"`
Format Format `json:"format,omitempty"`
}
type AggConfig struct {
AggType Aggregation `json:"agg_type,omitempty"` // Optional field
Interval string `json:"interval,omitempty"` // Mandatory field if "AggType" field is set MAX, MIN, COUNT, SUM, AVG
}
func (ac AggConfig) Validate() error {
if ac.AggType != AggregationNONE {
if ac.Interval == "" {
return errAggIntervalTimeNotProvided
}
if _, err := time.ParseDuration(ac.Interval); err != nil {
return errInvalidAggInterval
}
}
return nil
}
type MetricConfig struct {
From string `json:"from,omitempty"` // Mandatory field
To string `json:"to,omitempty"` // Mandatory field
Title string `json:"title,omitempty"` // Mandatory field
FileFormat Format `json:"file_format,omitempty"` // Optional field
Aggregation AggConfig `json:"aggregation,omitempty"` // Optional field
}
func (mc MetricConfig) Validate() error {
if mc.From == "" {
return errFromTimeNotProvided
}
if _, err := reltime.Parse(mc.From); err != nil {
return errInvalidFromTime
}
if mc.To == "" {
return errToTimeNotProvided
}
if _, err := reltime.Parse(mc.To); err != nil {
return errInvalidToTime
}
if mc.Title == "" {
return errTitleNotProvided
}
if err := mc.Aggregation.Validate(); err != nil {
return err
}
return nil
}
type Metric struct {
ChannelID string `json:"channel_id,omitempty"` // Mandatory field
ClientID string `json:"client_id,omitempty"` // Optional field
Name string `json:"name,omitempty"` // Mandatory field
Subtopic string `json:"subtopic,omitempty"` // Optional field
Protocol string `json:"protocol,omitempty"` // Optional field
Format string `json:"format,omitiempty"` // Optional field
}
type ReqMetric struct {
ChannelID string `json:"channel_id,omitempty"` // Mandatory field
ClientIDs []string `json:"client_ids,omitempty"` // Optional field
Name string `json:"name,omitempty"` // Mandatory field
Subtopic string `json:"subtopic,omitempty"` // Optional field
Protocol string `json:"protocol,omitempty"` // Optional field
Format string `json:"format,omitiempty"` // Optional field
}
func (rm ReqMetric) Validate() error {
if rm.ChannelID == "" {
return errChannelIDNotProvided
}
if rm.Name == "" {
return errNameNotProvided
}
return nil
}
type ReportConfig struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
DomainID string `json:"domain_id"`
Schedule schedule.Schedule `json:"schedule,omitempty"`
Config *MetricConfig `json:"config,omitempty"`
Email *EmailSetting `json:"email,omitempty"`
Metrics []ReqMetric `json:"metrics,omitempty"`
Status Status `json:"status"`
CreatedAt time.Time `json:"created_at,omitempty"`
CreatedBy string `json:"created_by,omitempty"`
UpdatedAt time.Time `json:"updated_at,omitempty"`
UpdatedBy string `json:"updated_by,omitempty"`
}
type ReportConfigPage struct {
PageMeta
ReportConfigs []ReportConfig `json:"report_configs"`
}
type EmailSetting struct {
To []string `json:"to,omitempty"`
Subject string `json:"subject,omitempty"`
Content string `json:"content,omitempty"`
}
func (es *EmailSetting) Validate() error {
if len(es.To) == 0 {
return errNoToEmail
}
for _, to := range es.To {
if _, err := mail.ParseAddress(to); err != nil {
return errors.Wrap(fmt.Errorf(errInvalidToEmail, to), err)
}
}
return nil
}
type Format uint8
const (
PDF = iota
CSV
AllFormats
)
const (
PdfFormat = "pdf"
CsvFormat = "csv"
All_Formats = "AllFormats"
)
func (f Format) String() string {
switch f {
case PDF:
return PdfFormat
case CSV:
return CsvFormat
case AllFormats:
return All_Formats
default:
return Unknown
}
}
func (f Format) Extension() string {
switch f {
case PDF:
return PdfFormat
case CSV:
return CsvFormat
default:
return Unknown
}
}
func (f Format) ContentType() string {
switch f {
case PDF:
return "application/pdf"
case CSV:
return "text/csv"
default:
return Unknown
}
}
func ToFormat(format string) (Format, error) {
switch format {
case "", PdfFormat:
return PDF, nil
case CsvFormat:
return CSV, nil
case All_Formats:
return AllFormats, nil
}
return Format(0), fmt.Errorf(errInvalidFormatFmt, format)
}
func (f Format) MarshalJSON() ([]byte, error) {
return json.Marshal(f.String())
}
func (f *Format) UnmarshalJSON(data []byte) error {
str := strings.Trim(string(data), "\"")
val, err := ToFormat(str)
*f = val
return err
}
type ReportAction uint8
const (
ViewReport = iota
DownloadReport
EmailReport
)
const (
ViewReportAction = "view"
DownloadReportAction = "download"
EmailReportAction = "email"
)
func (ra ReportAction) String() string {
switch ra {
case ViewReport:
return ViewReportAction
case DownloadReport:
return DownloadReportAction
case EmailReport:
return EmailReportAction
default:
return Unknown
}
}
func ToReportAction(action string) (ReportAction, error) {
switch action {
case "", ViewReportAction:
return ViewReport, nil
case DownloadReportAction:
return DownloadReport, nil
case EmailReportAction:
return EmailReport, nil
}
return ReportAction(0), fmt.Errorf(errInvalidReportActionFmt, action)
}
func (ra ReportAction) MarshalJSON() ([]byte, error) {
return json.Marshal(ra.String())
}
func (ra *ReportAction) UnmarshalJSON(data []byte) error {
str := strings.Trim(string(data), "\"")
val, err := ToReportAction(str)
*ra = val
return err
}
type Aggregation uint8
const (
AggregationNONE = iota
AggregationMAX
AggregationMIN
AggregationSUM
AggregationCOUNT
AggregationAVG
)
const (
aggregationNONE = "none"
aggregationMAX = "max"
aggregationMIN = "min"
aggregationSUM = "sum"
aggregationCOUNT = "count"
aggregationAVG = "avg"
)
func (a Aggregation) String() string {
switch a {
case AggregationNONE:
return aggregationNONE
case AggregationMAX:
return aggregationMAX
case AggregationMIN:
return aggregationMIN
case AggregationSUM:
return aggregationSUM
case AggregationCOUNT:
return aggregationCOUNT
case AggregationAVG:
return aggregationAVG
default:
return fmt.Sprintf(errUnknownAggregationFmt, a)
}
}
func ToAggregation(agg string) (Aggregation, error) {
switch strings.ToLower(agg) {
case "", aggregationNONE:
return AggregationNONE, nil
case aggregationMAX:
return AggregationMAX, nil
case aggregationMIN:
return AggregationMIN, nil
case aggregationSUM:
return AggregationSUM, nil
case aggregationCOUNT:
return AggregationCOUNT, nil
case aggregationAVG:
return AggregationAVG, nil
default:
return Aggregation(0), fmt.Errorf(errUnknownAggregationStringFmt, agg)
}
}
func (a Aggregation) MarshalJSON() ([]byte, error) {
return json.Marshal(a.String())
}
func (a *Aggregation) UnmarshalJSON(data []byte) error {
str := strings.Trim(string(data), "\"")
val, err := ToAggregation(str)
*a = val
return err
}
type PageMeta struct {
Total uint64 `json:"total" db:"total"`
Offset uint64 `json:"offset" db:"offset"`
Limit uint64 `json:"limit" db:"limit"`
Name string `json:"name" db:"name"`
Status Status `json:"status,omitempty" db:"status"`
Domain string `json:"domain_id,omitempty" db:"domain_id"`
ScheduledBefore *time.Time `json:"scheduled_before,omitempty" db:"scheduled_before"` // Filter rules scheduled before this time
ScheduledAfter *time.Time `json:"scheduled_after,omitempty" db:"scheduled_after"` // Filter rules scheduled after this time
}
type Repository interface {
AddReportConfig(ctx context.Context, cfg ReportConfig) (ReportConfig, error)
ViewReportConfig(ctx context.Context, id string) (ReportConfig, error)
UpdateReportConfig(ctx context.Context, cfg ReportConfig) (ReportConfig, error)
UpdateReportSchedule(ctx context.Context, cfg ReportConfig) (ReportConfig, error)
RemoveReportConfig(ctx context.Context, id string) error
UpdateReportConfigStatus(ctx context.Context, cfg ReportConfig) (ReportConfig, error)
ListReportsConfig(ctx context.Context, pm PageMeta) (ReportConfigPage, error)
UpdateReportDue(ctx context.Context, id string, due time.Time) (ReportConfig, error)
}
type Service interface {
AddReportConfig(ctx context.Context, session authn.Session, cfg ReportConfig) (ReportConfig, error)
ViewReportConfig(ctx context.Context, session authn.Session, id string) (ReportConfig, error)
UpdateReportConfig(ctx context.Context, session authn.Session, cfg ReportConfig) (ReportConfig, error)
UpdateReportSchedule(ctx context.Context, session authn.Session, cfg ReportConfig) (ReportConfig, error)
RemoveReportConfig(ctx context.Context, session authn.Session, id string) error
ListReportsConfig(ctx context.Context, session authn.Session, pm PageMeta) (ReportConfigPage, error)
EnableReportConfig(ctx context.Context, session authn.Session, id string) (ReportConfig, error)
DisableReportConfig(ctx context.Context, session authn.Session, id string) (ReportConfig, error)
GenerateReport(ctx context.Context, session authn.Session, config ReportConfig, action ReportAction) (ReportPage, error)
StartScheduler(ctx context.Context) error
}
+418
View File
@@ -0,0 +1,418 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package reports
import (
"context"
"fmt"
"strings"
"time"
grpcReadersV1 "github.com/absmach/magistrala/api/grpc/readers/v1"
"github.com/absmach/magistrala/pkg/emailer"
pkglog "github.com/absmach/magistrala/pkg/logger"
"github.com/absmach/magistrala/pkg/reltime"
"github.com/absmach/magistrala/pkg/ticker"
"github.com/absmach/supermq"
"github.com/absmach/supermq/pkg/authn"
"github.com/absmach/supermq/pkg/errors"
svcerr "github.com/absmach/supermq/pkg/errors/service"
"github.com/absmach/supermq/pkg/transformers/senml"
)
const limit = 1000
type report struct {
repo Repository
runInfo chan pkglog.RunInfo
idp supermq.IDProvider
email emailer.Emailer
ticker ticker.Ticker
readers grpcReadersV1.ReadersServiceClient
}
func NewService(repo Repository, runInfo chan pkglog.RunInfo, idp supermq.IDProvider, tck ticker.Ticker, emailer emailer.Emailer, readers grpcReadersV1.ReadersServiceClient) Service {
return &report{
repo: repo,
idp: idp,
runInfo: runInfo,
email: emailer,
ticker: tck,
readers: readers,
}
}
func (r *report) AddReportConfig(ctx context.Context, session authn.Session, cfg ReportConfig) (ReportConfig, error) {
id, err := r.idp.ID()
if err != nil {
return ReportConfig{}, err
}
now := time.Now()
cfg.ID = id
cfg.CreatedAt = now
cfg.CreatedBy = session.UserID
cfg.DomainID = session.DomainID
cfg.Status = EnabledStatus
if cfg.Schedule.StartDateTime.IsZero() {
cfg.Schedule.StartDateTime = now
}
cfg.Schedule.Time = cfg.Schedule.StartDateTime
reportConfig, err := r.repo.AddReportConfig(ctx, cfg)
if err != nil {
return ReportConfig{}, errors.Wrap(svcerr.ErrCreateEntity, err)
}
return reportConfig, nil
}
func (r *report) ViewReportConfig(ctx context.Context, session authn.Session, id string) (ReportConfig, error) {
cfg, err := r.repo.ViewReportConfig(ctx, id)
if err != nil {
return ReportConfig{}, errors.Wrap(svcerr.ErrViewEntity, err)
}
return cfg, nil
}
func (r *report) UpdateReportConfig(ctx context.Context, session authn.Session, cfg ReportConfig) (ReportConfig, error) {
cfg.UpdatedAt = time.Now().UTC()
cfg.UpdatedBy = session.UserID
reportConfig, err := r.repo.UpdateReportConfig(ctx, cfg)
if err != nil {
return ReportConfig{}, errors.Wrap(svcerr.ErrUpdateEntity, err)
}
return reportConfig, nil
}
func (r *report) UpdateReportSchedule(ctx context.Context, session authn.Session, cfg ReportConfig) (ReportConfig, error) {
cfg.UpdatedAt = time.Now().UTC()
cfg.UpdatedBy = session.UserID
cfg.Schedule.Time = cfg.Schedule.StartDateTime
c, err := r.repo.UpdateReportSchedule(ctx, cfg)
if err != nil {
return ReportConfig{}, errors.Wrap(svcerr.ErrUpdateEntity, err)
}
return c, nil
}
func (r *report) RemoveReportConfig(ctx context.Context, session authn.Session, id string) error {
if err := r.repo.RemoveReportConfig(ctx, id); err != nil {
return errors.Wrap(svcerr.ErrRemoveEntity, err)
}
return nil
}
func (r *report) ListReportsConfig(ctx context.Context, session authn.Session, pm PageMeta) (ReportConfigPage, error) {
pm.Domain = session.DomainID
page, err := r.repo.ListReportsConfig(ctx, pm)
if err != nil {
return ReportConfigPage{}, errors.Wrap(svcerr.ErrViewEntity, err)
}
return page, nil
}
func (r *report) EnableReportConfig(ctx context.Context, session authn.Session, id string) (ReportConfig, error) {
status, err := ToStatus(Enabled)
if err != nil {
return ReportConfig{}, err
}
cfg := ReportConfig{
ID: id,
UpdatedAt: time.Now().UTC(),
UpdatedBy: session.UserID,
Status: status,
}
cfg, err = r.repo.UpdateReportConfigStatus(ctx, cfg)
if err != nil {
return ReportConfig{}, errors.Wrap(svcerr.ErrUpdateEntity, err)
}
return cfg, nil
}
func (r *report) DisableReportConfig(ctx context.Context, session authn.Session, id string) (ReportConfig, error) {
status, err := ToStatus(Disabled)
if err != nil {
return ReportConfig{}, err
}
cfg := ReportConfig{
ID: id,
UpdatedAt: time.Now().UTC(),
UpdatedBy: session.UserID,
Status: status,
}
cfg, err = r.repo.UpdateReportConfigStatus(ctx, cfg)
if err != nil {
return ReportConfig{}, errors.Wrap(svcerr.ErrUpdateEntity, err)
}
return cfg, nil
}
func (r *report) GenerateReport(ctx context.Context, session authn.Session, config ReportConfig, action ReportAction) (ReportPage, error) {
config.DomainID = session.DomainID
if config.Status != EnabledStatus {
return ReportPage{}, svcerr.ErrInvalidStatus
}
reportPage, err := r.generateReport(ctx, config, action)
if err != nil {
return ReportPage{}, err
}
return reportPage, nil
}
func (r *report) generateReport(ctx context.Context, cfg ReportConfig, action ReportAction) (ReportPage, error) {
genReportFile, err := generateFileFunc(action, cfg.Config.FileFormat)
if err != nil {
return ReportPage{}, err
}
agg := grpcReadersV1.Aggregation_AGGREGATION_UNSPECIFIED
switch cfg.Config.Aggregation.AggType {
case AggregationMAX:
agg = grpcReadersV1.Aggregation_MAX
case AggregationMIN:
agg = grpcReadersV1.Aggregation_MIN
case AggregationCOUNT:
agg = grpcReadersV1.Aggregation_COUNT
case AggregationAVG:
agg = grpcReadersV1.Aggregation_AVG
case AggregationSUM:
agg = grpcReadersV1.Aggregation_SUM
}
from, err := reltime.Parse(cfg.Config.From)
if err != nil {
return ReportPage{}, err
}
to, err := reltime.Parse(cfg.Config.To)
if err != nil {
return ReportPage{}, err
}
pm := &grpcReadersV1.PageMetadata{
Aggregation: agg,
Limit: limit,
From: float64(from.UnixMicro()),
To: float64(to.UnixNano()),
Interval: cfg.Config.Aggregation.Interval,
}
var mets []Metric
var reports []Report
for _, metric := range cfg.Metrics {
switch {
case len(metric.ClientIDs) != 0:
for _, clientID := range metric.ClientIDs {
mets = append(mets, Metric{
ChannelID: metric.ChannelID,
ClientID: clientID,
Name: metric.Name,
Subtopic: metric.Subtopic,
Protocol: metric.Protocol,
Format: metric.Format,
})
}
default:
mets = append(mets, Metric{
ChannelID: metric.ChannelID,
Name: metric.Name,
Subtopic: metric.Subtopic,
Protocol: metric.Protocol,
Format: metric.Format,
})
}
}
for _, metric := range mets {
sMsgs := []senml.Message{}
pm.Offset = uint64(0)
pm.Name = metric.Name
if metric.ClientID != "" {
pm.Publisher = metric.ClientID
}
if metric.Subtopic != "" {
pm.Subtopic = metric.Subtopic
}
if metric.Protocol != "" {
pm.Protocol = metric.Protocol
}
if metric.Format != "" {
pm.Format = metric.Format
}
msgs, err := r.readers.ReadMessages(ctx, &grpcReadersV1.ReadMessagesReq{
ChannelId: metric.ChannelID,
DomainId: cfg.DomainID,
PageMetadata: pm,
})
if err != nil {
return ReportPage{}, err
}
for _, msg := range msgs.Messages {
sMsgs = append(sMsgs, convertToSenml(msg.GetSenml()))
}
for msgs.GetTotal() > (pm.Offset + pm.Limit) {
pm.Offset = pm.Offset + pm.Limit
msgs, err := r.readers.ReadMessages(ctx, &grpcReadersV1.ReadMessagesReq{
ChannelId: metric.ChannelID,
DomainId: cfg.DomainID,
PageMetadata: pm,
})
if err != nil {
return ReportPage{}, err
}
for _, msg := range msgs.Messages {
sMsgs = append(sMsgs, convertToSenml(msg.GetSenml()))
}
}
reports = append(reports, convertToReports(metric, sMsgs)...)
}
switch {
case genReportFile != nil:
data, err := genReportFile(cfg.Config.Title, reports)
if err != nil {
return ReportPage{}, err
}
timeStr := strings.ReplaceAll(time.Now().Format(time.RFC3339), ":", "")
filePrefix := cfg.Name
if filePrefix == "" {
filePrefix = "report"
}
fileName := fmt.Sprintf("%s_%s.%s", filePrefix, timeStr, cfg.Config.FileFormat.Extension())
file := ReportFile{
Name: fileName,
Data: data,
Format: cfg.Config.FileFormat,
}
switch action {
case EmailReport:
if err := r.emailReports(*cfg.Email, file); err != nil {
return ReportPage{}, errors.Wrap(err, svcerr.ErrCreateEntity)
}
return ReportPage{}, nil
default:
return ReportPage{
File: file,
}, nil
}
default:
return ReportPage{
From: from,
To: to,
Aggregation: cfg.Config.Aggregation,
Total: uint64(len(reports)),
Reports: reports,
}, nil
}
}
func generateFileFunc(action ReportAction, format Format) (func(string, []Report) ([]byte, error), error) {
switch action {
case DownloadReport, EmailReport:
switch format {
case PDF:
return generatePDFReport, nil
case CSV:
return generateCSVReport, nil
default:
return nil, errors.New("file format not supported")
}
default:
return nil, nil
}
}
func (r *report) emailReports(es EmailSetting, file ReportFile) error {
if err := es.Validate(); err != nil {
return errors.Wrap(svcerr.ErrMalformedEntity, err)
}
attachments := map[string][]byte{
file.Name: file.Data,
}
if err := r.email.SendEmailNotification(
es.To,
"",
es.Subject,
"",
"",
es.Content,
"",
attachments,
); err != nil {
return err
}
return nil
}
func convertToSenml(g *grpcReadersV1.SenMLMessage) senml.Message {
if g == nil {
return senml.Message{}
}
return senml.Message{
Protocol: g.Base.GetProtocol(),
Subtopic: g.Base.GetSubtopic(),
Publisher: g.Base.GetPublisher(),
Channel: g.Base.GetChannel(),
Name: g.GetName(),
Unit: g.GetUnit(),
Time: g.GetTime(),
UpdateTime: g.GetUpdateTime(),
Value: g.Value,
StringValue: g.StringValue,
DataValue: g.DataValue,
BoolValue: g.BoolValue,
Sum: g.Sum,
}
}
func convertToReports(metric Metric, senmlMsgs []senml.Message) []Report {
if metric.ClientID != "" {
return []Report{
{
Metric: metric,
Messages: senmlMsgs,
},
}
}
return groupReportsByPublisher(metric, senmlMsgs)
}
func groupReportsByPublisher(metric Metric, sMsgs []senml.Message) []Report {
publishers := map[string][]senml.Message{}
for _, msg := range sMsgs {
publishers[msg.Publisher] = append(publishers[msg.Publisher], msg)
}
var groupedReports []Report
for publisher, messages := range publishers {
gMetric := metric
gMetric.ClientID = publisher
groupedReports = append(groupedReports, Report{
Metric: gMetric,
Messages: messages,
})
}
return groupedReports
}
+466
View File
@@ -0,0 +1,466 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package reports_test
import (
"context"
"fmt"
"testing"
"time"
"github.com/0x6flab/namegenerator"
"github.com/absmach/magistrala/internal/testsutil"
pkglog "github.com/absmach/magistrala/pkg/logger"
pkgSch "github.com/absmach/magistrala/pkg/schedule"
remocks "github.com/absmach/magistrala/re/mocks"
readmocks "github.com/absmach/magistrala/readers/mocks"
"github.com/absmach/magistrala/reports"
"github.com/absmach/magistrala/reports/mocks"
"github.com/absmach/supermq/pkg/authn"
"github.com/absmach/supermq/pkg/errors"
repoerr "github.com/absmach/supermq/pkg/errors/repository"
svcerr "github.com/absmach/supermq/pkg/errors/service"
"github.com/absmach/supermq/pkg/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
var (
namegen = namegenerator.NewGenerator()
userID = testsutil.GenerateUUID(&testing.T{})
domainID = testsutil.GenerateUUID(&testing.T{})
schedule = pkgSch.Schedule{
StartDateTime: time.Now().Add(-time.Hour),
Recurring: pkgSch.Daily,
RecurringPeriod: 1,
Time: time.Now().Add(-time.Hour),
}
reportName = namegen.Generate()
rptConfig = reports.ReportConfig{
ID: testsutil.GenerateUUID(&testing.T{}),
Name: reportName,
DomainID: domainID,
Status: reports.EnabledStatus,
Schedule: schedule,
CreatedBy: userID,
UpdatedBy: userID,
UpdatedAt: time.Now(),
}
)
func newService(runInfo chan pkglog.RunInfo) (reports.Service, *mocks.Repository, *remocks.Ticker) {
repo := new(mocks.Repository)
mockTicker := new(remocks.Ticker)
idProvider := uuid.NewMock()
readersSvc := new(readmocks.ReadersServiceClient)
e := new(remocks.Emailer)
return reports.NewService(repo, runInfo, idProvider, mockTicker, e, readersSvc), repo, mockTicker
}
func TestAddReportConfig(t *testing.T) {
svc, repo, _ := newService(make(chan pkglog.RunInfo))
cases := []struct {
desc string
session authn.Session
cfg reports.ReportConfig
res reports.ReportConfig
err error
}{
{
desc: "Add report config successfully",
session: authn.Session{
UserID: userID,
DomainID: domainID,
},
cfg: reports.ReportConfig{
Name: reportName,
Schedule: schedule,
},
res: rptConfig,
err: nil,
},
{
desc: "Add report config with failed repo",
session: authn.Session{
UserID: userID,
DomainID: domainID,
},
cfg: reports.ReportConfig{
Name: reportName,
Schedule: schedule,
},
err: repoerr.ErrCreateEntity,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
repoCall := repo.On("AddReportConfig", mock.Anything, mock.Anything).Return(tc.res, tc.err)
res, err := svc.AddReportConfig(context.Background(), tc.session, tc.cfg)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
if err == nil {
assert.NotEmpty(t, res.ID, "expected non-empty result in ID")
assert.Equal(t, tc.cfg.Name, res.Name)
assert.Equal(t, tc.cfg.Schedule, res.Schedule)
}
defer repoCall.Unset()
})
}
}
func TestViewReportConfig(t *testing.T) {
svc, repo, _ := newService(make(chan pkglog.RunInfo))
cases := []struct {
desc string
session authn.Session
id string
res reports.ReportConfig
err error
}{
{
desc: "view report config successfully",
session: authn.Session{
UserID: userID,
DomainID: domainID,
},
id: rptConfig.ID,
res: rptConfig,
err: nil,
},
{
desc: "view report config with failed repo",
session: authn.Session{
UserID: userID,
DomainID: domainID,
},
id: rptConfig.ID,
err: svcerr.ErrViewEntity,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
repoCall := repo.On("ViewReportConfig", mock.Anything, mock.Anything).Return(tc.res, tc.err)
res, err := svc.ViewReportConfig(context.Background(), tc.session, tc.id)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
if err == nil {
assert.Equal(t, tc.res, res)
}
defer repoCall.Unset()
})
}
}
func TestUpdateReportConfig(t *testing.T) {
svc, repo, _ := newService(make(chan pkglog.RunInfo))
newName := namegen.Generate()
now := time.Now().Add(time.Hour)
cases := []struct {
desc string
session authn.Session
cfg reports.ReportConfig
res reports.ReportConfig
err error
}{
{
desc: "update report config successfully",
session: authn.Session{
UserID: userID,
DomainID: domainID,
},
cfg: reports.ReportConfig{
Name: newName,
ID: rptConfig.ID,
Schedule: schedule,
},
res: reports.ReportConfig{
Name: newName,
ID: rptConfig.ID,
DomainID: rptConfig.DomainID,
Status: rptConfig.Status,
Schedule: rptConfig.Schedule,
UpdatedAt: now,
UpdatedBy: userID,
},
err: nil,
},
{
desc: "update report config with failed repo",
session: authn.Session{
UserID: userID,
DomainID: domainID,
},
cfg: reports.ReportConfig{
Name: rptConfig.Name,
ID: rptConfig.ID,
Schedule: schedule,
},
err: svcerr.ErrUpdateEntity,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
repoCall := repo.On("UpdateReportConfig", mock.Anything, mock.Anything).Return(tc.res, tc.err)
res, err := svc.UpdateReportConfig(context.Background(), tc.session, tc.cfg)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
if err == nil {
assert.Equal(t, tc.res, res)
}
defer repoCall.Unset()
})
}
}
func TestListReportsConfig(t *testing.T) {
svc, repo, _ := newService(make(chan pkglog.RunInfo))
numConfigs := 50
now := time.Now().Add(time.Hour)
var configs []reports.ReportConfig
for i := 0; i < numConfigs; i++ {
c := reports.ReportConfig{
ID: testsutil.GenerateUUID(t),
Name: namegen.Generate(),
DomainID: domainID,
Status: reports.EnabledStatus,
CreatedAt: now,
CreatedBy: userID,
Schedule: schedule,
}
configs = append(configs, c)
}
cases := []struct {
desc string
session authn.Session
pageMeta reports.PageMeta
res reports.ReportConfigPage
err error
}{
{
desc: "list report configs successfully",
session: authn.Session{
UserID: userID,
DomainID: domainID,
},
pageMeta: reports.PageMeta{},
res: reports.ReportConfigPage{
PageMeta: reports.PageMeta{
Total: uint64(numConfigs),
Offset: 0,
Limit: 10,
},
ReportConfigs: configs[0:10],
},
err: nil,
},
{
desc: "list report configs successfully with limit",
session: authn.Session{
UserID: userID,
DomainID: domainID,
},
pageMeta: reports.PageMeta{
Limit: 100,
},
res: reports.ReportConfigPage{
PageMeta: reports.PageMeta{
Total: uint64(numConfigs),
Offset: 0,
Limit: 100,
},
ReportConfigs: configs[0:numConfigs],
},
err: nil,
},
{
desc: "list report configs successfully with offset",
session: authn.Session{
UserID: userID,
DomainID: domainID,
},
pageMeta: reports.PageMeta{
Offset: 20,
Limit: 10,
},
res: reports.ReportConfigPage{
PageMeta: reports.PageMeta{
Total: uint64(numConfigs),
Offset: 20,
Limit: 10,
},
ReportConfigs: configs[20:30],
},
err: nil,
},
{
desc: "list report configs with failed repo",
session: authn.Session{
UserID: userID,
DomainID: domainID,
},
pageMeta: reports.PageMeta{},
err: svcerr.ErrViewEntity,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
repoCall := repo.On("ListReportsConfig", mock.Anything, mock.Anything).Return(tc.res, tc.err)
res, err := svc.ListReportsConfig(context.Background(), tc.session, tc.pageMeta)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
if err == nil {
assert.Equal(t, tc.res, res)
}
defer repoCall.Unset()
})
}
}
func TestRemoveReportConfig(t *testing.T) {
svc, repo, _ := newService(make(chan pkglog.RunInfo))
cases := []struct {
desc string
session authn.Session
id string
err error
}{
{
desc: "remove report config successfully",
session: authn.Session{
UserID: userID,
DomainID: domainID,
},
id: rptConfig.ID,
err: nil,
},
{
desc: "remove report config with failed repo",
session: authn.Session{
UserID: userID,
DomainID: domainID,
},
id: rptConfig.ID,
err: svcerr.ErrRemoveEntity,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
repoCall := repo.On("RemoveReportConfig", mock.Anything, mock.Anything).Return(tc.err)
err := svc.RemoveReportConfig(context.Background(), tc.session, tc.id)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
defer repoCall.Unset()
})
}
}
func TestEnableReportConfig(t *testing.T) {
svc, repo, _ := newService(make(chan pkglog.RunInfo))
cases := []struct {
desc string
session authn.Session
id string
status reports.Status
res reports.ReportConfig
err error
}{
{
desc: "enable report config successfully",
session: authn.Session{
UserID: userID,
DomainID: domainID,
},
id: rptConfig.ID,
status: reports.EnabledStatus,
res: rptConfig,
err: nil,
},
{
desc: "enable report config with failed repo",
session: authn.Session{
UserID: userID,
DomainID: domainID,
},
id: rptConfig.ID,
status: reports.EnabledStatus,
err: svcerr.ErrUpdateEntity,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
repoCall := repo.On("UpdateReportConfigStatus", context.Background(), mock.Anything).Return(tc.res, tc.err)
res, err := svc.EnableReportConfig(context.Background(), tc.session, tc.id)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
if err == nil {
assert.Equal(t, tc.res, res)
}
defer repoCall.Unset()
})
}
}
func TestDisableReportConfig(t *testing.T) {
svc, repo, _ := newService(make(chan pkglog.RunInfo))
cases := []struct {
desc string
session authn.Session
id string
status reports.Status
res reports.ReportConfig
err error
}{
{
desc: "disable report config successfully",
session: authn.Session{
UserID: userID,
DomainID: domainID,
},
id: rptConfig.ID,
status: reports.DisabledStatus,
res: reports.ReportConfig{
ID: rptConfig.ID,
Name: rptConfig.Name,
DomainID: rptConfig.DomainID,
Status: reports.DisabledStatus,
Schedule: schedule,
UpdatedBy: userID,
UpdatedAt: time.Now(),
},
err: nil,
},
{
desc: "disable report config with failed repo",
session: authn.Session{
UserID: userID,
DomainID: domainID,
},
id: rptConfig.ID,
status: reports.DisabledStatus,
err: svcerr.ErrUpdateEntity,
},
}
for _, tc := range cases {
t.Run(tc.desc, func(t *testing.T) {
repoCall := repo.On("UpdateReportConfigStatus", mock.Anything, mock.Anything).Return(tc.res, tc.err)
res, err := svc.DisableReportConfig(context.Background(), tc.session, tc.id)
assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err))
if err == nil {
assert.Equal(t, tc.res, res)
}
defer repoCall.Unset()
})
}
}
+80
View File
@@ -0,0 +1,80 @@
// Copyright (c) Abstract Machines
// SPDX-License-Identifier: Apache-2.0
package reports
import (
"encoding/json"
"strings"
svcerr "github.com/absmach/supermq/pkg/errors/service"
)
// Status represents Rule status.
type Status uint8
// Possible User status values.
const (
// EnabledStatus represents enabled Rule.
EnabledStatus Status = iota
// DisabledStatus represents disabled Rule.
DisabledStatus
// DeletedStatus represents a rule that will be deleted.
DeletedStatus
// AllStatus is used for querying purposes to list rules irrespective
// of their status - both enabled and disabled. It is never stored in the
// database as the actual User status and should always be the largest
// value in this enumeration.
AllStatus
)
// String representation of the possible status values.
const (
Disabled = "disabled"
Enabled = "enabled"
Deleted = "deleted"
All = "all"
Unknown = "unknown"
)
func (s Status) String() string {
switch s {
case DisabledStatus:
return Disabled
case EnabledStatus:
return Enabled
case DeletedStatus:
return Deleted
case AllStatus:
return All
default:
return Unknown
}
}
// ToStatus converts string value to a valid status.
func ToStatus(status string) (Status, error) {
switch status {
case "", Enabled:
return EnabledStatus, nil
case Disabled:
return DisabledStatus, nil
case Deleted:
return DeletedStatus, nil
case All:
return AllStatus, nil
}
return Status(0), svcerr.ErrInvalidStatus
}
func (s Status) MarshalJSON() ([]byte, error) {
return json.Marshal(s.String())
}
func (s *Status) UnmarshalJSON(data []byte) error {
str := strings.Trim(string(data), "\"")
val, err := ToStatus(str)
*s = val
return err
}