From f83df9057d08045f8afd368039123547924bfc24 Mon Sep 17 00:00:00 2001 From: stefanskoricdev Date: Mon, 28 Jul 2025 19:48:34 +0200 Subject: [PATCH 1/7] FEAT: Add request body validation --- cmd/internal-api/main.go | 13 +++++++--- go.mod | 5 ++++ go.sum | 10 ++++++++ .../internal-api/handler/create_member.go | 24 +++++++++++++++---- internal/internal-api/handler/create_team.go | 22 +++++++++++++---- .../handler/create_tenant_database.go | 17 ++++++++----- .../handler/github_installation.go | 9 ++++--- 7 files changed, 79 insertions(+), 21 deletions(-) diff --git a/cmd/internal-api/main.go b/cmd/internal-api/main.go index ee3ce967..7418cf7c 100644 --- a/cmd/internal-api/main.go +++ b/cmd/internal-api/main.go @@ -15,6 +15,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" "github.com/go-chi/jwtauth/v5" + "github.com/go-playground/validator/v10" "go.temporal.io/sdk/client" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" @@ -129,6 +130,12 @@ func main() { defer temporalClient.Close() + // use a single instance of Validate, it caches struct info + validate := validator.New(validator.WithRequiredStructEnabled()) + + createTeamHandler := handler.NewCreateTeamHandler(validate) + createMemberHandler := handler.NewCreateMemberHandler(validate) + r.Route("/tenant", func(r chi.Router) { if os.Getenv("ENABLE_JWT_AUTH") == "true" { pubKey, _ := util.GetRawPublicKey() @@ -143,12 +150,12 @@ func main() { log.Fatalln("Enable jwt auth") } - r.Post("/teams", handler.CreateTeam) + r.Post("/teams", createTeamHandler.CreateTeam) r.Post("/teams/{team_id}/members/{member_id}", handler.AddMemberToTeam) - r.Post("/members", handler.CreateMember) + r.Post("/members", createMemberHandler.CreateMember) }) - temporalHandler := handler.NewTemporalHandler(temporalClient, *cfg) + temporalHandler := handler.NewTemporalHandler(temporalClient, *cfg, validate) r.Route("/onboarding", func(r chi.Router) { if os.Getenv("ENABLE_JWT_AUTH") == "true" { diff --git a/go.mod b/go.mod index 7511d91d..dbf33cd6 100644 --- a/go.mod +++ b/go.mod @@ -26,8 +26,12 @@ require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.27.0 // indirect github.com/goccy/go-json v0.10.3 // indirect github.com/gofri/go-github-ratelimit/v2 v2.0.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect @@ -40,6 +44,7 @@ require ( github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect github.com/klauspost/compress v1.15.15 // indirect + github.com/leodido/go-urn v1.4.0 // indirect github.com/lestrrat-go/blackmagic v1.0.2 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect github.com/lestrrat-go/httprc v1.0.6 // indirect diff --git a/go.sum b/go.sum index 2da26c05..b02d8e00 100644 --- a/go.sum +++ b/go.sum @@ -25,6 +25,8 @@ github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yi github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a/go.mod h1:7Ga40egUymuWXxAe151lTNnCv97MddSOVsjpPPkityA= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= 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.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= @@ -43,10 +45,16 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +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.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +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.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= +github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= @@ -111,6 +119,8 @@ 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.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +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/lestrrat-go/blackmagic v1.0.2 h1:Cg2gVSc9h7sz9NOByczrbUvLopQmXrfFx//N+AkAr5k= github.com/lestrrat-go/blackmagic v1.0.2/go.mod h1:UrEqBzIR2U6CnzVyUtfM6oZNMt/7O7Vohk2J0OGSAtU= github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= diff --git a/internal/internal-api/handler/create_member.go b/internal/internal-api/handler/create_member.go index 42a8e1d3..b6f5bdcc 100644 --- a/internal/internal-api/handler/create_member.go +++ b/internal/internal-api/handler/create_member.go @@ -7,18 +7,29 @@ import ( api "github.com/dxta-dev/app/internal/internal-api" "github.com/dxta-dev/app/internal/util" + "github.com/go-playground/validator/v10" ) type CreateMemberRequestBody struct { - Name string `json:"name"` - Email *string `json:"email"` + Name string `json:"name" validate:"required"` + Email *string `json:"email" validate:"omitempty,email"` } type CreateMemberResponse struct { MemberId int64 `json:"member_id"` } -func CreateMember(w http.ResponseWriter, r *http.Request) { +type CreateMemberHandler struct { + validate *validator.Validate +} + +func NewCreateMemberHandler(validate *validator.Validate) *CreateMemberHandler { + return &CreateMemberHandler{ + validate, + } +} + +func (cmh CreateMemberHandler) CreateMember(w http.ResponseWriter, r *http.Request) { ctx := r.Context() body := &CreateMemberRequestBody{} @@ -29,9 +40,12 @@ func CreateMember(w http.ResponseWriter, r *http.Request) { return } - if body.Name == "" { - fmt.Println("No member name in request body") + err := cmh.validate.Struct(body) + + if err != nil { + fmt.Printf("Bad request body: %v", err.Error()) util.JSONError(w, util.ErrorParam{Error: "Bad Request"}, http.StatusBadRequest) + return } authId := ctx.Value(util.AuthIdCtxKey).(string) diff --git a/internal/internal-api/handler/create_team.go b/internal/internal-api/handler/create_team.go index 6293be5b..41b5116d 100644 --- a/internal/internal-api/handler/create_team.go +++ b/internal/internal-api/handler/create_team.go @@ -7,6 +7,7 @@ import ( api "github.com/dxta-dev/app/internal/internal-api" "github.com/dxta-dev/app/internal/util" + "github.com/go-playground/validator/v10" ) type CreateTeamRequestBody struct { @@ -14,10 +15,20 @@ type CreateTeamRequestBody struct { } type CreateTeamResponse struct { - TeamId int64 `json:"team_id"` + TeamId int64 `json:"team_id" validate:"required"` } -func CreateTeam(w http.ResponseWriter, r *http.Request) { +type CreateTeamHandler struct { + validate *validator.Validate +} + +func NewCreateTeamHandler(validate *validator.Validate) *CreateTeamHandler { + return &CreateTeamHandler{ + validate, + } +} + +func (cth CreateTeamHandler) CreateTeam(w http.ResponseWriter, r *http.Request) { ctx := r.Context() body := &CreateTeamRequestBody{} @@ -28,9 +39,12 @@ func CreateTeam(w http.ResponseWriter, r *http.Request) { return } - if body.TeamName == "" { - fmt.Printf("No team name provided. Team name: %s", body.TeamName) + err := cth.validate.Struct(body) + + if err != nil { + fmt.Printf("Bad request body: %v", err.Error()) util.JSONError(w, util.ErrorParam{Error: "Bad Request"}, http.StatusBadRequest) + return } authId := ctx.Value(util.AuthIdCtxKey).(string) diff --git a/internal/internal-api/handler/create_tenant_database.go b/internal/internal-api/handler/create_tenant_database.go index 268d02df..c11d7d86 100644 --- a/internal/internal-api/handler/create_tenant_database.go +++ b/internal/internal-api/handler/create_tenant_database.go @@ -8,23 +8,26 @@ import ( "github.com/dxta-dev/app/internal/onboarding" "github.com/dxta-dev/app/internal/onboarding/workflow" "github.com/dxta-dev/app/internal/util" + "github.com/go-playground/validator/v10" "go.temporal.io/sdk/client" ) type CreateDatabaseRequestBody struct { - DBName string `json:"dbName"` - OrganizationName string `json:"organizationName"` + DBName string `json:"dbName" validate:"required"` + OrganizationName string `json:"organizationName" validate:"required"` } type TemporalHandler struct { temporalClient client.Client config onboarding.Config + validate *validator.Validate } -func NewTemporalHandler(temporalClient client.Client, config onboarding.Config) *TemporalHandler { +func NewTemporalHandler(temporalClient client.Client, config onboarding.Config, validate *validator.Validate) *TemporalHandler { return &TemporalHandler{ temporalClient, config, + validate, } } @@ -39,15 +42,17 @@ func (th *TemporalHandler) CreateTenantDB(w http.ResponseWriter, r *http.Request return } - if body.DBName == "" || body.OrganizationName == "" { - fmt.Printf("Bad request body: %v", body) + err := th.validate.Struct(body) + + if err != nil { + fmt.Printf("Bad request body: %v", err.Error()) util.JSONError(w, util.ErrorParam{Error: "Bad Request"}, http.StatusBadRequest) return } authId := ctx.Value(util.AuthIdCtxKey).(string) - _, err := workflow.ExecuteCreateTenantDBWorkflow(ctx, th.temporalClient, workflow.ExecuteCreateTenantDBWorkflowParams{ + _, err = workflow.ExecuteCreateTenantDBWorkflow(ctx, th.temporalClient, workflow.ExecuteCreateTenantDBWorkflowParams{ TemporalOnboardingQueueName: th.config.TemporalOnboardingQueueName, AuthID: authId, DBName: body.DBName, diff --git a/internal/internal-api/handler/github_installation.go b/internal/internal-api/handler/github_installation.go index bf5f5983..10078f5d 100644 --- a/internal/internal-api/handler/github_installation.go +++ b/internal/internal-api/handler/github_installation.go @@ -26,14 +26,17 @@ func (th *TemporalHandler) GithubInstallation(w http.ResponseWriter, r *http.Req return } - if body.InstallationID == 0 || body.DBURL == "" || body.DBDomainName == "" { - fmt.Printf("Bad request body %v", body) + err := th.validate.Struct(body) + + if err != nil { + fmt.Printf("Bad request body: %v", err.Error()) util.JSONError(w, util.ErrorParam{Error: "Bad Request"}, http.StatusBadRequest) + return } authId := ctx.Value(util.AuthIdCtxKey).(string) - _, err := workflow.ExecuteAfterGithubInstallationWorkflow(ctx, th.temporalClient, workflow.ExecuteAfterGithubInstallationParams{ + _, err = workflow.ExecuteAfterGithubInstallationWorkflow(ctx, th.temporalClient, workflow.ExecuteAfterGithubInstallationParams{ TemporalOnboardingQueueName: th.config.TemporalOnboardingQueueName, AuthID: authId, InstallationID: body.InstallationID, From 4f20c8f8988daa25f0e808db80fb0cc0b46b660c Mon Sep 17 00:00:00 2001 From: stefanskoricdev Date: Tue, 29 Jul 2025 11:25:55 +0200 Subject: [PATCH 2/7] refactor: Use closure to share validator with handlers --- cmd/internal-api/main.go | 15 +-- .../internal-api/handler/create_member.go | 110 +++++++--------- internal/internal-api/handler/create_team.go | 124 ++++++++---------- .../handler/create_tenant_database.go | 8 +- .../handler/github_installation.go | 2 +- internal/internal-api/handler/users_count.go | 2 +- .../workflow/after_github_installation.go | 1 - 7 files changed, 121 insertions(+), 141 deletions(-) diff --git a/cmd/internal-api/main.go b/cmd/internal-api/main.go index 7418cf7c..e4ba8e46 100644 --- a/cmd/internal-api/main.go +++ b/cmd/internal-api/main.go @@ -133,9 +133,6 @@ func main() { // use a single instance of Validate, it caches struct info validate := validator.New(validator.WithRequiredStructEnabled()) - createTeamHandler := handler.NewCreateTeamHandler(validate) - createMemberHandler := handler.NewCreateMemberHandler(validate) - r.Route("/tenant", func(r chi.Router) { if os.Getenv("ENABLE_JWT_AUTH") == "true" { pubKey, _ := util.GetRawPublicKey() @@ -150,12 +147,12 @@ func main() { log.Fatalln("Enable jwt auth") } - r.Post("/teams", createTeamHandler.CreateTeam) + r.Post("/teams", handler.CreateTeam(validate)) r.Post("/teams/{team_id}/members/{member_id}", handler.AddMemberToTeam) - r.Post("/members", createMemberHandler.CreateMember) + r.Post("/members", handler.CreateMember(validate)) }) - temporalHandler := handler.NewTemporalHandler(temporalClient, *cfg, validate) + onboardingHandler := handler.NewOnboardingHandler(temporalClient, *cfg, validate) r.Route("/onboarding", func(r chi.Router) { if os.Getenv("ENABLE_JWT_AUTH") == "true" { @@ -171,8 +168,8 @@ func main() { log.Fatalln("Enable jwt auth") } - r.Post("/databases", temporalHandler.CreateTenantDB) - r.Post("/github-installation", temporalHandler.GithubInstallation) + r.Post("/databases", onboardingHandler.CreateTenantDB) + r.Post("/github-installation", onboardingHandler.GithubInstallation) }) r.Get("/health", func(w http.ResponseWriter, r *http.Request) { @@ -183,7 +180,7 @@ func main() { w.Write([]byte(`OK`)) }) - r.Get("/users-count", temporalHandler.UsersCount) + r.Get("/users-count", onboardingHandler.UsersCount) go func() { log.Printf("Listening on %s\n", srv.Addr) diff --git a/internal/internal-api/handler/create_member.go b/internal/internal-api/handler/create_member.go index b6f5bdcc..17d0a064 100644 --- a/internal/internal-api/handler/create_member.go +++ b/internal/internal-api/handler/create_member.go @@ -19,64 +19,56 @@ type CreateMemberResponse struct { MemberId int64 `json:"member_id"` } -type CreateMemberHandler struct { - validate *validator.Validate -} - -func NewCreateMemberHandler(validate *validator.Validate) *CreateMemberHandler { - return &CreateMemberHandler{ - validate, - } -} - -func (cmh CreateMemberHandler) CreateMember(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - body := &CreateMemberRequestBody{} - - if err := json.NewDecoder(r.Body).Decode(body); err != nil { - fmt.Printf("Issue while parsing body. Error: %s", err.Error()) - util.JSONError(w, util.ErrorParam{Error: "Bad Request"}, http.StatusBadRequest) - return - } - - err := cmh.validate.Struct(body) - - if err != nil { - fmt.Printf("Bad request body: %v", err.Error()) - util.JSONError(w, util.ErrorParam{Error: "Bad Request"}, http.StatusBadRequest) - return - } - - authId := ctx.Value(util.AuthIdCtxKey).(string) - - apiState, err := api.InternalApiState(authId, ctx) - - if err != nil { - util.JSONError(w, util.ErrorParam{Error: "Internal Server Error"}, http.StatusInternalServerError) - return - } - - newMemberRes, err := apiState.DB.CreateMember(body.Name, body.Email, ctx) - - if err != nil { - util.JSONError( - w, - util.ErrorParam{Error: "Could not create new member"}, - http.StatusInternalServerError, - ) - return - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - if err := json.NewEncoder(w).Encode(CreateMemberResponse{MemberId: newMemberRes.Id}); err != nil { - fmt.Printf("Issue while formatting response. Error: %s", err.Error()) - util.JSONError( - w, - util.ErrorParam{Error: "Internal Server Error"}, - http.StatusInternalServerError, - ) - return +func CreateMember(validate *validator.Validate) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + body := &CreateMemberRequestBody{} + + if err := json.NewDecoder(r.Body).Decode(body); err != nil { + fmt.Printf("Issue while parsing body. Error: %s", err.Error()) + util.JSONError(w, util.ErrorParam{Error: "Bad Request"}, http.StatusBadRequest) + return + } + + err := validate.Struct(body) + + if err != nil { + fmt.Printf("Bad request body: %v", err.Error()) + util.JSONError(w, util.ErrorParam{Error: "Bad Request"}, http.StatusBadRequest) + return + } + + authId := ctx.Value(util.AuthIdCtxKey).(string) + + apiState, err := api.InternalApiState(authId, ctx) + + if err != nil { + util.JSONError(w, util.ErrorParam{Error: "Internal Server Error"}, http.StatusInternalServerError) + return + } + + newMemberRes, err := apiState.DB.CreateMember(body.Name, body.Email, ctx) + + if err != nil { + util.JSONError( + w, + util.ErrorParam{Error: "Could not create new member"}, + http.StatusInternalServerError, + ) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + if err := json.NewEncoder(w).Encode(CreateMemberResponse{MemberId: newMemberRes.Id}); err != nil { + fmt.Printf("Issue while formatting response. Error: %s", err.Error()) + util.JSONError( + w, + util.ErrorParam{Error: "Internal Server Error"}, + http.StatusInternalServerError, + ) + return + } } } diff --git a/internal/internal-api/handler/create_team.go b/internal/internal-api/handler/create_team.go index 41b5116d..d75c43d7 100644 --- a/internal/internal-api/handler/create_team.go +++ b/internal/internal-api/handler/create_team.go @@ -18,71 +18,63 @@ type CreateTeamResponse struct { TeamId int64 `json:"team_id" validate:"required"` } -type CreateTeamHandler struct { - validate *validator.Validate -} - -func NewCreateTeamHandler(validate *validator.Validate) *CreateTeamHandler { - return &CreateTeamHandler{ - validate, - } -} - -func (cth CreateTeamHandler) CreateTeam(w http.ResponseWriter, r *http.Request) { - ctx := r.Context() - - body := &CreateTeamRequestBody{} - - if err := json.NewDecoder(r.Body).Decode(body); err != nil { - fmt.Printf("Issue while parsing body. Error: %s", err.Error()) - util.JSONError(w, util.ErrorParam{Error: "Bad Request"}, http.StatusBadRequest) - return - } - - err := cth.validate.Struct(body) - - if err != nil { - fmt.Printf("Bad request body: %v", err.Error()) - util.JSONError(w, util.ErrorParam{Error: "Bad Request"}, http.StatusBadRequest) - return - } - - authId := ctx.Value(util.AuthIdCtxKey).(string) - - apiState, err := api.InternalApiState(authId, ctx) - - if err != nil { - util.JSONError(w, util.ErrorParam{Error: "Internal Server Error"}, http.StatusInternalServerError) - return - } - - organizationId, err := apiState.DB.GetOrganizationIdByAuthId(authId, ctx) - - if err != nil { - util.JSONError(w, util.ErrorParam{Error: "Bad request"}, http.StatusBadRequest) - return - } - - newTeamRes, err := apiState.DB.CreateTeam(body.TeamName, organizationId, ctx) - - if err != nil { - util.JSONError( - w, - util.ErrorParam{Error: "Could not create new team"}, - http.StatusInternalServerError, - ) - return - } - - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusOK) - if err := json.NewEncoder(w).Encode(CreateTeamResponse{TeamId: newTeamRes.Id}); err != nil { - fmt.Printf("Issue while formatting response. Error: %s", err.Error()) - util.JSONError( - w, - util.ErrorParam{Error: "Internal Server Error"}, - http.StatusInternalServerError, - ) - return +func CreateTeam(validate *validator.Validate) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + body := &CreateTeamRequestBody{} + + if err := json.NewDecoder(r.Body).Decode(body); err != nil { + fmt.Printf("Issue while parsing body. Error: %s", err.Error()) + util.JSONError(w, util.ErrorParam{Error: "Bad Request"}, http.StatusBadRequest) + return + } + + err := validate.Struct(body) + + if err != nil { + fmt.Printf("Bad request body: %v", err.Error()) + util.JSONError(w, util.ErrorParam{Error: "Bad Request"}, http.StatusBadRequest) + return + } + + authId := ctx.Value(util.AuthIdCtxKey).(string) + + apiState, err := api.InternalApiState(authId, ctx) + + if err != nil { + util.JSONError(w, util.ErrorParam{Error: "Internal Server Error"}, http.StatusInternalServerError) + return + } + + organizationId, err := apiState.DB.GetOrganizationIdByAuthId(authId, ctx) + + if err != nil { + util.JSONError(w, util.ErrorParam{Error: "Bad request"}, http.StatusBadRequest) + return + } + + newTeamRes, err := apiState.DB.CreateTeam(body.TeamName, organizationId, ctx) + + if err != nil { + util.JSONError( + w, + util.ErrorParam{Error: "Could not create new team"}, + http.StatusInternalServerError, + ) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + if err := json.NewEncoder(w).Encode(CreateTeamResponse{TeamId: newTeamRes.Id}); err != nil { + fmt.Printf("Issue while formatting response. Error: %s", err.Error()) + util.JSONError( + w, + util.ErrorParam{Error: "Internal Server Error"}, + http.StatusInternalServerError, + ) + return + } } } diff --git a/internal/internal-api/handler/create_tenant_database.go b/internal/internal-api/handler/create_tenant_database.go index c11d7d86..1ddf9705 100644 --- a/internal/internal-api/handler/create_tenant_database.go +++ b/internal/internal-api/handler/create_tenant_database.go @@ -17,21 +17,21 @@ type CreateDatabaseRequestBody struct { OrganizationName string `json:"organizationName" validate:"required"` } -type TemporalHandler struct { +type OnboardingHandler struct { temporalClient client.Client config onboarding.Config validate *validator.Validate } -func NewTemporalHandler(temporalClient client.Client, config onboarding.Config, validate *validator.Validate) *TemporalHandler { - return &TemporalHandler{ +func NewOnboardingHandler(temporalClient client.Client, config onboarding.Config, validate *validator.Validate) *OnboardingHandler { + return &OnboardingHandler{ temporalClient, config, validate, } } -func (th *TemporalHandler) CreateTenantDB(w http.ResponseWriter, r *http.Request) { +func (th *OnboardingHandler) CreateTenantDB(w http.ResponseWriter, r *http.Request) { ctx := r.Context() body := &CreateDatabaseRequestBody{} diff --git a/internal/internal-api/handler/github_installation.go b/internal/internal-api/handler/github_installation.go index 10078f5d..d435f2f2 100644 --- a/internal/internal-api/handler/github_installation.go +++ b/internal/internal-api/handler/github_installation.go @@ -15,7 +15,7 @@ type GithubInstallationRequestBody struct { DBDomainName string `json:"dbDomainName"` } -func (th *TemporalHandler) GithubInstallation(w http.ResponseWriter, r *http.Request) { +func (th *OnboardingHandler) GithubInstallation(w http.ResponseWriter, r *http.Request) { ctx := r.Context() body := &GithubInstallationRequestBody{} diff --git a/internal/internal-api/handler/users_count.go b/internal/internal-api/handler/users_count.go index 17d99da1..e7a764f1 100644 --- a/internal/internal-api/handler/users_count.go +++ b/internal/internal-api/handler/users_count.go @@ -15,7 +15,7 @@ type UsersCountResponse struct { Count int `json:"count"` } -func (th *TemporalHandler) UsersCount(w http.ResponseWriter, r *http.Request) { +func (th *OnboardingHandler) UsersCount(w http.ResponseWriter, r *http.Request) { out, err := workflow.ExecuteCountUsersWorkflow(r.Context(), th.temporalClient, th.config) if err != nil { log.Fatal(errors.Unwrap(err)) diff --git a/internal/onboarding/workflow/after_github_installation.go b/internal/onboarding/workflow/after_github_installation.go index a21ab23a..72e723e5 100644 --- a/internal/onboarding/workflow/after_github_installation.go +++ b/internal/onboarding/workflow/after_github_installation.go @@ -56,7 +56,6 @@ func AfterGithubInstallationWorkflow( ctx workflow.Context, params AfterGithubInstallationParams, ) (err error) { - if params.InstallationID == 0 || params.AuthID == "" || params.DBURL == "" { err = errors.New("bad request") return From 7b8f6695a6fd90cbe6b1f9e21750ed61d0eb61d1 Mon Sep 17 00:00:00 2001 From: stefanskoricdev Date: Wed, 30 Jul 2025 18:49:08 +0200 Subject: [PATCH 3/7] fix: add github_team reference to dxta team --- .../handler/github_installation.go | 18 ++++++--- internal/internal-api/internal-api.go | 37 ++++++++++++++----- internal/onboarding/activity/github_teams.go | 24 ++++++++++++ .../onboarding/workflow/create_tenant_db.go | 2 +- 4 files changed, 65 insertions(+), 16 deletions(-) diff --git a/internal/internal-api/handler/github_installation.go b/internal/internal-api/handler/github_installation.go index d435f2f2..aa1df1cf 100644 --- a/internal/internal-api/handler/github_installation.go +++ b/internal/internal-api/handler/github_installation.go @@ -5,21 +5,19 @@ import ( "fmt" "net/http" + api "github.com/dxta-dev/app/internal/internal-api" "github.com/dxta-dev/app/internal/onboarding/workflow" "github.com/dxta-dev/app/internal/util" ) type GithubInstallationRequestBody struct { - InstallationID int64 `json:"installationId"` - DBURL string `json:"dbUrl"` - DBDomainName string `json:"dbDomainName"` + InstallationID int64 `json:"installationId"` } func (th *OnboardingHandler) GithubInstallation(w http.ResponseWriter, r *http.Request) { ctx := r.Context() body := &GithubInstallationRequestBody{} - if err := json.NewDecoder(r.Body).Decode(body); err != nil { fmt.Printf("Issue while parsing body. Error: %s", err.Error()) util.JSONError(w, util.ErrorParam{Error: "Bad Request"}, http.StatusBadRequest) @@ -36,12 +34,20 @@ func (th *OnboardingHandler) GithubInstallation(w http.ResponseWriter, r *http.R authId := ctx.Value(util.AuthIdCtxKey).(string) + tenantData, err := api.GetTenantDBDataByAuthId(ctx, authId) + + if err != nil { + fmt.Printf("Failed to retrieve tenant db data: %v", err.Error()) + util.JSONError(w, util.ErrorParam{Error: "Failed to retrieve tenant db data"}, http.StatusInternalServerError) + return + } + _, err = workflow.ExecuteAfterGithubInstallationWorkflow(ctx, th.temporalClient, workflow.ExecuteAfterGithubInstallationParams{ TemporalOnboardingQueueName: th.config.TemporalOnboardingQueueName, AuthID: authId, InstallationID: body.InstallationID, - DBURL: body.DBURL, - DBDomainName: body.DBDomainName, + DBURL: tenantData.DBUrl, + DBDomainName: tenantData.Domain, }) if err != nil { diff --git a/internal/internal-api/internal-api.go b/internal/internal-api/internal-api.go index caa1ed77..f58aee17 100644 --- a/internal/internal-api/internal-api.go +++ b/internal/internal-api/internal-api.go @@ -17,14 +17,17 @@ type State struct { } type TenantDBData struct { - DBUrl string + DBUrl string + Name string + Domain string } var tenantDBURLcache sync.Map -func GetTenantDBUrlByAuthId(ctx context.Context, authID string) (TenantDBData, error) { +func GetTenantDBDataByAuthId(ctx context.Context, authID string) (TenantDBData, error) { if cached, ok := tenantDBURLcache.Load(authID); ok { - return TenantDBData{DBUrl: cached.(string)}, nil + c := cached.(*TenantDBData) + return TenantDBData{DBUrl: c.DBUrl, Name: c.Name, Domain: c.Domain}, nil } driverName := otel.GetDriverName() @@ -47,13 +50,25 @@ func GetTenantDBUrlByAuthId(ctx context.Context, authID string) (TenantDBData, e defer tenantOrganizationMapDB.Close() query := ` - SELECT db_url - FROM tenants - WHERE organization_id = ?;` + SELECT + db_url, + name, + domain + FROM + tenants + WHERE + organization_id = ?;` var tenantData TenantDBData - if err = tenantOrganizationMapDB.QueryRowContext(ctx, query, authID).Scan(&tenantData.DBUrl); err != nil { + if err = tenantOrganizationMapDB. + QueryRowContext(ctx, query, authID). + Scan( + &tenantData.DBUrl, + &tenantData.Name, + &tenantData.Domain, + ); err != nil { + fmt.Printf( "Could not retrieve tenant db url for organization with id: %s. Error: %s", authID, @@ -62,13 +77,17 @@ func GetTenantDBUrlByAuthId(ctx context.Context, authID string) (TenantDBData, e return TenantDBData{}, err } - tenantDBURLcache.Store(authID, tenantData.DBUrl) + tenantDBURLcache.Store(authID, &TenantDBData{ + DBUrl: tenantData.DBUrl, + Name: tenantData.Name, + Domain: tenantData.Domain, + }) return tenantData, nil } func InternalApiState(authId string, ctx context.Context) (State, error) { - tenantData, err := GetTenantDBUrlByAuthId(ctx, authId) + tenantData, err := GetTenantDBDataByAuthId(ctx, authId) if err != nil { return State{}, err diff --git a/internal/onboarding/activity/github_teams.go b/internal/onboarding/activity/github_teams.go index fb32699d..8bff76fc 100644 --- a/internal/onboarding/activity/github_teams.go +++ b/internal/onboarding/activity/github_teams.go @@ -166,6 +166,9 @@ func (ta *TenantActivities) UpsertTeams( } } + caseValues := make([]string, 0) + githubTeamIdValues := make([]string, 0) + if len(values) > 0 { query = fmt.Sprintf(` INSERT INTO teams @@ -197,6 +200,27 @@ func (ta *TenantActivities) UpsertTeams( teamRecord.TeamID = &res.ID (*teamsRecordMap)[res.Name] = teamRecord + + caseValues = append( + caseValues, + fmt.Sprintf("WHEN %d THEN %d", *teamRecord.GithubTeamID, *teamRecord.TeamID), + ) + githubTeamIdValues = append(githubTeamIdValues, fmt.Sprintf("%d", *teamRecord.GithubTeamID)) + } + + query := fmt.Sprintf(` + UPDATE + github_teams + SET team_id = CASE id + %s + END + WHERE id in (%s)`, + strings.Join(caseValues, "\n"), + strings.Join(githubTeamIdValues, ", "), + ) + + if _, err = tx.ExecContext(ctx, query); err != nil { + return nil, errors.New("failed to update team_id in github_teams: " + err.Error()) } } diff --git a/internal/onboarding/workflow/create_tenant_db.go b/internal/onboarding/workflow/create_tenant_db.go index 9e6e7df2..3940bc38 100644 --- a/internal/onboarding/workflow/create_tenant_db.go +++ b/internal/onboarding/workflow/create_tenant_db.go @@ -45,7 +45,7 @@ func CreateTenantDBWorkflow( err = workflow.ExecuteActivity( ctx, (*activity.CreateTenantActivities).CreateTenantDB, - sanitizedDBName, + fmt.Sprintf("%s-tenant", sanitizedDBName), ).Get(ctx, &newDBData) if err != nil { From 82a4a453bbff36ab947c14dc891ff9019a383e90 Mon Sep 17 00:00:00 2001 From: stefanskoricdev Date: Wed, 30 Jul 2025 19:23:43 +0200 Subject: [PATCH 4/7] fix: update workflow id --- internal/onboarding/workflow/after_github_installation.go | 2 +- internal/onboarding/workflow/create_tenant_db.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/onboarding/workflow/after_github_installation.go b/internal/onboarding/workflow/after_github_installation.go index 72e723e5..4ec55d5a 100644 --- a/internal/onboarding/workflow/after_github_installation.go +++ b/internal/onboarding/workflow/after_github_installation.go @@ -261,7 +261,7 @@ func ExecuteAfterGithubInstallationWorkflow( ctx, client.StartWorkflowOptions{ ID: fmt.Sprintf( - "onboarding-workflow-github-%v-%v", + "after-github-installation-workflow-github-%v-%v", params.DBDomainName, params.InstallationID, ), diff --git a/internal/onboarding/workflow/create_tenant_db.go b/internal/onboarding/workflow/create_tenant_db.go index 3940bc38..aced3e8d 100644 --- a/internal/onboarding/workflow/create_tenant_db.go +++ b/internal/onboarding/workflow/create_tenant_db.go @@ -120,7 +120,7 @@ func ExecuteCreateTenantDBWorkflow( ctx, client.StartWorkflowOptions{ ID: fmt.Sprintf( - "onboarding-workflow-github-%v-%v", + "create-tenant-workflow-%v-%v", params.AuthID, params.DBName, ), From 4d6c588918911d52cad2796e9461a183b5863f96 Mon Sep 17 00:00:00 2001 From: stefanskoricdev Date: Wed, 30 Jul 2025 19:31:47 +0200 Subject: [PATCH 5/7] fix: stop workflow if there are no teams in organization --- .../workflow/after_github_installation.go | 40 +++++++++---------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/internal/onboarding/workflow/after_github_installation.go b/internal/onboarding/workflow/after_github_installation.go index 4ec55d5a..cadaf429 100644 --- a/internal/onboarding/workflow/after_github_installation.go +++ b/internal/onboarding/workflow/after_github_installation.go @@ -120,7 +120,7 @@ func AfterGithubInstallationWorkflow( installation.OrganizationLogin, ).Get(ctx, &githubTeams) - if err != nil { + if err != nil || len(githubTeams) == 0 { return } @@ -212,33 +212,31 @@ func AfterGithubInstallationWorkflow( teamsMap, ).Get(ctx, &newGithubMembers) - if err != nil { + if err != nil || len(newGithubMembers) == 0 { return } - if len(newGithubMembers) > 0 { - newMembers := make([]activity.MemberRecord, 0) + newMembers := make([]activity.MemberRecord, 0) - err = workflow.ExecuteActivity( - ctx, - (*activity.TenantActivities).CreateTeamMembers, - params.DBURL, - newGithubMembers, - organizationId, - ).Get(ctx, &newMembers) + err = workflow.ExecuteActivity( + ctx, + (*activity.TenantActivities).CreateTeamMembers, + params.DBURL, + newGithubMembers, + organizationId, + ).Get(ctx, &newMembers) - var joinRes bool + var joinRes bool - err = workflow.ExecuteActivity( - ctx, - (*activity.TenantActivities).JoinTeamsMembers, - params.DBURL, - newMembers, - ).Get(ctx, &joinRes) + err = workflow.ExecuteActivity( + ctx, + (*activity.TenantActivities).JoinTeamsMembers, + params.DBURL, + newMembers, + ).Get(ctx, &joinRes) - if err != nil { - return - } + if err != nil { + return } return From cf320e0aee3a1b5a8f75db1f432af5a3aa25c95a Mon Sep 17 00:00:00 2001 From: stefanskoricdev Date: Thu, 31 Jul 2025 16:50:59 +0200 Subject: [PATCH 6/7] refactor: improve naming --- internal/onboarding/activity/github_teams.go | 32 +++++++++---------- internal/onboarding/activity/teams.go | 6 ++-- .../workflow/after_github_installation.go | 16 +++++----- 3 files changed, 27 insertions(+), 27 deletions(-) diff --git a/internal/onboarding/activity/github_teams.go b/internal/onboarding/activity/github_teams.go index 8bff76fc..c41fa9d3 100644 --- a/internal/onboarding/activity/github_teams.go +++ b/internal/onboarding/activity/github_teams.go @@ -70,7 +70,7 @@ func (ga *GithubActivities) GetExtendedTeamMember( return client.GetExtendedTeamMember(ctx, teamMember) } -type TeamsRecord struct { +type TeamRecord struct { ID *int64 Name *string // ID of a record after insertion to github_teams table @@ -79,15 +79,15 @@ type TeamsRecord struct { TeamID *int64 } -type TeamsRecordMap map[string]TeamsRecord +type TeamsMap map[string]TeamRecord func (ta *TenantActivities) UpsertTeams( ctx context.Context, DBURL string, githubOrganizationId int64, organizationId int64, - teamsRecordMap *TeamsRecordMap, -) (res *TeamsRecordMap, err error) { + teamsMap *TeamsMap, +) (res *TeamsMap, err error) { db, err := ta.GetCachedTenantDB(DBURL, ctx) if err != nil { @@ -109,7 +109,7 @@ func (ta *TenantActivities) UpsertTeams( args := make([]any, 0) values := make([]string, 0) - for _, t := range *teamsRecordMap { + for _, t := range *teamsMap { args = append(args, t.Name, t.ID, githubOrganizationId) values = append(values, "(?, ?, ?)") } @@ -150,7 +150,7 @@ func (ta *TenantActivities) UpsertTeams( return nil, errors.New("failed to scan github team upsert result: " + err.Error()) } - teamRecord, ok := (*teamsRecordMap)[res.Name] + teamRecord, ok := (*teamsMap)[res.Name] if !ok { return nil, errors.New("failed to get a team record from map") @@ -158,7 +158,7 @@ func (ta *TenantActivities) UpsertTeams( teamRecord.GithubTeamID = &res.ID - (*teamsRecordMap)[res.Name] = teamRecord + (*teamsMap)[res.Name] = teamRecord if res.TeamID == nil { args = append(args, res.Name, organizationId) @@ -192,14 +192,14 @@ func (ta *TenantActivities) UpsertTeams( return nil, errors.New("failed to scan team upsert result: " + err.Error()) } - teamRecord, ok := (*teamsRecordMap)[res.Name] + teamRecord, ok := (*teamsMap)[res.Name] if !ok { return nil, errors.New("failed to get a teamRecord from map") } teamRecord.TeamID = &res.ID - (*teamsRecordMap)[res.Name] = teamRecord + (*teamsMap)[res.Name] = teamRecord caseValues = append( caseValues, @@ -228,7 +228,7 @@ func (ta *TenantActivities) UpsertTeams( return nil, err } - return teamsRecordMap, nil + return teamsMap, nil } type MemberRecord struct { @@ -246,14 +246,14 @@ type MemberRecord struct { } } -type MembersRecordMap map[string]MemberRecord +type MembersMap map[string]MemberRecord func (ta *TenantActivities) UpsertGithubMembers( ctx context.Context, DBURL string, - membersMap MembersRecordMap, - teamsRecordMap TeamsRecordMap, -) (res *MembersRecordMap, err error) { + membersMap MembersMap, + teamsMap TeamsMap, +) (res *MembersMap, err error) { db, err := ta.GetCachedTenantDB(DBURL, ctx) if err != nil { @@ -300,7 +300,7 @@ func (ta *TenantActivities) UpsertGithubMembers( return nil, errors.New("failed to upsert github_members: " + err.Error()) } - newMembersMap := MembersRecordMap{} + newMembersMap := MembersMap{} args = make([]any, 0) values = make([]string, 0) @@ -320,7 +320,7 @@ func (ta *TenantActivities) UpsertGithubMembers( } for idx, t := range memberRecord.Teams { - team, ok := teamsRecordMap[*t.Name] + team, ok := teamsMap[*t.Name] t.TeamID = team.TeamID memberRecord.Teams[idx] = t diff --git a/internal/onboarding/activity/teams.go b/internal/onboarding/activity/teams.go index d997a4c3..1226469a 100644 --- a/internal/onboarding/activity/teams.go +++ b/internal/onboarding/activity/teams.go @@ -9,7 +9,7 @@ import ( func (ta *TenantActivities) CreateTeamMembers(ctx context.Context, DBURL string, - members MembersRecordMap, + membersMap MembersMap, organizationID int64) ([]MemberRecord, error) { db, err := ta.GetCachedTenantDB(DBURL, ctx) @@ -33,7 +33,7 @@ func (ta *TenantActivities) CreateTeamMembers(ctx context.Context, values := make([]string, 0) idsToUpdate := make([]string, 0) - for _, member := range members { + for _, member := range membersMap { args = append(args, member.Name, member.Email, member.Login) values = append(values, "(?, ?, ?)") idsToUpdate = append(idsToUpdate, fmt.Sprintf("%d", *member.GithubMemberId)) @@ -67,7 +67,7 @@ func (ta *TenantActivities) CreateTeamMembers(ctx context.Context, return nil, errors.New("failed to scan create member result: " + err.Error()) } - member, ok := members[*res.Username] + member, ok := membersMap[*res.Username] if !ok { return nil, errors.New("failed to get a member record from map") diff --git a/internal/onboarding/workflow/after_github_installation.go b/internal/onboarding/workflow/after_github_installation.go index cadaf429..63ea82b3 100644 --- a/internal/onboarding/workflow/after_github_installation.go +++ b/internal/onboarding/workflow/after_github_installation.go @@ -23,7 +23,7 @@ type AfterGithubInstallationParams struct { func addMemberToMap( team onboarding.Team, member onboarding.ExtendedMember, - membersMap activity.MembersRecordMap, + membersMap activity.MembersMap, ) activity.MemberRecord { m, ok := membersMap[*member.Login] @@ -124,10 +124,10 @@ func AfterGithubInstallationWorkflow( return } - counter := 0 + processedGHTeamsCount := 0 - teamsMap := activity.TeamsRecordMap{} - membersMap := activity.MembersRecordMap{} + teamsMap := activity.TeamsMap{} + membersMap := activity.MembersMap{} for _, team := range githubTeams { workflow.Go(ctx, func(gctx workflow.Context) { @@ -171,7 +171,7 @@ func AfterGithubInstallationWorkflow( addMemberToMap(team, member, membersMap) } - teamsMap[*team.Name] = activity.TeamsRecord{ + teamsMap[*team.Name] = activity.TeamRecord{ ID: team.ID, Name: team.Name, GithubTeamID: nil, @@ -181,12 +181,12 @@ func AfterGithubInstallationWorkflow( // Count number of finished go routines // so we can unblock calling thread when // all go routines finish - counter += 1 + processedGHTeamsCount += 1 }) } _ = workflow.Await(ctx, func() bool { - return err != nil || counter == len(githubTeams) + return err != nil || processedGHTeamsCount == len(githubTeams) }) err = workflow.ExecuteActivity( @@ -202,7 +202,7 @@ func AfterGithubInstallationWorkflow( return } - var newGithubMembers activity.MembersRecordMap + var newGithubMembers activity.MembersMap err = workflow.ExecuteActivity( ctx, From fdaee6ea453630956068ce9c55e2db836a11aeec Mon Sep 17 00:00:00 2001 From: stefanskoricdev Date: Fri, 1 Aug 2025 17:54:02 +0200 Subject: [PATCH 7/7] fix: validate team name --- internal/internal-api/handler/create_team.go | 4 ++-- internal/onboarding/activity/github_teams.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/internal-api/handler/create_team.go b/internal/internal-api/handler/create_team.go index d75c43d7..a6ca8a67 100644 --- a/internal/internal-api/handler/create_team.go +++ b/internal/internal-api/handler/create_team.go @@ -11,11 +11,11 @@ import ( ) type CreateTeamRequestBody struct { - TeamName string `json:"teamName"` + TeamName string `json:"teamName" validate:"required"` } type CreateTeamResponse struct { - TeamId int64 `json:"team_id" validate:"required"` + TeamId int64 `json:"team_id"` } func CreateTeam(validate *validator.Validate) http.HandlerFunc { diff --git a/internal/onboarding/activity/github_teams.go b/internal/onboarding/activity/github_teams.go index c41fa9d3..98b215d3 100644 --- a/internal/onboarding/activity/github_teams.go +++ b/internal/onboarding/activity/github_teams.go @@ -214,7 +214,7 @@ func (ta *TenantActivities) UpsertTeams( SET team_id = CASE id %s END - WHERE id in (%s)`, + WHERE id IN (%s)`, strings.Join(caseValues, "\n"), strings.Join(githubTeamIdValues, ", "), )