You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@devlake.apache.org by wa...@apache.org on 2023/03/27 05:57:50 UTC

[incubator-devlake] branch main updated: feat: add connection id to transformation rules (#4775)

This is an automated email from the ASF dual-hosted git repository.

warren pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-devlake.git


The following commit(s) were added to refs/heads/main by this push:
     new 2571e57f6 feat: add connection id to transformation rules (#4775)
2571e57f6 is described below

commit 2571e57f61fcfadf9f33dc8063cf1dd167d518a5
Author: Liang Zhang <li...@merico.dev>
AuthorDate: Mon Mar 27 13:57:45 2023 +0800

    feat: add connection id to transformation rules (#4775)
    
    * feat: add connection id to transformation rules
    
    * fix: gofmt
---
 .../pluginhelper/api/transformation_rule_helper.go | 21 +++++-
 backend/plugins/bamboo/api/transformation_rule.go  | 12 ++--
 backend/plugins/bamboo/impl/impl.go                |  4 +-
 ...22_add_connection_id_to_transformation_rules.go | 73 +++++++++++++++++++++
 .../bamboo/models/migrationscripts/register.go     |  1 +
 .../plugins/bamboo/models/transformation_rule.go   |  3 +-
 .../plugins/bitbucket/api/transformation_rule.go   | 12 ++--
 backend/plugins/bitbucket/impl/impl.go             |  4 +-
 ...22_add_connection_id_to_transformation_rules.go | 74 ++++++++++++++++++++++
 .../bitbucket/models/migrationscripts/register.go  |  1 +
 .../bitbucket/models/transformation_rule.go        |  1 +
 backend/plugins/github/api/transformation_rule.go  | 12 ++--
 backend/plugins/github/impl/impl.go                |  4 +-
 ...22_add_connection_id_to_transformation_rules.go | 74 ++++++++++++++++++++++
 .../github/models/migrationscripts/register.go     |  1 +
 .../plugins/github/models/transformation_rule.go   |  1 +
 backend/plugins/gitlab/api/transformation_rule.go  | 12 ++--
 backend/plugins/gitlab/impl/impl.go                |  4 +-
 ...22_add_connection_id_to_transformation_rules.go | 74 ++++++++++++++++++++++
 .../gitlab/models/migrationscripts/register.go     |  1 +
 .../plugins/gitlab/models/transformation_rule.go   |  1 +
 backend/plugins/jenkins/api/transformation_rule.go | 12 ++--
 backend/plugins/jenkins/impl/impl.go               |  4 +-
 ...22_add_connection_id_to_transformation_rules.go | 74 ++++++++++++++++++++++
 .../jenkins/models/migrationscripts/register.go    |  1 +
 .../plugins/jenkins/models/transformation_rule.go  |  1 +
 backend/plugins/jira/api/transformation_rule.go    | 62 +++++++++++++-----
 backend/plugins/jira/impl/impl.go                  |  4 +-
 ...22_add_connection_id_to_transformation_rules.go | 74 ++++++++++++++++++++++
 .../jira/models/migrationscripts/register.go       |  1 +
 .../plugins/jira/models/transformation_rules.go    |  1 +
 backend/plugins/jira/tasks/task_data.go            |  3 +
 32 files changed, 576 insertions(+), 51 deletions(-)

diff --git a/backend/helpers/pluginhelper/api/transformation_rule_helper.go b/backend/helpers/pluginhelper/api/transformation_rule_helper.go
index 645cc1385..003982218 100644
--- a/backend/helpers/pluginhelper/api/transformation_rule_helper.go
+++ b/backend/helpers/pluginhelper/api/transformation_rule_helper.go
@@ -18,6 +18,10 @@ limitations under the License.
 package api
 
 import (
+	"net/http"
+	"reflect"
+	"strconv"
+
 	"github.com/apache/incubator-devlake/core/context"
 	"github.com/apache/incubator-devlake/core/dal"
 	"github.com/apache/incubator-devlake/core/errors"
@@ -25,8 +29,6 @@ import (
 	"github.com/apache/incubator-devlake/core/plugin"
 	"github.com/go-playground/validator/v10"
 	"github.com/mitchellh/mapstructure"
-	"net/http"
-	"strconv"
 )
 
 // TransformationRuleHelper is used to write the CURD of transformation rule
@@ -52,11 +54,20 @@ func NewTransformationRuleHelper[Tr dal.Tabler](
 }
 
 func (t TransformationRuleHelper[Tr]) Create(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
+	connectionId, e := strconv.ParseUint(input.Params["connectionId"], 10, 64)
+	if e != nil || connectionId == 0 {
+		return nil, errors.Default.Wrap(e, "the connection ID should be an non-zero integer")
+	}
 	var rule Tr
 	err := Decode(input.Body, &rule, t.validator)
 	if err != nil {
 		return nil, errors.BadInput.Wrap(err, "error in decoding transformation rule")
 	}
+	valueConnectionId := reflect.ValueOf(&rule).Elem().FieldByName("ConnectionId")
+	if valueConnectionId.IsValid() {
+		valueConnectionId.SetUint(connectionId)
+	}
+
 	err = t.db.Create(&rule)
 	if err != nil {
 		if t.db.IsDuplicationError(err) {
@@ -105,9 +116,13 @@ func (t TransformationRuleHelper[Tr]) Get(input *plugin.ApiResourceInput) (*plug
 }
 
 func (t TransformationRuleHelper[Tr]) List(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
+	connectionId, e := strconv.ParseUint(input.Params["connectionId"], 10, 64)
+	if e != nil || connectionId == 0 {
+		return nil, errors.Default.Wrap(e, "the connection ID should be an non-zero integer")
+	}
 	var rules []Tr
 	limit, offset := GetLimitOffset(input.Query, "pageSize", "page")
-	err := t.db.All(&rules, dal.Limit(limit), dal.Offset(offset))
+	err := t.db.All(&rules, dal.Where("connection_id = ?", connectionId), dal.Limit(limit), dal.Offset(offset))
 	if err != nil {
 		return nil, errors.Default.Wrap(err, "error on get TransformationRule list")
 	}
diff --git a/backend/plugins/bamboo/api/transformation_rule.go b/backend/plugins/bamboo/api/transformation_rule.go
index 8e833e9c2..99e6812e5 100644
--- a/backend/plugins/bamboo/api/transformation_rule.go
+++ b/backend/plugins/bamboo/api/transformation_rule.go
@@ -27,11 +27,12 @@ import (
 // @Description create transformation rule for Bamboo
 // @Tags plugins/bamboo
 // @Accept application/json
+// @Param connectionId path int true "connectionId"
 // @Param transformationRule body models.BambooTransformationRule true "transformation rule"
 // @Success 200  {object} models.BambooTransformationRule
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
-// @Router /plugins/bamboo/transformation_rules [POST]
+// @Router /plugins/bamboo/connections/{connectionId}/transformation_rules [POST]
 func CreateTransformationRule(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
 	return trHelper.Create(input)
 }
@@ -43,10 +44,11 @@ func CreateTransformationRule(input *plugin.ApiResourceInput) (*plugin.ApiResour
 // @Accept application/json
 // @Param id path int true "id"
 // @Param transformationRule body models.BambooTransformationRule true "transformation rule"
+// @Param connectionId path int true "connectionId"
 // @Success 200  {object} models.BambooTransformationRule
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
-// @Router /plugins/bamboo/transformation_rules/{id} [PATCH]
+// @Router /plugins/bamboo/connections/{connectionId}/transformation_rules/{id} [PATCH]
 func UpdateTransformationRule(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
 	return trHelper.Update(input)
 }
@@ -56,10 +58,11 @@ func UpdateTransformationRule(input *plugin.ApiResourceInput) (*plugin.ApiResour
 // @Description return one transformation rule
 // @Tags plugins/bamboo
 // @Param id path int true "id"
+// @Param connectionId path int true "connectionId"
 // @Success 200  {object} models.BambooTransformationRule
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
-// @Router /plugins/bamboo/transformation_rules/{id} [GET]
+// @Router /plugins/bamboo/connections/{connectionId}/transformation_rules/{id} [GET]
 func GetTransformationRule(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
 	return trHelper.Get(input)
 }
@@ -70,10 +73,11 @@ func GetTransformationRule(input *plugin.ApiResourceInput) (*plugin.ApiResourceO
 // @Tags plugins/bamboo
 // @Param pageSize query int false "page size, default 50"
 // @Param page query int false "page size, default 1"
+// @Param connectionId path int true "connectionId"
 // @Success 200  {object} []models.BambooTransformationRule
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
-// @Router /plugins/bamboo/transformation_rules [GET]
+// @Router /plugins/bamboo/connections/{connectionId}/transformation_rules [GET]
 func GetTransformationRuleList(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
 	return trHelper.List(input)
 }
diff --git a/backend/plugins/bamboo/impl/impl.go b/backend/plugins/bamboo/impl/impl.go
index 479cebd58..f7b9f4c94 100644
--- a/backend/plugins/bamboo/impl/impl.go
+++ b/backend/plugins/bamboo/impl/impl.go
@@ -198,11 +198,11 @@ func (p Bamboo) ApiResources() map[string]map[string]plugin.ApiResourceHandler {
 			"PATCH":  api.PatchConnection,
 			"DELETE": api.DeleteConnection,
 		},
-		"transformation_rules": {
+		"connections/:connectionId/transformation_rules": {
 			"POST": api.CreateTransformationRule,
 			"GET":  api.GetTransformationRuleList,
 		},
-		"transformation_rules/:id": {
+		"connections/:connectionId/transformation_rules/:id": {
 			"PATCH": api.UpdateTransformationRule,
 			"GET":   api.GetTransformationRule,
 		},
diff --git a/backend/plugins/bamboo/models/migrationscripts/20230322_add_connection_id_to_transformation_rules.go b/backend/plugins/bamboo/models/migrationscripts/20230322_add_connection_id_to_transformation_rules.go
new file mode 100644
index 000000000..a72397df0
--- /dev/null
+++ b/backend/plugins/bamboo/models/migrationscripts/20230322_add_connection_id_to_transformation_rules.go
@@ -0,0 +1,73 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package migrationscripts
+
+import (
+	"github.com/apache/incubator-devlake/core/context"
+	"github.com/apache/incubator-devlake/core/dal"
+	"github.com/apache/incubator-devlake/core/errors"
+	"github.com/apache/incubator-devlake/helpers/migrationhelper"
+	"github.com/apache/incubator-devlake/plugins/bamboo/models"
+)
+
+type addConnectionIdToTransformationRule struct{}
+
+type transformationRule20220322 struct {
+	ConnectionId uint64
+}
+
+func (transformationRule20220322) TableName() string {
+	return "_tool_bamboo_transformation_rules"
+}
+func (u *addConnectionIdToTransformationRule) Up(baseRes context.BasicRes) errors.Error {
+	err := migrationhelper.AutoMigrateTables(baseRes, &transformationRule20220322{})
+	if err != nil {
+		return err
+	}
+	var scopes []models.BambooProject
+	err = baseRes.GetDal().All(&scopes)
+	if err != nil {
+		return err
+	}
+	// get all rules that are not referenced.
+	idMap := make(map[uint64]uint64)
+	for _, scope := range scopes {
+		if idMap[scope.TransformationRuleId] == 0 {
+			idMap[scope.TransformationRuleId] = scope.ConnectionId
+		}
+	}
+	// set connection_id for rules
+	for trId, cId := range idMap {
+		err = baseRes.GetDal().UpdateColumn(
+			&models.BambooTransformationRule{}, "connection_id", cId,
+			dal.Where("id = ?", trId))
+		if err != nil {
+			return err
+		}
+	}
+	// delete all rules that are not referenced.
+	return baseRes.GetDal().Delete(&models.BambooTransformationRule{}, dal.Where("connection_id = ? OR connection_id = ?", nil, 0))
+}
+
+func (*addConnectionIdToTransformationRule) Version() uint64 {
+	return 20230322150357
+}
+
+func (*addConnectionIdToTransformationRule) Name() string {
+	return "add connection_id to _tool_bamboo_transformation_rules"
+}
diff --git a/backend/plugins/bamboo/models/migrationscripts/register.go b/backend/plugins/bamboo/models/migrationscripts/register.go
index ec054748c..d6c907212 100644
--- a/backend/plugins/bamboo/models/migrationscripts/register.go
+++ b/backend/plugins/bamboo/models/migrationscripts/register.go
@@ -25,5 +25,6 @@ import (
 func All() []plugin.MigrationScript {
 	return []plugin.MigrationScript{
 		new(addInitTables),
+		new(addConnectionIdToTransformationRule),
 	}
 }
diff --git a/backend/plugins/bamboo/models/transformation_rule.go b/backend/plugins/bamboo/models/transformation_rule.go
index 2a6bca0cd..d4be4e9a5 100644
--- a/backend/plugins/bamboo/models/transformation_rule.go
+++ b/backend/plugins/bamboo/models/transformation_rule.go
@@ -24,7 +24,8 @@ import (
 
 type BambooTransformationRule struct {
 	common.Model
-	Name string `gorm:"type:varchar(255);index:idx_name_gitlab,unique" validate:"required" mapstructure:"name" json:"name"`
+	ConnectionId uint64 `mapstructure:"connectionId" json:"connectionId"`
+	Name         string `gorm:"type:varchar(255);index:idx_name_gitlab,unique" validate:"required" mapstructure:"name" json:"name"`
 	// should be {realRepoName: [bamboo_repoId]}
 	RepoMap           datatypes.JSONMap
 	DeploymentPattern string `mapstructure:"deploymentPattern,omitempty" json:"deploymentPattern" gorm:"type:varchar(255)"`
diff --git a/backend/plugins/bitbucket/api/transformation_rule.go b/backend/plugins/bitbucket/api/transformation_rule.go
index 9b37da739..922fbdb2f 100644
--- a/backend/plugins/bitbucket/api/transformation_rule.go
+++ b/backend/plugins/bitbucket/api/transformation_rule.go
@@ -27,11 +27,12 @@ import (
 // @Description create transformation rule for Bitbucket
 // @Tags plugins/bitbucket
 // @Accept application/json
+// @Param connectionId path int true "connectionId"
 // @Param transformationRule body models.BitbucketTransformationRule true "transformation rule"
 // @Success 200  {object} models.BitbucketTransformationRule
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
-// @Router /plugins/bitbucket/transformation_rules [POST]
+// @Router /plugins/bitbucket/connections/{connectionId}/transformation_rules [POST]
 func CreateTransformationRule(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
 	return trHelper.Create(input)
 }
@@ -42,11 +43,12 @@ func CreateTransformationRule(input *plugin.ApiResourceInput) (*plugin.ApiResour
 // @Tags plugins/bitbucket
 // @Accept application/json
 // @Param id path int true "id"
+// @Param connectionId path int true "connectionId"
 // @Param transformationRule body models.BitbucketTransformationRule true "transformation rule"
 // @Success 200  {object} models.BitbucketTransformationRule
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
-// @Router /plugins/bitbucket/transformation_rules/{id} [PATCH]
+// @Router /plugins/bitbucket/connections/{connectionId}/transformation_rules/{id} [PATCH]
 func UpdateTransformationRule(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
 	return trHelper.Update(input)
 }
@@ -56,10 +58,11 @@ func UpdateTransformationRule(input *plugin.ApiResourceInput) (*plugin.ApiResour
 // @Description return one transformation rule
 // @Tags plugins/bitbucket
 // @Param id path int true "id"
+// @Param connectionId path int true "connectionId"
 // @Success 200  {object} models.BitbucketTransformationRule
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
-// @Router /plugins/bitbucket/transformation_rules/{id} [GET]
+// @Router /plugins/bitbucket/connections/{connectionId}/transformation_rules/{id} [GET]
 func GetTransformationRule(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
 	return trHelper.Get(input)
 }
@@ -68,12 +71,13 @@ func GetTransformationRule(input *plugin.ApiResourceInput) (*plugin.ApiResourceO
 // @Summary return all transformation rules
 // @Description return all transformation rules
 // @Tags plugins/bitbucket
+// @Param connectionId path int true "connectionId"
 // @Param pageSize query int false "page size, default 50"
 // @Param page query int false "page size, default 1"
 // @Success 200  {object} []models.BitbucketTransformationRule
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
-// @Router /plugins/bitbucket/transformation_rules [GET]
+// @Router /plugins/bitbucket/connections/{connectionId}/transformation_rules [GET]
 func GetTransformationRuleList(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
 	return trHelper.List(input)
 }
diff --git a/backend/plugins/bitbucket/impl/impl.go b/backend/plugins/bitbucket/impl/impl.go
index 45019017a..c1eb862b3 100644
--- a/backend/plugins/bitbucket/impl/impl.go
+++ b/backend/plugins/bitbucket/impl/impl.go
@@ -207,11 +207,11 @@ func (p Bitbucket) ApiResources() map[string]map[string]plugin.ApiResourceHandle
 			"GET": api.GetScopeList,
 			"PUT": api.PutScope,
 		},
-		"transformation_rules": {
+		"connections/:connectionId/transformation_rules": {
 			"POST": api.CreateTransformationRule,
 			"GET":  api.GetTransformationRuleList,
 		},
-		"transformation_rules/:id": {
+		"connections/:connectionId/transformation_rules/:id": {
 			"PATCH": api.UpdateTransformationRule,
 			"GET":   api.GetTransformationRule,
 		},
diff --git a/backend/plugins/bitbucket/models/migrationscripts/20230322_add_connection_id_to_transformation_rules.go b/backend/plugins/bitbucket/models/migrationscripts/20230322_add_connection_id_to_transformation_rules.go
new file mode 100644
index 000000000..c05d46a00
--- /dev/null
+++ b/backend/plugins/bitbucket/models/migrationscripts/20230322_add_connection_id_to_transformation_rules.go
@@ -0,0 +1,74 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package migrationscripts
+
+import (
+	"github.com/apache/incubator-devlake/core/context"
+	"github.com/apache/incubator-devlake/core/dal"
+	"github.com/apache/incubator-devlake/core/errors"
+	"github.com/apache/incubator-devlake/helpers/migrationhelper"
+	"github.com/apache/incubator-devlake/plugins/bitbucket/models"
+)
+
+type addConnectionIdToTransformationRule struct{}
+
+type transformationRule20220322 struct {
+	ConnectionId uint64
+}
+
+func (transformationRule20220322) TableName() string {
+	return "_tool_bitbucket_transformation_rules"
+}
+
+func (u *addConnectionIdToTransformationRule) Up(baseRes context.BasicRes) errors.Error {
+	err := migrationhelper.AutoMigrateTables(baseRes, &transformationRule20220322{})
+	if err != nil {
+		return err
+	}
+	var scopes []models.BitbucketRepo
+	err = baseRes.GetDal().All(&scopes)
+	if err != nil {
+		return err
+	}
+	// get all rules that are not referenced.
+	idMap := make(map[uint64]uint64)
+	for _, scope := range scopes {
+		if scope.TransformationRuleId > 0 && idMap[scope.TransformationRuleId] == 0 {
+			idMap[scope.TransformationRuleId] = scope.ConnectionId
+		}
+	}
+	// set connection_id for rules
+	for trId, cId := range idMap {
+		err = baseRes.GetDal().UpdateColumn(
+			&models.BitbucketTransformationRule{}, "connection_id", cId,
+			dal.Where("id = ?", trId))
+		if err != nil {
+			return err
+		}
+	}
+	// delete all rules that are not referenced.
+	return baseRes.GetDal().Delete(&models.BitbucketTransformationRule{}, dal.Where("connection_id IS NULL OR connection_id = 0"))
+}
+
+func (*addConnectionIdToTransformationRule) Version() uint64 {
+	return 20230322150357
+}
+
+func (*addConnectionIdToTransformationRule) Name() string {
+	return "add connection_id to _tool_bitbucket_transformation_rules"
+}
diff --git a/backend/plugins/bitbucket/models/migrationscripts/register.go b/backend/plugins/bitbucket/models/migrationscripts/register.go
index bff36d850..27a5f1fa7 100644
--- a/backend/plugins/bitbucket/models/migrationscripts/register.go
+++ b/backend/plugins/bitbucket/models/migrationscripts/register.go
@@ -31,5 +31,6 @@ func All() []plugin.MigrationScript {
 		new(addRepoIdAndCommitShaField20221014),
 		new(addScope20230206),
 		new(addPipelineStep20230215),
+		new(addConnectionIdToTransformationRule),
 	}
 }
diff --git a/backend/plugins/bitbucket/models/transformation_rule.go b/backend/plugins/bitbucket/models/transformation_rule.go
index 26a622239..dd70788ba 100644
--- a/backend/plugins/bitbucket/models/transformation_rule.go
+++ b/backend/plugins/bitbucket/models/transformation_rule.go
@@ -24,6 +24,7 @@ import (
 
 type BitbucketTransformationRule struct {
 	common.Model      `mapstructure:"-"`
+	ConnectionId      uint64            `mapstructure:"connectionId" json:"connectionId"`
 	Name              string            `mapstructure:"name" json:"name" gorm:"type:varchar(255);index:idx_name_github,unique" validate:"required"`
 	DeploymentPattern string            `mapstructure:"deploymentPattern,omitempty" json:"deploymentPattern" gorm:"type:varchar(255)"`
 	ProductionPattern string            `mapstructure:"productionPattern,omitempty" json:"productionPattern" gorm:"type:varchar(255)"`
diff --git a/backend/plugins/github/api/transformation_rule.go b/backend/plugins/github/api/transformation_rule.go
index f064e675a..89a86bd12 100644
--- a/backend/plugins/github/api/transformation_rule.go
+++ b/backend/plugins/github/api/transformation_rule.go
@@ -27,11 +27,12 @@ import (
 // @Description create transformation rule for Github
 // @Tags plugins/github
 // @Accept application/json
+// @Param connectionId path int true "connectionId"
 // @Param transformationRule body models.GithubTransformationRule true "transformation rule"
 // @Success 200  {object} models.GithubTransformationRule
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
-// @Router /plugins/github/transformation_rules [POST]
+// @Router /plugins/github/connections/{connectionId}/transformation_rules [POST]
 func CreateTransformationRule(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
 	return trHelper.Create(input)
 }
@@ -42,11 +43,12 @@ func CreateTransformationRule(input *plugin.ApiResourceInput) (*plugin.ApiResour
 // @Tags plugins/github
 // @Accept application/json
 // @Param id path int true "id"
+// @Param connectionId path int true "connectionId"
 // @Param transformationRule body models.GithubTransformationRule true "transformation rule"
 // @Success 200  {object} models.GithubTransformationRule
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
-// @Router /plugins/github/transformation_rules/{id} [PATCH]
+// @Router /plugins/github/connections/{connectionId}/transformation_rules/{id} [PATCH]
 func UpdateTransformationRule(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
 	return trHelper.Update(input)
 }
@@ -56,10 +58,11 @@ func UpdateTransformationRule(input *plugin.ApiResourceInput) (*plugin.ApiResour
 // @Description return one transformation rule
 // @Tags plugins/github
 // @Param id path int true "id"
+// @Param connectionId path int true "connectionId"
 // @Success 200  {object} models.GithubTransformationRule
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
-// @Router /plugins/github/transformation_rules/{id} [GET]
+// @Router /plugins/github/connections/{connectionId}/transformation_rules/{id} [GET]
 func GetTransformationRule(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
 	return trHelper.Get(input)
 }
@@ -70,10 +73,11 @@ func GetTransformationRule(input *plugin.ApiResourceInput) (*plugin.ApiResourceO
 // @Tags plugins/github
 // @Param pageSize query int false "page size, default 50"
 // @Param page query int false "page size, default 1"
+// @Param connectionId path int true "connectionId"
 // @Success 200  {object} []models.GithubTransformationRule
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
-// @Router /plugins/github/transformation_rules [GET]
+// @Router /plugins/github/connections/{connectionId}/transformation_rules [GET]
 func GetTransformationRuleList(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
 	return trHelper.List(input)
 }
diff --git a/backend/plugins/github/impl/impl.go b/backend/plugins/github/impl/impl.go
index 71dd2dcea..d438632f2 100644
--- a/backend/plugins/github/impl/impl.go
+++ b/backend/plugins/github/impl/impl.go
@@ -212,11 +212,11 @@ func (p Github) ApiResources() map[string]map[string]plugin.ApiResourceHandler {
 			"GET": api.GetScopeList,
 			"PUT": api.PutScope,
 		},
-		"transformation_rules": {
+		"connections/:connectionId/transformation_rules": {
 			"POST": api.CreateTransformationRule,
 			"GET":  api.GetTransformationRuleList,
 		},
-		"transformation_rules/:id": {
+		"connections/:connectionId/transformation_rules/:id": {
 			"PATCH": api.UpdateTransformationRule,
 			"GET":   api.GetTransformationRule,
 		},
diff --git a/backend/plugins/github/models/migrationscripts/20230322_add_connection_id_to_transformation_rules.go b/backend/plugins/github/models/migrationscripts/20230322_add_connection_id_to_transformation_rules.go
new file mode 100644
index 000000000..61db54ce7
--- /dev/null
+++ b/backend/plugins/github/models/migrationscripts/20230322_add_connection_id_to_transformation_rules.go
@@ -0,0 +1,74 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package migrationscripts
+
+import (
+	"github.com/apache/incubator-devlake/core/context"
+	"github.com/apache/incubator-devlake/core/dal"
+	"github.com/apache/incubator-devlake/core/errors"
+	"github.com/apache/incubator-devlake/helpers/migrationhelper"
+	"github.com/apache/incubator-devlake/plugins/github/models"
+)
+
+type addConnectionIdToTransformationRule struct{}
+
+type transformationRule20220322 struct {
+	ConnectionId uint64
+}
+
+func (transformationRule20220322) TableName() string {
+	return "_tool_github_transformation_rules"
+}
+
+func (u *addConnectionIdToTransformationRule) Up(baseRes context.BasicRes) errors.Error {
+	err := migrationhelper.AutoMigrateTables(baseRes, &transformationRule20220322{})
+	if err != nil {
+		return err
+	}
+	var scopes []models.GithubRepo
+	err = baseRes.GetDal().All(&scopes)
+	if err != nil {
+		return err
+	}
+	// get all rules that are not referenced.
+	idMap := make(map[uint64]uint64)
+	for _, scope := range scopes {
+		if scope.TransformationRuleId > 0 && idMap[scope.TransformationRuleId] == 0 {
+			idMap[scope.TransformationRuleId] = scope.ConnectionId
+		}
+	}
+	// set connection_id for rules
+	for trId, cId := range idMap {
+		err = baseRes.GetDal().UpdateColumn(
+			&models.GithubTransformationRule{}, "connection_id", cId,
+			dal.Where("id = ?", trId))
+		if err != nil {
+			return err
+		}
+	}
+	// delete all rules that are not referenced.
+	return baseRes.GetDal().Delete(&models.GithubTransformationRule{}, dal.Where("connection_id IS NULL OR connection_id = 0"))
+}
+
+func (*addConnectionIdToTransformationRule) Version() uint64 {
+	return 20230322150357
+}
+
+func (*addConnectionIdToTransformationRule) Name() string {
+	return "add connection_id to _tool_github_transformation_rules"
+}
diff --git a/backend/plugins/github/models/migrationscripts/register.go b/backend/plugins/github/models/migrationscripts/register.go
index 9097ca55a..85a1859e7 100644
--- a/backend/plugins/github/models/migrationscripts/register.go
+++ b/backend/plugins/github/models/migrationscripts/register.go
@@ -34,5 +34,6 @@ func All() []plugin.MigrationScript {
 		new(addTransformationRule20221124),
 		new(concatOwnerAndName),
 		new(addStdTypeToIssue221230),
+		new(addConnectionIdToTransformationRule),
 	}
 }
diff --git a/backend/plugins/github/models/transformation_rule.go b/backend/plugins/github/models/transformation_rule.go
index 0bcc6fb22..d7ecb35df 100644
--- a/backend/plugins/github/models/transformation_rule.go
+++ b/backend/plugins/github/models/transformation_rule.go
@@ -24,6 +24,7 @@ import (
 
 type GithubTransformationRule struct {
 	common.Model         `mapstructure:"-"`
+	ConnectionId         uint64            `mapstructure:"connectionId" json:"connectionId"`
 	Name                 string            `mapstructure:"name" json:"name" gorm:"type:varchar(255);index:idx_name_github,unique" validate:"required"`
 	PrType               string            `mapstructure:"prType,omitempty" json:"prType" gorm:"type:varchar(255)"`
 	PrComponent          string            `mapstructure:"prComponent,omitempty" json:"prComponent" gorm:"type:varchar(255)"`
diff --git a/backend/plugins/gitlab/api/transformation_rule.go b/backend/plugins/gitlab/api/transformation_rule.go
index 10cfec795..73de6c296 100644
--- a/backend/plugins/gitlab/api/transformation_rule.go
+++ b/backend/plugins/gitlab/api/transformation_rule.go
@@ -27,11 +27,12 @@ import (
 // @Description create transformation rule for Gitlab
 // @Tags plugins/gitlab
 // @Accept application/json
+// @Param connectionId path int true "connectionId"
 // @Param transformationRule body models.GitlabTransformationRule true "transformation rule"
 // @Success 200  {object} models.GitlabTransformationRule
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
-// @Router /plugins/gitlab/transformation_rules [POST]
+// @Router /plugins/gitlab/connections/{connectionId}/transformation_rules [POST]
 func CreateTransformationRule(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
 	return trHelper.Create(input)
 }
@@ -42,11 +43,12 @@ func CreateTransformationRule(input *plugin.ApiResourceInput) (*plugin.ApiResour
 // @Tags plugins/gitlab
 // @Accept application/json
 // @Param id path int true "id"
+// @Param connectionId path int true "connectionId"
 // @Param transformationRule body models.GitlabTransformationRule true "transformation rule"
 // @Success 200  {object} models.GitlabTransformationRule
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
-// @Router /plugins/gitlab/transformation_rules/{id} [PATCH]
+// @Router /plugins/gitlab/connections/{connectionId}/transformation_rules/{id} [PATCH]
 func UpdateTransformationRule(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
 	return trHelper.Update(input)
 }
@@ -56,10 +58,11 @@ func UpdateTransformationRule(input *plugin.ApiResourceInput) (*plugin.ApiResour
 // @Description return one transformation rule
 // @Tags plugins/gitlab
 // @Param id path int true "id"
+// @Param connectionId path int true "connectionId"
 // @Success 200  {object} models.GitlabTransformationRule
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
-// @Router /plugins/gitlab/transformation_rules/{id} [GET]
+// @Router /plugins/gitlab/connections/{connectionId}/transformation_rules/{id} [GET]
 func GetTransformationRule(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
 	return trHelper.Get(input)
 }
@@ -68,12 +71,13 @@ func GetTransformationRule(input *plugin.ApiResourceInput) (*plugin.ApiResourceO
 // @Summary return all transformation rules
 // @Description return all transformation rules
 // @Tags plugins/gitlab
+// @Param connectionId path int true "connectionId"
 // @Param pageSize query int false "page size, default 50"
 // @Param page query int false "page size, default 1"
 // @Success 200  {object} []models.GitlabTransformationRule
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
-// @Router /plugins/gitlab/transformation_rules [GET]
+// @Router /plugins/gitlab/connections/{connectionId}/transformation_rules [GET]
 func GetTransformationRuleList(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
 	return trHelper.List(input)
 }
diff --git a/backend/plugins/gitlab/impl/impl.go b/backend/plugins/gitlab/impl/impl.go
index aa1f38d67..561538ae5 100644
--- a/backend/plugins/gitlab/impl/impl.go
+++ b/backend/plugins/gitlab/impl/impl.go
@@ -261,11 +261,11 @@ func (p Gitlab) ApiResources() map[string]map[string]plugin.ApiResourceHandler {
 			"GET": api.GetScopeList,
 			"PUT": api.PutScope,
 		},
-		"transformation_rules": {
+		"connections/:connectionId/transformation_rules": {
 			"POST": api.CreateTransformationRule,
 			"GET":  api.GetTransformationRuleList,
 		},
-		"transformation_rules/:id": {
+		"connections/:connectionId/transformation_rules/:id": {
 			"PATCH": api.UpdateTransformationRule,
 			"GET":   api.GetTransformationRule,
 		},
diff --git a/backend/plugins/gitlab/models/migrationscripts/20230322_add_connection_id_to_transformation_rules.go b/backend/plugins/gitlab/models/migrationscripts/20230322_add_connection_id_to_transformation_rules.go
new file mode 100644
index 000000000..4f710c71a
--- /dev/null
+++ b/backend/plugins/gitlab/models/migrationscripts/20230322_add_connection_id_to_transformation_rules.go
@@ -0,0 +1,74 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package migrationscripts
+
+import (
+	"github.com/apache/incubator-devlake/core/context"
+	"github.com/apache/incubator-devlake/core/dal"
+	"github.com/apache/incubator-devlake/core/errors"
+	"github.com/apache/incubator-devlake/helpers/migrationhelper"
+	"github.com/apache/incubator-devlake/plugins/gitlab/models"
+)
+
+type addConnectionIdToTransformationRule struct{}
+
+type transformationRule20220322 struct {
+	ConnectionId uint64
+}
+
+func (transformationRule20220322) TableName() string {
+	return "_tool_gitlab_transformation_rules"
+}
+
+func (u *addConnectionIdToTransformationRule) Up(baseRes context.BasicRes) errors.Error {
+	err := migrationhelper.AutoMigrateTables(baseRes, &transformationRule20220322{})
+	if err != nil {
+		return err
+	}
+	var scopes []models.GitlabProject
+	err = baseRes.GetDal().All(&scopes)
+	if err != nil {
+		return err
+	}
+	// get all rules that are not referenced.
+	idMap := make(map[uint64]uint64)
+	for _, scope := range scopes {
+		if scope.TransformationRuleId > 0 && idMap[scope.TransformationRuleId] == 0 {
+			idMap[scope.TransformationRuleId] = scope.ConnectionId
+		}
+	}
+	// set connection_id for rules
+	for trId, cId := range idMap {
+		err = baseRes.GetDal().UpdateColumn(
+			&models.GitlabTransformationRule{}, "connection_id", cId,
+			dal.Where("id = ?", trId))
+		if err != nil {
+			return err
+		}
+	}
+	// delete all rules that are not referenced.
+	return baseRes.GetDal().Delete(&models.GitlabTransformationRule{}, dal.Where("connection_id IS NULL OR connection_id = 0"))
+}
+
+func (*addConnectionIdToTransformationRule) Version() uint64 {
+	return 20230322150357
+}
+
+func (*addConnectionIdToTransformationRule) Name() string {
+	return "add connection_id to _tool_gitlab_transformation_rules"
+}
diff --git a/backend/plugins/gitlab/models/migrationscripts/register.go b/backend/plugins/gitlab/models/migrationscripts/register.go
index 562e48032..9612cf3f7 100644
--- a/backend/plugins/gitlab/models/migrationscripts/register.go
+++ b/backend/plugins/gitlab/models/migrationscripts/register.go
@@ -32,5 +32,6 @@ func All() []plugin.MigrationScript {
 		new(addTransformationRule20221125),
 		new(addStdTypeToIssue221230),
 		new(addIsDetailRequired20230210),
+		new(addConnectionIdToTransformationRule),
 	}
 }
diff --git a/backend/plugins/gitlab/models/transformation_rule.go b/backend/plugins/gitlab/models/transformation_rule.go
index 3d65b2aa0..255f30860 100644
--- a/backend/plugins/gitlab/models/transformation_rule.go
+++ b/backend/plugins/gitlab/models/transformation_rule.go
@@ -24,6 +24,7 @@ import (
 
 type GitlabTransformationRule struct {
 	common.Model
+	ConnectionId         uint64            `mapstructure:"connectionId" json:"connectionId"`
 	Name                 string            `gorm:"type:varchar(255);index:idx_name_gitlab,unique" validate:"required" mapstructure:"name" json:"name"`
 	PrType               string            `mapstructure:"prType" json:"prType"`
 	PrComponent          string            `mapstructure:"prComponent" json:"prComponent"`
diff --git a/backend/plugins/jenkins/api/transformation_rule.go b/backend/plugins/jenkins/api/transformation_rule.go
index 9cd27682a..31377baa5 100644
--- a/backend/plugins/jenkins/api/transformation_rule.go
+++ b/backend/plugins/jenkins/api/transformation_rule.go
@@ -27,11 +27,12 @@ import (
 // @Description create transformation rule for Jenkins
 // @Tags plugins/jenkins
 // @Accept application/json
+// @Param connectionId path int true "connectionId"
 // @Param transformationRule body models.JenkinsTransformationRule true "transformation rule"
 // @Success 200  {object} models.JenkinsTransformationRule
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
-// @Router /plugins/jenkins/transformation_rules [POST]
+// @Router /plugins/jenkins/connections/{connectionId}/transformation_rules [POST]
 func CreateTransformationRule(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
 	return trHelper.Create(input)
 }
@@ -43,10 +44,11 @@ func CreateTransformationRule(input *plugin.ApiResourceInput) (*plugin.ApiResour
 // @Accept application/json
 // @Param id path int true "id"
 // @Param transformationRule body models.JenkinsTransformationRule true "transformation rule"
+// @Param connectionId path int true "connectionId"
 // @Success 200  {object} models.JenkinsTransformationRule
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
-// @Router /plugins/jenkins/transformation_rules/{id} [PATCH]
+// @Router /plugins/jenkins/connections/{connectionId}/transformation_rules/{id} [PATCH]
 func UpdateTransformationRule(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
 	return trHelper.Update(input)
 }
@@ -56,10 +58,11 @@ func UpdateTransformationRule(input *plugin.ApiResourceInput) (*plugin.ApiResour
 // @Description return one transformation rule
 // @Tags plugins/jenkins
 // @Param id path int true "id"
+// @Param connectionId path int true "connectionId"
 // @Success 200  {object} models.JenkinsTransformationRule
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
-// @Router /plugins/jenkins/transformation_rules/{id} [GET]
+// @Router /plugins/jenkins/connections/{connectionId}/transformation_rules/{id} [GET]
 func GetTransformationRule(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
 	return trHelper.Update(input)
 }
@@ -70,10 +73,11 @@ func GetTransformationRule(input *plugin.ApiResourceInput) (*plugin.ApiResourceO
 // @Tags plugins/jenkins
 // @Param pageSize query int false "page size, default 50"
 // @Param page query int false "page size, default 1"
+// @Param connectionId path int true "connectionId"
 // @Success 200  {object} []models.JenkinsTransformationRule
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
-// @Router /plugins/jenkins/transformation_rules [GET]
+// @Router /plugins/jenkins/connections/{connectionId}/transformation_rules [GET]
 func GetTransformationRuleList(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
 	return trHelper.List(input)
 }
diff --git a/backend/plugins/jenkins/impl/impl.go b/backend/plugins/jenkins/impl/impl.go
index 04c2a3742..fa4986cf8 100644
--- a/backend/plugins/jenkins/impl/impl.go
+++ b/backend/plugins/jenkins/impl/impl.go
@@ -181,11 +181,11 @@ func (p Jenkins) ApiResources() map[string]map[string]plugin.ApiResourceHandler
 			"GET": api.GetScopeList,
 			"PUT": api.PutScope,
 		},
-		"transformation_rules": {
+		"connections/:connectionId/transformation_rules": {
 			"POST": api.CreateTransformationRule,
 			"GET":  api.GetTransformationRuleList,
 		},
-		"transformation_rules/:id": {
+		"connections/:connectionId/transformation_rules/:id": {
 			"PATCH": api.UpdateTransformationRule,
 			"GET":   api.GetTransformationRule,
 		},
diff --git a/backend/plugins/jenkins/models/migrationscripts/20230322_add_connection_id_to_transformation_rules.go b/backend/plugins/jenkins/models/migrationscripts/20230322_add_connection_id_to_transformation_rules.go
new file mode 100644
index 000000000..38236b7f4
--- /dev/null
+++ b/backend/plugins/jenkins/models/migrationscripts/20230322_add_connection_id_to_transformation_rules.go
@@ -0,0 +1,74 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package migrationscripts
+
+import (
+	"github.com/apache/incubator-devlake/core/context"
+	"github.com/apache/incubator-devlake/core/dal"
+	"github.com/apache/incubator-devlake/core/errors"
+	"github.com/apache/incubator-devlake/helpers/migrationhelper"
+	"github.com/apache/incubator-devlake/plugins/jenkins/models"
+)
+
+type addConnectionIdToTransformationRule struct{}
+
+type transformationRule20220322 struct {
+	ConnectionId uint64
+}
+
+func (transformationRule20220322) TableName() string {
+	return "_tool_jenkins_transformation_rules"
+}
+
+func (u *addConnectionIdToTransformationRule) Up(baseRes context.BasicRes) errors.Error {
+	err := migrationhelper.AutoMigrateTables(baseRes, &transformationRule20220322{})
+	if err != nil {
+		return err
+	}
+	var scopes []models.JenkinsJob
+	err = baseRes.GetDal().All(&scopes)
+	if err != nil {
+		return err
+	}
+	// get all rules that are not referenced.
+	idMap := make(map[uint64]uint64)
+	for _, scope := range scopes {
+		if scope.TransformationRuleId > 0 && idMap[scope.TransformationRuleId] == 0 {
+			idMap[scope.TransformationRuleId] = scope.ConnectionId
+		}
+	}
+	// set connection_id for rules
+	for trId, cId := range idMap {
+		err = baseRes.GetDal().UpdateColumn(
+			&models.JenkinsTransformationRule{}, "connection_id", cId,
+			dal.Where("id = ?", trId))
+		if err != nil {
+			return err
+		}
+	}
+	// delete all rules that are not referenced.
+	return baseRes.GetDal().Delete(&models.JenkinsTransformationRule{}, dal.Where("connection_id IS NULL OR connection_id = 0"))
+}
+
+func (*addConnectionIdToTransformationRule) Version() uint64 {
+	return 20230322150357
+}
+
+func (*addConnectionIdToTransformationRule) Name() string {
+	return "add connection_id to _tool_jenkins_transformation_rules"
+}
diff --git a/backend/plugins/jenkins/models/migrationscripts/register.go b/backend/plugins/jenkins/models/migrationscripts/register.go
index 8e451c273..bc0d54f57 100644
--- a/backend/plugins/jenkins/models/migrationscripts/register.go
+++ b/backend/plugins/jenkins/models/migrationscripts/register.go
@@ -32,5 +32,6 @@ func All() []plugin.MigrationScript {
 		new(changeIndexOfJobPath),
 		new(addTransformationRule20221128),
 		new(addFullNameForBuilds),
+		new(addConnectionIdToTransformationRule),
 	}
 }
diff --git a/backend/plugins/jenkins/models/transformation_rule.go b/backend/plugins/jenkins/models/transformation_rule.go
index 70b44b2f7..2845f0a04 100644
--- a/backend/plugins/jenkins/models/transformation_rule.go
+++ b/backend/plugins/jenkins/models/transformation_rule.go
@@ -23,6 +23,7 @@ import (
 
 type JenkinsTransformationRule struct {
 	common.Model      `mapstructure:"-"`
+	ConnectionId      uint64 `mapstructure:"connectionId" json:"connectionId"`
 	Name              string `gorm:"type:varchar(255);index:idx_name_jenkins,unique" validate:"required" mapstructure:"name" json:"name"`
 	DeploymentPattern string `gorm:"type:varchar(255)" mapstructure:"deploymentPattern,omitempty" json:"deploymentPattern"`
 	ProductionPattern string `gorm:"type:varchar(255)" mapstructure:"productionPattern,omitempty" json:"productionPattern"`
diff --git a/backend/plugins/jira/api/transformation_rule.go b/backend/plugins/jira/api/transformation_rule.go
index e8b44e417..f3fffe97c 100644
--- a/backend/plugins/jira/api/transformation_rule.go
+++ b/backend/plugins/jira/api/transformation_rule.go
@@ -18,6 +18,10 @@ limitations under the License.
 package api
 
 import (
+	"net/http"
+	"strconv"
+
+	"github.com/apache/incubator-devlake/core/dal"
 	"github.com/apache/incubator-devlake/core/errors"
 	"github.com/apache/incubator-devlake/core/plugin"
 	"github.com/apache/incubator-devlake/helpers/pluginhelper/api"
@@ -31,11 +35,12 @@ import (
 // @Description create transformation rule for Jira
 // @Tags plugins/jira
 // @Accept application/json
+// @Param connectionId path int true "connectionId"
 // @Param transformationRule body tasks.JiraTransformationRule true "transformation rule"
 // @Success 200  {object} tasks.JiraTransformationRule
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
-// @Router /plugins/jira/transformation_rules [POST]
+// @Router /plugins/jira/connections/{connectionId}/transformation_rules [POST]
 func CreateTransformationRule(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
 	rule, err := makeDbTransformationRuleFromInput(input)
 	if err != nil {
@@ -56,40 +61,65 @@ func CreateTransformationRule(input *plugin.ApiResourceInput) (*plugin.ApiResour
 // @Tags plugins/jira
 // @Accept application/json
 // @Param id path int true "id"
+// @Param connectionId path int true "connectionId"
 // @Param transformationRule body tasks.JiraTransformationRule true "transformation rule"
 // @Success 200  {object} tasks.JiraTransformationRule
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
-// @Router /plugins/jira/transformation_rules/{id} [PATCH]
+// @Router /plugins/jira/connections/{connectionId}/transformation_rules/{id} [PATCH]
 func UpdateTransformationRule(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
-	rule, err := makeDbTransformationRuleFromInput(input)
+	connectionId, e := strconv.ParseUint(input.Params["connectionId"], 10, 64)
+	if e != nil || connectionId == 0 {
+		return nil, errors.Default.Wrap(e, "the connection ID should be an non-zero integer")
+	}
+	transformationRuleId, e := strconv.ParseUint(input.Params["id"], 10, 64)
+	if e != nil {
+		return nil, errors.Default.Wrap(e, "the transformation rule ID should be an integer")
+	}
+	var req tasks.JiraTransformationRule
+	err := api.Decode(input.Body, &req, vld)
 	if err != nil {
-		return nil, errors.BadInput.Wrap(err, "error in makeJiraTransformationRule")
+		return nil, err
 	}
-	newRule := map[string]interface{}{}
-	err = errors.Convert(mapstructure.Decode(rule, &newRule))
+	var oldDB models.JiraTransformationRule
+	err = basicRes.GetDal().First(&oldDB, dal.Where("id = ?", transformationRuleId))
 	if err != nil {
-		return nil, errors.BadInput.Wrap(err, "error in makeJiraTransformationRule")
+		return nil, errors.Default.Wrap(err, "error on getting TransformationRule")
 	}
-	input.Body = newRule
-	output, err := trHelper.Update(input)
+	oldTr, err := tasks.MakeTransformationRules(oldDB)
+	if err != nil {
+		return nil, err
+	}
+	err = api.DecodeMapStruct(input.Body, oldTr)
+	if err != nil {
+		return nil, err
+	}
+
+	newDB, err := oldTr.ToDb()
 	if err != nil {
 		return nil, err
 	}
-	tr := output.Body.(models.JiraTransformationRule)
-	err = tr.VerifyRegexp()
+	newDB.ID = transformationRuleId
+	newDB.ConnectionId = connectionId
+	newDB.CreatedAt = oldDB.CreatedAt
+	err = basicRes.GetDal().Update(newDB)
 	if err != nil {
-		return nil, errors.Default.Wrap(err, "error verify the regexps of transformationRule")
+		return nil, err
 	}
-	return output, err
+	return &plugin.ApiResourceOutput{Body: newDB, Status: http.StatusOK}, err
 }
 
 func makeDbTransformationRuleFromInput(input *plugin.ApiResourceInput) (*models.JiraTransformationRule, errors.Error) {
+	connectionId, e := strconv.ParseUint(input.Params["connectionId"], 10, 64)
+	if e != nil || connectionId == 0 {
+		return nil, errors.Default.Wrap(e, "the connection ID should be an non-zero integer")
+	}
 	var req tasks.JiraTransformationRule
 	err := api.Decode(input.Body, &req, vld)
 	if err != nil {
 		return nil, err
 	}
+	req.ConnectionId = connectionId
 	return req.ToDb()
 }
 
@@ -98,10 +128,11 @@ func makeDbTransformationRuleFromInput(input *plugin.ApiResourceInput) (*models.
 // @Description return one transformation rule
 // @Tags plugins/jira
 // @Param id path int true "id"
+// @Param connectionId path int true "connectionId"
 // @Success 200  {object} tasks.JiraTransformationRule
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
-// @Router /plugins/jira/transformation_rules/{id} [GET]
+// @Router /plugins/jira/connections/{connectionId}/transformation_rules/{id} [GET]
 func GetTransformationRule(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
 	return trHelper.Get(input)
 }
@@ -110,12 +141,13 @@ func GetTransformationRule(input *plugin.ApiResourceInput) (*plugin.ApiResourceO
 // @Summary return all transformation rules
 // @Description return all transformation rules
 // @Tags plugins/jira
+// @Param connectionId path int true "connectionId"
 // @Param pageSize query int false "page size, default 50"
 // @Param page query int false "page size, default 1"
 // @Success 200  {object} []tasks.JiraTransformationRule
 // @Failure 400  {object} shared.ApiBody "Bad Request"
 // @Failure 500  {object} shared.ApiBody "Internal Error"
-// @Router /plugins/jira/transformation_rules [GET]
+// @Router /plugins/jira/connections/{connectionId}/transformation_rules [GET]
 func GetTransformationRuleList(input *plugin.ApiResourceInput) (*plugin.ApiResourceOutput, errors.Error) {
 	return trHelper.List(input)
 }
diff --git a/backend/plugins/jira/impl/impl.go b/backend/plugins/jira/impl/impl.go
index 3663a672d..cb8c21515 100644
--- a/backend/plugins/jira/impl/impl.go
+++ b/backend/plugins/jira/impl/impl.go
@@ -291,11 +291,11 @@ func (p Jira) ApiResources() map[string]map[string]plugin.ApiResourceHandler {
 			"GET": api.GetScopeList,
 			"PUT": api.PutScope,
 		},
-		"transformation_rules": {
+		"connections/:connectionId/transformation_rules": {
 			"POST": api.CreateTransformationRule,
 			"GET":  api.GetTransformationRuleList,
 		},
-		"transformation_rules/:id": {
+		"connections/:connectionId/transformation_rules/:id": {
 			"PATCH": api.UpdateTransformationRule,
 			"GET":   api.GetTransformationRule,
 		},
diff --git a/backend/plugins/jira/models/migrationscripts/20230322_add_connection_id_to_transformation_rules.go b/backend/plugins/jira/models/migrationscripts/20230322_add_connection_id_to_transformation_rules.go
new file mode 100644
index 000000000..7a6af67a5
--- /dev/null
+++ b/backend/plugins/jira/models/migrationscripts/20230322_add_connection_id_to_transformation_rules.go
@@ -0,0 +1,74 @@
+/*
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package migrationscripts
+
+import (
+	"github.com/apache/incubator-devlake/core/context"
+	"github.com/apache/incubator-devlake/core/dal"
+	"github.com/apache/incubator-devlake/core/errors"
+	"github.com/apache/incubator-devlake/helpers/migrationhelper"
+	"github.com/apache/incubator-devlake/plugins/jira/models"
+)
+
+type addConnectionIdToTransformationRule struct{}
+
+type transformationRule20220322 struct {
+	ConnectionId uint64
+}
+
+func (transformationRule20220322) TableName() string {
+	return "_tool_jira_transformation_rules"
+}
+
+func (u *addConnectionIdToTransformationRule) Up(baseRes context.BasicRes) errors.Error {
+	err := migrationhelper.AutoMigrateTables(baseRes, &transformationRule20220322{})
+	if err != nil {
+		return err
+	}
+	var scopes []models.JiraBoard
+	err = baseRes.GetDal().All(&scopes)
+	if err != nil {
+		return err
+	}
+	// get all rules that are not referenced.
+	idMap := make(map[uint64]uint64)
+	for _, scope := range scopes {
+		if scope.TransformationRuleId > 0 && idMap[scope.TransformationRuleId] == 0 {
+			idMap[scope.TransformationRuleId] = scope.ConnectionId
+		}
+	}
+	// set connection_id for rules
+	for trId, cId := range idMap {
+		err = baseRes.GetDal().UpdateColumn(
+			&models.JiraTransformationRule{}, "connection_id", cId,
+			dal.Where("id = ?", trId))
+		if err != nil {
+			return err
+		}
+	}
+	// delete all rules that are not referenced.
+	return baseRes.GetDal().Delete(&models.JiraTransformationRule{}, dal.Where("connection_id IS NULL OR connection_id = 0"))
+}
+
+func (*addConnectionIdToTransformationRule) Version() uint64 {
+	return 20230322150357
+}
+
+func (*addConnectionIdToTransformationRule) Name() string {
+	return "add connection_id to _tool_jira_transformation_rules"
+}
diff --git a/backend/plugins/jira/models/migrationscripts/register.go b/backend/plugins/jira/models/migrationscripts/register.go
index 8a69859f5..996656967 100644
--- a/backend/plugins/jira/models/migrationscripts/register.go
+++ b/backend/plugins/jira/models/migrationscripts/register.go
@@ -33,5 +33,6 @@ func All() []plugin.MigrationScript {
 		new(removeIssueStdStoryPoint),
 		new(addCommitRepoPattern),
 		new(expandRemotelinkUrl),
+		new(addConnectionIdToTransformationRule),
 	}
 }
diff --git a/backend/plugins/jira/models/transformation_rules.go b/backend/plugins/jira/models/transformation_rules.go
index bb2b7e876..58d104555 100644
--- a/backend/plugins/jira/models/transformation_rules.go
+++ b/backend/plugins/jira/models/transformation_rules.go
@@ -27,6 +27,7 @@ import (
 
 type JiraTransformationRule struct {
 	common.Model               `mapstructure:"-"`
+	ConnectionId               uint64          `mapstructure:"connectionId" json:"connectionId"`
 	Name                       string          `mapstructure:"name" json:"name" gorm:"type:varchar(255);index:idx_name_jira,unique" validate:"required"`
 	EpicKeyField               string          `mapstructure:"epicKeyField,omitempty" json:"epicKeyField" gorm:"type:varchar(255)"`
 	StoryPointField            string          `mapstructure:"storyPointField,omitempty" json:"storyPointField" gorm:"type:varchar(255)"`
diff --git a/backend/plugins/jira/tasks/task_data.go b/backend/plugins/jira/tasks/task_data.go
index 3a5e3933f..97b0174af 100644
--- a/backend/plugins/jira/tasks/task_data.go
+++ b/backend/plugins/jira/tasks/task_data.go
@@ -41,6 +41,7 @@ type TypeMapping struct {
 type TypeMappings map[string]TypeMapping
 
 type JiraTransformationRule struct {
+	ConnectionId               uint64       `mapstructure:"connectionId" json:"connectionId"`
 	Name                       string       `gorm:"type:varchar(255)" validate:"required"`
 	EpicKeyField               string       `json:"epicKeyField"`
 	StoryPointField            string       `json:"storyPointField"`
@@ -59,6 +60,7 @@ func (r *JiraTransformationRule) ToDb() (*models.JiraTransformationRule, errors.
 		return nil, errors.Default.Wrap(err, "error marshaling RemotelinkRepoPattern")
 	}
 	rule := &models.JiraTransformationRule{
+		ConnectionId:               r.ConnectionId,
 		Name:                       r.Name,
 		EpicKeyField:               r.EpicKeyField,
 		StoryPointField:            r.StoryPointField,
@@ -89,6 +91,7 @@ func MakeTransformationRules(rule models.JiraTransformationRule) (*JiraTransform
 		}
 	}
 	result := &JiraTransformationRule{
+		ConnectionId:               rule.ConnectionId,
 		Name:                       rule.Name,
 		EpicKeyField:               rule.EpicKeyField,
 		StoryPointField:            rule.StoryPointField,