diff --git a/Makefile b/Makefile index 6da8f51..2d20098 100644 --- a/Makefile +++ b/Makefile @@ -36,13 +36,13 @@ build: GOOS=linux GOARCH=amd64 go build -o bin/main cmd/main.go local-migration-status: - $(LOCAL_BIN)/goose -dir ${LOCAL_MIGRATION_DIR} postgres ${LOCAL_MIGRATION_DSN} status -v + $(LOCAL_BIN)/goose -dir ${MIGRATION_DR} postgres ${PG_DSN} status -v local-migration-up: - $(LOCAL_BIN)/goose -dir ${LOCAL_MIGRATION_DIR} postgres ${LOCAL_MIGRATION_DSN} up -v + $(LOCAL_BIN)/goose -dir ${MIGRATION_DR} postgres ${PG_DSN} up -v local-migration-down: - $(LOCAL_BIN)/goose -dir ${LOCAL_MIGRATION_DIR} postgres ${LOCAL_MIGRATION_DSN} down -v + $(LOCAL_BIN)/goose -dir ${MIGRATION_DR} postgres ${PG_DSN} down -v copy-to-server: diff --git a/cmd/main.go b/cmd/main.go index f6e4995..7ebf806 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,172 +1,20 @@ package main import ( - desc "auth/pkg/user_v1" + "auth/internal/app" "context" - "fmt" - "github.com/Masterminds/squirrel" - "github.com/golang/protobuf/ptypes/empty" - "github.com/jackc/pgx/v4" - "github.com/pkg/errors" - "google.golang.org/grpc" - "google.golang.org/grpc/reflection" - "google.golang.org/protobuf/types/known/timestamppb" "log" - "net" - "time" ) -const grpcPort = 50051 -const dbDSN = "host=localhost port=54321 dbname=auth-service user=dev-course password=1801 sslmode=disable" - -type server struct { - desc.UnimplementedUserV1Server -} - -func (s *server) Create(ctx context.Context, req *desc.CreateUserRequest) (*desc.CreateUserResponse, error) { - if req.User.Password != req.User.PasswordConfirm { - return nil, errors.New("Passwords are not equal") - } - - pool, err := pgx.Connect(ctx, dbDSN) - if err != nil { - return nil, errors.Wrapf(err, "Failed to connect to database %s", err) - } - - insertBuilder := squirrel.Insert("\"user\""). - PlaceholderFormat(squirrel.Dollar). - Columns("name", "email", "role", "password"). - Values(req.User.Name, req.User.Email, req.User.Role.String(), req.User.Password). - Suffix("RETURNING id") - - query, args, err := insertBuilder.ToSql() - if err != nil { - return nil, errors.Wrapf(err, "Failed to build query %s", err) - } - - var id int64 - err = pool.QueryRow(ctx, query, args...).Scan(&id) - if err != nil { - return nil, errors.Wrapf(err, "Failed to make query: %s", err) - } - - return &desc.CreateUserResponse{ - Id: id, - }, nil -} - -func (s *server) Get(ctx context.Context, req *desc.GetUserRequest) (*desc.GetUserResponse, error) { - pool, err := pgx.Connect(ctx, dbDSN) - if err != nil { - return nil, errors.Wrapf(err, "Failed to connect to database %s", err) - } - - selectBuilder := squirrel.Select("id", "name", "email", "role", "created_at", "updated_at"). - PlaceholderFormat(squirrel.Dollar). - From("\"user\""). - Where(squirrel.Eq{"id": req.Id}) - - query, args, err := selectBuilder.ToSql() - if err != nil { - return nil, errors.Wrapf(err, "Failed to build query %s", err) - } - - var userId int64 - var userName string - var email string - var role string - var createdAt time.Time - var updatedAt *time.Time - err = pool.QueryRow(ctx, query, args...).Scan(&userId, &userName, &email, &role, &createdAt, &updatedAt) - if err != nil { - return nil, errors.Wrapf(err, "Failed to make query: %s", err) - } - - user := &desc.GetUserResponse{ - Id: userId, - Name: userName, - Email: email, - Role: desc.Role(desc.Role_value[role]), - CreatedAt: timestamppb.New(createdAt), - } - if updatedAt != nil { - user.UpdatedAt = timestamppb.New(*updatedAt) - } - - return user, nil -} - -func (s *server) Update(ctx context.Context, req *desc.UpdateUserRequest) (*empty.Empty, error) { - if req.Email == nil && req.Name == nil && req.Role.Number() == 0 { - return &empty.Empty{}, nil - } - - pool, err := pgx.Connect(ctx, dbDSN) - if err != nil { - return nil, errors.Wrapf(err, "Failed to connect to database %s", err) - } - - builderUpdate := squirrel.Update("\"user\""). - PlaceholderFormat(squirrel.Dollar). - Set("updated_at", time.Now()) - if req.Email != nil { - builderUpdate = builderUpdate.Set("email", req.Email.Value) - } - if req.Name != nil { - builderUpdate = builderUpdate.Set("name", req.Name.Value) - } - if req.Role.Number() != 0 { - builderUpdate = builderUpdate.Set("role", req.Role.String()) - } - builderUpdate = builderUpdate.Where(squirrel.Eq{"id": req.Id}) - - query, args, err := builderUpdate.ToSql() - if err != nil { - return nil, errors.Wrapf(err, "Failed to build query: %s", err) - } - - _, err = pool.Exec(ctx, query, args...) - if err != nil { - return nil, errors.Wrapf(err, "Failed to executed query: %s", err) - } - - return &empty.Empty{}, nil -} - -func (s *server) Delete(ctx context.Context, req *desc.DeleteUserRequest) (*empty.Empty, error) { - pool, err := pgx.Connect(ctx, dbDSN) - if err != nil { - return nil, errors.Wrapf(err, "Failed to connect to database %s", err) - } - - deleteBuilder := squirrel.Delete("\"user\""). - PlaceholderFormat(squirrel.Dollar). - Where(squirrel.Eq{"id": req.Id}) - - query, args, err := deleteBuilder.ToSql() - if err != nil { - return nil, errors.Wrapf(err, "Failed to build query %s", err) - } - _, err = pool.Exec(ctx, query, args...) - if err != nil { - return nil, errors.Wrapf(err, "Failed to execute %s", err) - } - - return &empty.Empty{}, nil -} - func main() { - lis, err := net.Listen("tcp", fmt.Sprintf(":%d", grpcPort)) + ctx := context.Background() + a, err := app.NewApp(ctx) if err != nil { - log.Fatalf("failed to listen: %v", err) + log.Fatalf("failed to create new app: %s", err.Error()) } - s := grpc.NewServer() - reflection.Register(s) - desc.RegisterUserV1Server(s, &server{}) - - fmt.Println("Server has been started") - if err = s.Serve(lis); err != nil { - log.Fatalf("failed to serve: %v", err) + err = a.Run() + if err != nil { + log.Fatalf("failed to run app: %s", err.Error()) } } diff --git a/go.mod b/go.mod index 3f66330..691e89d 100644 --- a/go.mod +++ b/go.mod @@ -4,20 +4,23 @@ go 1.20 require ( github.com/Masterminds/squirrel v1.5.4 + github.com/georgysavva/scany v1.2.1 + github.com/jackc/pgconn v1.14.0 github.com/jackc/pgx/v4 v4.18.1 - github.com/pkg/errors v0.8.1 + github.com/joho/godotenv v1.5.1 + github.com/pkg/errors v0.9.1 google.golang.org/grpc v1.57.0 google.golang.org/protobuf v1.31.0 ) require ( github.com/jackc/chunkreader/v2 v2.0.1 // indirect - github.com/jackc/pgconn v1.14.0 // indirect github.com/jackc/pgio v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgproto3/v2 v2.3.2 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgtype v1.14.0 // indirect + github.com/jackc/puddle v1.3.0 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect golang.org/x/crypto v0.6.0 // indirect diff --git a/go.sum b/go.sum index 667e44e..49ddf32 100644 --- a/go.sum +++ b/go.sum @@ -4,15 +4,23 @@ github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8 github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= +github.com/cockroachdb/cockroach-go/v2 v2.2.0 h1:/5znzg5n373N/3ESjHF5SMLxiW4RKB05Ql//KWfeTFs= +github.com/cockroachdb/cockroach-go/v2 v2.2.0/go.mod h1:u3MiKYGupPPjkn3ozknpMUpxPaNLTFWAya419/zv6eI= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/georgysavva/scany v1.2.1 h1:91PAMBpwBtDjvn46TaLQmuVhxpAG6p6sjQaU4zPHPSM= +github.com/georgysavva/scany v1.2.1/go.mod h1:vGBpL5XRLOocMFFa55pj0P04DrL3I7qKVRL49K6Eu5o= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= +github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= +github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= @@ -28,6 +36,9 @@ github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgO github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= +github.com/jackc/pgconn v1.4.0/go.mod h1:Y2O3ZDF0q4mMacyWV3AstPJpeHXWGEetiFttmq5lahk= +github.com/jackc/pgconn v1.5.0/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= +github.com/jackc/pgconn v1.5.1-0.20200601181101-fa742c524853/go.mod h1:QeD3lBfpTFe8WUnPZWN5KY/mB8FGMIYRdd8P8Jr0fAI= github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= @@ -46,29 +57,47 @@ github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= +github.com/jackc/pgproto3/v2 v2.0.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.3.2 h1:7eY55bdBeCz1F2fTzSz69QC+pG46jYq9/jtSPiJ5nn0= github.com/jackc/pgproto3/v2 v2.3.2/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgservicefile v0.0.0-20200307190119-3430c5407db8/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= +github.com/jackc/pgtype v1.2.0/go.mod h1:5m2OfMh1wTK7x+Fk952IDmI4nw3nPrvtQdM0ZT4WpC0= +github.com/jackc/pgtype v1.3.1-0.20200510190516-8cd94a14c75a/go.mod h1:vaogEUkALtxZMCH411K+tKzNpwzCKU+AnPzBKZ+I+Po= +github.com/jackc/pgtype v1.3.1-0.20200606141011-f6355165a91c/go.mod h1:cvk9Bgu/VzJ9/lxTO5R5sf80p0DiucVtN7ZxvaC4GmQ= +github.com/jackc/pgtype v1.6.2/go.mod h1:JCULISAZBFGrHaOXIIFiyfzW5VY0GRitRr8NeJsrdig= github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= github.com/jackc/pgtype v1.14.0 h1:y+xUdabmyMkJLyApYuPj38mW+aAIqCe5uuBB51rH3Vw= github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= +github.com/jackc/pgx/v4 v4.5.0/go.mod h1:EpAKPLdnTorwmPUUsqrPxy5fphV18j9q3wrfRXgo+kA= +github.com/jackc/pgx/v4 v4.6.1-0.20200510190926-94ba730bb1e9/go.mod h1:t3/cdRQl6fOLDxqtlyhe9UWgfIi9R8+8v8GKV5TRA/o= +github.com/jackc/pgx/v4 v4.6.1-0.20200606145419-4e5062306904/go.mod h1:ZDaNWkt9sW1JMiNn0kdYBaLelIhw7Pg4qd+Vk6tw7Hg= +github.com/jackc/pgx/v4 v4.10.1/go.mod h1:QlrWebbs3kqEZPHCTGyxecvzG6tvIsYu+A5b1raylkA= github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= github.com/jackc/pgx/v4 v4.18.1 h1:YP7G1KABtKpB5IHrO9vYwSrCOhs7p3uqhvhhQBptya0= github.com/jackc/pgx/v4 v4.18.1/go.mod h1:FydWkUyadDmdNH/mHnGob881GawxeEm7TcMCzkb+qQE= 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/jackc/puddle v1.1.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.1.1/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.3.0 h1:eHK/5clGOatcjX3oWGBO/MpxpbHzSwud5EWTSCI+MX0= github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jmoiron/sqlx v1.3.1/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= @@ -83,15 +112,22 @@ github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6Fm 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.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/lib/pq v1.10.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 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-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 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/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= @@ -100,6 +136,7 @@ github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OK github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= +github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= @@ -108,6 +145,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= @@ -135,9 +173,12 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= @@ -165,11 +206,13 @@ golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -183,6 +226,7 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= @@ -217,7 +261,11 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/postgres v1.0.8/go.mod h1:4eOzrI1MUfm6ObJU/UcmbXyiHSs8jSwH95G5P5dxcAg= +gorm.io/gorm v1.20.12/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= +gorm.io/gorm v1.21.4/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= diff --git a/internal/api/create.go b/internal/api/create.go new file mode 100644 index 0000000..aa1b19f --- /dev/null +++ b/internal/api/create.go @@ -0,0 +1,18 @@ +package api + +import ( + userServiceConverter "auth/internal/converter" + desc "auth/pkg/user_v1" + "context" +) + +func (i *Implementation) Create(ctx context.Context, req *desc.CreateUserRequest) (*desc.CreateUserResponse, error) { + id, err := i.userService.Create(ctx, userServiceConverter.ToCreateUserInfoFromDesc(req)) + if err != nil { + return nil, err + } + + return &desc.CreateUserResponse{ + Id: id, + }, nil +} diff --git a/internal/api/delete.go b/internal/api/delete.go new file mode 100644 index 0000000..137b60f --- /dev/null +++ b/internal/api/delete.go @@ -0,0 +1,16 @@ +package api + +import ( + desc "auth/pkg/user_v1" + "context" + "github.com/golang/protobuf/ptypes/empty" +) + +func (i *Implementation) Delete(ctx context.Context, req *desc.DeleteUserRequest) (*empty.Empty, error) { + err := i.userService.Delete(ctx, req.Id) + if err != nil { + return nil, err + } + + return &empty.Empty{}, nil +} diff --git a/internal/api/get.go b/internal/api/get.go new file mode 100644 index 0000000..51f1c8e --- /dev/null +++ b/internal/api/get.go @@ -0,0 +1,16 @@ +package api + +import ( + userServiceConverter "auth/internal/converter" + desc "auth/pkg/user_v1" + "context" +) + +func (i *Implementation) Get(ctx context.Context, req *desc.GetUserRequest) (*desc.GetUserResponse, error) { + userFromDb, err := i.userService.Get(ctx, req.Id) + if err != nil { + return nil, err + } + + return userServiceConverter.ToDescFromService(userFromDb), nil +} diff --git a/internal/api/service.go b/internal/api/service.go new file mode 100644 index 0000000..da1324e --- /dev/null +++ b/internal/api/service.go @@ -0,0 +1,17 @@ +package api + +import ( + "auth/internal/service" + desc "auth/pkg/user_v1" +) + +type Implementation struct { + desc.UnimplementedUserV1Server + userService service.UserService +} + +func NewImplementation(userService service.UserService) *Implementation { + return &Implementation{ + userService: userService, + } +} diff --git a/internal/api/update.go b/internal/api/update.go new file mode 100644 index 0000000..fc21f87 --- /dev/null +++ b/internal/api/update.go @@ -0,0 +1,17 @@ +package api + +import ( + userServiceConverter "auth/internal/converter" + desc "auth/pkg/user_v1" + "context" + "github.com/golang/protobuf/ptypes/empty" +) + +func (i *Implementation) Update(ctx context.Context, req *desc.UpdateUserRequest) (*empty.Empty, error) { + err := i.userService.Update(ctx, userServiceConverter.ToUpdateUserInfoFromDesc(req)) + if err != nil { + return nil, err + } + + return &empty.Empty{}, nil +} diff --git a/internal/app/app.go b/internal/app/app.go new file mode 100644 index 0000000..e647548 --- /dev/null +++ b/internal/app/app.go @@ -0,0 +1,101 @@ +package app + +import ( + "auth/internal/closer" + "auth/internal/config" + desc "auth/pkg/user_v1" + "context" + "fmt" + "google.golang.org/grpc" + "google.golang.org/grpc/reflection" + "log" + "net" +) + +const ( + envPathName = "local.env" +) + +type App struct { + serviceProvider *serviceProvider + grpcServer *grpc.Server +} + +func NewApp(ctx context.Context) (*App, error) { + a := &App{} + + err := a.initDeps(ctx) + if err != nil { + return nil, err + } + + return a, nil +} + +func (a *App) Run() error { + defer func() { + closer.CloseAll() + closer.Wait() + }() + + return a.runGrpcServer() +} + +func (a *App) initDeps(ctx context.Context) error { + inits := []func(context.Context) error{ + a.initConfig, + a.initServiceProvider, + a.initGrpcServer, + } + + for _, f := range inits { + err := f(ctx) + if err != nil { + return err + } + } + + return nil +} + +func (a *App) initConfig(_ context.Context) error { + err := config.Load(envPathName) + if err != nil { + return err + } + + return nil +} + +func (a *App) initServiceProvider(_ context.Context) error { + a.serviceProvider = newServiceProvider() + + return nil +} + +func (a *App) initGrpcServer(ctx context.Context) error { + a.grpcServer = grpc.NewServer() + + reflection.Register(a.grpcServer) + desc.RegisterUserV1Server(a.grpcServer, a.serviceProvider.UserImpl(ctx)) + + return nil +} + +func (a *App) runGrpcServer() error { + log.Printf("GRPC server is running on %s", a.serviceProvider.GrpcConfig().Address()) + + port := a.serviceProvider.GrpcConfig().Address() + lis, err := net.Listen("tcp", port) + if err != nil { + return nil + } + + fmt.Println("Server has been started") + err = a.grpcServer.Serve(lis) + if err != nil { + return err + } + + return nil +} diff --git a/internal/app/service_provider.go b/internal/app/service_provider.go new file mode 100644 index 0000000..c22c294 --- /dev/null +++ b/internal/app/service_provider.go @@ -0,0 +1,120 @@ +package app + +import ( + "context" + "log" + + "auth/internal/api" + "auth/internal/client/db" + "auth/internal/client/db/pg" + "auth/internal/client/db/transaction" + "auth/internal/closer" + "auth/internal/config" + "auth/internal/repository" + "auth/internal/repository/user" + "auth/internal/repository/user_log" + "auth/internal/service" + userService "auth/internal/service/user" +) + +type serviceProvider struct { + pgConfig config.PGConfig + grpcConfig config.GRPCConfig + + pg db.Client + txManager db.TxManager + userLogRepository repository.UserLogRepository + userRepository repository.UserRepository + + userService service.UserService + + userImpl *api.Implementation +} + +func newServiceProvider() *serviceProvider { + return &serviceProvider{} +} + +func (s *serviceProvider) GrpcConfig() config.GRPCConfig { + if s.grpcConfig == nil { + grpcConfig, err := config.NewGrpcConfig() + if err != nil { + log.Fatalf("failed to create grpc config") + } + s.grpcConfig = grpcConfig + } + + return s.grpcConfig +} + +func (s *serviceProvider) PgConfig() config.PGConfig { + if s.pgConfig == nil { + pgConfig, err := config.NewPGConfig() + if err != nil { + log.Fatalf("failed to create pg config") + } + s.pgConfig = pgConfig + } + + return s.pgConfig +} + +func (s *serviceProvider) DBClient(ctx context.Context) db.Client { + if s.pg == nil { + cl, err := pg.New(ctx, s.PgConfig().DSN()) + if err != nil { + log.Fatalf("failed to connect to database: %v", err) + } + + err = cl.DB().Ping(ctx) + if err != nil { + log.Fatalf("ping error: %s", err) + } + + closer.Add(cl.Close) + + s.pg = cl + } + + return s.pg +} + +func (s *serviceProvider) TxManager(ctx context.Context) db.TxManager { + if s.txManager == nil { + s.txManager = transaction.NewTransactionManager(s.DBClient(ctx).DB()) + } + + return s.txManager +} + +func (s *serviceProvider) UserLogRepository(ctx context.Context) repository.UserLogRepository { + if s.userLogRepository == nil { + s.userLogRepository = user_log.NewUserLogRepository(s.DBClient(ctx)) + } + + return s.userLogRepository +} + +func (s *serviceProvider) UserRepository(ctx context.Context) repository.UserRepository { + if s.userRepository == nil { + s.userRepository = user.NewRepository(s.DBClient(ctx)) + } + + return s.userRepository +} + +func (s *serviceProvider) UserService(ctx context.Context) service.UserService { + if s.userService == nil { + s.userService = userService.NewService(s.TxManager(ctx), s.UserRepository(ctx), s.UserLogRepository(ctx)) + } + + return s.userService +} + +func (s *serviceProvider) UserImpl(ctx context.Context) *api.Implementation { + if s.userImpl == nil { + s.userImpl = api.NewImplementation(s.UserService(ctx)) + } + + return s.userImpl +} diff --git a/internal/client/db/db.go b/internal/client/db/db.go new file mode 100644 index 0000000..86cfddc --- /dev/null +++ b/internal/client/db/db.go @@ -0,0 +1,66 @@ +package db + +import ( + "context" + + "github.com/jackc/pgconn" + "github.com/jackc/pgx/v4" +) + +// Handler - функция, которая выполняется в транзакции +type Handler func(ctx context.Context) error + +// Client клиент для работы с БД +type Client interface { + DB() DB + Close() error +} + +// TxManager менеджер транзакций, который выполняет указанный пользователем обработчик в транзакции +type TxManager interface { + ReadCommitted(ctx context.Context, f Handler) error +} + +// Query обертка над запросом, хранящая имя запроса и сам запрос +// Имя запроса используется для логирования и потенциально может использоваться еще где-то, например, для трейсинга +type Query struct { + Name string + QueryRaw string +} + +// Transactor интерфейс для работы с транзакциями +type Transactor interface { + BeginTx(ctx context.Context, txOptions pgx.TxOptions) (pgx.Tx, error) +} + +// SQLExecer комбинирует NamedExecer и QueryExecer +type SQLExecer interface { + NamedExecer + QueryExecer +} + +// NamedExecer интерфейс для работы с именованными запросами с помощью тегов в структурах +type NamedExecer interface { + ScanOneContext(ctx context.Context, dest interface{}, q Query, args ...interface{}) error + ScanAllContext(ctx context.Context, dest interface{}, q Query, args ...interface{}) error +} + +// QueryExecer интерфейс для работы с обычными запросами +type QueryExecer interface { + ExecContext(ctx context.Context, q Query, args ...interface{}) (pgconn.CommandTag, error) + QueryContext(ctx context.Context, q Query, args ...interface{}) (pgx.Rows, error) + QueryRowContext(ctx context.Context, q Query, args ...interface{}) pgx.Row +} + +// Pinger интерфейс для проверки соединения с БД +type Pinger interface { + Ping(ctx context.Context) error +} + +// DB интерфейс для работы с БД +type DB interface { + SQLExecer + Transactor + Pinger + Close() +} diff --git a/internal/client/db/pg/client.go b/internal/client/db/pg/client.go new file mode 100644 index 0000000..16c03e5 --- /dev/null +++ b/internal/client/db/pg/client.go @@ -0,0 +1,37 @@ +package pg + +import ( + "context" + + "github.com/jackc/pgx/v4/pgxpool" + "github.com/pkg/errors" + + "auth/internal/client/db" +) + +type pgClient struct { + masterDBC db.DB +} + +func New(ctx context.Context, dsn string) (db.Client, error) { + dbc, err := pgxpool.Connect(ctx, dsn) + if err != nil { + return nil, errors.Errorf("failed to connect to db: %v", err) + } + + return &pgClient{ + masterDBC: &pg{dbc: dbc}, + }, nil +} + +func (c *pgClient) DB() db.DB { + return c.masterDBC +} + +func (c *pgClient) Close() error { + if c.masterDBC != nil { + c.masterDBC.Close() + } + + return nil +} diff --git a/internal/client/db/pg/pg.go b/internal/client/db/pg/pg.go new file mode 100644 index 0000000..567e3d5 --- /dev/null +++ b/internal/client/db/pg/pg.go @@ -0,0 +1,111 @@ +package pg + +import ( + "context" + "fmt" + "log" + + "github.com/georgysavva/scany/pgxscan" + "github.com/jackc/pgconn" + "github.com/jackc/pgx/v4" + "github.com/jackc/pgx/v4/pgxpool" + + "auth/internal/client/db" + "auth/internal/client/db/prettier" +) + +type key string + +const ( + TxKey key = "tx" +) + +type pg struct { + dbc *pgxpool.Pool +} + +func NewDB(dbc *pgxpool.Pool) db.DB { + return &pg{ + dbc: dbc, + } +} + +func (p *pg) ScanOneContext(ctx context.Context, dest interface{}, q db.Query, args ...interface{}) error { + logQuery(ctx, q, args...) + + row, err := p.QueryContext(ctx, q, args...) + if err != nil { + return err + } + + return pgxscan.ScanOne(dest, row) +} + +func (p *pg) ScanAllContext(ctx context.Context, dest interface{}, q db.Query, args ...interface{}) error { + logQuery(ctx, q, args...) + + rows, err := p.QueryContext(ctx, q, args...) + if err != nil { + return err + } + + return pgxscan.ScanAll(dest, rows) +} + +func (p *pg) ExecContext(ctx context.Context, q db.Query, args ...interface{}) (pgconn.CommandTag, error) { + logQuery(ctx, q, args...) + + tx, ok := ctx.Value(TxKey).(pgx.Tx) + if ok { + return tx.Exec(ctx, q.QueryRaw, args...) + } + + return p.dbc.Exec(ctx, q.QueryRaw, args...) +} + +func (p *pg) QueryContext(ctx context.Context, q db.Query, args ...interface{}) (pgx.Rows, error) { + logQuery(ctx, q, args...) + + tx, ok := ctx.Value(TxKey).(pgx.Tx) + if ok { + return tx.Query(ctx, q.QueryRaw, args...) + } + + return p.dbc.Query(ctx, q.QueryRaw, args...) +} + +func (p *pg) QueryRowContext(ctx context.Context, q db.Query, args ...interface{}) pgx.Row { + logQuery(ctx, q, args...) + + tx, ok := ctx.Value(TxKey).(pgx.Tx) + if ok { + return tx.QueryRow(ctx, q.QueryRaw, args...) + } + + return p.dbc.QueryRow(ctx, q.QueryRaw, args...) +} + +func (p *pg) BeginTx(ctx context.Context, txOptions pgx.TxOptions) (pgx.Tx, error) { + return p.dbc.BeginTx(ctx, txOptions) +} + +func (p *pg) Ping(ctx context.Context) error { + return p.dbc.Ping(ctx) +} + +func (p *pg) Close() { + p.dbc.Close() +} + +func MakeContextTx(ctx context.Context, tx pgx.Tx) context.Context { + return context.WithValue(ctx, TxKey, tx) +} + +func logQuery(ctx context.Context, q db.Query, args ...interface{}) { + prettyQuery := prettier.Pretty(q.QueryRaw, prettier.PlaceholderDollar, args...) + log.Println( + ctx, + fmt.Sprintf("sql: %s", q.Name), + fmt.Sprintf("query: %s", prettyQuery), + ) +} diff --git a/internal/client/db/prettier/querry_prettier.go b/internal/client/db/prettier/querry_prettier.go new file mode 100644 index 0000000..5a82fa6 --- /dev/null +++ b/internal/client/db/prettier/querry_prettier.go @@ -0,0 +1,33 @@ +package prettier + +import ( + "fmt" + "strconv" + "strings" +) + +const ( + PlaceholderDollar = "$" + PlaceholderQuestion = "?" +) + +func Pretty(query string, placeholder string, args ...any) string { + for i, param := range args { + var value string + switch v := param.(type) { + case string: + value = fmt.Sprintf("%q", v) + case []byte: + value = fmt.Sprintf("%q", string(v)) + default: + value = fmt.Sprintf("%v", v) + } + + query = strings.Replace(query, fmt.Sprintf("%s%s", placeholder, strconv.Itoa(i+1)), value, -1) + } + + query = strings.ReplaceAll(query, "\t", "") + query = strings.ReplaceAll(query, "\n", " ") + + return strings.TrimSpace(query) +} diff --git a/internal/client/db/transaction/transaction.go b/internal/client/db/transaction/transaction.go new file mode 100644 index 0000000..e95e1c7 --- /dev/null +++ b/internal/client/db/transaction/transaction.go @@ -0,0 +1,66 @@ +package transaction + +import ( + "auth/internal/client/db" + "auth/internal/client/db/pg" + "context" + "github.com/jackc/pgx/v4" + "github.com/pkg/errors" +) + +type manager struct { + db db.Transactor +} + +func NewTransactionManager(db db.Transactor) db.TxManager { + return &manager{ + db: db, + } +} + +func (m *manager) transaction(ctx context.Context, opts pgx.TxOptions, fn db.Handler) (err error) { + tx, ok := ctx.Value(pg.TxKey).(pgx.Tx) + if ok { + return fn(ctx) + } + + tx, err = m.db.BeginTx(ctx, opts) + if err != nil { + return errors.Wrapf(err, "failed to begix transaction: %s", err) + } + + ctx = pg.MakeContextTx(ctx, tx) + + defer func() { + if r := recover(); r != nil { + err = errors.Errorf("panic recovered: %v", r) + } + + if err != nil { + errRollback := tx.Rollback(ctx) + if errRollback != nil { + err = errors.Wrapf(err, "failed to rollback transaction: %s", err) + } + + return + } + + if err == nil { + err = tx.Commit(ctx) + if err != nil { + err = errors.Wrapf(err, "tx commit failed") + } + } + }() + + if err = fn(ctx); err != nil { + err = errors.Wrapf(err, "failed to execute tx func: %s", err) + } + + return err +} + +func (m *manager) ReadCommitted(ctx context.Context, f db.Handler) error { + txOpts := pgx.TxOptions{IsoLevel: pgx.ReadCommitted} + return m.transaction(ctx, txOpts, f) +} diff --git a/internal/closer/closer.go b/internal/closer/closer.go new file mode 100644 index 0000000..881effa --- /dev/null +++ b/internal/closer/closer.go @@ -0,0 +1,79 @@ +package closer + +import ( + "log" + "os" + "os/signal" + "sync" +) + +var globalCloser = New() + +func Add(f ...func() error) { + globalCloser.Add(f...) +} + +func Wait() { + globalCloser.Wait() +} + +func CloseAll() { + globalCloser.CloseAll() +} + +type Closer struct { + mu sync.Mutex + once sync.Once + done chan struct{} + funcs []func() error +} + +func New(sig ...os.Signal) *Closer { + c := &Closer{done: make(chan struct{})} + + if len(sig) > 0 { + go func() { + ch := make(chan os.Signal, 1) + signal.Notify(ch, sig...) + <-ch + signal.Stop(ch) + c.CloseAll() + }() + } + + return c +} + +func (c *Closer) Add(f ...func() error) { + c.mu.Lock() + c.funcs = append(c.funcs, f...) + c.mu.Unlock() +} + +func (c *Closer) Wait() { + <-c.done +} + +func (c *Closer) CloseAll() { + c.once.Do(func() { + defer close(c.done) + + c.mu.Lock() + funcs := c.funcs + c.funcs = nil + c.mu.Unlock() + + errs := make(chan error, len(funcs)) + for _, f := range funcs { + go func(f func() error) { + errs <- f() + }(f) + } + + for i := 0; i < cap(errs); i++ { + if err := <-errs; err != nil { + log.Println("error returned from Closer") + } + } + }) +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..798c433 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,12 @@ +package config + +import "github.com/joho/godotenv" + +func Load(path string) error { + err := godotenv.Load(path) + if err != nil { + return err + } + + return nil +} diff --git a/internal/config/grpc.go b/internal/config/grpc.go new file mode 100644 index 0000000..ef543a0 --- /dev/null +++ b/internal/config/grpc.go @@ -0,0 +1,38 @@ +package config + +import ( + "errors" + "net" + "os" +) + +const ( + grpcPortName = "GRPC_PORT" + grpcHostName = "GRPC_HOST" +) + +type GRPCConfig interface { + Address() string +} +type grpcConfig struct { + port string + host string +} + +func NewGrpcConfig() (GRPCConfig, error) { + port := os.Getenv(grpcPortName) + if len(port) == 0 { + return nil, errors.New("GRPC_PORT not found") + } + + host := os.Getenv(grpcHostName) + if len(host) == 0 { + return nil, errors.New("GRPC_HOST not found") + } + + return &grpcConfig{port: port, host: host}, nil +} + +func (g *grpcConfig) Address() string { + return net.JoinHostPort(g.host, g.port) +} diff --git a/internal/config/pg.go b/internal/config/pg.go new file mode 100644 index 0000000..20930bc --- /dev/null +++ b/internal/config/pg.go @@ -0,0 +1,31 @@ +package config + +import ( + "errors" + "os" +) + +const ( + dsnEnvName = "PG_DSN" +) + +type PGConfig interface { + DSN() string +} + +type pgConfig struct { + dsn string +} + +func NewPGConfig() (PGConfig, error) { + dsn := os.Getenv(dsnEnvName) + if len(dsn) == 0 { + return nil, errors.New("pg dsn not found") + } + + return &pgConfig{dsn: dsn}, nil +} + +func (cfg *pgConfig) DSN() string { + return cfg.dsn +} diff --git a/internal/converter/user.go b/internal/converter/user.go new file mode 100644 index 0000000..7cb09bd --- /dev/null +++ b/internal/converter/user.go @@ -0,0 +1,44 @@ +package converter + +import ( + "auth/internal/model" + desc "auth/pkg/user_v1" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func ToCreateUserInfoFromDesc(request *desc.CreateUserRequest) *model.CreateUserInfo { + user := request.User + + return &model.CreateUserInfo{ + Name: user.Name, + Email: user.Email, + Role: model.UserRole(user.Role.String()), + Password: user.Password, + PasswordConfirm: user.PasswordConfirm, + } +} + +func ToUpdateUserInfoFromDesc(request *desc.UpdateUserRequest) *model.UpdateUserInfo { + return &model.UpdateUserInfo{ + Id: request.Id, + Name: request.Name.Value, + Email: request.Email.Value, + Role: model.UserRole(request.Role.String()), + } +} + +func ToDescFromService(user *model.User) *desc.GetUserResponse { + var updatedAt *timestamppb.Timestamp + if user.UpdateAt != nil { + updatedAt = timestamppb.New(*user.UpdateAt) + } + + return &desc.GetUserResponse{ + Id: user.Id, + Name: user.Name, + Email: user.Email, + Role: desc.Role(desc.Role_value[string(user.Role)]), + CreatedAt: timestamppb.New(user.CreatedAt), + UpdatedAt: updatedAt, + } +} diff --git a/internal/model/user.go b/internal/model/user.go new file mode 100644 index 0000000..12ae686 --- /dev/null +++ b/internal/model/user.go @@ -0,0 +1,36 @@ +package model + +import ( + "time" +) + +type UserRole string + +const ( + USER_ROLE UserRole = "USER" + ADMIN_ROLE UserRole = "ADMIN" +) + +type CreateUserInfo struct { + Name string + Email string + Role UserRole + Password string + PasswordConfirm string +} + +type User struct { + Id int64 + Name string + Email string + Role UserRole + CreatedAt time.Time + UpdateAt *time.Time +} + +type UpdateUserInfo struct { + Id int64 + Name string + Email string + Role UserRole +} diff --git a/internal/repository/repository.go b/internal/repository/repository.go new file mode 100644 index 0000000..d75e635 --- /dev/null +++ b/internal/repository/repository.go @@ -0,0 +1,17 @@ +package repository + +import ( + "auth/internal/repository/user/model" + "context" +) + +type UserRepository interface { + Create(ctx context.Context, userInfo *model.CreateUserInfo) (int64, error) + Get(ctx context.Context, id int64) (*model.User, error) + Update(ctx context.Context, updateUserInfo *model.UpdateUserInfo) error + Delete(ctx context.Context, id int64) error +} + +type UserLogRepository interface { + Create(ctx context.Context, message string) error +} diff --git a/internal/repository/user/converter/user.go b/internal/repository/user/converter/user.go new file mode 100644 index 0000000..cede013 --- /dev/null +++ b/internal/repository/user/converter/user.go @@ -0,0 +1,41 @@ +package converter + +import ( + userServiceModel "auth/internal/model" + userRepoModel "auth/internal/repository/user/model" + "time" +) + +func ToCreateUserInfoFromService(userInfo *userServiceModel.CreateUserInfo) *userRepoModel.CreateUserInfo { + return &userRepoModel.CreateUserInfo{ + Name: userInfo.Name, + Email: userInfo.Email, + Role: userRepoModel.UserRole(userInfo.Role), + Password: userInfo.Password, + } +} + +func ToUpdateUserInfoFromService(userInfo *userServiceModel.UpdateUserInfo) *userRepoModel.UpdateUserInfo { + return &userRepoModel.UpdateUserInfo{ + Id: userInfo.Id, + Name: userInfo.Name, + Email: userInfo.Email, + Role: userRepoModel.UserRole(userInfo.Role), + } +} + +func ToUserFromRepo(user *userRepoModel.User) *userServiceModel.User { + var updatedAt *time.Time + if user.UpdatedAt.Valid { + updatedAt = &user.UpdatedAt.Time + } + + return &userServiceModel.User{ + Id: user.Id, + Name: user.Name, + Email: user.Email, + Role: userServiceModel.UserRole(user.Role), + CreatedAt: user.CreatedAt, + UpdateAt: updatedAt, + } +} diff --git a/internal/repository/user/model/user.go b/internal/repository/user/model/user.go new file mode 100644 index 0000000..311f71e --- /dev/null +++ b/internal/repository/user/model/user.go @@ -0,0 +1,36 @@ +package model + +import ( + "database/sql" + "time" +) + +type UserRole string + +const ( + USER_ROLE UserRole = "USER" + ADMIN_ROLE UserRole = "ADMIN" +) + +type User struct { + Id int64 `db:"id"` + Name string `db:"name"` + Email string `db:"email"` + Role UserRole `db:"role"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt sql.NullTime `db:"updated_at"` +} + +type CreateUserInfo struct { + Name string + Email string + Role UserRole + Password string +} + +type UpdateUserInfo struct { + Id int64 + Name string + Email string + Role UserRole +} diff --git a/internal/repository/user/repository.go b/internal/repository/user/repository.go new file mode 100644 index 0000000..395f593 --- /dev/null +++ b/internal/repository/user/repository.go @@ -0,0 +1,125 @@ +package user + +import ( + "context" + "fmt" + "time" + + "github.com/Masterminds/squirrel" + "github.com/pkg/errors" + + "auth/internal/client/db" + "auth/internal/repository" + "auth/internal/repository/user/model" +) + +const ( + tableName = "\"user\"" + + idColumn = "id" + nameColumn = "name" + emailColumn = "email" + roleColumn = "role" + passwordColumn = "password" + createdAtColumn = "created_at" + updatedAtColumn = "updated_at" +) + +type repo struct { + db db.Client +} + +func NewRepository(db db.Client) repository.UserRepository { + return &repo{ + db: db, + } +} + +func (r *repo) Create(ctx context.Context, userInfo *model.CreateUserInfo) (int64, error) { + insertBuilder := squirrel.Insert(tableName). + PlaceholderFormat(squirrel.Dollar). + Columns(nameColumn, emailColumn, roleColumn, passwordColumn). + Values(userInfo.Name, userInfo.Email, userInfo.Role, userInfo.Password). + Suffix("RETURNING id") + + query, args, err := insertBuilder.ToSql() + if err != nil { + return 0, errors.New("failed to build query") + } + + var id int64 + + ro, err := r.db.DB().ExecContext(ctx, db.Query{Name: "Insert user", QueryRaw: query}, args...) + fmt.Println(ro) + if err != nil { + return 0, errors.New("failed to make query") + } + + return id, nil +} + +func (r *repo) Get(ctx context.Context, id int64) (*model.User, error) { + selectBuilder := squirrel.Select( + idColumn, + nameColumn, + emailColumn, + roleColumn, + createdAtColumn, + updatedAtColumn, + ). + PlaceholderFormat(squirrel.Dollar). + From(tableName). + Where(squirrel.Eq{idColumn: id}) + + query, args, err := selectBuilder.ToSql() + if err != nil { + return nil, errors.New("failed to build query") + } + + var user = &model.User{} + err = r.db.DB().ScanOneContext(ctx, user, db.Query{Name: "user_repository.Get", QueryRaw: query}, args...) + if err != nil { + return nil, errors.New("failed to make query") + } + + return user, nil +} + +func (r *repo) Update(ctx context.Context, updateUserInfo *model.UpdateUserInfo) error { + builderUpdate := squirrel.Update(tableName). + PlaceholderFormat(squirrel.Dollar). + Set(emailColumn, updateUserInfo.Email). + Set(nameColumn, updateUserInfo.Name). + Set(roleColumn, updateUserInfo.Role). + Set(updatedAtColumn, time.Now()). + Where(squirrel.Eq{idColumn: updateUserInfo.Id}) + + query, args, err := builderUpdate.ToSql() + if err != nil { + return errors.New("failed to build query") + } + + _, err = r.db.DB().ExecContext(ctx, db.Query{Name: "user_repository.Update", QueryRaw: query}, args...) + if err != nil { + return errors.New("failed to executed query") + } + + return nil +} + +func (r *repo) Delete(ctx context.Context, id int64) error { + deleteBuilder := squirrel.Delete(tableName). + PlaceholderFormat(squirrel.Dollar). + Where(squirrel.Eq{idColumn: id}) + + query, args, err := deleteBuilder.ToSql() + if err != nil { + return errors.New("failed to build query") + } + _, err = r.db.DB().ExecContext(ctx, db.Query{Name: "user_repository.Delete", QueryRaw: query}, args...) + if err != nil { + return errors.New("failed to execute") + } + + return nil +} diff --git a/internal/repository/user_log/repository.go b/internal/repository/user_log/repository.go new file mode 100644 index 0000000..d518f51 --- /dev/null +++ b/internal/repository/user_log/repository.go @@ -0,0 +1,46 @@ +package user_log + +import ( + "context" + + "github.com/Masterminds/squirrel" + "github.com/pkg/errors" + + "auth/internal/client/db" + "auth/internal/repository" +) + +const ( + tableName = "user_log" + messageColumn = "message" +) + +type repo struct { + db db.Client +} + +func NewUserLogRepository(db db.Client) repository.UserLogRepository { + return &repo{ + db: db, + } +} + +func (r repo) Create(ctx context.Context, message string) error { + insertBuilder := squirrel.Insert(tableName). + PlaceholderFormat(squirrel.Dollar). + Columns(messageColumn). + Values(message). + Suffix("RETURNING id") + + query, args, err := insertBuilder.ToSql() + if err != nil { + return errors.Wrapf(err, "failed to build query: %s", err) + } + + _, err = r.db.DB().ExecContext(ctx, db.Query{Name: "user_log_repository.Create", QueryRaw: query}, args...) + if err != nil { + return errors.Wrapf(err, "failed to make query: %s", err) + } + + return nil +} diff --git a/internal/service/service.go b/internal/service/service.go new file mode 100644 index 0000000..896a92f --- /dev/null +++ b/internal/service/service.go @@ -0,0 +1,13 @@ +package service + +import ( + "auth/internal/model" + "context" +) + +type UserService interface { + Create(ctx context.Context, createUserInfo *model.CreateUserInfo) (int64, error) + Get(ctx context.Context, id int64) (*model.User, error) + Update(ctx context.Context, updateUserInfo *model.UpdateUserInfo) error + Delete(ctx context.Context, id int64) error +} diff --git a/internal/service/user/create.go b/internal/service/user/create.go new file mode 100644 index 0000000..888dc19 --- /dev/null +++ b/internal/service/user/create.go @@ -0,0 +1,32 @@ +package user + +import ( + "auth/internal/model" + "auth/internal/repository/user/converter" + "context" + "errors" +) + +func (s serv) Create(ctx context.Context, createUserInfo *model.CreateUserInfo) (int64, error) { + if createUserInfo.Password != createUserInfo.PasswordConfirm { + return 0, errors.New("passwords should be equal") + } + + var userId int64 + err := s.txManager.ReadCommitted(ctx, func(ctx context.Context) error { + id, errTx := s.userRepository.Create(ctx, converter.ToCreateUserInfoFromService(createUserInfo)) + if errTx != nil { + return errTx + } + userId = id + + return errors.New("asd") + + return s.userLogRepository.Create(ctx, "user_created") + }) + if err != nil { + return 0, err + } + + return userId, nil +} diff --git a/internal/service/user/delete.go b/internal/service/user/delete.go new file mode 100644 index 0000000..a8a3c71 --- /dev/null +++ b/internal/service/user/delete.go @@ -0,0 +1,9 @@ +package user + +import "context" + +func (s serv) Delete(ctx context.Context, id int64) error { + err := s.userRepository.Delete(ctx, id) + + return err +} diff --git a/internal/service/user/get.go b/internal/service/user/get.go new file mode 100644 index 0000000..2fc9e44 --- /dev/null +++ b/internal/service/user/get.go @@ -0,0 +1,16 @@ +package user + +import ( + "auth/internal/model" + "auth/internal/repository/user/converter" + "context" +) + +func (s serv) Get(ctx context.Context, id int64) (*model.User, error) { + user, err := s.userRepository.Get(ctx, id) + if err != nil { + return nil, err + } + + return converter.ToUserFromRepo(user), nil +} diff --git a/internal/service/user/service.go b/internal/service/user/service.go new file mode 100644 index 0000000..10ccc52 --- /dev/null +++ b/internal/service/user/service.go @@ -0,0 +1,28 @@ +package user + +import ( + "auth/internal/client/db" + "auth/internal/repository" + "auth/internal/service" +) + +type serv struct { + txManager db.TxManager + + userRepository repository.UserRepository + userLogRepository repository.UserLogRepository +} + +var _ service.UserService = (*serv)(nil) + +func NewService( + txManager db.TxManager, + userRepository repository.UserRepository, + userLogRepository repository.UserLogRepository, +) service.UserService { + return &serv{ + txManager, + userRepository, + userLogRepository, + } +} diff --git a/internal/service/user/update.go b/internal/service/user/update.go new file mode 100644 index 0000000..14b355b --- /dev/null +++ b/internal/service/user/update.go @@ -0,0 +1,13 @@ +package user + +import ( + "auth/internal/model" + "auth/internal/repository/user/converter" + "context" +) + +func (s serv) Update(ctx context.Context, updateUserInfo *model.UpdateUserInfo) error { + err := s.userRepository.Update(ctx, converter.ToUpdateUserInfoFromService(updateUserInfo)) + + return err +} diff --git a/local.env b/local.env index 548644c..658ac89 100644 --- a/local.env +++ b/local.env @@ -1,5 +1,10 @@ -POSTGRES_DB=course -POSTGRES_USER=dev_course +POSTGRES_DB=auth-service +POSTGRES_USER=dev-course POSTGRES_PASSWORD=1801 PG_PORT=54321 -MIGRATION_DR=./migrations \ No newline at end of file +MIGRATION_DR=./migrations + +PG_DSN="host=localhost port=54321 dbname=auth-service user=dev-course password=1801 sslmode=disable" + +GRPC_PORT=50051 +GRPC_HOST=localhost \ No newline at end of file diff --git a/migrations/20231031070651_create_user_log_table.sql b/migrations/20231031070651_create_user_log_table.sql new file mode 100644 index 0000000..a3fb116 --- /dev/null +++ b/migrations/20231031070651_create_user_log_table.sql @@ -0,0 +1,12 @@ +-- +goose Up +-- +goose StatementBegin +create table user_log ( + id int primary key generated always as identity, + message text not null +); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +drop table user_log; +-- +goose StatementEnd