From ef46e643afdd44aaeee967fa66a1a4299eae8cf1 Mon Sep 17 00:00:00 2001 From: Denis Arh Date: Fri, 6 Jul 2018 10:22:19 +0200 Subject: [PATCH] Add (suspended|archived|deleted)_at fields & logic everywhere Took 1 minute --- sam/channel.go | 25 +++++++--- sam/channel.structs.go | 7 +++ .../schema/mysql/20180704080000.base.up.sql | 12 +++++ sam/organisation.go | 22 ++++++-- sam/organisation.structs.go | 7 +++ sam/team.go | 23 +++++++-- sam/team.structs.go | 7 +++ sam/user.go | 50 ++++++++++++++++++- sam/user.structs.go | 4 ++ 9 files changed, 139 insertions(+), 18 deletions(-) diff --git a/sam/channel.go b/sam/channel.go index 0bdb7f5c5..ef2233e68 100644 --- a/sam/channel.go +++ b/sam/channel.go @@ -9,6 +9,11 @@ import ( var _ = errors.Wrap +const ( + sqlChannelScope = "deleted_at IS NULL AND archived_at IS NULL" + sqlChannelSelect = "SELECT * FROM channels WHERE " + sqlChannelScope +) + func (*Channel) Edit(r *channelEditRequest) (interface{}, error) { db, err := factory.Database.Get() if err != nil { @@ -19,6 +24,7 @@ func (*Channel) Edit(r *channelEditRequest) (interface{}, error) { // @todo: channel name change message/log entry // @todo: permission check if user can edit channel // @todo: permission check if user can add channel + // @todo: make sure archived & deleted entries can not be edited c := Channel{}.new().SetID(r.id).SetName(r.name).SetTopic(r.topic) if c.GetID() > 0 { @@ -37,12 +43,14 @@ func (*Channel) Remove(r *channelRemoveRequest) (interface{}, error) { return nil, err } - // @todo: don't actually delete the channel (mark as deleted, history becomes unavailable) + // @todo: make history unavailable // @todo: notify users that channel has been removed (remove from web UI) // @todo: permissions check if user cah remove channel + stmt := "UPDATE channels SET deleted_at = NOW() WHERE id = ? AND deleted_at IS NULL" + return nil, func() error { - _, err := db.Exec("delete from channel where id=?", r.id) + _, err := db.Exec(stmt, r.id) return err }() } @@ -56,7 +64,7 @@ func (*Channel) Read(r *channelReadRequest) (interface{}, error) { // @todo: permission check if user can read channel c := Channel{}.new() - return c, db.Get(c, "select * from channel where id=?", r.id) + return c, db.Get(c, sqlChannelSelect+" AND id = ?", r.id) } func (*Channel) Search(r *channelSearchRequest) (interface{}, error) { @@ -69,7 +77,7 @@ func (*Channel) Search(r *channelSearchRequest) (interface{}, error) { // @todo: actual searching not just a full select res := make([]Channel, 0) - err = db.Select(&res, "select * from channel order by name asc") + err = db.Select(&res, sqlChannelSelect+" ORDER BY name ASC") return res, err } @@ -79,12 +87,15 @@ func (*Channel) Archive(r *channelArchiveRequest) (interface{}, error) { return nil, err } - // @todo: don't actually delete the channel (mark as archived, history stays available) // @todo: notify users that channel has been archived (last message - archival, disable new messages) - // @todo: permissions check if user cah archive channel + // @todo: permissions check if user can archive channel + + stmt := fmt.Sprintf( + "UPDATE channels SET archived_at = NOW() WHERE %s AND id = ?", + sqlChannelScope) return nil, func() error { - _, err = db.Exec("delete from channel where id=?", r.id) + _, err = db.Exec(stmt, r.id) return err }() } diff --git a/sam/channel.structs.go b/sam/channel.structs.go index b89aa913f..a76bede29 100644 --- a/sam/channel.structs.go +++ b/sam/channel.structs.go @@ -1,11 +1,18 @@ package sam +import ( + "time" +) + // Channels type Channel struct { ID uint64 Name string Topic string + ArchivedAt *time.Time `json:",omitempty"` + DeletedAt *time.Time `json:",omitempty"` + changed []string } diff --git a/sam/db/schema/mysql/20180704080000.base.up.sql b/sam/db/schema/mysql/20180704080000.base.up.sql index dedf7d411..02fc0aa3c 100644 --- a/sam/db/schema/mysql/20180704080000.base.up.sql +++ b/sam/db/schema/mysql/20180704080000.base.up.sql @@ -4,6 +4,9 @@ CREATE TABLE organisations ( fqn TEXT NOT NULL, -- fully qualified name of the organisation label TEXT NOT NULL, -- display name of the organisation + archived_at DATETIME NULL, + deleted_at DATETIME NULL, -- organisation soft delete + PRIMARY KEY (id) ); @@ -13,6 +16,9 @@ CREATE TABLE teams ( label TEXT NOT NULL, -- display name of the team handle TEXT NOT NULL, -- team handle string + archived_at DATETIME NULL, + deleted_at DATETIME NULL, -- team soft delete + PRIMARY KEY (id) ); @@ -22,6 +28,9 @@ CREATE TABLE channels ( label TEXT NOT NULL, -- display name of the channel meta JSON NOT NULL, + archived_at DATETIME NULL, + deleted_at DATETIME NULL, -- channel soft delete + PRIMARY KEY (id) ); @@ -35,6 +44,9 @@ CREATE TABLE users ( rel_organisation BIGINT UNSIGNED NOT NULL REFERENCES organisation(id), + suspended_at DATETIME NULL, + deleted_at DATETIME NULL, -- user soft delete + PRIMARY KEY (id) ); diff --git a/sam/organisation.go b/sam/organisation.go index 11f78bb2f..de9f37480 100644 --- a/sam/organisation.go +++ b/sam/organisation.go @@ -1,12 +1,18 @@ package sam import ( + "fmt" "github.com/pkg/errors" "github.com/titpetric/factory" ) var _ = errors.Wrap +const ( + sqlOrganisationScope = "deleted_at IS NULL AND archived_at IS NULL" + sqlOrganisationSelect = "SELECT * FROM organisations WHERE " + sqlOrganisationScope +) + func (*Organisation) Edit(r *organisationEditRequest) (interface{}, error) { db, err := factory.Database.Get() if err != nil { @@ -14,6 +20,7 @@ func (*Organisation) Edit(r *organisationEditRequest) (interface{}, error) { } // @todo: permission check if user can add/edit organisation + // @todo: make sure archived & deleted entries can not be edited o := Organisation{}.new().SetID(r.id).SetName(r.name) if o.GetID() > 0 { @@ -32,8 +39,10 @@ func (*Organisation) Remove(r *organisationRemoveRequest) (interface{}, error) { // @todo: permission check // @todo: don't actually delete organisation?! + stmt := "UPDATE organisationss SET deleted_at = NOW() WHERE deleted_at IS NULL AND id = ?" + return nil, func() error { - _, err := db.Exec("delete from organisation where id=?", r.id) + _, err := db.Exec(stmt, r.id) return err }() } @@ -47,7 +56,7 @@ func (*Organisation) Read(r *organisationReadRequest) (interface{}, error) { // @todo: permissions check o := Organisation{}.new() - return o, db.Get(o, "select * from organisation where id=?", r.id) + return o, db.Get(o, sqlOrganisationSelect+" AND id = ?", r.id) } func (*Organisation) Search(r *organisationSearchRequest) (interface{}, error) { @@ -60,7 +69,7 @@ func (*Organisation) Search(r *organisationSearchRequest) (interface{}, error) { // @todo: actual search for org res := make([]Organisation, 0) - err = db.Select(&res, "select * from organisation order by name asc") + err = db.Select(&res, sqlOrganisationSelect+" WHERE label LIKE = ? ORDER BY label ASC", r.query+"%") return res, err } @@ -71,10 +80,13 @@ func (*Organisation) Archive(r *organisationArchiveRequest) (interface{}, error) } // @todo: permission check - // @todo: don't actually delete organisation?! + + stmt := fmt.Sprintf( + "UPDATE organisation SET archived_at = NOW() WHERE %s AND id = ?", + sqlChannelScope) return nil, func() error { - _, err := db.Exec("delete from organisation where id=?", r.id) + _, err := db.Exec(stmt, r.id) return err }() } diff --git a/sam/organisation.structs.go b/sam/organisation.structs.go index 20d214129..370ece26b 100644 --- a/sam/organisation.structs.go +++ b/sam/organisation.structs.go @@ -1,10 +1,17 @@ package sam +import ( + "time" +) + // Organisations type Organisation struct { ID uint64 Name string + ArchivedAt *time.Time + DeletedAt *time.Time + changed []string } diff --git a/sam/team.go b/sam/team.go index 2e14a5649..d044ef386 100644 --- a/sam/team.go +++ b/sam/team.go @@ -1,18 +1,27 @@ package sam import ( + "fmt" "github.com/pkg/errors" "github.com/titpetric/factory" ) var _ = errors.Wrap +const ( + sqlTeamScope = "deleted_at IS NULL AND archived_at IS NULL" + sqlTeamSelect = "SELECT * FROM teams WHERE " + sqlTeamScope +) + func (*Team) Edit(r *teamEditRequest) (interface{}, error) { db, err := factory.Database.Get() if err != nil { return nil, err } + // @todo: permission check if user can add/edit the team + // @todo: make sure archived & deleted entries can not be edited + t := Team{}.new() t.SetID(r.id).SetName(r.name).SetMemberIDs(r.members) if t.GetID() > 0 { @@ -28,8 +37,10 @@ func (*Team) Remove(r *teamRemoveRequest) (interface{}, error) { return nil, err } + stmt := "UPDATE teams SET deleted_at = NOW() WHERE deleted_at IS NULL AND id = ?" + return nil, func() error { - _, err := db.Exec("delete from team where id=?", r.id) + _, err := db.Exec(stmt, r.id) return err }() } @@ -41,7 +52,7 @@ func (*Team) Read(r *teamReadRequest) (interface{}, error) { } t := Team{}.new() - return t, db.Get(t, "select * from team where id=?", r.id) + return t, db.Get(t, sqlTeamSelect+" AND id = ?", r.id) } func (*Team) Search(r *teamSearchRequest) (interface{}, error) { @@ -51,7 +62,7 @@ func (*Team) Search(r *teamSearchRequest) (interface{}, error) { } res := make([]Team, 0) - err = db.Select(&res, "select * from team order by name asc") + err = db.Select(&res, sqlTeamSelect+" ORDER BY name ASC") return res, err } @@ -61,8 +72,12 @@ func (*Team) Archive(r *teamArchiveRequest) (interface{}, error) { return nil, err } + stmt := fmt.Sprintf( + "UPDATE teams SET archived_at = NOW() WHERE %s AND id = ?", + sqlTeamScope) + return nil, func() error { - _, err := db.Exec("delete from team where id=?", r.id) + _, err := db.Exec(stmt, r.id) return err }() } diff --git a/sam/team.structs.go b/sam/team.structs.go index 2b24e1a78..82c209cab 100644 --- a/sam/team.structs.go +++ b/sam/team.structs.go @@ -1,5 +1,9 @@ package sam +import ( + "time" +) + // Teams type Team struct { ID uint64 @@ -7,6 +11,9 @@ type Team struct { MemberIDs []uint64 `json:"-"` Members []User `json:",omitempty"` + ArchivedAt *time.Time `json:",omitempty"` + DeletedAt *time.Time `json:",omitempty"` + changed []string } diff --git a/sam/user.go b/sam/user.go index 801364e45..1512cb489 100644 --- a/sam/user.go +++ b/sam/user.go @@ -1,12 +1,28 @@ package sam import ( + "fmt" "github.com/pkg/errors" "github.com/titpetric/factory" ) var _ = errors.Wrap +const ( + sqlUserScope = "suspended_at IS NULL AND archived_at IS NULL" + sqlUserSelect = "SELECT * FROM users WHERE " + sqlUserScope +) + +func (*User) Read(r *teamReadRequest) (interface{}, error) { + db, err := factory.Database.Get() + if err != nil { + return nil, err + } + + t := User{}.new() + return t, db.Get(t, sqlUserSelect+" AND id = ?", r.id) +} + // User lookup & login func (*User) Login(r *userLoginRequest) (interface{}, error) { db, err := factory.Database.Get() @@ -15,7 +31,7 @@ func (*User) Login(r *userLoginRequest) (interface{}, error) { } u := &User{} - if err != db.Get(u, "SELECT * FROM users WHERE username = ?", r.username) { + if err != db.Get(u, sqlUserSelect+" AND username = ?", r.username) { return nil, err } @@ -39,9 +55,39 @@ func (*User) Search(r *userSearchRequest) (interface{}, error) { uu := []*User{} - if err != db.Get(uu, "SELECT * FROM users WHERE username LIKE ?", r.query+"%") { + if err != db.Get(uu, sqlUserSelect+" AND username LIKE ?", r.query+"%") { return nil, err } return uu, nil } + +func (*User) Remove(r *teamRemoveRequest) (interface{}, error) { + db, err := factory.Database.Get() + if err != nil { + return nil, err + } + + stmt := "UPDATE users SET deleted_at = NOW() WHERE deleted_at IS NULL AND id = ?" + + return nil, func() error { + _, err := db.Exec(stmt, r.id) + return err + }() +} + +func (*User) Archive(r *teamArchiveRequest) (interface{}, error) { + db, err := factory.Database.Get() + if err != nil { + return nil, err + } + + stmt := fmt.Sprintf( + "UPDATE users SET archived_at = NOW() WHERE %s AND id = ?", + sqlUserScope) + + return nil, func() error { + _, err := db.Exec(stmt, r.id) + return err + }() +} diff --git a/sam/user.structs.go b/sam/user.structs.go index 430b4a9fb..7678803c9 100644 --- a/sam/user.structs.go +++ b/sam/user.structs.go @@ -2,6 +2,7 @@ package sam import ( "golang.org/x/crypto/bcrypt" + "time" ) // Users @@ -10,6 +11,9 @@ type User struct { Username string Password []byte `json:"-"` + SuspendedAt *time.Time `json:",omitempty"` + DeletedAt *time.Time `json:",omitempty"` + changed []string }