pgsql: Move notification to its module

master
Sida Chen 5 years ago
parent 921acb26fe
commit dfa07f6d86

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package pgsql package notification
import ( import (
"testing" "testing"
@ -22,6 +22,8 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/coreos/clair/database" "github.com/coreos/clair/database"
"github.com/coreos/clair/database/pgsql/page"
"github.com/coreos/clair/database/pgsql/testutil"
"github.com/coreos/clair/pkg/pagination" "github.com/coreos/clair/pkg/pagination"
) )
@ -38,6 +40,8 @@ type findVulnerabilityNotificationOut struct {
err string err string
} }
var testPaginationKey = pagination.Must(pagination.NewKey())
var findVulnerabilityNotificationTests = []struct { var findVulnerabilityNotificationTests = []struct {
title string title string
in findVulnerabilityNotificationIn in findVulnerabilityNotificationIn
@ -77,21 +81,21 @@ var findVulnerabilityNotificationTests = []struct {
}, },
out: findVulnerabilityNotificationOut{ out: findVulnerabilityNotificationOut{
&database.VulnerabilityNotificationWithVulnerable{ &database.VulnerabilityNotificationWithVulnerable{
NotificationHook: realNotification[1].NotificationHook, NotificationHook: testutil.RealNotification[1].NotificationHook,
Old: &database.PagedVulnerableAncestries{ Old: &database.PagedVulnerableAncestries{
Vulnerability: realVulnerability[2], Vulnerability: testutil.RealVulnerability[2],
Limit: 1, Limit: 1,
Affected: make(map[int]string), Affected: make(map[int]string),
Current: mustMarshalToken(testPaginationKey, Page{0}), Current: testutil.MustMarshalToken(testutil.TestPaginationKey, page.Page{0}),
Next: mustMarshalToken(testPaginationKey, Page{0}), Next: testutil.MustMarshalToken(testutil.TestPaginationKey, page.Page{0}),
End: true, End: true,
}, },
New: &database.PagedVulnerableAncestries{ New: &database.PagedVulnerableAncestries{
Vulnerability: realVulnerability[1], Vulnerability: testutil.RealVulnerability[1],
Limit: 1, Limit: 1,
Affected: map[int]string{3: "ancestry-3"}, Affected: map[int]string{3: "ancestry-3"},
Current: mustMarshalToken(testPaginationKey, Page{0}), Current: testutil.MustMarshalToken(testutil.TestPaginationKey, page.Page{0}),
Next: mustMarshalToken(testPaginationKey, Page{4}), Next: testutil.MustMarshalToken(testutil.TestPaginationKey, page.Page{4}),
End: false, End: false,
}, },
}, },
@ -100,32 +104,31 @@ var findVulnerabilityNotificationTests = []struct {
"", "",
}, },
}, },
{ {
title: "find existing notification of second page of new affected ancestry", title: "find existing notification of second page of new affected ancestry",
in: findVulnerabilityNotificationIn{ in: findVulnerabilityNotificationIn{
notificationName: "test", notificationName: "test",
pageSize: 1, pageSize: 1,
oldAffectedAncestryPage: pagination.FirstPageToken, oldAffectedAncestryPage: pagination.FirstPageToken,
newAffectedAncestryPage: mustMarshalToken(testPaginationKey, Page{4}), newAffectedAncestryPage: testutil.MustMarshalToken(testutil.TestPaginationKey, page.Page{4}),
}, },
out: findVulnerabilityNotificationOut{ out: findVulnerabilityNotificationOut{
&database.VulnerabilityNotificationWithVulnerable{ &database.VulnerabilityNotificationWithVulnerable{
NotificationHook: realNotification[1].NotificationHook, NotificationHook: testutil.RealNotification[1].NotificationHook,
Old: &database.PagedVulnerableAncestries{ Old: &database.PagedVulnerableAncestries{
Vulnerability: realVulnerability[2], Vulnerability: testutil.RealVulnerability[2],
Limit: 1, Limit: 1,
Affected: make(map[int]string), Affected: make(map[int]string),
Current: mustMarshalToken(testPaginationKey, Page{0}), Current: testutil.MustMarshalToken(testutil.TestPaginationKey, page.Page{0}),
Next: mustMarshalToken(testPaginationKey, Page{0}), Next: testutil.MustMarshalToken(testutil.TestPaginationKey, page.Page{0}),
End: true, End: true,
}, },
New: &database.PagedVulnerableAncestries{ New: &database.PagedVulnerableAncestries{
Vulnerability: realVulnerability[1], Vulnerability: testutil.RealVulnerability[1],
Limit: 1, Limit: 1,
Affected: map[int]string{4: "ancestry-4"}, Affected: map[int]string{4: "ancestry-4"},
Current: mustMarshalToken(testPaginationKey, Page{4}), Current: testutil.MustMarshalToken(testutil.TestPaginationKey, page.Page{4}),
Next: mustMarshalToken(testPaginationKey, Page{0}), Next: testutil.MustMarshalToken(testutil.TestPaginationKey, page.Page{0}),
End: true, End: true,
}, },
}, },
@ -137,12 +140,12 @@ var findVulnerabilityNotificationTests = []struct {
} }
func TestFindVulnerabilityNotification(t *testing.T) { func TestFindVulnerabilityNotification(t *testing.T) {
datastore, tx := openSessionForTest(t, "pagination", true) tx, cleanup := testutil.CreateTestTxWithFixtures(t, "pagination")
defer closeTest(t, datastore, tx) defer cleanup()
for _, test := range findVulnerabilityNotificationTests { for _, test := range findVulnerabilityNotificationTests {
t.Run(test.title, func(t *testing.T) { t.Run(test.title, func(t *testing.T) {
notification, ok, err := tx.FindVulnerabilityNotification(test.in.notificationName, test.in.pageSize, test.in.oldAffectedAncestryPage, test.in.newAffectedAncestryPage) notification, ok, err := FindVulnerabilityNotification(tx, test.in.notificationName, test.in.pageSize, test.in.oldAffectedAncestryPage, test.in.newAffectedAncestryPage, testutil.TestPaginationKey)
if test.out.err != "" { if test.out.err != "" {
require.EqualError(t, err, test.out.err) require.EqualError(t, err, test.out.err)
return return
@ -155,13 +158,14 @@ func TestFindVulnerabilityNotification(t *testing.T) {
} }
require.True(t, ok) require.True(t, ok)
assertVulnerabilityNotificationWithVulnerableEqual(t, testPaginationKey, test.out.notification, &notification) testutil.AssertVulnerabilityNotificationWithVulnerableEqual(t, testutil.TestPaginationKey, test.out.notification, &notification)
}) })
} }
} }
func TestInsertVulnerabilityNotifications(t *testing.T) { func TestInsertVulnerabilityNotifications(t *testing.T) {
datastore, tx := openSessionForTest(t, "InsertVulnerabilityNotifications", true) datastore, cleanup := testutil.CreateTestDBWithFixture(t, "InsertVulnerabilityNotifications")
defer cleanup()
n1 := database.VulnerabilityNotification{} n1 := database.VulnerabilityNotification{}
n3 := database.VulnerabilityNotification{ n3 := database.VulnerabilityNotification{
@ -187,34 +191,37 @@ func TestInsertVulnerabilityNotifications(t *testing.T) {
}, },
} }
tx, err := datastore.Begin()
require.Nil(t, err)
// invalid case // invalid case
err := tx.InsertVulnerabilityNotifications([]database.VulnerabilityNotification{n1}) err = InsertVulnerabilityNotifications(tx, []database.VulnerabilityNotification{n1})
assert.NotNil(t, err) require.NotNil(t, err)
// invalid case: unknown vulnerability // invalid case: unknown vulnerability
err = tx.InsertVulnerabilityNotifications([]database.VulnerabilityNotification{n3}) err = InsertVulnerabilityNotifications(tx, []database.VulnerabilityNotification{n3})
assert.NotNil(t, err) require.NotNil(t, err)
// invalid case: duplicated input notification // invalid case: duplicated input notification
err = tx.InsertVulnerabilityNotifications([]database.VulnerabilityNotification{n4, n4}) err = InsertVulnerabilityNotifications(tx, []database.VulnerabilityNotification{n4, n4})
assert.NotNil(t, err) require.NotNil(t, err)
tx = restartSession(t, datastore, tx, false) tx = testutil.RestartTransaction(datastore, tx, false)
// valid case // valid case
err = tx.InsertVulnerabilityNotifications([]database.VulnerabilityNotification{n4}) err = InsertVulnerabilityNotifications(tx, []database.VulnerabilityNotification{n4})
assert.Nil(t, err) require.Nil(t, err)
// invalid case: notification is already in database // invalid case: notification is already in database
err = tx.InsertVulnerabilityNotifications([]database.VulnerabilityNotification{n4}) err = InsertVulnerabilityNotifications(tx, []database.VulnerabilityNotification{n4})
assert.NotNil(t, err) require.NotNil(t, err)
closeTest(t, datastore, tx) require.Nil(t, tx.Rollback())
} }
func TestFindNewNotification(t *testing.T) { func TestFindNewNotification(t *testing.T) {
tx, cleanup := createTestPgSessionWithFixtures(t, "TestFindNewNotification") tx, cleanup := testutil.CreateTestTxWithFixtures(t, "TestFindNewNotification")
defer cleanup() defer cleanup()
noti, ok, err := tx.FindNewNotification(time.Now()) noti, ok, err := FindNewNotification(tx, time.Now())
if assert.Nil(t, err) && assert.True(t, ok) { if assert.Nil(t, err) && assert.True(t, ok) {
assert.Equal(t, "test", noti.Name) assert.Equal(t, "test", noti.Name)
assert.Equal(t, time.Time{}, noti.Notified) assert.Equal(t, time.Time{}, noti.Notified)
@ -223,13 +230,13 @@ func TestFindNewNotification(t *testing.T) {
} }
// can't find the notified // can't find the notified
assert.Nil(t, tx.MarkNotificationAsRead("test")) assert.Nil(t, MarkNotificationAsRead(tx, "test"))
// if the notified time is before // if the notified time is before
noti, ok, err = tx.FindNewNotification(time.Now().Add(-time.Duration(10 * time.Second))) noti, ok, err = FindNewNotification(tx, time.Now().Add(-time.Duration(10*time.Second)))
assert.Nil(t, err) assert.Nil(t, err)
assert.False(t, ok) assert.False(t, ok)
// can find the notified after a period of time // can find the notified after a period of time
noti, ok, err = tx.FindNewNotification(time.Now().Add(time.Duration(10 * time.Second))) noti, ok, err = FindNewNotification(tx, time.Now().Add(time.Duration(10*time.Second)))
if assert.Nil(t, err) && assert.True(t, ok) { if assert.Nil(t, err) && assert.True(t, ok) {
assert.Equal(t, "test", noti.Name) assert.Equal(t, "test", noti.Name)
assert.NotEqual(t, time.Time{}, noti.Notified) assert.NotEqual(t, time.Time{}, noti.Notified)
@ -237,37 +244,37 @@ func TestFindNewNotification(t *testing.T) {
assert.Equal(t, time.Time{}, noti.Deleted) assert.Equal(t, time.Time{}, noti.Deleted)
} }
assert.Nil(t, tx.DeleteNotification("test")) assert.Nil(t, DeleteNotification(tx, "test"))
// can't find in any time // can't find in any time
noti, ok, err = tx.FindNewNotification(time.Now().Add(-time.Duration(1000))) noti, ok, err = FindNewNotification(tx, time.Now().Add(-time.Duration(1000)))
assert.Nil(t, err) assert.Nil(t, err)
assert.False(t, ok) assert.False(t, ok)
noti, ok, err = tx.FindNewNotification(time.Now().Add(time.Duration(1000))) noti, ok, err = FindNewNotification(tx, time.Now().Add(time.Duration(1000)))
assert.Nil(t, err) assert.Nil(t, err)
assert.False(t, ok) assert.False(t, ok)
} }
func TestMarkNotificationAsRead(t *testing.T) { func TestMarkNotificationAsRead(t *testing.T) {
datastore, tx := openSessionForTest(t, "MarkNotificationAsRead", true) tx, cleanup := testutil.CreateTestTxWithFixtures(t, "MarkNotificationAsRead")
defer closeTest(t, datastore, tx) defer cleanup()
// invalid case: notification doesn't exist // invalid case: notification doesn't exist
assert.NotNil(t, tx.MarkNotificationAsRead("non-existing")) assert.NotNil(t, MarkNotificationAsRead(tx, "non-existing"))
// valid case // valid case
assert.Nil(t, tx.MarkNotificationAsRead("test")) assert.Nil(t, MarkNotificationAsRead(tx, "test"))
// valid case // valid case
assert.Nil(t, tx.MarkNotificationAsRead("test")) assert.Nil(t, MarkNotificationAsRead(tx, "test"))
} }
func TestDeleteNotification(t *testing.T) { func TestDeleteNotification(t *testing.T) {
datastore, tx := openSessionForTest(t, "DeleteNotification", true) tx, cleanup := testutil.CreateTestTxWithFixtures(t, "DeleteNotification")
defer closeTest(t, datastore, tx) defer cleanup()
// invalid case: notification doesn't exist // invalid case: notification doesn't exist
assert.NotNil(t, tx.DeleteNotification("non-existing")) assert.NotNil(t, DeleteNotification(tx, "non-existing"))
// valid case // valid case
assert.Nil(t, tx.DeleteNotification("test")) assert.Nil(t, DeleteNotification(tx, "test"))
// invalid case: notification is already deleted // invalid case: notification is already deleted
assert.NotNil(t, tx.DeleteNotification("test")) assert.NotNil(t, DeleteNotification(tx, "test"))
} }

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
package pgsql package notification
import ( import (
"database/sql" "database/sql"
@ -22,6 +22,8 @@ import (
"github.com/guregu/null/zero" "github.com/guregu/null/zero"
"github.com/coreos/clair/database" "github.com/coreos/clair/database"
"github.com/coreos/clair/database/pgsql/util"
"github.com/coreos/clair/database/pgsql/vulnerability"
"github.com/coreos/clair/pkg/commonerr" "github.com/coreos/clair/pkg/commonerr"
"github.com/coreos/clair/pkg/pagination" "github.com/coreos/clair/pkg/pagination"
) )
@ -54,26 +56,24 @@ const (
SELECT created_at, notified_at, deleted_at, old_vulnerability_id, new_vulnerability_id SELECT created_at, notified_at, deleted_at, old_vulnerability_id, new_vulnerability_id
FROM Vulnerability_Notification FROM Vulnerability_Notification
WHERE name = $1` WHERE name = $1`
searchNotificationVulnerableAncestry = `
SELECT DISTINCT ON (a.id)
a.id, a.name
FROM vulnerability_affected_namespaced_feature AS vanf,
ancestry_layer AS al, ancestry_feature AS af, ancestry AS a
WHERE vanf.vulnerability_id = $1
AND a.id >= $2
AND al.ancestry_id = a.id
AND al.id = af.ancestry_layer_id
AND af.namespaced_feature_id = vanf.namespaced_feature_id
ORDER BY a.id ASC
LIMIT $3;`
) )
func queryInsertNotifications(count int) string {
return util.QueryInsert(count,
"vulnerability_notification",
"name",
"created_at",
"old_vulnerability_id",
"new_vulnerability_id",
)
}
var ( var (
errNotificationNotFound = errors.New("requested notification is not found") errNotificationNotFound = errors.New("requested notification is not found")
errVulnerabilityNotFound = errors.New("vulnerability is not in database")
) )
func (tx *pgSession) InsertVulnerabilityNotifications(notifications []database.VulnerabilityNotification) error { func InsertVulnerabilityNotifications(tx *sql.Tx, notifications []database.VulnerabilityNotification) error {
if len(notifications) == 0 { if len(notifications) == 0 {
return nil return nil
} }
@ -122,26 +122,26 @@ func (tx *pgSession) InsertVulnerabilityNotifications(notifications []database.V
oldVulnIDs = append(oldVulnIDs, vulnID) oldVulnIDs = append(oldVulnIDs, vulnID)
} }
ids, err := tx.findNotDeletedVulnerabilityIDs(newVulnIDs) ids, err := vulnerability.FindNotDeletedVulnerabilityIDs(tx, newVulnIDs)
if err != nil { if err != nil {
return err return err
} }
for i, id := range ids { for i, id := range ids {
if !id.Valid { if !id.Valid {
return handleError("findNotDeletedVulnerabilityIDs", errVulnerabilityNotFound) return util.HandleError("findNotDeletedVulnerabilityIDs", errVulnerabilityNotFound)
} }
newVulnIDMap[newVulnIDs[i]] = id newVulnIDMap[newVulnIDs[i]] = id
} }
ids, err = tx.findLatestDeletedVulnerabilityIDs(oldVulnIDs) ids, err = vulnerability.FindLatestDeletedVulnerabilityIDs(tx, oldVulnIDs)
if err != nil { if err != nil {
return err return err
} }
for i, id := range ids { for i, id := range ids {
if !id.Valid { if !id.Valid {
return handleError("findLatestDeletedVulnerabilityIDs", errVulnerabilityNotFound) return util.HandleError("findLatestDeletedVulnerabilityIDs", errVulnerabilityNotFound)
} }
oldVulnIDMap[oldVulnIDs[i]] = id oldVulnIDMap[oldVulnIDs[i]] = id
} }
@ -178,13 +178,13 @@ func (tx *pgSession) InsertVulnerabilityNotifications(notifications []database.V
// multiple updaters, deadlock may happen. // multiple updaters, deadlock may happen.
_, err = tx.Exec(queryInsertNotifications(len(notifications)), keys...) _, err = tx.Exec(queryInsertNotifications(len(notifications)), keys...)
if err != nil { if err != nil {
return handleError("queryInsertNotifications", err) return util.HandleError("queryInsertNotifications", err)
} }
return nil return nil
} }
func (tx *pgSession) FindNewNotification(notifiedBefore time.Time) (database.NotificationHook, bool, error) { func FindNewNotification(tx *sql.Tx, notifiedBefore time.Time) (database.NotificationHook, bool, error) {
var ( var (
notification database.NotificationHook notification database.NotificationHook
created zero.Time created zero.Time
@ -197,7 +197,7 @@ func (tx *pgSession) FindNewNotification(notifiedBefore time.Time) (database.Not
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return notification, false, nil return notification, false, nil
} }
return notification, false, handleError("searchNotificationAvailable", err) return notification, false, util.HandleError("searchNotificationAvailable", err)
} }
notification.Created = created.Time notification.Created = created.Time
@ -207,71 +207,7 @@ func (tx *pgSession) FindNewNotification(notifiedBefore time.Time) (database.Not
return notification, true, nil return notification, true, nil
} }
func (tx *pgSession) findPagedVulnerableAncestries(vulnID int64, limit int, currentToken pagination.Token) (database.PagedVulnerableAncestries, error) { func FindVulnerabilityNotification(tx *sql.Tx, name string, limit int, oldPageToken pagination.Token, newPageToken pagination.Token, key pagination.Key) (
vulnPage := database.PagedVulnerableAncestries{Limit: limit}
currentPage := Page{0}
if currentToken != pagination.FirstPageToken {
if err := tx.key.UnmarshalToken(currentToken, &currentPage); err != nil {
return vulnPage, err
}
}
if err := tx.QueryRow(searchVulnerabilityByID, vulnID).Scan(
&vulnPage.Name,
&vulnPage.Description,
&vulnPage.Link,
&vulnPage.Severity,
&vulnPage.Metadata,
&vulnPage.Namespace.Name,
&vulnPage.Namespace.VersionFormat,
); err != nil {
return vulnPage, handleError("searchVulnerabilityByID", err)
}
// the last result is used for the next page's startID
rows, err := tx.Query(searchNotificationVulnerableAncestry, vulnID, currentPage.StartID, limit+1)
if err != nil {
return vulnPage, handleError("searchNotificationVulnerableAncestry", err)
}
defer rows.Close()
ancestries := []affectedAncestry{}
for rows.Next() {
var ancestry affectedAncestry
err := rows.Scan(&ancestry.id, &ancestry.name)
if err != nil {
return vulnPage, handleError("searchNotificationVulnerableAncestry", err)
}
ancestries = append(ancestries, ancestry)
}
lastIndex := 0
if len(ancestries)-1 < limit {
lastIndex = len(ancestries)
vulnPage.End = true
} else {
// Use the last ancestry's ID as the next page.
lastIndex = len(ancestries) - 1
vulnPage.Next, err = tx.key.MarshalToken(Page{ancestries[len(ancestries)-1].id})
if err != nil {
return vulnPage, err
}
}
vulnPage.Affected = map[int]string{}
for _, ancestry := range ancestries[0:lastIndex] {
vulnPage.Affected[int(ancestry.id)] = ancestry.name
}
vulnPage.Current, err = tx.key.MarshalToken(currentPage)
if err != nil {
return vulnPage, err
}
return vulnPage, nil
}
func (tx *pgSession) FindVulnerabilityNotification(name string, limit int, oldPageToken pagination.Token, newPageToken pagination.Token) (
database.VulnerabilityNotificationWithVulnerable, bool, error) { database.VulnerabilityNotificationWithVulnerable, bool, error) {
var ( var (
noti database.VulnerabilityNotificationWithVulnerable noti database.VulnerabilityNotificationWithVulnerable
@ -294,7 +230,7 @@ func (tx *pgSession) FindVulnerabilityNotification(name string, limit int, oldPa
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return noti, false, nil return noti, false, nil
} }
return noti, false, handleError("searchNotification", err) return noti, false, util.HandleError("searchNotification", err)
} }
if created.Valid { if created.Valid {
@ -310,7 +246,7 @@ func (tx *pgSession) FindVulnerabilityNotification(name string, limit int, oldPa
} }
if oldVulnID.Valid { if oldVulnID.Valid {
page, err := tx.findPagedVulnerableAncestries(oldVulnID.Int64, limit, oldPageToken) page, err := vulnerability.FindPagedVulnerableAncestries(tx, oldVulnID.Int64, limit, oldPageToken, key)
if err != nil { if err != nil {
return noti, false, err return noti, false, err
} }
@ -318,7 +254,7 @@ func (tx *pgSession) FindVulnerabilityNotification(name string, limit int, oldPa
} }
if newVulnID.Valid { if newVulnID.Valid {
page, err := tx.findPagedVulnerableAncestries(newVulnID.Int64, limit, newPageToken) page, err := vulnerability.FindPagedVulnerableAncestries(tx, newVulnID.Int64, limit, newPageToken, key)
if err != nil { if err != nil {
return noti, false, err return noti, false, err
} }
@ -328,44 +264,44 @@ func (tx *pgSession) FindVulnerabilityNotification(name string, limit int, oldPa
return noti, true, nil return noti, true, nil
} }
func (tx *pgSession) MarkNotificationAsRead(name string) error { func MarkNotificationAsRead(tx *sql.Tx, name string) error {
if name == "" { if name == "" {
return commonerr.NewBadRequestError("Empty notification name is not allowed") return commonerr.NewBadRequestError("Empty notification name is not allowed")
} }
r, err := tx.Exec(updatedNotificationAsRead, name) r, err := tx.Exec(updatedNotificationAsRead, name)
if err != nil { if err != nil {
return handleError("updatedNotificationAsRead", err) return util.HandleError("updatedNotificationAsRead", err)
} }
affected, err := r.RowsAffected() affected, err := r.RowsAffected()
if err != nil { if err != nil {
return handleError("updatedNotificationAsRead", err) return util.HandleError("updatedNotificationAsRead", err)
} }
if affected <= 0 { if affected <= 0 {
return handleError("updatedNotificationAsRead", errNotificationNotFound) return util.HandleError("updatedNotificationAsRead", errNotificationNotFound)
} }
return nil return nil
} }
func (tx *pgSession) DeleteNotification(name string) error { func DeleteNotification(tx *sql.Tx, name string) error {
if name == "" { if name == "" {
return commonerr.NewBadRequestError("Empty notification name is not allowed") return commonerr.NewBadRequestError("Empty notification name is not allowed")
} }
result, err := tx.Exec(removeNotification, name) result, err := tx.Exec(removeNotification, name)
if err != nil { if err != nil {
return handleError("removeNotification", err) return util.HandleError("removeNotification", err)
} }
affected, err := result.RowsAffected() affected, err := result.RowsAffected()
if err != nil { if err != nil {
return handleError("removeNotification", err) return util.HandleError("removeNotification", err)
} }
if affected <= 0 { if affected <= 0 {
return handleError("removeNotification", commonerr.ErrNotFound) return util.HandleError("removeNotification", commonerr.ErrNotFound)
} }
return nil return nil
Loading…
Cancel
Save