Repository: absmach/magistrala
Pull request #138 (2025-05-05 09:36:05)

Bump github.com/nats-io/nats.go from 1.41.2 to 1.42.0
Bumps [github.com/nats-io/nats.go](https://github.com/nats-io/nats.go) from 1.41.2 to 1.42.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/nats-io/nats.go/releases">github.com/nats-io/nats.go's releases</a>.</em></p>
<blockquote>
<h2>Release v1.42.0</h2>
<h2>Changelog</h2>
<h3>Overview</h3>
<p>This release adds per-key TTL functionality to key-value stores. It adds:</p>
<ol>
<li><code>LimitMarkerTTL</code> config option to enable automatic tombstone deletion.</li>
<li><code>KeyTTL</code> option to <code>kv.Create()</code></li>
<li><code>PurgeTTL()</code> option to <code>kv.Purge()</code></li>
</ol>
<h3>ADDED</h3>
<ul>
<li>KeyValue:
<ul>
<li>Added KeyValue per key TTL support and limit markers (<a href="https://redirect.github.com/nats-io/nats.go/issues/1864">#1864</a>)</li>
</ul>
</li>
</ul>
<h3>Complete Changes</h3>
<p><a href="https://github.com/nats-io/nats.go/compare/v1.41.2...v1.42.0">https://github.com/nats-io/nats.go/compare/v1.41.2...v1.42.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/nats-io/nats.go/commit/70300b2e9480965e567f465acc26acb18efbc046"><code>70300b2</code></a> Release v1.42.0 (<a href="https://redirect.github.com/nats-io/nats.go/issues/1869">#1869</a>)</li>
<li><a href="https://github.com/nats-io/nats.go/commit/c93af509625227d62333f4670407a5672259c09d"><code>c93af50</code></a> [ADDED] KeyValue per key TTL support and limit markers (<a href="https://redirect.github.com/nats-io/nats.go/issues/1864">#1864</a>)</li>
<li>See full diff in <a href="https://github.com/nats-io/nats.go/compare/v1.41.2...v1.42.0">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/nats-io/nats.go&package-manager=go_modules&previous-version=1.41.2&new-version=1.42.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Patch:
diff --git a/docker/supermq-docker/addons/certs/docker-compose.yaml b/docker/supermq-docker/addons/certs/docker-compose.yaml
index b7aafcff2..f1fcac173 100644
--- a/docker/supermq-docker/addons/certs/docker-compose.yaml
+++ b/docker/supermq-docker/addons/certs/docker-compose.yaml
@@ -8,7 +8,8 @@
 
 networks:
   supermq-base-net:
-
+    name: supermq-base-net
+    external: true
 
 volumes:
   supermq-certs-db-volume:
diff --git a/docker/supermq-docker/addons/journal/docker-compose.yaml b/docker/supermq-docker/addons/journal/docker-compose.yaml
index 1b075d051..5b1c77dc4 100644
--- a/docker/supermq-docker/addons/journal/docker-compose.yaml
+++ b/docker/supermq-docker/addons/journal/docker-compose.yaml
@@ -10,7 +10,8 @@
 
 networks:
   supermq-base-net:
-
+    name: supermq-base-net
+    external: true
 
 volumes:
   supermq-journal-volume:
diff --git a/docker/supermq-docker/addons/prometheus/docker-compose.yaml b/docker/supermq-docker/addons/prometheus/docker-compose.yaml
index d3893b571..5abc0bf22 100644
--- a/docker/supermq-docker/addons/prometheus/docker-compose.yaml
+++ b/docker/supermq-docker/addons/prometheus/docker-compose.yaml
@@ -9,7 +9,8 @@
 
 networks:
   supermq-base-net:
-
+    name: supermq-base-net
+    external: true
 
 volumes:
   supermq-prometheus-volume:
diff --git a/docker/supermq-docker/addons/vault/docker-compose.yaml b/docker/supermq-docker/addons/vault/docker-compose.yaml
index 6993f3f11..1712ed283 100644
--- a/docker/supermq-docker/addons/vault/docker-compose.yaml
+++ b/docker/supermq-docker/addons/vault/docker-compose.yaml
@@ -10,7 +10,8 @@
 
 networks:
   supermq-base-net:
-
+    name: supermq-base-net
+    external: true
 
 volumes:
   supermq-vault-volume:
diff --git a/docker/supermq-docker/addons/vault/scripts/magistrala_clients_certs_issue.template.hcl b/docker/supermq-docker/addons/vault/scripts/supermq_clients_certs_issue.template.hcl
similarity index 100%
rename from docker/supermq-docker/addons/vault/scripts/magistrala_clients_certs_issue.template.hcl
rename to docker/supermq-docker/addons/vault/scripts/supermq_clients_certs_issue.template.hcl
diff --git a/go.mod b/go.mod
index 2e078cc70..6aa0de690 100644
--- a/go.mod
+++ b/go.mod
@@ -100,7 +100,7 @@ require (
 	github.com/moby/sys/user v0.3.0 // indirect
 	github.com/moby/term v0.5.0 // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
-	github.com/nats-io/nats.go v1.41.2
+	github.com/nats-io/nats.go v1.42.0
 	github.com/nats-io/nkeys v0.4.11 // indirect
 	github.com/nats-io/nuid v1.0.1 // indirect
 	github.com/oklog/ulid/v2 v2.1.0 // indirect
diff --git a/go.sum b/go.sum
index bd8c58776..2ee0ceab7 100644
--- a/go.sum
+++ b/go.sum
@@ -350,8 +350,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
 github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
 github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
-github.com/nats-io/nats.go v1.41.2 h1:5UkfLAtu/036s99AhFRlyNDI1Ieylb36qbGjJzHixos=
-github.com/nats-io/nats.go v1.41.2/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
+github.com/nats-io/nats.go v1.42.0 h1:ynIMupIOvf/ZWH/b2qda6WGKGNSjwOUutTpWRvAmhaM=
+github.com/nats-io/nats.go v1.42.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
 github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
 github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=
 github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
diff --git a/re/api/requests.go b/re/api/requests.go
index 010bdafdd..7d204bbd1 100644
--- a/re/api/requests.go
+++ b/re/api/requests.go
@@ -19,11 +19,13 @@ var (
 	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")
 )
 
 const (
 	maxLimitSize = 1000
 	MaxNameSize  = 1024
+	MaxTitleSize = 37
 
 	errInvalidMetric = "invalid metric[%d]: %w"
 )
@@ -192,6 +194,10 @@ type generateReportReq struct {
 }
 
 func (req generateReportReq) validate() error {
+	if len(req.Config.Title) > MaxTitleSize {
+		return errors.Wrap(apiutil.ErrValidation, errTitleSize)
+	}
+
 	switch req.action {
 	case re.ViewReport, re.DownloadReport:
 		return validateReportConfig(req.ReportConfig, true, true)
diff --git a/re/generator.go b/re/generator.go
index a21f6eb82..d4cdb49c2 100644
--- a/re/generator.go
+++ b/re/generator.go
@@ -19,7 +19,7 @@ import (
 	"github.com/johnfercher/maroto/pkg/props"
 )
 
-func generatePDFReport(reports []Report) ([]byte, error) {
+func generatePDFReport(title string, reports []Report) ([]byte, error) {
 	m := pdf.NewMaroto(consts.Portrait, consts.A4)
 	m.SetPageMargins(10, 15, 10)
 
@@ -41,7 +41,7 @@ func generatePDFReport(reports []Report) ([]byte, error) {
 			m.Col(2, func() {})
 
 			m.Col(8, func() {
-				m.Text("Magistrala IoT Report", props.Text{
+				m.Text(title, props.Text{
 					Size:  20,
 					Style: consts.Bold,
 					Color: primaryColor,
@@ -340,7 +340,7 @@ func formatValue(msg senml.Message) string {
 	}
 }
 
-func generateCSVReport(reports []Report) ([]byte, error) {
+func generateCSVReport(title string, reports []Report) ([]byte, error) {
 	var buf bytes.Buffer
 	writer := csv.NewWriter(&buf)
 
@@ -357,6 +357,13 @@ func generateCSVReport(reports []Report) ([]byte, error) {
 			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 {
diff --git a/re/postgres/rule.go b/re/postgres/rule.go
index 7e2d76393..12f5e3e04 100644
--- a/re/postgres/rule.go
+++ b/re/postgres/rule.go
@@ -213,7 +213,7 @@ func dbToReport(dto dbReport) (re.ReportConfig, error) {
 		}
 	}
 
-	var metrics []re.Metric
+	var metrics []re.ReqMetric
 	if dto.Metrics != nil {
 		if err := json.Unmarshal(dto.Metrics, &metrics); err != nil {
 			return re.ReportConfig{}, errors.Wrap(errors.ErrMalformedEntity, err)
diff --git a/re/reports.go b/re/reports.go
index faa93d60b..c1d4fd085 100644
--- a/re/reports.go
+++ b/re/reports.go
@@ -19,6 +19,7 @@ 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")
@@ -75,8 +76,9 @@ func (ac AggConfig) Validate() error {
 }
 
 type MetricConfig struct {
-	From string `json:"from,omitempty"` // Mandatory field
-	To   string `json:"to,omitempty"`   // Mandatory field
+	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
 
@@ -99,6 +101,11 @@ func (mc MetricConfig) Validate() error {
 	if _, err := reltime.Parse(mc.To); err != nil {
 		return errInvalidToTime
 	}
+
+	if mc.Title == "" {
+		return errTitleNotProvided
+	}
+
 	if err := mc.Aggregation.Validate(); err != nil {
 		return err
 	}
@@ -115,11 +122,20 @@ type Metric struct {
 	Format    string `json:"format,omitiempty"`    // Optional field
 }
 
-func (m Metric) Validate() error {
-	if m.ChannelID == "" {
+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 m.Name == "" {
+	if rm.Name == "" {
 		return errNameNotProvided
 	}
 	return nil
@@ -133,7 +149,7 @@ type ReportConfig struct {
 	Schedule    Schedule      `json:"schedule,omitempty"`
 	Config      *MetricConfig `json:"config,omitempty"`
 	Email       *EmailSetting `json:"email,omitempty"`
-	Metrics     []Metric      `json:"metrics,omitempty"`
+	Metrics     []ReqMetric   `json:"metrics,omitempty"`
 	Status      Status        `json:"status"`
 	CreatedAt   time.Time     `json:"created_at,omitempty"`
 	CreatedBy   string        `json:"created_by,omitempty"`
diff --git a/re/service.go b/re/service.go
index 04edb5351..d60207826 100644
--- a/re/service.go
+++ b/re/service.go
@@ -392,8 +392,33 @@ func (re *re) generateReport(ctx context.Context, cfg ReportConfig, action Repor
 		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)
@@ -443,7 +468,7 @@ func (re *re) generateReport(ctx context.Context, cfg ReportConfig, action Repor
 
 	switch {
 	case genReportFile != nil:
-		data, err := genReportFile(reports)
+		data, err := genReportFile(cfg.Config.Title, reports)
 		if err != nil {
 			return ReportPage{}, err
 		}
@@ -484,7 +509,7 @@ func (re *re) generateReport(ctx context.Context, cfg ReportConfig, action Repor
 	}
 }
 
-func generateFileFunc(action ReportAction, format Format) (func([]Report) ([]byte, error), error) {
+func generateFileFunc(action ReportAction, format Format) (func(string, []Report) ([]byte, error), error) {
 	switch action {
 	case DownloadReport, EmailReport:
 		switch format {


Test patch:
diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml
index 12f99a45e..77f8f45e4 100644
--- a/.github/workflows/tests.yaml
+++ b/.github/workflows/tests.yaml
@@ -39,9 +39,9 @@ jobs:
           make all -j $(nproc)
 
       - name: Run linters
-        uses: golangci/golangci-lint-action@v7
+        uses: golangci/golangci-lint-action@v8
         with:
-          version: v2.0.2
+          version: v2.1.6
           args: --config ./tools/config/.golangci.yaml
 
   run-tests:
diff --git a/re/api/endpoints_test.go b/re/api/endpoints_test.go
index e5a318849..78f7cc8be 100644
--- a/re/api/endpoints_test.go
+++ b/re/api/endpoints_test.go
@@ -61,16 +61,17 @@ var (
 		DomainID: domainID,
 		Schedule: schedule,
 		Status:   re.EnabledStatus,
-		Metrics: []re.Metric{
+		Metrics: []re.ReqMetric{
 			{
 				ChannelID: "channel1",
-				ClientID:  "channel2",
+				ClientIDs: []string{"client1"},
 				Name:      "metric_name",
 			},
 		},
 		Config: &re.MetricConfig{
 			From:        "now()-1h",
 			To:          "now()",
+			Title:       title,
 			Aggregation: re.AggConfig{AggType: re.AggregationAVG, Interval: "1h"},
 		},
 		Email: &re.EmailSetting{
@@ -78,6 +79,7 @@ var (
 			Subject: "Test Report",
 		},
 	}
+	title = "test_title"
 )
 
 type testRequest struct {