From a0807d568beffa7701aee54a9ddac848c74bd705 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Santos?= Date: Thu, 30 Jan 2025 10:49:32 -0300 Subject: [PATCH 01/27] feat: docker-compose * postgres * kafka (complete) --- docker-compose.yaml | 52 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index 6ec20b6..aba8328 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,7 +1,57 @@ -version: "3" +version: '3.8' + services: api: build: . container_name: twitter_clone_api ports: - 3001:8001 + postgres: + image: postgres:15 + container_name: postgres + restart: always + environment: + POSTGRES_USER: user + POSTGRES_PASSWORD: password + POSTGRES_DB: mydatabase + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + + zookeeper: + image: confluentinc/cp-zookeeper:7.5.0 + container_name: zookeeper + restart: always + environment: + ZOOKEEPER_CLIENT_PORT: 2181 + ports: + - "2181:2181" + + kafka: + image: confluentinc/cp-kafka:7.5.0 + container_name: kafka + restart: always + depends_on: + - zookeeper + environment: + KAFKA_BROKER_ID: 1 + KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 + KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092 + KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 + ports: + - "9092:9092" + + kafdrop: + image: obsidiandynamics/kafdrop:latest + container_name: kafdrop + restart: always + depends_on: + - kafka + environment: + KAFKA_BROKERCONNECT: kafka:9092 + ports: + - "9000:9000" + +volumes: + postgres_data: From a7f3b6fdda030131bce6f3636b09e2477d619970 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Santos?= Date: Thu, 30 Jan 2025 17:38:59 -0300 Subject: [PATCH 02/27] feat: migrations --- LICENSE | 28 +++++++++++++++++++ cmd/api/main.go | 15 ++++++++++ migrations/000001_create_users_table.down.sql | 1 + migrations/000001_create_users_table.up.sql | 7 +++++ migrations/000002_create_posts_table.down.sql | 1 + migrations/000002_create_posts_table.up.sql | 16 +++++++++++ 6 files changed, 68 insertions(+) create mode 100644 LICENSE create mode 100644 migrations/000001_create_users_table.down.sql create mode 100644 migrations/000001_create_users_table.up.sql create mode 100644 migrations/000002_create_posts_table.down.sql create mode 100644 migrations/000002_create_posts_table.up.sql diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d03742c --- /dev/null +++ b/LICENSE @@ -0,0 +1,28 @@ +The MIT License (MIT) + +Original Work +Copyright (c) 2016 Matthias Kadenbach +https://github.com/mattes/migrate + +Modified Work +Copyright (c) 2018 Dale Hui +https://github.com/golang-migrate/migrate + + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/cmd/api/main.go b/cmd/api/main.go index 3c483a7..341ee29 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -6,6 +6,9 @@ import ( inmemory_schema "github.com/dexfs/go-twitter-clone/adapter/output/repository/inmemory/schema" "github.com/dexfs/go-twitter-clone/internal/core/port/output" "github.com/dexfs/go-twitter-clone/pkg/database" + "github.com/golang-migrate/migrate/v4" + _ "github.com/golang-migrate/migrate/v4/database/postgres" + _ "github.com/golang-migrate/migrate/v4/source/file" "log" "time" ) @@ -17,6 +20,18 @@ var ( ) func init() { + + m, err := migrate.New("file://migrations", "postgres://gouser:123456@localhost:5432/gotwitterclone?sslmode=disable") + if err != nil { + log.Fatal("Error on load migrations:", err) + } + + if err := m.Up(); err != nil && err != migrate.ErrNoChange { + log.Fatal("Error on apply migrations:", err) + } + + log.Println("Migrations applied") + db = database.NewInMemoryDB() if db == nil { log.Fatal("database is nil") diff --git a/migrations/000001_create_users_table.down.sql b/migrations/000001_create_users_table.down.sql new file mode 100644 index 0000000..93ad809 --- /dev/null +++ b/migrations/000001_create_users_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS users CASCADE; \ No newline at end of file diff --git a/migrations/000001_create_users_table.up.sql b/migrations/000001_create_users_table.up.sql new file mode 100644 index 0000000..2170d1f --- /dev/null +++ b/migrations/000001_create_users_table.up.sql @@ -0,0 +1,7 @@ +CREATE TABLE users +( + id VARCHAR(36) PRIMARY KEY, -- Sem default, deve ser fornecido manualmente, uuid V7: ex: 0194b7b8-1110-79c0-8d41-cff79b880911 + username varchar(50) NOT NULL UNIQUE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); diff --git a/migrations/000002_create_posts_table.down.sql b/migrations/000002_create_posts_table.down.sql new file mode 100644 index 0000000..181b11a --- /dev/null +++ b/migrations/000002_create_posts_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS posts CASCADE; \ No newline at end of file diff --git a/migrations/000002_create_posts_table.up.sql b/migrations/000002_create_posts_table.up.sql new file mode 100644 index 0000000..9cf7116 --- /dev/null +++ b/migrations/000002_create_posts_table.up.sql @@ -0,0 +1,16 @@ +CREATE TABLE posts +( + id VARCHAR(36) PRIMARY KEY, -- Sem default, deve ser fornecido manualmente, uuid V7: ex: 0194b7b8-1110-79c0-8d41-cff79b880911 + user_id VARCHAR(36) NOT NULL REFERENCES users (id) ON DELETE CASCADE, + content TEXT NOT NULL, + is_quote BOOLEAN NOT NULL DEFAULT FALSE, + is_repost BOOLEAN NOT NULL DEFAULT FALSE, + original_post_id VARCHAR(36) NULL, + original_post_content TEXT NULL, + original_post_user_id VARCHAR(36) NULL, + original_post_screen_name TEXT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + CONSTRAINT fk_original_post FOREIGN KEY (original_post_id) REFERENCES posts (id) ON DELETE SET NULL +); + +CREATE INDEX idx_posts_user_created_at ON posts(user_id, created_at DESC); \ No newline at end of file From 7f44817b145a47e7cebcbac9df85a74d7efbdab5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Santos?= Date: Thu, 30 Jan 2025 17:40:20 -0300 Subject: [PATCH 03/27] feat: docker-compose --- docker-compose.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index aba8328..b1380a1 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -11,9 +11,9 @@ services: container_name: postgres restart: always environment: - POSTGRES_USER: user - POSTGRES_PASSWORD: password - POSTGRES_DB: mydatabase + POSTGRES_USER: gouser + POSTGRES_PASSWORD: 123456 + POSTGRES_DB: gotwitterclone ports: - "5432:5432" volumes: From 94c2c57ab44adb71ca76abb9bc8afdf9e76bd230 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Santos?= Date: Thu, 30 Jan 2025 17:40:37 -0300 Subject: [PATCH 04/27] chore: update dependencies --- go.mod | 17 +++++++---- go.sum | 94 ++++++++++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 83 insertions(+), 28 deletions(-) diff --git a/go.mod b/go.mod index 0f2efd9..7d6da6e 100644 --- a/go.mod +++ b/go.mod @@ -4,10 +4,12 @@ go 1.22.0 require ( github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6 + github.com/gin-contrib/cors v1.7.2 github.com/gin-gonic/gin v1.9.1 github.com/go-playground/locales v0.14.1 github.com/go-playground/universal-translator v0.18.1 github.com/go-playground/validator/v10 v10.20.0 + github.com/golang-migrate/migrate/v4 v4.18.2 github.com/google/uuid v1.6.0 github.com/stretchr/testify v1.9.0 ) @@ -19,12 +21,14 @@ require ( github.com/cloudwego/iasm v0.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect - github.com/gin-contrib/cors v1.7.2 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/goccy/go-json v0.10.2 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/leodido/go-urn v1.4.0 // indirect + github.com/lib/pq v1.10.9 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect @@ -32,11 +36,12 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.12 // indirect + go.uber.org/atomic v1.7.0 // indirect golang.org/x/arch v0.7.0 // indirect - golang.org/x/crypto v0.22.0 // indirect - golang.org/x/net v0.24.0 // indirect - golang.org/x/sys v0.19.0 // indirect - golang.org/x/text v0.14.0 // indirect - google.golang.org/protobuf v1.34.0 // indirect + golang.org/x/crypto v0.31.0 // indirect + golang.org/x/net v0.33.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 351c887..46578e5 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,11 @@ -github.com/bytedance/sonic v1.11.5 h1:G00FYjjqll5iQ1PYXynbg/hyzqBqavH8Mo9/oTopd9k= -github.com/bytedance/sonic v1.11.5/go.mod h1:X2PC2giUdj/Cv2lliWFLk6c/DUQok5rViJSemeB0wDw= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= -github.com/bytedance/sonic/loader v0.1.0/go.mod h1:UmRT+IRTGKz/DAkzcEGzyVqQFJ7H9BqwBO3pm9H/+HY= github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= -github.com/cloudwego/base64x v0.1.3 h1:b5J/l8xolB7dyDTTmhJP2oTs5LdrjyrUFuNxdfq5hAg= -github.com/cloudwego/base64x v0.1.3/go.mod h1:1+1K5BUHIQzyapgpF7LwvOGAEDicKtt1umPV+aN8pi8= github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= @@ -14,6 +13,18 @@ github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dhui/dktest v0.4.4 h1:+I4s6JRE1yGuqflzwqG+aIaMdgXIorCf5P98JnaAWa8= +github.com/dhui/dktest v0.4.4/go.mod h1:4+22R4lgsdAXrDyaH4Nqx2JEz2hLp49MqQmm9HLCQhM= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v27.2.0+incompatible h1:Rk9nIVdfH3+Vz4cyI/uhbINhEZ/oLmc+CBXmH6fbNk4= +github.com/docker/docker v27.2.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6 h1:6VSn3hB5U5GeA6kQw4TwWIWbOhtvR2hmbBJnTOtqTWc= github.com/fvbock/endless v0.0.0-20170109170031-447134032cb6/go.mod h1:YxOVT5+yHzKvwhsiSIWmbAYM3Dr9AEEbER2dVayfBkg= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= @@ -24,42 +35,73 @@ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +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-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4= -github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= 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/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-migrate/migrate/v4 v4.18.2 h1:2VSCMz7x7mjyTXx3m2zPokOY82LTRgxK1yQYKo6wWQ8= +github.com/golang-migrate/migrate/v4 v4.18.2/go.mod h1:2CM6tJvn2kqPXwnXO/d3rAQYiyoIm180VsO8PRX6Rpk= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 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/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= +github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +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-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/pelletier/go-toml/v2 v2.2.1 h1:9TA9+T8+8CUCO2+WYnDLCgrYi9+omqKXyjDtosvtEhg= github.com/pelletier/go-toml/v2 v2.2.1/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -76,28 +118,36 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0 h1:TT4fX+nBOA/+LUkobKGW1ydGcn+G3vRw9+g5HwCphpk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.54.0/go.mod h1:L7UH0GbB0p47T4Rri3uHjbpCFYrVrwc1I25QhNPiGK8= +go.opentelemetry.io/otel v1.29.0 h1:PdomN/Al4q/lN6iBJEN3AwPvUiHPMlt93c8bqTG5Llw= +go.opentelemetry.io/otel v1.29.0/go.mod h1:N/WtXPs1CNCUEx+Agz5uouwCba+i+bJGFicT8SR4NP8= +go.opentelemetry.io/otel/metric v1.29.0 h1:vPf/HFWTNkPu1aYeIsc98l4ktOQaL6LeSoeV2g+8YLc= +go.opentelemetry.io/otel/metric v1.29.0/go.mod h1:auu/QWieFVWx+DmQOUMgj0F8LHWdgalxXqvp7BII/W8= +go.opentelemetry.io/otel/trace v1.29.0 h1:J/8ZNK4XgR7a21DZUAsbF8pZ5Jcw1VhACmnYt39JTi4= +go.opentelemetry.io/otel/trace v1.29.0/go.mod h1:eHl3w0sp3paPkYstJOmAimxhiFXPg+MMTlEh3nsQgWQ= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc= golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= -golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= -golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= -golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -google.golang.org/protobuf v1.34.0 h1:Qo/qEd2RZPCf2nKuorzksSknv0d3ERwp1vFG38gSmH4= -google.golang.org/protobuf v1.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 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= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From f8c45b279a332fd43d223022348f13e5d725a672 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Santos?= Date: Fri, 31 Jan 2025 09:30:01 -0300 Subject: [PATCH 05/27] feat: postgresDB and dotenv --- .env.example | 1 + .gitignore | 3 ++- cmd/api/main.go | 10 ++++++++-- go.mod | 4 ++++ go.sum | 8 ++++++++ pkg/database/postgres_db.go | 38 +++++++++++++++++++++++++++++++++++++ 6 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 .env.example create mode 100644 pkg/database/postgres_db.go diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..9651b3a --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +DATABASE_URL="postgres://gouser:123456@localhost:5432/gotwitterclone?sslmode=disable" \ No newline at end of file diff --git a/.gitignore b/.gitignore index 7a47eee..a84216c 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -bin/api \ No newline at end of file +bin/api +/.env diff --git a/cmd/api/main.go b/cmd/api/main.go index 341ee29..5e70e80 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -9,19 +9,22 @@ import ( "github.com/golang-migrate/migrate/v4" _ "github.com/golang-migrate/migrate/v4/database/postgres" _ "github.com/golang-migrate/migrate/v4/source/file" + "github.com/joho/godotenv" "log" + "os" "time" ) var ( db *database.InMemoryDB + pgDB *database.PostgresDB userRepo output.UserPort postRepo output.PostPort ) func init() { - - m, err := migrate.New("file://migrations", "postgres://gouser:123456@localhost:5432/gotwitterclone?sslmode=disable") + err := godotenv.Load() + m, err := migrate.New("file://migrations", os.Getenv("DATABASE_URL")) if err != nil { log.Fatal("Error on load migrations:", err) } @@ -33,6 +36,8 @@ func init() { log.Println("Migrations applied") db = database.NewInMemoryDB() + pgDB = database.NewPostgresDB() + pgDB.Version() if db == nil { log.Fatal("database is nil") } @@ -75,6 +80,7 @@ func (s *APIServer) Run() error { func main() { log.Printf("Starting Application") server := NewAPIServer("8001") + defer pgDB.Close() if err := server.Run(); err != nil { log.Fatal("Error starting server:", err) diff --git a/go.mod b/go.mod index 7d6da6e..1d1f69b 100644 --- a/go.mod +++ b/go.mod @@ -25,6 +25,10 @@ require ( github.com/goccy/go-json v0.10.2 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.7.2 // indirect + github.com/joho/godotenv v1.5.1 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/leodido/go-urn v1.4.0 // indirect diff --git a/go.sum b/go.sum index 46578e5..1d71105 100644 --- a/go.sum +++ b/go.sum @@ -63,6 +63,14 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI= +github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= diff --git a/pkg/database/postgres_db.go b/pkg/database/postgres_db.go new file mode 100644 index 0000000..2a04559 --- /dev/null +++ b/pkg/database/postgres_db.go @@ -0,0 +1,38 @@ +package database + +import ( + "context" + "fmt" + "github.com/jackc/pgx/v5" + "log" + "os" +) + +type PostgresDB struct { + conn *pgx.Conn +} + +func NewPostgresDB() *PostgresDB { + conn, err := pgx.Connect(context.Background(), os.Getenv("DATABASE_URL")) + if err != nil { + log.Fatal("Error on connect database", err) + } + + return &PostgresDB{ + conn: conn, + } +} + +func (db *PostgresDB) Close() { + db.conn.Close(context.Background()) +} + +func (db *PostgresDB) Version() { + var version string + err := db.conn.QueryRow(context.Background(), "SELECT version()").Scan(&version) + defer db.conn.Close(context.Background()) + if err != nil { + log.Fatal("Erro ao executar query:", err) + } + fmt.Println("PostgreSQL version:", version) +} From 24827a64b442bda9134024f3cff46bc4dcc0d297 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Santos?= Date: Fri, 31 Jan 2025 15:53:03 -0300 Subject: [PATCH 06/27] feat: change user repo to postgres database * update user_port interface * seeds ** command for it * initialize connection * initialize user postgres repo --- adapter/output/repository/postgres/schema.go | 10 ++++ .../repository/postgres/user_postgres_repo.go | 40 +++++++++++++++ cmd/api/main.go | 42 +++++++++------- cmd/seeds/main.go | 7 +++ internal/core/port/output/user_port.go | 9 ++-- pkg/database/postgres_db.go | 26 ++++++++-- pkg/seeders/postgres_seed.go | 50 +++++++++++++++++++ pkg/seeders/seeds.go | 25 ++++++++++ 8 files changed, 183 insertions(+), 26 deletions(-) create mode 100644 adapter/output/repository/postgres/schema.go create mode 100644 adapter/output/repository/postgres/user_postgres_repo.go create mode 100644 cmd/seeds/main.go create mode 100644 pkg/seeders/postgres_seed.go create mode 100644 pkg/seeders/seeds.go diff --git a/adapter/output/repository/postgres/schema.go b/adapter/output/repository/postgres/schema.go new file mode 100644 index 0000000..7e9ec17 --- /dev/null +++ b/adapter/output/repository/postgres/schema.go @@ -0,0 +1,10 @@ +package postgres + +import "time" + +type UserSchema struct { + ID string + Username string + CreatedAt time.Time + UpdatedAt time.Time +} diff --git a/adapter/output/repository/postgres/user_postgres_repo.go b/adapter/output/repository/postgres/user_postgres_repo.go new file mode 100644 index 0000000..97d7adc --- /dev/null +++ b/adapter/output/repository/postgres/user_postgres_repo.go @@ -0,0 +1,40 @@ +package postgres + +import ( + "context" + "errors" + "github.com/dexfs/go-twitter-clone/internal/core/domain" + "github.com/dexfs/go-twitter-clone/pkg/database" +) + +type PostgresUserRepository struct { + db *database.PostgresDB +} + +func NewPostgresUserRepository(db *database.PostgresDB) *PostgresUserRepository { + return &PostgresUserRepository{ + db: db, + } +} + +func (r *PostgresUserRepository) ByUsername(ctx context.Context, username string) (*domain.User, error) { + query := "SELECT * FROM users WHERE username = $1" + row := r.db.FindOne(ctx, query, username) + + var user domain.User + if err := row.Scan(&user.ID, &user.Username, &user.CreatedAt, &user.UpdatedAt); err != nil { + return nil, errors.New("user not found") + } + return &user, nil +} + +func (r *PostgresUserRepository) FindByID(ctx context.Context, id string) (*domain.User, error) { + query := "SELECT * FROM users WHERE id = $1" + row := r.db.FindOne(ctx, query, id) + + var user domain.User + if err := row.Scan(&user.ID, &user.Username, &user.CreatedAt, &user.UpdatedAt); err != nil { + return nil, errors.New("user not found") + } + return &user, nil +} diff --git a/cmd/api/main.go b/cmd/api/main.go index 5e70e80..75f1e63 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -1,9 +1,11 @@ package main import ( + "context" "github.com/dexfs/go-twitter-clone/adapter/input/routes" "github.com/dexfs/go-twitter-clone/adapter/output/repository/inmemory" inmemory_schema "github.com/dexfs/go-twitter-clone/adapter/output/repository/inmemory/schema" + "github.com/dexfs/go-twitter-clone/adapter/output/repository/postgres" "github.com/dexfs/go-twitter-clone/internal/core/port/output" "github.com/dexfs/go-twitter-clone/pkg/database" "github.com/golang-migrate/migrate/v4" @@ -12,7 +14,6 @@ import ( "github.com/joho/godotenv" "log" "os" - "time" ) var ( @@ -23,7 +24,11 @@ var ( ) func init() { + ctx := context.Background() err := godotenv.Load() + if err != nil { + log.Fatal("Error loading .env file") + } m, err := migrate.New("file://migrations", os.Getenv("DATABASE_URL")) if err != nil { log.Fatal("Error on load migrations:", err) @@ -37,28 +42,29 @@ func init() { db = database.NewInMemoryDB() pgDB = database.NewPostgresDB() - pgDB.Version() + pgDB.Version(ctx) if db == nil { log.Fatal("database is nil") } - initialUsers := make([]*inmemory_schema.UserSchema, 0) - initialUsers = append(initialUsers, &inmemory_schema.UserSchema{ - ID: "4cfe67a9-defc-42b9-8410-cb5086bec2f5", - Username: "alucard", - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - }) - initialUsers = append(initialUsers, &inmemory_schema.UserSchema{ - ID: "b8903f77-5d16-4176-890f-f597594ff952", - Username: "alexander", - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - }) + //initialUsers := make([]*inmemory_schema.UserSchema, 0) + //initialUsers = append(initialUsers, &inmemory_schema.UserSchema{ + // ID: "4cfe67a9-defc-42b9-8410-cb5086bec2f5", + // Username: "alucard", + // CreatedAt: time.Now(), + // UpdatedAt: time.Now(), + //}) + //initialUsers = append(initialUsers, &inmemory_schema.UserSchema{ + // ID: "b8903f77-5d16-4176-890f-f597594ff952", + // Username: "alexander", + // CreatedAt: time.Now(), + // UpdatedAt: time.Now(), + //}) - db.RegisterSchema(inmemory.USER_SCHEMA_NAME, initialUsers) + //db.RegisterSchema(inmemory.USER_SCHEMA_NAME, initialUsers) db.RegisterSchema(inmemory.POST_SCHEMA_NAME, []*inmemory_schema.PostSchema{}) - userRepo = inmemory.NewInMemoryUserRepository(db) + //userRepo = inmemory.NewInMemoryUserRepository(db) + userRepo = postgres.NewPostgresUserRepository(pgDB) postRepo = inmemory.NewInMemoryPostRepository(db) } @@ -80,7 +86,7 @@ func (s *APIServer) Run() error { func main() { log.Printf("Starting Application") server := NewAPIServer("8001") - defer pgDB.Close() + defer pgDB.Close(context.Background()) if err := server.Run(); err != nil { log.Fatal("Error starting server:", err) diff --git a/cmd/seeds/main.go b/cmd/seeds/main.go new file mode 100644 index 0000000..724a22d --- /dev/null +++ b/cmd/seeds/main.go @@ -0,0 +1,7 @@ +package main + +import "github.com/dexfs/go-twitter-clone/pkg/seeders" + +func main() { + seeders.Run() +} diff --git a/internal/core/port/output/user_port.go b/internal/core/port/output/user_port.go index 032ba54..a13119f 100644 --- a/internal/core/port/output/user_port.go +++ b/internal/core/port/output/user_port.go @@ -1,8 +1,11 @@ package output -import "github.com/dexfs/go-twitter-clone/internal/core/domain" +import ( + "context" + "github.com/dexfs/go-twitter-clone/internal/core/domain" +) type UserPort interface { - ByUsername(username string) (*domain.User, error) - FindByID(id string) (*domain.User, error) + ByUsername(ctx context.Context, username string) (*domain.User, error) + FindByID(ctx context.Context, id string) (*domain.User, error) } diff --git a/pkg/database/postgres_db.go b/pkg/database/postgres_db.go index 2a04559..692998f 100644 --- a/pkg/database/postgres_db.go +++ b/pkg/database/postgres_db.go @@ -2,6 +2,7 @@ package database import ( "context" + "errors" "fmt" "github.com/jackc/pgx/v5" "log" @@ -23,14 +24,29 @@ func NewPostgresDB() *PostgresDB { } } -func (db *PostgresDB) Close() { - db.conn.Close(context.Background()) +func (db *PostgresDB) FindOne(ctx context.Context, query string, args ...any) pgx.Row { + return db.conn.QueryRow(ctx, query, args...) } -func (db *PostgresDB) Version() { +func (db *PostgresDB) Batch(ctx context.Context, batch *pgx.Batch, dataSize int) error { + br := db.conn.SendBatch(ctx, batch) + defer br.Close() + for range dataSize { + _, err := br.Exec() + if err != nil { + return errors.New("User seed failed: " + err.Error()) + } + } + return nil +} + +func (db *PostgresDB) Close(ctx context.Context) { + db.conn.Close(ctx) +} + +func (db *PostgresDB) Version(ctx context.Context) { var version string - err := db.conn.QueryRow(context.Background(), "SELECT version()").Scan(&version) - defer db.conn.Close(context.Background()) + err := db.conn.QueryRow(ctx, "SELECT version()").Scan(&version) if err != nil { log.Fatal("Erro ao executar query:", err) } diff --git a/pkg/seeders/postgres_seed.go b/pkg/seeders/postgres_seed.go new file mode 100644 index 0000000..8200dd5 --- /dev/null +++ b/pkg/seeders/postgres_seed.go @@ -0,0 +1,50 @@ +package seeders + +import ( + "context" + "github.com/dexfs/go-twitter-clone/adapter/output/repository/postgres" + "github.com/dexfs/go-twitter-clone/pkg/database" + "github.com/jackc/pgx/v5" + "time" +) + +type PostgresSeeder struct { + db *database.PostgresDB +} + +func NewPostgresSeed(db *database.PostgresDB) *PostgresSeeder { + return &PostgresSeeder{db: db} +} + +func (s *PostgresSeeder) UsersSeed(ctx context.Context) error { + batch := &pgx.Batch{} + initialUsers := make([]*postgres.UserSchema, 0) + initialUsers = append(initialUsers, &postgres.UserSchema{ + ID: "0194bd04-66e2-7cd8-b3d9-66eda709f2ee", + Username: "alucard", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }) + + initialUsers = append(initialUsers, &postgres.UserSchema{ + ID: "0194bd04-8eac-7e70-97cd-c526cdda3d6a", + Username: "alexander", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }) + + initialUsers = append(initialUsers, &postgres.UserSchema{ + ID: "0194bdb1-0588-7181-809e-a825badac714", + Username: "seras_victoria", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + }) + + batch.Queue("DELETE FROM users") + for _, u := range initialUsers { + batch.Queue(`INSERT INTO users (id, username, created_at, updated_at) VALUES ($1, $2, $3, $4)`, + u.ID, u.Username, u.CreatedAt, u.UpdatedAt) + } + + return s.db.Batch(ctx, batch, len(initialUsers)) +} diff --git a/pkg/seeders/seeds.go b/pkg/seeders/seeds.go new file mode 100644 index 0000000..93066a7 --- /dev/null +++ b/pkg/seeders/seeds.go @@ -0,0 +1,25 @@ +package seeders + +import ( + "context" + "github.com/dexfs/go-twitter-clone/pkg/database" + "github.com/joho/godotenv" + "log" + "time" +) + +func Run() { + ctx := context.Background() + err := godotenv.Load() + pgDB := database.NewPostgresDB() + defer pgDB.Close(ctx) + pgSeeder := NewPostgresSeed(pgDB) + ctxWithCancel, cancel := context.WithTimeout(ctx, 20*time.Second) + defer cancel() + + err = pgSeeder.UsersSeed(ctxWithCancel) + if err != nil { + log.Fatal("Error on seeding users: ", err) + } + log.Println("Users seeder applied") +} From 4935d57f77bdc094411c7a690bbf458554e09fe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Santos?= Date: Fri, 31 Jan 2025 15:53:49 -0300 Subject: [PATCH 07/27] refactor: update call for support interface changes --- adapter/output/repository/inmemory/user_inmemory_repo.go | 5 +++-- internal/core/usecase/create_quote_usecase.go | 4 +++- internal/core/usecase/createpost_usecase.go | 4 +++- internal/core/usecase/createrepost_usecase.go | 4 +++- internal/core/usecase/getuserfeed_usecase.go | 4 +++- internal/core/usecase/getuserinfo_usecase.go | 4 +++- 6 files changed, 18 insertions(+), 7 deletions(-) diff --git a/adapter/output/repository/inmemory/user_inmemory_repo.go b/adapter/output/repository/inmemory/user_inmemory_repo.go index ecd0c60..9e0fc50 100644 --- a/adapter/output/repository/inmemory/user_inmemory_repo.go +++ b/adapter/output/repository/inmemory/user_inmemory_repo.go @@ -1,6 +1,7 @@ package inmemory import ( + "context" "errors" inmemory_schema "github.com/dexfs/go-twitter-clone/adapter/output/repository/inmemory/schema" "github.com/dexfs/go-twitter-clone/internal/core/domain" @@ -19,7 +20,7 @@ func NewInMemoryUserRepository(db *database.InMemoryDB) *inMemoryUserRepository } } -func (r *inMemoryUserRepository) ByUsername(username string) (*domain.User, error) { +func (r *inMemoryUserRepository) ByUsername(ctx context.Context, username string) (*domain.User, error) { for _, currentUser := range r.getAll() { if currentUser.Username == username { return &domain.User{ @@ -34,7 +35,7 @@ func (r *inMemoryUserRepository) ByUsername(username string) (*domain.User, erro return nil, errors.New("user not found") } -func (r *inMemoryUserRepository) FindByID(id string) (*domain.User, error) { +func (r *inMemoryUserRepository) FindByID(ctx context.Context, id string) (*domain.User, error) { for _, currentUser := range r.getAll() { if currentUser.ID == id { return &domain.User{ diff --git a/internal/core/usecase/create_quote_usecase.go b/internal/core/usecase/create_quote_usecase.go index ed2fcf9..21edcc4 100644 --- a/internal/core/usecase/create_quote_usecase.go +++ b/internal/core/usecase/create_quote_usecase.go @@ -1,6 +1,7 @@ package usecase import ( + "context" "github.com/dexfs/go-twitter-clone/adapter/input/model/rest_errors" "github.com/dexfs/go-twitter-clone/internal/core/domain" "github.com/dexfs/go-twitter-clone/internal/core/port/input" @@ -24,7 +25,8 @@ func NewCreateQuoteUseCase(postPort output.PostPort, userPort output.UserPort) ( } func (uc *createQuoteUseCase) Execute(anInput input.CreateQuoteUseCaseInput) (*domain.Post, *rest_errors.RestError) { - user, err := uc.userPort.FindByID(anInput.UserID) + ctx := context.Background() + user, err := uc.userPort.FindByID(ctx, anInput.UserID) if err != nil { return &domain.Post{}, rest_errors.NewBadRequestError(err.Error()) } diff --git a/internal/core/usecase/createpost_usecase.go b/internal/core/usecase/createpost_usecase.go index e8829d4..0d1a9f2 100644 --- a/internal/core/usecase/createpost_usecase.go +++ b/internal/core/usecase/createpost_usecase.go @@ -1,6 +1,7 @@ package usecase import ( + "context" "github.com/dexfs/go-twitter-clone/adapter/input/model/rest_errors" "github.com/dexfs/go-twitter-clone/internal/core/domain" "github.com/dexfs/go-twitter-clone/internal/core/port/input" @@ -21,12 +22,13 @@ func NewCreatePostUseCase(postPort output.PostPort, userPort output.UserPort) (* } func (uc *createPostUseCase) Execute(aInput input.CreatePostUseCaseInput) (*domain.Post, *rest_errors.RestError) { + ctx := context.Background() hasReachedLimit := uc.postPort.HasReachedPostingLimitDay(aInput.UserID, 5) // @TODO mudar isso para vir das configurações if hasReachedLimit { return &domain.Post{}, rest_errors.NewBadRequestError("you reached your posts day limit") } - user, err := uc.userPort.FindByID(aInput.UserID) + user, err := uc.userPort.FindByID(ctx, aInput.UserID) if err != nil { return &domain.Post{}, rest_errors.NewNotFoundError(err.Error()) } diff --git a/internal/core/usecase/createrepost_usecase.go b/internal/core/usecase/createrepost_usecase.go index aed4655..a516b45 100644 --- a/internal/core/usecase/createrepost_usecase.go +++ b/internal/core/usecase/createrepost_usecase.go @@ -1,6 +1,7 @@ package usecase import ( + "context" "github.com/dexfs/go-twitter-clone/adapter/input/model/rest_errors" "github.com/dexfs/go-twitter-clone/internal/core/domain" "github.com/dexfs/go-twitter-clone/internal/core/port/input" @@ -21,7 +22,8 @@ func NewCreateRepostUseCase(postPort output.PostPort, userPort output.UserPort) } func (uc *createRepostUseCase) Execute(aInput input.CreateRepostUseCaseInput) (*domain.Post, *rest_errors.RestError) { - user, err := uc.userPort.FindByID(aInput.UserID) + ctx := context.Background() + user, err := uc.userPort.FindByID(ctx, aInput.UserID) if err != nil { return &domain.Post{}, rest_errors.NewBadRequestError(err.Error()) diff --git a/internal/core/usecase/getuserfeed_usecase.go b/internal/core/usecase/getuserfeed_usecase.go index efbafaa..b6605bf 100644 --- a/internal/core/usecase/getuserfeed_usecase.go +++ b/internal/core/usecase/getuserfeed_usecase.go @@ -1,6 +1,7 @@ package usecase import ( + "context" "github.com/dexfs/go-twitter-clone/adapter/input/model/rest_errors" "github.com/dexfs/go-twitter-clone/internal/core/domain" "github.com/dexfs/go-twitter-clone/internal/core/port/output" @@ -23,7 +24,8 @@ func NewGetUserFeedUseCase(userPort output.UserPort, postPort output.PostPort) ( } func (uc *getUserFeedUseCase) Execute(username string) ([]*domain.Post, *rest_errors.RestError) { - user, err := uc.userPort.ByUsername(username) + ctx := context.Background() + user, err := uc.userPort.ByUsername(ctx, username) if err != nil { return []*domain.Post{}, rest_errors.NewNotFoundError(err.Error()) } diff --git a/internal/core/usecase/getuserinfo_usecase.go b/internal/core/usecase/getuserinfo_usecase.go index ea8f27b..ebaa757 100644 --- a/internal/core/usecase/getuserinfo_usecase.go +++ b/internal/core/usecase/getuserinfo_usecase.go @@ -1,6 +1,7 @@ package usecase import ( + "context" "fmt" "github.com/dexfs/go-twitter-clone/adapter/input/model/rest_errors" "github.com/dexfs/go-twitter-clone/internal/core/domain" @@ -21,8 +22,9 @@ func NewGetUserInfoUseCase(userPort output.UserPort) (*getUserInfoUseCase, *rest } func (s *getUserInfoUseCase) Execute(username string) (*domain.User, *rest_errors.RestError) { + ctx := context.Background() fmt.Sprintf("GetUserInfoService_Execute(%s)", username) - userInfoResponse, err := s.userPort.ByUsername(username) + userInfoResponse, err := s.userPort.ByUsername(ctx, username) if err != nil { return nil, rest_errors.NewNotFoundError(err.Error()) } From c9fde827094fcbbdb42f849263999886bb038954 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Santos?= Date: Fri, 31 Jan 2025 15:54:01 -0300 Subject: [PATCH 08/27] chore: create seed command --- Makefile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Makefile b/Makefile index a079ffa..a1a4724 100644 --- a/Makefile +++ b/Makefile @@ -6,3 +6,6 @@ build: start: build ./bin/api + +seed: + @go run cmd/seeds/main.go \ No newline at end of file From 61bf48bf0f76adf6d8a2cdb4df8781d499c11d17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Santos?= Date: Fri, 31 Jan 2025 15:54:11 -0300 Subject: [PATCH 09/27] chore: update README --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8738be2..3f10859 100644 --- a/README.md +++ b/README.md @@ -23,16 +23,16 @@ https://dbdiagram.io/d/go-twitter-clone-660b4e8437b7e33fd741027f // users data const users = [ { - id: '4cfe67a9-defc-42b9-8410-cb5086bec2f5', + id: '0194bd04-66e2-7cd8-b3d9-66eda709f2ee', username: 'alucard', }, { - id: 'b8903f77-5d16-4176-890f-f597594ff952', + id: '0194bd04-8eac-7e70-97cd-c526cdda3d6a', username: 'alexander', }, { - id: '75135a97-46be-405f-8948-0821290ca83e', + id: '0194bdb1-0588-7181-809e-a825badac714', username: 'seras_victoria', }, ]; @@ -54,6 +54,6 @@ ___ -## Styleguide +## Styleguide -[uber go style guide](https://github.com/alcir-junior-caju/uber-go-style-guide-pt-br) +[uber go style guide](https://github.com/alcir-junior-caju/uber-go-style-guide-pt-br) \ No newline at end of file From da01a229fb43e75667d7580ec2436e40cf164db8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Santos?= Date: Fri, 31 Jan 2025 16:05:52 -0300 Subject: [PATCH 10/27] chore: ignore test * because there is a new database dependency, it will be resolved with integration test --- cmd/api/main_test.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/cmd/api/main_test.go b/cmd/api/main_test.go index 9e0bd26..bb95d26 100644 --- a/cmd/api/main_test.go +++ b/cmd/api/main_test.go @@ -1,3 +1,6 @@ +//go:build ignore +// +build ignore + package main import ( From f8af254d6f7f674199ddaef836368f8c9271331b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Santos?= Date: Fri, 31 Jan 2025 16:09:36 -0300 Subject: [PATCH 11/27] chore: temp changes for coverage --- .testcoverage.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.testcoverage.yml b/.testcoverage.yml index d7a4d94..e9cf5fb 100644 --- a/.testcoverage.yml +++ b/.testcoverage.yml @@ -15,15 +15,15 @@ local-prefix: "github.com/org/project" threshold: # (optional; default 0) # The minimum coverage that each file should have - file: 60 + file: 5 # (optional; default 0) # The minimum coverage that each package should have - package: 70 + package: 5 # (optional; default 0) # The minimum total coverage project should have - total: 70 + total: 5 # Holds regexp rules which will override thresholds for matched files or packages # using their paths. From ae4ceb01fc77fe2a2f5491a8421f904fc2939896 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Santos?= Date: Fri, 31 Jan 2025 16:11:36 -0300 Subject: [PATCH 12/27] chore: no coverage step for now --- .../{test-coverage-action.yml => test-coverage-action.__yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/workflows/{test-coverage-action.yml => test-coverage-action.__yml} (100%) diff --git a/.github/workflows/test-coverage-action.yml b/.github/workflows/test-coverage-action.__yml similarity index 100% rename from .github/workflows/test-coverage-action.yml rename to .github/workflows/test-coverage-action.__yml From 387e48d8db42c2a07bfeabf913a8fa5a3c071941 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Santos?= Date: Fri, 31 Jan 2025 17:14:37 -0300 Subject: [PATCH 13/27] refactor: change user resources to support context --- adapter/input/adapter_http/users_controller.go | 4 ++-- adapter/input/model/request/post_create_request.go | 2 +- adapter/input/model/request/user_request.go | 2 +- internal/core/port/input/getinfo_usecase.go | 3 ++- internal/core/port/input/getuserfeed_usecase.go | 3 ++- internal/core/usecase/getuserfeed_usecase.go | 3 +-- internal/core/usecase/getuserinfo_usecase.go | 3 +-- 7 files changed, 10 insertions(+), 10 deletions(-) diff --git a/adapter/input/adapter_http/users_controller.go b/adapter/input/adapter_http/users_controller.go index 407d1af..48aa405 100644 --- a/adapter/input/adapter_http/users_controller.go +++ b/adapter/input/adapter_http/users_controller.go @@ -31,7 +31,7 @@ func (uc *usersController) GetInfo(c *gin.Context) { c.JSON(errRest.Code, errRest) return } - userFeedDomain, err := uc.getUserInfoUseCase.Execute(userInfoRequest.Username) + userFeedDomain, err := uc.getUserInfoUseCase.Execute(c, userInfoRequest.Username) if err != nil { c.JSON(err.Code, err) return @@ -53,7 +53,7 @@ func (uc *usersController) GetFeed(c *gin.Context) { return } - userFeedDomain, err := uc.getUserFeedUseCase.Execute(userRequest.Username) + userFeedDomain, err := uc.getUserFeedUseCase.Execute(c, userRequest.Username) if err != nil { c.JSON(err.Code, err) return diff --git a/adapter/input/model/request/post_create_request.go b/adapter/input/model/request/post_create_request.go index 6c71e29..8fa1a0e 100644 --- a/adapter/input/model/request/post_create_request.go +++ b/adapter/input/model/request/post_create_request.go @@ -1,6 +1,6 @@ package request type CreatePostRequest struct { - UserID string `json:"user_id" binding:"required,uuid4"` + UserID string `json:"user_id" binding:"required,ulid"` Content string `json:"content" binding:"required,min=10,max=255"` } diff --git a/adapter/input/model/request/user_request.go b/adapter/input/model/request/user_request.go index b9ae3db..3e88f33 100644 --- a/adapter/input/model/request/user_request.go +++ b/adapter/input/model/request/user_request.go @@ -1,7 +1,7 @@ package request type UserInfoRequest struct { - Username string `uri:"username" binding:"required,min=5,max=10,alphanum,lowercase"` + Username string `uri:"username" binding:"required,min=5,max=14,alphanum,lowercase"` } type UserFeedRequest struct { diff --git a/internal/core/port/input/getinfo_usecase.go b/internal/core/port/input/getinfo_usecase.go index 9608941..387906b 100644 --- a/internal/core/port/input/getinfo_usecase.go +++ b/internal/core/port/input/getinfo_usecase.go @@ -1,10 +1,11 @@ package input import ( + "context" "github.com/dexfs/go-twitter-clone/adapter/input/model/rest_errors" "github.com/dexfs/go-twitter-clone/internal/core/domain" ) type GetUserInfoUseCase interface { - Execute(username string) (*domain.User, *rest_errors.RestError) + Execute(ctx context.Context, username string) (*domain.User, *rest_errors.RestError) } diff --git a/internal/core/port/input/getuserfeed_usecase.go b/internal/core/port/input/getuserfeed_usecase.go index a3ead52..b2db3bd 100644 --- a/internal/core/port/input/getuserfeed_usecase.go +++ b/internal/core/port/input/getuserfeed_usecase.go @@ -1,10 +1,11 @@ package input import ( + "context" "github.com/dexfs/go-twitter-clone/adapter/input/model/rest_errors" "github.com/dexfs/go-twitter-clone/internal/core/domain" ) type GetUserFeedUseCase interface { - Execute(username string) ([]*domain.Post, *rest_errors.RestError) + Execute(ctx context.Context, username string) ([]*domain.Post, *rest_errors.RestError) } diff --git a/internal/core/usecase/getuserfeed_usecase.go b/internal/core/usecase/getuserfeed_usecase.go index b6605bf..ae99ba3 100644 --- a/internal/core/usecase/getuserfeed_usecase.go +++ b/internal/core/usecase/getuserfeed_usecase.go @@ -23,8 +23,7 @@ func NewGetUserFeedUseCase(userPort output.UserPort, postPort output.PostPort) ( }, nil } -func (uc *getUserFeedUseCase) Execute(username string) ([]*domain.Post, *rest_errors.RestError) { - ctx := context.Background() +func (uc *getUserFeedUseCase) Execute(ctx context.Context, username string) ([]*domain.Post, *rest_errors.RestError) { user, err := uc.userPort.ByUsername(ctx, username) if err != nil { return []*domain.Post{}, rest_errors.NewNotFoundError(err.Error()) diff --git a/internal/core/usecase/getuserinfo_usecase.go b/internal/core/usecase/getuserinfo_usecase.go index ebaa757..522adf8 100644 --- a/internal/core/usecase/getuserinfo_usecase.go +++ b/internal/core/usecase/getuserinfo_usecase.go @@ -21,8 +21,7 @@ func NewGetUserInfoUseCase(userPort output.UserPort) (*getUserInfoUseCase, *rest }, nil } -func (s *getUserInfoUseCase) Execute(username string) (*domain.User, *rest_errors.RestError) { - ctx := context.Background() +func (s *getUserInfoUseCase) Execute(ctx context.Context, username string) (*domain.User, *rest_errors.RestError) { fmt.Sprintf("GetUserInfoService_Execute(%s)", username) userInfoResponse, err := s.userPort.ByUsername(ctx, username) if err != nil { From 97596f4e2e832e918a36f303972f3fe4da1b277a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Santos?= Date: Fri, 31 Jan 2025 17:14:50 -0300 Subject: [PATCH 14/27] chore: update seeds --- pkg/seeders/postgres_seed.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/seeders/postgres_seed.go b/pkg/seeders/postgres_seed.go index 8200dd5..c46952d 100644 --- a/pkg/seeders/postgres_seed.go +++ b/pkg/seeders/postgres_seed.go @@ -20,21 +20,21 @@ func (s *PostgresSeeder) UsersSeed(ctx context.Context) error { batch := &pgx.Batch{} initialUsers := make([]*postgres.UserSchema, 0) initialUsers = append(initialUsers, &postgres.UserSchema{ - ID: "0194bd04-66e2-7cd8-b3d9-66eda709f2ee", + ID: "01JJYY0V9AMD9656HT4BSV0ZEK", Username: "alucard", CreatedAt: time.Now(), UpdatedAt: time.Now(), }) initialUsers = append(initialUsers, &postgres.UserSchema{ - ID: "0194bd04-8eac-7e70-97cd-c526cdda3d6a", + ID: "01JJYY1S0JY0ERC1VQ3EEFNJC7", Username: "alexander", CreatedAt: time.Now(), UpdatedAt: time.Now(), }) initialUsers = append(initialUsers, &postgres.UserSchema{ - ID: "0194bdb1-0588-7181-809e-a825badac714", + ID: "01JJYY1Z0E3BMZQ0HFDH8A6NMT", Username: "seras_victoria", CreatedAt: time.Now(), UpdatedAt: time.Now(), From 0be75e9e107558322e24b3bd14fea67e12b02bc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Santos?= Date: Sun, 2 Feb 2025 16:55:02 -0300 Subject: [PATCH 15/27] refactor: pass context to dependencies --- adapter/input/adapter_http/posts_controller.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/adapter/input/adapter_http/posts_controller.go b/adapter/input/adapter_http/posts_controller.go index 6aa152d..23b2cc1 100644 --- a/adapter/input/adapter_http/posts_controller.go +++ b/adapter/input/adapter_http/posts_controller.go @@ -32,7 +32,7 @@ func (pc *postsController) CreatePost(c *gin.Context) { return } - postDomain, err := pc.createPostUseCase.Execute(input.CreatePostUseCaseInput{ + postDomain, err := pc.createPostUseCase.Execute(c, input.CreatePostUseCaseInput{ UserID: createPostRequest.UserID, Content: createPostRequest.Content, }) @@ -56,7 +56,7 @@ func (pc *postsController) CreateRepost(c *gin.Context) { return } - postDomain, err := pc.createRepostUseCase.Execute(input.CreateRepostUseCaseInput{ + postDomain, err := pc.createRepostUseCase.Execute(c, input.CreateRepostUseCaseInput{ PostID: createRequest.PostID, UserID: createRequest.UserID, }) @@ -80,7 +80,7 @@ func (pc *postsController) CreateQuote(c *gin.Context) { return } - postDomain, err := pc.createQuoteUseCase.Execute(input.CreateQuoteUseCaseInput{ + postDomain, err := pc.createQuoteUseCase.Execute(c, input.CreateQuoteUseCaseInput{ PostID: createRequest.PostID, UserID: createRequest.UserID, Quote: createRequest.Quote, From 7cd1ecffad1346c1c1e1ccbbe6f12f4adad5ec6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Santos?= Date: Sun, 2 Feb 2025 16:55:25 -0300 Subject: [PATCH 16/27] refactor: change validation from uuid to ulid --- adapter/input/model/request/quote_request.go | 4 ++-- adapter/input/model/request/repost_request.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/adapter/input/model/request/quote_request.go b/adapter/input/model/request/quote_request.go index 2efd38b..067fd0c 100644 --- a/adapter/input/model/request/quote_request.go +++ b/adapter/input/model/request/quote_request.go @@ -1,7 +1,7 @@ package request type QuoteRequest struct { - UserID string `json:"user_id" binding:"required,uuid4"` - PostID string `json:"post_id" binding:"required,uuid4"` + UserID string `json:"user_id" binding:"required,ulid"` + PostID string `json:"post_id" binding:"required,ulid"` Quote string `json:"quote" binding:"required,min=10,max=100"` } diff --git a/adapter/input/model/request/repost_request.go b/adapter/input/model/request/repost_request.go index d92ed7d..7568ffb 100644 --- a/adapter/input/model/request/repost_request.go +++ b/adapter/input/model/request/repost_request.go @@ -1,6 +1,6 @@ package request type RepostRequest struct { - UserID string `json:"user_id" binding:"required,uuid4"` - PostID string `json:"post_id" binding:"required,uuid4"` + UserID string `json:"user_id" binding:"required,ulid"` + PostID string `json:"post_id" binding:"required,ulid"` } From 0941171c898f26aa2a669439d2eea5f820882124 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Santos?= Date: Sun, 2 Feb 2025 16:55:50 -0300 Subject: [PATCH 17/27] feat: postgres repository --- .../repository/postgres/post_postgres_repo.go | 184 ++++++++++++++++++ adapter/output/repository/postgres/schema.go | 50 ++++- .../repository/postgres/user_postgres_repo.go | 2 +- 3 files changed, 230 insertions(+), 6 deletions(-) create mode 100644 adapter/output/repository/postgres/post_postgres_repo.go diff --git a/adapter/output/repository/postgres/post_postgres_repo.go b/adapter/output/repository/postgres/post_postgres_repo.go new file mode 100644 index 0000000..46590ba --- /dev/null +++ b/adapter/output/repository/postgres/post_postgres_repo.go @@ -0,0 +1,184 @@ +package postgres + +import ( + "context" + "errors" + "fmt" + "github.com/dexfs/go-twitter-clone/internal/core/domain" + "github.com/dexfs/go-twitter-clone/pkg/database" + "github.com/jackc/pgx/v5" +) + +type postgresPostRepository struct { + db *database.PostgresDB +} + +func NewPostgresPostRepository(db *database.PostgresDB) *postgresPostRepository { + return &postgresPostRepository{ + db: db, + } +} + +func (r *postgresPostRepository) CreatePost(ctx context.Context, aPost *domain.Post) error { + query := ` + INSERT INTO posts ( + post_id, user_id, content, is_quote, is_repost, + original_post_id, original_post_content, original_post_user_id, original_post_screen_name, created_at + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) + ` + + return r.db.Insert(ctx, query, + aPost.ID, + aPost.User.ID, + aPost.Content, + aPost.IsQuote, + aPost.IsRepost, + aPost.OriginalPostID, + aPost.OriginalPostContent, + aPost.OriginalPostUserID, + aPost.OriginalPostScreenName, + aPost.CreatedAt, + ) +} + +func (r *postgresPostRepository) HasReachedPostingLimitDay(ctx context.Context, aUserId string, aLimit uint64) bool { + query := "select count(id) from posts where user_id = $1 AND DATE(created_at) = CURRENT_DATE" + row := r.db.FindOne(ctx, query, aUserId) + var count uint64 + + err := row.Scan(&count) + if err != nil { + return false + } + + reached := count >= aLimit + + if reached { + return true + } + + return false +} + +func (r *postgresPostRepository) HasPostBeenRepostedByUser(ctx context.Context, aPostID string, aUserID string) bool { + query := "select count(id) from posts where user_id = $1 AND original_post_id = $2 AND is_repost = true" + row := r.db.FindOne(ctx, query, aUserID, aPostID) + + var count uint64 + + err := row.Scan(&count) + if err != nil { + return false + } + + if count > 0 { + return true + } + + return false +} + +func (r *postgresPostRepository) AllByUserID(ctx context.Context, aUser *domain.User) []*domain.Post { + result := make([]*domain.Post, 0) + + query := ` + SELECT + posts.post_id, + posts.content, + posts.is_quote, + posts.is_repost, + posts.original_post_id, + posts.original_post_content, + posts.original_post_user_id, + posts.original_post_screen_name, + users.user_id, + users.username + FROM posts + INNER JOIN users ON posts.user_id = users.user_id + WHERE posts.user_id = $1` + + rows, err := r.db.Find(ctx, query, aUser.ID) + defer rows.Close() + + if err != nil { + return nil + } + + if !rows.Next() { + return nil + } + for rows.Next() { + var postSchema PostSchema + var userSchema UserSchema + if err := rows.Scan( + &postSchema.ID, + &postSchema.Content, + &postSchema.IsQuote, + &postSchema.IsRepost, + &postSchema.OriginalPostID, + &postSchema.OriginalPostContent, + &postSchema.OriginalPostUserID, + &postSchema.OriginalPostScreenName, + &userSchema.ID, + &userSchema.Username, + ); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil + } + return nil + } + user := userSchema.FromPersistence() + post := postSchema.FromPersistence(user) + result = append(result, post) + } + + return result +} + +func (r *postgresPostRepository) FindByID(ctx context.Context, aPostID string) (*domain.Post, error) { + query := ` + SELECT + posts.post_id, + posts.content, + posts.is_quote, + posts.is_repost, + posts.original_post_id, + posts.original_post_content, + posts.original_post_user_id, + posts.original_post_screen_name, + users.user_id, + users.username + FROM posts + INNER JOIN users ON posts.user_id = users.user_id + WHERE posts.post_id = $1` + + row := r.db.FindOne(ctx, query, aPostID) + + var postSchema PostSchema + var userSchema UserSchema + + if err := row.Scan( + &postSchema.ID, + &postSchema.Content, + &postSchema.IsQuote, + &postSchema.IsRepost, + &postSchema.OriginalPostID, + &postSchema.OriginalPostContent, + &postSchema.OriginalPostUserID, + &postSchema.OriginalPostScreenName, + &userSchema.ID, + &userSchema.Username, + ); err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return nil, errors.New("post not found") + } + return nil, fmt.Errorf("error scanning post: %w", err) + + } + + user := userSchema.FromPersistence() + post := postSchema.FromPersistence(user) + + return post, nil + +} diff --git a/adapter/output/repository/postgres/schema.go b/adapter/output/repository/postgres/schema.go index 7e9ec17..a32bef6 100644 --- a/adapter/output/repository/postgres/schema.go +++ b/adapter/output/repository/postgres/schema.go @@ -1,10 +1,50 @@ package postgres -import "time" +import ( + "github.com/dexfs/go-twitter-clone/internal/core/domain" + "time" +) type UserSchema struct { - ID string - Username string - CreatedAt time.Time - UpdatedAt time.Time + ID string `db:"user_id"` + Username string `db:"username"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` +} + +func (u UserSchema) FromPersistence() *domain.User { + return &domain.User{ + ID: u.ID, + Username: u.Username, + CreatedAt: u.CreatedAt, + UpdatedAt: u.UpdatedAt, + } +} + +type PostSchema struct { + ID string `db:"post_id"` + UserID string `db:"user_id"` + Content string `db:"content"` + CreatedAt time.Time `db:"created_at"` + IsQuote bool `db:"is_quote"` + IsRepost bool `db:"is_repost"` + OriginalPostID string `db:"original_post_id"` + OriginalPostContent string `db:"original_post_content"` + OriginalPostUserID string `db:"original_post_user_id"` + OriginalPostScreenName string `db:"original_post_screen_name"` +} + +func (p *PostSchema) FromPersistence(aUser *domain.User) *domain.Post { + return &domain.Post{ + ID: p.ID, + User: aUser, + Content: p.Content, + CreatedAt: p.CreatedAt, + IsQuote: p.IsQuote, + IsRepost: p.IsRepost, + OriginalPostID: p.OriginalPostID, + OriginalPostContent: p.OriginalPostContent, + OriginalPostUserID: p.OriginalPostUserID, + OriginalPostScreenName: p.OriginalPostScreenName, + } } diff --git a/adapter/output/repository/postgres/user_postgres_repo.go b/adapter/output/repository/postgres/user_postgres_repo.go index 97d7adc..2fbeb02 100644 --- a/adapter/output/repository/postgres/user_postgres_repo.go +++ b/adapter/output/repository/postgres/user_postgres_repo.go @@ -29,7 +29,7 @@ func (r *PostgresUserRepository) ByUsername(ctx context.Context, username string } func (r *PostgresUserRepository) FindByID(ctx context.Context, id string) (*domain.User, error) { - query := "SELECT * FROM users WHERE id = $1" + query := "SELECT * FROM users WHERE user_id = $1" row := r.db.FindOne(ctx, query, id) var user domain.User From 7bcb19a65d4625f5ddc59063012fac49cb16c4d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Santos?= Date: Sun, 2 Feb 2025 16:56:08 -0300 Subject: [PATCH 18/27] refactor: pass context to in memory repository --- .../output/repository/inmemory/post_inmemory_repo.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/adapter/output/repository/inmemory/post_inmemory_repo.go b/adapter/output/repository/inmemory/post_inmemory_repo.go index 8d92bbe..f74a651 100644 --- a/adapter/output/repository/inmemory/post_inmemory_repo.go +++ b/adapter/output/repository/inmemory/post_inmemory_repo.go @@ -1,6 +1,7 @@ package inmemory import ( + "context" "errors" "github.com/dexfs/go-twitter-clone/adapter/output/mappers" inmemory_schema "github.com/dexfs/go-twitter-clone/adapter/output/repository/inmemory/schema" @@ -22,7 +23,7 @@ func NewInMemoryPostRepository(db *database.InMemoryDB) *inMemoryPostRepository } } -func (r *inMemoryPostRepository) CreatePost(aPost *domain.Post) error { +func (r *inMemoryPostRepository) CreatePost(ctx context.Context, aPost *domain.Post) error { r.insert(&inmemory_schema.PostSchema{ ID: aPost.ID, UserID: aPost.User.ID, @@ -38,7 +39,7 @@ func (r *inMemoryPostRepository) CreatePost(aPost *domain.Post) error { return nil } -func (r *inMemoryPostRepository) HasReachedPostingLimitDay(aUserId string, aLimit uint64) bool { +func (r *inMemoryPostRepository) HasReachedPostingLimitDay(ctx context.Context, aUserId string, aLimit uint64) bool { var count = uint64(0) for _, currentData := range r.getAll() { @@ -57,7 +58,7 @@ func (r *inMemoryPostRepository) HasReachedPostingLimitDay(aUserId string, aLimi } } -func (r *inMemoryPostRepository) AllByUserID(aUser *domain.User) []*domain.Post { +func (r *inMemoryPostRepository) AllByUserID(ctx context.Context, aUser *domain.User) []*domain.Post { var feed []*domain.Post for _, currentData := range r.getAll() { if currentData.UserID == aUser.ID { @@ -68,7 +69,7 @@ func (r *inMemoryPostRepository) AllByUserID(aUser *domain.User) []*domain.Post return feed } -func (r *inMemoryPostRepository) FindByID(aPostID string) (*domain.Post, error) { +func (r *inMemoryPostRepository) FindByID(ctx context.Context, aPostID string) (*domain.Post, error) { for _, currentData := range r.getAll() { if currentData.ID == aPostID { var postUser *domain.User @@ -84,7 +85,7 @@ func (r *inMemoryPostRepository) FindByID(aPostID string) (*domain.Post, error) return nil, errors.New("post not found") } -func (r *inMemoryPostRepository) HasPostBeenRepostedByUser(postID string, userID string) bool { +func (r *inMemoryPostRepository) HasPostBeenRepostedByUser(ctx context.Context, postID string, userID string) bool { for _, vPost := range r.getAll() { if vPost.IsRepost { if vPost.UserID == userID && vPost.OriginalPostID == postID { From 10676cc4b92a7d1b6738a92763f327350ba39dd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Santos?= Date: Sun, 2 Feb 2025 16:57:02 -0300 Subject: [PATCH 19/27] feat: use postgres repository --- cmd/api/main.go | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/cmd/api/main.go b/cmd/api/main.go index 75f1e63..0b258df 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -3,8 +3,6 @@ package main import ( "context" "github.com/dexfs/go-twitter-clone/adapter/input/routes" - "github.com/dexfs/go-twitter-clone/adapter/output/repository/inmemory" - inmemory_schema "github.com/dexfs/go-twitter-clone/adapter/output/repository/inmemory/schema" "github.com/dexfs/go-twitter-clone/adapter/output/repository/postgres" "github.com/dexfs/go-twitter-clone/internal/core/port/output" "github.com/dexfs/go-twitter-clone/pkg/database" @@ -47,25 +45,9 @@ func init() { log.Fatal("database is nil") } - //initialUsers := make([]*inmemory_schema.UserSchema, 0) - //initialUsers = append(initialUsers, &inmemory_schema.UserSchema{ - // ID: "4cfe67a9-defc-42b9-8410-cb5086bec2f5", - // Username: "alucard", - // CreatedAt: time.Now(), - // UpdatedAt: time.Now(), - //}) - //initialUsers = append(initialUsers, &inmemory_schema.UserSchema{ - // ID: "b8903f77-5d16-4176-890f-f597594ff952", - // Username: "alexander", - // CreatedAt: time.Now(), - // UpdatedAt: time.Now(), - //}) - - //db.RegisterSchema(inmemory.USER_SCHEMA_NAME, initialUsers) - db.RegisterSchema(inmemory.POST_SCHEMA_NAME, []*inmemory_schema.PostSchema{}) - //userRepo = inmemory.NewInMemoryUserRepository(db) userRepo = postgres.NewPostgresUserRepository(pgDB) - postRepo = inmemory.NewInMemoryPostRepository(db) + postRepo = postgres.NewPostgresPostRepository(pgDB) + } type APIServer struct { From 15c9772f572dee97b07c3b4c7811f9a258fcd332 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Santos?= Date: Sun, 2 Feb 2025 16:57:15 -0300 Subject: [PATCH 20/27] feat: use ulid --- internal/core/domain/post.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/core/domain/post.go b/internal/core/domain/post.go index d041701..5165fcf 100644 --- a/internal/core/domain/post.go +++ b/internal/core/domain/post.go @@ -3,6 +3,7 @@ package domain import ( "errors" "github.com/google/uuid" + "github.com/oklog/ulid/v2" "time" ) @@ -40,7 +41,7 @@ func NewPost(aNewPost NewPostInput) (*Post, error) { } return &Post{ - ID: uuid.NewString(), + ID: ulid.Make().String(), User: aNewPost.User, Content: aNewPost.Content, CreatedAt: time.Now(), @@ -63,7 +64,7 @@ func NewRepost(aRepostInput NewRepostQuoteInput) (*Post, error) { } return &Post{ - ID: uuid.NewString(), + ID: ulid.Make().String(), User: aRepostInput.User, CreatedAt: time.Now(), IsQuote: false, From ecaf3db48fba9f176a774752c9a1f1071b7b4123 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Santos?= Date: Sun, 2 Feb 2025 16:58:07 -0300 Subject: [PATCH 21/27] refactor: interfaces input and output --- internal/core/port/input/create_quote_usecase.go | 3 ++- internal/core/port/input/create_repost_usecase.go | 3 ++- internal/core/port/input/createpost_usecase.go | 3 ++- internal/core/port/output/post_port.go | 15 +++++++++------ internal/core/usecase/create_quote_usecase.go | 11 +++++++---- internal/core/usecase/createpost_usecase.go | 10 ++++++---- internal/core/usecase/createrepost_usecase.go | 9 ++++----- internal/core/usecase/getuserfeed_usecase.go | 2 +- 8 files changed, 33 insertions(+), 23 deletions(-) diff --git a/internal/core/port/input/create_quote_usecase.go b/internal/core/port/input/create_quote_usecase.go index 6503dd8..f2c9f38 100644 --- a/internal/core/port/input/create_quote_usecase.go +++ b/internal/core/port/input/create_quote_usecase.go @@ -1,12 +1,13 @@ package input import ( + "context" "github.com/dexfs/go-twitter-clone/adapter/input/model/rest_errors" "github.com/dexfs/go-twitter-clone/internal/core/domain" ) type CreateQuoteUseCase interface { - Execute(anInput CreateQuoteUseCaseInput) (*domain.Post, *rest_errors.RestError) + Execute(ctx context.Context, anInput CreateQuoteUseCaseInput) (*domain.Post, *rest_errors.RestError) } type CreateQuoteUseCaseInput struct { PostID string diff --git a/internal/core/port/input/create_repost_usecase.go b/internal/core/port/input/create_repost_usecase.go index a5c4de0..8fe5954 100644 --- a/internal/core/port/input/create_repost_usecase.go +++ b/internal/core/port/input/create_repost_usecase.go @@ -1,6 +1,7 @@ package input import ( + "context" "github.com/dexfs/go-twitter-clone/adapter/input/model/rest_errors" "github.com/dexfs/go-twitter-clone/internal/core/domain" ) @@ -11,5 +12,5 @@ type CreateRepostUseCaseInput struct { } type CreateRepostUseCase interface { - Execute(input CreateRepostUseCaseInput) (*domain.Post, *rest_errors.RestError) + Execute(ctx context.Context, input CreateRepostUseCaseInput) (*domain.Post, *rest_errors.RestError) } diff --git a/internal/core/port/input/createpost_usecase.go b/internal/core/port/input/createpost_usecase.go index 7e810a7..3fa6055 100644 --- a/internal/core/port/input/createpost_usecase.go +++ b/internal/core/port/input/createpost_usecase.go @@ -1,6 +1,7 @@ package input import ( + "context" "github.com/dexfs/go-twitter-clone/adapter/input/model/rest_errors" "github.com/dexfs/go-twitter-clone/internal/core/domain" ) @@ -11,5 +12,5 @@ type CreatePostUseCaseInput struct { } type CreatePostUseCase interface { - Execute(aInput CreatePostUseCaseInput) (*domain.Post, *rest_errors.RestError) + Execute(ctx context.Context, aInput CreatePostUseCaseInput) (*domain.Post, *rest_errors.RestError) } diff --git a/internal/core/port/output/post_port.go b/internal/core/port/output/post_port.go index ba0954f..3840ede 100644 --- a/internal/core/port/output/post_port.go +++ b/internal/core/port/output/post_port.go @@ -1,11 +1,14 @@ package output -import "github.com/dexfs/go-twitter-clone/internal/core/domain" +import ( + "context" + "github.com/dexfs/go-twitter-clone/internal/core/domain" +) type PostPort interface { - CreatePost(aPost *domain.Post) error - HasReachedPostingLimitDay(aUserId string, aLimit uint64) bool - HasPostBeenRepostedByUser(postID string, userID string) bool - AllByUserID(aUser *domain.User) []*domain.Post - FindByID(aPostID string) (*domain.Post, error) + CreatePost(ctx context.Context, aPost *domain.Post) error + HasReachedPostingLimitDay(ctx context.Context, aUserId string, aLimit uint64) bool + HasPostBeenRepostedByUser(ctx context.Context, aPostID string, aUserID string) bool + AllByUserID(ctx context.Context, aUser *domain.User) []*domain.Post + FindByID(ctx context.Context, aPostID string) (*domain.Post, error) } diff --git a/internal/core/usecase/create_quote_usecase.go b/internal/core/usecase/create_quote_usecase.go index 21edcc4..794c31c 100644 --- a/internal/core/usecase/create_quote_usecase.go +++ b/internal/core/usecase/create_quote_usecase.go @@ -24,14 +24,13 @@ func NewCreateQuoteUseCase(postPort output.PostPort, userPort output.UserPort) ( }, nil } -func (uc *createQuoteUseCase) Execute(anInput input.CreateQuoteUseCaseInput) (*domain.Post, *rest_errors.RestError) { - ctx := context.Background() +func (uc *createQuoteUseCase) Execute(ctx context.Context, anInput input.CreateQuoteUseCaseInput) (*domain.Post, *rest_errors.RestError) { user, err := uc.userPort.FindByID(ctx, anInput.UserID) if err != nil { return &domain.Post{}, rest_errors.NewBadRequestError(err.Error()) } - post, err := uc.postPort.FindByID(anInput.PostID) + post, err := uc.postPort.FindByID(ctx, anInput.PostID) if err != nil { return &domain.Post{}, rest_errors.NewBadRequestError(err.Error()) } @@ -46,7 +45,11 @@ func (uc *createQuoteUseCase) Execute(anInput input.CreateQuoteUseCaseInput) (*d return &domain.Post{}, rest_errors.NewBadRequestError(err.Error()) } - uc.postPort.CreatePost(newQuotePost) + err = uc.postPort.CreatePost(ctx, newQuotePost) + + if err != nil { + return &domain.Post{}, rest_errors.NewBadRequestError(err.Error()) + } return newQuotePost, nil } diff --git a/internal/core/usecase/createpost_usecase.go b/internal/core/usecase/createpost_usecase.go index 0d1a9f2..1be1ca9 100644 --- a/internal/core/usecase/createpost_usecase.go +++ b/internal/core/usecase/createpost_usecase.go @@ -21,9 +21,8 @@ func NewCreatePostUseCase(postPort output.PostPort, userPort output.UserPort) (* return &createPostUseCase{postPort: postPort, userPort: userPort}, nil } -func (uc *createPostUseCase) Execute(aInput input.CreatePostUseCaseInput) (*domain.Post, *rest_errors.RestError) { - ctx := context.Background() - hasReachedLimit := uc.postPort.HasReachedPostingLimitDay(aInput.UserID, 5) // @TODO mudar isso para vir das configurações +func (uc *createPostUseCase) Execute(ctx context.Context, aInput input.CreatePostUseCaseInput) (*domain.Post, *rest_errors.RestError) { + hasReachedLimit := uc.postPort.HasReachedPostingLimitDay(ctx, aInput.UserID, 5) // @TODO mudar isso para vir das configurações if hasReachedLimit { return &domain.Post{}, rest_errors.NewBadRequestError("you reached your posts day limit") } @@ -41,7 +40,10 @@ func (uc *createPostUseCase) Execute(aInput input.CreatePostUseCaseInput) (*doma return &domain.Post{}, rest_errors.NewBadRequestError(err.Error()) } - uc.postPort.CreatePost(aNewPost) + err = uc.postPort.CreatePost(ctx, aNewPost) + if err != nil { + return &domain.Post{}, rest_errors.NewInternalServerError(err.Error()) + } return aNewPost, nil } diff --git a/internal/core/usecase/createrepost_usecase.go b/internal/core/usecase/createrepost_usecase.go index a516b45..b8e76dc 100644 --- a/internal/core/usecase/createrepost_usecase.go +++ b/internal/core/usecase/createrepost_usecase.go @@ -21,20 +21,19 @@ func NewCreateRepostUseCase(postPort output.PostPort, userPort output.UserPort) return &createRepostUseCase{postPort: postPort, userPort: userPort}, nil } -func (uc *createRepostUseCase) Execute(aInput input.CreateRepostUseCaseInput) (*domain.Post, *rest_errors.RestError) { - ctx := context.Background() +func (uc *createRepostUseCase) Execute(ctx context.Context, aInput input.CreateRepostUseCaseInput) (*domain.Post, *rest_errors.RestError) { user, err := uc.userPort.FindByID(ctx, aInput.UserID) if err != nil { return &domain.Post{}, rest_errors.NewBadRequestError(err.Error()) } - isReposted := uc.postPort.HasPostBeenRepostedByUser(aInput.PostID, aInput.UserID) + isReposted := uc.postPort.HasPostBeenRepostedByUser(ctx, aInput.PostID, aInput.UserID) if isReposted { return &domain.Post{}, rest_errors.NewBadRequestError("it is not possible repost a repost post") } - post, err := uc.postPort.FindByID(aInput.PostID) + post, err := uc.postPort.FindByID(ctx, aInput.PostID) if err != nil { return &domain.Post{}, rest_errors.NewBadRequestError(err.Error()) } @@ -50,7 +49,7 @@ func (uc *createRepostUseCase) Execute(aInput input.CreateRepostUseCaseInput) (* return &domain.Post{}, rest_errors.NewInternalServerError(err.Error()) } - uc.postPort.CreatePost(newRepost) + uc.postPort.CreatePost(ctx, newRepost) return newRepost, nil } diff --git a/internal/core/usecase/getuserfeed_usecase.go b/internal/core/usecase/getuserfeed_usecase.go index ae99ba3..933c832 100644 --- a/internal/core/usecase/getuserfeed_usecase.go +++ b/internal/core/usecase/getuserfeed_usecase.go @@ -29,6 +29,6 @@ func (uc *getUserFeedUseCase) Execute(ctx context.Context, username string) ([]* return []*domain.Post{}, rest_errors.NewNotFoundError(err.Error()) } - posts := uc.postPort.AllByUserID(user) + posts := uc.postPort.AllByUserID(ctx, user) return posts, nil } From ec18764a718f0a2711d541fb051d28dfa166955d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Santos?= Date: Sun, 2 Feb 2025 16:58:20 -0300 Subject: [PATCH 22/27] feat: migrations --- migrations/000001_create_users_table.up.sql | 2 +- migrations/000002_create_posts_table.up.sql | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/migrations/000001_create_users_table.up.sql b/migrations/000001_create_users_table.up.sql index 2170d1f..33ea57a 100644 --- a/migrations/000001_create_users_table.up.sql +++ b/migrations/000001_create_users_table.up.sql @@ -1,6 +1,6 @@ CREATE TABLE users ( - id VARCHAR(36) PRIMARY KEY, -- Sem default, deve ser fornecido manualmente, uuid V7: ex: 0194b7b8-1110-79c0-8d41-cff79b880911 + user_id VARCHAR(36) PRIMARY KEY, -- Sem default, deve ser fornecido manualmente, ulid : ex: 01JJYY0V9AMD9656HT4BSV0ZEK username varchar(50) NOT NULL UNIQUE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() diff --git a/migrations/000002_create_posts_table.up.sql b/migrations/000002_create_posts_table.up.sql index 9cf7116..51bcc7f 100644 --- a/migrations/000002_create_posts_table.up.sql +++ b/migrations/000002_create_posts_table.up.sql @@ -1,7 +1,7 @@ CREATE TABLE posts ( - id VARCHAR(36) PRIMARY KEY, -- Sem default, deve ser fornecido manualmente, uuid V7: ex: 0194b7b8-1110-79c0-8d41-cff79b880911 - user_id VARCHAR(36) NOT NULL REFERENCES users (id) ON DELETE CASCADE, + post_id VARCHAR(36) PRIMARY KEY, -- Sem default, deve ser fornecido manualmente, ulid: ex: 01JJYY0V9AMD9656HT4BSV0ZEK + user_id VARCHAR(36) NOT NULL REFERENCES users (user_id) ON DELETE CASCADE, content TEXT NOT NULL, is_quote BOOLEAN NOT NULL DEFAULT FALSE, is_repost BOOLEAN NOT NULL DEFAULT FALSE, @@ -9,8 +9,9 @@ CREATE TABLE posts original_post_content TEXT NULL, original_post_user_id VARCHAR(36) NULL, original_post_screen_name TEXT NULL, - created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), - CONSTRAINT fk_original_post FOREIGN KEY (original_post_id) REFERENCES posts (id) ON DELETE SET NULL + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +-- CONSTRAINT fk_original_post FOREIGN KEY (original_post_id) REFERENCES posts (id) ON DELETE SET NULL ); -CREATE INDEX idx_posts_user_created_at ON posts(user_id, created_at DESC); \ No newline at end of file +CREATE INDEX idx_posts_user_created_at ON posts (user_id, created_at DESC); +CREATE INDEX idx_posts_original_post_id ON posts (original_post_id); \ No newline at end of file From 2e1e97d794dd6d6249ad52cab9bdbc0fd75109fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Santos?= Date: Sun, 2 Feb 2025 16:58:36 -0300 Subject: [PATCH 23/27] feat: postgres db --- pkg/database/postgres_db.go | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/pkg/database/postgres_db.go b/pkg/database/postgres_db.go index 692998f..90443cb 100644 --- a/pkg/database/postgres_db.go +++ b/pkg/database/postgres_db.go @@ -7,6 +7,7 @@ import ( "github.com/jackc/pgx/v5" "log" "os" + "time" ) type PostgresDB struct { @@ -28,15 +29,37 @@ func (db *PostgresDB) FindOne(ctx context.Context, query string, args ...any) pg return db.conn.QueryRow(ctx, query, args...) } +func (db *PostgresDB) Find(ctx context.Context, query string, args ...any) (pgx.Rows, error) { + return db.conn.Query(ctx, query, args...) +} + func (db *PostgresDB) Batch(ctx context.Context, batch *pgx.Batch, dataSize int) error { + sendStart := time.Now() br := db.conn.SendBatch(ctx, batch) + sendDuration := time.Since(sendStart) + defer br.Close() - for range dataSize { + for i := 0; i < dataSize; i++ { + queryStart := time.Now() _, err := br.Exec() if err != nil { return errors.New("User seed failed: " + err.Error()) } + queryDuration := time.Since(queryStart) + fmt.Printf("Query %d: %v\n", i+1, queryDuration) + } + + fmt.Printf("Tempo de envio do batch: %v\n", sendDuration) + fmt.Printf("Tempo total: %v\n", time.Since(sendStart)) + return nil +} + +func (db *PostgresDB) Insert(ctx context.Context, query string, args ...any) error { + _, err := db.conn.Exec(ctx, query, args...) + if err != nil { + return err } + return nil } From d2a978d5bc64455e95666da7a366b9e285ce14d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Santos?= Date: Sun, 2 Feb 2025 16:58:50 -0300 Subject: [PATCH 24/27] feat: postgres seed --- pkg/seeders/postgres_seed.go | 80 +++++++++++++++++++++++++++++++++++- pkg/seeders/seeds.go | 8 +++- 2 files changed, 86 insertions(+), 2 deletions(-) diff --git a/pkg/seeders/postgres_seed.go b/pkg/seeders/postgres_seed.go index c46952d..bcbedb5 100644 --- a/pkg/seeders/postgres_seed.go +++ b/pkg/seeders/postgres_seed.go @@ -3,8 +3,10 @@ package seeders import ( "context" "github.com/dexfs/go-twitter-clone/adapter/output/repository/postgres" + "github.com/dexfs/go-twitter-clone/internal/core/domain" "github.com/dexfs/go-twitter-clone/pkg/database" "github.com/jackc/pgx/v5" + "strconv" "time" ) @@ -42,9 +44,85 @@ func (s *PostgresSeeder) UsersSeed(ctx context.Context) error { batch.Queue("DELETE FROM users") for _, u := range initialUsers { - batch.Queue(`INSERT INTO users (id, username, created_at, updated_at) VALUES ($1, $2, $3, $4)`, + batch.Queue(`INSERT INTO users (user_id, username, created_at, updated_at) VALUES ($1, $2, $3, $4)`, u.ID, u.Username, u.CreatedAt, u.UpdatedAt) } return s.db.Batch(ctx, batch, len(initialUsers)) } + +func (s *PostgresSeeder) PostsSeed(ctx context.Context) error { + batch := &pgx.Batch{} + + users := make([]*domain.User, 0) + users = append(users, &domain.User{ + ID: "01JJYY0V9AMD9656HT4BSV0ZEK", + Username: "alucard", + }) + users = append(users, &domain.User{ + ID: "01JJYY1S0JY0ERC1VQ3EEFNJC7", + Username: "alexander", + }) + batch.Queue("DELETE FROM posts") + // Criar 3 posts por user + // - alucard criar postatgem + queryInsertPost := ` + INSERT INTO posts ( + post_id, user_id, content, is_quote, is_repost, + original_post_id, original_post_content, original_post_user_id, original_post_screen_name, created_at + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) + ` + var userRepost *domain.User + for _, user := range users { + for i := 0; i < 3; i++ { + aPost, _ := domain.NewPost(domain.NewPostInput{User: user, Content: user.Username + " post " + strconv.Itoa(i) + " from seed"}) + batch.Queue(queryInsertPost, aPost.ID, + aPost.User.ID, + aPost.Content, + aPost.IsQuote, + aPost.IsRepost, + aPost.OriginalPostID, + aPost.OriginalPostContent, + aPost.OriginalPostUserID, + aPost.OriginalPostScreenName, + aPost.CreatedAt) + + if user.Username == "alucard" { + userRepost = users[1] + } else { + userRepost = users[0] + } + + aRepost, _ := domain.NewRepost(domain.NewRepostQuoteInput{User: userRepost, Post: aPost}) + batch.Queue(queryInsertPost, aRepost.ID, + aRepost.User.ID, + aRepost.Content, + aRepost.IsQuote, + aRepost.IsRepost, + aRepost.OriginalPostID, + aRepost.OriginalPostContent, + aRepost.OriginalPostUserID, + aRepost.OriginalPostScreenName, + aRepost.CreatedAt) + + aQuote, _ := domain.NewQuote(domain.NewRepostQuoteInput{User: userRepost, Post: aPost, Content: userRepost.Username + " quote " + strconv.Itoa(i)}) + batch.Queue(queryInsertPost, aQuote.ID, + aQuote.User.ID, + aQuote.Content, + aQuote.IsQuote, + aQuote.IsRepost, + aQuote.OriginalPostID, + aQuote.OriginalPostContent, + aQuote.OriginalPostUserID, + aQuote.OriginalPostScreenName, + aQuote.CreatedAt) + + } + } + err := s.db.Batch(ctx, batch, len(users)) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/seeders/seeds.go b/pkg/seeders/seeds.go index 93066a7..e51b7bc 100644 --- a/pkg/seeders/seeds.go +++ b/pkg/seeders/seeds.go @@ -21,5 +21,11 @@ func Run() { if err != nil { log.Fatal("Error on seeding users: ", err) } - log.Println("Users seeder applied") + + err = pgSeeder.PostsSeed(ctxWithCancel) + if err != nil { + log.Fatal("Error on seeding posts: ", err) + } + + log.Println("seeds applied") } From 493f3c4f5a850b4e558e54c24b8584a97ffab0b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Santos?= Date: Sun, 2 Feb 2025 16:59:32 -0300 Subject: [PATCH 25/27] chore: update user id for test --- api/go-twitter-clone.http | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/go-twitter-clone.http b/api/go-twitter-clone.http index 97f7b9f..8e947b2 100644 --- a/api/go-twitter-clone.http +++ b/api/go-twitter-clone.http @@ -9,7 +9,7 @@ Content-Type: application/json { "content": "Teste Post", - "user_id": "4cfe67a9-defc-42b9-8410-cb5086bec2f5" + "user_id": "01JJYY1S0JY0ERC1VQ3EEFNJC7" } ### @@ -19,7 +19,7 @@ Accept: application/json ### -GET http://localhost:8001/users/alucard/feed +GET http://localhost:8001/users/alexander/feed Accept: application/json ### From 9f81e3f6dc489e44a3b6c6f06e7e14010e88f372 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Santos?= Date: Sun, 2 Feb 2025 17:01:01 -0300 Subject: [PATCH 26/27] chore: update dependencies --- go.mod | 1 + go.sum | 3 +++ 2 files changed, 4 insertions(+) diff --git a/go.mod b/go.mod index 1d1f69b..9b3a506 100644 --- a/go.mod +++ b/go.mod @@ -36,6 +36,7 @@ require ( github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/oklog/ulid/v2 v2.1.0 // indirect github.com/pelletier/go-toml/v2 v2.2.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect diff --git a/go.sum b/go.sum index 1d71105..0b15565 100644 --- a/go.sum +++ b/go.sum @@ -98,10 +98,13 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= +github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pelletier/go-toml/v2 v2.2.1 h1:9TA9+T8+8CUCO2+WYnDLCgrYi9+omqKXyjDtosvtEhg= github.com/pelletier/go-toml/v2 v2.2.1/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= From b44b8c3f710e94b4d15952b9a2d8722d93606f54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Santos?= Date: Sun, 2 Feb 2025 17:01:19 -0300 Subject: [PATCH 27/27] chore: make command --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index a1a4724..8e67b54 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ test: @go test ./... -v build: - @go build -o bin/api cmd/api/api.go + @go build -o bin/api cmd/api/main.go start: build ./bin/api