From 7f4f7ff697049a7abca837a0fac243249bda4089 Mon Sep 17 00:00:00 2001 From: Nayyara Airlangga Date: Sun, 20 Aug 2023 21:38:06 +0700 Subject: [PATCH 1/7] refactor: move migration to makefile tooling --- Makefile | 9 +++-- db/migrations/migrate.go | 86 ---------------------------------------- go.mod | 4 -- go.sum | 24 ----------- 4 files changed, 6 insertions(+), 117 deletions(-) delete mode 100644 db/migrations/migrate.go diff --git a/Makefile b/Makefile index 468cc31..231c144 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,6 @@ +#!make +include .env + .PHONY: build migrate migrate-down migrate-fix install: @@ -25,10 +28,10 @@ migration: migrate create -seq -ext sql -dir db/migrations $(filter-out $@,$(MAKECMDGOALS)) migrate: - go run db/migrations/migrate.go $(filter-out $@,$(MAKECMDGOALS)) + migrate -path db/migrations -database "postgres://${DB_USER}:${DB_PWD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?${DB_SSL}" up $(filter-out $@,$(MAKECMDGOALS)) migrate-down: - go run db/migrations/migrate.go -action down $(filter-out $@,$(MAKECMDGOALS)) + migrate -path db/migrations -database "postgres://${DB_USER}:${DB_PWD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?${DB_SSL}" down $(filter-out $@,$(MAKECMDGOALS)) migrate-fix: - go run db/migrations/migrate.go -action force -version $(filter-out $@,$(MAKECMDGOALS)) + migrate -path db/migrations -database "postgres://${DB_USER}:${DB_PWD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?${DB_SSL}" force $(filter-out $@,$(MAKECMDGOALS)) diff --git a/db/migrations/migrate.go b/db/migrations/migrate.go deleted file mode 100644 index 94a5d2a..0000000 --- a/db/migrations/migrate.go +++ /dev/null @@ -1,86 +0,0 @@ -package main - -import ( - "flag" - "log" - - "github.com/golang-migrate/migrate/v4" - _ "github.com/golang-migrate/migrate/v4/database/postgres" - _ "github.com/golang-migrate/migrate/v4/source/file" - "github.com/lexica-app/lexicapi/app" -) - -var action string -var steps uint -var version uint - -func init() { - flag.StringVar(&action, "action", "up", "run db migrations [up | down]") - flag.UintVar(&steps, "steps", 0, "amount of migrations run. If not specified, run all") - flag.UintVar(&version, "version", 0, "version of migration to force to") - flag.Parse() -} - -func main() { - config, err := app.LoadConfig() - if err != nil { - log.Fatal("Failed to load config:", err) - } - - migrate, err := migrate.New("file://db/migrations", config.DbUrl) - if err != nil { - log.Fatal("Failed to read migration files:", err) - } - - if action == "up" { - if steps != 0 { - if err = migrate.Steps(int(steps)); err != nil { - if err.Error() == "no change" { - log.Println("Nothing to run") - } else { - log.Fatal("Failed to run migration:", err.Error()) - } - } - } else { - if err = migrate.Up(); err != nil { - if err.Error() == "no change" { - log.Println("Nothing to run") - } else { - log.Fatal("Failed to run migration:", err.Error()) - } - } - } - } else if action == "down" { - if steps != 0 { - if err = migrate.Steps(-1 * int(steps)); err != nil { - if err.Error() == "no change" { - log.Println("Nothing to run") - } else { - log.Fatal("Failed to run migration:", err.Error()) - } - } - } else { - if err = migrate.Down(); err != nil { - if err.Error() == "no change" { - log.Println("Nothing to run") - } else { - log.Fatal("Failed to run migration:", err.Error()) - } - } - } - } else if action == "force" { - if version != 0 { - if err = migrate.Force(int(version)); err != nil { - if err.Error() == "no change" { - log.Println("Nothing to run") - } else { - log.Fatal("Failed to fix/force migration:", err) - } - } - } else { - log.Fatal("Invalid migration version target") - } - } else { - log.Fatal("Invalid migration action") - } -} diff --git a/go.mod b/go.mod index 56aa9cb..6ededb3 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,6 @@ require ( github.com/georgysavva/scany/v2 v2.0.0 github.com/go-chi/chi/v5 v5.0.8 github.com/golang-jwt/jwt/v5 v5.0.0 - github.com/golang-migrate/migrate/v4 v4.16.2 github.com/jackc/pgx/v5 v5.4.1 github.com/jellydator/validation v1.1.0 github.com/oklog/ulid/v2 v2.1.0 @@ -28,8 +27,6 @@ require ( github.com/golang/protobuf v1.5.3 // indirect github.com/google/s2a-go v0.1.4 // indirect github.com/googleapis/enterprise-certificate-proxy v0.2.5 // indirect - github.com/hashicorp/errwrap v1.1.0 // indirect - github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect @@ -48,7 +45,6 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.4.2 // indirect go.opencensus.io v0.24.0 // indirect - go.uber.org/atomic v1.11.0 // indirect golang.org/x/crypto v0.11.0 // indirect golang.org/x/net v0.12.0 // indirect golang.org/x/oauth2 v0.10.0 // indirect diff --git a/go.sum b/go.sum index 35ff90a..ef21e8d 100644 --- a/go.sum +++ b/go.sum @@ -40,12 +40,10 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= -github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ= github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= @@ -67,11 +65,6 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV 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.3.16 h1:i6gq2YQEtcrjKbeJpBkWjE8MmLZPYllcjOFbTZuPDnw= -github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= -github.com/docker/docker v20.10.24+incompatible h1:Ugvxm7a8+Gz6vqQYQQ2W7GYq5EUPaAiuPgIfVyI3dYE= -github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= -github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -92,11 +85,8 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= -github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/golang-migrate/migrate/v4 v4.16.2 h1:8coYbMKUyInrFk1lfGfRovTLAW7PhWp8qQDT2iKfuoA= -github.com/golang-migrate/migrate/v4 v4.16.2/go.mod h1:pfcJX4nPHaVdc5nmdCikFBWtm+UBpiZjRNNsyBbp0/o= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -167,11 +157,6 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m github.com/googleapis/gax-go/v2 v2.11.0 h1:9V9PWXEsWnPpQhu/PeQIkS4eGzMlTLGgt80cUUI8Ki4= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -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/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= @@ -214,12 +199,8 @@ github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APP github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= -github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= 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/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM= github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o= github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= @@ -239,7 +220,6 @@ github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc= github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU= github.com/sashabaranov/go-openai v1.12.0 h1:aRNHH0gtVfrpIaEolD0sWrLLRnYQNK4cH/bIAHwL8Rk= github.com/sashabaranov/go-openai v1.12.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= -github.com/sirupsen/logrus v1.9.2 h1:oxx1eChJGI6Uks2ZC4W1zpLlVgqB8ner4EuQwV4Ik1Y= github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= @@ -280,8 +260,6 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= -go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -327,7 +305,6 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -496,7 +473,6 @@ golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From d1ddc5f2abbcdd5920833bb0ea906657712a64bd Mon Sep 17 00:00:00 2001 From: Nayyara Airlangga Date: Mon, 21 Aug 2023 01:52:47 +0700 Subject: [PATCH 2/7] feat: add onboarding models and field migrations --- db/migrations/000004_onboarding_models.down.sql | 5 +++++ db/migrations/000004_onboarding_models.up.sql | 12 ++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 db/migrations/000004_onboarding_models.down.sql create mode 100644 db/migrations/000004_onboarding_models.up.sql diff --git a/db/migrations/000004_onboarding_models.down.sql b/db/migrations/000004_onboarding_models.down.sql new file mode 100644 index 0000000..38c694d --- /dev/null +++ b/db/migrations/000004_onboarding_models.down.sql @@ -0,0 +1,5 @@ +ALTER TABLE IF EXISTS users +DROP COLUMN IF EXISTS role, +DROP COLUMN IF EXISTS education_level; + +DROP TABLE IF EXISTS users_interests; \ No newline at end of file diff --git a/db/migrations/000004_onboarding_models.up.sql b/db/migrations/000004_onboarding_models.up.sql new file mode 100644 index 0000000..d56479e --- /dev/null +++ b/db/migrations/000004_onboarding_models.up.sql @@ -0,0 +1,12 @@ +CREATE TABLE IF NOT EXISTS users_interests ( + id BYTEA NOT NULL, + user_id BYTEA NOT NULL, + category_id BYTEA NOT NULL, + deleted_at TIMESTAMPTZ, + PRIMARY KEY(id), + CONSTRAINT users_interests_unique UNIQUE NULLS NOT DISTINCT (user_id, category_id, deleted_at) +); + +ALTER TABLE IF EXISTS users +ADD COLUMN IF NOT EXISTS role VARCHAR(50), +ADD COLUMN IF NOT EXISTS education_level VARCHAR(50); \ No newline at end of file From 9ad5ff6dbc8423c4ef5fa1a70fba0b13deab83dc Mon Sep 17 00:00:00 2001 From: Nayyara Airlangga Date: Mon, 21 Aug 2023 08:13:57 +0700 Subject: [PATCH 3/7] feat: add domain model logic for onboarding --- app/auth/request.go | 6 ++++ app/auth/user.go | 72 ++++++++++++++++++++++++++++++++----- app/auth/user_validators.go | 31 +++++++++++++--- 3 files changed, 96 insertions(+), 13 deletions(-) diff --git a/app/auth/request.go b/app/auth/request.go index 99c0a0e..5b37e06 100644 --- a/app/auth/request.go +++ b/app/auth/request.go @@ -4,3 +4,9 @@ type superadminSignInReq struct { Email string `json:"email"` Password string `json:"password"` } + +type onboardReq struct { + Role string `json:"role"` + EducationLevel string `json:"education_level"` + InterestIds []string `json:"interest_ids"` +} diff --git a/app/auth/user.go b/app/auth/user.go index 94c7905..cdd3df0 100644 --- a/app/auth/user.go +++ b/app/auth/user.go @@ -1,13 +1,20 @@ package auth import ( + "errors" "time" "github.com/oklog/ulid/v2" "gopkg.in/guregu/null.v4" ) +var ( + ErrUnverifiedUser = errors.New("User must be verified first") +) + type UserStatus uint +type UserRole null.String +type UserEducationLevel null.String const ( NOT_VERIFIED UserStatus = iota @@ -15,15 +22,30 @@ const ( ACTIVE ) +var ( + STUDENT = UserRole(null.StringFrom("pelajar")) + EDUCATOR = UserRole(null.StringFrom("pengajar")) + CIVILIAN = UserRole(null.StringFrom("umum")) +) + +var ( + SMP UserEducationLevel = UserEducationLevel(null.StringFrom("smp")) + SMA UserEducationLevel = UserEducationLevel(null.StringFrom("sma")) + SARJANA UserEducationLevel = UserEducationLevel(null.StringFrom("sarjana")) + LAINNYA UserEducationLevel = UserEducationLevel(null.StringFrom("lainnya")) +) + type User struct { - Id ulid.ULID `json:"id"` - Name null.String `json:"name"` - Email null.String `json:"email"` - ImageUrl null.String `json:"image_url"` - Status UserStatus `json:"status"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt null.Time `json:"updated_at"` - DeletedAt null.Time `json:"deleted_at"` + Id ulid.ULID `json:"id"` + Name null.String `json:"name"` + Email null.String `json:"email"` + ImageUrl null.String `json:"image_url"` + Status UserStatus `json:"status"` + Role UserRole `json:"role"` + EducationLevel UserEducationLevel `json:"education_level"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt null.Time `json:"updated_at"` + DeletedAt null.Time `json:"deleted_at"` } func NewUserWithOAuth(name, email, imageUrl null.String) (User, map[string]error) { @@ -53,3 +75,37 @@ func NewUserWithOAuth(name, email, imageUrl null.String) (User, map[string]error CreatedAt: time.Now(), }, nil } + +func (u *User) Onboard(roleStr, educationLevelStr string) (errs map[string]error) { + errs = make(map[string]error) + + if u.Status == ACTIVE { + return nil + } + + if u.Status == NOT_VERIFIED { + errs["status"] = ErrUnverifiedUser + return errs + } + + role, err := validateUserRole(null.StringFrom(roleStr)) + if err != nil { + errs["role"] = err + } + + educationLevel, err := validateUserEducationLevel(null.StringFrom(educationLevelStr)) + if err != nil { + errs["education_level"] = err + } + + if len(errs) != 0 { + return errs + } + + u.Role = role + u.EducationLevel = educationLevel + u.Status = ACTIVE + u.UpdatedAt = null.TimeFrom(time.Now()) + + return nil +} diff --git a/app/auth/user_validators.go b/app/auth/user_validators.go index d066079..f7f4410 100644 --- a/app/auth/user_validators.go +++ b/app/auth/user_validators.go @@ -6,14 +6,17 @@ import ( "github.com/jellydator/validation" "github.com/jellydator/validation/is" "github.com/oklog/ulid/v2" + "gopkg.in/guregu/null.v4" ) var ( - ErrInvalidUserId = validation.NewError("auth:invalid_user_id", "Invalid user id") - ErrUserNameTooLong = validation.NewError("auth:user_name_too_long", "Name can't be longer than 255 characters") - ErrInvalidUserEmail = validation.NewError("auth:invalid_user_email", "Invalid user email") - ErrInvalidUserImageUrl = validation.NewError("auth:invalid_user_image_url", "Invalid user image url") - ErrInvalidUserStatus = validation.NewError("auth:invalid_user_status", "Invalid user status") + ErrInvalidUserId = validation.NewError("auth:invalid_user_id", "Invalid user id") + ErrUserNameTooLong = validation.NewError("auth:user_name_too_long", "Name can't be longer than 255 characters") + ErrInvalidUserEmail = validation.NewError("auth:invalid_user_email", "Invalid user email") + ErrInvalidUserImageUrl = validation.NewError("auth:invalid_user_image_url", "Invalid user image url") + ErrInvalidUserStatus = validation.NewError("auth:invalid_user_status", "Invalid user status") + ErrInvalidUserRole = validation.NewError("auth:invalid_user_role", "Invalid user role") + ErrInvalidUserEducationlevel = validation.NewError("auth:invalid_user_education_level", "Invalid user education level") ) func validateUserId(idStr string) (id ulid.ULID, err error) { @@ -65,3 +68,21 @@ func validateUserStatus(status int) (err error) { return nil } + +func validateUserRole(roleStr null.String) (role UserRole, err error) { + switch roleStr.String { + case STUDENT.String, EDUCATOR.String, CIVILIAN.String: + return UserRole(role), nil + } + + return role, ErrInvalidUserRole +} + +func validateUserEducationLevel(levelStr null.String) (level UserEducationLevel, err error) { + switch levelStr.String { + case SMP.String, SMA.String, SARJANA.String, LAINNYA.String: + return UserEducationLevel(levelStr), nil + } + + return level, ErrInvalidUserEducationlevel +} From 839f13d1b2e69b32f925bbf6dab122dfafe25628 Mon Sep 17 00:00:00 2001 From: Nayyara Airlangga Date: Mon, 21 Aug 2023 08:41:46 +0700 Subject: [PATCH 4/7] refactor: use embedded structs for nullable enums --- app/auth/handlers.go | 12 ++++++++++++ app/auth/router.go | 1 + app/auth/user.go | 23 ++++++++++++++--------- app/auth/user_validators.go | 8 ++++---- 4 files changed, 31 insertions(+), 13 deletions(-) diff --git a/app/auth/handlers.go b/app/auth/handlers.go index 73dea93..72914fa 100644 --- a/app/auth/handlers.go +++ b/app/auth/handlers.go @@ -61,3 +61,15 @@ func signInWithGoogleHandler(w http.ResponseWriter, r *http.Request) { app.WriteHttpBodyJson(w, http.StatusOK, signIn) } + +func onboardUserHandler(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + user, ok := ctx.Value(UserInfoCtx).(User) + if !ok { + app.WriteHttpError(w, http.StatusUnauthorized, ErrInvalidAccessToken) + return + } + + app.WriteHttpBodyJson(w, http.StatusOK, user) +} diff --git a/app/auth/router.go b/app/auth/router.go index 202106e..c206203 100644 --- a/app/auth/router.go +++ b/app/auth/router.go @@ -19,6 +19,7 @@ func Router() *chi.Mux { r.Use(UserAuthMiddleware) r.Get("/userinfo", getUserInfoHandler) + r.Put("/onboarding", onboardUserHandler) }) r.Group(func(r chi.Router) { diff --git a/app/auth/user.go b/app/auth/user.go index cdd3df0..3220572 100644 --- a/app/auth/user.go +++ b/app/auth/user.go @@ -13,8 +13,13 @@ var ( ) type UserStatus uint -type UserRole null.String -type UserEducationLevel null.String +type UserRole struct { + null.String +} + +type UserEducationLevel struct { + null.String +} const ( NOT_VERIFIED UserStatus = iota @@ -23,16 +28,16 @@ const ( ) var ( - STUDENT = UserRole(null.StringFrom("pelajar")) - EDUCATOR = UserRole(null.StringFrom("pengajar")) - CIVILIAN = UserRole(null.StringFrom("umum")) + STUDENT = UserRole{null.StringFrom("pelajar")} + EDUCATOR = UserRole{null.StringFrom("pengajar")} + CIVILIAN = UserRole{null.StringFrom("umum")} ) var ( - SMP UserEducationLevel = UserEducationLevel(null.StringFrom("smp")) - SMA UserEducationLevel = UserEducationLevel(null.StringFrom("sma")) - SARJANA UserEducationLevel = UserEducationLevel(null.StringFrom("sarjana")) - LAINNYA UserEducationLevel = UserEducationLevel(null.StringFrom("lainnya")) + SMP = UserEducationLevel{null.StringFrom("smp")} + SMA = UserEducationLevel{null.StringFrom("sma")} + SARJANA = UserEducationLevel{null.StringFrom("sarjana")} + LAINNYA = UserEducationLevel{null.StringFrom("lainnya")} ) type User struct { diff --git a/app/auth/user_validators.go b/app/auth/user_validators.go index f7f4410..e07a10d 100644 --- a/app/auth/user_validators.go +++ b/app/auth/user_validators.go @@ -71,8 +71,8 @@ func validateUserStatus(status int) (err error) { func validateUserRole(roleStr null.String) (role UserRole, err error) { switch roleStr.String { - case STUDENT.String, EDUCATOR.String, CIVILIAN.String: - return UserRole(role), nil + case STUDENT.String.String, EDUCATOR.String.String, CIVILIAN.String.String: + return UserRole{roleStr}, nil } return role, ErrInvalidUserRole @@ -80,8 +80,8 @@ func validateUserRole(roleStr null.String) (role UserRole, err error) { func validateUserEducationLevel(levelStr null.String) (level UserEducationLevel, err error) { switch levelStr.String { - case SMP.String, SMA.String, SARJANA.String, LAINNYA.String: - return UserEducationLevel(levelStr), nil + case SMP.String.String, SMA.String.String, SARJANA.String.String, LAINNYA.String.String: + return UserEducationLevel{levelStr}, nil } return level, ErrInvalidUserEducationlevel From b943b6530c8197f40c4fc9f5d2bf1bb1efa0b6e9 Mon Sep 17 00:00:00 2001 From: Nayyara Airlangga Date: Mon, 21 Aug 2023 08:53:39 +0700 Subject: [PATCH 5/7] feat: add baseline service logic for onboarding --- app/auth/handlers.go | 13 +++++++++++++ app/auth/service.go | 9 +++++++++ app/auth/user.go | 4 ++-- app/auth/user_validators.go | 35 ++++++++++++++++++++++------------- 4 files changed, 46 insertions(+), 15 deletions(-) diff --git a/app/auth/handlers.go b/app/auth/handlers.go index 72914fa..f4bbf92 100644 --- a/app/auth/handlers.go +++ b/app/auth/handlers.go @@ -1,6 +1,7 @@ package auth import ( + "encoding/json" "net/http" "github.com/lexica-app/lexicapi/app" @@ -71,5 +72,17 @@ func onboardUserHandler(w http.ResponseWriter, r *http.Request) { return } + var body onboardReq + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + app.WriteHttpError(w, http.StatusBadRequest, err) + return + } + + user, errs, _ := onboardUser(ctx, user, body) + if errs != nil { + app.WriteHttpErrors(w, http.StatusBadRequest, errs) + return + } + app.WriteHttpBodyJson(w, http.StatusOK, user) } diff --git a/app/auth/service.go b/app/auth/service.go index 3e382b4..cf7e264 100644 --- a/app/auth/service.go +++ b/app/auth/service.go @@ -128,3 +128,12 @@ func signInWithGoogle(ctx context.Context, idToken string) (signIn UserSignIn, e RefreshToken: refreshToken, }, nil, nil } + +func onboardUser(ctx context.Context, user User, body onboardReq) (User, map[string]error, error) { + errs := user.Onboard(body.Role, body.EducationLevel) + if errs != nil { + return user, errs, nil + } + + return user, nil, nil +} diff --git a/app/auth/user.go b/app/auth/user.go index 3220572..53d1057 100644 --- a/app/auth/user.go +++ b/app/auth/user.go @@ -93,12 +93,12 @@ func (u *User) Onboard(roleStr, educationLevelStr string) (errs map[string]error return errs } - role, err := validateUserRole(null.StringFrom(roleStr)) + role, err := validateUserRole(roleStr) if err != nil { errs["role"] = err } - educationLevel, err := validateUserEducationLevel(null.StringFrom(educationLevelStr)) + educationLevel, err := validateUserEducationLevel(educationLevelStr) if err != nil { errs["education_level"] = err } diff --git a/app/auth/user_validators.go b/app/auth/user_validators.go index e07a10d..d0cb71f 100644 --- a/app/auth/user_validators.go +++ b/app/auth/user_validators.go @@ -6,7 +6,6 @@ import ( "github.com/jellydator/validation" "github.com/jellydator/validation/is" "github.com/oklog/ulid/v2" - "gopkg.in/guregu/null.v4" ) var ( @@ -69,20 +68,30 @@ func validateUserStatus(status int) (err error) { return nil } -func validateUserRole(roleStr null.String) (role UserRole, err error) { - switch roleStr.String { - case STUDENT.String.String, EDUCATOR.String.String, CIVILIAN.String.String: - return UserRole{roleStr}, nil +func validateUserRole(roleStr string) (role UserRole, err error) { + switch roleStr { + case STUDENT.String.String: + return STUDENT, nil + case EDUCATOR.String.String: + return EDUCATOR, nil + case CIVILIAN.String.String: + return CIVILIAN, nil + default: + return UserRole{}, ErrInvalidUserRole } - - return role, ErrInvalidUserRole } -func validateUserEducationLevel(levelStr null.String) (level UserEducationLevel, err error) { - switch levelStr.String { - case SMP.String.String, SMA.String.String, SARJANA.String.String, LAINNYA.String.String: - return UserEducationLevel{levelStr}, nil +func validateUserEducationLevel(levelStr string) (level UserEducationLevel, err error) { + switch levelStr { + case SMP.String.String: + return SMP, nil + case SMA.String.String: + return SMA, nil + case SARJANA.String.String: + return SARJANA, nil + case LAINNYA.String.String: + return LAINNYA, nil + default: + return UserEducationLevel{}, ErrInvalidUserRole } - - return level, ErrInvalidUserEducationlevel } From 23f19fe4c9b6a32c9953fed1c02beca59f3a4176 Mon Sep 17 00:00:00 2001 From: Nayyara Airlangga Date: Mon, 21 Aug 2023 09:10:12 +0700 Subject: [PATCH 6/7] feat: handle saving onboarding data without interests --- app/auth/repo.go | 34 ++++++++++++++++++++++++++++++++++ app/auth/request.go | 8 +++++--- app/auth/service.go | 28 +++++++++++++++++++++++----- 3 files changed, 62 insertions(+), 8 deletions(-) diff --git a/app/auth/repo.go b/app/auth/repo.go index deec7df..eb7e4e8 100644 --- a/app/auth/repo.go +++ b/app/auth/repo.go @@ -136,3 +136,37 @@ func saveAccount(ctx context.Context, tx pgx.Tx, account Account) (newAccount Ac return newAccount, nil } + +func updateUserForOnboarding(ctx context.Context, tx pgx.Tx, user User, interests []ulid.ULID) (onboardedUser User, err error) { + if _, err = findUserById(ctx, tx, user.Id); err != nil { + return + } + + q := ` + UPDATE users + SET role = $1, education_level = $2, status = $3, updated_at = $4 + WHERE id = $5 AND deleted_at IS NULL + RETURNING * + ` + + if err = pgxscan.Get( + ctx, + tx, + &onboardedUser, + q, + user.Role, + user.EducationLevel, + user.Status, + user.UpdatedAt, + user.Id, + ); err != nil { + if err.Error() == "scanning one: no rows in result set" { + return User{}, ErrUserDoesNotExist + } + + log.Err(err).Msg("Failed to update user for onboarding") + return + } + + return onboardedUser, nil +} diff --git a/app/auth/request.go b/app/auth/request.go index 5b37e06..ee3b126 100644 --- a/app/auth/request.go +++ b/app/auth/request.go @@ -1,12 +1,14 @@ package auth +import "github.com/oklog/ulid/v2" + type superadminSignInReq struct { Email string `json:"email"` Password string `json:"password"` } type onboardReq struct { - Role string `json:"role"` - EducationLevel string `json:"education_level"` - InterestIds []string `json:"interest_ids"` + Role string `json:"role"` + EducationLevel string `json:"education_level"` + InterestIds []ulid.ULID `json:"interest_ids"` } diff --git a/app/auth/service.go b/app/auth/service.go index cf7e264..3f87787 100644 --- a/app/auth/service.go +++ b/app/auth/service.go @@ -60,7 +60,7 @@ func signInWithGoogle(ctx context.Context, idToken string) (signIn UserSignIn, e return } - account, err = findAccountByProviderAndProviderAccountId(ctx, tx, GOOGLE, accountId) + _, err = findAccountByProviderAndProviderAccountId(ctx, tx, GOOGLE, accountId) if err != nil { if err != ErrAccountDoesNotExist { return @@ -77,13 +77,13 @@ func signInWithGoogle(ctx context.Context, idToken string) (signIn UserSignIn, e return } - account, err = saveAccount(ctx, tx, account) + _, err = saveAccount(ctx, tx, account) if err != nil { return } } } else { - account, err = findAccountByProviderAndProviderAccountId(ctx, tx, GOOGLE, accountId) + _, err = findAccountByProviderAndProviderAccountId(ctx, tx, GOOGLE, accountId) if err != nil { if err != ErrAccountDoesNotExist { return @@ -100,7 +100,7 @@ func signInWithGoogle(ctx context.Context, idToken string) (signIn UserSignIn, e return } - account, err = saveAccount(ctx, tx, account) + _, err = saveAccount(ctx, tx, account) if err != nil { return } @@ -132,7 +132,25 @@ func signInWithGoogle(ctx context.Context, idToken string) (signIn UserSignIn, e func onboardUser(ctx context.Context, user User, body onboardReq) (User, map[string]error, error) { errs := user.Onboard(body.Role, body.EducationLevel) if errs != nil { - return user, errs, nil + return User{}, errs, nil + } + + tx, err := pool.Begin(ctx) + if err != nil { + log.Err(err).Msg("Failed to onboard user") + return User{}, nil, err + } + + defer tx.Rollback(ctx) + + user, err = updateUserForOnboarding(ctx, tx, user, body.InterestIds) + if err != nil { + return User{}, nil, err + } + + if err = tx.Commit(ctx); err != nil { + log.Err(err).Msg("Failed to onboard user") + return User{}, nil, err } return user, nil, nil From e083ad631dbd4b6d6243eb55eb633f11428386f2 Mon Sep 17 00:00:00 2001 From: Nayyara Airlangga Date: Mon, 21 Aug 2023 10:18:13 +0700 Subject: [PATCH 7/7] feat: handle inserting user interests to db --- app/auth/repo.go | 35 ++++++++++++++++++- db/migrations/000004_onboarding_models.up.sql | 1 + 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/app/auth/repo.go b/app/auth/repo.go index eb7e4e8..2c85f3f 100644 --- a/app/auth/repo.go +++ b/app/auth/repo.go @@ -3,6 +3,7 @@ package auth import ( "context" "errors" + "fmt" "github.com/georgysavva/scany/v2/pgxscan" "github.com/jackc/pgx/v5" @@ -137,7 +138,7 @@ func saveAccount(ctx context.Context, tx pgx.Tx, account Account) (newAccount Ac return newAccount, nil } -func updateUserForOnboarding(ctx context.Context, tx pgx.Tx, user User, interests []ulid.ULID) (onboardedUser User, err error) { +func updateUserForOnboarding(ctx context.Context, tx pgx.Tx, user User, interestIds []ulid.ULID) (onboardedUser User, err error) { if _, err = findUserById(ctx, tx, user.Id); err != nil { return } @@ -168,5 +169,37 @@ func updateUserForOnboarding(ctx context.Context, tx pgx.Tx, user User, interest return } + q = ` + UPDATE users_interests + SET deleted_at = $2 + WHERE user_id = $1 AND deleted_at IS NULL + ` + + _, err = tx.Exec(ctx, q, user.Id, user.UpdatedAt) + if err != nil { + log.Err(err).Msg("Failed to update user for onboarding") + return + } + + q = "INSERT INTO users_interests (id, user_id, category_id, created_at) VALUES" + + params := []any{user.Id, user.UpdatedAt} + paramCount := 3 + for i, interestId := range interestIds { + q += fmt.Sprintf("\n($%d, $1, $%d, $2)", paramCount, paramCount+1) + if i+1 < len(interestIds) { + q += "," + } + + params = append(params, ulid.Make(), interestId) + paramCount += 2 + } + + _, err = tx.Exec(ctx, q, params...) + if err != nil { + log.Err(err).Msg("Failed to update user for onboarding") + return + } + return onboardedUser, nil } diff --git a/db/migrations/000004_onboarding_models.up.sql b/db/migrations/000004_onboarding_models.up.sql index d56479e..54553ab 100644 --- a/db/migrations/000004_onboarding_models.up.sql +++ b/db/migrations/000004_onboarding_models.up.sql @@ -2,6 +2,7 @@ CREATE TABLE IF NOT EXISTS users_interests ( id BYTEA NOT NULL, user_id BYTEA NOT NULL, category_id BYTEA NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL, deleted_at TIMESTAMPTZ, PRIMARY KEY(id), CONSTRAINT users_interests_unique UNIQUE NULLS NOT DISTINCT (user_id, category_id, deleted_at)