Repository: absmach/magistrala
Pull request #120 (2025-04-17 11:09:03)

MG-664 - Refactor update rule
<!-- Copyright (c) Abstract Machines
SPDX-License-Identifier: Apache-2.0 -->

<!--

Pull request title should be `MG-XXX - description` or `NOISSUE - description` where XXX is ID of the issue that this PR relate to.
Please review the [CONTRIBUTING.md](https://github.com/absmach/magistrala/blob/main/CONTRIBUTING.md) file for detailed contributing guidelines.

For Work In Progress Pull Requests, please use the Draft PR feature, see https://github.blog/2019-02-14-introducing-draft-pull-requests/ for further details.

For a timely review/response, please avoid force-pushing additional commits if your PR already received reviews or comments.

- Provide tests for your changes.
- Use descriptive commit messages.
- Comment your code where appropriate.
- Squash your commits
- Update any related documentation.
-->

# What type of PR is this?
This is a refactor pr
<!--This represents the type of PR you are submitting.

For example:
This is a bug fix because it fixes the following issue: #1234
This is a feature because it adds the following functionality: ...
This is a refactor because it changes the following functionality: ...
This is a documentation update because it updates the following documentation: ...
This is a dependency update because it updates the following dependencies: ...
This is an optimization because it improves the following functionality: ...
-->

## What does this do?
Refactors the update rule to allow updating individual parts only. We also split to add update rule schedule endpoint.
<!--
Please provide a brief description of what this PR is intended to do.
Include List any changes that modify/break current functionality.
-->

## Which issue(s) does this PR fix/relate to?

- Related Issue  #https://github.com/absmach/magistrala-ui/issues/664

## Have you included tests for your changes?
no
<!--If you have not included tests, please explain why.
For example:
Yes, I have included tests for my changes.
No, I have not included tests because I do not know how to.
-->

## Did you document any new/modified feature?
no
<!--If you have not included documentation, please explain why.
For example:
Yes, I have updated the documentation for the new feature.
No, I have not updated the documentation because I do not know how to.
-->

### Notes

<!--Please provide any additional information you feel is important.-->


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/absmach/magistrala/pull/120?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=absmach) Report
Attention: Patch coverage is `15.43624%` with `252 lines` in your changes missing coverage. Please review.
> Project coverage is 23.93%. Comparing base [(`73e3389`)](https://app.codecov.io/gh/absmach/magistrala/commit/73e338966fa6d4c41f97049a0093e53d7eff9bc1?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=absmach) to head [(`b2587df`)](https://app.codecov.io/gh/absmach/magistrala/commit/b2587df423e24c81680e5274b385c1fbb489e302?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=absmach).

| [Files with missing lines](https://app.codecov.io/gh/absmach/magistrala/pull/120?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=absmach) | Patch % | Lines |
|---|---|---|
| [re/postgres/repository.go](https://app.codecov.io/gh/absmach/magistrala/pull/120?src=pr&el=tree&filepath=re%2Fpostgres%2Frepository.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=absmach#diff-cmUvcG9zdGdyZXMvcmVwb3NpdG9yeS5nbw==) | 0.00% | [122 Missing :warning: ](https://app.codecov.io/gh/absmach/magistrala/pull/120?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=absmach) |
| [re/mocks/repository.go](https://app.codecov.io/gh/absmach/magistrala/pull/120?src=pr&el=tree&filepath=re%2Fmocks%2Frepository.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=absmach#diff-cmUvbW9ja3MvcmVwb3NpdG9yeS5nbw==) | 0.00% | [34 Missing :warning: ](https://app.codecov.io/gh/absmach/magistrala/pull/120?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=absmach) |
| [re/mocks/service.go](https://app.codecov.io/gh/absmach/magistrala/pull/120?src=pr&el=tree&filepath=re%2Fmocks%2Fservice.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=absmach#diff-cmUvbW9ja3Mvc2VydmljZS5nbw==) | 0.00% | [34 Missing :warning: ](https://app.codecov.io/gh/absmach/magistrala/pull/120?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=absmach) |
| [re/api/endpoints.go](https://app.codecov.io/gh/absmach/magistrala/pull/120?src=pr&el=tree&filepath=re%2Fapi%2Fendpoints.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=absmach#diff-cmUvYXBpL2VuZHBvaW50cy5nbw==) | 9.52% | [18 Missing and 1 partial :warning: ](https://app.codecov.io/gh/absmach/magistrala/pull/120?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=absmach) |
| [re/middleware/logging.go](https://app.codecov.io/gh/absmach/magistrala/pull/120?src=pr&el=tree&filepath=re%2Fmiddleware%2Flogging.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=absmach#diff-cmUvbWlkZGxld2FyZS9sb2dnaW5nLmdv) | 0.00% | [17 Missing :warning: ](https://app.codecov.io/gh/absmach/magistrala/pull/120?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=absmach) |
| [re/api/transport.go](https://app.codecov.io/gh/absmach/magistrala/pull/120?src=pr&el=tree&filepath=re%2Fapi%2Ftransport.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=absmach#diff-cmUvYXBpL3RyYW5zcG9ydC5nbw==) | 76.78% | [13 Missing :warning: ](https://app.codecov.io/gh/absmach/magistrala/pull/120?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=absmach) |
| [re/service.go](https://app.codecov.io/gh/absmach/magistrala/pull/120?src=pr&el=tree&filepath=re%2Fservice.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=absmach#diff-cmUvc2VydmljZS5nbw==) | 0.00% | [8 Missing :warning: ](https://app.codecov.io/gh/absmach/magistrala/pull/120?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=absmach) |
| [re/api/requests.go](https://app.codecov.io/gh/absmach/magistrala/pull/120?src=pr&el=tree&filepath=re%2Fapi%2Frequests.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=absmach#diff-cmUvYXBpL3JlcXVlc3RzLmdv) | 16.66% | [5 Missing :warning: ](https://app.codecov.io/gh/absmach/magistrala/pull/120?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=absmach) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #120      +/-   ##
==========================================
- Coverage   27.30%   23.93%   -3.37%     
==========================================
  Files         117       19      -98     
  Lines       16397     2043   -14354     
==========================================
- Hits         4477      489    -3988     
+ Misses      11616     1527   -10089     
+ Partials      304       27     -277     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/absmach/magistrala/pull/120?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=absmach).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=absmach).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>

Patch:
diff --git a/re/api/endpoints.go b/re/api/endpoints.go
index 8217e8669..43fd44485 100644
--- a/re/api/endpoints.go
+++ b/re/api/endpoints.go
@@ -70,6 +70,31 @@ func updateRuleEndpoint(s re.Service) endpoint.Endpoint {
 	}
 }
 
+func updateRuleScheduleEndpoint(s re.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.(updateRuleScheduleReq)
+		if err := req.validate(); err != nil {
+			return updateRuleRes{}, err
+		}
+
+		rule := re.Rule{
+			ID:       req.id,
+			Schedule: req.Schedule,
+		}
+
+		updatedRule, err := s.UpdateRuleSchedule(ctx, session, rule)
+		if err != nil {
+			return updateRuleRes{}, err
+		}
+		return updateRuleRes{Rule: updatedRule}, nil
+	}
+}
+
 func listRulesEndpoint(s re.Service) endpoint.Endpoint {
 	return func(ctx context.Context, request interface{}) (interface{}, error) {
 		session, ok := ctx.Value(api.SessionKey).(authn.Session)
@@ -86,12 +111,7 @@ func listRulesEndpoint(s re.Service) endpoint.Endpoint {
 			return rulesPageRes{}, nil
 		}
 		ret := rulesPageRes{
-			pageRes: pageRes{
-				Limit:  page.Limit,
-				Offset: page.Offset,
-				Total:  page.Total,
-			},
-			Rules: page.Rules,
+			Page: page,
 		}
 		return ret, nil
 	}
diff --git a/re/api/requests.go b/re/api/requests.go
index 23df70ed8..3b9de83c0 100644
--- a/re/api/requests.go
+++ b/re/api/requests.go
@@ -60,16 +60,26 @@ func (req updateRuleReq) validate() error {
 	if req.Rule.ID == "" {
 		return apiutil.ErrMissingID
 	}
-	if len(req.Rule.Logic.Value) == 0 {
-		return apiutil.ErrEmptyList
-	}
-	if len(req.Rule.Name) > api.MaxNameSize || req.Rule.Name == "" {
+	if len(req.Rule.Name) > api.MaxNameSize {
 		return apiutil.ErrNameSize
 	}
 
 	return nil
 }
 
+type updateRuleScheduleReq struct {
+	id       string
+	Schedule re.Schedule `json:"schedule,omitempty"`
+}
+
+func (req updateRuleScheduleReq) validate() error {
+	if req.id == "" {
+		return apiutil.ErrMissingID
+	}
+
+	return nil
+}
+
 type updateRuleStatusReq struct {
 	id string
 }
diff --git a/re/api/responses.go b/re/api/responses.go
index 71823aaff..d13753dc1 100644
--- a/re/api/responses.go
+++ b/re/api/responses.go
@@ -86,8 +86,7 @@ func (res viewRuleRes) Empty() bool {
 }
 
 type rulesPageRes struct {
-	pageRes
-	Rules []re.Rule `json:"rules"`
+	re.Page `json:",inline"`
 }
 
 func (res rulesPageRes) Code() int {
diff --git a/re/api/transport.go b/re/api/transport.go
index 357025d23..fbeda5fca 100644
--- a/re/api/transport.go
+++ b/re/api/transport.go
@@ -44,13 +44,6 @@ func MakeHandler(svc re.Service, authn mgauthn.Authentication, mux *chi.Mux, log
 				opts...,
 			), "create_rule").ServeHTTP)
 
-			r.Get("/{ruleID}", otelhttp.NewHandler(kithttp.NewServer(
-				viewRuleEndpoint(svc),
-				decodeViewRuleRequest,
-				api.EncodeResponse,
-				opts...,
-			), "view_rule").ServeHTTP)
-
 			r.Get("/", otelhttp.NewHandler(kithttp.NewServer(
 				listRulesEndpoint(svc),
 				decodeListRulesRequest,
@@ -58,33 +51,49 @@ func MakeHandler(svc re.Service, authn mgauthn.Authentication, mux *chi.Mux, log
 				opts...,
 			), "list_rules").ServeHTTP)
 
-			r.Patch("/{ruleID}", otelhttp.NewHandler(kithttp.NewServer(
-				updateRuleEndpoint(svc),
-				decodeUpdateRuleRequest,
-				api.EncodeResponse,
-				opts...,
-			), "update_rule").ServeHTTP)
-
-			r.Delete("/{ruleID}", otelhttp.NewHandler(kithttp.NewServer(
-				deleteRuleEndpoint(svc),
-				decodeDeleteRuleRequest,
-				api.EncodeResponse,
-				opts...,
-			), "delete_rule").ServeHTTP)
-
-			r.Post("/{ruleID}/enable", otelhttp.NewHandler(kithttp.NewServer(
-				enableRuleEndpoint(svc),
-				decodeUpdateRuleStatusRequest,
-				api.EncodeResponse,
-				opts...,
-			), "enable_rule").ServeHTTP)
-
-			r.Post("/{ruleID}/disable", otelhttp.NewHandler(kithttp.NewServer(
-				disableRuleEndpoint(svc),
-				decodeUpdateRuleStatusRequest,
-				api.EncodeResponse,
-				opts...,
-			), "disable_rule").ServeHTTP)
+			r.Route("/{ruleID}", func(r chi.Router) {
+				r.Get("/", otelhttp.NewHandler(kithttp.NewServer(
+					viewRuleEndpoint(svc),
+					decodeViewRuleRequest,
+					api.EncodeResponse,
+					opts...,
+				), "view_rule").ServeHTTP)
+
+				r.Patch("/", otelhttp.NewHandler(kithttp.NewServer(
+					updateRuleEndpoint(svc),
+					decodeUpdateRuleRequest,
+					api.EncodeResponse,
+					opts...,
+				), "update_rule").ServeHTTP)
+
+				r.Patch("/schedule", otelhttp.NewHandler(kithttp.NewServer(
+					updateRuleScheduleEndpoint(svc),
+					decodeUpdateRuleScheduleRequest,
+					api.EncodeResponse,
+					opts...,
+				), "update_rule_scheduler").ServeHTTP)
+
+				r.Delete("/", otelhttp.NewHandler(kithttp.NewServer(
+					deleteRuleEndpoint(svc),
+					decodeDeleteRuleRequest,
+					api.EncodeResponse,
+					opts...,
+				), "delete_rule").ServeHTTP)
+
+				r.Post("/enable", otelhttp.NewHandler(kithttp.NewServer(
+					enableRuleEndpoint(svc),
+					decodeUpdateRuleStatusRequest,
+					api.EncodeResponse,
+					opts...,
+				), "enable_rule").ServeHTTP)
+
+				r.Post("/disable", otelhttp.NewHandler(kithttp.NewServer(
+					disableRuleEndpoint(svc),
+					decodeUpdateRuleStatusRequest,
+					api.EncodeResponse,
+					opts...,
+				), "disable_rule").ServeHTTP)
+			})
 		})
 	})
 
@@ -100,7 +109,7 @@ func decodeAddRuleRequest(_ context.Context, r *http.Request) (interface{}, erro
 	}
 	var rule re.Rule
 	if err := json.NewDecoder(r.Body).Decode(&rule); err != nil {
-		return nil, errors.Wrap(err, apiutil.ErrValidation)
+		return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(errors.ErrMalformedEntity, err))
 	}
 	return addRuleReq{Rule: rule}, nil
 }
@@ -116,12 +125,27 @@ func decodeUpdateRuleRequest(_ context.Context, r *http.Request) (interface{}, e
 	}
 	var rule re.Rule
 	if err := json.NewDecoder(r.Body).Decode(&rule); err != nil {
-		return nil, err
+		return nil, errors.Wrap(apiutil.ErrValidation, errors.Wrap(errors.ErrMalformedEntity, err))
 	}
 	rule.ID = chi.URLParam(r, idKey)
 	return updateRuleReq{Rule: rule}, nil
 }
 
+func decodeUpdateRuleScheduleRequest(_ 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 := updateRuleScheduleReq{
+		id: chi.URLParam(r, idKey),
+	}
+	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 decodeUpdateRuleStatusRequest(_ context.Context, r *http.Request) (interface{}, error) {
 	req := updateRuleStatusReq{
 		id: chi.URLParam(r, idKey),
diff --git a/re/middleware/logging.go b/re/middleware/logging.go
index 37f102387..2b2cbefff 100644
--- a/re/middleware/logging.go
+++ b/re/middleware/logging.go
@@ -82,6 +82,26 @@ func (lm *loggingMiddleware) UpdateRule(ctx context.Context, session authn.Sessi
 	return lm.svc.UpdateRule(ctx, session, r)
 }
 
+func (lm *loggingMiddleware) UpdateRuleSchedule(ctx context.Context, session authn.Session, r re.Rule) (res re.Rule, err error) {
+	defer func(begin time.Time) {
+		args := []any{
+			slog.String("duration", time.Since(begin).String()),
+			slog.String("domain_id", session.DomainID),
+			slog.Group("rule",
+				slog.String("id", r.ID),
+				slog.Any("schedule", r.Schedule),
+			),
+		}
+		if err != nil {
+			args = append(args, slog.String("error", err.Error()))
+			lm.logger.Warn("Update rule schedule failed", args...)
+			return
+		}
+		lm.logger.Info("Update rule schedule completed successfully", args...)
+	}(time.Now())
+	return lm.svc.UpdateRuleSchedule(ctx, session, r)
+}
+
 func (lm *loggingMiddleware) ListRules(ctx context.Context, session authn.Session, pm re.PageMeta) (pg re.Page, err error) {
 	defer func(begin time.Time) {
 		args := []any{
diff --git a/re/mocks/repository.go b/re/mocks/repository.go
index d65547e26..d5a18355e 100644
--- a/re/mocks/repository.go
+++ b/re/mocks/repository.go
@@ -252,6 +252,61 @@ func (_c *Repository_UpdateRule_Call) RunAndReturn(run func(ctx context.Context,
 	return _c
 }
 
+// UpdateRuleSchedule provides a mock function for the type Repository
+func (_mock *Repository) UpdateRuleSchedule(ctx context.Context, r re.Rule) (re.Rule, error) {
+	ret := _mock.Called(ctx, r)
+
+	if len(ret) == 0 {
+		panic("no return value specified for UpdateRuleSchedule")
+	}
+
+	var r0 re.Rule
+	var r1 error
+	if returnFunc, ok := ret.Get(0).(func(context.Context, re.Rule) (re.Rule, error)); ok {
+		return returnFunc(ctx, r)
+	}
+	if returnFunc, ok := ret.Get(0).(func(context.Context, re.Rule) re.Rule); ok {
+		r0 = returnFunc(ctx, r)
+	} else {
+		r0 = ret.Get(0).(re.Rule)
+	}
+	if returnFunc, ok := ret.Get(1).(func(context.Context, re.Rule) error); ok {
+		r1 = returnFunc(ctx, r)
+	} else {
+		r1 = ret.Error(1)
+	}
+	return r0, r1
+}
+
+// Repository_UpdateRuleSchedule_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateRuleSchedule'
+type Repository_UpdateRuleSchedule_Call struct {
+	*mock.Call
+}
+
+// UpdateRuleSchedule is a helper method to define mock.On call
+//   - ctx
+//   - r
+func (_e *Repository_Expecter) UpdateRuleSchedule(ctx interface{}, r interface{}) *Repository_UpdateRuleSchedule_Call {
+	return &Repository_UpdateRuleSchedule_Call{Call: _e.mock.On("UpdateRuleSchedule", ctx, r)}
+}
+
+func (_c *Repository_UpdateRuleSchedule_Call) Run(run func(ctx context.Context, r re.Rule)) *Repository_UpdateRuleSchedule_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(context.Context), args[1].(re.Rule))
+	})
+	return _c
+}
+
+func (_c *Repository_UpdateRuleSchedule_Call) Return(rule re.Rule, err error) *Repository_UpdateRuleSchedule_Call {
+	_c.Call.Return(rule, err)
+	return _c
+}
+
+func (_c *Repository_UpdateRuleSchedule_Call) RunAndReturn(run func(ctx context.Context, r re.Rule) (re.Rule, error)) *Repository_UpdateRuleSchedule_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
 // UpdateRuleStatus provides a mock function for the type Repository
 func (_mock *Repository) UpdateRuleStatus(ctx context.Context, id string, status re.Status) (re.Rule, error) {
 	ret := _mock.Called(ctx, id, status)
diff --git a/re/mocks/service.go b/re/mocks/service.go
index 0924a12ad..5ec46a99f 100644
--- a/re/mocks/service.go
+++ b/re/mocks/service.go
@@ -550,6 +550,62 @@ func (_c *Service_UpdateRule_Call) RunAndReturn(run func(ctx context.Context, se
 	return _c
 }
 
+// UpdateRuleSchedule provides a mock function for the type Service
+func (_mock *Service) UpdateRuleSchedule(ctx context.Context, session authn.Session, r re.Rule) (re.Rule, error) {
+	ret := _mock.Called(ctx, session, r)
+
+	if len(ret) == 0 {
+		panic("no return value specified for UpdateRuleSchedule")
+	}
+
+	var r0 re.Rule
+	var r1 error
+	if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, re.Rule) (re.Rule, error)); ok {
+		return returnFunc(ctx, session, r)
+	}
+	if returnFunc, ok := ret.Get(0).(func(context.Context, authn.Session, re.Rule) re.Rule); ok {
+		r0 = returnFunc(ctx, session, r)
+	} else {
+		r0 = ret.Get(0).(re.Rule)
+	}
+	if returnFunc, ok := ret.Get(1).(func(context.Context, authn.Session, re.Rule) error); ok {
+		r1 = returnFunc(ctx, session, r)
+	} else {
+		r1 = ret.Error(1)
+	}
+	return r0, r1
+}
+
+// Service_UpdateRuleSchedule_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateRuleSchedule'
+type Service_UpdateRuleSchedule_Call struct {
+	*mock.Call
+}
+
+// UpdateRuleSchedule is a helper method to define mock.On call
+//   - ctx
+//   - session
+//   - r
+func (_e *Service_Expecter) UpdateRuleSchedule(ctx interface{}, session interface{}, r interface{}) *Service_UpdateRuleSchedule_Call {
+	return &Service_UpdateRuleSchedule_Call{Call: _e.mock.On("UpdateRuleSchedule", ctx, session, r)}
+}
+
+func (_c *Service_UpdateRuleSchedule_Call) Run(run func(ctx context.Context, session authn.Session, r re.Rule)) *Service_UpdateRuleSchedule_Call {
+	_c.Call.Run(func(args mock.Arguments) {
+		run(args[0].(context.Context), args[1].(authn.Session), args[2].(re.Rule))
+	})
+	return _c
+}
+
+func (_c *Service_UpdateRuleSchedule_Call) Return(rule re.Rule, err error) *Service_UpdateRuleSchedule_Call {
+	_c.Call.Return(rule, err)
+	return _c
+}
+
+func (_c *Service_UpdateRuleSchedule_Call) RunAndReturn(run func(ctx context.Context, session authn.Session, r re.Rule) (re.Rule, error)) *Service_UpdateRuleSchedule_Call {
+	_c.Call.Return(run)
+	return _c
+}
+
 // ViewRule provides a mock function for the type Service
 func (_mock *Service) ViewRule(ctx context.Context, session authn.Session, id string) (re.Rule, error) {
 	ret := _mock.Called(ctx, session, id)
diff --git a/re/postgres/repository.go b/re/postgres/repository.go
index a1821fda8..da88b443c 100644
--- a/re/postgres/repository.go
+++ b/re/postgres/repository.go
@@ -14,57 +14,6 @@ import (
 	"github.com/absmach/supermq/pkg/postgres"
 )
 
-// SQL Queries as Strings.
-const (
-	addRuleQuery = `
-		INSERT INTO rules (id, name, domain_id, metadata, input_channel, input_topic, logic_type, logic_value,
-			output_channel, output_topic, start_datetime, time, recurring, recurring_period, created_at, created_by, updated_at, updated_by, status)
-		VALUES (:id, :name, :domain_id, :metadata, :input_channel, :input_topic, :logic_type, :logic_value,
-			:output_channel, :output_topic, :start_datetime, :time, :recurring, :recurring_period, :created_at, :created_by, :updated_at, :updated_by, :status)
-		RETURNING id, name, domain_id, metadata, input_channel, input_topic, logic_type, logic_value,
-			output_channel, output_topic, start_datetime, time, recurring, recurring_period, created_at, created_by, updated_at, updated_by, status;
-	`
-
-	viewRuleQuery = `
-		SELECT id, name, domain_id, metadata, input_channel, input_topic, logic_type, logic_value, output_channel, 
-			output_topic, start_datetime, time, recurring, recurring_period, created_at, created_by, updated_at, updated_by, status
-		FROM rules
-		WHERE id = $1;
-	`
-
-	updateRuleQuery = `
-		UPDATE rules
-		SET name = :name, metadata = :metadata, input_channel = :input_channel, input_topic = :input_topic, logic_type = :logic_type, 
-			logic_value = :logic_value, output_channel = :output_channel, output_topic = :output_topic, 
-			start_datetime = :start_datetime, time = :time, recurring = :recurring, 
-			recurring_period = :recurring_period, updated_at = :updated_at, updated_by = :updated_by, status = :status
-		WHERE id = :id
-		RETURNING id, name, domain_id, metadata, input_channel, input_topic, logic_type, logic_value,
-			output_channel, output_topic, start_datetime, time, recurring, recurring_period, created_at, created_by, updated_at, updated_by, status;
-	`
-
-	removeRuleQuery = `
-		DELETE FROM rules
-		WHERE id = $1;
-	`
-
-	updateRuleStatusQuery = `
-		UPDATE rules
-		SET status = $2
-		WHERE id = $1
-		RETURNING id, name, domain_id, metadata, input_channel, input_topic, logic_type, logic_value,
-			output_channel, output_topic, start_datetime, time, recurring, recurring_period, created_at, created_by, updated_at, updated_by, status;  
-	`
-
-	listRulesQuery = `
-		SELECT id, name, domain_id, input_channel, input_topic, logic_type, logic_value, output_channel, 
-			output_topic, start_datetime, time, recurring, recurring_period, created_at, created_by, updated_at, updated_by, status
-		FROM rules r %s %s;
-	`
-
-	totalQuery = `SELECT COUNT(*) FROM rules r %s;`
-)
-
 type PostgresRepository struct {
 	DB postgres.Database
 }
@@ -74,99 +23,197 @@ func NewRepository(db postgres.Database) re.Repository {
 }
 
 func (repo *PostgresRepository) AddRule(ctx context.Context, r re.Rule) (re.Rule, error) {
+	q := `
+	INSERT INTO rules (id, name, domain_id, metadata, input_channel, input_topic, logic_type, logic_value,
+		output_channel, output_topic, start_datetime, time, recurring, recurring_period, created_at, created_by, updated_at, updated_by, status)
+	VALUES (:id, :name, :domain_id, :metadata, :input_channel, :input_topic, :logic_type, :logic_value,
+		:output_channel, :output_topic, :start_datetime, :time, :recurring, :recurring_period, :created_at, :created_by, :updated_at, :updated_by, :status)
+	RETURNING id, name, domain_id, metadata, input_channel, input_topic, logic_type, logic_value,
+		output_channel, output_topic, start_datetime, time, recurring, recurring_period, created_at, created_by, updated_at, updated_by, status;
+`
 	dbr, err := ruleToDb(r)
 	if err != nil {
-		return re.Rule{}, err
+		return re.Rule{}, errors.Wrap(repoerr.ErrCreateEntity, err)
 	}
-	row, err := repo.DB.NamedQueryContext(ctx, addRuleQuery, dbr)
+	row, err := repo.DB.NamedQueryContext(ctx, q, dbr)
 	if err != nil {
-		return re.Rule{}, err
+		return re.Rule{}, postgres.HandleError(repoerr.ErrCreateEntity, err)
 	}
 	defer row.Close()
 
 	var dbRule dbRule
 	if row.Next() {
 		if err := row.StructScan(&dbRule); err != nil {
-			return re.Rule{}, err
+			return re.Rule{}, errors.Wrap(repoerr.ErrCreateEntity, err)
 		}
 	}
 
 	rule, err := dbToRule(dbRule)
 	if err != nil {
-		return re.Rule{}, err
+		return re.Rule{}, errors.Wrap(repoerr.ErrCreateEntity, err)
 	}
 
 	return rule, nil
 }
 
 func (repo *PostgresRepository) ViewRule(ctx context.Context, id string) (re.Rule, error) {
-	row := repo.DB.QueryRowxContext(ctx, viewRuleQuery, id)
+	q := `
+		SELECT id, name, domain_id, metadata, input_channel, input_topic, logic_type, logic_value, output_channel, 
+			output_topic, start_datetime, time, recurring, recurring_period, created_at, created_by, updated_at, updated_by, status
+		FROM rules
+		WHERE id = $1;
+	`
+	row := repo.DB.QueryRowxContext(ctx, q, id)
 	if err := row.Err(); err != nil {
-		return re.Rule{}, err
+		return re.Rule{}, postgres.HandleError(repoerr.ErrViewEntity, err)
 	}
 	var dbr dbRule
 	if err := row.StructScan(&dbr); err != nil {
-		return re.Rule{}, err
+		return re.Rule{}, errors.Wrap(repoerr.ErrViewEntity, err)
 	}
 	ret, err := dbToRule(dbr)
 	if err != nil {
-		return re.Rule{}, err
+		return re.Rule{}, errors.Wrap(repoerr.ErrViewEntity, err)
 	}
 
 	return ret, nil
 }
 
 func (repo *PostgresRepository) UpdateRuleStatus(ctx context.Context, id string, status re.Status) (re.Rule, error) {
-	row := repo.DB.QueryRowxContext(ctx, updateRuleStatusQuery, id, status)
+	q := `
+		UPDATE rules
+		SET status = $2
+		WHERE id = $1
+		RETURNING id, name, domain_id, metadata, input_channel, input_topic, logic_type, logic_value,
+			output_channel, output_topic, start_datetime, time, recurring, recurring_period, created_at, created_by, updated_at, updated_by, status;  
+	`
+	row := repo.DB.QueryRowxContext(ctx, q, id, status)
 	if err := row.Err(); err != nil {
-		return re.Rule{}, err
+		return re.Rule{}, postgres.HandleError(repoerr.ErrUpdateEntity, err)
 	}
 
 	var dbr dbRule
 	if err := row.StructScan(&dbr); err != nil {
-		return re.Rule{}, err
+		return re.Rule{}, errors.Wrap(repoerr.ErrUpdateEntity, err)
 	}
 
 	rule, err := dbToRule(dbr)
 	if err != nil {
-		return re.Rule{}, err
+		return re.Rule{}, errors.Wrap(repoerr.ErrUpdateEntity, err)
 	}
 
 	return rule, nil
 }
 
 func (repo *PostgresRepository) UpdateRule(ctx context.Context, r re.Rule) (re.Rule, error) {
+	var query []string
+	var upq string
+	if r.Name != "" {
+		query = append(query, "name = :name,")
+	}
+	if r.Metadata != nil {
+		query = append(query, "metadata = :metadata,")
+	}
+	if r.InputChannel != "" {
+		query = append(query, "input_channel = :input_channel,")
+	}
+	if r.InputTopic != "" {
+		query = append(query, "input_topic = :input_topic,")
+	}
+	if r.OutputChannel != "" {
+		query = append(query, "output_channel = :output_channel,")
+	}
+	if r.OutputTopic != "" {
+		query = append(query, "output_topic = :output_topic,")
+	}
+	if r.Logic.Value != "" {
+		query = append(query, "logic_value = :logic_value,")
+		query = append(query, "logic_type = :logic_type,")
+	}
+
+	if len(query) > 0 {
+		upq = strings.Join(query, " ")
+	}
+
+	q := fmt.Sprintf(`
+		UPDATE rules
+		SET %s updated_at = :updated_at, updated_by = :updated_by WHERE id = :id
+		RETURNING id, name, domain_id, metadata, input_channel, input_topic, logic_type, logic_value,
+			output_channel, output_topic, start_datetime, time, recurring, recurring_period, created_at, created_by, updated_at, updated_by, status;
+	`, upq)
+
+	dbr, err := ruleToDb(r)
+	if err != nil {
+		return re.Rule{}, errors.Wrap(repoerr.ErrUpdateEntity, err)
+	}
+	row, err := repo.DB.NamedQueryContext(ctx, q, dbr)
+	if err != nil {
+		return re.Rule{}, postgres.HandleError(repoerr.ErrUpdateEntity, err)
+	}
+	defer row.Close()
+
+	var dbRule dbRule
+	if row.Next() {
+		if err := row.StructScan(&dbRule); err != nil {
+			return re.Rule{}, errors.Wrap(repoerr.ErrUpdateEntity, err)
+		}
+	}
+	rule, err := dbToRule(dbRule)
+	if err != nil {
+		return re.Rule{}, errors.Wrap(repoerr.ErrUpdateEntity, err)
+	}
+
+	return rule, nil
+}
+
+func (repo *PostgresRepository) UpdateRuleSchedule(ctx context.Context, r re.Rule) (re.Rule, error) {
+	q := `
+		UPDATE rules
+		SET start_datetime = :start_datetime, time = :time, recurring = :recurring, 
+			recurring_period = :recurring_period, updated_at = :updated_at, updated_by = :updated_by WHERE id = :id
+		RETURNING id, name, domain_id, metadata, input_channel, input_topic, logic_type, logic_value,
+			output_channel, output_topic, start_datetime, time, recurring, recurring_period, created_at, created_by, updated_at, updated_by, status;
+	`
 	dbr, err := ruleToDb(r)
 	if err != nil {
-		return re.Rule{}, err
+		return re.Rule{}, errors.Wrap(repoerr.ErrUpdateEntity, err)
 	}
-	row, err := repo.DB.NamedQueryContext(ctx, updateRuleQuery, dbr)
+	row, err := repo.DB.NamedQueryContext(ctx, q, dbr)
 	if err != nil {
-		return re.Rule{}, err
+		return re.Rule{}, postgres.HandleError(repoerr.ErrUpdateEntity, err)
 	}
 	defer row.Close()
 
 	var dbRule dbRule
 	if row.Next() {
 		if err := row.StructScan(&dbRule); err != nil {
-			return re.Rule{}, err
+			return re.Rule{}, errors.Wrap(repoerr.ErrUpdateEntity, err)
 		}
 	}
 	rule, err := dbToRule(dbRule)
 	if err != nil {
-		return re.Rule{}, err
+		return re.Rule{}, errors.Wrap(repoerr.ErrUpdateEntity, err)
 	}
 
 	return rule, nil
 }
 
 func (repo *PostgresRepository) RemoveRule(ctx context.Context, id string) error {
-	result, err := repo.DB.ExecContext(ctx, removeRuleQuery, id)
+	q := `
+	DELETE FROM rules
+	WHERE id = $1;
+`
+	result, err := repo.DB.ExecContext(ctx, q, id)
 	if err != nil {
-		return err
+		return postgres.HandleError(repoerr.ErrRemoveEntity, err)
 	}
 
-	if _, err := result.RowsAffected(); err != nil {
+	rowsAffected, err := result.RowsAffected()
+	if err != nil {
+		return errors.Wrap(repoerr.ErrRemoveEntity, err)
+	}
+
+	if rowsAffected == 0 {
 		return repoerr.ErrNotFound
 	}
 
@@ -182,7 +229,11 @@ func (repo *PostgresRepository) ListRules(ctx context.Context, pm re.PageMeta) (
 		pgData += " OFFSET :offset"
 	}
 	pq := pageQuery(pm)
-	q := fmt.Sprintf(listRulesQuery, pq, pgData)
+	q := fmt.Sprintf(`
+		SELECT id, name, domain_id, input_channel, input_topic, logic_type, logic_value, output_channel, 
+			output_topic, start_datetime, time, recurring, recurring_period, created_at, created_by, updated_at, updated_by, status
+		FROM rules r %s %s;
+	`, pq, pgData)
 	rows, err := repo.DB.NamedQueryContext(ctx, q, pm)
 	if err != nil {
 		return re.Page{}, err
@@ -202,16 +253,17 @@ func (repo *PostgresRepository) ListRules(ctx context.Context, pm re.PageMeta) (
 		rules = append(rules, ret)
 	}
 
-	cq := fmt.Sprintf(totalQuery, pq)
+	cq := fmt.Sprintf(`SELECT COUNT(*) FROM rules r %s;`, pq)
 
 	total, err := postgres.Total(ctx, repo.DB, cq, pm)
 	if err != nil {
 		return re.Page{}, errors.Wrap(repoerr.ErrViewEntity, err)
 	}
-	pm.Total = total
 	ret := re.Page{
-		PageMeta: pm,
-		Rules:    rules,
+		Total:  total,
+		Offset: pm.Offset,
+		Limit:  pm.Limit,
+		Rules:  rules,
 	}
 
 	return ret, nil
diff --git a/re/service.go b/re/service.go
index 0d66cce1a..de1b738e8 100644
--- a/re/service.go
+++ b/re/service.go
@@ -56,6 +56,7 @@ type Repository interface {
 	AddRule(ctx context.Context, r Rule) (Rule, error)
 	ViewRule(ctx context.Context, id string) (Rule, error)
 	UpdateRule(ctx context.Context, r Rule) (Rule, error)
+	UpdateRuleSchedule(ctx context.Context, r Rule) (Rule, error)
 	RemoveRule(ctx context.Context, id string) error
 	UpdateRuleStatus(ctx context.Context, id string, status Status) (Rule, error)
 	ListRules(ctx context.Context, pm PageMeta) (Page, error)
@@ -79,8 +80,10 @@ type PageMeta struct {
 }
 
 type Page struct {
-	PageMeta
-	Rules []Rule `json:"rules"`
+	Offset uint64 `json:"offset"`
+	Limit  uint64 `json:"limit"`
+	Total  uint64 `json:"total"`
+	Rules  []Rule `json:"rules"`
 }
 
 type Service interface {
@@ -88,6 +91,7 @@ type Service interface {
 	AddRule(ctx context.Context, session authn.Session, r Rule) (Rule, error)
 	ViewRule(ctx context.Context, session authn.Session, id string) (Rule, error)
 	UpdateRule(ctx context.Context, session authn.Session, r Rule) (Rule, error)
+	UpdateRuleSchedule(ctx context.Context, session authn.Session, r Rule) (Rule, error)
 	ListRules(ctx context.Context, session authn.Session, pm PageMeta) (Page, error)
 	RemoveRule(ctx context.Context, session authn.Session, id string) error
 	EnableRule(ctx context.Context, session authn.Session, id string) (Rule, error)
@@ -164,6 +168,17 @@ func (re *re) UpdateRule(ctx context.Context, session authn.Session, r Rule) (Ru
 	return rule, nil
 }
 
+func (re *re) UpdateRuleSchedule(ctx context.Context, session authn.Session, r Rule) (Rule, error) {
+	r.UpdatedAt = time.Now()
+	r.UpdatedBy = session.UserID
+	rule, err := re.repo.UpdateRuleSchedule(ctx, r)
+	if err != nil {
+		return Rule{}, errors.Wrap(svcerr.ErrUpdateEntity, err)
+	}
+
+	return rule, nil
+}
+
 func (re *re) ListRules(ctx context.Context, session authn.Session, pm PageMeta) (Page, error) {
 	pm.Domain = session.DomainID
 	page, err := re.repo.ListRules(ctx, pm)


Test patch:
diff --git a/re/api/endpoints_test.go b/re/api/endpoints_test.go
index 1b310b8f8..387cc8d02 100644
--- a/re/api/endpoints_test.go
+++ b/re/api/endpoints_test.go
@@ -349,9 +349,7 @@ func TestListRulesEndpoint(t *testing.T) {
 			token:    validToken,
 			status:   http.StatusOK,
 			listRulesResponse: re.Page{
-				PageMeta: re.PageMeta{
-					Total: 1,
-				},
+				Total: 1,
 				Rules: []re.Rule{rule},
 			},
 			err: nil,
@@ -376,9 +374,8 @@ func TestListRulesEndpoint(t *testing.T) {
 			domainID: domainID,
 			token:    validToken,
 			listRulesResponse: re.Page{
-				PageMeta: re.PageMeta{
-					Total: 1,
-				},
+				Total: 1,
+
 				Rules: []re.Rule{rule},
 			},
 			query:  "offset=1",
@@ -398,9 +395,8 @@ func TestListRulesEndpoint(t *testing.T) {
 			domainID: domainID,
 			token:    validToken,
 			listRulesResponse: re.Page{
-				PageMeta: re.PageMeta{
-					Total: 1,
-				},
+				Total: 1,
+
 				Rules: []re.Rule{rule},
 			},
 			query:  "limit=1",
@@ -436,9 +432,7 @@ func TestListRulesEndpoint(t *testing.T) {
 			domainID: domainID,
 			token:    validToken,
 			listRulesResponse: re.Page{
-				PageMeta: re.PageMeta{
-					Total: 1,
-				},
+				Total: 1,
 				Rules: []re.Rule{rule},
 			},
 			query:  "input_channel=input.channel",
@@ -458,9 +452,7 @@ func TestListRulesEndpoint(t *testing.T) {
 			domainID: domainID,
 			token:    validToken,
 			listRulesResponse: re.Page{
-				PageMeta: re.PageMeta{
-					Total: 1,
-				},
+				Total: 1,
 				Rules: []re.Rule{rule},
 			},
 			query:  "status=enabled",
@@ -488,9 +480,7 @@ func TestListRulesEndpoint(t *testing.T) {
 			domainID: domainID,
 			token:    validToken,
 			listRulesResponse: re.Page{
-				PageMeta: re.PageMeta{
-					Total: 1,
-				},
+				Total: 1,
 				Rules: []re.Rule{rule},
 			},
 			query:  "output_channel=output.channel",
@@ -553,14 +543,6 @@ func TestUpdateRulesEndpoint(t *testing.T) {
 		},
 	}
 
-	invalidReq := re.Rule{
-		ID:   rule.ID,
-		Name: rule.Name,
-		Metadata: map[string]any{
-			"name": "test",
-		},
-	}
-
 	cases := []struct {
 		desc        string
 		token       string
@@ -618,17 +600,6 @@ func TestUpdateRulesEndpoint(t *testing.T) {
 			status:      http.StatusBadRequest,
 			err:         apiutil.ErrMissingDomainID,
 		},
-		{
-			desc:        "update rule with empty logic",
-			token:       validToken,
-			domainID:    domainID,
-			id:          rule.ID,
-			updateReq:   invalidReq,
-			contentType: contentType,
-			svcResp:     rule,
-			status:      http.StatusBadRequest,
-			err:         apiutil.ErrEmptyList,
-		},
 		{
 			desc:     "update rule with name that is too long",
 			token:    validToken,
diff --git a/re/service_test.go b/re/service_test.go
index 730878ca6..a8f693e93 100644
--- a/re/service_test.go
+++ b/re/service_test.go
@@ -321,12 +321,10 @@ func TestListRules(t *testing.T) {
 			},
 			pageMeta: re.PageMeta{},
 			res: re.Page{
-				PageMeta: re.PageMeta{
-					Total:  uint64(numRules),
-					Offset: 0,
-					Limit:  10,
-				},
-				Rules: rules[0:10],
+				Total:  uint64(numRules),
+				Offset: 0,
+				Limit:  10,
+				Rules:  rules[0:10],
 			},
 			err: nil,
 		},
@@ -340,12 +338,10 @@ func TestListRules(t *testing.T) {
 				Limit: 100,
 			},
 			res: re.Page{
-				PageMeta: re.PageMeta{
-					Total:  uint64(numRules),
-					Offset: 0,
-					Limit:  100,
-				},
-				Rules: rules[0:numRules],
+				Total:  uint64(numRules),
+				Offset: 0,
+				Limit:  100,
+				Rules:  rules[0:numRules],
 			},
 			err: nil,
 		},
@@ -360,12 +356,10 @@ func TestListRules(t *testing.T) {
 				Limit:  10,
 			},
 			res: re.Page{
-				PageMeta: re.PageMeta{
-					Total:  uint64(numRules),
-					Offset: 20,
-					Limit:  10,
-				},
-				Rules: rules[20:30],
+				Total:  uint64(numRules),
+				Offset: 20,
+				Limit:  10,
+				Rules:  rules[20:30],
 			},
 			err: nil,
 		},