Files
2026-04-08 10:40:27 +02:00

446 lines
13 KiB
Go

// 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/authn"
"github.com/absmach/magistrala/pkg/errors"
"github.com/absmach/magistrala/pkg/reltime"
"github.com/absmach/magistrala/pkg/roles"
"github.com/absmach/magistrala/pkg/schedule"
"github.com/absmach/magistrala/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"` // Optional field
Timezone string `json:"timezone,omitempty"` // Optional field, defaults to UTC
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
}
if tz := strings.TrimSpace(mc.Timezone); tz != "" {
if _, err := time.LoadLocation(tz); err != nil {
return errors.Wrap(fmt.Errorf("invalid timezone: %s", tz), 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,omitempty"` // 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,omitempty"` // 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"`
ReportTemplate ReportTemplate `json:"report_template,omitempty"`
Status Status `json:"status"`
CreatedAt time.Time `json:"created_at"`
CreatedBy string `json:"created_by,omitempty"`
UpdatedAt time.Time `json:"updated_at"`
UpdatedBy string `json:"updated_by,omitempty"`
// Extended
RoleID string `json:"role_id,omitempty"`
RoleName string `json:"role_name,omitempty"`
Actions []string `json:"actions,omitempty"`
AccessType string `json:"access_type,omitempty"`
AccessProviderId string `json:"access_provider_id,omitempty"`
AccessProviderRoleId string `json:"access_provider_role_id,omitempty"`
AccessProviderRoleName string `json:"access_provider_role_name,omitempty"`
AccessProviderRoleActions []string `json:"access_provider_role_actions,omitempty"`
Roles []roles.MemberRoleActions `json:"roles,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"`
Dir string `json:"dir" db:"dir"`
Order string `json:"order" db:"order"`
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
UserID string `json:"user_id,omitempty" db:"user_id"`
}
type Repository interface {
AddReportConfig(ctx context.Context, cfg ReportConfig) (ReportConfig, error)
ViewReportConfig(ctx context.Context, id string) (ReportConfig, error)
RetrieveByIDWithRoles(ctx context.Context, id, memberID 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)
ListAllReportsConfig(ctx context.Context, pm PageMeta) (ReportConfigPage, error)
ListUserReportsConfig(ctx context.Context, userID string, pm PageMeta) (ReportConfigPage, error)
UpdateReportDue(ctx context.Context, id string, due time.Time) (ReportConfig, error)
UpdateReportTemplate(ctx context.Context, domainID, reportID string, template ReportTemplate) error
ViewReportTemplate(ctx context.Context, domainID, reportID string) (ReportTemplate, error)
DeleteReportTemplate(ctx context.Context, domainID, reportID string) error
roles.Repository
}
type Service interface {
AddReportConfig(ctx context.Context, session authn.Session, cfg ReportConfig) (ReportConfig, error)
ViewReportConfig(ctx context.Context, session authn.Session, id string, withRoles bool) (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)
UpdateReportTemplate(ctx context.Context, session authn.Session, cfg ReportConfig) error
ViewReportTemplate(ctx context.Context, session authn.Session, id string) (ReportTemplate, error)
DeleteReportTemplate(ctx context.Context, session authn.Session, id string) error
GenerateReport(ctx context.Context, session authn.Session, config ReportConfig, action ReportAction) (ReportPage, error)
StartScheduler(ctx context.Context) error
roles.RoleManager
}