diff --git a/README.md b/README.md index 67d4874..d4e2579 100644 --- a/README.md +++ b/README.md @@ -9,20 +9,27 @@ This service communicates based on gRPC. You can refer to the proto files in [tk ## Quick Start +### Development environment +* Installed docker 20.x +* Running postgresql and Initilizing database. + ``` + docker run -p 5432:5432 --name postgres -e POSTGRES_PASSWORD=password -d postgres + docker cp scripts/script.sql postgres:/script.sql + docker exec -ti postgres psql -U postgres -a -f script.sql + ``` ### For go developers ``` go install -v ./... -contract-server -port 50051 -enable-mockup +server -port 9110 ``` ### For docker users ``` TAGS=$(curl --silent "https://api.github.com/repos/sktelecom/tks-contract/tags" | grep name | head -1 |cut -d '"' -f 4) docker pull docker.pkg.github.com/sktelecom/tks-contract/tks-contract:$TAGS -docker run --name tks-contract -p 50051:50051 -d \ +docker run --name tks-contract -p 9110:9110 -d \ docker.pkg.github.com/sktelecom/tks-contract/tks-contract:$TAGS \ - contract-server \ - # -enable-mockup \ - # -port 50051 + server \ + # -port 9110 ``` diff --git a/cmd/contract-server/handlers.go b/cmd/server/handlers.go similarity index 100% rename from cmd/contract-server/handlers.go rename to cmd/server/handlers.go diff --git a/cmd/contract-server/handlers_test.go b/cmd/server/handlers_test.go similarity index 100% rename from cmd/contract-server/handlers_test.go rename to cmd/server/handlers_test.go diff --git a/cmd/contract-server/server.go b/cmd/server/main.go similarity index 100% rename from cmd/contract-server/server.go rename to cmd/server/main.go index c5859cd..e1bf88f 100644 --- a/cmd/contract-server/server.go +++ b/cmd/server/main.go @@ -37,9 +37,9 @@ func setFlags() { } func main() { + flag.Parse() lis, err := net.Listen("tcp", ":"+strconv.Itoa(port)) log.Info("Starting to listen port ", port) - flag.Parse() if err != nil { log.Fatal("failed to listen:", err) } diff --git a/cmd/contract-server/mockup_contracts.go b/cmd/server/mockup_contracts.go similarity index 100% rename from cmd/contract-server/mockup_contracts.go rename to cmd/server/mockup_contracts.go diff --git a/go.mod b/go.mod index e6b3973..adbba8d 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,12 @@ module github.com/sktelecom/tks-contract go 1.16 require ( + github.com/DATA-DOG/go-sqlmock v1.5.0 + github.com/google/uuid v1.2.0 + github.com/lib/pq v1.10.2 github.com/sirupsen/logrus v1.8.1 github.com/sktelecom/tks-proto v0.0.4-0.20210419072147-cbafa000deab + github.com/stretchr/testify v1.7.0 // indirect google.golang.org/grpc v1.36.1 google.golang.org/protobuf v1.26.0 ) diff --git a/go.sum b/go.sum index c97638a..a87b1dc 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= +github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= @@ -32,19 +34,22 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= +github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sktelecom/tks-proto v0.0.4-0.20210419050352-2299e8d5d653 h1:plu5huLviWEh5uJyyVf3AZoYl8sajUtn2KfzkU2+wFs= -github.com/sktelecom/tks-proto v0.0.4-0.20210419050352-2299e8d5d653/go.mod h1:5r0c5Sq4RhX5IuVIyD/aRunO7WUHmpymBOBz9LTCaRY= github.com/sktelecom/tks-proto v0.0.4-0.20210419072147-cbafa000deab h1:Jqxkx4bq1uC6Y5BRaqo2eKE8eCdewluZ+1aTtDAk0d0= github.com/sktelecom/tks-proto v0.0.4-0.20210419072147-cbafa000deab/go.mod h1:5r0c5Sq4RhX5IuVIyD/aRunO7WUHmpymBOBz9LTCaRY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -97,7 +102,8 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0 google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/pkg/postgresql/accessor.go b/pkg/postgresql/accessor.go new file mode 100644 index 0000000..b03304a --- /dev/null +++ b/pkg/postgresql/accessor.go @@ -0,0 +1,217 @@ +package postgresql + +import ( + "database/sql" + "fmt" + + "github.com/sktelecom/tks-contract/pkg/log" + + _ "github.com/lib/pq" +) + +// Accessor is an accessor for PostgresqlDB. +type Accessor struct { + db *sql.DB +} + +// New returns a new Postgresql. +func New(db *sql.DB) *Accessor { + return &Accessor{ + db: db, + } +} + +// Close closes database session. +func (p *Accessor) Close() error { + p.db.Close() + return nil +} + +// Get returns result of querying from DB. +// Support both non-transactional and transactional queries. +func (p *Accessor) Get(tx *sql.Tx, fields, table string, conditions map[string]interface{}) (*sql.Rows, error) { + if len(conditions) == 0 { + return p.getAll(tx, fields, table) + } + + conditionValues := getValueSliceFromMaps(conditions) + conditionKeySql := getVarSyntaxFromMaps(conditions) + query := fmt.Sprintf(`SELECT %s FROM %s WHERE %s`, fields, table, conditionKeySql[0]) + + if tx == nil { + return p.db.Query(query, conditionValues...) + } + + rows, err := tx.Query(query, conditionValues...) + if err != nil { + if errRollback := tx.Rollback(); errRollback != nil { + log.Fatal("failed to rollback transaction: ", errRollback) + return nil, errRollback + } + return nil, err + } + return rows, nil +} + +// Insert inserts new column into table. +func (p *Accessor) Insert(tx *sql.Tx, table string, values ...interface{}) (int64, error) { + query := fmt.Sprintf(`INSERT INTO %s VALUES(%s)`, table, getVarSyntax(len(values))) + var ( + res sql.Result + err error + ) + if tx == nil { + if res, err = p.db.Exec(query, values...); err != nil { + return 0, err + } + return res.RowsAffected() + } + + if res, err = tx.Exec(query, values...); err != nil { + if errRollback := tx.Rollback(); errRollback != nil { + log.Fatal("failed to rollback transaction: ", errRollback) + return 0, errRollback + } + return 0, err + } + return res.RowsAffected() +} + +// Delete deletes a row which meets a condition in table. +func (p *Accessor) Delete(tx *sql.Tx, table string, conditions map[string]interface{}) (int64, error) { + var ( + res sql.Result + err error + ) + conditionKeySql := getVarSyntaxFromMaps(conditions) + conditionValues := getValueSliceFromMaps(conditions) + + query := fmt.Sprintf(`DELETE FROM %s WHERE %s`, table, conditionKeySql[0]) + if tx == nil { + if res, err = p.db.Exec(query, conditionValues...); err != nil { + log.Fatal(err) + return 0, err + } + return res.RowsAffected() + } + + if res, err = tx.Exec(query, conditionValues...); err != nil { + if errRollback := tx.Rollback(); errRollback != nil { + log.Fatal("failed to rollback transaction: ", errRollback) + return 0, errRollback + } + return 0, err + } + return res.RowsAffected() +} + +// Update updates values of specific row which meets a condition in table. +func (p *Accessor) Update(tx *sql.Tx, table string, values, conditions map[string]interface{}) (int64, error) { + var ( + res sql.Result + err error + ) + conditionKeySql := getVarSyntaxFromMaps(values, conditions) + conditionValues := getValueSliceFromMaps(values, conditions) + query := fmt.Sprintf(`UPDATE %s SET %s WHERE %s`, table, conditionKeySql[0], conditionKeySql[1]) + if tx == nil { + if res, err = p.db.Exec(query, conditionValues...); err != nil { + log.Fatal(err) + return 0, err + } + return res.RowsAffected() + } + if res, err = tx.Exec(query, conditionValues...); err != nil { + if errRollback := tx.Rollback(); errRollback != nil { + log.Fatal("failed to rollback transaction: ", errRollback) + return 0, errRollback + } + return 0, err + } + return res.RowsAffected() +} + +// Query quries rows in DB with pure SQL statement. +func (p *Accessor) Query(query string, args ...interface{}) (*sql.Rows, error) { + return p.db.Query(query, args...) +} + +// BeginTx returns a new transaction. +func (p *Accessor) BeginTx() (*sql.Tx, error) { + return p.db.Begin() +} + +// CommitTx commits a transaction. +func (p *Accessor) CommitTx(tx *sql.Tx) error { + return tx.Commit() +} + +func (p *Accessor) getAll(tx *sql.Tx, fields, table string) (*sql.Rows, error) { + query := fmt.Sprintf(`SELECT %s FROM %s`, fields, table) + + if tx == nil { + return p.db.Query(query) + } + rows, err := tx.Query(query) + if err != nil { + if errRollback := tx.Rollback(); errRollback != nil { + log.Fatal("failed to rollback transaction: ", errRollback) + return nil, errRollback + } + return nil, err + } + return rows, nil +} + +// getVarSyntax makes "$1, $2, $3..." string for SQL query. +func getVarSyntax(count int) string { + var ( + result string + idx int + start bool = true + ) + for idx = 1; idx <= count; idx++ { + if !start { + result += ", " + } + result += fmt.Sprintf(`$%d`, idx) + start = false + } + return result +} + +// getValuesSliceFromMaps returns one slice gathering all values from multiple maps. +func getValueSliceFromMaps(maps ...map[string]interface{}) []interface{} { + result := make([]interface{}, 0) + for i := range maps { + for _, v := range maps[i] { + result = append(result, v) + } + } + return result +} + +// getVarSyntaxFromMaps returns multiple varSyntax "name=$1, id=$2 ..." from multiple maps. +// Index of varSyntax between multiple maps increases continously. +func getVarSyntaxFromMaps(maps ...map[string]interface{}) []string { + var ( + idx int = 1 + result []string + ) + for i := range maps { + var ( + temp string + start bool = true + ) + for k := range maps[i] { + if !start { + temp += ", " + } + temp += fmt.Sprintf("%s=$%d", k, idx) + start = false + idx++ + } + result = append(result, temp) + } + return result +} diff --git a/pkg/postgresql/accessor_test.go b/pkg/postgresql/accessor_test.go new file mode 100644 index 0000000..53f7685 --- /dev/null +++ b/pkg/postgresql/accessor_test.go @@ -0,0 +1,165 @@ +package postgresql_test + +import ( + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/sktelecom/tks-contract/pkg/postgresql" +) + +func TestInsert(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a stub database connection.", err) + } + defer db.Close() + accessor := postgresql.New(db) + defer accessor.Close() + + mock.ExpectExec("INSERT INTO records"). + WithArgs("gopher", "828cec77-1da5-4ba6-90d0-270d71be3c55", 50). + WillReturnResult(sqlmock.NewResult(1, 1)) + + count, err := accessor.Insert(nil, "records(name, id, score)", + "gopher", + "828cec77-1da5-4ba6-90d0-270d71be3c55", + 50) + + if err != nil { + t.Errorf("error was not expected while creating contract: %s", err) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulilled expectations: %s", err) + } + t.Log("updated count: ", count) +} + +func TestInsertWithTransaction(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a stub database connection.", err) + } + defer db.Close() + accessor := postgresql.New(db) + defer accessor.Close() + + mock.ExpectBegin() + mock.ExpectExec("INSERT INTO records"). + WithArgs("gopher", "828cec77-1da5-4ba6-90d0-270d71be3c55", 50). + WillReturnResult(sqlmock.NewResult(1, 1)) + mock.ExpectCommit() + + tx, err := accessor.BeginTx() + if err != nil { + t.Errorf("an error was not expected while beginning transaction %s", err) + } + count, err := accessor.Insert(tx, "records(name, id, score)", + "gopher", + "828cec77-1da5-4ba6-90d0-270d71be3c55", + 50) + if err != nil { + t.Errorf("error was not expected while creating contract: %s", err) + } + if err = tx.Commit(); err != nil { + t.Errorf("error was not expected while committing transaction: %s", err) + } + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulilled expectations: %s", err) + } + t.Log("updated count: ", count) +} + +func TestGet(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a stub database connection.", err) + } + defer db.Close() + + mockRows := sqlmock.NewRows([]string{"name", "id", "score"}). + AddRow("gopher", "828cec77-1da5-4ba6-90d0-270d71be3c55", 50) + mock.ExpectQuery("^SELECT (.+) FROM records$").WillReturnRows(mockRows) + + accessor := postgresql.New(db) + // have to call Close() + defer accessor.Close() + + rows, err := accessor.Get(nil, "*", "records", map[string]interface{}{}) + if err != nil { + t.Error(err) + } + if err = mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + defer rows.Close() + for rows.Next() { + var ( + name, id string + score int + ) + if err = rows.Scan(&name, &id, &score); err != nil { + t.Errorf("error was not expected while scanning rows: %s", err) + } + t.Logf("scanned row: %s %s %d", name, id, score) + } +} + +func TestUpdate(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a stub database connection.", err) + } + defer db.Close() + + mock.ExpectExec("UPDATE records"). + WithArgs(70, "8192039", "gopher"). + WillReturnResult(sqlmock.NewResult(1, 1)) + accessor := postgresql.New(db) + // have to call Close() + defer accessor.Close() + updateValues := map[string]interface{}{ + "score": 70, + } + conditionValues := map[string]interface{}{ + "id": "8192039", + "name": "gopher", + } + count, err := accessor.Update(nil, "records", updateValues, conditionValues) + if err != nil { + t.Error(err) + } + if err = mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + + t.Logf("updated count: %d", count) +} + +func TestDelete(t *testing.T) { + db, mock, err := sqlmock.New() + if err != nil { + t.Fatalf("an error '%s' was not expected when opening a stub database connection.", err) + } + defer db.Close() + + mock.ExpectExec("DELETE FROM records"). + WithArgs("gopher", 31, 50). + WillReturnResult(sqlmock.NewResult(1, 1)) + accessor := postgresql.New(db) + // have to call Close() + defer accessor.Close() + condition := map[string]interface{}{ + "name": "gopher", + "age": 31, + "score": 50, + } + count, err := accessor.Delete(nil, "records", condition) + if err != nil { + t.Error(err) + } + if err = mock.ExpectationsWereMet(); err != nil { + t.Errorf("there were unfulfilled expectations: %s", err) + } + + t.Logf("updated count: %d", count) +} diff --git a/scripts/script.sql b/scripts/script.sql new file mode 100644 index 0000000..875e809 --- /dev/null +++ b/scripts/script.sql @@ -0,0 +1,14 @@ +CREATE DATABASE tks; +\c tks; +CREATE TABLE contract +( + contractor_name character varying(50) COLLATE pg_catalog."default", + contract_id uuid primary key, + available_services character varying(10)[] COLLATE pg_catalog."default", + csp_id uuid, + last_updated_ts timestamp with time zone +); + +INSERT INTO contract( + contractor_name, contract_id, available_services, csp_id, last_updated_ts) + VALUES ('tester', 'edcaa975-dde4-4c4d-94f7-36bc38fe7064', ARRAY['lma'], '3390f92b-0da8-4628-83e2-e266b1928e11', '2021-05-01'::timestamp); \ No newline at end of file