From 34e2f1bcdd1aebb9ec0d91d5aced4e3711b7ca5c Mon Sep 17 00:00:00 2001 From: Fabi <38692350+fgerschwiler@users.noreply.github.com> Date: Tue, 14 Apr 2020 18:20:20 +0200 Subject: [PATCH] feat: View (#27) * feat: eventstore repository * fix: remove gorm * version * feat: pkg * feat: eventstore without eventstore-lib * rename files * gnueg * fix: add object * fix: global model * feat: add global view functions * feat(eventstore): sdk * fix(eventstore): search query * fix(eventstore): rename app to eventstore * delete empty test * remove unused func * merge master * fix(eventstore): tests * fix(models): delete unused struct * feat(eventstore): implemented push events * feat(eventstore): overwrite context data * feat(types): SQL-config * feat(eventstore): options to overwrite editor * fix: use global sql config * fix: changes from mr * fix: add some tests * Update internal/eventstore/models/field.go Co-Authored-By: livio-a * fix(eventstore): code quality * fix: try tests * fix: query tests * fix: use prepare funcs * fix: go mod * fix: go tests * fix: better error func testing * fix: merge master * fix: changes for mr * fix: change value to interface * fix: searchmethods * fix: check if value is string on equal ignore case Co-authored-by: adlerhurst Co-authored-by: livio-a --- go.mod | 1 + go.sum | 13 ++ internal/model/enum.go | 1 + internal/model/search_method.go | 24 +- internal/view/config.go | 9 + internal/view/db_mock_test.go | 353 ++++++++++++++++++++++++++++ internal/view/failed_events.go | 91 ++++++++ internal/view/query.go | 104 +++++++++ internal/view/query_test.go | 156 +++++++++++++ internal/view/requests.go | 72 ++++++ internal/view/requests_test.go | 392 ++++++++++++++++++++++++++++++++ internal/view/sequence.go | 58 +++++ 12 files changed, 1259 insertions(+), 15 deletions(-) create mode 100644 internal/view/config.go create mode 100644 internal/view/db_mock_test.go create mode 100644 internal/view/failed_events.go create mode 100644 internal/view/query.go create mode 100644 internal/view/query_test.go create mode 100644 internal/view/requests.go create mode 100644 internal/view/requests_test.go create mode 100644 internal/view/sequence.go diff --git a/go.mod b/go.mod index 59e501bbd3..ffc157ae0e 100644 --- a/go.mod +++ b/go.mod @@ -26,6 +26,7 @@ require ( github.com/huandu/xstrings v1.3.0 // indirect github.com/imdario/mergo v0.3.9 // indirect github.com/jackc/pgconn v1.5.0 // indirect + github.com/jinzhu/gorm v1.9.12 github.com/lib/pq v1.3.0 github.com/mitchellh/copystructure v1.0.0 // indirect github.com/mitchellh/reflectwalk v1.0.1 // indirect diff --git a/go.sum b/go.sum index 0492b6d3f0..b41ac92d1d 100644 --- a/go.sum +++ b/go.sum @@ -72,6 +72,7 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/deckarep/golang-set v1.7.1 h1:SCQV0S6gTtp6itiFrTqI+pfmJ4LN85S1YzhDf9rTHJQ= github.com/deckarep/golang-set v1.7.1/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ= +github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -79,6 +80,7 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0 h1:EQciDnbrYxy13PgWoY8AqoxGiPrp github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.3.0 h1:Y2J74o+yAfcD8jpqtkLnUqRo+yshLr4eR1WPYGX0cic= github.com/envoyproxy/protoc-gen-validate v0.3.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= @@ -86,6 +88,7 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= @@ -93,6 +96,7 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -185,6 +189,11 @@ github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9 github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jinzhu/gorm v1.9.12 h1:Drgk1clyWT9t9ERbzHza6Mj/8FY/CqMyVzOiHviMo6Q= +github.com/jinzhu/gorm v1.9.12/go.mod h1:vhTjlKSJUTWNtcbQtrMBFCxy7eXTzeCAzfL5fBZT/Qs= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc= @@ -205,12 +214,14 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU= github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY= @@ -266,6 +277,7 @@ go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/ go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190506204251-e1dfcc566284/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -275,6 +287,7 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 h1:3zb4D3T4G8jdExgVU/95+vQXfpEPiMdCaZgmGVxjNHM= golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200403201458-baeed622b8d8 h1:fpnn/HnJONpIu6hkXi1u/7rR0NzilgWr4T0JmWkEitk= diff --git a/internal/model/enum.go b/internal/model/enum.go index d737a2649c..84c5e2cf03 100644 --- a/internal/model/enum.go +++ b/internal/model/enum.go @@ -1,5 +1,6 @@ package model +//Deprecated: Enum is useless, better use normal enums, because we rarely need string value type Enum interface { String() string } diff --git a/internal/model/search_method.go b/internal/model/search_method.go index 0eaccbdb1b..a59323a086 100644 --- a/internal/model/search_method.go +++ b/internal/model/search_method.go @@ -1,26 +1,20 @@ package model -// code below could be generated -type SearchMethod Enum - -var methods = []string{"Equals", "StartsWith", "Contains"} - -type method int32 - -func (s method) String() string { - return methods[s] -} +type SearchMethod int32 const ( - Equals method = iota - StartsWith - Contains + SEARCHMETHOD_EQUALS SearchMethod = iota + SEARCHMETHOD_STARTS_WITH + SEARCHMETHOD_CONTAINS + SEARCHMETHOD_EQUALS_IGNORE_CASE + SEARCHMETHOD_STARTS_WITH_IGNORE_CASE + SEARCHMETHOD_CONTAINS_IGNORE_CASE ) func SearchMethodToInt(s SearchMethod) int32 { - return int32(s.(method)) + return int32(s) } func SearchMethodFromInt(index int32) SearchMethod { - return method(index) + return SearchMethod(index) } diff --git a/internal/view/config.go b/internal/view/config.go new file mode 100644 index 0000000000..d4f91893f2 --- /dev/null +++ b/internal/view/config.go @@ -0,0 +1,9 @@ +package view + +import ( + "github.com/caos/zitadel/internal/config/types" +) + +type ViewConfig struct { + SQL *types.SQL +} diff --git a/internal/view/db_mock_test.go b/internal/view/db_mock_test.go new file mode 100644 index 0000000000..5db0e11768 --- /dev/null +++ b/internal/view/db_mock_test.go @@ -0,0 +1,353 @@ +package view + +import ( + "fmt" + "github.com/DATA-DOG/go-sqlmock" + "github.com/caos/zitadel/internal/model" + "github.com/jinzhu/gorm" + "testing" +) + +var ( + expectedGetByID = `SELECT \* FROM "%s" WHERE \(%s = \$1\) LIMIT 1` + expectedGetByQuery = `SELECT \* FROM "%s" WHERE \(LOWER\(%s\) %s LOWER\(\$1\)\) LIMIT 1` + expectedGetByQueryCaseSensitive = `SELECT \* FROM "%s" WHERE \(%s %s \$1\) LIMIT 1` + expectedSave = `UPDATE "%s" SET "test" = \$1 WHERE "%s"."%s" = \$2` + expectedRemove = `DELETE FROM "%s" WHERE \(%s = \$1\)` + expectedSearch = `SELECT \* FROM "%s" OFFSET 0` + expectedSearchCount = `SELECT count\(\*\) FROM "%s"` + expectedSearchLimit = `SELECT \* FROM "%s" LIMIT %v OFFSET 0` + expectedSearchLimitCount = `SELECT count\(\*\) FROM "%s"` + expectedSearchOffset = `SELECT \* FROM "%s" OFFSET %v` + expectedSearchOffsetCount = `SELECT count\(\*\) FROM "%s"` + expectedSearchSorting = `SELECT \* FROM "%s" ORDER BY %s %s OFFSET 0` + expectedSearchSortingCount = `SELECT count\(\*\) FROM "%s"` + expectedSearchQuery = `SELECT \* FROM "%s" WHERE \(LOWER\(%s\) %s LOWER\(\$1\)\) OFFSET 0` + expectedSearchQueryCount = `SELECT count\(\*\) FROM "%s" WHERE \(LOWER\(%s\) %s LOWER\(\$1\)\)` + expectedSearchQueryAllParams = `SELECT \* FROM "%s" WHERE \(LOWER\(%s\) %s LOWER\(\$1\)\) ORDER BY %s %s LIMIT %v OFFSET %v` + expectedSearchQueryAllParamCount = `SELECT count\(\*\) FROM "%s" WHERE \(LOWER\(%s\) %s LOWER\(\$1\)\)` +) + +type TestSearchRequest struct { + limit uint64 + offset uint64 + sortingColumn ColumnKey + asc bool + queries []SearchQuery +} + +func (req TestSearchRequest) GetLimit() uint64 { + return req.limit +} + +func (req TestSearchRequest) GetOffset() uint64 { + return req.offset +} + +func (req TestSearchRequest) GetSortingColumn() ColumnKey { + return req.sortingColumn +} + +func (req TestSearchRequest) GetAsc() bool { + return req.asc +} + +func (req TestSearchRequest) GetQueries() []SearchQuery { + return req.queries +} + +type TestSearchQuery struct { + key TestSearchKey + method model.SearchMethod + value string +} + +func (req TestSearchQuery) GetKey() ColumnKey { + return req.key +} + +func (req TestSearchQuery) GetMethod() model.SearchMethod { + return req.method +} + +func (req TestSearchQuery) GetValue() interface{} { + return req.value +} + +type TestSearchKey int32 + +const ( + TestSearchKey_UNDEFINED TestSearchKey = iota + TestSearchKey_TEST + TestSearchKey_ID +) + +func (key TestSearchKey) ToColumnName() string { + switch TestSearchKey(key) { + case TestSearchKey_TEST: + return "test" + case TestSearchKey_ID: + return "id" + default: + return "" + } +} + +type Test struct { + ID string `json:"-" gorm:"column:id;primary_key"` + Test string `json:"test" gorm:"column:test"` +} + +type dbMock struct { + db *gorm.DB + mock sqlmock.Sqlmock +} + +func (db *dbMock) close() { + db.db.Close() +} + +func mockDB(t *testing.T) *dbMock { + mockDB := dbMock{} + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("error occured while creating stub db %v", err) + } + + mockDB.mock = mock + mockDB.db, err = gorm.Open("postgres", db) + if err != nil { + t.Fatalf("error occured while connecting to stub db: %v", err) + } + + mockDB.mock.MatchExpectationsInOrder(true) + + return &mockDB +} + +func (db *dbMock) expectBegin(err error) *dbMock { + if err != nil { + db.mock.ExpectBegin().WillReturnError(err) + } else { + db.mock.ExpectBegin() + } + return db +} + +func (db *dbMock) expectCommit(err error) *dbMock { + if err != nil { + db.mock.ExpectCommit().WillReturnError(err) + } else { + db.mock.ExpectCommit() + } + return db +} + +func (db *dbMock) expectRollback(err error) *dbMock { + if err != nil { + db.mock.ExpectRollback().WillReturnError(err) + } else { + db.mock.ExpectRollback() + } + return db +} + +func (db *dbMock) expectGetByID(table, key, value string) *dbMock { + query := fmt.Sprintf(expectedGetByID, table, key) + db.mock.ExpectQuery(query). + WithArgs(value). + WillReturnRows(sqlmock.NewRows([]string{key}). + AddRow(key)) + + return db +} + +func (db *dbMock) expectGetByIDErr(table, key, value string, err error) *dbMock { + query := fmt.Sprintf(expectedGetByID, table, key) + db.mock.ExpectQuery(query). + WithArgs(value). + WillReturnError(err) + + return db +} + +func (db *dbMock) expectGetByQuery(table, key, method, value string) *dbMock { + query := fmt.Sprintf(expectedGetByQuery, table, key, method) + db.mock.ExpectQuery(query). + WithArgs(value). + WillReturnRows(sqlmock.NewRows([]string{key}). + AddRow(key)) + + return db +} + +func (db *dbMock) expectGetByQueryCaseSensitive(table, key, method, value string) *dbMock { + query := fmt.Sprintf(expectedGetByQueryCaseSensitive, table, key, method) + db.mock.ExpectQuery(query). + WithArgs(value). + WillReturnRows(sqlmock.NewRows([]string{key}). + AddRow(key)) + + return db +} + +func (db *dbMock) expectGetByQueryErr(table, key, method, value string, err error) *dbMock { + query := fmt.Sprintf(expectedGetByQuery, table, key, method) + db.mock.ExpectQuery(query). + WithArgs(value). + WillReturnError(err) + + return db +} + +func (db *dbMock) expectSave(table string, object Test) *dbMock { + query := fmt.Sprintf(expectedSave, table, table, "id") + db.mock.ExpectExec(query). + WithArgs(object.Test, object.ID). + WillReturnResult(sqlmock.NewResult(1, 1)) + + return db +} + +func (db *dbMock) expectSaveErr(table string, object Test, err error) *dbMock { + query := fmt.Sprintf(expectedSave, table, table, "id") + db.mock.ExpectExec(query). + WithArgs(object.Test, object.ID). + WillReturnError(err) + + return db +} + +func (db *dbMock) expectRemove(table, key, value string) *dbMock { + query := fmt.Sprintf(expectedRemove, table, key) + db.mock.ExpectExec(query). + WithArgs(value). + WillReturnResult(sqlmock.NewResult(1, 1)) + + return db +} + +func (db *dbMock) expectRemoveErr(table, key, value string, err error) *dbMock { + query := fmt.Sprintf(expectedRemove, table, key) + db.mock.ExpectExec(query). + WithArgs(value). + WillReturnError(err) + + return db +} + +func (db *dbMock) expectGetSearchRequestNoParams(table string, resultAmount, total int) *dbMock { + query := fmt.Sprintf(expectedSearch, table) + queryCount := fmt.Sprintf(expectedSearchCount, table) + + rows := sqlmock.NewRows([]string{"id"}) + for i := 0; i < resultAmount; i++ { + rows.AddRow(fmt.Sprintf("hodor-%d", i)) + } + + db.mock.ExpectQuery(queryCount). + WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(total)) + db.mock.ExpectQuery(query). + WillReturnRows(rows) + return db +} + +func (db *dbMock) expectGetSearchRequestWithLimit(table string, limit, resultAmount, total int) *dbMock { + query := fmt.Sprintf(expectedSearchLimit, table, limit) + queryCount := fmt.Sprintf(expectedSearchLimitCount, table) + + rows := sqlmock.NewRows([]string{"id"}) + for i := 0; i < resultAmount; i++ { + rows.AddRow(fmt.Sprintf("hodor-%d", i)) + } + + db.mock.ExpectQuery(queryCount). + WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(total)) + db.mock.ExpectQuery(query). + WillReturnRows(rows) + return db +} + +func (db *dbMock) expectGetSearchRequestWithOffset(table string, offset, resultAmount, total int) *dbMock { + query := fmt.Sprintf(expectedSearchOffset, table, offset) + queryCount := fmt.Sprintf(expectedSearchOffsetCount, table) + + rows := sqlmock.NewRows([]string{"id"}) + for i := 0; i < resultAmount; i++ { + rows.AddRow(fmt.Sprintf("hodor-%d", i)) + } + + db.mock.ExpectQuery(queryCount). + WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(total)) + db.mock.ExpectQuery(query). + WillReturnRows(rows) + return db +} + +func (db *dbMock) expectGetSearchRequestWithSorting(table, sorting string, sortingColumn ColumnKey, resultAmount, total int) *dbMock { + query := fmt.Sprintf(expectedSearchSorting, table, sortingColumn.ToColumnName(), sorting) + queryCount := fmt.Sprintf(expectedSearchSortingCount, table) + + rows := sqlmock.NewRows([]string{"id"}) + for i := 0; i < resultAmount; i++ { + rows.AddRow(fmt.Sprintf("hodor-%d", i)) + } + + db.mock.ExpectQuery(queryCount). + WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(total)) + db.mock.ExpectQuery(query). + WillReturnRows(rows) + return db +} + +func (db *dbMock) expectGetSearchRequestWithSearchQuery(table, key, method, value string, resultAmount, total int) *dbMock { + query := fmt.Sprintf(expectedSearchQuery, table, key, method) + queryCount := fmt.Sprintf(expectedSearchQueryCount, table, key, method) + + rows := sqlmock.NewRows([]string{"id"}) + for i := 0; i < resultAmount; i++ { + rows.AddRow(fmt.Sprintf("hodor-%d", i)) + } + + db.mock.ExpectQuery(queryCount). + WithArgs(value). + WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(total)) + db.mock.ExpectQuery(query). + WithArgs(value). + WillReturnRows(rows) + return db +} + +func (db *dbMock) expectGetSearchRequestWithAllParams(table, key, method, value, sorting string, sortingColumn ColumnKey, limit, offset, resultAmount, total int) *dbMock { + query := fmt.Sprintf(expectedSearchQueryAllParams, table, key, method, sortingColumn.ToColumnName(), sorting, limit, offset) + queryCount := fmt.Sprintf(expectedSearchQueryAllParamCount, table, key, method) + + rows := sqlmock.NewRows([]string{"id"}) + for i := 0; i < resultAmount; i++ { + rows.AddRow(fmt.Sprintf("hodor-%d", i)) + } + + db.mock.ExpectQuery(queryCount). + WithArgs(value). + WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(total)) + db.mock.ExpectQuery(query). + WithArgs(value). + WillReturnRows(rows) + return db +} + +func (db *dbMock) expectGetSearchRequestErr(table string, resultAmount, total int, err error) *dbMock { + query := fmt.Sprintf(expectedSearch, table) + queryCount := fmt.Sprintf(expectedSearchCount, table) + + rows := sqlmock.NewRows([]string{"id"}) + for i := 0; i < resultAmount; i++ { + rows.AddRow(fmt.Sprintf("hodor-%d", i)) + } + + db.mock.ExpectQuery(queryCount). + WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(total)) + db.mock.ExpectQuery(query). + WillReturnError(err) + return db +} diff --git a/internal/view/failed_events.go b/internal/view/failed_events.go new file mode 100644 index 0000000000..a3cf43797b --- /dev/null +++ b/internal/view/failed_events.go @@ -0,0 +1,91 @@ +package view + +import ( + "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/model" + "github.com/jinzhu/gorm" +) + +const ( + errViewNameKey = "view_name" + errFailedSeqKey = "failed_sequence" +) + +type FailedEvent struct { + ViewName string `gorm:"column:view_name;primary_key"` + FailedSequnce uint64 `gorm:"column:failed_sequence;primary_key` + FailureCount uint64 `gorm:"column:failure_count` + ErrMsg uint64 `gorm:"column:err_msg` +} + +type FailedEventSearchQuery struct { + Key FailedEventSearchKey + Method model.SearchMethod + Value interface{} +} + +func (req FailedEventSearchQuery) GetKey() ColumnKey { + return failedEventSearchKey(req.Key) +} + +func (req FailedEventSearchQuery) GetMethod() model.SearchMethod { + return req.Method +} + +func (req FailedEventSearchQuery) GetValue() interface{} { + return req.Value +} + +type FailedEventSearchKey int32 + +const ( + FAILEDEVENTKEY_UNDEFINED FailedEventSearchKey = iota + FAILEDEVENTKEY_VIEW_NAME + FAILEDEVENTKEY_FAILED_SEQUENCE +) + +type failedEventSearchKey FailedEventSearchKey + +func (key failedEventSearchKey) ToColumnName() string { + switch FailedEventSearchKey(key) { + case FAILEDEVENTKEY_VIEW_NAME: + return "view_name" + case FAILEDEVENTKEY_FAILED_SEQUENCE: + return "failed_sequence" + default: + return "" + } +} + +func SaveFailedEvent(db *gorm.DB, table string, failedEvent *FailedEvent) error { + save := PrepareSave(table) + err := save(db, failedEvent) + + if err != nil { + return errors.ThrowInternal(err, "VIEW-5kOhP", "unable to updated failed events") + } + return nil +} + +func LatestFailedEvent(db *gorm.DB, table, viewName string, sequence uint64) (*FailedEvent, error) { + failedEvent := new(FailedEvent) + queries := []SearchQuery{ + FailedEventSearchQuery{Key: FAILEDEVENTKEY_VIEW_NAME, Method: model.SEARCHMETHOD_EQUALS_IGNORE_CASE, Value: viewName}, + FailedEventSearchQuery{Key: FAILEDEVENTKEY_FAILED_SEQUENCE, Method: model.SEARCHMETHOD_EQUALS_IGNORE_CASE, Value: sequence}, + } + query := PrepareGetByQuery(table, queries...) + err := query(db, sequence) + + if err == nil { + return failedEvent, nil + } + + if gorm.IsRecordNotFoundError(err) { + failedEvent.ViewName = viewName + failedEvent.FailedSequnce = sequence + failedEvent.FailureCount = 0 + return failedEvent, nil + } + return nil, errors.ThrowInternalf(err, "VIEW-9LyCB", "unable to get failed events of %s", viewName) + +} diff --git a/internal/view/query.go b/internal/view/query.go new file mode 100644 index 0000000000..17550bbca3 --- /dev/null +++ b/internal/view/query.go @@ -0,0 +1,104 @@ +package view + +import ( + "fmt" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/model" + "github.com/jinzhu/gorm" +) + +type SearchRequest interface { + GetLimit() uint64 + GetOffset() uint64 + GetSortingColumn() ColumnKey + GetAsc() bool + GetQueries() []SearchQuery +} + +type SearchQuery interface { + GetKey() ColumnKey + GetMethod() model.SearchMethod + GetValue() interface{} +} + +type ColumnKey interface { + ToColumnName() string +} + +func PrepareSearchQuery(table string, request SearchRequest) func(db *gorm.DB, res interface{}) (int, error) { + return func(db *gorm.DB, res interface{}) (int, error) { + count := 0 + query := db.Table(table) + if column := request.GetSortingColumn(); column != nil { + order := "DESC" + if request.GetAsc() { + order = "ASC" + } + query = query.Order(fmt.Sprintf("%s %s", column.ToColumnName(), order)) + } + for _, q := range request.GetQueries() { + var err error + query, err = SetQuery(query, q.GetKey(), q.GetValue(), q.GetMethod()) + if err != nil { + return count, caos_errs.ThrowInvalidArgument(err, "VIEW-KaGue", "query is invalid") + } + } + + query = query.Count(&count) + if request.GetLimit() != 0 { + query = query.Limit(request.GetLimit()) + } + query = query.Offset(request.GetOffset()) + err := query.Find(res).Error + if err != nil { + return count, caos_errs.ThrowInternal(err, "VIEW-muSDK", "unable to find result") + } + return count, nil + } +} + +func SetQuery(query *gorm.DB, key ColumnKey, value interface{}, method model.SearchMethod) (*gorm.DB, error) { + column := key.ToColumnName() + if column == "" { + return nil, caos_errs.ThrowInvalidArgument(nil, "VIEW-7dz3w", "Column name missing") + } + + switch method { + case model.SEARCHMETHOD_EQUALS: + query = query.Where(""+column+" = ?", value) + case model.SEARCHMETHOD_EQUALS_IGNORE_CASE: + valueText, ok := value.(string) + if !ok { + return nil, caos_errs.ThrowInvalidArgument(nil, "VIEW-idu8e", "Starts with only possible for strings") + } + query = query.Where("LOWER("+column+") = LOWER(?)", valueText) + case model.SEARCHMETHOD_STARTS_WITH: + valueText, ok := value.(string) + if !ok { + return nil, caos_errs.ThrowInvalidArgument(nil, "VIEW-idu8e", "Starts with only possible for strings") + } + query = query.Where(column+" LIKE ?", valueText+"%") + case model.SEARCHMETHOD_STARTS_WITH_IGNORE_CASE: + valueText, ok := value.(string) + if !ok { + return nil, caos_errs.ThrowInvalidArgument(nil, "VIEW-eidus", "Starts with only possible for strings") + } + query = query.Where("LOWER("+column+") LIKE LOWER(?)", valueText+"%") + case model.SEARCHMETHOD_CONTAINS: + valueText, ok := value.(string) + if !ok { + return nil, caos_errs.ThrowInvalidArgument(nil, "VIEW-3ids", "Contains with only possible for strings") + } + query = query.Where(column+" LIKE ?", "%"+valueText+"%") + case model.SEARCHMETHOD_CONTAINS_IGNORE_CASE: + valueText, ok := value.(string) + if !ok { + return nil, caos_errs.ThrowInvalidArgument(nil, "VIEW-eid73", "Contains with only possible for strings") + } + query = query.Where("LOWER("+column+") LIKE LOWER(?)", "%"+valueText+"%") + + default: + return nil, nil + } + return query, nil +} diff --git a/internal/view/query_test.go b/internal/view/query_test.go new file mode 100644 index 0000000000..1d937aa56d --- /dev/null +++ b/internal/view/query_test.go @@ -0,0 +1,156 @@ +package view + +import ( + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/model" + "github.com/jinzhu/gorm" + "testing" +) + +func TestPrepareSearchQuery(t *testing.T) { + type args struct { + table string + searchRequest SearchRequest + } + type res struct { + count int + wantErr bool + errFunc func(err error) bool + } + tests := []struct { + name string + db *dbMock + args args + res res + }{ + { + "search with no params", + mockDB(t). + expectGetSearchRequestNoParams("TESTTABLE", 1, 1), + args{ + table: "TESTTABLE", + searchRequest: TestSearchRequest{}, + }, + res{ + count: 1, + wantErr: false, + }, + }, + { + "search with limit", + mockDB(t). + expectGetSearchRequestWithLimit("TESTTABLE", 2, 2, 5), + args{ + table: "TESTTABLE", + searchRequest: TestSearchRequest{limit: 2}, + }, + res{ + count: 5, + wantErr: false, + }, + }, + { + "search with offset", + mockDB(t). + expectGetSearchRequestWithOffset("TESTTABLE", 2, 2, 2), + args{ + table: "TESTTABLE", + searchRequest: TestSearchRequest{offset: 2}, + }, + res{ + count: 2, + wantErr: false, + }, + }, + { + "search with sorting asc", + mockDB(t). + expectGetSearchRequestWithSorting("TESTTABLE", "ASC", TestSearchKey_ID, 2, 2), + args{ + table: "TESTTABLE", + searchRequest: TestSearchRequest{sortingColumn: TestSearchKey_ID, asc: true}, + }, + res{ + count: 2, + wantErr: false, + }, + }, + { + "search with sorting asc", + mockDB(t). + expectGetSearchRequestWithSorting("TESTTABLE", "DESC", TestSearchKey_ID, 2, 2), + args{ + table: "TESTTABLE", + searchRequest: TestSearchRequest{sortingColumn: TestSearchKey_ID}, + }, + res{ + count: 2, + wantErr: false, + }, + }, + { + "search with search query", + mockDB(t). + expectGetSearchRequestWithSearchQuery("TESTTABLE", TestSearchKey_ID.ToColumnName(), "=", "ID", 2, 2), + args{ + table: "TESTTABLE", + searchRequest: TestSearchRequest{queries: []SearchQuery{TestSearchQuery{key: TestSearchKey_ID, method: model.SEARCHMETHOD_EQUALS_IGNORE_CASE, value: "ID"}}}, + }, + res{ + count: 2, + wantErr: false, + }, + }, + { + "search with all params", + mockDB(t). + expectGetSearchRequestWithAllParams("TESTTABLE", TestSearchKey_ID.ToColumnName(), "=", "ID", "ASC", TestSearchKey_ID, 2, 2, 2, 5), + args{ + table: "TESTTABLE", + searchRequest: TestSearchRequest{limit: 2, offset: 2, sortingColumn: TestSearchKey_ID, asc: true, queries: []SearchQuery{TestSearchQuery{key: TestSearchKey_ID, method: model.SEARCHMETHOD_EQUALS_IGNORE_CASE, value: "ID"}}}, + }, + res{ + count: 5, + wantErr: false, + }, + }, + { + "search db error", + mockDB(t). + expectGetSearchRequestErr("TESTTABLE", 1, 1, gorm.ErrUnaddressable), + args{ + table: "TESTTABLE", + searchRequest: TestSearchRequest{}, + }, + res{ + count: 1, + wantErr: true, + errFunc: caos_errs.IsInternal, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + res := &Test{} + getQuery := PrepareSearchQuery(tt.args.table, tt.args.searchRequest) + count, err := getQuery(tt.db.db, res) + + if !tt.res.wantErr && err != nil { + t.Errorf("got wrong err should be nil: %v ", err) + } + + if !tt.res.wantErr && count != tt.res.count { + t.Errorf("got wrong count: %v ", err) + } + + if tt.res.wantErr && !tt.res.errFunc(err) { + t.Errorf("got wrong err: %v ", err) + } + if err := tt.db.mock.ExpectationsWereMet(); !tt.res.wantErr && err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + + tt.db.close() + }) + } +} diff --git a/internal/view/requests.go b/internal/view/requests.go new file mode 100644 index 0000000000..d967270307 --- /dev/null +++ b/internal/view/requests.go @@ -0,0 +1,72 @@ +package view + +import ( + "errors" + "fmt" + "github.com/caos/logging" + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/jinzhu/gorm" +) + +func PrepareGetByKey(table string, key ColumnKey, id string) func(db *gorm.DB, res interface{}) error { + return func(db *gorm.DB, res interface{}) error { + err := db.Table(table). + Where(fmt.Sprintf("%s = ?", key.ToColumnName()), id). + Take(res). + Error + if err == nil { + return nil + } + if errors.Is(err, gorm.ErrRecordNotFound) { + return caos_errs.ThrowNotFound(err, "VIEW-XRI9c", "object not found") + } + logging.LogWithFields("VIEW-xVShS", "ID", id).WithError(err).Warn("get from view error") + return caos_errs.ThrowInternal(err, "VIEW-J92Td", "view error") + } +} + +func PrepareGetByQuery(table string, queries ...SearchQuery) func(db *gorm.DB, res interface{}) error { + return func(db *gorm.DB, res interface{}) error { + query := db.Table(table) + for _, q := range queries { + var err error + query, err = SetQuery(query, q.GetKey(), q.GetValue(), q.GetMethod()) + if err != nil { + return caos_errs.ThrowInvalidArgument(err, "VIEW-KaGue", "query is invalid") + } + } + + err := query.Take(res).Error + if err == nil { + return nil + } + if errors.Is(err, gorm.ErrRecordNotFound) { + return caos_errs.ThrowNotFound(err, "VIEW-hodc6", "object not found") + } + logging.LogWithFields("VIEW-Mg6la", "table ", table).WithError(err).Warn("get from cache error") + return caos_errs.ThrowInternal(err, "VIEW-qJBg9", "cache error") + } +} + +func PrepareSave(table string) func(db *gorm.DB, object interface{}) error { + return func(db *gorm.DB, object interface{}) error { + err := db.Table(table).Save(object).Error + if err != nil { + return caos_errs.ThrowInternal(err, "VIEW-AfC7G", "unable to put object to view") + } + return nil + } +} + +func PrepareDelete(table string, key ColumnKey, id string) func(db *gorm.DB) error { + return func(db *gorm.DB) error { + err := db.Table(table). + Where(fmt.Sprintf("%s = ?", key.ToColumnName()), id). + Delete(nil). + Error + if err != nil { + return caos_errs.ThrowInternal(err, "VIEW-die73", "could not delete object") + } + return nil + } +} diff --git a/internal/view/requests_test.go b/internal/view/requests_test.go new file mode 100644 index 0000000000..d60a386201 --- /dev/null +++ b/internal/view/requests_test.go @@ -0,0 +1,392 @@ +package view + +import ( + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/caos/zitadel/internal/model" + "github.com/jinzhu/gorm" + "testing" +) + +func TestPrepareGetByKey(t *testing.T) { + type args struct { + table string + key ColumnKey + value string + } + type res struct { + result Test + wantErr bool + errFunc func(err error) bool + } + tests := []struct { + name string + db *dbMock + args args + res res + }{ + { + "ok", + mockDB(t). + expectGetByID("TESTTABLE", "test", "VALUE"), + args{ + table: "TESTTABLE", + key: TestSearchKey_TEST, + value: "VALUE", + }, + res{ + result: Test{ID: "VALUE"}, + wantErr: false, + }, + }, + { + "not found", + mockDB(t). + expectGetByIDErr("TESTTABLE", "test", "VALUE", gorm.ErrRecordNotFound), + args{ + table: "TESTTABLE", + key: TestSearchKey_TEST, + value: "VALUE", + }, + res{ + result: Test{ID: "VALUE"}, + wantErr: true, + errFunc: caos_errs.IsNotFound, + }, + }, + { + "db err", + mockDB(t). + expectGetByIDErr("TESTTABLE", "test", "VALUE", gorm.ErrUnaddressable), + args{ + table: "TESTTABLE", + key: TestSearchKey_TEST, + value: "VALUE", + }, + res{ + result: Test{ID: "VALUE"}, + wantErr: true, + errFunc: caos_errs.IsInternal, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + res := &Test{} + getByID := PrepareGetByKey(tt.args.table, tt.args.key, tt.args.value) + err := getByID(tt.db.db, res) + + if !tt.res.wantErr && err != nil { + t.Errorf("got wrong err should be nil: %v ", err) + } + + if tt.res.wantErr && !tt.res.errFunc(err) { + t.Errorf("got wrong err: %v ", err) + } + if err := tt.db.mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + + tt.db.close() + }) + } +} + +func TestPrepareGetByQuery(t *testing.T) { + type args struct { + table string + searchQuery SearchQuery + } + type res struct { + result Test + wantErr bool + errFunc func(err error) bool + } + tests := []struct { + name string + db *dbMock + args args + res res + }{ + { + "search with equals case insensitive", + mockDB(t). + expectGetByQuery("TESTTABLE", "test", "=", "VALUE"), + args{ + table: "TESTTABLE", + searchQuery: TestSearchQuery{key: TestSearchKey_TEST, method: model.SEARCHMETHOD_EQUALS_IGNORE_CASE, value: "VALUE"}, + }, + res{ + result: Test{ID: "VALUE"}, + wantErr: false, + }, + }, + { + "search with equals case sensitive", + mockDB(t). + expectGetByQueryCaseSensitive("TESTTABLE", "test", "=", "VALUE"), + args{ + table: "TESTTABLE", + searchQuery: TestSearchQuery{key: TestSearchKey_TEST, method: model.SEARCHMETHOD_EQUALS, value: "VALUE"}, + }, + res{ + result: Test{ID: "VALUE"}, + wantErr: false, + }, + }, + { + "search with startswith, case insensitive", + mockDB(t). + expectGetByQuery("TESTTABLE", "test", "LIKE", "VALUE%"), + args{ + table: "TESTTABLE", + searchQuery: TestSearchQuery{key: TestSearchKey_TEST, method: model.SEARCHMETHOD_STARTS_WITH_IGNORE_CASE, value: "VALUE"}, + }, + res{ + result: Test{ID: "VALUE"}, + wantErr: false, + }, + }, + { + "search with startswith case sensitive", + mockDB(t). + expectGetByQueryCaseSensitive("TESTTABLE", "test", "LIKE", "VALUE%"), + args{ + table: "TESTTABLE", + searchQuery: TestSearchQuery{key: TestSearchKey_TEST, method: model.SEARCHMETHOD_STARTS_WITH, value: "VALUE"}, + }, + res{ + result: Test{ID: "VALUE"}, + wantErr: false, + }, + }, + { + "search with contains case insensitive", + mockDB(t). + expectGetByQuery("TESTTABLE", "test", "LIKE", "%VALUE%"), + args{ + table: "TESTTABLE", + searchQuery: TestSearchQuery{key: TestSearchKey_TEST, method: model.SEARCHMETHOD_CONTAINS_IGNORE_CASE, value: "VALUE"}, + }, + res{ + result: Test{ID: "VALUE"}, + wantErr: false, + }, + }, + { + "search with contains case sensitive", + mockDB(t). + expectGetByQueryCaseSensitive("TESTTABLE", "test", "LIKE", "%VALUE%"), + args{ + table: "TESTTABLE", + searchQuery: TestSearchQuery{key: TestSearchKey_TEST, method: model.SEARCHMETHOD_CONTAINS, value: "VALUE"}, + }, + res{ + result: Test{ID: "VALUE"}, + wantErr: false, + }, + }, + { + "search expect not found err", + mockDB(t). + expectGetByQueryErr("TESTTABLE", "test", "LIKE", "%VALUE%", gorm.ErrRecordNotFound), + args{ + table: "TESTTABLE", + searchQuery: TestSearchQuery{key: TestSearchKey_TEST, method: model.SEARCHMETHOD_CONTAINS_IGNORE_CASE, value: "VALUE"}, + }, + res{ + result: Test{ID: "VALUE"}, + wantErr: true, + errFunc: caos_errs.IsNotFound, + }, + }, + { + "search expect internal err", + mockDB(t). + expectGetByQueryErr("TESTTABLE", "test", "LIKE", "%VALUE%", gorm.ErrUnaddressable), + args{ + table: "TESTTABLE", + searchQuery: TestSearchQuery{key: TestSearchKey_TEST, method: model.SEARCHMETHOD_CONTAINS_IGNORE_CASE, value: "VALUE"}, + }, + res{ + result: Test{ID: "VALUE"}, + wantErr: true, + errFunc: caos_errs.IsInternal, + }, + }, + { + "search with invalid column", + mockDB(t). + expectGetByQuery("TESTTABLE", "", "=", "VALUE"), + args{ + table: "TESTTABLE", + searchQuery: TestSearchQuery{key: TestSearchKey_UNDEFINED, method: model.SEARCHMETHOD_EQUALS_IGNORE_CASE, value: "VALUE"}, + }, + res{ + result: Test{ID: "VALUE"}, + wantErr: true, + errFunc: caos_errs.IsErrorInvalidArgument, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + res := &Test{} + getByQuery := PrepareGetByQuery(tt.args.table, tt.args.searchQuery) + err := getByQuery(tt.db.db, res) + + if !tt.res.wantErr && err != nil { + t.Errorf("got wrong err should be nil: %v ", err) + } + + if tt.res.wantErr && !tt.res.errFunc(err) { + t.Errorf("got wrong err: %v ", err) + } + if err := tt.db.mock.ExpectationsWereMet(); !tt.res.wantErr && err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + + tt.db.close() + }) + } +} + +func TestPreparePut(t *testing.T) { + type args struct { + table string + object *Test + } + type res struct { + result Test + wantErr bool + errFunc func(err error) bool + } + tests := []struct { + name string + db *dbMock + args args + res res + }{ + { + "ok", + mockDB(t). + expectBegin(nil). + expectSave("TESTTABLE", Test{ID: "ID", Test: "VALUE"}). + expectCommit(nil), + args{ + table: "TESTTABLE", + object: &Test{ID: "ID", Test: "VALUE"}, + }, + res{ + result: Test{ID: "VALUE"}, + wantErr: false, + }, + }, + { + "db error", + mockDB(t). + expectBegin(nil). + expectSaveErr("TESTTABLE", Test{ID: "ID", Test: "VALUE"}, gorm.ErrUnaddressable). + expectCommit(nil), + args{ + table: "TESTTABLE", + object: &Test{ID: "ID", Test: "VALUE"}, + }, + res{ + result: Test{ID: "VALUE"}, + wantErr: true, + errFunc: caos_errs.IsInternal, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + getPut := PrepareSave(tt.args.table) + err := getPut(tt.db.db, tt.args.object) + + if !tt.res.wantErr && err != nil { + t.Errorf("got wrong err should be nil: %v ", err) + } + + if tt.res.wantErr && !tt.res.errFunc(err) { + t.Errorf("got wrong err: %v ", err) + } + if err := tt.db.mock.ExpectationsWereMet(); !tt.res.wantErr && err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + + tt.db.close() + }) + } +} + +func TestPrepareDelete(t *testing.T) { + type args struct { + table string + key ColumnKey + value string + } + type res struct { + result Test + wantErr bool + errFunc func(err error) bool + } + tests := []struct { + name string + db *dbMock + args args + res res + }{ + { + "delete", + mockDB(t). + expectBegin(nil). + expectRemove("TESTTABLE", "id", "VALUE"). + expectCommit(nil), + args{ + table: "TESTTABLE", + key: TestSearchKey_ID, + value: "VALUE", + }, + res{ + result: Test{ID: "VALUE"}, + wantErr: false, + }, + }, + { + "db error", + mockDB(t). + expectBegin(nil). + expectRemoveErr("TESTTABLE", "id", "VALUE", gorm.ErrUnaddressable). + expectCommit(nil), + args{ + table: "TESTTABLE", + key: TestSearchKey_ID, + value: "VALUE", + }, + res{ + result: Test{ID: "VALUE"}, + wantErr: true, + errFunc: caos_errs.IsInternal, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + getDelete := PrepareDelete(tt.args.table, tt.args.key, tt.args.value) + err := getDelete(tt.db.db) + + if !tt.res.wantErr && err != nil { + t.Errorf("got wrong err should be nil: %v ", err) + } + + if tt.res.wantErr && !tt.res.errFunc(err) { + t.Errorf("got wrong err: %v ", err) + } + if err := tt.db.mock.ExpectationsWereMet(); !tt.res.wantErr && err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + + tt.db.close() + }) + } +} diff --git a/internal/view/sequence.go b/internal/view/sequence.go new file mode 100644 index 0000000000..3b79f43f34 --- /dev/null +++ b/internal/view/sequence.go @@ -0,0 +1,58 @@ +package view + +import ( + caos_errs "github.com/caos/zitadel/internal/errors" + "github.com/jinzhu/gorm" +) + +type actualSequece struct { + ActualSequence uint64 `gorm:"column:current_sequence"` +} + +type currentSequence struct { + ViewName string `gorm:"column:view_name;primary_key"` + CurrentSequence uint64 `gorm:"column:current_sequence` +} + +type SequenceSearchKey int32 + +const ( + SEQUENCESEARCHKEY_UNDEFINED SequenceSearchKey = iota + SEQUENCESEARCHKEY_VIEW_NAME +) + +type sequenceSearchKey SequenceSearchKey + +func (key sequenceSearchKey) ToColumnName() string { + switch SequenceSearchKey(key) { + case SEQUENCESEARCHKEY_VIEW_NAME: + return "view_name" + default: + return "" + } +} + +func SaveCurrentSequence(db *gorm.DB, table, viewName string, sequence uint64) error { + save := PrepareSave(table) + err := save(db, ¤tSequence{viewName, sequence}) + + if err != nil { + return caos_errs.ThrowInternal(err, "VIEW-5kOhP", "unable to updated processed sequence") + } + return nil +} + +func LatestSequence(db *gorm.DB, table, viewName string) (uint64, error) { + sequence := new(actualSequece) + query := PrepareGetByKey(table, sequenceSearchKey(SEQUENCESEARCHKEY_VIEW_NAME), viewName) + err := query(db, sequence) + + if err == nil { + return sequence.ActualSequence, nil + } + + if gorm.IsRecordNotFoundError(err) { + return 0, nil + } + return 0, caos_errs.ThrowInternalf(err, "VIEW-9LyCB", "unable to get latest sequence of %s", viewName) +}