From 3834565b568f024387773d73f3874d7e3b210c8e Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Thu, 5 Jun 2025 15:48:20 +0530 Subject: [PATCH 01/27] create contribution fetching for all users in the system from bigquery service (in future this will be a cron job that will be scheduled to run daily) --- .gitignore | 4 +- cmd/main.go | 13 ++- go.mod | 44 +++++++- go.sum | 87 ++++++++++++++++ internal/app/bigquery/domain.go | 15 +++ internal/app/bigquery/service.go | 87 ++++++++++++++++ internal/app/contribution/domain.go | 40 ++++++++ internal/app/contribution/handler.go | 35 +++++++ internal/app/contribution/service.go | 148 +++++++++++++++++++++++++++ internal/app/dependencies.go | 36 +++++-- internal/app/repository/domain.go | 30 ++++++ internal/app/repository/service.go | 91 ++++++++++++++++ internal/app/router.go | 1 + internal/config/app.go | 18 ++-- internal/config/bigquery.go | 23 +++++ internal/pkg/apperrors/errors.go | 17 +-- internal/repository/base.go | 3 +- internal/repository/contribution.go | 73 +++++++++++++ internal/repository/domain.go | 26 +++++ internal/repository/repository.go | 111 ++++++++++++++++++++ internal/repository/user.go | 25 +++++ 21 files changed, 897 insertions(+), 30 deletions(-) create mode 100644 internal/app/bigquery/domain.go create mode 100644 internal/app/bigquery/service.go create mode 100644 internal/app/contribution/domain.go create mode 100644 internal/app/contribution/handler.go create mode 100644 internal/app/contribution/service.go create mode 100644 internal/app/repository/domain.go create mode 100644 internal/app/repository/service.go create mode 100644 internal/config/bigquery.go create mode 100644 internal/repository/contribution.go create mode 100644 internal/repository/repository.go diff --git a/.gitignore b/.gitignore index 9890de4..6f3ff51 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -local.yaml \ No newline at end of file +local.yaml + +*.ps1 \ No newline at end of file diff --git a/cmd/main.go b/cmd/main.go index 35ae97a..fd6692d 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -6,7 +6,7 @@ import ( "log/slog" "net/http" "os" - + "os/signal" "syscall" "time" @@ -18,13 +18,12 @@ import ( func main() { ctx := context.Background() - cfg,err := config.LoadAppConfig() + cfg, err := config.LoadAppConfig() if err != nil { slog.Error("error loading app config", "error", err) return } - db, err := config.InitDataStore(cfg) if err != nil { slog.Error("error initializing database", "error", err) @@ -32,7 +31,13 @@ func main() { } defer db.Close() - dependencies := app.InitDependencies(db,cfg) + bigqueryInstance, err := config.BigqueryInit(ctx, cfg) + if err != nil { + slog.Error("error initializing bigquery", "error", err) + return + } + + dependencies := app.InitDependencies(db, cfg, bigqueryInstance) router := app.NewRouter(dependencies) diff --git a/go.mod b/go.mod index 79cfc35..93d5a95 100644 --- a/go.mod +++ b/go.mod @@ -11,10 +11,52 @@ require ( ) require ( + cloud.google.com/go v0.121.0 // indirect + cloud.google.com/go/auth v0.16.1 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect + cloud.google.com/go/bigquery v1.68.0 // indirect + cloud.google.com/go/compute/metadata v0.6.0 // indirect + cloud.google.com/go/iam v1.5.2 // indirect github.com/BurntSushi/toml v1.2.1 // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/apache/arrow/go/v15 v15.0.2 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/google/flatbuffers v23.5.26+incompatible // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/s2a-go v0.1.9 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect + github.com/googleapis/gax-go/v2 v2.14.1 // indirect github.com/joho/godotenv v1.5.1 // indirect + github.com/klauspost/compress v1.16.7 // indirect + github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/kr/pretty v0.3.1 // indirect + github.com/pierrec/lz4/v4 v4.1.18 // indirect + github.com/zeebo/xxh3 v1.0.2 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 // indirect + go.opentelemetry.io/otel v1.35.0 // indirect + go.opentelemetry.io/otel/metric v1.35.0 // indirect + go.opentelemetry.io/otel/trace v1.35.0 // indirect + golang.org/x/crypto v0.37.0 // indirect + golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect + golang.org/x/mod v0.23.0 // indirect + golang.org/x/net v0.39.0 // indirect + golang.org/x/sync v0.14.0 // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/text v0.24.0 // indirect + golang.org/x/time v0.11.0 // indirect + golang.org/x/tools v0.30.0 // indirect + golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect + google.golang.org/api v0.231.0 // indirect + google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 // indirect + google.golang.org/grpc v1.72.0 // indirect + google.golang.org/protobuf v1.36.6 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect diff --git a/go.sum b/go.sum index b892459..2bbeb64 100644 --- a/go.sum +++ b/go.sum @@ -1,20 +1,58 @@ +cloud.google.com/go v0.121.0 h1:pgfwva8nGw7vivjZiRfrmglGWiCJBP+0OmDpenG/Fwg= +cloud.google.com/go v0.121.0/go.mod h1:rS7Kytwheu/y9buoDmu5EIpMMCI4Mb8ND4aeN4Vwj7Q= +cloud.google.com/go/auth v0.16.1 h1:XrXauHMd30LhQYVRHLGvJiYeczweKQXZxsTbV9TiguU= +cloud.google.com/go/auth v0.16.1/go.mod h1:1howDHJ5IETh/LwYs3ZxvlkXF48aSqqJUM+5o02dNOI= +cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= +cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= +cloud.google.com/go/bigquery v1.68.0 h1:F+CPqdcMxZGUDBACzGtOJ1E6E0MWSYcKeFthxnhpYIU= +cloud.google.com/go/bigquery v1.68.0/go.mod h1:1UAksG8IFXJomQV38xUsRB+2m2c1H9U0etvoGHgyhDk= +cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= +cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= +cloud.google.com/go/iam v1.5.2 h1:qgFRAGEmd8z6dJ/qyEchAuL9jpswyODjA2lS+w234g8= +cloud.google.com/go/iam v1.5.2/go.mod h1:SE1vg0N81zQqLzQEwxL2WI6yhetBdbNQuTvIKCSkUHE= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/apache/arrow/go/v15 v15.0.2 h1:60IliRbiyTWCWjERBCkO1W4Qun9svcYoZrSLcyOsMLE= +github.com/apache/arrow/go/v15 v15.0.2/go.mod h1:DGXsR3ajT524njufqf95822i+KTh+yea1jass9YXgjA= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/google/flatbuffers v23.5.26+incompatible h1:M9dgRyhJemaM4Sw8+66GHBu8ioaQmyPLg1b8VwK5WJg= +github.com/google/flatbuffers v23.5.26+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= +github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= +github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= github.com/ilyakaznacheev/cleanenv v1.5.0 h1:0VNZXggJE2OYdXE87bfSSwGxeiGt9moSR2lOrsHHvr4= github.com/ilyakaznacheev/cleanenv v1.5.0/go.mod h1:a5aDzaJrLCQZsazHol1w8InnDcOX0OColm64SlIi6gk= github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= +github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= +github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -26,11 +64,60 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ= +github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= +github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0 h1:sbiXRNDSWJOTobXh5HyQKjq6wUC5tNybqjIqDpAY4CU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.60.0/go.mod h1:69uWxva0WgAA/4bu2Yy70SLDBwZXuQ6PbBpbsa5iZrQ= +go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ= +go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y= +go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M= +go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE= +go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs= +go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= +golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= +golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= +golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= +golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY= +golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +google.golang.org/api v0.231.0 h1:LbUD5FUl0C4qwia2bjXhCMH65yz1MLPzA/0OYEsYY7Q= +google.golang.org/api v0.231.0/go.mod h1:H52180fPI/QQlUc0F4xWfGZILdv09GCWKt2bcsn164A= +google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb h1:ITgPrl429bc6+2ZraNSzMDk3I95nmQln2fuPstKwFDE= +google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:sAo5UzpjUwgFBCzupwhcLcxHVDK7vG5IqI30YnwX2eE= +google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34 h1:0PeQib/pH3nB/5pEmFeVQJotzGohV0dq4Vcp09H5yhE= +google.golang.org/genproto/googleapis/api v0.0.0-20250428153025-10db94c68c34/go.mod h1:0awUlEkap+Pb1UMeJwJQQAdJQrt3moU7J2moTy69irI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34 h1:h6p3mQqrmT1XkHVTfzLdNz1u7IhINeZkz67/xTbOuWs= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250428153025-10db94c68c34/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM= +google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/internal/app/bigquery/domain.go b/internal/app/bigquery/domain.go new file mode 100644 index 0000000..3a8575a --- /dev/null +++ b/internal/app/bigquery/domain.go @@ -0,0 +1,15 @@ +package bigquery + +import "time" + +type ContributionResponse struct { + ID string `bigquery:"id"` + Type string `bigquery:"type"` + ActorID int `bigquery:"actor_id"` + ActorLogin string `bigquery:"actor_login"` + RepoID int `bigquery:"repo_id"` + RepoName string `bigquery:"repo_name"` + RepoUrl string `bigquery:"repo_url"` + Payload string `bigquery:"payload"` + CreatedAt time.Time `bigquery:"created_at"` +} diff --git a/internal/app/bigquery/service.go b/internal/app/bigquery/service.go new file mode 100644 index 0000000..5b623e4 --- /dev/null +++ b/internal/app/bigquery/service.go @@ -0,0 +1,87 @@ +package bigquery + +import ( + "context" + "fmt" + "log/slog" + "strings" + "time" + + bq "cloud.google.com/go/bigquery" + "github.com/joshsoftware/code-curiosity-2025/internal/config" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" + "github.com/joshsoftware/code-curiosity-2025/internal/repository" +) + +type service struct { + bigqueryInstance config.Bigquery + userRepository repository.UserRepository +} + +type Service interface { + FetchDailyContributions(ctx context.Context) (*bq.RowIterator, error) +} + +func NewService(bigqueryInstance config.Bigquery) Service { + return &service{ + bigqueryInstance: bigqueryInstance, + } +} + +func (s *service) FetchDailyContributions(ctx context.Context) (*bq.RowIterator, error) { + YesterdayDate := time.Now().AddDate(0, 0, -1) + YesterdayYearMonthDay := YesterdayDate.Format("20030101") + + usersNamesList, err := s.userRepository.GetAllUsersGithubUsernames(ctx, nil) + if err != nil { + slog.Error("error fetching users github usernames") + return nil, apperrors.ErrInternalServer + } + + var quotedUsernamesList []string + for _, username := range usersNamesList { + quotedUsernamesList = append(quotedUsernamesList, fmt.Sprintf("'%s'", username)) + } + + githubUsernames := strings.Join(quotedUsernamesList, ",") + fetchDailyContributionsQuery := fmt.Sprintf(` +SELECT + id, + type, + public, + actor.id AS actor_id, + actor.login AS actor_login, + actor.gravatar_id AS actor_gravatar_id, + actor.url AS actor_url, + actor.avatar_url AS actor_avatar_url, + repo.id AS repo_id, + repo.name AS repo_name, + repo.url AS repo_url, + payload, + created_at, + other +FROM + githubarchive.day.%s +WHERE + type IN ( + 'IssuesEvent', + 'PullRequestEvent', + 'PullRequestReviewEvent', + 'IssueCommentEvent', + 'PullRequestReviewCommentEvent' + ) + AND ( + actor.login IN (%s) OR + JSON_EXTRACT_SCALAR(payload, "$.pull_request.user.login") IN (%s) + ) +`, YesterdayYearMonthDay, githubUsernames, githubUsernames) + + bigqueryQuery := s.bigqueryInstance.Client.Query(fetchDailyContributionsQuery) + contributionRows, err := bigqueryQuery.Read(ctx) + if err != nil { + slog.Error("error fetching contributions", "error", err) + return nil, err + } + + return contributionRows, err +} diff --git a/internal/app/contribution/domain.go b/internal/app/contribution/domain.go new file mode 100644 index 0000000..2017962 --- /dev/null +++ b/internal/app/contribution/domain.go @@ -0,0 +1,40 @@ +package contribution + +import "time" + +type ContributionResponse struct { + ID string `bigquery:"id"` + Type string `bigquery:"type"` + ActorID int `bigquery:"actor_id"` + ActorLogin string `bigquery:"actor_login"` + RepoID int `bigquery:"repo_id"` + RepoName string `bigquery:"repo_name"` + RepoUrl string `bigquery:"repo_url"` + Payload string `bigquery:"payload"` + CreatedAt time.Time `bigquery:"created_at"` +} + +type Repository struct { + Id int + GithubRepoId int + RepoName string + Description string + LanguagesUrl string + RepoUrl string + OwnerName string + UpdateDate time.Time + CreatedAt int64 + UpdatedAt int64 +} + +type Contribution struct { + Id int + UserId int + RepositoryId int + ContributionScoreId int + ContributionType string + BalanceChange int + ContributedAt time.Time + CreatedAt int64 + UpdatedAt int64 +} diff --git a/internal/app/contribution/handler.go b/internal/app/contribution/handler.go new file mode 100644 index 0000000..b680d18 --- /dev/null +++ b/internal/app/contribution/handler.go @@ -0,0 +1,35 @@ +package contribution + +import ( + "log/slog" + "net/http" + + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/response" +) + +type handler struct { + contributionService Service +} + +type Handler interface { + FetchUserLatestContributions(w http.ResponseWriter, r *http.Request) +} + +func NewHandler(contributionService Service) Handler { + return &handler{ + contributionService: contributionService, + } +} + +func (h *handler) FetchUserLatestContributions(w http.ResponseWriter, r *http.Request) { + + ctx := r.Context() + + err := h.contributionService.ProcessFetchedContributions(ctx) + if err != nil { + slog.Error("error fetching latest contributions") + return + } + + response.WriteJson(w, http.StatusOK, "contribution fetched successfully", nil) +} diff --git a/internal/app/contribution/service.go b/internal/app/contribution/service.go new file mode 100644 index 0000000..62ff3c0 --- /dev/null +++ b/internal/app/contribution/service.go @@ -0,0 +1,148 @@ +package contribution + +import ( + "context" + "encoding/json" + "log/slog" + + "github.com/joshsoftware/code-curiosity-2025/internal/app/bigquery" + repoService "github.com/joshsoftware/code-curiosity-2025/internal/app/repository" + "github.com/joshsoftware/code-curiosity-2025/internal/repository" + "google.golang.org/api/iterator" +) + +type service struct { + bigqueryService bigquery.Service + contributionRepository repository.ContributionRepository + repositoryService repoService.Service +} + +type Service interface { + ProcessFetchedContributions(ctx context.Context) error + CreateContribution(ctx context.Context, contributionType string, contributionDetails ContributionResponse, repositoryId int) (Contribution, error) +} + +func NewService(bigqueryService bigquery.Service, contributionRepository repository.ContributionRepository, repositoryService repoService.Service) Service { + return &service{ + bigqueryService: bigqueryService, + contributionRepository: contributionRepository, + repositoryService: repositoryService, + } +} + +func (s *service) ProcessFetchedContributions(ctx context.Context) error { + contributions, err := s.bigqueryService.FetchDailyContributions(ctx) + if err != nil { + slog.Error("error fetching daily contributions", "error", err) + return err + } + + for { + var contribution ContributionResponse + if err := contributions.Next(&contribution); err == iterator.Done { + break + } else if err != nil { + slog.Error("error iterating contribution rows", "error", err) + break + } + + var contributionPayload map[string]interface{} + err := json.Unmarshal([]byte(contribution.Payload), &contributionPayload) + if err != nil { + slog.Warn("invalid payload", "error", err) + continue + } + + var action string + if actionVal, ok := contributionPayload["action"]; ok { + action = actionVal.(string) + } + + var pullRequest map[string]interface{} + var isMerged bool + if pullRequestPayload, ok := contributionPayload["pull_request"]; ok { + pullRequest = pullRequestPayload.(map[string]interface{}) + isMerged = pullRequest["merged"].(bool) + } + + var issue map[string]interface{} + var stateReason string + if issuePayload, ok := contributionPayload["issue"]; ok { + issue = issuePayload.(map[string]interface{}) + stateReason = issue["state_reason"].(string) + } + + var contributionType string + switch contribution.Type { + case "PullRequestEvent": + if action == "closed" && isMerged { + contributionType = "PullRequestMerged" + } else if action == "opened" { + contributionType = "PullRequestOpened" + } + + case "IssuesEvent": + if action == "opened" { + contributionType = "IssueOpened" + } else if action == "closed" && stateReason == "not_planned" { + contributionType = "IssueClosed" + } else if action == "closed" && stateReason == "completed" { + contributionType = "IssueResolved" + } + + case "PushEvent": + contributionType = "PullRequestUpdated" + + case "IssueCommentEvent": + contributionType = "IssueComment" + + case "PullRequestComment ": + contributionType = "PullRequestComment" + } + + repoFetched, err := s.repositoryService.GetRepoByGithubId(ctx, contribution.RepoID) + repositoryId := repoFetched.Id + if err != nil { + repo, err := s.repositoryService.FetchRepositoryDetails(ctx, contribution.RepoUrl) + if err != nil { + slog.Error("error fetching repository details") + return err + } + + repositoryCreated, err := s.repositoryService.CreateRepository(ctx, contribution.RepoID, repo) + if err != nil { + slog.Error("error creating repository", "error", err) + return err + } + + repositoryId = repositoryCreated.Id + } + + _, err = s.CreateContribution(ctx, contributionType, contribution, repositoryId) + if err != nil { + slog.Error("error creating contribution", "error", err) + return err + } + } + return nil +} + +func (s *service) CreateContribution(ctx context.Context, contributionType string, contributionDetails ContributionResponse, repositoryId int) (Contribution, error) { + contribution := Contribution{ + UserId: contributionDetails.ActorID, + RepositoryId: repositoryId, + ContributionType: contributionType, + //get id and balance from contribution_score_id table by sending it contribution_type (hardcoded for now) + ContributionScoreId: 1, + BalanceChange: 10, + ContributedAt: contributionDetails.CreatedAt, + } + + contributionResponse, err := s.contributionRepository.CreateContribution(ctx, nil, repository.Contribution(contribution)) + if err != nil { + slog.Error("error creating contribution", "error", err) + return Contribution{}, err + } + + return Contribution(contributionResponse), nil +} diff --git a/internal/app/dependencies.go b/internal/app/dependencies.go index fa49370..57a0660 100644 --- a/internal/app/dependencies.go +++ b/internal/app/dependencies.go @@ -3,33 +3,47 @@ package app import ( "github.com/jmoiron/sqlx" "github.com/joshsoftware/code-curiosity-2025/internal/app/auth" + "github.com/joshsoftware/code-curiosity-2025/internal/app/bigquery" + "github.com/joshsoftware/code-curiosity-2025/internal/app/contribution" + repoService "github.com/joshsoftware/code-curiosity-2025/internal/app/repository" "github.com/joshsoftware/code-curiosity-2025/internal/app/user" "github.com/joshsoftware/code-curiosity-2025/internal/config" + "github.com/joshsoftware/code-curiosity-2025/internal/repository" ) type Dependencies struct { - AuthService auth.Service - UserService user.Service - AuthHandler auth.Handler - UserHandler user.Handler - AppCfg config.AppConfig + AuthService auth.Service + UserService user.Service + AuthHandler auth.Handler + UserHandler user.Handler + ContributionHandler contribution.Handler + AppCfg config.AppConfig + Client config.Bigquery } -func InitDependencies(db *sqlx.DB, appCfg config.AppConfig) Dependencies { +func InitDependencies(db *sqlx.DB, appCfg config.AppConfig, client config.Bigquery) Dependencies { userRepository := repository.NewUserRepository(db) + contributionRepository := repository.NewContributionRepository(db) + repositoryRepository := repository.NewRepositoryRepository(db) userService := user.NewService(userRepository) authService := auth.NewService(userService, appCfg) + bigqueryService := bigquery.NewService(client) + repositoryService := repoService.NewService(repositoryRepository, appCfg) + contributionService := contribution.NewService(bigqueryService, contributionRepository, repositoryService) authHandler := auth.NewHandler(authService, appCfg) userHandler := user.NewHandler(userService) + contributionHandler := contribution.NewHandler(contributionService) return Dependencies{ - AuthService: authService, - UserService: userService, - AuthHandler: authHandler, - UserHandler: userHandler, - AppCfg: appCfg, + AuthService: authService, + UserService: userService, + AuthHandler: authHandler, + UserHandler: userHandler, + ContributionHandler: contributionHandler, + AppCfg: appCfg, + Client: client, } } diff --git a/internal/app/repository/domain.go b/internal/app/repository/domain.go new file mode 100644 index 0000000..b2da319 --- /dev/null +++ b/internal/app/repository/domain.go @@ -0,0 +1,30 @@ +package repository + +import "time" + +type RepoOWner struct { + Login string `json:"login"` +} + +type FetchRepositoryDetailsResponse struct { + Id int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + LanguagesURL string `json:"languages_url"` + UpdateDate time.Time `json:"updated_at"` + RepoOwnerName RepoOWner `json:"owner"` + RepoUrl string `json:"html_url"` +} + +type Repository struct { + Id int + GithubRepoId int + RepoName string + Description string + LanguagesUrl string + RepoUrl string + OwnerName string + UpdateDate time.Time + CreatedAt int64 + UpdatedAt int64 +} diff --git a/internal/app/repository/service.go b/internal/app/repository/service.go new file mode 100644 index 0000000..6b449d8 --- /dev/null +++ b/internal/app/repository/service.go @@ -0,0 +1,91 @@ +package repository + +import ( + "context" + "encoding/json" + "io" + "log/slog" + "net/http" + + "github.com/joshsoftware/code-curiosity-2025/internal/config" + "github.com/joshsoftware/code-curiosity-2025/internal/repository" +) + +type service struct { + repositoryRepository repository.RepositoryRepository + appCfg config.AppConfig +} + +type Service interface { + GetRepoByGithubId(ctx context.Context, githubRepoId int) (Repository, error) + FetchRepositoryDetails(ctx context.Context, getUserRepoDetailsUrl string) (FetchRepositoryDetailsResponse, error) + CreateRepository(ctx context.Context, repoGithubId int, repo FetchRepositoryDetailsResponse) (Repository, error) +} + +func NewService(repositoryRepository repository.RepositoryRepository, appCfg config.AppConfig) Service { + return &service{ + repositoryRepository: repositoryRepository, + appCfg: appCfg, + } +} + +func (s *service) GetRepoByGithubId(ctx context.Context, repoGithubId int) (Repository, error) { + repoDetails, err := s.repositoryRepository.GetRepoByGithubId(ctx, nil, repoGithubId) + if err != nil { + slog.Error("failed to get repository by github id") + return Repository{}, err + } + + return Repository(repoDetails), nil +} + +func (s *service) FetchRepositoryDetails(ctx context.Context, getUserRepoDetailsUrl string) (FetchRepositoryDetailsResponse, error) { + req, err := http.NewRequest("GET", getUserRepoDetailsUrl, nil) + if err != nil { + slog.Error("error fetching user repositories details", "error", err) + return FetchRepositoryDetailsResponse{}, err + } + + req.Header.Add("Authorization", s.appCfg.GithubPersonalAccessToken) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + slog.Error("error fetching user repositories details", "error", err) + return FetchRepositoryDetailsResponse{}, err + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + slog.Error("error freading body", "error", err) + return FetchRepositoryDetailsResponse{}, err + } + + var repoDetails FetchRepositoryDetailsResponse + err = json.Unmarshal(body, &repoDetails) + if err != nil { + slog.Error("error unmarshalling fetch repository details body", "error", err) + return FetchRepositoryDetailsResponse{}, err + } + + return repoDetails, nil +} + +func (s *service) CreateRepository(ctx context.Context, repoGithubId int, repo FetchRepositoryDetailsResponse) (Repository, error) { + createRepo := Repository{ + GithubRepoId: repoGithubId, + RepoName: repo.Name, + RepoUrl: repo.RepoUrl, + Description: repo.Description, + LanguagesUrl: repo.LanguagesURL, + OwnerName: repo.RepoOwnerName.Login, + UpdateDate: repo.UpdateDate, + } + repositoryCreated, err := s.repositoryRepository.CreateRepository(ctx, nil, repository.Repository(createRepo)) + if err != nil { + slog.Error("failed to create repository", "error", err) + return Repository{}, err + } + + return Repository(repositoryCreated), nil +} diff --git a/internal/app/router.go b/internal/app/router.go index 072a53a..d844c3c 100644 --- a/internal/app/router.go +++ b/internal/app/router.go @@ -20,5 +20,6 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("PATCH /api/v1/user/email", middleware.Authentication(deps.UserHandler.UpdateUserEmail, deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/contributions/recent", middleware.Authentication(deps.ContributionHandler.FetchUserLatestContributions, deps.AppCfg)) return middleware.CorsMiddleware(router, deps.AppCfg) } diff --git a/internal/config/app.go b/internal/config/app.go index c4715f5..4070c0f 100644 --- a/internal/config/app.go +++ b/internal/config/app.go @@ -25,13 +25,19 @@ type GithubOauth struct { RedirectURL string `yaml:"redirect_url" required:"true"` } +type BigqueryProject struct { + ProjectID string `yaml:"project_id" required:"true"` +} + type AppConfig struct { - IsProduction bool `yaml:"is_production"` - HTTPServer HTTPServer `yaml:"http_server"` - Database Database `yaml:"database"` - JWTSecret string `yaml:"jwt_secret"` - ClientURL string `yaml:"client_url"` - GithubOauth GithubOauth `yaml:"github_oauth"` + IsProduction bool `yaml:"is_production"` + HTTPServer HTTPServer `yaml:"http_server"` + Database Database `yaml:"database"` + JWTSecret string `yaml:"jwt_secret"` + ClientURL string `yaml:"client_url"` + GithubOauth GithubOauth `yaml:"github_oauth"` + BigqueryProject BigqueryProject `yaml:"bigquery_project"` + GithubPersonalAccessToken string `yaml:"github_personal_access_token"` } func LoadAppConfig() (AppConfig, error) { diff --git a/internal/config/bigquery.go b/internal/config/bigquery.go new file mode 100644 index 0000000..30294c5 --- /dev/null +++ b/internal/config/bigquery.go @@ -0,0 +1,23 @@ +package config + +import ( + "context" + + "cloud.google.com/go/bigquery" +) + +type Bigquery struct { + Client *bigquery.Client +} + +func BigqueryInit(ctx context.Context, appCfg AppConfig) (Bigquery, error) { + client, err := bigquery.NewClient(ctx, appCfg.BigqueryProject.ProjectID) + if err != nil { + return Bigquery{}, err + } + + bigqueryInstance := Bigquery{ + Client: client, + } + return bigqueryInstance, nil +} diff --git a/internal/pkg/apperrors/errors.go b/internal/pkg/apperrors/errors.go index 5c7244d..beeba07 100644 --- a/internal/pkg/apperrors/errors.go +++ b/internal/pkg/apperrors/errors.go @@ -20,16 +20,21 @@ var ( ErrNoAppConfigPath = errors.New("no config path provided") ErrFailedToLoadAppConfig = errors.New("failed to load environment configuration") - ErrLoginWithGithubFailed = errors.New("failed to login with Github") + ErrLoginWithGithubFailed = errors.New("failed to login with Github") ErrGithubTokenExchangeFailed = errors.New("failed to exchange Github token") - ErrFailedToGetGithubUser = errors.New("failed to get Github user info") - ErrFailedToGetUserEmail = errors.New("failed to get user email from Github") + ErrFailedToGetGithubUser = errors.New("failed to get Github user info") + ErrFailedToGetUserEmail = errors.New("failed to get user email from Github") - ErrUserNotFound = errors.New("user not found") + ErrUserNotFound = errors.New("user not found") ErrUserCreationFailed = errors.New("failed to create user") - ErrJWTCreationFailed = errors.New("failed to create jwt token") - ErrAuthorizationFailed=errors.New("failed to authorize user") + ErrJWTCreationFailed = errors.New("failed to create jwt token") + ErrAuthorizationFailed = errors.New("failed to authorize user") + + ErrRepoNotFound = errors.New("repository not found") + ErrRepoCreationFailed = errors.New("failed to create repo for user") + + ErrContributionCreationFailed = errors.New("failed to create contrbitution") ) func MapError(err error) (statusCode int, errMessage string) { diff --git a/internal/repository/base.go b/internal/repository/base.go index 5516997..a38e9ba 100644 --- a/internal/repository/base.go +++ b/internal/repository/base.go @@ -23,6 +23,7 @@ type RepositoryTransaction interface { type QueryExecuter interface { QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) + QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) } func (b *BaseRepository) BeginTx(ctx context.Context) (*sqlx.Tx, error) { @@ -61,7 +62,7 @@ func (b *BaseRepository) HandleTransaction(ctx context.Context, tx *sqlx.Tx, inc } return nil } - + err := tx.Commit() if err != nil { slog.Error("error occurred while committing database transaction", "error", err) diff --git a/internal/repository/contribution.go b/internal/repository/contribution.go new file mode 100644 index 0000000..8b9f9ff --- /dev/null +++ b/internal/repository/contribution.go @@ -0,0 +1,73 @@ +package repository + +import ( + "context" + "log/slog" + "time" + + "github.com/jmoiron/sqlx" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" +) + +type contributionRepository struct { + BaseRepository +} + +type ContributionRepository interface { + RepositoryTransaction + CreateContribution(ctx context.Context, tx *sqlx.Tx, contributionDetails Contribution) (Contribution, error) +} + +func NewContributionRepository(db *sqlx.DB) ContributionRepository { + return &contributionRepository{ + BaseRepository: BaseRepository{db}, + } +} + +const ( + createContributionQuery = ` + INSERT INTO contributions ( + user_id, + repository_id, + contribution_score_id, + contribution_type, + balance_change, + contributed_at, + created_at, + updated_at + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + RETURNING *` +) + +func (cr *contributionRepository) CreateContribution(ctx context.Context, tx *sqlx.Tx, contributionInfo Contribution) (Contribution, error) { + executer := cr.BaseRepository.initiateQueryExecuter(tx) + + var contribution Contribution + err := executer.QueryRowContext(ctx, createContributionQuery, + contributionInfo.UserId, + contributionInfo.RepositoryId, + contributionInfo.ContributionScoreId, + contributionInfo.ContributionType, + contributionInfo.BalanceChange, + contributionInfo.ContributedAt, + time.Now().Unix(), + time.Now().Unix(), + ).Scan( + &contribution.Id, + &contribution.UserId, + &contribution.RepositoryId, + &contribution.ContributionScoreId, + &contribution.ContributionType, + &contribution.BalanceChange, + &contribution.ContributedAt, + &contribution.CreatedAt, + &contribution.UpdatedAt, + ) + if err != nil { + slog.Error("error occured while inserting contributions", "error", err) + return Contribution{}, apperrors.ErrContributionCreationFailed + } + + return contribution, err +} diff --git a/internal/repository/domain.go b/internal/repository/domain.go index b8f6e57..90b2066 100644 --- a/internal/repository/domain.go +++ b/internal/repository/domain.go @@ -2,6 +2,7 @@ package repository import ( "database/sql" + "time" ) type User struct { @@ -28,3 +29,28 @@ type CreateUserRequestBody struct { Email string IsAdmin bool } + +type Contribution struct { + Id int + UserId int + RepositoryId int + ContributionScoreId int + ContributionType string + BalanceChange int + ContributedAt time.Time + CreatedAt int64 + UpdatedAt int64 +} + +type Repository struct { + Id int + GithubRepoId int + RepoName string + Description string + LanguagesUrl string + RepoUrl string + OwnerName string + UpdateDate time.Time + CreatedAt int64 + UpdatedAt int64 +} diff --git a/internal/repository/repository.go b/internal/repository/repository.go new file mode 100644 index 0000000..1ed9e2f --- /dev/null +++ b/internal/repository/repository.go @@ -0,0 +1,111 @@ +package repository + +import ( + "context" + "database/sql" + "errors" + "log/slog" + "time" + + "github.com/jmoiron/sqlx" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" +) + +type repositoryRepository struct { + BaseRepository +} + +type RepositoryRepository interface { + RepositoryTransaction + GetRepoByGithubId(ctx context.Context, tx *sqlx.Tx, repoGithubId int) (Repository, error) + CreateRepository(ctx context.Context, tx *sqlx.Tx, repository Repository) (Repository, error) +} + +func NewRepositoryRepository(db *sqlx.DB) RepositoryRepository { + return &repositoryRepository{ + BaseRepository: BaseRepository{db}, + } +} + +const ( + getRepoByGithubIdQuery = `SELECT * from repositories where github_repo_id=$1` + + createRepositoryQuery = ` + INSERT INTO repositories ( + github_repo_id, + repo_name, + description, + languages_url, + repo_url, + owner_name, + update_date, + created_at, + updated_at + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) + RETURNING *` +) + +func (rr *repositoryRepository) GetRepoByGithubId(ctx context.Context, tx *sqlx.Tx, repoGithubId int) (Repository, error) { + executer := rr.BaseRepository.initiateQueryExecuter(tx) + + var repository Repository + err := executer.QueryRowContext(ctx, getRepoByGithubIdQuery, repoGithubId).Scan( + &repository.Id, + &repository.GithubRepoId, + &repository.RepoName, + &repository.Description, + &repository.LanguagesUrl, + &repository.RepoUrl, + &repository.OwnerName, + &repository.UpdateDate, + &repository.CreatedAt, + &repository.UpdatedAt, + ) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + slog.Error("repository not found", "error", err) + return Repository{}, apperrors.ErrRepoNotFound + } + slog.Error("error occurred while getting repository by id", "error", err) + return Repository{}, apperrors.ErrInternalServer + } + + return repository, nil + +} + +func (rr *repositoryRepository) CreateRepository(ctx context.Context, tx *sqlx.Tx, repositoryInfo Repository) (Repository, error) { + executer := rr.BaseRepository.initiateQueryExecuter(tx) + + var repository Repository + err := executer.QueryRowContext(ctx, createRepositoryQuery, + repositoryInfo.GithubRepoId, + repositoryInfo.RepoName, + repositoryInfo.Description, + repositoryInfo.LanguagesUrl, + repositoryInfo.RepoUrl, + repositoryInfo.OwnerName, + repositoryInfo.UpdateDate, + time.Now().Unix(), + time.Now().Unix(), + ).Scan( + &repository.Id, + &repository.GithubRepoId, + &repository.RepoName, + &repository.Description, + &repository.LanguagesUrl, + &repository.RepoUrl, + &repository.OwnerName, + &repository.UpdateDate, + &repository.CreatedAt, + &repository.UpdatedAt, + ) + if err != nil { + slog.Error("error occured while creating repository", "error", err) + return Repository{}, apperrors.ErrInternalServer + } + + return repository, nil + +} diff --git a/internal/repository/user.go b/internal/repository/user.go index c7fb093..bf2a713 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -21,6 +21,7 @@ type UserRepository interface { GetUserByGithubId(ctx context.Context, tx *sqlx.Tx, githubId int) (User, error) CreateUser(ctx context.Context, tx *sqlx.Tx, userInfo CreateUserRequestBody) (User, error) UpdateUserEmail(ctx context.Context, tx *sqlx.Tx, userId int, email string) error + GetAllUsersGithubUsernames(ctx context.Context, tx *sqlx.Tx) ([]string, error) } func NewUserRepository(db *sqlx.DB) UserRepository { @@ -47,6 +48,8 @@ const ( RETURNING *` updateEmailQuery = "UPDATE users SET email=$1 where id=$2" + + getAllUsersGithubUsernamesQuery = "SELECT github_username from users" ) func (ur *userRepository) GetUserById(ctx context.Context, tx *sqlx.Tx, userId int) (User, error) { @@ -160,3 +163,25 @@ func (ur *userRepository) UpdateUserEmail(ctx context.Context, tx *sqlx.Tx, Id i return nil } + +func (ur *userRepository) GetAllUsersGithubUsernames(ctx context.Context, tx *sqlx.Tx) ([]string, error) { + executer := ur.BaseRepository.initiateQueryExecuter(tx) + + rows, err := executer.QueryContext(ctx, getAllUsersGithubUsernamesQuery) + if err != nil { + slog.Error("failed to get github usernames", "error", err) + return nil, apperrors.ErrInternalServer + } + defer rows.Close() + + var githubUsernames []string + for rows.Next() { + var username string + if err := rows.Scan(&username); err != nil { + return nil, err + } + githubUsernames = append(githubUsernames, username) + } + + return githubUsernames, nil +} From d3d91a4dfa87e38b91536d9c863cebff0ab08ba7 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Mon, 9 Jun 2025 21:18:19 +0530 Subject: [PATCH 02/27] use userid while adding contribution --- internal/app/contribution/domain.go | 8 ++++---- internal/app/contribution/service.go | 21 +++++++++++++++------ internal/app/dependencies.go | 4 ++-- internal/app/repository/domain.go | 4 ++-- internal/app/repository/service.go | 4 ++-- internal/repository/contribution.go | 9 ++------- internal/repository/domain.go | 8 ++++---- internal/repository/repository.go | 9 ++------- internal/repository/user.go | 1 - 9 files changed, 33 insertions(+), 35 deletions(-) diff --git a/internal/app/contribution/domain.go b/internal/app/contribution/domain.go index 2017962..e1eb0ea 100644 --- a/internal/app/contribution/domain.go +++ b/internal/app/contribution/domain.go @@ -23,8 +23,8 @@ type Repository struct { RepoUrl string OwnerName string UpdateDate time.Time - CreatedAt int64 - UpdatedAt int64 + CreatedAt time.Time + UpdatedAt time.Time } type Contribution struct { @@ -35,6 +35,6 @@ type Contribution struct { ContributionType string BalanceChange int ContributedAt time.Time - CreatedAt int64 - UpdatedAt int64 + CreatedAt time.Time + UpdatedAt time.Time } diff --git a/internal/app/contribution/service.go b/internal/app/contribution/service.go index 62ff3c0..0756582 100644 --- a/internal/app/contribution/service.go +++ b/internal/app/contribution/service.go @@ -7,6 +7,7 @@ import ( "github.com/joshsoftware/code-curiosity-2025/internal/app/bigquery" repoService "github.com/joshsoftware/code-curiosity-2025/internal/app/repository" + "github.com/joshsoftware/code-curiosity-2025/internal/app/user" "github.com/joshsoftware/code-curiosity-2025/internal/repository" "google.golang.org/api/iterator" ) @@ -15,18 +16,20 @@ type service struct { bigqueryService bigquery.Service contributionRepository repository.ContributionRepository repositoryService repoService.Service + userService user.Service } type Service interface { ProcessFetchedContributions(ctx context.Context) error - CreateContribution(ctx context.Context, contributionType string, contributionDetails ContributionResponse, repositoryId int) (Contribution, error) + CreateContribution(ctx context.Context, contributionType string, contributionDetails ContributionResponse, repositoryId int, userId int) (Contribution, error) } -func NewService(bigqueryService bigquery.Service, contributionRepository repository.ContributionRepository, repositoryService repoService.Service) Service { +func NewService(bigqueryService bigquery.Service, contributionRepository repository.ContributionRepository, repositoryService repoService.Service, userService user.Service) Service { return &service{ bigqueryService: bigqueryService, contributionRepository: contributionRepository, repositoryService: repositoryService, + userService: userService, } } @@ -100,7 +103,7 @@ func (s *service) ProcessFetchedContributions(ctx context.Context) error { contributionType = "PullRequestComment" } - repoFetched, err := s.repositoryService.GetRepoByGithubId(ctx, contribution.RepoID) + repoFetched, err := s.repositoryService.GetRepoByRepoId(ctx, contribution.RepoID) repositoryId := repoFetched.Id if err != nil { repo, err := s.repositoryService.FetchRepositoryDetails(ctx, contribution.RepoUrl) @@ -118,7 +121,13 @@ func (s *service) ProcessFetchedContributions(ctx context.Context) error { repositoryId = repositoryCreated.Id } - _, err = s.CreateContribution(ctx, contributionType, contribution, repositoryId) + user, err := s.userService.GetUserByGithubId(ctx, contribution.ActorID) + if err != nil { + slog.Error("error getting user id", "error", err) + return err + } + + _, err = s.CreateContribution(ctx, contributionType, contribution, repositoryId, user.Id) if err != nil { slog.Error("error creating contribution", "error", err) return err @@ -127,9 +136,9 @@ func (s *service) ProcessFetchedContributions(ctx context.Context) error { return nil } -func (s *service) CreateContribution(ctx context.Context, contributionType string, contributionDetails ContributionResponse, repositoryId int) (Contribution, error) { +func (s *service) CreateContribution(ctx context.Context, contributionType string, contributionDetails ContributionResponse, repositoryId int, userId int) (Contribution, error) { contribution := Contribution{ - UserId: contributionDetails.ActorID, + UserId: userId, RepositoryId: repositoryId, ContributionType: contributionType, //get id and balance from contribution_score_id table by sending it contribution_type (hardcoded for now) diff --git a/internal/app/dependencies.go b/internal/app/dependencies.go index 57a0660..7f56523 100644 --- a/internal/app/dependencies.go +++ b/internal/app/dependencies.go @@ -29,9 +29,9 @@ func InitDependencies(db *sqlx.DB, appCfg config.AppConfig, client config.Bigque userService := user.NewService(userRepository) authService := auth.NewService(userService, appCfg) - bigqueryService := bigquery.NewService(client) + bigqueryService := bigquery.NewService(client, userRepository) repositoryService := repoService.NewService(repositoryRepository, appCfg) - contributionService := contribution.NewService(bigqueryService, contributionRepository, repositoryService) + contributionService := contribution.NewService(bigqueryService, contributionRepository, repositoryService, userService) authHandler := auth.NewHandler(authService, appCfg) userHandler := user.NewHandler(userService) diff --git a/internal/app/repository/domain.go b/internal/app/repository/domain.go index b2da319..3c0a80c 100644 --- a/internal/app/repository/domain.go +++ b/internal/app/repository/domain.go @@ -25,6 +25,6 @@ type Repository struct { RepoUrl string OwnerName string UpdateDate time.Time - CreatedAt int64 - UpdatedAt int64 + CreatedAt time.Time + UpdatedAt time.Time } diff --git a/internal/app/repository/service.go b/internal/app/repository/service.go index 6b449d8..b598f1b 100644 --- a/internal/app/repository/service.go +++ b/internal/app/repository/service.go @@ -17,7 +17,7 @@ type service struct { } type Service interface { - GetRepoByGithubId(ctx context.Context, githubRepoId int) (Repository, error) + GetRepoByRepoId(ctx context.Context, githubRepoId int) (Repository, error) FetchRepositoryDetails(ctx context.Context, getUserRepoDetailsUrl string) (FetchRepositoryDetailsResponse, error) CreateRepository(ctx context.Context, repoGithubId int, repo FetchRepositoryDetailsResponse) (Repository, error) } @@ -29,7 +29,7 @@ func NewService(repositoryRepository repository.RepositoryRepository, appCfg con } } -func (s *service) GetRepoByGithubId(ctx context.Context, repoGithubId int) (Repository, error) { +func (s *service) GetRepoByRepoId(ctx context.Context, repoGithubId int) (Repository, error) { repoDetails, err := s.repositoryRepository.GetRepoByGithubId(ctx, nil, repoGithubId) if err != nil { slog.Error("failed to get repository by github id") diff --git a/internal/repository/contribution.go b/internal/repository/contribution.go index 8b9f9ff..807c646 100644 --- a/internal/repository/contribution.go +++ b/internal/repository/contribution.go @@ -3,7 +3,6 @@ package repository import ( "context" "log/slog" - "time" "github.com/jmoiron/sqlx" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" @@ -32,11 +31,9 @@ const ( contribution_score_id, contribution_type, balance_change, - contributed_at, - created_at, - updated_at + contributed_at ) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + VALUES ($1, $2, $3, $4, $5, $6) RETURNING *` ) @@ -51,8 +48,6 @@ func (cr *contributionRepository) CreateContribution(ctx context.Context, tx *sq contributionInfo.ContributionType, contributionInfo.BalanceChange, contributionInfo.ContributedAt, - time.Now().Unix(), - time.Now().Unix(), ).Scan( &contribution.Id, &contribution.UserId, diff --git a/internal/repository/domain.go b/internal/repository/domain.go index f817e75..5dcab55 100644 --- a/internal/repository/domain.go +++ b/internal/repository/domain.go @@ -38,8 +38,8 @@ type Contribution struct { ContributionType string BalanceChange int ContributedAt time.Time - CreatedAt int64 - UpdatedAt int64 + CreatedAt time.Time + UpdatedAt time.Time } type Repository struct { @@ -51,6 +51,6 @@ type Repository struct { RepoUrl string OwnerName string UpdateDate time.Time - CreatedAt int64 - UpdatedAt int64 + CreatedAt time.Time + UpdatedAt time.Time } diff --git a/internal/repository/repository.go b/internal/repository/repository.go index 1ed9e2f..32a77b6 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -5,7 +5,6 @@ import ( "database/sql" "errors" "log/slog" - "time" "github.com/jmoiron/sqlx" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" @@ -38,11 +37,9 @@ const ( languages_url, repo_url, owner_name, - update_date, - created_at, - updated_at + update_date ) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9) + VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING *` ) @@ -87,8 +84,6 @@ func (rr *repositoryRepository) CreateRepository(ctx context.Context, tx *sqlx.T repositoryInfo.RepoUrl, repositoryInfo.OwnerName, repositoryInfo.UpdateDate, - time.Now().Unix(), - time.Now().Unix(), ).Scan( &repository.Id, &repository.GithubRepoId, diff --git a/internal/repository/user.go b/internal/repository/user.go index 3d77131..c504048 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -162,7 +162,6 @@ func (ur *userRepository) UpdateUserEmail(ctx context.Context, tx *sqlx.Tx, user func (ur *userRepository) GetAllUsersGithubUsernames(ctx context.Context, tx *sqlx.Tx) ([]string, error) { executer := ur.BaseRepository.initiateQueryExecuter(tx) - rows, err := executer.QueryContext(ctx, getAllUsersGithubUsernamesQuery) if err != nil { slog.Error("failed to get github usernames", "error", err) From 1c1135f2cf2764ab5e8f4fdbe1706f1a66c40712 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Mon, 9 Jun 2025 21:52:52 +0530 Subject: [PATCH 03/27] get id and score from contribution_score --- internal/app/bigquery/service.go | 8 ++++---- internal/app/contribution/domain.go | 9 +++++++++ internal/app/contribution/service.go | 25 +++++++++++++++++++++---- internal/repository/contribution.go | 23 +++++++++++++++++++++++ internal/repository/domain.go | 9 +++++++++ 5 files changed, 66 insertions(+), 8 deletions(-) diff --git a/internal/app/bigquery/service.go b/internal/app/bigquery/service.go index 5b623e4..9d00a48 100644 --- a/internal/app/bigquery/service.go +++ b/internal/app/bigquery/service.go @@ -22,9 +22,10 @@ type Service interface { FetchDailyContributions(ctx context.Context) (*bq.RowIterator, error) } -func NewService(bigqueryInstance config.Bigquery) Service { +func NewService(bigqueryInstance config.Bigquery, userRepository repository.UserRepository) Service { return &service{ bigqueryInstance: bigqueryInstance, + userRepository: userRepository, } } @@ -71,10 +72,9 @@ WHERE 'PullRequestReviewCommentEvent' ) AND ( - actor.login IN (%s) OR - JSON_EXTRACT_SCALAR(payload, "$.pull_request.user.login") IN (%s) + actor.login IN (%s) ) -`, YesterdayYearMonthDay, githubUsernames, githubUsernames) +`, YesterdayYearMonthDay, githubUsernames) bigqueryQuery := s.bigqueryInstance.Client.Query(fetchDailyContributionsQuery) contributionRows, err := bigqueryQuery.Read(ctx) diff --git a/internal/app/contribution/domain.go b/internal/app/contribution/domain.go index e1eb0ea..b736d91 100644 --- a/internal/app/contribution/domain.go +++ b/internal/app/contribution/domain.go @@ -38,3 +38,12 @@ type Contribution struct { CreatedAt time.Time UpdatedAt time.Time } + +type ContributionScore struct { + Id int + AdminId int + ContributionType string + Score int + CreatedAt time.Time + UpdatedAt time.Time +} diff --git a/internal/app/contribution/service.go b/internal/app/contribution/service.go index 0756582..fbd025d 100644 --- a/internal/app/contribution/service.go +++ b/internal/app/contribution/service.go @@ -22,6 +22,7 @@ type service struct { type Service interface { ProcessFetchedContributions(ctx context.Context) error CreateContribution(ctx context.Context, contributionType string, contributionDetails ContributionResponse, repositoryId int, userId int) (Contribution, error) + GetContributionScoreDetailsByContributionType(ctx context.Context, contributionType string) (ContributionScore, error) } func NewService(bigqueryService bigquery.Service, contributionRepository repository.ContributionRepository, repositoryService repoService.Service, userService user.Service) Service { @@ -137,16 +138,23 @@ func (s *service) ProcessFetchedContributions(ctx context.Context) error { } func (s *service) CreateContribution(ctx context.Context, contributionType string, contributionDetails ContributionResponse, repositoryId int, userId int) (Contribution, error) { + contribution := Contribution{ UserId: userId, RepositoryId: repositoryId, ContributionType: contributionType, - //get id and balance from contribution_score_id table by sending it contribution_type (hardcoded for now) - ContributionScoreId: 1, - BalanceChange: 10, - ContributedAt: contributionDetails.CreatedAt, + ContributedAt: contributionDetails.CreatedAt, + } + + contributionScoreDetails, err := s.GetContributionScoreDetailsByContributionType(ctx, contributionType) + if err != nil { + slog.Error("error occured while getting contribution score details", "error", err) + return Contribution{}, err } + contribution.ContributionScoreId = contributionScoreDetails.Id + contribution.BalanceChange = contributionScoreDetails.Score + contributionResponse, err := s.contributionRepository.CreateContribution(ctx, nil, repository.Contribution(contribution)) if err != nil { slog.Error("error creating contribution", "error", err) @@ -155,3 +163,12 @@ func (s *service) CreateContribution(ctx context.Context, contributionType strin return Contribution(contributionResponse), nil } + +func (s *service) GetContributionScoreDetailsByContributionType(ctx context.Context, contributionType string) (ContributionScore, error) { + contributionScoreDetails, err := s.contributionRepository.GetContributionScoreDetailsByContributionType(ctx, nil, contributionType) + if err != nil { + slog.Error("error occured while getting contribution score details", "error", err) + return ContributionScore{}, err + } + return ContributionScore(contributionScoreDetails), nil +} diff --git a/internal/repository/contribution.go b/internal/repository/contribution.go index 807c646..7105069 100644 --- a/internal/repository/contribution.go +++ b/internal/repository/contribution.go @@ -15,6 +15,7 @@ type contributionRepository struct { type ContributionRepository interface { RepositoryTransaction CreateContribution(ctx context.Context, tx *sqlx.Tx, contributionDetails Contribution) (Contribution, error) + GetContributionScoreDetailsByContributionType(ctx context.Context, tx *sqlx.Tx, contributionType string) (ContributionScore, error) } func NewContributionRepository(db *sqlx.DB) ContributionRepository { @@ -35,6 +36,8 @@ const ( ) VALUES ($1, $2, $3, $4, $5, $6) RETURNING *` + + getContributionScoreDetailsByContributionTypeQuery = `SELECT * from contribution_score where contribution_type=$1` ) func (cr *contributionRepository) CreateContribution(ctx context.Context, tx *sqlx.Tx, contributionInfo Contribution) (Contribution, error) { @@ -66,3 +69,23 @@ func (cr *contributionRepository) CreateContribution(ctx context.Context, tx *sq return contribution, err } + +func (cr *contributionRepository) GetContributionScoreDetailsByContributionType(ctx context.Context, tx *sqlx.Tx, contributionType string) (ContributionScore, error) { + executer := cr.BaseRepository.initiateQueryExecuter(tx) + + var contributionScoreDetails ContributionScore + err := executer.QueryRowContext(ctx, getContributionScoreDetailsByContributionTypeQuery, contributionType).Scan( + &contributionScoreDetails.Id, + &contributionScoreDetails.AdminId, + &contributionScoreDetails.ContributionType, + &contributionScoreDetails.Score, + &contributionScoreDetails.CreatedAt, + &contributionScoreDetails.UpdatedAt, + ) + if err != nil { + slog.Error("error occured while getting contribution score details", "error", err) + return ContributionScore{}, err + } + + return contributionScoreDetails, nil +} diff --git a/internal/repository/domain.go b/internal/repository/domain.go index 5dcab55..13cedc5 100644 --- a/internal/repository/domain.go +++ b/internal/repository/domain.go @@ -54,3 +54,12 @@ type Repository struct { CreatedAt time.Time UpdatedAt time.Time } + +type ContributionScore struct { + Id int + AdminId int + ContributionType string + Score int + CreatedAt time.Time + UpdatedAt time.Time +} \ No newline at end of file From f94000e1954463a5a1f4920e0ce35067a6f81ee4 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Mon, 9 Jun 2025 22:00:32 +0530 Subject: [PATCH 04/27] fix date format --- internal/app/bigquery/service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/app/bigquery/service.go b/internal/app/bigquery/service.go index 9d00a48..7a15d7c 100644 --- a/internal/app/bigquery/service.go +++ b/internal/app/bigquery/service.go @@ -31,7 +31,7 @@ func NewService(bigqueryInstance config.Bigquery, userRepository repository.User func (s *service) FetchDailyContributions(ctx context.Context) (*bq.RowIterator, error) { YesterdayDate := time.Now().AddDate(0, 0, -1) - YesterdayYearMonthDay := YesterdayDate.Format("20030101") + YesterdayYearMonthDay := YesterdayDate.Format("20060102") usersNamesList, err := s.userRepository.GetAllUsersGithubUsernames(ctx, nil) if err != nil { From a17215cf33717feb000de2d8813cbe823eaece0b Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Mon, 16 Jun 2025 15:50:07 +0530 Subject: [PATCH 05/27] refactor processfetchedcontribution function and update http client handling while fetching repositoryDetails --- internal/app/contribution/handler.go | 3 +- internal/app/contribution/service.go | 125 +++++++++++++++------------ internal/app/repository/service.go | 5 +- 3 files changed, 74 insertions(+), 59 deletions(-) diff --git a/internal/app/contribution/handler.go b/internal/app/contribution/handler.go index b680d18..195debb 100644 --- a/internal/app/contribution/handler.go +++ b/internal/app/contribution/handler.go @@ -25,7 +25,8 @@ func (h *handler) FetchUserLatestContributions(w http.ResponseWriter, r *http.Re ctx := r.Context() - err := h.contributionService.ProcessFetchedContributions(ctx) + client := &http.Client{} + err := h.contributionService.ProcessFetchedContributions(ctx, client) if err != nil { slog.Error("error fetching latest contributions") return diff --git a/internal/app/contribution/service.go b/internal/app/contribution/service.go index fbd025d..b38a151 100644 --- a/internal/app/contribution/service.go +++ b/internal/app/contribution/service.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "log/slog" + "net/http" "github.com/joshsoftware/code-curiosity-2025/internal/app/bigquery" repoService "github.com/joshsoftware/code-curiosity-2025/internal/app/repository" @@ -20,7 +21,7 @@ type service struct { } type Service interface { - ProcessFetchedContributions(ctx context.Context) error + ProcessFetchedContributions(ctx context.Context, client *http.Client) error CreateContribution(ctx context.Context, contributionType string, contributionDetails ContributionResponse, repositoryId int, userId int) (Contribution, error) GetContributionScoreDetailsByContributionType(ctx context.Context, contributionType string) (ContributionScore, error) } @@ -34,7 +35,7 @@ func NewService(bigqueryService bigquery.Service, contributionRepository reposit } } -func (s *service) ProcessFetchedContributions(ctx context.Context) error { +func (s *service) ProcessFetchedContributions(ctx context.Context, client *http.Client) error { contributions, err := s.bigqueryService.FetchDailyContributions(ctx) if err != nil { slog.Error("error fetching daily contributions", "error", err) @@ -50,64 +51,16 @@ func (s *service) ProcessFetchedContributions(ctx context.Context) error { break } - var contributionPayload map[string]interface{} - err := json.Unmarshal([]byte(contribution.Payload), &contributionPayload) + contributionType, err := s.GetContributionType(ctx, contribution) if err != nil { - slog.Warn("invalid payload", "error", err) - continue - } - - var action string - if actionVal, ok := contributionPayload["action"]; ok { - action = actionVal.(string) - } - - var pullRequest map[string]interface{} - var isMerged bool - if pullRequestPayload, ok := contributionPayload["pull_request"]; ok { - pullRequest = pullRequestPayload.(map[string]interface{}) - isMerged = pullRequest["merged"].(bool) - } - - var issue map[string]interface{} - var stateReason string - if issuePayload, ok := contributionPayload["issue"]; ok { - issue = issuePayload.(map[string]interface{}) - stateReason = issue["state_reason"].(string) - } - - var contributionType string - switch contribution.Type { - case "PullRequestEvent": - if action == "closed" && isMerged { - contributionType = "PullRequestMerged" - } else if action == "opened" { - contributionType = "PullRequestOpened" - } - - case "IssuesEvent": - if action == "opened" { - contributionType = "IssueOpened" - } else if action == "closed" && stateReason == "not_planned" { - contributionType = "IssueClosed" - } else if action == "closed" && stateReason == "completed" { - contributionType = "IssueResolved" - } - - case "PushEvent": - contributionType = "PullRequestUpdated" - - case "IssueCommentEvent": - contributionType = "IssueComment" - - case "PullRequestComment ": - contributionType = "PullRequestComment" + slog.Error("error getting contribution type") + return err } + var repositoryId int repoFetched, err := s.repositoryService.GetRepoByRepoId(ctx, contribution.RepoID) - repositoryId := repoFetched.Id if err != nil { - repo, err := s.repositoryService.FetchRepositoryDetails(ctx, contribution.RepoUrl) + repo, err := s.repositoryService.FetchRepositoryDetails(ctx, client, contribution.RepoUrl) if err != nil { slog.Error("error fetching repository details") return err @@ -120,6 +73,8 @@ func (s *service) ProcessFetchedContributions(ctx context.Context) error { } repositoryId = repositoryCreated.Id + } else { + repositoryId = repoFetched.Id } user, err := s.userService.GetUserByGithubId(ctx, contribution.ActorID) @@ -134,9 +89,68 @@ func (s *service) ProcessFetchedContributions(ctx context.Context) error { return err } } + return nil } +func (s *service) GetContributionType(ctx context.Context, contribution ContributionResponse) (string, error) { + var contributionPayload map[string]interface{} + err := json.Unmarshal([]byte(contribution.Payload), &contributionPayload) + if err != nil { + slog.Warn("invalid payload", "error", err) + return "", err + } + + var action string + if actionVal, ok := contributionPayload["action"]; ok { + action = actionVal.(string) + } + + var pullRequest map[string]interface{} + var isMerged bool + if pullRequestPayload, ok := contributionPayload["pull_request"]; ok { + pullRequest = pullRequestPayload.(map[string]interface{}) + isMerged = pullRequest["merged"].(bool) + } + + var issue map[string]interface{} + var stateReason string + if issuePayload, ok := contributionPayload["issue"]; ok { + issue = issuePayload.(map[string]interface{}) + stateReason = issue["state_reason"].(string) + } + + var contributionType string + switch contribution.Type { + case "PullRequestEvent": + if action == "closed" && isMerged { + contributionType = "PullRequestMerged" + } else if action == "opened" { + contributionType = "PullRequestOpened" + } + + case "IssuesEvent": + if action == "opened" { + contributionType = "IssueOpened" + } else if action == "closed" && stateReason == "not_planned" { + contributionType = "IssueClosed" + } else if action == "closed" && stateReason == "completed" { + contributionType = "IssueResolved" + } + + case "PushEvent": + contributionType = "PullRequestUpdated" + + case "IssueCommentEvent": + contributionType = "IssueComment" + + case "PullRequestComment ": + contributionType = "PullRequestComment" + } + + return contributionType, nil +} + func (s *service) CreateContribution(ctx context.Context, contributionType string, contributionDetails ContributionResponse, repositoryId int, userId int) (Contribution, error) { contribution := Contribution{ @@ -170,5 +184,6 @@ func (s *service) GetContributionScoreDetailsByContributionType(ctx context.Cont slog.Error("error occured while getting contribution score details", "error", err) return ContributionScore{}, err } + return ContributionScore(contributionScoreDetails), nil } diff --git a/internal/app/repository/service.go b/internal/app/repository/service.go index b598f1b..1643745 100644 --- a/internal/app/repository/service.go +++ b/internal/app/repository/service.go @@ -18,7 +18,7 @@ type service struct { type Service interface { GetRepoByRepoId(ctx context.Context, githubRepoId int) (Repository, error) - FetchRepositoryDetails(ctx context.Context, getUserRepoDetailsUrl string) (FetchRepositoryDetailsResponse, error) + FetchRepositoryDetails(ctx context.Context, client *http.Client, getUserRepoDetailsUrl string) (FetchRepositoryDetailsResponse, error) CreateRepository(ctx context.Context, repoGithubId int, repo FetchRepositoryDetailsResponse) (Repository, error) } @@ -39,7 +39,7 @@ func (s *service) GetRepoByRepoId(ctx context.Context, repoGithubId int) (Reposi return Repository(repoDetails), nil } -func (s *service) FetchRepositoryDetails(ctx context.Context, getUserRepoDetailsUrl string) (FetchRepositoryDetailsResponse, error) { +func (s *service) FetchRepositoryDetails(ctx context.Context, client *http.Client, getUserRepoDetailsUrl string) (FetchRepositoryDetailsResponse, error) { req, err := http.NewRequest("GET", getUserRepoDetailsUrl, nil) if err != nil { slog.Error("error fetching user repositories details", "error", err) @@ -48,7 +48,6 @@ func (s *service) FetchRepositoryDetails(ctx context.Context, getUserRepoDetails req.Header.Add("Authorization", s.appCfg.GithubPersonalAccessToken) - client := &http.Client{} resp, err := client.Do(req) if err != nil { slog.Error("error fetching user repositories details", "error", err) From de13e7a2dd998c554b3b4e5e713200b563ca4297 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Tue, 17 Jun 2025 11:40:49 +0530 Subject: [PATCH 06/27] implement users five recent contributions --- internal/app/contribution/handler.go | 19 ++++++++++++ internal/app/contribution/service.go | 16 ++++++++++ internal/app/router.go | 3 +- internal/pkg/apperrors/errors.go | 3 +- internal/repository/contribution.go | 45 +++++++++++++++++++++++++++- 5 files changed, 83 insertions(+), 3 deletions(-) diff --git a/internal/app/contribution/handler.go b/internal/app/contribution/handler.go index 195debb..62b2dbe 100644 --- a/internal/app/contribution/handler.go +++ b/internal/app/contribution/handler.go @@ -4,6 +4,7 @@ import ( "log/slog" "net/http" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/response" ) @@ -13,6 +14,7 @@ type handler struct { type Handler interface { FetchUserLatestContributions(w http.ResponseWriter, r *http.Request) + FetchUsersFiveRecentContributions(w http.ResponseWriter, r *http.Request) } func NewHandler(contributionService Service) Handler { @@ -29,8 +31,25 @@ func (h *handler) FetchUserLatestContributions(w http.ResponseWriter, r *http.Re err := h.contributionService.ProcessFetchedContributions(ctx, client) if err != nil { slog.Error("error fetching latest contributions") + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) return } response.WriteJson(w, http.StatusOK, "contribution fetched successfully", nil) } + +func (h *handler) FetchUsersFiveRecentContributions(w http.ResponseWriter, r *http.Request) { + + ctx := r.Context() + + usersFiveRecentContributions, err := h.contributionService.FetchUsersFiveRecentContributions(ctx) + if err != nil { + slog.Error("error fetching users five recent contributions") + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + response.WriteJson(w, http.StatusOK, "users five recent contributions fetched successfully", usersFiveRecentContributions) +} diff --git a/internal/app/contribution/service.go b/internal/app/contribution/service.go index b38a151..15377f3 100644 --- a/internal/app/contribution/service.go +++ b/internal/app/contribution/service.go @@ -24,6 +24,7 @@ type Service interface { ProcessFetchedContributions(ctx context.Context, client *http.Client) error CreateContribution(ctx context.Context, contributionType string, contributionDetails ContributionResponse, repositoryId int, userId int) (Contribution, error) GetContributionScoreDetailsByContributionType(ctx context.Context, contributionType string) (ContributionScore, error) + FetchUsersFiveRecentContributions(ctx context.Context) ([]Contribution, error) } func NewService(bigqueryService bigquery.Service, contributionRepository repository.ContributionRepository, repositoryService repoService.Service, userService user.Service) Service { @@ -187,3 +188,18 @@ func (s *service) GetContributionScoreDetailsByContributionType(ctx context.Cont return ContributionScore(contributionScoreDetails), nil } + +func (s *service) FetchUsersFiveRecentContributions(ctx context.Context) ([]Contribution, error) { + usersFiveRecentContributions, err := s.contributionRepository.FetchUsersFiveRecentContributions(ctx, nil) + if err != nil { + slog.Error("error occured while fetching users five recent contributions", "error", err) + return nil, err + } + + serviceContributions := make([]Contribution, len(usersFiveRecentContributions)) + for i, c := range usersFiveRecentContributions { + serviceContributions[i] = Contribution((c)) + } + + return serviceContributions, nil +} diff --git a/internal/app/router.go b/internal/app/router.go index d844c3c..3d55723 100644 --- a/internal/app/router.go +++ b/internal/app/router.go @@ -20,6 +20,7 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("PATCH /api/v1/user/email", middleware.Authentication(deps.UserHandler.UpdateUserEmail, deps.AppCfg)) - router.HandleFunc("GET /api/v1/user/contributions/recent", middleware.Authentication(deps.ContributionHandler.FetchUserLatestContributions, deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/contributions/latest", middleware.Authentication(deps.ContributionHandler.FetchUserLatestContributions, deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/contributions/recent", middleware.Authentication(deps.ContributionHandler.FetchUsersFiveRecentContributions, deps.AppCfg)) return middleware.CorsMiddleware(router, deps.AppCfg) } diff --git a/internal/pkg/apperrors/errors.go b/internal/pkg/apperrors/errors.go index beeba07..34d97ca 100644 --- a/internal/pkg/apperrors/errors.go +++ b/internal/pkg/apperrors/errors.go @@ -34,7 +34,8 @@ var ( ErrRepoNotFound = errors.New("repository not found") ErrRepoCreationFailed = errors.New("failed to create repo for user") - ErrContributionCreationFailed = errors.New("failed to create contrbitution") + ErrContributionCreationFailed = errors.New("failed to create contrbitution") + ErrFetchingRecentContributions = errors.New("failed to fetch users five recent contributions") ) func MapError(err error) (statusCode int, errMessage string) { diff --git a/internal/repository/contribution.go b/internal/repository/contribution.go index 7105069..020ccf5 100644 --- a/internal/repository/contribution.go +++ b/internal/repository/contribution.go @@ -6,6 +6,7 @@ import ( "github.com/jmoiron/sqlx" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware" ) type contributionRepository struct { @@ -16,6 +17,7 @@ type ContributionRepository interface { RepositoryTransaction CreateContribution(ctx context.Context, tx *sqlx.Tx, contributionDetails Contribution) (Contribution, error) GetContributionScoreDetailsByContributionType(ctx context.Context, tx *sqlx.Tx, contributionType string) (ContributionScore, error) + FetchUsersFiveRecentContributions(ctx context.Context, tx *sqlx.Tx) ([]Contribution, error) } func NewContributionRepository(db *sqlx.DB) ContributionRepository { @@ -38,6 +40,8 @@ const ( RETURNING *` getContributionScoreDetailsByContributionTypeQuery = `SELECT * from contribution_score where contribution_type=$1` + + fetchUsersFiveRecentContributionsQuery = `SELECT * from contributions where user_id=$1 order by contributed_at desc limit 5` ) func (cr *contributionRepository) CreateContribution(ctx context.Context, tx *sqlx.Tx, contributionInfo Contribution) (Contribution, error) { @@ -81,7 +85,7 @@ func (cr *contributionRepository) GetContributionScoreDetailsByContributionType( &contributionScoreDetails.Score, &contributionScoreDetails.CreatedAt, &contributionScoreDetails.UpdatedAt, - ) + ) if err != nil { slog.Error("error occured while getting contribution score details", "error", err) return ContributionScore{}, err @@ -89,3 +93,42 @@ func (cr *contributionRepository) GetContributionScoreDetailsByContributionType( return contributionScoreDetails, nil } + +func (cr *contributionRepository) FetchUsersFiveRecentContributions(ctx context.Context, tx *sqlx.Tx) ([]Contribution, error) { + userIdValue := ctx.Value(middleware.UserIdKey) + + userId, ok := userIdValue.(int) + if !ok { + slog.Error("error obtaining user id from context") + return nil, apperrors.ErrInternalServer + } + + executer := cr.BaseRepository.initiateQueryExecuter(tx) + + rows, err := executer.QueryContext(ctx, fetchUsersFiveRecentContributionsQuery, userId) + if err != nil { + slog.Error("error fetching users five recent contributions") + return nil, apperrors.ErrFetchingRecentContributions + } + defer rows.Close() + + var usersFiveRecentContributions []Contribution + for rows.Next() { + var recentContribution Contribution + if err = rows.Scan( + &recentContribution.Id, + &recentContribution.UserId, + &recentContribution.RepositoryId, + &recentContribution.ContributionScoreId, + &recentContribution.ContributionType, + &recentContribution.BalanceChange, + &recentContribution.ContributedAt, + &recentContribution.CreatedAt, &recentContribution.UpdatedAt); err != nil { + return nil, err + } + + usersFiveRecentContributions = append(usersFiveRecentContributions, recentContribution) + } + + return usersFiveRecentContributions, nil +} From f7e8fa9833097885ce2cbb74d360a4a3a1df9252 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Tue, 17 Jun 2025 11:59:22 +0530 Subject: [PATCH 07/27] fetch all contributions for the user --- internal/app/contribution/handler.go | 16 +++++++++++ internal/app/contribution/service.go | 18 +++++++++++- internal/app/router.go | 1 + internal/pkg/apperrors/errors.go | 1 + internal/repository/contribution.go | 42 ++++++++++++++++++++++++++++ 5 files changed, 77 insertions(+), 1 deletion(-) diff --git a/internal/app/contribution/handler.go b/internal/app/contribution/handler.go index 62b2dbe..cbcaaaa 100644 --- a/internal/app/contribution/handler.go +++ b/internal/app/contribution/handler.go @@ -15,6 +15,7 @@ type handler struct { type Handler interface { FetchUserLatestContributions(w http.ResponseWriter, r *http.Request) FetchUsersFiveRecentContributions(w http.ResponseWriter, r *http.Request) + FetchUsersAllContributions(w http.ResponseWriter, r *http.Request) } func NewHandler(contributionService Service) Handler { @@ -53,3 +54,18 @@ func (h *handler) FetchUsersFiveRecentContributions(w http.ResponseWriter, r *ht response.WriteJson(w, http.StatusOK, "users five recent contributions fetched successfully", usersFiveRecentContributions) } + +func (h *handler) FetchUsersAllContributions(w http.ResponseWriter, r *http.Request) { + + ctx := r.Context() + + usersAllContributions, err := h.contributionService.FetchUsersAllContributions(ctx) + if err != nil { + slog.Error("error fetching all contributions for user") + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + response.WriteJson(w, http.StatusOK, "all contributions for user fetched successfully", usersAllContributions) +} diff --git a/internal/app/contribution/service.go b/internal/app/contribution/service.go index 15377f3..1bb6e30 100644 --- a/internal/app/contribution/service.go +++ b/internal/app/contribution/service.go @@ -25,6 +25,7 @@ type Service interface { CreateContribution(ctx context.Context, contributionType string, contributionDetails ContributionResponse, repositoryId int, userId int) (Contribution, error) GetContributionScoreDetailsByContributionType(ctx context.Context, contributionType string) (ContributionScore, error) FetchUsersFiveRecentContributions(ctx context.Context) ([]Contribution, error) + FetchUsersAllContributions(ctx context.Context) ([]Contribution, error) } func NewService(bigqueryService bigquery.Service, contributionRepository repository.ContributionRepository, repositoryService repoService.Service, userService user.Service) Service { @@ -195,7 +196,7 @@ func (s *service) FetchUsersFiveRecentContributions(ctx context.Context) ([]Cont slog.Error("error occured while fetching users five recent contributions", "error", err) return nil, err } - + serviceContributions := make([]Contribution, len(usersFiveRecentContributions)) for i, c := range usersFiveRecentContributions { serviceContributions[i] = Contribution((c)) @@ -203,3 +204,18 @@ func (s *service) FetchUsersFiveRecentContributions(ctx context.Context) ([]Cont return serviceContributions, nil } + +func (s *service) FetchUsersAllContributions(ctx context.Context) ([]Contribution, error) { + usersAllContributions, err := s.contributionRepository.FetchUsersAllContributions(ctx, nil) + if err != nil { + slog.Error("error occured while fetching all contributions for user", "error", err) + return nil, err + } + + serviceContributions := make([]Contribution, len(usersAllContributions)) + for i, c := range usersAllContributions { + serviceContributions[i] = Contribution((c)) + } + + return serviceContributions, nil +} diff --git a/internal/app/router.go b/internal/app/router.go index 3d55723..fe2c51d 100644 --- a/internal/app/router.go +++ b/internal/app/router.go @@ -22,5 +22,6 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("GET /api/v1/user/contributions/latest", middleware.Authentication(deps.ContributionHandler.FetchUserLatestContributions, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/contributions/recent", middleware.Authentication(deps.ContributionHandler.FetchUsersFiveRecentContributions, deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/contributions/all", middleware.Authentication(deps.ContributionHandler.FetchUsersAllContributions, deps.AppCfg)) return middleware.CorsMiddleware(router, deps.AppCfg) } diff --git a/internal/pkg/apperrors/errors.go b/internal/pkg/apperrors/errors.go index 34d97ca..c9c7f2d 100644 --- a/internal/pkg/apperrors/errors.go +++ b/internal/pkg/apperrors/errors.go @@ -36,6 +36,7 @@ var ( ErrContributionCreationFailed = errors.New("failed to create contrbitution") ErrFetchingRecentContributions = errors.New("failed to fetch users five recent contributions") + ErrFetchingAllContributions = errors.New("failed to fetch all contributions for user") ) func MapError(err error) (statusCode int, errMessage string) { diff --git a/internal/repository/contribution.go b/internal/repository/contribution.go index 020ccf5..1f6f2b3 100644 --- a/internal/repository/contribution.go +++ b/internal/repository/contribution.go @@ -18,6 +18,7 @@ type ContributionRepository interface { CreateContribution(ctx context.Context, tx *sqlx.Tx, contributionDetails Contribution) (Contribution, error) GetContributionScoreDetailsByContributionType(ctx context.Context, tx *sqlx.Tx, contributionType string) (ContributionScore, error) FetchUsersFiveRecentContributions(ctx context.Context, tx *sqlx.Tx) ([]Contribution, error) + FetchUsersAllContributions(ctx context.Context, tx *sqlx.Tx) ([]Contribution, error) } func NewContributionRepository(db *sqlx.DB) ContributionRepository { @@ -42,6 +43,8 @@ const ( getContributionScoreDetailsByContributionTypeQuery = `SELECT * from contribution_score where contribution_type=$1` fetchUsersFiveRecentContributionsQuery = `SELECT * from contributions where user_id=$1 order by contributed_at desc limit 5` + + fetchUsersAllContributionsQuery = `SELECT * from contributions where user_id=$1 order by contributed_at desc` ) func (cr *contributionRepository) CreateContribution(ctx context.Context, tx *sqlx.Tx, contributionInfo Contribution) (Contribution, error) { @@ -132,3 +135,42 @@ func (cr *contributionRepository) FetchUsersFiveRecentContributions(ctx context. return usersFiveRecentContributions, nil } + +func (cr *contributionRepository) FetchUsersAllContributions(ctx context.Context, tx *sqlx.Tx) ([]Contribution, error) { + userIdValue := ctx.Value(middleware.UserIdKey) + + userId, ok := userIdValue.(int) + if !ok { + slog.Error("error obtaining user id from context") + return nil, apperrors.ErrInternalServer + } + + executer := cr.BaseRepository.initiateQueryExecuter(tx) + + rows, err := executer.QueryContext(ctx, fetchUsersAllContributionsQuery, userId) + if err != nil { + slog.Error("error fetching all contributions for user") + return nil, apperrors.ErrFetchingAllContributions + } + defer rows.Close() + + var usersAllContributions []Contribution + for rows.Next() { + var currentContribution Contribution + if err = rows.Scan( + ¤tContribution.Id, + ¤tContribution.UserId, + ¤tContribution.RepositoryId, + ¤tContribution.ContributionScoreId, + ¤tContribution.ContributionType, + ¤tContribution.BalanceChange, + ¤tContribution.ContributedAt, + ¤tContribution.CreatedAt, ¤tContribution.UpdatedAt); err != nil { + return nil, err + } + + usersAllContributions = append(usersAllContributions, currentContribution) + } + + return usersAllContributions, nil +} From 887faa4de4ed537ca3a6148a9b2a665031970183 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Tue, 17 Jun 2025 18:17:21 +0530 Subject: [PATCH 08/27] remove unnecessary lines --- internal/app/contribution/handler.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/internal/app/contribution/handler.go b/internal/app/contribution/handler.go index cbcaaaa..d8b9f92 100644 --- a/internal/app/contribution/handler.go +++ b/internal/app/contribution/handler.go @@ -25,7 +25,6 @@ func NewHandler(contributionService Service) Handler { } func (h *handler) FetchUserLatestContributions(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() client := &http.Client{} @@ -41,7 +40,6 @@ func (h *handler) FetchUserLatestContributions(w http.ResponseWriter, r *http.Re } func (h *handler) FetchUsersFiveRecentContributions(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() usersFiveRecentContributions, err := h.contributionService.FetchUsersFiveRecentContributions(ctx) @@ -56,7 +54,6 @@ func (h *handler) FetchUsersFiveRecentContributions(w http.ResponseWriter, r *ht } func (h *handler) FetchUsersAllContributions(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() usersAllContributions, err := h.contributionService.FetchUsersAllContributions(ctx) From f59627d00ebf9715c4572643e858636998c22c52 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Wed, 18 Jun 2025 12:08:10 +0530 Subject: [PATCH 09/27] implement fetch all contributed repositories of user (my contributions section) --- internal/app/dependencies.go | 28 +++-- internal/app/repository/domain.go | 41 +++++++ internal/app/repository/handler.go | 39 +++++++ internal/app/repository/service.go | 157 +++++++++++++++++++++++++ internal/app/router.go | 5 + internal/pkg/apperrors/errors.go | 15 ++- internal/repository/repository.go | 176 +++++++++++++++++++++++++++++ 7 files changed, 448 insertions(+), 13 deletions(-) create mode 100644 internal/app/repository/domain.go create mode 100644 internal/app/repository/handler.go create mode 100644 internal/app/repository/service.go create mode 100644 internal/repository/repository.go diff --git a/internal/app/dependencies.go b/internal/app/dependencies.go index fa49370..3f6448a 100644 --- a/internal/app/dependencies.go +++ b/internal/app/dependencies.go @@ -9,11 +9,14 @@ import ( ) type Dependencies struct { - AuthService auth.Service - UserService user.Service - AuthHandler auth.Handler - UserHandler user.Handler - AppCfg config.AppConfig + AuthService auth.Service + UserService user.Service + AuthHandler auth.Handler + UserHandler user.Handler + ContributionHandler contribution.Handler + RepositoryHandler repoService.Handler + AppCfg config.AppConfig + Client config.Bigquery } func InitDependencies(db *sqlx.DB, appCfg config.AppConfig) Dependencies { @@ -24,12 +27,17 @@ func InitDependencies(db *sqlx.DB, appCfg config.AppConfig) Dependencies { authHandler := auth.NewHandler(authService, appCfg) userHandler := user.NewHandler(userService) + repositoryHandler := repoService.NewHandler(repositoryService) + contributionHandler := contribution.NewHandler(contributionService) return Dependencies{ - AuthService: authService, - UserService: userService, - AuthHandler: authHandler, - UserHandler: userHandler, - AppCfg: appCfg, + AuthService: authService, + UserService: userService, + AuthHandler: authHandler, + UserHandler: userHandler, + RepositoryHandler: repositoryHandler, + ContributionHandler: contributionHandler, + AppCfg: appCfg, + Client: client, } } diff --git a/internal/app/repository/domain.go b/internal/app/repository/domain.go new file mode 100644 index 0000000..fdb1204 --- /dev/null +++ b/internal/app/repository/domain.go @@ -0,0 +1,41 @@ +package repository + +import "time" + +type RepoOWner struct { + Login string `json:"login"` +} + +type FetchRepositoryDetailsResponse struct { + Id int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + LanguagesURL string `json:"languages_url"` + UpdateDate time.Time `json:"updated_at"` + RepoOwnerName RepoOWner `json:"owner"` + RepoUrl string `json:"html_url"` +} + +type Repository struct { + Id int + GithubRepoId int + RepoName string + Description string + LanguagesUrl string + RepoUrl string + OwnerName string + UpdateDate time.Time + CreatedAt time.Time + UpdatedAt time.Time +} + + +type RepoLanguages map[string]int + +type FetchUsersContributedReposResponse struct { + RepoName string + Description string + Languages []string + UpdateDate time.Time + TotalCoinsEarned int +} diff --git a/internal/app/repository/handler.go b/internal/app/repository/handler.go new file mode 100644 index 0000000..6868bdb --- /dev/null +++ b/internal/app/repository/handler.go @@ -0,0 +1,39 @@ +package repository + +import ( + "log/slog" + "net/http" + + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/response" +) + +type handler struct { + repositoryService Service +} + +type Handler interface { + FetchUsersContributedRepos(w http.ResponseWriter, r *http.Request) +} + +func NewHandler(repositoryService Service) Handler { + return &handler{ + repositoryService: repositoryService, + } +} + +func (h *handler) FetchUsersContributedRepos(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + client := &http.Client{} + + usersContributedRepos, err := h.repositoryService.FetchUsersContributedRepos(ctx, client) + if err != nil { + slog.Error("error fetching users conributed repos") + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + response.WriteJson(w, http.StatusOK, "users contributed repositories fetched successfully", usersContributedRepos) +} diff --git a/internal/app/repository/service.go b/internal/app/repository/service.go new file mode 100644 index 0000000..988e5d7 --- /dev/null +++ b/internal/app/repository/service.go @@ -0,0 +1,157 @@ +package repository + +import ( + "context" + "encoding/json" + "io" + "log/slog" + "net/http" + + "github.com/joshsoftware/code-curiosity-2025/internal/config" + "github.com/joshsoftware/code-curiosity-2025/internal/repository" +) + +type service struct { + repositoryRepository repository.RepositoryRepository + appCfg config.AppConfig +} + +type Service interface { + GetRepoByRepoId(ctx context.Context, githubRepoId int) (Repository, error) + FetchRepositoryDetails(ctx context.Context, client *http.Client, getUserRepoDetailsUrl string) (FetchRepositoryDetailsResponse, error) + CreateRepository(ctx context.Context, repoGithubId int, repo FetchRepositoryDetailsResponse) (Repository, error) + FetchRepositoryLanguages(ctx context.Context, client *http.Client, getRepoLanguagesURL string) (RepoLanguages, error) + FetchUsersContributedRepos(ctx context.Context, client *http.Client) ([]FetchUsersContributedReposResponse, error) +} + +func NewService(repositoryRepository repository.RepositoryRepository, appCfg config.AppConfig) Service { + return &service{ + repositoryRepository: repositoryRepository, + appCfg: appCfg, + } +} + +func (s *service) GetRepoByRepoId(ctx context.Context, repoGithubId int) (Repository, error) { + repoDetails, err := s.repositoryRepository.GetRepoByGithubId(ctx, nil, repoGithubId) + if err != nil { + slog.Error("failed to get repository by github id") + return Repository{}, err + } + + return Repository(repoDetails), nil +} + +func (s *service) FetchRepositoryDetails(ctx context.Context, client *http.Client, getUserRepoDetailsUrl string) (FetchRepositoryDetailsResponse, error) { + req, err := http.NewRequest("GET", getUserRepoDetailsUrl, nil) + if err != nil { + slog.Error("error fetching user repositories details", "error", err) + return FetchRepositoryDetailsResponse{}, err + } + + req.Header.Add("Authorization", s.appCfg.GithubPersonalAccessToken) + + resp, err := client.Do(req) + if err != nil { + slog.Error("error fetching user repositories details", "error", err) + return FetchRepositoryDetailsResponse{}, err + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + slog.Error("error reading body", "error", err) + return FetchRepositoryDetailsResponse{}, err + } + + var repoDetails FetchRepositoryDetailsResponse + err = json.Unmarshal(body, &repoDetails) + if err != nil { + slog.Error("error unmarshalling fetch repository details body", "error", err) + return FetchRepositoryDetailsResponse{}, err + } + + return repoDetails, nil +} + +func (s *service) CreateRepository(ctx context.Context, repoGithubId int, repo FetchRepositoryDetailsResponse) (Repository, error) { + createRepo := Repository{ + GithubRepoId: repoGithubId, + RepoName: repo.Name, + RepoUrl: repo.RepoUrl, + Description: repo.Description, + LanguagesUrl: repo.LanguagesURL, + OwnerName: repo.RepoOwnerName.Login, + UpdateDate: repo.UpdateDate, + } + repositoryCreated, err := s.repositoryRepository.CreateRepository(ctx, nil, repository.Repository(createRepo)) + if err != nil { + slog.Error("failed to create repository", "error", err) + return Repository{}, err + } + + return Repository(repositoryCreated), nil +} + +func (s *service) FetchRepositoryLanguages(ctx context.Context, client *http.Client, getRepoLanguagesURL string) (RepoLanguages, error) { + req, err := http.NewRequest("GET", getRepoLanguagesURL, nil) + if err != nil { + slog.Error("error fetching languages for repository", "error", err) + return RepoLanguages{}, err + } + + resp, err := client.Do(req) + if err != nil { + slog.Error("error fetching languages for repository", "error", err) + return RepoLanguages{}, err + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + slog.Error("error reading body", "error", err) + return RepoLanguages{}, err + } + + var repoLanguages RepoLanguages + err = json.Unmarshal(body, &repoLanguages) + if err != nil { + slog.Error("error unmarshalling fetch repository languages body", "error", err) + return RepoLanguages{}, err + } + + return repoLanguages, nil +} + +func (s *service) FetchUsersContributedRepos(ctx context.Context, client *http.Client) ([]FetchUsersContributedReposResponse, error) { + usersContributedRepos, err := s.repositoryRepository.FetchUsersContributedRepos(ctx, nil) + if err != nil { + slog.Error("error fetching users conributed repos") + return nil, err + } + + fetchUsersContributedReposResponse := make([]FetchUsersContributedReposResponse, len(usersContributedRepos)) + + for i, usersContributedRepo := range usersContributedRepos { + fetchUsersContributedReposResponse[i].RepoName = usersContributedRepo.RepoName + fetchUsersContributedReposResponse[i].Description = usersContributedRepo.Description + fetchUsersContributedReposResponse[i].UpdateDate = usersContributedRepo.UpdateDate + + contributedRepoLanguages, err := s.FetchRepositoryLanguages(ctx, client, usersContributedRepo.LanguagesUrl) + if err != nil { + slog.Error("error fetching languages for repository", "error", err) + return nil, err + } + + for language := range contributedRepoLanguages { + fetchUsersContributedReposResponse[i].Languages = append(fetchUsersContributedReposResponse[i].Languages, language) + } + + userRepoTotalCoins, err := s.repositoryRepository.GetUserRepoTotalCoins(ctx, nil, usersContributedRepo.Id) + if err != nil { + slog.Error("error calculating total coins earned by user for the repository") + return nil, err + } + + fetchUsersContributedReposResponse[i].TotalCoinsEarned = userRepoTotalCoins + } + + return fetchUsersContributedReposResponse, nil +} diff --git a/internal/app/router.go b/internal/app/router.go index 072a53a..ad5d19b 100644 --- a/internal/app/router.go +++ b/internal/app/router.go @@ -20,5 +20,10 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("PATCH /api/v1/user/email", middleware.Authentication(deps.UserHandler.UpdateUserEmail, deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/contributions/latest", middleware.Authentication(deps.ContributionHandler.FetchUserLatestContributions, deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/contributions/recent", middleware.Authentication(deps.ContributionHandler.FetchUsersFiveRecentContributions, deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/contributions/all", middleware.Authentication(deps.ContributionHandler.FetchUsersAllContributions, deps.AppCfg)) + + router.HandleFunc("GET /api/v1/user/repositories", middleware.Authentication(deps.RepositoryHandler.FetchUsersContributedRepos, deps.AppCfg)) return middleware.CorsMiddleware(router, deps.AppCfg) } diff --git a/internal/pkg/apperrors/errors.go b/internal/pkg/apperrors/errors.go index 5c7244d..878bbaf 100644 --- a/internal/pkg/apperrors/errors.go +++ b/internal/pkg/apperrors/errors.go @@ -28,8 +28,17 @@ var ( ErrUserNotFound = errors.New("user not found") ErrUserCreationFailed = errors.New("failed to create user") - ErrJWTCreationFailed = errors.New("failed to create jwt token") - ErrAuthorizationFailed=errors.New("failed to authorize user") + ErrJWTCreationFailed = errors.New("failed to create jwt token") + ErrAuthorizationFailed = errors.New("failed to authorize user") + + ErrRepoNotFound = errors.New("repository not found") + ErrRepoCreationFailed = errors.New("failed to create repo for user") + ErrCalculatingUserRepoTotalCoins = errors.New("error calculating total coins earned by user for the repository") + ErrFetchingUsersContributedRepos = errors.New("error fetching users contributed repositories") + + ErrContributionCreationFailed = errors.New("failed to create contrbitution") + ErrFetchingRecentContributions = errors.New("failed to fetch users five recent contributions") + ErrFetchingAllContributions = errors.New("failed to fetch all contributions for user") ) func MapError(err error) (statusCode int, errMessage string) { @@ -40,7 +49,7 @@ func MapError(err error) (statusCode int, errMessage string) { return http.StatusUnauthorized, err.Error() case ErrAccessForbidden: return http.StatusForbidden, err.Error() - case ErrUserNotFound: + case ErrUserNotFound, ErrRepoNotFound: return http.StatusNotFound, err.Error() case ErrInvalidToken: return http.StatusUnprocessableEntity, err.Error() diff --git a/internal/repository/repository.go b/internal/repository/repository.go new file mode 100644 index 0000000..21b5b10 --- /dev/null +++ b/internal/repository/repository.go @@ -0,0 +1,176 @@ +package repository + +import ( + "context" + "database/sql" + "errors" + "log/slog" + + "github.com/jmoiron/sqlx" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" + "github.com/joshsoftware/code-curiosity-2025/internal/pkg/middleware" +) + +type repositoryRepository struct { + BaseRepository +} + +type RepositoryRepository interface { + RepositoryTransaction + GetRepoByGithubId(ctx context.Context, tx *sqlx.Tx, repoGithubId int) (Repository, error) + CreateRepository(ctx context.Context, tx *sqlx.Tx, repository Repository) (Repository, error) + GetUserRepoTotalCoins(ctx context.Context, tx *sqlx.Tx, repoId int) (int, error) + FetchUsersContributedRepos(ctx context.Context, tx *sqlx.Tx) ([]Repository, error) +} + +func NewRepositoryRepository(db *sqlx.DB) RepositoryRepository { + return &repositoryRepository{ + BaseRepository: BaseRepository{db}, + } +} + +const ( + getRepoByGithubIdQuery = `SELECT * from repositories where github_repo_id=$1` + + createRepositoryQuery = ` + INSERT INTO repositories ( + github_repo_id, + repo_name, + description, + languages_url, + repo_url, + owner_name, + update_date + ) + VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING *` + + getUserRepoTotalCoinsQuery = `SELECT sum(balance_change) from contributions where user_id = $1 and repository_id = $2;` + + fetchUsersContributedReposQuery = `SELECT * from repositories where id in (SELECT repository_id from contributions where user_id=$1);` +) + +func (rr *repositoryRepository) GetRepoByGithubId(ctx context.Context, tx *sqlx.Tx, repoGithubId int) (Repository, error) { + executer := rr.BaseRepository.initiateQueryExecuter(tx) + + var repository Repository + err := executer.QueryRowContext(ctx, getRepoByGithubIdQuery, repoGithubId).Scan( + &repository.Id, + &repository.GithubRepoId, + &repository.RepoName, + &repository.Description, + &repository.LanguagesUrl, + &repository.RepoUrl, + &repository.OwnerName, + &repository.UpdateDate, + &repository.CreatedAt, + &repository.UpdatedAt, + ) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + slog.Error("repository not found", "error", err) + return Repository{}, apperrors.ErrRepoNotFound + } + slog.Error("error occurred while getting repository by id", "error", err) + return Repository{}, apperrors.ErrInternalServer + } + + return repository, nil + +} + +func (rr *repositoryRepository) CreateRepository(ctx context.Context, tx *sqlx.Tx, repositoryInfo Repository) (Repository, error) { + executer := rr.BaseRepository.initiateQueryExecuter(tx) + + var repository Repository + err := executer.QueryRowContext(ctx, createRepositoryQuery, + repositoryInfo.GithubRepoId, + repositoryInfo.RepoName, + repositoryInfo.Description, + repositoryInfo.LanguagesUrl, + repositoryInfo.RepoUrl, + repositoryInfo.OwnerName, + repositoryInfo.UpdateDate, + ).Scan( + &repository.Id, + &repository.GithubRepoId, + &repository.RepoName, + &repository.Description, + &repository.LanguagesUrl, + &repository.RepoUrl, + &repository.OwnerName, + &repository.UpdateDate, + &repository.CreatedAt, + &repository.UpdatedAt, + ) + if err != nil { + slog.Error("error occured while creating repository", "error", err) + return Repository{}, apperrors.ErrInternalServer + } + + return repository, nil + +} + +func (r *repositoryRepository) GetUserRepoTotalCoins(ctx context.Context, tx *sqlx.Tx, repoId int) (int, error) { + userIdValue := ctx.Value(middleware.UserIdKey) + + userId, ok := userIdValue.(int) + if !ok { + slog.Error("error obtaining user id from context") + return 0, apperrors.ErrInternalServer + } + + executer := r.BaseRepository.initiateQueryExecuter(tx) + + var totalCoins int + + err := executer.QueryRowContext(ctx, getUserRepoTotalCoinsQuery, userId, repoId).Scan(&totalCoins) + if err != nil { + slog.Error("error calculating total coins earned by user for the repository") + return 0, apperrors.ErrCalculatingUserRepoTotalCoins + } + + return totalCoins, nil +} + +func (r *repositoryRepository) FetchUsersContributedRepos(ctx context.Context, tx *sqlx.Tx) ([]Repository, error) { + userIdValue := ctx.Value(middleware.UserIdKey) + + userId, ok := userIdValue.(int) + if !ok { + slog.Error("error obtaining user id from context") + return nil, apperrors.ErrInternalServer + } + + executer := r.BaseRepository.initiateQueryExecuter(tx) + + rows, err := executer.QueryContext(ctx, fetchUsersContributedReposQuery, userId) + if err != nil { + slog.Error("error fetching users contributed repositories") + return nil, apperrors.ErrFetchingUsersContributedRepos + } + defer rows.Close() + + var usersContributedRepos []Repository + for rows.Next() { + var usersContributedRepo Repository + if err = rows.Scan( + &usersContributedRepo.Id, + &usersContributedRepo.GithubRepoId, + &usersContributedRepo.RepoName, + &usersContributedRepo.Description, + &usersContributedRepo.LanguagesUrl, + &usersContributedRepo.RepoUrl, + &usersContributedRepo.OwnerName, + &usersContributedRepo.UpdateDate, + &usersContributedRepo.CreatedAt, + &usersContributedRepo.UpdatedAt); err != nil { + return nil, err + } + + usersContributedRepos = append(usersContributedRepos, usersContributedRepo) + } + + return usersContributedRepos, nil +} From 82afbddd58fb8db5f81ed543719e442aa0b571a3 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Wed, 18 Jun 2025 15:31:07 +0530 Subject: [PATCH 10/27] send all details for the repository --- internal/app/repository/domain.go | 5 +---- internal/app/repository/service.go | 4 +--- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/internal/app/repository/domain.go b/internal/app/repository/domain.go index fdb1204..11dc558 100644 --- a/internal/app/repository/domain.go +++ b/internal/app/repository/domain.go @@ -29,13 +29,10 @@ type Repository struct { UpdatedAt time.Time } - type RepoLanguages map[string]int type FetchUsersContributedReposResponse struct { - RepoName string - Description string + Repository Languages []string - UpdateDate time.Time TotalCoinsEarned int } diff --git a/internal/app/repository/service.go b/internal/app/repository/service.go index 988e5d7..53ef3b8 100644 --- a/internal/app/repository/service.go +++ b/internal/app/repository/service.go @@ -130,9 +130,7 @@ func (s *service) FetchUsersContributedRepos(ctx context.Context, client *http.C fetchUsersContributedReposResponse := make([]FetchUsersContributedReposResponse, len(usersContributedRepos)) for i, usersContributedRepo := range usersContributedRepos { - fetchUsersContributedReposResponse[i].RepoName = usersContributedRepo.RepoName - fetchUsersContributedReposResponse[i].Description = usersContributedRepo.Description - fetchUsersContributedReposResponse[i].UpdateDate = usersContributedRepo.UpdateDate + fetchUsersContributedReposResponse[i].Repository = Repository(usersContributedRepo) contributedRepoLanguages, err := s.FetchRepositoryLanguages(ctx, client, usersContributedRepo.LanguagesUrl) if err != nil { From c8da5f1295b12a9bde34970b1923c52cbd6861fb Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Wed, 18 Jun 2025 15:50:12 +0530 Subject: [PATCH 11/27] fetch particular repository details --- internal/app/repository/handler.go | 24 ++++++++++++++++++++++++ internal/app/router.go | 2 ++ 2 files changed, 26 insertions(+) diff --git a/internal/app/repository/handler.go b/internal/app/repository/handler.go index 6868bdb..dbfd979 100644 --- a/internal/app/repository/handler.go +++ b/internal/app/repository/handler.go @@ -3,6 +3,7 @@ package repository import ( "log/slog" "net/http" + "strconv" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/apperrors" "github.com/joshsoftware/code-curiosity-2025/internal/pkg/response" @@ -14,6 +15,7 @@ type handler struct { type Handler interface { FetchUsersContributedRepos(w http.ResponseWriter, r *http.Request) + FetchParticularRepoDetails(w http.ResponseWriter, r *http.Request) } func NewHandler(repositoryService Service) Handler { @@ -37,3 +39,25 @@ func (h *handler) FetchUsersContributedRepos(w http.ResponseWriter, r *http.Requ response.WriteJson(w, http.StatusOK, "users contributed repositories fetched successfully", usersContributedRepos) } + +func (h *handler) FetchParticularRepoDetails(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + repoIdPath := r.PathValue("repo_id") + repoId, err := strconv.Atoi(repoIdPath) + if err != nil { + slog.Error("error getting repo id from request url") + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + repoDetails, err := h.repositoryService.GetRepoByRepoId(ctx, repoId) + if err != nil { + slog.Error("error fetching particular repo details") + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + response.WriteJson(w, http.StatusOK, "repository details fetched successfully", repoDetails) +} diff --git a/internal/app/router.go b/internal/app/router.go index ad5d19b..1117988 100644 --- a/internal/app/router.go +++ b/internal/app/router.go @@ -25,5 +25,7 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("GET /api/v1/user/contributions/all", middleware.Authentication(deps.ContributionHandler.FetchUsersAllContributions, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/repositories", middleware.Authentication(deps.RepositoryHandler.FetchUsersContributedRepos, deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/repositories/{repo_id}", middleware.Authentication(deps.RepositoryHandler.FetchParticularRepoDetails, deps.AppCfg)) + return middleware.CorsMiddleware(router, deps.AppCfg) } From 121d59c07a50fec4a7b7a9d1eecea9285461c1f1 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Wed, 18 Jun 2025 17:35:53 +0530 Subject: [PATCH 12/27] implement fetch particular repo contributors --- internal/app/repository/domain.go | 49 +++++++++++++++++++----------- internal/app/repository/handler.go | 33 ++++++++++++++++++++ internal/app/repository/service.go | 30 ++++++++++++++++++ internal/repository/domain.go | 23 +++++++------- 4 files changed, 107 insertions(+), 28 deletions(-) diff --git a/internal/app/repository/domain.go b/internal/app/repository/domain.go index 11dc558..1b6f91e 100644 --- a/internal/app/repository/domain.go +++ b/internal/app/repository/domain.go @@ -7,26 +7,28 @@ type RepoOWner struct { } type FetchRepositoryDetailsResponse struct { - Id int `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - LanguagesURL string `json:"languages_url"` - UpdateDate time.Time `json:"updated_at"` - RepoOwnerName RepoOWner `json:"owner"` - RepoUrl string `json:"html_url"` + Id int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + LanguagesURL string `json:"languages_url"` + UpdateDate time.Time `json:"updated_at"` + RepoOwnerName RepoOWner `json:"owner"` + RepoUrl string `json:"html_url"` + ContributorsUrl string `json:"contributors_url"` } type Repository struct { - Id int - GithubRepoId int - RepoName string - Description string - LanguagesUrl string - RepoUrl string - OwnerName string - UpdateDate time.Time - CreatedAt time.Time - UpdatedAt time.Time + Id int + GithubRepoId int + RepoName string + Description string + LanguagesUrl string + RepoUrl string + ContributorsUrl string + OwnerName string + UpdateDate time.Time + CreatedAt time.Time + UpdatedAt time.Time } type RepoLanguages map[string]int @@ -36,3 +38,16 @@ type FetchUsersContributedReposResponse struct { Languages []string TotalCoinsEarned int } + +type FetchRepoContributorsResponse struct { + Id int `json:"id"` + Name string `json:"login"` + AvatarUrl string `json:"avatar_url"` + GithubUrl string `json:"html_url"` + Contributions int `json:"contributions"` +} + +type FetchParticularRepoDetails struct { + Repository + Contributors []FetchRepoContributorsResponse +} diff --git a/internal/app/repository/handler.go b/internal/app/repository/handler.go index dbfd979..13bceb2 100644 --- a/internal/app/repository/handler.go +++ b/internal/app/repository/handler.go @@ -61,3 +61,36 @@ func (h *handler) FetchParticularRepoDetails(w http.ResponseWriter, r *http.Requ response.WriteJson(w, http.StatusOK, "repository details fetched successfully", repoDetails) } + +func (h *handler) FetchParticularRepoContributors(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + client := &http.Client{} + + repoIdPath := r.PathValue("repo_id") + repoId, err := strconv.Atoi(repoIdPath) + if err != nil { + slog.Error("error getting repo id from request url") + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + repoDetails, err := h.repositoryService.GetRepoByRepoId(ctx, repoId) + if err != nil { + slog.Error("error fetching particular repo details") + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + repoContributors, err := h.repositoryService.FetchRepositoryContributors(ctx, client, repoDetails.ContributorsUrl) + if err != nil { + slog.Error("error fetching repo contributors") + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + response.WriteJson(w, http.StatusOK, "contributors for repo fetched successfully", repoContributors) +} diff --git a/internal/app/repository/service.go b/internal/app/repository/service.go index 53ef3b8..b4c5d8c 100644 --- a/internal/app/repository/service.go +++ b/internal/app/repository/service.go @@ -22,6 +22,7 @@ type Service interface { CreateRepository(ctx context.Context, repoGithubId int, repo FetchRepositoryDetailsResponse) (Repository, error) FetchRepositoryLanguages(ctx context.Context, client *http.Client, getRepoLanguagesURL string) (RepoLanguages, error) FetchUsersContributedRepos(ctx context.Context, client *http.Client) ([]FetchUsersContributedReposResponse, error) + FetchRepositoryContributors(ctx context.Context, client *http.Client, getRepoContributorsURl string) ([]FetchRepoContributorsResponse, error) } func NewService(repositoryRepository repository.RepositoryRepository, appCfg config.AppConfig) Service { @@ -153,3 +154,32 @@ func (s *service) FetchUsersContributedRepos(ctx context.Context, client *http.C return fetchUsersContributedReposResponse, nil } + +func (s *service) FetchRepositoryContributors(ctx context.Context, client *http.Client, getRepoContributorsURl string) ([]FetchRepoContributorsResponse, error) { + req, err := http.NewRequest("GET", getRepoContributorsURl, nil) + if err != nil { + slog.Error("error fetching contributors for repository", "error", err) + return nil, err + } + + resp, err := client.Do(req) + if err != nil { + slog.Error("error fetching contributors for repository", "error", err) + return nil, err + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + slog.Error("error reading body", "error", err) + return nil, err + } + + var repoContributors []FetchRepoContributorsResponse + err = json.Unmarshal(body, &repoContributors) + if err != nil { + slog.Error("error unmarshalling fetch contributors body", "error", err) + return nil, err + } + + return repoContributors, nil +} diff --git a/internal/repository/domain.go b/internal/repository/domain.go index 13cedc5..4a6966b 100644 --- a/internal/repository/domain.go +++ b/internal/repository/domain.go @@ -43,16 +43,17 @@ type Contribution struct { } type Repository struct { - Id int - GithubRepoId int - RepoName string - Description string - LanguagesUrl string - RepoUrl string - OwnerName string - UpdateDate time.Time - CreatedAt time.Time - UpdatedAt time.Time + Id int + GithubRepoId int + RepoName string + Description string + LanguagesUrl string + RepoUrl string + ContributorsUrl string + OwnerName string + UpdateDate time.Time + CreatedAt time.Time + UpdatedAt time.Time } type ContributionScore struct { @@ -62,4 +63,4 @@ type ContributionScore struct { Score int CreatedAt time.Time UpdatedAt time.Time -} \ No newline at end of file +} From 465fcd4666e1aff463c800678668b74f7b30333b Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Wed, 18 Jun 2025 18:27:14 +0530 Subject: [PATCH 13/27] implement fetch contributions of user in a particular repository --- internal/app/repository/domain.go | 12 +++++++++ internal/app/repository/handler.go | 24 +++++++++++++++++ internal/app/repository/service.go | 16 +++++++++++ internal/app/router.go | 2 +- internal/pkg/apperrors/errors.go | 9 ++++--- internal/repository/repository.go | 43 ++++++++++++++++++++++++++++++ 6 files changed, 101 insertions(+), 5 deletions(-) diff --git a/internal/app/repository/domain.go b/internal/app/repository/domain.go index 1b6f91e..0dcfa99 100644 --- a/internal/app/repository/domain.go +++ b/internal/app/repository/domain.go @@ -51,3 +51,15 @@ type FetchParticularRepoDetails struct { Repository Contributors []FetchRepoContributorsResponse } + +type Contribution struct { + Id int + UserId int + RepositoryId int + ContributionScoreId int + ContributionType string + BalanceChange int + ContributedAt time.Time + CreatedAt time.Time + UpdatedAt time.Time +} diff --git a/internal/app/repository/handler.go b/internal/app/repository/handler.go index 13bceb2..2567e8c 100644 --- a/internal/app/repository/handler.go +++ b/internal/app/repository/handler.go @@ -16,6 +16,7 @@ type handler struct { type Handler interface { FetchUsersContributedRepos(w http.ResponseWriter, r *http.Request) FetchParticularRepoDetails(w http.ResponseWriter, r *http.Request) + FetchUserContributionsInRepo(w http.ResponseWriter, r *http.Request) } func NewHandler(repositoryService Service) Handler { @@ -94,3 +95,26 @@ func (h *handler) FetchParticularRepoContributors(w http.ResponseWriter, r *http response.WriteJson(w, http.StatusOK, "contributors for repo fetched successfully", repoContributors) } + +func (h *handler) FetchUserContributionsInRepo(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + repoIdPath := r.PathValue("repo_id") + repoId, err := strconv.Atoi(repoIdPath) + if err != nil { + slog.Error("error getting repo id from request url") + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + usersContributionsInRepo, err := h.repositoryService.FetchUserContributionsInRepo(ctx, repoId) + if err != nil { + slog.Error("error fetching users contribution in repository") + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + response.WriteJson(w, http.StatusOK, "users contribution for repository fetched successfully", usersContributionsInRepo) +} diff --git a/internal/app/repository/service.go b/internal/app/repository/service.go index b4c5d8c..bb2bf17 100644 --- a/internal/app/repository/service.go +++ b/internal/app/repository/service.go @@ -23,6 +23,7 @@ type Service interface { FetchRepositoryLanguages(ctx context.Context, client *http.Client, getRepoLanguagesURL string) (RepoLanguages, error) FetchUsersContributedRepos(ctx context.Context, client *http.Client) ([]FetchUsersContributedReposResponse, error) FetchRepositoryContributors(ctx context.Context, client *http.Client, getRepoContributorsURl string) ([]FetchRepoContributorsResponse, error) + FetchUserContributionsInRepo(ctx context.Context, githubRepoId int) ([]Contribution, error) } func NewService(repositoryRepository repository.RepositoryRepository, appCfg config.AppConfig) Service { @@ -183,3 +184,18 @@ func (s *service) FetchRepositoryContributors(ctx context.Context, client *http. return repoContributors, nil } + +func (s *service) FetchUserContributionsInRepo(ctx context.Context, githubRepoId int) ([]Contribution, error) { + userContributionsInRepo, err := s.repositoryRepository.FetchUserContributionsInRepo(ctx, nil, githubRepoId) + if err != nil { + slog.Error("error fetching users contribution in repository") + return nil, err + } + + serviceUserContributionsInRepo := make([]Contribution, len(userContributionsInRepo)) + for i, c := range userContributionsInRepo { + serviceUserContributionsInRepo[i] = Contribution(c) + } + + return serviceUserContributionsInRepo, nil +} diff --git a/internal/app/router.go b/internal/app/router.go index 1117988..6e77e6c 100644 --- a/internal/app/router.go +++ b/internal/app/router.go @@ -26,6 +26,6 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("GET /api/v1/user/repositories", middleware.Authentication(deps.RepositoryHandler.FetchUsersContributedRepos, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/repositories/{repo_id}", middleware.Authentication(deps.RepositoryHandler.FetchParticularRepoDetails, deps.AppCfg)) - + router.HandleFunc("GET /api/v1/user/repositories/contributions/recent/{repo_id}", middleware.Authentication(deps.RepositoryHandler.FetchUserContributionsInRepo, deps.AppCfg)) return middleware.CorsMiddleware(router, deps.AppCfg) } diff --git a/internal/pkg/apperrors/errors.go b/internal/pkg/apperrors/errors.go index 8703f38..743e37a 100644 --- a/internal/pkg/apperrors/errors.go +++ b/internal/pkg/apperrors/errors.go @@ -31,10 +31,11 @@ var ( ErrJWTCreationFailed = errors.New("failed to create jwt token") ErrAuthorizationFailed = errors.New("failed to authorize user") - ErrRepoNotFound = errors.New("repository not found") - ErrRepoCreationFailed = errors.New("failed to create repo for user") - ErrCalculatingUserRepoTotalCoins = errors.New("error calculating total coins earned by user for the repository") - ErrFetchingUsersContributedRepos = errors.New("error fetching users contributed repositories") + ErrRepoNotFound = errors.New("repository not found") + ErrRepoCreationFailed = errors.New("failed to create repo for user") + ErrCalculatingUserRepoTotalCoins = errors.New("error calculating total coins earned by user for the repository") + ErrFetchingUsersContributedRepos = errors.New("error fetching users contributed repositories") + ErrFetchingUserContributionsInRepo = errors.New("error fetching users contribution in repository") ErrContributionCreationFailed = errors.New("failed to create contrbitution") ErrFetchingRecentContributions = errors.New("failed to fetch users five recent contributions") diff --git a/internal/repository/repository.go b/internal/repository/repository.go index 21b5b10..3bf0d81 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -21,6 +21,7 @@ type RepositoryRepository interface { CreateRepository(ctx context.Context, tx *sqlx.Tx, repository Repository) (Repository, error) GetUserRepoTotalCoins(ctx context.Context, tx *sqlx.Tx, repoId int) (int, error) FetchUsersContributedRepos(ctx context.Context, tx *sqlx.Tx) ([]Repository, error) + FetchUserContributionsInRepo(ctx context.Context, tx *sqlx.Tx, repoGithubId int) ([]Contribution, error) } func NewRepositoryRepository(db *sqlx.DB) RepositoryRepository { @@ -48,6 +49,8 @@ const ( getUserRepoTotalCoinsQuery = `SELECT sum(balance_change) from contributions where user_id = $1 and repository_id = $2;` fetchUsersContributedReposQuery = `SELECT * from repositories where id in (SELECT repository_id from contributions where user_id=$1);` + + fetchUserContributionsInRepoQuery = `SELECT * from contributions where repository_id in (SELECT id from repositories where github_repo_id=$1) and user_id=$2;` ) func (rr *repositoryRepository) GetRepoByGithubId(ctx context.Context, tx *sqlx.Tx, repoGithubId int) (Repository, error) { @@ -174,3 +177,43 @@ func (r *repositoryRepository) FetchUsersContributedRepos(ctx context.Context, t return usersContributedRepos, nil } + +func (r *repositoryRepository) FetchUserContributionsInRepo(ctx context.Context, tx *sqlx.Tx, repoGithubId int) ([]Contribution, error) { + userIdValue := ctx.Value(middleware.UserIdKey) + + userId, ok := userIdValue.(int) + if !ok { + slog.Error("error obtaining user id from context") + return nil, apperrors.ErrInternalServer + } + + executer := r.BaseRepository.initiateQueryExecuter(tx) + + rows, err := executer.QueryContext(ctx, fetchUserContributionsInRepoQuery, repoGithubId, userId) + if err != nil { + slog.Error("error fetching users contribution in repository") + return nil, apperrors.ErrFetchingUserContributionsInRepo + } + defer rows.Close() + + var userContributionsInRepo []Contribution + for rows.Next() { + var userContributionInRepo Contribution + if err = rows.Scan( + &userContributionInRepo.Id, + &userContributionInRepo.UserId, + &userContributionInRepo.RepositoryId, + &userContributionInRepo.ContributionScoreId, + &userContributionInRepo.ContributionType, + &userContributionInRepo.BalanceChange, + &userContributionInRepo.ContributedAt, + &userContributionInRepo.CreatedAt, + &userContributionInRepo.UpdatedAt); err != nil { + return nil, err + } + + userContributionsInRepo = append(userContributionsInRepo, userContributionInRepo) + } + + return userContributionsInRepo, nil +} From f85a010a4f7ab3cfae652f7a9478c7384c56bcb8 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Thu, 19 Jun 2025 13:39:23 +0530 Subject: [PATCH 14/27] fetch particular repository language percentage --- internal/app/repository/domain.go | 6 +++++ internal/app/repository/handler.go | 42 ++++++++++++++++++++++++++++++ internal/app/repository/service.go | 22 ++++++++++++++++ internal/app/router.go | 1 + 4 files changed, 71 insertions(+) diff --git a/internal/app/repository/domain.go b/internal/app/repository/domain.go index 0dcfa99..45305de 100644 --- a/internal/app/repository/domain.go +++ b/internal/app/repository/domain.go @@ -63,3 +63,9 @@ type Contribution struct { CreatedAt time.Time UpdatedAt time.Time } + +type LanguagePercent struct { + Name string + Bytes int + Percentage float64 +} diff --git a/internal/app/repository/handler.go b/internal/app/repository/handler.go index 2567e8c..e8f65f8 100644 --- a/internal/app/repository/handler.go +++ b/internal/app/repository/handler.go @@ -17,6 +17,7 @@ type Handler interface { FetchUsersContributedRepos(w http.ResponseWriter, r *http.Request) FetchParticularRepoDetails(w http.ResponseWriter, r *http.Request) FetchUserContributionsInRepo(w http.ResponseWriter, r *http.Request) + FetchLanguagePercentInRepo(w http.ResponseWriter, r *http.Request) } func NewHandler(repositoryService Service) Handler { @@ -118,3 +119,44 @@ func (h *handler) FetchUserContributionsInRepo(w http.ResponseWriter, r *http.Re response.WriteJson(w, http.StatusOK, "users contribution for repository fetched successfully", usersContributionsInRepo) } + +func (h *handler) FetchLanguagePercentInRepo(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + client := &http.Client{} + + repoIdPath := r.PathValue("repo_id") + repoId, err := strconv.Atoi(repoIdPath) + if err != nil { + slog.Error("error getting repo id from request url") + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + repoDetails, err := h.repositoryService.GetRepoByRepoId(ctx, repoId) + if err != nil { + slog.Error("error fetching particular repo details") + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + repoLanguages, err := h.repositoryService.FetchRepositoryLanguages(ctx, client, repoDetails.LanguagesUrl) + if err != nil { + slog.Error("error fetching particular repo languages") + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + langPercent, err := h.repositoryService.CalculateLanguagePercentInRepo(ctx, repoLanguages) + if err != nil { + slog.Error("error fetching particular repo languages") + status, errorMessage := apperrors.MapError(err) + response.WriteJson(w, status, errorMessage, nil) + return + } + + response.WriteJson(w, http.StatusOK, "language percentages for repo fetched successfully", langPercent) +} diff --git a/internal/app/repository/service.go b/internal/app/repository/service.go index bb2bf17..0eddc1e 100644 --- a/internal/app/repository/service.go +++ b/internal/app/repository/service.go @@ -5,6 +5,7 @@ import ( "encoding/json" "io" "log/slog" + "math" "net/http" "github.com/joshsoftware/code-curiosity-2025/internal/config" @@ -24,6 +25,7 @@ type Service interface { FetchUsersContributedRepos(ctx context.Context, client *http.Client) ([]FetchUsersContributedReposResponse, error) FetchRepositoryContributors(ctx context.Context, client *http.Client, getRepoContributorsURl string) ([]FetchRepoContributorsResponse, error) FetchUserContributionsInRepo(ctx context.Context, githubRepoId int) ([]Contribution, error) + CalculateLanguagePercentInRepo(ctx context.Context, repoLanguages RepoLanguages) ([]LanguagePercent, error) } func NewService(repositoryRepository repository.RepositoryRepository, appCfg config.AppConfig) Service { @@ -199,3 +201,23 @@ func (s *service) FetchUserContributionsInRepo(ctx context.Context, githubRepoId return serviceUserContributionsInRepo, nil } + +func (s *service) CalculateLanguagePercentInRepo(ctx context.Context, repoLanguages RepoLanguages) ([]LanguagePercent, error) { + var total int + for _, bytes := range repoLanguages { + total += bytes + } + + var langPercent []LanguagePercent + + for lang, bytes := range repoLanguages { + percentage := (float64(bytes) / float64(total)) * 100 + langPercent = append(langPercent, LanguagePercent{ + Name: lang, + Bytes: bytes, + Percentage: math.Round(percentage*10) / 10, + }) + } + + return langPercent, nil +} diff --git a/internal/app/router.go b/internal/app/router.go index 6e77e6c..3e3a1b5 100644 --- a/internal/app/router.go +++ b/internal/app/router.go @@ -27,5 +27,6 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("GET /api/v1/user/repositories", middleware.Authentication(deps.RepositoryHandler.FetchUsersContributedRepos, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/repositories/{repo_id}", middleware.Authentication(deps.RepositoryHandler.FetchParticularRepoDetails, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/repositories/contributions/recent/{repo_id}", middleware.Authentication(deps.RepositoryHandler.FetchUserContributionsInRepo, deps.AppCfg)) + router.HandleFunc("GET /api/v1/user/repositories/languages/{repo_id}", middleware.Authentication(deps.RepositoryHandler.FetchLanguagePercentInRepo, deps.AppCfg)) return middleware.CorsMiddleware(router, deps.AppCfg) } From bb68dfaa87dc624273ccc677e0cb9b5d2c0720bc Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Thu, 19 Jun 2025 15:41:51 +0530 Subject: [PATCH 15/27] pass http client in service struct --- cmd/main.go | 4 +++- internal/app/contribution/handler.go | 3 +-- internal/app/contribution/service.go | 12 +++++++----- internal/app/dependencies.go | 8 +++++--- internal/app/repository/service.go | 10 ++++++---- 5 files changed, 22 insertions(+), 15 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index fd6692d..947cd10 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -37,7 +37,9 @@ func main() { return } - dependencies := app.InitDependencies(db, cfg, bigqueryInstance) + httpClient := &http.Client{} + + dependencies := app.InitDependencies(db, cfg, bigqueryInstance, httpClient) router := app.NewRouter(dependencies) diff --git a/internal/app/contribution/handler.go b/internal/app/contribution/handler.go index d8b9f92..fc1416d 100644 --- a/internal/app/contribution/handler.go +++ b/internal/app/contribution/handler.go @@ -27,8 +27,7 @@ func NewHandler(contributionService Service) Handler { func (h *handler) FetchUserLatestContributions(w http.ResponseWriter, r *http.Request) { ctx := r.Context() - client := &http.Client{} - err := h.contributionService.ProcessFetchedContributions(ctx, client) + err := h.contributionService.ProcessFetchedContributions(ctx) if err != nil { slog.Error("error fetching latest contributions") status, errorMessage := apperrors.MapError(err) diff --git a/internal/app/contribution/service.go b/internal/app/contribution/service.go index 1bb6e30..e6b1e25 100644 --- a/internal/app/contribution/service.go +++ b/internal/app/contribution/service.go @@ -18,26 +18,28 @@ type service struct { contributionRepository repository.ContributionRepository repositoryService repoService.Service userService user.Service + httpClient *http.Client } type Service interface { - ProcessFetchedContributions(ctx context.Context, client *http.Client) error + ProcessFetchedContributions(ctx context.Context) error CreateContribution(ctx context.Context, contributionType string, contributionDetails ContributionResponse, repositoryId int, userId int) (Contribution, error) GetContributionScoreDetailsByContributionType(ctx context.Context, contributionType string) (ContributionScore, error) FetchUsersFiveRecentContributions(ctx context.Context) ([]Contribution, error) FetchUsersAllContributions(ctx context.Context) ([]Contribution, error) } -func NewService(bigqueryService bigquery.Service, contributionRepository repository.ContributionRepository, repositoryService repoService.Service, userService user.Service) Service { +func NewService(bigqueryService bigquery.Service, contributionRepository repository.ContributionRepository, repositoryService repoService.Service, userService user.Service, httpClient *http.Client) Service { return &service{ bigqueryService: bigqueryService, contributionRepository: contributionRepository, repositoryService: repositoryService, userService: userService, + httpClient: httpClient, } } -func (s *service) ProcessFetchedContributions(ctx context.Context, client *http.Client) error { +func (s *service) ProcessFetchedContributions(ctx context.Context) error { contributions, err := s.bigqueryService.FetchDailyContributions(ctx) if err != nil { slog.Error("error fetching daily contributions", "error", err) @@ -60,9 +62,9 @@ func (s *service) ProcessFetchedContributions(ctx context.Context, client *http. } var repositoryId int - repoFetched, err := s.repositoryService.GetRepoByRepoId(ctx, contribution.RepoID) + repoFetched, err := s.repositoryService.GetRepoByRepoId(ctx, contribution.RepoID) //err no rows if err != nil { - repo, err := s.repositoryService.FetchRepositoryDetails(ctx, client, contribution.RepoUrl) + repo, err := s.repositoryService.FetchRepositoryDetails(ctx, contribution.RepoUrl) if err != nil { slog.Error("error fetching repository details") return err diff --git a/internal/app/dependencies.go b/internal/app/dependencies.go index 7f56523..616e377 100644 --- a/internal/app/dependencies.go +++ b/internal/app/dependencies.go @@ -1,6 +1,8 @@ package app import ( + "net/http" + "github.com/jmoiron/sqlx" "github.com/joshsoftware/code-curiosity-2025/internal/app/auth" "github.com/joshsoftware/code-curiosity-2025/internal/app/bigquery" @@ -22,7 +24,7 @@ type Dependencies struct { Client config.Bigquery } -func InitDependencies(db *sqlx.DB, appCfg config.AppConfig, client config.Bigquery) Dependencies { +func InitDependencies(db *sqlx.DB, appCfg config.AppConfig, client config.Bigquery, httpClient *http.Client) Dependencies { userRepository := repository.NewUserRepository(db) contributionRepository := repository.NewContributionRepository(db) repositoryRepository := repository.NewRepositoryRepository(db) @@ -30,8 +32,8 @@ func InitDependencies(db *sqlx.DB, appCfg config.AppConfig, client config.Bigque userService := user.NewService(userRepository) authService := auth.NewService(userService, appCfg) bigqueryService := bigquery.NewService(client, userRepository) - repositoryService := repoService.NewService(repositoryRepository, appCfg) - contributionService := contribution.NewService(bigqueryService, contributionRepository, repositoryService, userService) + repositoryService := repoService.NewService(repositoryRepository, appCfg, httpClient) + contributionService := contribution.NewService(bigqueryService, contributionRepository, repositoryService, userService, httpClient) authHandler := auth.NewHandler(authService, appCfg) userHandler := user.NewHandler(userService) diff --git a/internal/app/repository/service.go b/internal/app/repository/service.go index 1643745..b46a33c 100644 --- a/internal/app/repository/service.go +++ b/internal/app/repository/service.go @@ -14,18 +14,20 @@ import ( type service struct { repositoryRepository repository.RepositoryRepository appCfg config.AppConfig + httpClient *http.Client } type Service interface { GetRepoByRepoId(ctx context.Context, githubRepoId int) (Repository, error) - FetchRepositoryDetails(ctx context.Context, client *http.Client, getUserRepoDetailsUrl string) (FetchRepositoryDetailsResponse, error) + FetchRepositoryDetails(ctx context.Context, getUserRepoDetailsUrl string) (FetchRepositoryDetailsResponse, error) CreateRepository(ctx context.Context, repoGithubId int, repo FetchRepositoryDetailsResponse) (Repository, error) } -func NewService(repositoryRepository repository.RepositoryRepository, appCfg config.AppConfig) Service { +func NewService(repositoryRepository repository.RepositoryRepository, appCfg config.AppConfig, httpClient *http.Client) Service { return &service{ repositoryRepository: repositoryRepository, appCfg: appCfg, + httpClient: httpClient, } } @@ -39,7 +41,7 @@ func (s *service) GetRepoByRepoId(ctx context.Context, repoGithubId int) (Reposi return Repository(repoDetails), nil } -func (s *service) FetchRepositoryDetails(ctx context.Context, client *http.Client, getUserRepoDetailsUrl string) (FetchRepositoryDetailsResponse, error) { +func (s *service) FetchRepositoryDetails(ctx context.Context, getUserRepoDetailsUrl string) (FetchRepositoryDetailsResponse, error) { req, err := http.NewRequest("GET", getUserRepoDetailsUrl, nil) if err != nil { slog.Error("error fetching user repositories details", "error", err) @@ -48,7 +50,7 @@ func (s *service) FetchRepositoryDetails(ctx context.Context, client *http.Clien req.Header.Add("Authorization", s.appCfg.GithubPersonalAccessToken) - resp, err := client.Do(req) + resp, err := s.httpClient.Do(req) if err != nil { slog.Error("error fetching user repositories details", "error", err) return FetchRepositoryDetailsResponse{}, err From e5a4d0bf24b030f810d62a4a61884949e34113f8 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Thu, 19 Jun 2025 18:05:29 +0530 Subject: [PATCH 16/27] add contributors_url in repository table --- .../1750328591_add_column_contributors_url.down.sql | 1 + .../migrations/1750328591_add_column_contributors_url.up.sql | 5 +++++ 2 files changed, 6 insertions(+) create mode 100644 internal/db/migrations/1750328591_add_column_contributors_url.down.sql create mode 100644 internal/db/migrations/1750328591_add_column_contributors_url.up.sql diff --git a/internal/db/migrations/1750328591_add_column_contributors_url.down.sql b/internal/db/migrations/1750328591_add_column_contributors_url.down.sql new file mode 100644 index 0000000..1ed0731 --- /dev/null +++ b/internal/db/migrations/1750328591_add_column_contributors_url.down.sql @@ -0,0 +1 @@ +ALTER TABLE repositories DROP COLUMN contributors_url; \ No newline at end of file diff --git a/internal/db/migrations/1750328591_add_column_contributors_url.up.sql b/internal/db/migrations/1750328591_add_column_contributors_url.up.sql new file mode 100644 index 0000000..cc05125 --- /dev/null +++ b/internal/db/migrations/1750328591_add_column_contributors_url.up.sql @@ -0,0 +1,5 @@ +ALTER TABLE repositories ADD COLUMN contributors_url VARCHAR(255); + +UPDATE repositories SET contributors_url = '' WHERE contributors_url IS NULL; + +ALTER TABLE repositories ALTER COLUMN contributors_url SET NOT NULL; From 577324577e1e46ec4c8f5d6832dd0b07d93c5324 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Fri, 20 Jun 2025 12:14:57 +0530 Subject: [PATCH 17/27] fetch and contributors url for repository --- internal/app/contribution/domain.go | 21 +++++++++-------- internal/app/repository/domain.go | 36 +++++++++++++++-------------- internal/app/repository/service.go | 15 ++++++------ internal/repository/domain.go | 21 +++++++++-------- internal/repository/repository.go | 8 +++++-- 5 files changed, 55 insertions(+), 46 deletions(-) diff --git a/internal/app/contribution/domain.go b/internal/app/contribution/domain.go index b736d91..5fecd14 100644 --- a/internal/app/contribution/domain.go +++ b/internal/app/contribution/domain.go @@ -15,16 +15,17 @@ type ContributionResponse struct { } type Repository struct { - Id int - GithubRepoId int - RepoName string - Description string - LanguagesUrl string - RepoUrl string - OwnerName string - UpdateDate time.Time - CreatedAt time.Time - UpdatedAt time.Time + Id int + GithubRepoId int + RepoName string + Description string + LanguagesUrl string + RepoUrl string + OwnerName string + UpdateDate time.Time + ContributorsUrl string + CreatedAt time.Time + UpdatedAt time.Time } type Contribution struct { diff --git a/internal/app/repository/domain.go b/internal/app/repository/domain.go index 3c0a80c..90101b5 100644 --- a/internal/app/repository/domain.go +++ b/internal/app/repository/domain.go @@ -7,24 +7,26 @@ type RepoOWner struct { } type FetchRepositoryDetailsResponse struct { - Id int `json:"id"` - Name string `json:"name"` - Description string `json:"description"` - LanguagesURL string `json:"languages_url"` - UpdateDate time.Time `json:"updated_at"` - RepoOwnerName RepoOWner `json:"owner"` - RepoUrl string `json:"html_url"` + Id int `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + LanguagesURL string `json:"languages_url"` + UpdateDate time.Time `json:"updated_at"` + RepoOwnerName RepoOWner `json:"owner"` + ContributorsUrl string `json:"contributors_url"` + RepoUrl string `json:"html_url"` } type Repository struct { - Id int - GithubRepoId int - RepoName string - Description string - LanguagesUrl string - RepoUrl string - OwnerName string - UpdateDate time.Time - CreatedAt time.Time - UpdatedAt time.Time + Id int + GithubRepoId int + RepoName string + Description string + LanguagesUrl string + RepoUrl string + OwnerName string + UpdateDate time.Time + ContributorsUrl string + CreatedAt time.Time + UpdatedAt time.Time } diff --git a/internal/app/repository/service.go b/internal/app/repository/service.go index b46a33c..92c51e0 100644 --- a/internal/app/repository/service.go +++ b/internal/app/repository/service.go @@ -74,13 +74,14 @@ func (s *service) FetchRepositoryDetails(ctx context.Context, getUserRepoDetails func (s *service) CreateRepository(ctx context.Context, repoGithubId int, repo FetchRepositoryDetailsResponse) (Repository, error) { createRepo := Repository{ - GithubRepoId: repoGithubId, - RepoName: repo.Name, - RepoUrl: repo.RepoUrl, - Description: repo.Description, - LanguagesUrl: repo.LanguagesURL, - OwnerName: repo.RepoOwnerName.Login, - UpdateDate: repo.UpdateDate, + GithubRepoId: repoGithubId, + RepoName: repo.Name, + RepoUrl: repo.RepoUrl, + Description: repo.Description, + LanguagesUrl: repo.LanguagesURL, + OwnerName: repo.RepoOwnerName.Login, + UpdateDate: repo.UpdateDate, + ContributorsUrl: repo.ContributorsUrl, } repositoryCreated, err := s.repositoryRepository.CreateRepository(ctx, nil, repository.Repository(createRepo)) if err != nil { diff --git a/internal/repository/domain.go b/internal/repository/domain.go index 13cedc5..d56a615 100644 --- a/internal/repository/domain.go +++ b/internal/repository/domain.go @@ -43,16 +43,17 @@ type Contribution struct { } type Repository struct { - Id int - GithubRepoId int - RepoName string - Description string - LanguagesUrl string - RepoUrl string - OwnerName string - UpdateDate time.Time - CreatedAt time.Time - UpdatedAt time.Time + Id int + GithubRepoId int + RepoName string + Description string + LanguagesUrl string + RepoUrl string + OwnerName string + UpdateDate time.Time + ContributorsUrl string + CreatedAt time.Time + UpdatedAt time.Time } type ContributionScore struct { diff --git a/internal/repository/repository.go b/internal/repository/repository.go index 32a77b6..9a2fbf7 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -37,9 +37,10 @@ const ( languages_url, repo_url, owner_name, - update_date + update_date, + contributors_url ) - VALUES ($1, $2, $3, $4, $5, $6, $7) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *` ) @@ -58,6 +59,7 @@ func (rr *repositoryRepository) GetRepoByGithubId(ctx context.Context, tx *sqlx. &repository.UpdateDate, &repository.CreatedAt, &repository.UpdatedAt, + &repository.ContributorsUrl, ) if err != nil { if errors.Is(err, sql.ErrNoRows) { @@ -84,6 +86,7 @@ func (rr *repositoryRepository) CreateRepository(ctx context.Context, tx *sqlx.T repositoryInfo.RepoUrl, repositoryInfo.OwnerName, repositoryInfo.UpdateDate, + repositoryInfo.ContributorsUrl, ).Scan( &repository.Id, &repository.GithubRepoId, @@ -95,6 +98,7 @@ func (rr *repositoryRepository) CreateRepository(ctx context.Context, tx *sqlx.T &repository.UpdateDate, &repository.CreatedAt, &repository.UpdatedAt, + &repository.ContributorsUrl, ) if err != nil { slog.Error("error occured while creating repository", "error", err) From f365171d3831b234d221a8d9d19aa0a2a9096739 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Fri, 20 Jun 2025 12:20:25 +0530 Subject: [PATCH 18/27] remove fetch five recent contributions for the user- this could be handled in frontend --- internal/app/contribution/handler.go | 15 ---------- internal/app/contribution/service.go | 16 ----------- internal/app/router.go | 1 - internal/repository/contribution.go | 42 ---------------------------- 4 files changed, 74 deletions(-) diff --git a/internal/app/contribution/handler.go b/internal/app/contribution/handler.go index fc1416d..19b9d2e 100644 --- a/internal/app/contribution/handler.go +++ b/internal/app/contribution/handler.go @@ -14,7 +14,6 @@ type handler struct { type Handler interface { FetchUserLatestContributions(w http.ResponseWriter, r *http.Request) - FetchUsersFiveRecentContributions(w http.ResponseWriter, r *http.Request) FetchUsersAllContributions(w http.ResponseWriter, r *http.Request) } @@ -38,20 +37,6 @@ func (h *handler) FetchUserLatestContributions(w http.ResponseWriter, r *http.Re response.WriteJson(w, http.StatusOK, "contribution fetched successfully", nil) } -func (h *handler) FetchUsersFiveRecentContributions(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - usersFiveRecentContributions, err := h.contributionService.FetchUsersFiveRecentContributions(ctx) - if err != nil { - slog.Error("error fetching users five recent contributions") - status, errorMessage := apperrors.MapError(err) - response.WriteJson(w, status, errorMessage, nil) - return - } - - response.WriteJson(w, http.StatusOK, "users five recent contributions fetched successfully", usersFiveRecentContributions) -} - func (h *handler) FetchUsersAllContributions(w http.ResponseWriter, r *http.Request) { ctx := r.Context() diff --git a/internal/app/contribution/service.go b/internal/app/contribution/service.go index e6b1e25..046e617 100644 --- a/internal/app/contribution/service.go +++ b/internal/app/contribution/service.go @@ -25,7 +25,6 @@ type Service interface { ProcessFetchedContributions(ctx context.Context) error CreateContribution(ctx context.Context, contributionType string, contributionDetails ContributionResponse, repositoryId int, userId int) (Contribution, error) GetContributionScoreDetailsByContributionType(ctx context.Context, contributionType string) (ContributionScore, error) - FetchUsersFiveRecentContributions(ctx context.Context) ([]Contribution, error) FetchUsersAllContributions(ctx context.Context) ([]Contribution, error) } @@ -192,21 +191,6 @@ func (s *service) GetContributionScoreDetailsByContributionType(ctx context.Cont return ContributionScore(contributionScoreDetails), nil } -func (s *service) FetchUsersFiveRecentContributions(ctx context.Context) ([]Contribution, error) { - usersFiveRecentContributions, err := s.contributionRepository.FetchUsersFiveRecentContributions(ctx, nil) - if err != nil { - slog.Error("error occured while fetching users five recent contributions", "error", err) - return nil, err - } - - serviceContributions := make([]Contribution, len(usersFiveRecentContributions)) - for i, c := range usersFiveRecentContributions { - serviceContributions[i] = Contribution((c)) - } - - return serviceContributions, nil -} - func (s *service) FetchUsersAllContributions(ctx context.Context) ([]Contribution, error) { usersAllContributions, err := s.contributionRepository.FetchUsersAllContributions(ctx, nil) if err != nil { diff --git a/internal/app/router.go b/internal/app/router.go index fe2c51d..6ab919a 100644 --- a/internal/app/router.go +++ b/internal/app/router.go @@ -21,7 +21,6 @@ func NewRouter(deps Dependencies) http.Handler { router.HandleFunc("PATCH /api/v1/user/email", middleware.Authentication(deps.UserHandler.UpdateUserEmail, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/contributions/latest", middleware.Authentication(deps.ContributionHandler.FetchUserLatestContributions, deps.AppCfg)) - router.HandleFunc("GET /api/v1/user/contributions/recent", middleware.Authentication(deps.ContributionHandler.FetchUsersFiveRecentContributions, deps.AppCfg)) router.HandleFunc("GET /api/v1/user/contributions/all", middleware.Authentication(deps.ContributionHandler.FetchUsersAllContributions, deps.AppCfg)) return middleware.CorsMiddleware(router, deps.AppCfg) } diff --git a/internal/repository/contribution.go b/internal/repository/contribution.go index 1f6f2b3..436dc11 100644 --- a/internal/repository/contribution.go +++ b/internal/repository/contribution.go @@ -17,7 +17,6 @@ type ContributionRepository interface { RepositoryTransaction CreateContribution(ctx context.Context, tx *sqlx.Tx, contributionDetails Contribution) (Contribution, error) GetContributionScoreDetailsByContributionType(ctx context.Context, tx *sqlx.Tx, contributionType string) (ContributionScore, error) - FetchUsersFiveRecentContributions(ctx context.Context, tx *sqlx.Tx) ([]Contribution, error) FetchUsersAllContributions(ctx context.Context, tx *sqlx.Tx) ([]Contribution, error) } @@ -42,8 +41,6 @@ const ( getContributionScoreDetailsByContributionTypeQuery = `SELECT * from contribution_score where contribution_type=$1` - fetchUsersFiveRecentContributionsQuery = `SELECT * from contributions where user_id=$1 order by contributed_at desc limit 5` - fetchUsersAllContributionsQuery = `SELECT * from contributions where user_id=$1 order by contributed_at desc` ) @@ -97,45 +94,6 @@ func (cr *contributionRepository) GetContributionScoreDetailsByContributionType( return contributionScoreDetails, nil } -func (cr *contributionRepository) FetchUsersFiveRecentContributions(ctx context.Context, tx *sqlx.Tx) ([]Contribution, error) { - userIdValue := ctx.Value(middleware.UserIdKey) - - userId, ok := userIdValue.(int) - if !ok { - slog.Error("error obtaining user id from context") - return nil, apperrors.ErrInternalServer - } - - executer := cr.BaseRepository.initiateQueryExecuter(tx) - - rows, err := executer.QueryContext(ctx, fetchUsersFiveRecentContributionsQuery, userId) - if err != nil { - slog.Error("error fetching users five recent contributions") - return nil, apperrors.ErrFetchingRecentContributions - } - defer rows.Close() - - var usersFiveRecentContributions []Contribution - for rows.Next() { - var recentContribution Contribution - if err = rows.Scan( - &recentContribution.Id, - &recentContribution.UserId, - &recentContribution.RepositoryId, - &recentContribution.ContributionScoreId, - &recentContribution.ContributionType, - &recentContribution.BalanceChange, - &recentContribution.ContributedAt, - &recentContribution.CreatedAt, &recentContribution.UpdatedAt); err != nil { - return nil, err - } - - usersFiveRecentContributions = append(usersFiveRecentContributions, recentContribution) - } - - return usersFiveRecentContributions, nil -} - func (cr *contributionRepository) FetchUsersAllContributions(ctx context.Context, tx *sqlx.Tx) ([]Contribution, error) { userIdValue := ctx.Value(middleware.UserIdKey) From 966d2a23d5b5292ab63445b2e6e272ba32fe0902 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Fri, 20 Jun 2025 13:34:23 +0530 Subject: [PATCH 19/27] Fix: Restore correct merge changes for internal/app/repository/domain.go --- internal/app/repository/domain.go | 39 +++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/internal/app/repository/domain.go b/internal/app/repository/domain.go index 90101b5..7788e8c 100644 --- a/internal/app/repository/domain.go +++ b/internal/app/repository/domain.go @@ -30,3 +30,42 @@ type Repository struct { CreatedAt time.Time UpdatedAt time.Time } + +type RepoLanguages map[string]int + +type FetchUsersContributedReposResponse struct { + Repository + Languages []string + TotalCoinsEarned int +} + +type FetchRepoContributorsResponse struct { + Id int `json:"id"` + Name string `json:"login"` + AvatarUrl string `json:"avatar_url"` + GithubUrl string `json:"html_url"` + Contributions int `json:"contributions"` +} + +type FetchParticularRepoDetails struct { + Repository + Contributors []FetchRepoContributorsResponse +} + +type Contribution struct { + Id int + UserId int + RepositoryId int + ContributionScoreId int + ContributionType string + BalanceChange int + ContributedAt time.Time + CreatedAt time.Time + UpdatedAt time.Time +} + +type LanguagePercent struct { + Name string + Bytes int + Percentage float64 +} \ No newline at end of file From 671c8ac8ad0762d90ec0bc35a0b99e70b123ee3e Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Fri, 20 Jun 2025 13:39:47 +0530 Subject: [PATCH 20/27] add contributors url when fetching users contributes repositories --- internal/repository/repository.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/repository/repository.go b/internal/repository/repository.go index 084099a..75d61a8 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -172,7 +172,8 @@ func (r *repositoryRepository) FetchUsersContributedRepos(ctx context.Context, t &usersContributedRepo.OwnerName, &usersContributedRepo.UpdateDate, &usersContributedRepo.CreatedAt, - &usersContributedRepo.UpdatedAt); err != nil { + &usersContributedRepo.UpdatedAt, + &usersContributedRepo.ContributorsUrl); err != nil { return nil, err } From 67ab318b49c635b0b5dc6e8486d765b775660fa3 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Fri, 20 Jun 2025 13:59:08 +0530 Subject: [PATCH 21/27] seperate logic for fetching repo by github id and repository table id --- internal/app/repository/service.go | 17 ++++++++++++--- internal/repository/repository.go | 34 +++++++++++++++++++++++++++++- 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/internal/app/repository/service.go b/internal/app/repository/service.go index 23ab6f3..9a6774f 100644 --- a/internal/app/repository/service.go +++ b/internal/app/repository/service.go @@ -19,7 +19,8 @@ type service struct { } type Service interface { - GetRepoByRepoId(ctx context.Context, githubRepoId int) (Repository, error) + GetRepoByGithubId(ctx context.Context, githubRepoId int) (Repository, error) + GetRepoByRepoId(ctx context.Context, repoId int) (Repository, error) FetchRepositoryDetails(ctx context.Context, getUserRepoDetailsUrl string) (FetchRepositoryDetailsResponse, error) CreateRepository(ctx context.Context, repoGithubId int, repo FetchRepositoryDetailsResponse) (Repository, error) FetchRepositoryLanguages(ctx context.Context, client *http.Client, getRepoLanguagesURL string) (RepoLanguages, error) @@ -37,10 +38,20 @@ func NewService(repositoryRepository repository.RepositoryRepository, appCfg con } } -func (s *service) GetRepoByRepoId(ctx context.Context, repoGithubId int) (Repository, error) { +func (s *service) GetRepoByGithubId(ctx context.Context, repoGithubId int) (Repository, error) { repoDetails, err := s.repositoryRepository.GetRepoByGithubId(ctx, nil, repoGithubId) if err != nil { - slog.Error("failed to get repository by github id") + slog.Error("failed to get repository by repo github id") + return Repository{}, err + } + + return Repository(repoDetails), nil +} + +func (s *service) GetRepoByRepoId(ctx context.Context, repobId int) (Repository, error) { + repoDetails, err := s.repositoryRepository.GetRepoByRepoId(ctx, nil, repobId) + if err != nil { + slog.Error("failed to get repository by repo id") return Repository{}, err } diff --git a/internal/repository/repository.go b/internal/repository/repository.go index 75d61a8..453947b 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -18,6 +18,7 @@ type repositoryRepository struct { type RepositoryRepository interface { RepositoryTransaction GetRepoByGithubId(ctx context.Context, tx *sqlx.Tx, repoGithubId int) (Repository, error) + GetRepoByRepoId(ctx context.Context, tx *sqlx.Tx, repoId int) (Repository, error) CreateRepository(ctx context.Context, tx *sqlx.Tx, repository Repository) (Repository, error) GetUserRepoTotalCoins(ctx context.Context, tx *sqlx.Tx, repoId int) (int, error) FetchUsersContributedRepos(ctx context.Context, tx *sqlx.Tx) ([]Repository, error) @@ -33,6 +34,8 @@ func NewRepositoryRepository(db *sqlx.DB) RepositoryRepository { const ( getRepoByGithubIdQuery = `SELECT * from repositories where github_repo_id=$1` + getrepoByRepoIdQuery = `SELECT * from repositories where id=$1` + createRepositoryQuery = ` INSERT INTO repositories ( github_repo_id, @@ -76,7 +79,7 @@ func (rr *repositoryRepository) GetRepoByGithubId(ctx context.Context, tx *sqlx. slog.Error("repository not found", "error", err) return Repository{}, apperrors.ErrRepoNotFound } - slog.Error("error occurred while getting repository by id", "error", err) + slog.Error("error occurred while getting repository by repo github id", "error", err) return Repository{}, apperrors.ErrInternalServer } @@ -84,6 +87,35 @@ func (rr *repositoryRepository) GetRepoByGithubId(ctx context.Context, tx *sqlx. } +func (rr *repositoryRepository) GetRepoByRepoId(ctx context.Context, tx *sqlx.Tx, repoId int) (Repository, error) { + executer := rr.BaseRepository.initiateQueryExecuter(tx) + + var repository Repository + err := executer.QueryRowContext(ctx, getrepoByRepoIdQuery, repoId).Scan( + &repository.Id, + &repository.GithubRepoId, + &repository.RepoName, + &repository.Description, + &repository.LanguagesUrl, + &repository.RepoUrl, + &repository.OwnerName, + &repository.UpdateDate, + &repository.CreatedAt, + &repository.UpdatedAt, + &repository.ContributorsUrl, + ) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + slog.Error("repository not found", "error", err) + return Repository{}, apperrors.ErrRepoNotFound + } + slog.Error("error occurred while getting repository by id", "error", err) + return Repository{}, apperrors.ErrInternalServer + } + + return repository, nil +} + func (rr *repositoryRepository) CreateRepository(ctx context.Context, tx *sqlx.Tx, repositoryInfo Repository) (Repository, error) { executer := rr.BaseRepository.initiateQueryExecuter(tx) From 778c359fc5361f2556316e34fa950753c3bd7d1e Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Mon, 23 Jun 2025 12:38:43 +0530 Subject: [PATCH 22/27] use repo id for fetching contributions of a particular repo of a user --- internal/repository/repository.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/repository/repository.go b/internal/repository/repository.go index 453947b..fcda6b2 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -54,7 +54,7 @@ const ( fetchUsersContributedReposQuery = `SELECT * from repositories where id in (SELECT repository_id from contributions where user_id=$1);` - fetchUserContributionsInRepoQuery = `SELECT * from contributions where repository_id in (SELECT id from repositories where github_repo_id=$1) and user_id=$2;` + fetchUserContributionsInRepoQuery = `SELECT * from contributions where repository_id=$1 and user_id=$2;` ) func (rr *repositoryRepository) GetRepoByGithubId(ctx context.Context, tx *sqlx.Tx, repoGithubId int) (Repository, error) { From cbafcc09738f627db01c9ce7f2222e54a8f95f69 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Mon, 23 Jun 2025 13:06:35 +0530 Subject: [PATCH 23/27] use sqlx methods in user.go --- internal/repository/base.go | 2 + internal/repository/domain.go | 38 +++++++++---------- internal/repository/user.go | 69 ++++------------------------------- 3 files changed, 29 insertions(+), 80 deletions(-) diff --git a/internal/repository/base.go b/internal/repository/base.go index a38e9ba..e4b0795 100644 --- a/internal/repository/base.go +++ b/internal/repository/base.go @@ -24,6 +24,8 @@ type QueryExecuter interface { QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error) QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) + GetContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error + SelectContext(ctx context.Context, dest interface{}, query string, args ...interface{}) error } func (b *BaseRepository) BeginTx(ctx context.Context) (*sqlx.Tx, error) { diff --git a/internal/repository/domain.go b/internal/repository/domain.go index 29450f0..8febe1f 100644 --- a/internal/repository/domain.go +++ b/internal/repository/domain.go @@ -6,28 +6,28 @@ import ( ) type User struct { - Id int - GithubId int - GithubUsername string - Email string - AvatarUrl string - CurrentBalance int - CurrentActiveGoalId sql.NullInt64 - IsBlocked bool - IsAdmin bool - Password string - IsDeleted bool - DeletedAt sql.NullTime - CreatedAt time.Time - UpdatedAt time.Time + Id int `db:"id"` + GithubId int `db:"github_id"` + GithubUsername string `db:"github_username"` + Email string `db:"email"` + AvatarUrl string `db:"avatar_url"` + CurrentBalance int `db:"current_balance"` + CurrentActiveGoalId sql.NullInt64 `db:"current_active_goal_id"` + IsBlocked bool `db:"is_blocked"` + IsAdmin bool `db:"is_admin"` + Password string `db:"password"` + IsDeleted bool `db:"is_deleted"` + DeletedAt sql.NullTime `db:"deleted_at"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` } type CreateUserRequestBody struct { - GithubId int - GithubUsername string - AvatarUrl string - Email string - IsAdmin bool + GithubId int `db:"github_id"` + GithubUsername string `db:"github_username"` + AvatarUrl string `db:"avatar_url"` + Email string `db:"email"` + IsAdmin bool `db:"is_admin"` } type Contribution struct { diff --git a/internal/repository/user.go b/internal/repository/user.go index c504048..806124a 100644 --- a/internal/repository/user.go +++ b/internal/repository/user.go @@ -54,22 +54,7 @@ func (ur *userRepository) GetUserById(ctx context.Context, tx *sqlx.Tx, userId i executer := ur.BaseRepository.initiateQueryExecuter(tx) var user User - err := executer.QueryRowContext(ctx, getUserByIdQuery, userId).Scan( - &user.Id, - &user.GithubId, - &user.GithubUsername, - &user.AvatarUrl, - &user.Email, - &user.CurrentActiveGoalId, - &user.CurrentBalance, - &user.IsBlocked, - &user.IsAdmin, - &user.Password, - &user.IsDeleted, - &user.DeletedAt, - &user.CreatedAt, - &user.UpdatedAt, - ) + err := executer.GetContext(ctx, &user, getUserByIdQuery, userId) if err != nil { if errors.Is(err, sql.ErrNoRows) { slog.Error("user not found", "error", err) @@ -86,22 +71,7 @@ func (ur *userRepository) GetUserByGithubId(ctx context.Context, tx *sqlx.Tx, gi executer := ur.BaseRepository.initiateQueryExecuter(tx) var user User - err := executer.QueryRowContext(ctx, getUserByGithubIdQuery, githubId).Scan( - &user.Id, - &user.GithubId, - &user.GithubUsername, - &user.AvatarUrl, - &user.Email, - &user.CurrentActiveGoalId, - &user.CurrentBalance, - &user.IsBlocked, - &user.IsAdmin, - &user.Password, - &user.IsDeleted, - &user.DeletedAt, - &user.CreatedAt, - &user.UpdatedAt, - ) + err := executer.GetContext(ctx, &user, getUserByGithubIdQuery, githubId) if err != nil { if errors.Is(err, sql.ErrNoRows) { slog.Error("user not found", "error", err) @@ -118,27 +88,12 @@ func (ur *userRepository) CreateUser(ctx context.Context, tx *sqlx.Tx, userInfo executer := ur.BaseRepository.initiateQueryExecuter(tx) var user User - err := executer.QueryRowContext(ctx, createUserQuery, + err := executer.GetContext(ctx, &user, createUserQuery, userInfo.GithubId, userInfo.GithubUsername, userInfo.Email, - userInfo.AvatarUrl, - ).Scan( - &user.Id, - &user.GithubId, - &user.GithubUsername, - &user.AvatarUrl, - &user.Email, - &user.CurrentActiveGoalId, - &user.CurrentBalance, - &user.IsBlocked, - &user.IsAdmin, - &user.Password, - &user.IsDeleted, - &user.DeletedAt, - &user.CreatedAt, - &user.UpdatedAt, - ) + userInfo.AvatarUrl) + if err != nil { slog.Error("error occurred while creating user", "error", err) return User{}, apperrors.ErrUserCreationFailed @@ -162,21 +117,13 @@ func (ur *userRepository) UpdateUserEmail(ctx context.Context, tx *sqlx.Tx, user func (ur *userRepository) GetAllUsersGithubUsernames(ctx context.Context, tx *sqlx.Tx) ([]string, error) { executer := ur.BaseRepository.initiateQueryExecuter(tx) - rows, err := executer.QueryContext(ctx, getAllUsersGithubUsernamesQuery) + + var githubUsernames []string + err := executer.SelectContext(ctx, &githubUsernames, getAllUsersGithubUsernamesQuery) if err != nil { slog.Error("failed to get github usernames", "error", err) return nil, apperrors.ErrInternalServer } - defer rows.Close() - - var githubUsernames []string - for rows.Next() { - var username string - if err := rows.Scan(&username); err != nil { - return nil, err - } - githubUsernames = append(githubUsernames, username) - } return githubUsernames, nil } From f36069f9577c22510d74bb05e6aa8581719881ce Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Mon, 23 Jun 2025 13:27:09 +0530 Subject: [PATCH 24/27] use sqlx methods in repository.go --- internal/repository/domain.go | 40 +++++++------- internal/repository/repository.go | 92 +++---------------------------- 2 files changed, 28 insertions(+), 104 deletions(-) diff --git a/internal/repository/domain.go b/internal/repository/domain.go index 8febe1f..7a00a25 100644 --- a/internal/repository/domain.go +++ b/internal/repository/domain.go @@ -31,29 +31,29 @@ type CreateUserRequestBody struct { } type Contribution struct { - Id int - UserId int - RepositoryId int - ContributionScoreId int - ContributionType string - BalanceChange int - ContributedAt time.Time - CreatedAt time.Time - UpdatedAt time.Time + Id int `db:"id"` + UserId int `db:"user_id"` + RepositoryId int `db:"repository_id"` + ContributionScoreId int `db:"contribution_score_id"` + ContributionType string `db:"contribution_type"` + BalanceChange int `db:"balance_change"` + ContributedAt time.Time `db:"contributed_at"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` } type Repository struct { - Id int - GithubRepoId int - RepoName string - Description string - LanguagesUrl string - RepoUrl string - OwnerName string - UpdateDate time.Time - ContributorsUrl string - CreatedAt time.Time - UpdatedAt time.Time + Id int `db:"id"` + GithubRepoId int `db:"github_repo_id"` + RepoName string `db:"repo_name"` + Description string `db:"description"` + LanguagesUrl string `db:"languages_url"` + RepoUrl string `db:"repo_url"` + OwnerName string `db:"owner_name"` + UpdateDate time.Time `db:"update_date"` + ContributorsUrl string `db:"contributors_url"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` } type ContributionScore struct { diff --git a/internal/repository/repository.go b/internal/repository/repository.go index fcda6b2..c1b03ce 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -61,19 +61,7 @@ func (rr *repositoryRepository) GetRepoByGithubId(ctx context.Context, tx *sqlx. executer := rr.BaseRepository.initiateQueryExecuter(tx) var repository Repository - err := executer.QueryRowContext(ctx, getRepoByGithubIdQuery, repoGithubId).Scan( - &repository.Id, - &repository.GithubRepoId, - &repository.RepoName, - &repository.Description, - &repository.LanguagesUrl, - &repository.RepoUrl, - &repository.OwnerName, - &repository.UpdateDate, - &repository.CreatedAt, - &repository.UpdatedAt, - &repository.ContributorsUrl, - ) + err := executer.GetContext(ctx, &repository, getRepoByGithubIdQuery, repoGithubId) if err != nil { if errors.Is(err, sql.ErrNoRows) { slog.Error("repository not found", "error", err) @@ -91,19 +79,7 @@ func (rr *repositoryRepository) GetRepoByRepoId(ctx context.Context, tx *sqlx.Tx executer := rr.BaseRepository.initiateQueryExecuter(tx) var repository Repository - err := executer.QueryRowContext(ctx, getrepoByRepoIdQuery, repoId).Scan( - &repository.Id, - &repository.GithubRepoId, - &repository.RepoName, - &repository.Description, - &repository.LanguagesUrl, - &repository.RepoUrl, - &repository.OwnerName, - &repository.UpdateDate, - &repository.CreatedAt, - &repository.UpdatedAt, - &repository.ContributorsUrl, - ) + err := executer.GetContext(ctx, &repository, getrepoByRepoIdQuery, repoId) if err != nil { if errors.Is(err, sql.ErrNoRows) { slog.Error("repository not found", "error", err) @@ -120,7 +96,7 @@ func (rr *repositoryRepository) CreateRepository(ctx context.Context, tx *sqlx.T executer := rr.BaseRepository.initiateQueryExecuter(tx) var repository Repository - err := executer.QueryRowContext(ctx, createRepositoryQuery, + err := executer.GetContext(ctx, &repository, createRepositoryQuery, repositoryInfo.GithubRepoId, repositoryInfo.RepoName, repositoryInfo.Description, @@ -129,18 +105,6 @@ func (rr *repositoryRepository) CreateRepository(ctx context.Context, tx *sqlx.T repositoryInfo.OwnerName, repositoryInfo.UpdateDate, repositoryInfo.ContributorsUrl, - ).Scan( - &repository.Id, - &repository.GithubRepoId, - &repository.RepoName, - &repository.Description, - &repository.LanguagesUrl, - &repository.RepoUrl, - &repository.OwnerName, - &repository.UpdateDate, - &repository.CreatedAt, - &repository.UpdatedAt, - &repository.ContributorsUrl, ) if err != nil { slog.Error("error occured while creating repository", "error", err) @@ -164,7 +128,7 @@ func (r *repositoryRepository) GetUserRepoTotalCoins(ctx context.Context, tx *sq var totalCoins int - err := executer.QueryRowContext(ctx, getUserRepoTotalCoinsQuery, userId, repoId).Scan(&totalCoins) + err := executer.GetContext(ctx, &totalCoins, getUserRepoTotalCoinsQuery, userId, repoId) if err != nil { slog.Error("error calculating total coins earned by user for the repository") return 0, apperrors.ErrCalculatingUserRepoTotalCoins @@ -184,33 +148,12 @@ func (r *repositoryRepository) FetchUsersContributedRepos(ctx context.Context, t executer := r.BaseRepository.initiateQueryExecuter(tx) - rows, err := executer.QueryContext(ctx, fetchUsersContributedReposQuery, userId) + var usersContributedRepos []Repository + err := executer.SelectContext(ctx, &usersContributedRepos, fetchUsersContributedReposQuery, userId) if err != nil { slog.Error("error fetching users contributed repositories") return nil, apperrors.ErrFetchingUsersContributedRepos } - defer rows.Close() - - var usersContributedRepos []Repository - for rows.Next() { - var usersContributedRepo Repository - if err = rows.Scan( - &usersContributedRepo.Id, - &usersContributedRepo.GithubRepoId, - &usersContributedRepo.RepoName, - &usersContributedRepo.Description, - &usersContributedRepo.LanguagesUrl, - &usersContributedRepo.RepoUrl, - &usersContributedRepo.OwnerName, - &usersContributedRepo.UpdateDate, - &usersContributedRepo.CreatedAt, - &usersContributedRepo.UpdatedAt, - &usersContributedRepo.ContributorsUrl); err != nil { - return nil, err - } - - usersContributedRepos = append(usersContributedRepos, usersContributedRepo) - } return usersContributedRepos, nil } @@ -226,31 +169,12 @@ func (r *repositoryRepository) FetchUserContributionsInRepo(ctx context.Context, executer := r.BaseRepository.initiateQueryExecuter(tx) - rows, err := executer.QueryContext(ctx, fetchUserContributionsInRepoQuery, repoGithubId, userId) + var userContributionsInRepo []Contribution + err := executer.SelectContext(ctx, &userContributionsInRepo, fetchUserContributionsInRepoQuery, repoGithubId, userId) if err != nil { slog.Error("error fetching users contribution in repository") return nil, apperrors.ErrFetchingUserContributionsInRepo } - defer rows.Close() - - var userContributionsInRepo []Contribution - for rows.Next() { - var userContributionInRepo Contribution - if err = rows.Scan( - &userContributionInRepo.Id, - &userContributionInRepo.UserId, - &userContributionInRepo.RepositoryId, - &userContributionInRepo.ContributionScoreId, - &userContributionInRepo.ContributionType, - &userContributionInRepo.BalanceChange, - &userContributionInRepo.ContributedAt, - &userContributionInRepo.CreatedAt, - &userContributionInRepo.UpdatedAt); err != nil { - return nil, err - } - - userContributionsInRepo = append(userContributionsInRepo, userContributionInRepo) - } return userContributionsInRepo, nil } From 050487e1b12dd66fbaa45f6d401347173a111767 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Mon, 23 Jun 2025 13:38:26 +0530 Subject: [PATCH 25/27] use sqlx methods in contribution.go --- internal/repository/contribution.go | 43 +++-------------------------- internal/repository/domain.go | 12 ++++---- 2 files changed, 10 insertions(+), 45 deletions(-) diff --git a/internal/repository/contribution.go b/internal/repository/contribution.go index 436dc11..54081ab 100644 --- a/internal/repository/contribution.go +++ b/internal/repository/contribution.go @@ -48,23 +48,13 @@ func (cr *contributionRepository) CreateContribution(ctx context.Context, tx *sq executer := cr.BaseRepository.initiateQueryExecuter(tx) var contribution Contribution - err := executer.QueryRowContext(ctx, createContributionQuery, + err := executer.GetContext(ctx, &contribution, createContributionQuery, contributionInfo.UserId, contributionInfo.RepositoryId, contributionInfo.ContributionScoreId, contributionInfo.ContributionType, contributionInfo.BalanceChange, contributionInfo.ContributedAt, - ).Scan( - &contribution.Id, - &contribution.UserId, - &contribution.RepositoryId, - &contribution.ContributionScoreId, - &contribution.ContributionType, - &contribution.BalanceChange, - &contribution.ContributedAt, - &contribution.CreatedAt, - &contribution.UpdatedAt, ) if err != nil { slog.Error("error occured while inserting contributions", "error", err) @@ -78,14 +68,7 @@ func (cr *contributionRepository) GetContributionScoreDetailsByContributionType( executer := cr.BaseRepository.initiateQueryExecuter(tx) var contributionScoreDetails ContributionScore - err := executer.QueryRowContext(ctx, getContributionScoreDetailsByContributionTypeQuery, contributionType).Scan( - &contributionScoreDetails.Id, - &contributionScoreDetails.AdminId, - &contributionScoreDetails.ContributionType, - &contributionScoreDetails.Score, - &contributionScoreDetails.CreatedAt, - &contributionScoreDetails.UpdatedAt, - ) + err := executer.GetContext(ctx, &contributionScoreDetails, getContributionScoreDetailsByContributionTypeQuery, contributionType) if err != nil { slog.Error("error occured while getting contribution score details", "error", err) return ContributionScore{}, err @@ -105,30 +88,12 @@ func (cr *contributionRepository) FetchUsersAllContributions(ctx context.Context executer := cr.BaseRepository.initiateQueryExecuter(tx) - rows, err := executer.QueryContext(ctx, fetchUsersAllContributionsQuery, userId) + var usersAllContributions []Contribution + err := executer.SelectContext(ctx, &usersAllContributions, fetchUsersAllContributionsQuery, userId) if err != nil { slog.Error("error fetching all contributions for user") return nil, apperrors.ErrFetchingAllContributions } - defer rows.Close() - - var usersAllContributions []Contribution - for rows.Next() { - var currentContribution Contribution - if err = rows.Scan( - ¤tContribution.Id, - ¤tContribution.UserId, - ¤tContribution.RepositoryId, - ¤tContribution.ContributionScoreId, - ¤tContribution.ContributionType, - ¤tContribution.BalanceChange, - ¤tContribution.ContributedAt, - ¤tContribution.CreatedAt, ¤tContribution.UpdatedAt); err != nil { - return nil, err - } - - usersAllContributions = append(usersAllContributions, currentContribution) - } return usersAllContributions, nil } diff --git a/internal/repository/domain.go b/internal/repository/domain.go index 7a00a25..148c2e3 100644 --- a/internal/repository/domain.go +++ b/internal/repository/domain.go @@ -57,10 +57,10 @@ type Repository struct { } type ContributionScore struct { - Id int - AdminId int - ContributionType string - Score int - CreatedAt time.Time - UpdatedAt time.Time + Id int `db:"id"` + AdminId int `db:"admin_id"` + ContributionType string `db:"contribution_type"` + Score int `db:"score"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` } From f5af0ac6f607a8923b24f971b1a1e6bc52af7987 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Mon, 23 Jun 2025 13:44:08 +0530 Subject: [PATCH 26/27] log error in slog --- internal/app/contribution/handler.go | 4 ++-- internal/app/contribution/service.go | 4 ++-- internal/app/repository/handler.go | 24 ++++++++++++------------ internal/app/repository/service.go | 10 +++++----- internal/repository/contribution.go | 2 +- internal/repository/repository.go | 6 +++--- 6 files changed, 25 insertions(+), 25 deletions(-) diff --git a/internal/app/contribution/handler.go b/internal/app/contribution/handler.go index 19b9d2e..b4b8a4e 100644 --- a/internal/app/contribution/handler.go +++ b/internal/app/contribution/handler.go @@ -28,7 +28,7 @@ func (h *handler) FetchUserLatestContributions(w http.ResponseWriter, r *http.Re err := h.contributionService.ProcessFetchedContributions(ctx) if err != nil { - slog.Error("error fetching latest contributions") + slog.Error("error fetching latest contributions", "error", err) status, errorMessage := apperrors.MapError(err) response.WriteJson(w, status, errorMessage, nil) return @@ -42,7 +42,7 @@ func (h *handler) FetchUsersAllContributions(w http.ResponseWriter, r *http.Requ usersAllContributions, err := h.contributionService.FetchUsersAllContributions(ctx) if err != nil { - slog.Error("error fetching all contributions for user") + slog.Error("error fetching all contributions for user", "error", err) status, errorMessage := apperrors.MapError(err) response.WriteJson(w, status, errorMessage, nil) return diff --git a/internal/app/contribution/service.go b/internal/app/contribution/service.go index 046e617..085c269 100644 --- a/internal/app/contribution/service.go +++ b/internal/app/contribution/service.go @@ -56,7 +56,7 @@ func (s *service) ProcessFetchedContributions(ctx context.Context) error { contributionType, err := s.GetContributionType(ctx, contribution) if err != nil { - slog.Error("error getting contribution type") + slog.Error("error getting contribution type", "error", err) return err } @@ -65,7 +65,7 @@ func (s *service) ProcessFetchedContributions(ctx context.Context) error { if err != nil { repo, err := s.repositoryService.FetchRepositoryDetails(ctx, contribution.RepoUrl) if err != nil { - slog.Error("error fetching repository details") + slog.Error("error fetching repository details", "error", err) return err } diff --git a/internal/app/repository/handler.go b/internal/app/repository/handler.go index e8f65f8..083d14c 100644 --- a/internal/app/repository/handler.go +++ b/internal/app/repository/handler.go @@ -33,7 +33,7 @@ func (h *handler) FetchUsersContributedRepos(w http.ResponseWriter, r *http.Requ usersContributedRepos, err := h.repositoryService.FetchUsersContributedRepos(ctx, client) if err != nil { - slog.Error("error fetching users conributed repos") + slog.Error("error fetching users conributed repos", "error", err) status, errorMessage := apperrors.MapError(err) response.WriteJson(w, status, errorMessage, nil) return @@ -47,7 +47,7 @@ func (h *handler) FetchParticularRepoDetails(w http.ResponseWriter, r *http.Requ repoIdPath := r.PathValue("repo_id") repoId, err := strconv.Atoi(repoIdPath) if err != nil { - slog.Error("error getting repo id from request url") + slog.Error("error getting repo id from request url", "error", err) status, errorMessage := apperrors.MapError(err) response.WriteJson(w, status, errorMessage, nil) return @@ -55,7 +55,7 @@ func (h *handler) FetchParticularRepoDetails(w http.ResponseWriter, r *http.Requ repoDetails, err := h.repositoryService.GetRepoByRepoId(ctx, repoId) if err != nil { - slog.Error("error fetching particular repo details") + slog.Error("error fetching particular repo details", "error", err) status, errorMessage := apperrors.MapError(err) response.WriteJson(w, status, errorMessage, nil) return @@ -72,7 +72,7 @@ func (h *handler) FetchParticularRepoContributors(w http.ResponseWriter, r *http repoIdPath := r.PathValue("repo_id") repoId, err := strconv.Atoi(repoIdPath) if err != nil { - slog.Error("error getting repo id from request url") + slog.Error("error getting repo id from request url", "error", err) status, errorMessage := apperrors.MapError(err) response.WriteJson(w, status, errorMessage, nil) return @@ -80,7 +80,7 @@ func (h *handler) FetchParticularRepoContributors(w http.ResponseWriter, r *http repoDetails, err := h.repositoryService.GetRepoByRepoId(ctx, repoId) if err != nil { - slog.Error("error fetching particular repo details") + slog.Error("error fetching particular repo details", "error", err) status, errorMessage := apperrors.MapError(err) response.WriteJson(w, status, errorMessage, nil) return @@ -88,7 +88,7 @@ func (h *handler) FetchParticularRepoContributors(w http.ResponseWriter, r *http repoContributors, err := h.repositoryService.FetchRepositoryContributors(ctx, client, repoDetails.ContributorsUrl) if err != nil { - slog.Error("error fetching repo contributors") + slog.Error("error fetching repo contributors", "error", err) status, errorMessage := apperrors.MapError(err) response.WriteJson(w, status, errorMessage, nil) return @@ -103,7 +103,7 @@ func (h *handler) FetchUserContributionsInRepo(w http.ResponseWriter, r *http.Re repoIdPath := r.PathValue("repo_id") repoId, err := strconv.Atoi(repoIdPath) if err != nil { - slog.Error("error getting repo id from request url") + slog.Error("error getting repo id from request url", "error", err) status, errorMessage := apperrors.MapError(err) response.WriteJson(w, status, errorMessage, nil) return @@ -111,7 +111,7 @@ func (h *handler) FetchUserContributionsInRepo(w http.ResponseWriter, r *http.Re usersContributionsInRepo, err := h.repositoryService.FetchUserContributionsInRepo(ctx, repoId) if err != nil { - slog.Error("error fetching users contribution in repository") + slog.Error("error fetching users contribution in repository", "error", err) status, errorMessage := apperrors.MapError(err) response.WriteJson(w, status, errorMessage, nil) return @@ -128,7 +128,7 @@ func (h *handler) FetchLanguagePercentInRepo(w http.ResponseWriter, r *http.Requ repoIdPath := r.PathValue("repo_id") repoId, err := strconv.Atoi(repoIdPath) if err != nil { - slog.Error("error getting repo id from request url") + slog.Error("error getting repo id from request url", "error", err) status, errorMessage := apperrors.MapError(err) response.WriteJson(w, status, errorMessage, nil) return @@ -136,7 +136,7 @@ func (h *handler) FetchLanguagePercentInRepo(w http.ResponseWriter, r *http.Requ repoDetails, err := h.repositoryService.GetRepoByRepoId(ctx, repoId) if err != nil { - slog.Error("error fetching particular repo details") + slog.Error("error fetching particular repo details", "error", err) status, errorMessage := apperrors.MapError(err) response.WriteJson(w, status, errorMessage, nil) return @@ -144,7 +144,7 @@ func (h *handler) FetchLanguagePercentInRepo(w http.ResponseWriter, r *http.Requ repoLanguages, err := h.repositoryService.FetchRepositoryLanguages(ctx, client, repoDetails.LanguagesUrl) if err != nil { - slog.Error("error fetching particular repo languages") + slog.Error("error fetching particular repo languages", "error", err) status, errorMessage := apperrors.MapError(err) response.WriteJson(w, status, errorMessage, nil) return @@ -152,7 +152,7 @@ func (h *handler) FetchLanguagePercentInRepo(w http.ResponseWriter, r *http.Requ langPercent, err := h.repositoryService.CalculateLanguagePercentInRepo(ctx, repoLanguages) if err != nil { - slog.Error("error fetching particular repo languages") + slog.Error("error fetching particular repo languages", "error", err) status, errorMessage := apperrors.MapError(err) response.WriteJson(w, status, errorMessage, nil) return diff --git a/internal/app/repository/service.go b/internal/app/repository/service.go index 9a6774f..a7499f6 100644 --- a/internal/app/repository/service.go +++ b/internal/app/repository/service.go @@ -41,7 +41,7 @@ func NewService(repositoryRepository repository.RepositoryRepository, appCfg con func (s *service) GetRepoByGithubId(ctx context.Context, repoGithubId int) (Repository, error) { repoDetails, err := s.repositoryRepository.GetRepoByGithubId(ctx, nil, repoGithubId) if err != nil { - slog.Error("failed to get repository by repo github id") + slog.Error("failed to get repository by repo github id", "error", err) return Repository{}, err } @@ -51,7 +51,7 @@ func (s *service) GetRepoByGithubId(ctx context.Context, repoGithubId int) (Repo func (s *service) GetRepoByRepoId(ctx context.Context, repobId int) (Repository, error) { repoDetails, err := s.repositoryRepository.GetRepoByRepoId(ctx, nil, repobId) if err != nil { - slog.Error("failed to get repository by repo id") + slog.Error("failed to get repository by repo id", "error", err) return Repository{}, err } @@ -141,7 +141,7 @@ func (s *service) FetchRepositoryLanguages(ctx context.Context, client *http.Cli func (s *service) FetchUsersContributedRepos(ctx context.Context, client *http.Client) ([]FetchUsersContributedReposResponse, error) { usersContributedRepos, err := s.repositoryRepository.FetchUsersContributedRepos(ctx, nil) if err != nil { - slog.Error("error fetching users conributed repos") + slog.Error("error fetching users conributed repos", "error", err) return nil, err } @@ -162,7 +162,7 @@ func (s *service) FetchUsersContributedRepos(ctx context.Context, client *http.C userRepoTotalCoins, err := s.repositoryRepository.GetUserRepoTotalCoins(ctx, nil, usersContributedRepo.Id) if err != nil { - slog.Error("error calculating total coins earned by user for the repository") + slog.Error("error calculating total coins earned by user for the repository", "error", err) return nil, err } @@ -204,7 +204,7 @@ func (s *service) FetchRepositoryContributors(ctx context.Context, client *http. func (s *service) FetchUserContributionsInRepo(ctx context.Context, githubRepoId int) ([]Contribution, error) { userContributionsInRepo, err := s.repositoryRepository.FetchUserContributionsInRepo(ctx, nil, githubRepoId) if err != nil { - slog.Error("error fetching users contribution in repository") + slog.Error("error fetching users contribution in repository", "error", err) return nil, err } diff --git a/internal/repository/contribution.go b/internal/repository/contribution.go index 436dc11..94b7e13 100644 --- a/internal/repository/contribution.go +++ b/internal/repository/contribution.go @@ -107,7 +107,7 @@ func (cr *contributionRepository) FetchUsersAllContributions(ctx context.Context rows, err := executer.QueryContext(ctx, fetchUsersAllContributionsQuery, userId) if err != nil { - slog.Error("error fetching all contributions for user") + slog.Error("error fetching all contributions for user", "error", err) return nil, apperrors.ErrFetchingAllContributions } defer rows.Close() diff --git a/internal/repository/repository.go b/internal/repository/repository.go index fcda6b2..05e5cd9 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -166,7 +166,7 @@ func (r *repositoryRepository) GetUserRepoTotalCoins(ctx context.Context, tx *sq err := executer.QueryRowContext(ctx, getUserRepoTotalCoinsQuery, userId, repoId).Scan(&totalCoins) if err != nil { - slog.Error("error calculating total coins earned by user for the repository") + slog.Error("error calculating total coins earned by user for the repository", "error", err) return 0, apperrors.ErrCalculatingUserRepoTotalCoins } @@ -186,7 +186,7 @@ func (r *repositoryRepository) FetchUsersContributedRepos(ctx context.Context, t rows, err := executer.QueryContext(ctx, fetchUsersContributedReposQuery, userId) if err != nil { - slog.Error("error fetching users contributed repositories") + slog.Error("error fetching users contributed repositories", "error", err) return nil, apperrors.ErrFetchingUsersContributedRepos } defer rows.Close() @@ -228,7 +228,7 @@ func (r *repositoryRepository) FetchUserContributionsInRepo(ctx context.Context, rows, err := executer.QueryContext(ctx, fetchUserContributionsInRepoQuery, repoGithubId, userId) if err != nil { - slog.Error("error fetching users contribution in repository") + slog.Error("error fetching users contribution in repository", "error", err) return nil, apperrors.ErrFetchingUserContributionsInRepo } defer rows.Close() From 187dc89eeae4d596417d7951e31633cc395440c6 Mon Sep 17 00:00:00 2001 From: VishakhaSainani Date: Tue, 1 Jul 2025 12:03:15 +0530 Subject: [PATCH 27/27] handle no row error in GetContributionScoreDetails function --- internal/pkg/apperrors/errors.go | 1 + internal/repository/contribution.go | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/internal/pkg/apperrors/errors.go b/internal/pkg/apperrors/errors.go index 743e37a..7c6a6e4 100644 --- a/internal/pkg/apperrors/errors.go +++ b/internal/pkg/apperrors/errors.go @@ -40,6 +40,7 @@ var ( ErrContributionCreationFailed = errors.New("failed to create contrbitution") ErrFetchingRecentContributions = errors.New("failed to fetch users five recent contributions") ErrFetchingAllContributions = errors.New("failed to fetch all contributions for user") + ErrContributionScoreNotFound = errors.New("failed to get contributionscore details for given contribution type") ) func MapError(err error) (statusCode int, errMessage string) { diff --git a/internal/repository/contribution.go b/internal/repository/contribution.go index 674dec6..3293992 100644 --- a/internal/repository/contribution.go +++ b/internal/repository/contribution.go @@ -2,6 +2,8 @@ package repository import ( "context" + "database/sql" + "errors" "log/slog" "github.com/jmoiron/sqlx" @@ -70,6 +72,11 @@ func (cr *contributionRepository) GetContributionScoreDetailsByContributionType( var contributionScoreDetails ContributionScore err := executer.GetContext(ctx, &contributionScoreDetails, getContributionScoreDetailsByContributionTypeQuery, contributionType) if err != nil { + if errors.Is(err, sql.ErrNoRows) { + slog.Warn("no contribution score details found for contribution type", "contributionType", contributionType) + return ContributionScore{}, apperrors.ErrContributionScoreNotFound + } + slog.Error("error occured while getting contribution score details", "error", err) return ContributionScore{}, err }