You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@devlake.apache.org by zh...@apache.org on 2022/09/09 07:52:57 UTC

[incubator-devlake] branch main updated: feat: implement API for plugin `customize` (#2911)

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

zhangliang2022 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 075e3d3b feat: implement API for plugin `customize` (#2911)
075e3d3b is described below

commit 075e3d3b480e407aede5a0893bd61de9b0fd051d
Author: mindlesscloud <li...@merico.dev>
AuthorDate: Fri Sep 9 15:52:53 2022 +0800

    feat: implement API for plugin `customize` (#2911)
    
    * feat: add new plugin
    
    * feat: sub-task ExtractCustomizedFields for plugin customize
    
    * refactor: new task Options
    
    * fix: update swagger docs
    
    * fix: fix e2e test
    
    * refactor: use join statement reduce query times
    
    * refactor: switch to new error type
---
 go.mod                                             |   3 +
 go.sum                                             |   6 +
 impl/dalgorm/dalgorm.go                            |  10 ++
 plugins/core/dal/dal.go                            |   4 +
 plugins/customize/README.md                        |   1 +
 plugins/customize/api/api.go                       | 163 +++++++++++++++++++++
 plugins/customize/api/blueprint.go                 |  55 +++++++
 plugins/customize/api/swagger.go                   |  52 +++++++
 plugins/customize/customize.go                     |  42 ++++++
 plugins/customize/e2e/extract_fields_test.go       |  62 ++++++++
 .../e2e/raw_tables/_raw_jira_api_issues.csv        |  31 ++++
 plugins/customize/e2e/raw_tables/issues.csv        |  31 ++++
 plugins/customize/e2e/snapshot_tables/issues.csv   |  31 ++++
 plugins/customize/impl/impl.go                     |  88 +++++++++++
 plugins/customize/models/model.go                  |  26 ++++
 .../customize/tasks/customized_fields_extractor.go | 134 +++++++++++++++++
 plugins/customize/tasks/task_data.go               |  33 +++++
 17 files changed, 772 insertions(+)

diff --git a/go.mod b/go.mod
index c99ecc60..1b88ccd5 100644
--- a/go.mod
+++ b/go.mod
@@ -25,6 +25,7 @@ require (
 	github.com/stretchr/testify v1.7.0
 	github.com/swaggo/gin-swagger v1.4.3
 	github.com/swaggo/swag v1.8.3
+	github.com/tidwall/gjson v1.14.3
 	github.com/viant/afs v1.16.0
 	github.com/x-cray/logrus-prefixed-formatter v0.5.2
 	go.temporal.io/api v1.7.1-0.20220223032354-6e6fe738916a
@@ -113,6 +114,8 @@ require (
 	github.com/spf13/pflag v1.0.6-0.20200504143853-81378bbcd8a1 // indirect
 	github.com/stretchr/objx v0.3.0 // indirect
 	github.com/subosito/gotenv v1.2.0 // indirect
+	github.com/tidwall/match v1.1.1 // indirect
+	github.com/tidwall/pretty v1.2.0 // indirect
 	github.com/ugorji/go/codec v1.2.6 // indirect
 	github.com/xanzy/ssh-agent v0.3.0 // indirect
 	go.uber.org/atomic v1.9.0 // indirect
diff --git a/go.sum b/go.sum
index dff4852e..367862a6 100644
--- a/go.sum
+++ b/go.sum
@@ -676,6 +676,12 @@ github.com/swaggo/gin-swagger v1.4.3/go.mod h1:hBg6tGeKJsUu/P79BH+WGUR8nq2LuGE0O
 github.com/swaggo/swag v1.8.1/go.mod h1:ugemnJsPZm/kRwFUnzBlbHRd0JY9zE1M4F+uy2pAaPQ=
 github.com/swaggo/swag v1.8.3 h1:3pZSSCQ//gAH88lfmxM3Cd1+JCsxV8Md6f36b9hrZ5s=
 github.com/swaggo/swag v1.8.3/go.mod h1:jMLeXOOmYyjk8PvHTsXBdrubsNd9gUJTTCzL5iBnseg=
+github.com/tidwall/gjson v1.14.3 h1:9jvXn7olKEHU1S9vwoMGliaT8jq1vJ7IH/n9zD9Dnlw=
+github.com/tidwall/gjson v1.14.3/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
+github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
+github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
+github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
+github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
 github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
 github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
 github.com/ugorji/go v1.2.6/go.mod h1:anCg0y61KIhDlPZmnH+so+RQbysYVyDko0IMgJv0Nn0=
diff --git a/impl/dalgorm/dalgorm.go b/impl/dalgorm/dalgorm.go
index 82adf354..2b41cfe6 100644
--- a/impl/dalgorm/dalgorm.go
+++ b/impl/dalgorm/dalgorm.go
@@ -162,6 +162,16 @@ func (d *Dalgorm) GetColumns(dst schema.Tabler, filter func(columnMeta dal.Colum
 	return cms, nil
 }
 
+// AddColumn add one column for the table
+func (d *Dalgorm) AddColumn(table, columnName, columnType string) error {
+	return d.Exec("ALTER TABLE ? ADD ? ?", clause.Table{Name: table}, clause.Column{Name: columnName}, clause.Expr{SQL: columnType})
+}
+
+// DropColumn drop one column from the table
+func (d *Dalgorm) DropColumn(table, columnName string) error {
+	return d.Exec("ALTER TABLE ? DROP COLUMN ?", clause.Table{Name: table}, clause.Column{Name: columnName})
+}
+
 // GetPrimaryKeyFields get the PrimaryKey from `gorm` tag
 func (d *Dalgorm) GetPrimaryKeyFields(t reflect.Type) []reflect.StructField {
 	return utils.WalkFields(t, func(field *reflect.StructField) bool {
diff --git a/plugins/core/dal/dal.go b/plugins/core/dal/dal.go
index 11ab5d85..810609c9 100644
--- a/plugins/core/dal/dal.go
+++ b/plugins/core/dal/dal.go
@@ -49,6 +49,10 @@ type ColumnMeta interface {
 type Dal interface {
 	// AutoMigrate runs auto migration for given entity
 	AutoMigrate(entity interface{}, clauses ...Clause) error
+	// AddColumn add column for the table
+	AddColumn(table, columnName, columnType string) error
+	// DropColumn drop column from the table
+	DropColumn(table, columnName string) error
 	// Exec executes raw sql query
 	Exec(query string, params ...interface{}) error
 	// RawCursor executes raw sql query and returns a database cursor
diff --git a/plugins/customize/README.md b/plugins/customize/README.md
new file mode 100644
index 00000000..825e35f0
--- /dev/null
+++ b/plugins/customize/README.md
@@ -0,0 +1 @@
+Please see details in the [Apache DevLake website](https://devlake.apache.org/docs/Plugins/customize)
\ No newline at end of file
diff --git a/plugins/customize/api/api.go b/plugins/customize/api/api.go
new file mode 100644
index 00000000..08312c43
--- /dev/null
+++ b/plugins/customize/api/api.go
@@ -0,0 +1,163 @@
+/*
+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 api
+
+import (
+	"fmt"
+	"net/http"
+	"strings"
+
+	"github.com/apache/incubator-devlake/errors"
+	"github.com/apache/incubator-devlake/plugins/core"
+	"github.com/apache/incubator-devlake/plugins/core/dal"
+	"github.com/apache/incubator-devlake/plugins/customize/models"
+)
+
+type field struct {
+	ColumnName string `json:"columnName"`
+	ColumnType string `json:"columnType"`
+}
+
+func getFields(d dal.Dal, tbl string) ([]field, error) {
+	columns, err := d.GetColumns(&models.Table{tbl}, func(columnMeta dal.ColumnMeta) bool {
+		return strings.HasPrefix(columnMeta.Name(), "x_")
+	})
+	if err != nil {
+		return nil, errors.Default.Wrap(err, "GetColumns error")
+	}
+	var result []field
+	for _, col := range columns {
+		result = append(result, field{
+			ColumnName: col.Name(),
+			ColumnType: "VARCHAR(255)",
+		})
+	}
+	return result, nil
+}
+func checkField(d dal.Dal, table, field string) (bool, error) {
+	if !strings.HasPrefix(field, "x_") {
+		return false, errors.Default.New("column name should start with `x_`")
+	}
+	fields, err := getFields(d, table)
+	if err != nil {
+		return false, err
+	}
+	for _, fld := range fields {
+		if fld.ColumnName == field {
+			return true, nil
+		}
+	}
+	return false, nil
+}
+
+func CreateField(d dal.Dal, table, field string) error {
+	exists, err := checkField(d, table, field)
+	if err != nil {
+		return err
+	}
+	if exists {
+		return nil
+	}
+	err = d.AddColumn(table, field, "VARCHAR(255)")
+	if err != nil {
+		return errors.Default.Wrap(err, "AddColumn error")
+	}
+	return nil
+}
+
+func deleteField(d dal.Dal, table, field string) error {
+	exists, err := checkField(d, table, field)
+	if err != nil {
+		return err
+	}
+	if !exists {
+		return nil
+	}
+	err = d.DropColumn(table, field)
+	if err != nil {
+		return errors.Default.Wrap(err, "DropColumn error")
+	}
+	return nil
+}
+
+type input struct {
+	Name string `json:"name" example:"x_new_column"`
+}
+type Handlers struct {
+	dal dal.Dal
+}
+
+func NewHandlers(dal dal.Dal) *Handlers {
+	return &Handlers{dal: dal}
+}
+
+// ListFields return all customized fields
+// @Summary return all customized fields
+// @Description return all customized fields
+// @Tags plugins/customize
+// @Success 200  {object} shared.ApiBody "Success"
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internel Error"
+// @Router /plugins/customize/{table}/fields [GET]
+func (h *Handlers) ListFields(input *core.ApiResourceInput) (*core.ApiResourceOutput, error) {
+	fields, err := getFields(h.dal, input.Params["table"])
+	if err != nil {
+		return &core.ApiResourceOutput{Status: http.StatusBadRequest}, errors.Default.Wrap(err, "getFields error")
+	}
+	return &core.ApiResourceOutput{Body: fields, Status: http.StatusOK}, nil
+}
+
+// CreateFields create a customized field
+// @Summary create a customized field
+// @Description create a customized field
+// @Tags plugins/customize
+// @Param request body input true "request body"
+// @Success 200  {object} shared.ApiBody "Success"
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internel Error"
+// @Router /plugins/customize/{table}/fields [POST]
+func (h *Handlers) CreateFields(input *core.ApiResourceInput) (*core.ApiResourceOutput, error) {
+	table := input.Params["table"]
+	fld, ok := input.Body["name"].(string)
+	if !ok {
+		return &core.ApiResourceOutput{Status: http.StatusBadRequest}, fmt.Errorf("the name is not string")
+	}
+	err := CreateField(h.dal, table, fld)
+	if err != nil {
+		return nil, errors.Default.Wrap(err, "CreateField error")
+	}
+	return &core.ApiResourceOutput{Body: field{fld, "varchar(255)"}, Status: http.StatusOK}, nil
+}
+
+// DeleteField delete a customized fields
+// @Summary return all customized fields
+// @Description return all customized fields
+// @Tags plugins/customize
+// @Success 200  {object} shared.ApiBody "Success"
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internel Error"
+// @Router /plugins/customize/{table}/fields [DELETE]
+func (h *Handlers) DeleteField(input *core.ApiResourceInput) (*core.ApiResourceOutput, error) {
+	table := input.Params["table"]
+	fld := input.Params["field"]
+	err := deleteField(h.dal, table, fld)
+	if err != nil {
+		return &core.ApiResourceOutput{Status: http.StatusBadRequest}, errors.Default.Wrap(err, "deleteField error")
+	}
+	return &core.ApiResourceOutput{Status: http.StatusOK}, nil
+}
diff --git a/plugins/customize/api/blueprint.go b/plugins/customize/api/blueprint.go
new file mode 100644
index 00000000..2e12ca5e
--- /dev/null
+++ b/plugins/customize/api/blueprint.go
@@ -0,0 +1,55 @@
+/*
+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 api
+
+import (
+	"encoding/json"
+
+	"github.com/apache/incubator-devlake/plugins/core"
+	"github.com/apache/incubator-devlake/plugins/helper"
+	"github.com/apache/incubator-devlake/plugins/jira/tasks"
+)
+
+func MakePipelinePlan(subtaskMetas []core.SubTaskMeta, connectionId uint64, scope []*core.BlueprintScopeV100) (core.PipelinePlan, error) {
+	var err error
+	plan := make(core.PipelinePlan, len(scope))
+	for i, scopeElem := range scope {
+		taskOptions := make(map[string]interface{})
+		err = json.Unmarshal(scopeElem.Options, &taskOptions)
+		if err != nil {
+			return nil, err
+		}
+		_, err := tasks.DecodeAndValidateTaskOptions(taskOptions)
+		if err != nil {
+			return nil, err
+		}
+		// subtasks
+		subtasks, err := helper.MakePipelinePlanSubtasks(subtaskMetas, scopeElem.Entities)
+		if err != nil {
+			return nil, err
+		}
+		plan[i] = core.PipelineStage{
+			{
+				Plugin:   "customize",
+				Subtasks: subtasks,
+				Options:  taskOptions,
+			},
+		}
+	}
+	return plan, nil
+}
diff --git a/plugins/customize/api/swagger.go b/plugins/customize/api/swagger.go
new file mode 100644
index 00000000..7d8d3be1
--- /dev/null
+++ b/plugins/customize/api/swagger.go
@@ -0,0 +1,52 @@
+/*
+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 api
+
+import "github.com/apache/incubator-devlake/plugins/customize/tasks"
+
+// @Summary blueprints setting for customize
+// @Description blueprint setting for customize
+// @Tags plugins/customize
+// @Accept application/json
+// @Param blueprint-setting body blueprintSetting true "json"
+// @Router /blueprints/customize/blueprint-setting [post]
+func _() {}
+
+type blueprintSetting []struct {
+	Version     string `json:"version" example:"1.0.0"`
+	Connections []struct {
+		Plugin string `json:"plugin" example:"customize"`
+		Scope  []struct {
+			Options tasks.Options `json:"options"`
+		} `json:"scope"`
+	} `json:"connections"`
+}
+
+// @Summary pipelines plan for customize
+// @Description pipelines plan for customize
+// @Tags plugins/customize
+// @Accept application/json
+// @Param pipeline-plan body pipelinePlan true "json"
+// @Router /pipelines/customize/pipeline-plan [post]
+func _() {}
+
+type pipelinePlan [][]struct {
+	Plugin   string        `json:"plugin"`
+	Subtasks []string      `json:"subtasks"`
+	Options  tasks.Options `json:"options"`
+}
diff --git a/plugins/customize/customize.go b/plugins/customize/customize.go
new file mode 100644
index 00000000..644c8359
--- /dev/null
+++ b/plugins/customize/customize.go
@@ -0,0 +1,42 @@
+/*
+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 main // must be main for plugin entry point
+
+import (
+	"github.com/apache/incubator-devlake/plugins/customize/impl"
+	"github.com/apache/incubator-devlake/plugins/customize/tasks"
+	"github.com/apache/incubator-devlake/runner"
+	"github.com/spf13/cobra"
+)
+
+var PluginEntry impl.Customize //nolint
+
+// standalone mode for debugging
+func main() {
+	cmd := &cobra.Command{Use: "customize"}
+	cmd.Run = func(c *cobra.Command, args []string) {
+		runner.DirectRun(c, args, PluginEntry, map[string]interface{}{"transformationRules": []tasks.MappingRules{{
+			Table:         "issues",
+			RawDataTable:  "_raw_jira_api_issues",
+			RawDataParams: "{\"ConnectionId\":1,\"BoardId\":8}",
+			Mapping:       map[string]string{"x_test": "fields.timespent"},
+		}},
+		})
+	}
+	runner.RunCmd(cmd)
+}
diff --git a/plugins/customize/e2e/extract_fields_test.go b/plugins/customize/e2e/extract_fields_test.go
new file mode 100644
index 00000000..10a1bbc3
--- /dev/null
+++ b/plugins/customize/e2e/extract_fields_test.go
@@ -0,0 +1,62 @@
+/*
+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 e2e
+
+import (
+	"testing"
+
+	"github.com/apache/incubator-devlake/helpers/e2ehelper"
+	"github.com/apache/incubator-devlake/models/domainlayer/ticket"
+	"github.com/apache/incubator-devlake/plugins/customize/api"
+	"github.com/apache/incubator-devlake/plugins/customize/impl"
+	"github.com/apache/incubator-devlake/plugins/customize/tasks"
+)
+
+func TestBoardDataFlow(t *testing.T) {
+	var plugin impl.Customize
+	dataflowTester := e2ehelper.NewDataFlowTester(t, "customize", plugin)
+
+	taskData := &tasks.TaskData{
+		Options: &tasks.Options{[]tasks.MappingRules{{
+			Table:         "issues",
+			RawDataTable:  "_raw_jira_api_issues",
+			RawDataParams: "{\"ConnectionId\":1,\"BoardId\":8}",
+			Mapping:       map[string]string{"x_test": "fields.created"},
+		}}}}
+
+	// import raw data table
+	dataflowTester.ImportCsvIntoRawTable("./raw_tables/_raw_jira_api_issues.csv", "_raw_jira_api_issues")
+	dataflowTester.ImportCsvIntoTabler("./raw_tables/issues.csv", &ticket.Issue{})
+	err := api.CreateField(dataflowTester.Dal, "issues", "x_test")
+	if err != nil {
+		t.Fatal(err)
+	}
+	// verify extension fields extraction
+	dataflowTester.Subtask(tasks.ExtractCustomizedFieldsMeta, taskData)
+	dataflowTester.VerifyTable(
+		ticket.Issue{},
+		"./snapshot_tables/issues.csv",
+		[]string{
+			"id",
+			"_raw_data_params",
+			"_raw_data_table",
+			"_raw_data_id",
+			"x_test",
+		},
+	)
+}
diff --git a/plugins/customize/e2e/raw_tables/_raw_jira_api_issues.csv b/plugins/customize/e2e/raw_tables/_raw_jira_api_issues.csv
new file mode 100644
index 00000000..8b546b68
--- /dev/null
+++ b/plugins/customize/e2e/raw_tables/_raw_jira_api_issues.csv
@@ -0,0 +1,31 @@
+"id","params","data","url","input","created_at"
+1701,"{""ConnectionId"":1,""BoardId"":8}","{""expand"":""operations,versionedRepresentations,editmeta,changelog,renderedFields"",""id"":""10063"",""self"":""https://merico.atlassian.net/rest/agile/1.0/issue/10063"",""key"":""EE-1"",""changelog"":{""startAt"":0,""maxResults"":25,""total"":25,""histories"":[{""id"":""90706"",""author"":{""self"":""https://merico.atlassian.net/rest/api/2/user?accountId=5e9711ba34f7b90c0fbc37d3"",""accountId"":""5e9711ba34f7b90c0fbc37d3"",""avatarUrls"":{""4 [...]
+1702,"{""ConnectionId"":1,""BoardId"":8}","{""expand"":""operations,versionedRepresentations,editmeta,changelog,renderedFields"",""id"":""10064"",""self"":""https://merico.atlassian.net/rest/agile/1.0/issue/10064"",""key"":""EE-2"",""changelog"":{""startAt"":0,""maxResults"":24,""total"":24,""histories"":[{""id"":""90429"",""author"":{""self"":""https://merico.atlassian.net/rest/api/2/user?accountId=5e9711ba34f7b90c0fbc37d3"",""accountId"":""5e9711ba34f7b90c0fbc37d3"",""avatarUrls"":{""4 [...]
+1703,"{""ConnectionId"":1,""BoardId"":8}","{""expand"":""operations,versionedRepresentations,editmeta,changelog,renderedFields"",""id"":""10065"",""self"":""https://merico.atlassian.net/rest/agile/1.0/issue/10065"",""key"":""EE-3"",""changelog"":{""startAt"":0,""maxResults"":26,""total"":26,""histories"":[{""id"":""90430"",""author"":{""self"":""https://merico.atlassian.net/rest/api/2/user?accountId=5e9711ba34f7b90c0fbc37d3"",""accountId"":""5e9711ba34f7b90c0fbc37d3"",""avatarUrls"":{""4 [...]
+1704,"{""ConnectionId"":1,""BoardId"":8}","{""expand"":""operations,versionedRepresentations,editmeta,changelog,renderedFields"",""id"":""10066"",""self"":""https://merico.atlassian.net/rest/agile/1.0/issue/10066"",""key"":""EE-4"",""changelog"":{""startAt"":0,""maxResults"":23,""total"":23,""histories"":[{""id"":""90722"",""author"":{""self"":""https://merico.atlassian.net/rest/api/2/user?accountId=5e9711ba34f7b90c0fbc37d3"",""accountId"":""5e9711ba34f7b90c0fbc37d3"",""avatarUrls"":{""4 [...]
+1705,"{""ConnectionId"":1,""BoardId"":8}","{""expand"":""operations,versionedRepresentations,editmeta,changelog,renderedFields"",""id"":""10067"",""self"":""https://merico.atlassian.net/rest/agile/1.0/issue/10067"",""key"":""EE-5"",""changelog"":{""startAt"":0,""maxResults"":18,""total"":18,""histories"":[{""id"":""295895"",""author"":{""self"":""https://merico.atlassian.net/rest/api/2/user?accountId=5ecfbd0c2490cf0c09e2e598"",""accountId"":""5ecfbd0c2490cf0c09e2e598"",""avatarUrls"":{"" [...]
+1706,"{""ConnectionId"":1,""BoardId"":8}","{""expand"":""operations,versionedRepresentations,editmeta,changelog,renderedFields"",""id"":""10068"",""self"":""https://merico.atlassian.net/rest/agile/1.0/issue/10068"",""key"":""EE-6"",""changelog"":{""startAt"":0,""maxResults"":17,""total"":17,""histories"":[{""id"":""90473"",""author"":{""self"":""https://merico.atlassian.net/rest/api/2/user?accountId=5e9711ba34f7b90c0fbc37d3"",""accountId"":""5e9711ba34f7b90c0fbc37d3"",""avatarUrls"":{""4 [...]
+1707,"{""ConnectionId"":1,""BoardId"":8}","{""expand"":""operations,versionedRepresentations,editmeta,changelog,renderedFields"",""id"":""10070"",""self"":""https://merico.atlassian.net/rest/agile/1.0/issue/10070"",""key"":""EE-8"",""changelog"":{""startAt"":0,""maxResults"":17,""total"":17,""histories"":[{""id"":""295892"",""author"":{""self"":""https://merico.atlassian.net/rest/api/2/user?accountId=5ecfbd0c2490cf0c09e2e598"",""accountId"":""5ecfbd0c2490cf0c09e2e598"",""avatarUrls"":{"" [...]
+1708,"{""ConnectionId"":1,""BoardId"":8}","{""expand"":""operations,versionedRepresentations,editmeta,changelog,renderedFields"",""id"":""10071"",""self"":""https://merico.atlassian.net/rest/agile/1.0/issue/10071"",""key"":""EE-9"",""changelog"":{""startAt"":0,""maxResults"":18,""total"":18,""histories"":[{""id"":""296002"",""author"":{""self"":""https://merico.atlassian.net/rest/api/2/user?accountId=5ecfbd0c2490cf0c09e2e598"",""accountId"":""5ecfbd0c2490cf0c09e2e598"",""avatarUrls"":{"" [...]
+1709,"{""ConnectionId"":1,""BoardId"":8}","{""expand"":""operations,versionedRepresentations,editmeta,changelog,renderedFields"",""id"":""10072"",""self"":""https://merico.atlassian.net/rest/agile/1.0/issue/10072"",""key"":""EE-10"",""changelog"":{""startAt"":0,""maxResults"":17,""total"":17,""histories"":[{""id"":""295894"",""author"":{""self"":""https://merico.atlassian.net/rest/api/2/user?accountId=5ecfbd0c2490cf0c09e2e598"",""accountId"":""5ecfbd0c2490cf0c09e2e598"",""avatarUrls"":{" [...]
+1710,"{""ConnectionId"":1,""BoardId"":8}","{""expand"":""operations,versionedRepresentations,editmeta,changelog,renderedFields"",""id"":""10076"",""self"":""https://merico.atlassian.net/rest/agile/1.0/issue/10076"",""key"":""EE-14"",""changelog"":{""startAt"":0,""maxResults"":18,""total"":18,""histories"":[{""id"":""295897"",""author"":{""self"":""https://merico.atlassian.net/rest/api/2/user?accountId=5ecfbd0c2490cf0c09e2e598"",""accountId"":""5ecfbd0c2490cf0c09e2e598"",""avatarUrls"":{" [...]
+1711,"{""ConnectionId"":1,""BoardId"":8}","{""expand"":""operations,versionedRepresentations,editmeta,changelog,renderedFields"",""id"":""10077"",""self"":""https://merico.atlassian.net/rest/agile/1.0/issue/10077"",""key"":""EE-15"",""changelog"":{""startAt"":0,""maxResults"":20,""total"":20,""histories"":[{""id"":""296102"",""author"":{""self"":""https://merico.atlassian.net/rest/api/2/user?accountId=5ecfbd0c2490cf0c09e2e598"",""accountId"":""5ecfbd0c2490cf0c09e2e598"",""avatarUrls"":{" [...]
+1712,"{""ConnectionId"":1,""BoardId"":8}","{""expand"":""operations,versionedRepresentations,editmeta,changelog,renderedFields"",""id"":""10078"",""self"":""https://merico.atlassian.net/rest/agile/1.0/issue/10078"",""key"":""EE-16"",""changelog"":{""startAt"":0,""maxResults"":18,""total"":18,""histories"":[{""id"":""295898"",""author"":{""self"":""https://merico.atlassian.net/rest/api/2/user?accountId=5ecfbd0c2490cf0c09e2e598"",""accountId"":""5ecfbd0c2490cf0c09e2e598"",""avatarUrls"":{" [...]
+1713,"{""ConnectionId"":1,""BoardId"":8}","{""expand"":""operations,versionedRepresentations,editmeta,changelog,renderedFields"",""id"":""10079"",""self"":""https://merico.atlassian.net/rest/agile/1.0/issue/10079"",""key"":""EE-17"",""changelog"":{""startAt"":0,""maxResults"":31,""total"":31,""histories"":[{""id"":""296582"",""author"":{""self"":""https://merico.atlassian.net/rest/api/2/user?accountId=5ecfbd0c2490cf0c09e2e598"",""accountId"":""5ecfbd0c2490cf0c09e2e598"",""avatarUrls"":{" [...]
+1714,"{""ConnectionId"":1,""BoardId"":8}","{""expand"":""operations,versionedRepresentations,editmeta,changelog,renderedFields"",""id"":""10081"",""self"":""https://merico.atlassian.net/rest/agile/1.0/issue/10081"",""key"":""EE-19"",""changelog"":{""startAt"":0,""maxResults"":17,""total"":17,""histories"":[{""id"":""90485"",""author"":{""self"":""https://merico.atlassian.net/rest/api/2/user?accountId=5e9711ba34f7b90c0fbc37d3"",""accountId"":""5e9711ba34f7b90c0fbc37d3"",""avatarUrls"":{"" [...]
+1715,"{""ConnectionId"":1,""BoardId"":8}","{""expand"":""operations,versionedRepresentations,editmeta,changelog,renderedFields"",""id"":""10082"",""self"":""https://merico.atlassian.net/rest/agile/1.0/issue/10082"",""key"":""EE-20"",""changelog"":{""startAt"":0,""maxResults"":14,""total"":14,""histories"":[{""id"":""142717"",""author"":{""self"":""https://merico.atlassian.net/rest/api/2/user?accountId=5ecfbd0984083c0c12e5af8f"",""accountId"":""5ecfbd0984083c0c12e5af8f"",""avatarUrls"":{" [...]
+1716,"{""ConnectionId"":1,""BoardId"":8}","{""expand"":""operations,versionedRepresentations,editmeta,changelog,renderedFields"",""id"":""10085"",""self"":""https://merico.atlassian.net/rest/agile/1.0/issue/10085"",""key"":""EE-23"",""changelog"":{""startAt"":0,""maxResults"":20,""total"":20,""histories"":[{""id"":""123813"",""author"":{""self"":""https://merico.atlassian.net/rest/api/2/user?accountId=5e9711ba34f7b90c0fbc37d3"",""accountId"":""5e9711ba34f7b90c0fbc37d3"",""avatarUrls"":{" [...]
+1717,"{""ConnectionId"":1,""BoardId"":8}","{""expand"":""operations,versionedRepresentations,editmeta,changelog,renderedFields"",""id"":""10086"",""self"":""https://merico.atlassian.net/rest/agile/1.0/issue/10086"",""key"":""EE-24"",""changelog"":{""startAt"":0,""maxResults"":11,""total"":11,""histories"":[{""id"":""90454"",""author"":{""self"":""https://merico.atlassian.net/rest/api/2/user?accountId=5e9711ba34f7b90c0fbc37d3"",""accountId"":""5e9711ba34f7b90c0fbc37d3"",""avatarUrls"":{"" [...]
+1718,"{""ConnectionId"":1,""BoardId"":8}","{""expand"":""operations,versionedRepresentations,editmeta,changelog,renderedFields"",""id"":""10087"",""self"":""https://merico.atlassian.net/rest/agile/1.0/issue/10087"",""key"":""EE-25"",""changelog"":{""startAt"":0,""maxResults"":17,""total"":17,""histories"":[{""id"":""90481"",""author"":{""self"":""https://merico.atlassian.net/rest/api/2/user?accountId=5e9711ba34f7b90c0fbc37d3"",""accountId"":""5e9711ba34f7b90c0fbc37d3"",""avatarUrls"":{"" [...]
+1719,"{""ConnectionId"":1,""BoardId"":8}","{""expand"":""operations,versionedRepresentations,editmeta,changelog,renderedFields"",""id"":""10088"",""self"":""https://merico.atlassian.net/rest/agile/1.0/issue/10088"",""key"":""EE-26"",""changelog"":{""startAt"":0,""maxResults"":14,""total"":14,""histories"":[{""id"":""90445"",""author"":{""self"":""https://merico.atlassian.net/rest/api/2/user?accountId=5e9711ba34f7b90c0fbc37d3"",""accountId"":""5e9711ba34f7b90c0fbc37d3"",""avatarUrls"":{"" [...]
+1720,"{""ConnectionId"":1,""BoardId"":8}","{""expand"":""operations,versionedRepresentations,editmeta,changelog,renderedFields"",""id"":""10089"",""self"":""https://merico.atlassian.net/rest/agile/1.0/issue/10089"",""key"":""EE-27"",""changelog"":{""startAt"":0,""maxResults"":15,""total"":15,""histories"":[{""id"":""90451"",""author"":{""self"":""https://merico.atlassian.net/rest/api/2/user?accountId=5e9711ba34f7b90c0fbc37d3"",""accountId"":""5e9711ba34f7b90c0fbc37d3"",""avatarUrls"":{"" [...]
+1721,"{""ConnectionId"":1,""BoardId"":8}","{""expand"":""operations,versionedRepresentations,editmeta,changelog,renderedFields"",""id"":""10090"",""self"":""https://merico.atlassian.net/rest/agile/1.0/issue/10090"",""key"":""EE-28"",""changelog"":{""startAt"":0,""maxResults"":14,""total"":14,""histories"":[{""id"":""90642"",""author"":{""self"":""https://merico.atlassian.net/rest/api/2/user?accountId=5e9711ba34f7b90c0fbc37d3"",""accountId"":""5e9711ba34f7b90c0fbc37d3"",""avatarUrls"":{"" [...]
+1722,"{""ConnectionId"":1,""BoardId"":8}","{""expand"":""operations,versionedRepresentations,editmeta,changelog,renderedFields"",""id"":""10091"",""self"":""https://merico.atlassian.net/rest/agile/1.0/issue/10091"",""key"":""EE-29"",""changelog"":{""startAt"":0,""maxResults"":15,""total"":15,""histories"":[{""id"":""90443"",""author"":{""self"":""https://merico.atlassian.net/rest/api/2/user?accountId=5e9711ba34f7b90c0fbc37d3"",""accountId"":""5e9711ba34f7b90c0fbc37d3"",""avatarUrls"":{"" [...]
+1723,"{""ConnectionId"":1,""BoardId"":8}","{""expand"":""operations,versionedRepresentations,editmeta,changelog,renderedFields"",""id"":""10092"",""self"":""https://merico.atlassian.net/rest/agile/1.0/issue/10092"",""key"":""EE-30"",""changelog"":{""startAt"":0,""maxResults"":11,""total"":11,""histories"":[{""id"":""90434"",""author"":{""self"":""https://merico.atlassian.net/rest/api/2/user?accountId=5e9711ba34f7b90c0fbc37d3"",""accountId"":""5e9711ba34f7b90c0fbc37d3"",""avatarUrls"":{"" [...]
+1724,"{""ConnectionId"":1,""BoardId"":8}","{""expand"":""operations,versionedRepresentations,editmeta,changelog,renderedFields"",""id"":""10093"",""self"":""https://merico.atlassian.net/rest/agile/1.0/issue/10093"",""key"":""EE-31"",""changelog"":{""startAt"":0,""maxResults"":15,""total"":15,""histories"":[{""id"":""90439"",""author"":{""self"":""https://merico.atlassian.net/rest/api/2/user?accountId=5e9711ba34f7b90c0fbc37d3"",""accountId"":""5e9711ba34f7b90c0fbc37d3"",""avatarUrls"":{"" [...]
+1725,"{""ConnectionId"":1,""BoardId"":8}","{""expand"":""operations,versionedRepresentations,editmeta,changelog,renderedFields"",""id"":""10094"",""self"":""https://merico.atlassian.net/rest/agile/1.0/issue/10094"",""key"":""EE-32"",""changelog"":{""startAt"":0,""maxResults"":14,""total"":14,""histories"":[{""id"":""90440"",""author"":{""self"":""https://merico.atlassian.net/rest/api/2/user?accountId=5e9711ba34f7b90c0fbc37d3"",""accountId"":""5e9711ba34f7b90c0fbc37d3"",""avatarUrls"":{"" [...]
+1726,"{""ConnectionId"":1,""BoardId"":8}","{""expand"":""operations,versionedRepresentations,editmeta,changelog,renderedFields"",""id"":""10095"",""self"":""https://merico.atlassian.net/rest/agile/1.0/issue/10095"",""key"":""EE-33"",""changelog"":{""startAt"":0,""maxResults"":10,""total"":10,""histories"":[{""id"":""90446"",""author"":{""self"":""https://merico.atlassian.net/rest/api/2/user?accountId=5e9711ba34f7b90c0fbc37d3"",""accountId"":""5e9711ba34f7b90c0fbc37d3"",""avatarUrls"":{"" [...]
+1727,"{""ConnectionId"":1,""BoardId"":8}","{""expand"":""operations,versionedRepresentations,editmeta,changelog,renderedFields"",""id"":""10096"",""self"":""https://merico.atlassian.net/rest/agile/1.0/issue/10096"",""key"":""EE-34"",""changelog"":{""startAt"":0,""maxResults"":16,""total"":16,""histories"":[{""id"":""90482"",""author"":{""self"":""https://merico.atlassian.net/rest/api/2/user?accountId=5e9711ba34f7b90c0fbc37d3"",""accountId"":""5e9711ba34f7b90c0fbc37d3"",""avatarUrls"":{"" [...]
+1728,"{""ConnectionId"":1,""BoardId"":8}","{""expand"":""operations,versionedRepresentations,editmeta,changelog,renderedFields"",""id"":""10097"",""self"":""https://merico.atlassian.net/rest/agile/1.0/issue/10097"",""key"":""EE-35"",""changelog"":{""startAt"":0,""maxResults"":14,""total"":14,""histories"":[{""id"":""90448"",""author"":{""self"":""https://merico.atlassian.net/rest/api/2/user?accountId=5e9711ba34f7b90c0fbc37d3"",""accountId"":""5e9711ba34f7b90c0fbc37d3"",""avatarUrls"":{"" [...]
+1729,"{""ConnectionId"":1,""BoardId"":8}","{""expand"":""operations,versionedRepresentations,editmeta,changelog,renderedFields"",""id"":""10098"",""self"":""https://merico.atlassian.net/rest/agile/1.0/issue/10098"",""key"":""EE-36"",""changelog"":{""startAt"":0,""maxResults"":15,""total"":15,""histories"":[{""id"":""90442"",""author"":{""self"":""https://merico.atlassian.net/rest/api/2/user?accountId=5e9711ba34f7b90c0fbc37d3"",""accountId"":""5e9711ba34f7b90c0fbc37d3"",""avatarUrls"":{"" [...]
+1730,"{""ConnectionId"":,""BoardId"":8}","{""expand"":""operations,versionedRepresentations,editmeta,changelog,renderedFields"",""id"":""10099"",""self"":""https://merico.atlassian.net/rest/agile/1.0/issue/10099"",""key"":""EE-37"",""changelog"":{""startAt"":0,""maxResults"":13,""total"":13,""histories"":[{""id"":""90441"",""author"":{""self"":""https://merico.atlassian.net/rest/api/2/user?accountId=5e9711ba34f7b90c0fbc37d3"",""accountId"":""5e9711ba34f7b90c0fbc37d3"",""avatarUrls"":{""4 [...]
diff --git a/plugins/customize/e2e/raw_tables/issues.csv b/plugins/customize/e2e/raw_tables/issues.csv
new file mode 100644
index 00000000..8130ef26
--- /dev/null
+++ b/plugins/customize/e2e/raw_tables/issues.csv
@@ -0,0 +1,31 @@
+"id","created_at","updated_at","_raw_data_params","_raw_data_table","_raw_data_id","_raw_data_remark","url","icon_url","issue_key","title","description","epic_key","type","status","original_status","story_point","resolution_date","created_date","updated_date","lead_time_minutes","parent_issue_id","priority","original_estimate_minutes","time_spent_minutes","time_remaining_minutes","creator_id","creator_name","assignee_id","assignee_name","severity","component","deployment_id"
+"jira:JiraIssue:1:10063","2022-08-01 08:18:48.762","2022-08-23 13:12:41.015","{""ConnectionId"":1,""BoardId"":8}","_raw_jira_api_issues",1701,"","https://merico.atlassian.net/browse/EE-1","https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10315?size=medium","EE-1","​四个排序图:测试/注释覆盖度、复用度、模块性","","","STORY","DONE","已完成",0,"2020-06-19 06:31:18.495","2020-06-12 00:13:13.360","2021-03-28 08:06:08.713",10458,"","Medium",0,0,0,"jira:JiraAccount:1:5e9711ba34f7b90c [...]
+"jira:JiraIssue:1:10064","2022-08-01 08:18:48.762","2022-08-23 13:12:41.015","{""ConnectionId"":1,""BoardId"":8}","_raw_jira_api_issues",1702,"","https://merico.atlassian.net/browse/EE-2","https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10315?size=medium","EE-2","​问题堆叠分布排序图","","","STORY","DONE","已完成",0,"2020-06-23 10:20:58.999","2020-06-12 00:15:36.123","2021-03-28 08:05:55.016",16445,"","Medium",0,0,0,"jira:JiraAccount:1:5e9711ba34f7b90c0fbc37d3","Ra [...]
+"jira:JiraIssue:1:10065","2022-08-01 08:18:48.762","2022-08-23 13:12:41.015","{""ConnectionId"":1,""BoardId"":8}","_raw_jira_api_issues",1703,"","https://merico.atlassian.net/browse/EE-3","https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10315?size=medium","EE-3","​问题积压图率","","","STORY","DONE","已完成",0,"2020-06-23 10:21:11.996","2020-06-12 00:15:41.600","2021-03-28 08:05:55.061",16445,"","Medium",0,0,0,"jira:JiraAccount:1:5e9711ba34f7b90c0fbc37d3","Ranki [...]
+"jira:JiraIssue:1:10066","2022-08-01 08:18:48.762","2022-08-23 13:12:41.015","{""ConnectionId"":1,""BoardId"":8}","_raw_jira_api_issues",1704,"","https://merico.atlassian.net/browse/EE-4","https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10315?size=medium","EE-4","​问题分布的帕累托图","","","STORY","DONE","已完成",0,"2020-06-23 10:21:23.562","2020-06-12 00:15:46.144","2021-03-28 08:06:09.535",16445,"","Medium",0,0,0,"jira:JiraAccount:1:5e9711ba34f7b90c0fbc37d3","Ra [...]
+"jira:JiraIssue:1:10067","2022-08-01 08:18:48.762","2022-08-23 13:12:41.015","{""ConnectionId"":1,""BoardId"":8}","_raw_jira_api_issues",1705,"","https://merico.atlassian.net/browse/EE-5","https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10318?size=medium","EE-5","​通用技术任务","","","TASK","DONE","已完成",0,"2020-06-18 04:06:00.747","2020-06-12 00:16:44.157","2021-03-28 08:05:54.622",8869,"","Medium",0,0,0,"jira:JiraAccount:1:5e9711ba34f7b90c0fbc37d3","Rankin  [...]
+"jira:JiraIssue:1:10068","2022-08-01 08:18:48.762","2022-08-23 13:12:41.015","{""ConnectionId"":1,""BoardId"":8}","_raw_jira_api_issues",1706,"","https://merico.atlassian.net/browse/EE-6","https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10315?size=medium","EE-6","​变异系数、生产率的四象限图","","","STORY","DONE","已完成",0,"2020-06-16 11:56:14.433","2020-06-12 00:17:26.986","2021-03-28 08:05:56.750",6458,"","Medium",0,0,0,"jira:JiraAccount:1:5e9711ba34f7b90c0fbc37d3", [...]
+"jira:JiraIssue:1:10070","2022-08-01 08:18:48.762","2022-08-23 13:12:41.015","{""ConnectionId"":1,""BoardId"":8}","_raw_jira_api_issues",1707,"","https://merico.atlassian.net/browse/EE-8","https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10318?size=medium","EE-8","​多团队支持","","","TASK","DONE","已完成",0,"2020-07-08 17:11:45.201","2020-06-12 00:18:58.050","2021-03-28 08:05:54.576",38452,"","Medium",0,0,0,"jira:JiraAccount:1:5e9711ba34f7b90c0fbc37d3","Rankin  [...]
+"jira:JiraIssue:1:10071","2022-08-01 08:18:48.762","2022-08-23 13:12:41.015","{""ConnectionId"":1,""BoardId"":8}","_raw_jira_api_issues",1708,"","https://merico.atlassian.net/browse/EE-9","https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10318?size=medium","EE-9","Common backend 拆分","","","TASK","DONE","已完成",0,"2020-07-08 17:12:05.663","2020-06-12 00:19:17.336","2021-03-28 08:06:20.165",38452,"","Medium",1680,0,0,"jira:JiraAccount:1:5e9711ba34f7b90c0fbc [...]
+"jira:JiraIssue:1:10072","2022-08-01 08:18:48.762","2022-08-23 13:12:41.015","{""ConnectionId"":1,""BoardId"":8}","_raw_jira_api_issues",1709,"","https://merico.atlassian.net/browse/EE-10","https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10318?size=medium","EE-10","​部署SaaS版本","","","TASK","DONE","已完成",0,"2020-07-08 17:11:55.247","2020-06-12 00:19:24.637","2021-03-28 08:05:54.472",38452,"","Medium",0,0,0,"jira:JiraAccount:1:5e9711ba34f7b90c0fbc37d3","Ra [...]
+"jira:JiraIssue:1:10076","2022-08-01 08:18:48.762","2022-08-23 13:12:41.015","{""ConnectionId"":1,""BoardId"":8}","_raw_jira_api_issues",1710,"","https://merico.atlassian.net/browse/EE-14","https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10318?size=medium","EE-14","调整​文件夹结构","","","TASK","DONE","已完成",0,"2020-06-15 08:59:51.304","2020-06-12 00:24:25.922","2021-03-28 08:05:56.152",4835,"","Medium",240,600,0,"jira:JiraAccount:1:5e9711ba34f7b90c0fbc37d3"," [...]
+"jira:JiraIssue:1:10077","2022-08-01 08:18:48.762","2022-08-23 13:12:41.015","{""ConnectionId"":1,""BoardId"":8}","_raw_jira_api_issues",1711,"","https://merico.atlassian.net/browse/EE-15","https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10318?size=medium","EE-15","路由权限控制","","","TASK","DONE","已完成",0,"2020-06-15 09:00:26.956","2020-06-12 00:24:39.624","2021-03-28 08:06:01.995",4835,"","Medium",240,480,0,"jira:JiraAccount:1:5e9711ba34f7b90c0fbc37d3","Ra [...]
+"jira:JiraIssue:1:10078","2022-08-01 08:18:48.762","2022-08-23 13:12:41.015","{""ConnectionId"":1,""BoardId"":8}","_raw_jira_api_issues",1712,"","https://merico.atlassian.net/browse/EE-16","https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10318?size=medium","EE-16","​优化前端 webpack 开发阶段构建速度","","","TASK","DONE","已完成",0,"2020-06-15 09:01:44.159","2020-06-12 00:24:49.017","2021-03-28 08:05:55.863",4836,"","Medium",0,60,0,"jira:JiraAccount:1:5e9711ba34f7b90c [...]
+"jira:JiraIssue:1:10079","2022-08-01 08:18:48.762","2022-08-23 13:12:41.015","{""ConnectionId"":1,""BoardId"":8}","_raw_jira_api_issues",1713,"","https://merico.atlassian.net/browse/EE-17","https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10318?size=medium","EE-17","​新的错误处理机制","","","TASK","DONE","已完成",0,"2020-07-22 07:25:29.104","2020-06-12 00:24:56.048","2021-03-28 08:05:54.426",58020,"","Medium",360,120,0,"jira:JiraAccount:1:5e9711ba34f7b90c0fbc37d3" [...]
+"jira:JiraIssue:1:10081","2022-08-01 08:18:48.762","2022-08-23 13:12:41.015","{""ConnectionId"":1,""BoardId"":8}","_raw_jira_api_issues",1714,"","https://merico.atlassian.net/browse/EE-19","https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10315?size=medium","EE-19","​LDAP需要支持TLS和证书","","","STORY","DONE","已完成",0,"2020-06-18 08:34:11.117","2020-06-12 00:28:00.241","2021-03-28 08:05:57.326",9126,"","Medium",0,0,0,"jira:JiraAccount:1:5e9711ba34f7b90c0fbc37d [...]
+"jira:JiraIssue:1:10082","2022-08-01 08:18:48.762","2022-08-23 13:12:41.015","{""ConnectionId"":1,""BoardId"":8}","_raw_jira_api_issues",1715,"","https://merico.atlassian.net/browse/EE-20","https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10315?size=medium","EE-20","团队首页垂直化","","","STORY","DONE","已完成",0,"2020-06-17 07:25:54.370","2020-06-12 00:29:43.677","2021-08-06 06:14:54.647",7616,"","Medium",0,0,0,"jira:JiraAccount:1:5e9711ba34f7b90c0fbc37d3","Rank [...]
+"jira:JiraIssue:1:10085","2022-08-01 08:18:48.762","2022-08-23 13:12:41.015","{""ConnectionId"":1,""BoardId"":8}","_raw_jira_api_issues",1716,"","https://merico.atlassian.net/browse/EE-23","https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10303?size=medium","EE-23","​批量删除事故","","","BUG","DONE","已完成",0,"2020-06-15 09:07:56.798","2020-06-12 00:33:57.204","2021-03-28 08:05:57.095",4833,"","Medium",0,60,0,"jira:JiraAccount:1:5e9711ba34f7b90c0fbc37d3","Ranki [...]
+"jira:JiraIssue:1:10086","2022-08-01 08:18:48.762","2022-08-23 13:12:41.015","{""ConnectionId"":1,""BoardId"":8}","_raw_jira_api_issues",1717,"","https://merico.atlassian.net/browse/EE-24","https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10315?size=medium","EE-24","​LDAP支持自定义的证书上传","","","STORY","DONE","已完成",0,"2020-06-12 07:17:28.659","2020-06-12 00:35:15.489","2021-03-28 08:05:55.819",402,"","Medium",0,0,0,"jira:JiraAccount:1:5e9711ba34f7b90c0fbc37d3 [...]
+"jira:JiraIssue:1:10087","2022-08-01 08:18:48.762","2022-08-23 13:12:41.015","{""ConnectionId"":1,""BoardId"":8}","_raw_jira_api_issues",1718,"","https://merico.atlassian.net/browse/EE-25","https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10316?size=medium","EE-25","​组件封装及Demo","","","SUB-TASK","DONE","已完成",0,"2020-06-18 04:02:22.350","2020-06-12 00:40:54.210","2021-03-28 08:05:57.189",8841,"jira:JiraIssue:1:10063","Medium",240,0,0,"jira:JiraAccount:1:5 [...]
+"jira:JiraIssue:1:10088","2022-08-01 08:18:48.762","2022-08-23 13:12:41.015","{""ConnectionId"":1,""BoardId"":8}","_raw_jira_api_issues",1719,"","https://merico.atlassian.net/browse/EE-26","https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10316?size=medium","EE-26","​定接口","","","SUB-TASK","DONE","已完成",0,"2020-06-15 09:06:51.438","2020-06-12 00:41:01.683","2021-03-28 08:05:55.208",4825,"jira:JiraIssue:1:10063","Medium",0,0,0,"jira:JiraAccount:1:5e9711ba3 [...]
+"jira:JiraIssue:1:10089","2022-08-01 08:18:48.762","2022-08-23 13:12:41.015","{""ConnectionId"":1,""BoardId"":8}","_raw_jira_api_issues",1720,"","https://merico.atlassian.net/browse/EE-27","https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10316?size=medium","EE-27","​提供后端接口","","","SUB-TASK","DONE","已完成",0,"2020-06-19 06:31:31.662","2020-06-12 00:41:16.622","2021-03-28 08:05:55.498",10430,"jira:JiraIssue:1:10063","Medium",660,0,0,"jira:JiraAccount:1:5e9 [...]
+"jira:JiraIssue:1:10090","2022-08-01 08:18:48.762","2022-08-23 13:12:41.015","{""ConnectionId"":1,""BoardId"":8}","_raw_jira_api_issues",1721,"","https://merico.atlassian.net/browse/EE-28","https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10316?size=medium","EE-28","​数据填充与联调","","","SUB-TASK","DONE","已完成",0,"2020-06-18 04:03:04.637","2020-06-12 00:41:36.317","2021-03-28 08:06:05.443",8841,"jira:JiraIssue:1:10063","Medium",360,0,0,"jira:JiraAccount:1:5e9 [...]
+"jira:JiraIssue:1:10091","2022-08-01 08:18:48.762","2022-08-23 13:12:41.015","{""ConnectionId"":1,""BoardId"":8}","_raw_jira_api_issues",1722,"","https://merico.atlassian.net/browse/EE-29","https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10316?size=medium","EE-29","​组件封装及Demo","","","SUB-TASK","DONE","已完成",0,"2020-06-18 04:03:30.760","2020-06-12 00:48:29.035","2021-03-28 08:05:55.731",8835,"jira:JiraIssue:1:10064","Medium",360,0,0,"jira:JiraAccount:1:5 [...]
+"jira:JiraIssue:1:10092","2022-08-01 08:18:48.762","2022-08-23 13:12:41.015","{""ConnectionId"":1,""BoardId"":8}","_raw_jira_api_issues",1723,"","https://merico.atlassian.net/browse/EE-30","https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10316?size=medium","EE-30","​定接口","","","SUB-TASK","DONE","已完成",0,"2020-06-15 09:06:40.206","2020-06-12 00:48:39.803","2021-03-28 08:05:55.159",4818,"jira:JiraIssue:1:10064","Medium",0,0,0,"jira:JiraAccount:1:5e9711ba3 [...]
+"jira:JiraIssue:1:10093","2022-08-01 08:18:48.762","2022-08-23 13:12:41.015","{""ConnectionId"":1,""BoardId"":8}","_raw_jira_api_issues",1724,"","https://merico.atlassian.net/browse/EE-31","https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10316?size=medium","EE-31","​后端接口","","","SUB-TASK","DONE","已完成",0,"2020-06-19 07:35:31.762","2020-06-12 00:48:46.751","2021-03-28 08:05:55.544",10486,"jira:JiraIssue:1:10064","Medium",120,0,0,"jira:JiraAccount:1:5e971 [...]
+"jira:JiraIssue:1:10094","2022-08-01 08:18:48.762","2022-08-23 13:12:41.015","{""ConnectionId"":1,""BoardId"":8}","_raw_jira_api_issues",1725,"","https://merico.atlassian.net/browse/EE-32","https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10316?size=medium","EE-32","​数据填充与联调","","","SUB-TASK","DONE","已完成",0,"2020-06-18 04:03:48.818","2020-06-12 00:48:53.279","2021-03-28 08:05:55.592",8834,"jira:JiraIssue:1:10064","Medium",360,0,0,"jira:JiraAccount:1:5e9 [...]
+"jira:JiraIssue:1:10095","2022-08-01 08:18:48.762","2022-08-23 13:12:41.015","{""ConnectionId"":1,""BoardId"":8}","_raw_jira_api_issues",1726,"","https://merico.atlassian.net/browse/EE-33","https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10316?size=medium","EE-33","准备测试用例","","","SUB-TASK","DONE","已完成",0,"2020-06-19 06:32:19.340","2020-06-12 00:50:45.674","2021-03-28 08:05:55.253",10421,"jira:JiraIssue:1:10063","Medium",0,0,0,"jira:JiraAccount:1:5e9711 [...]
+"jira:JiraIssue:1:10096","2022-08-01 08:18:48.762","2022-08-23 13:12:41.015","{""ConnectionId"":1,""BoardId"":8}","_raw_jira_api_issues",1727,"","https://merico.atlassian.net/browse/EE-34","https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10316?size=medium","EE-34","​ 组件封装及Demo","","","SUB-TASK","DONE","已完成",0,"2020-06-18 04:04:05.951","2020-06-12 00:51:57.807","2021-03-28 08:05:57.232",8832,"jira:JiraIssue:1:10065","Medium",240,0,0,"jira:JiraAccount:1: [...]
+"jira:JiraIssue:1:10097","2022-08-01 08:18:48.762","2022-08-23 13:12:41.015","{""ConnectionId"":1,""BoardId"":8}","_raw_jira_api_issues",1728,"","https://merico.atlassian.net/browse/EE-35","https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10316?size=medium","EE-35","定接口","","","SUB-TASK","DONE","已完成",0,"2020-06-15 09:06:30.942","2020-06-12 00:52:04.767","2021-03-28 08:05:55.353",4814,"jira:JiraIssue:1:10065","Medium",0,0,0,"jira:JiraAccount:1:5e9711ba34 [...]
+"jira:JiraIssue:1:10098","2022-08-01 08:18:48.762","2022-08-23 13:12:41.015","{""ConnectionId"":1,""BoardId"":8}","_raw_jira_api_issues",1729,"","https://merico.atlassian.net/browse/EE-36","https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10316?size=medium","EE-36","后端接口","","","SUB-TASK","DONE","已完成",0,"2020-06-19 07:35:44.696","2020-06-12 00:52:12.678","2021-03-28 08:05:55.685",10483,"jira:JiraIssue:1:10065","Medium",120,0,0,"jira:JiraAccount:1:5e9711 [...]
+"jira:JiraIssue:1:10099","2022-08-01 08:18:48.762","2022-08-23 13:12:41.015","{""ConnectionId"":1,""BoardId"":8}","_raw_jira_api_issues",1730,"","https://merico.atlassian.net/browse/EE-37","https://merico.atlassian.net/rest/api/2/universal_avatar/view/type/issuetype/avatar/10316?size=medium","EE-37","数据填充与联调 ","","","SUB-TASK","DONE","已完成",0,"2020-06-18 04:04:31.261","2020-06-12 00:52:23.456","2021-03-28 08:05:55.638",8832,"jira:JiraIssue:1:10065","Medium",0,0,0,"jira:JiraAccount:1:5e971 [...]
diff --git a/plugins/customize/e2e/snapshot_tables/issues.csv b/plugins/customize/e2e/snapshot_tables/issues.csv
new file mode 100644
index 00000000..2a662778
--- /dev/null
+++ b/plugins/customize/e2e/snapshot_tables/issues.csv
@@ -0,0 +1,31 @@
+id,_raw_data_params,_raw_data_table,_raw_data_id,x_test
+jira:JiraIssue:1:10063,"{""ConnectionId"":1,""BoardId"":8}",_raw_jira_api_issues,1701,2020-06-12T08:13:13.360+0800
+jira:JiraIssue:1:10064,"{""ConnectionId"":1,""BoardId"":8}",_raw_jira_api_issues,1702,2020-06-12T08:15:36.123+0800
+jira:JiraIssue:1:10065,"{""ConnectionId"":1,""BoardId"":8}",_raw_jira_api_issues,1703,2020-06-12T08:15:41.600+0800
+jira:JiraIssue:1:10066,"{""ConnectionId"":1,""BoardId"":8}",_raw_jira_api_issues,1704,2020-06-12T08:15:46.144+0800
+jira:JiraIssue:1:10067,"{""ConnectionId"":1,""BoardId"":8}",_raw_jira_api_issues,1705,2020-06-12T08:16:44.157+0800
+jira:JiraIssue:1:10068,"{""ConnectionId"":1,""BoardId"":8}",_raw_jira_api_issues,1706,2020-06-12T08:17:26.986+0800
+jira:JiraIssue:1:10070,"{""ConnectionId"":1,""BoardId"":8}",_raw_jira_api_issues,1707,2020-06-12T08:18:58.050+0800
+jira:JiraIssue:1:10071,"{""ConnectionId"":1,""BoardId"":8}",_raw_jira_api_issues,1708,2020-06-12T08:19:17.336+0800
+jira:JiraIssue:1:10072,"{""ConnectionId"":1,""BoardId"":8}",_raw_jira_api_issues,1709,2020-06-12T08:19:24.637+0800
+jira:JiraIssue:1:10076,"{""ConnectionId"":1,""BoardId"":8}",_raw_jira_api_issues,1710,2020-06-12T08:24:25.922+0800
+jira:JiraIssue:1:10077,"{""ConnectionId"":1,""BoardId"":8}",_raw_jira_api_issues,1711,2020-06-12T08:24:39.624+0800
+jira:JiraIssue:1:10078,"{""ConnectionId"":1,""BoardId"":8}",_raw_jira_api_issues,1712,2020-06-12T08:24:49.017+0800
+jira:JiraIssue:1:10079,"{""ConnectionId"":1,""BoardId"":8}",_raw_jira_api_issues,1713,2020-06-12T08:24:56.048+0800
+jira:JiraIssue:1:10081,"{""ConnectionId"":1,""BoardId"":8}",_raw_jira_api_issues,1714,2020-06-12T08:28:00.241+0800
+jira:JiraIssue:1:10082,"{""ConnectionId"":1,""BoardId"":8}",_raw_jira_api_issues,1715,2020-06-12T08:29:43.677+0800
+jira:JiraIssue:1:10085,"{""ConnectionId"":1,""BoardId"":8}",_raw_jira_api_issues,1716,2020-06-12T08:33:57.204+0800
+jira:JiraIssue:1:10086,"{""ConnectionId"":1,""BoardId"":8}",_raw_jira_api_issues,1717,2020-06-12T08:35:15.489+0800
+jira:JiraIssue:1:10087,"{""ConnectionId"":1,""BoardId"":8}",_raw_jira_api_issues,1718,2020-06-12T08:40:54.210+0800
+jira:JiraIssue:1:10088,"{""ConnectionId"":1,""BoardId"":8}",_raw_jira_api_issues,1719,2020-06-12T08:41:01.683+0800
+jira:JiraIssue:1:10089,"{""ConnectionId"":1,""BoardId"":8}",_raw_jira_api_issues,1720,2020-06-12T08:41:16.622+0800
+jira:JiraIssue:1:10090,"{""ConnectionId"":1,""BoardId"":8}",_raw_jira_api_issues,1721,2020-06-12T08:41:36.317+0800
+jira:JiraIssue:1:10091,"{""ConnectionId"":1,""BoardId"":8}",_raw_jira_api_issues,1722,2020-06-12T08:48:29.035+0800
+jira:JiraIssue:1:10092,"{""ConnectionId"":1,""BoardId"":8}",_raw_jira_api_issues,1723,2020-06-12T08:48:39.803+0800
+jira:JiraIssue:1:10093,"{""ConnectionId"":1,""BoardId"":8}",_raw_jira_api_issues,1724,2020-06-12T08:48:46.751+0800
+jira:JiraIssue:1:10094,"{""ConnectionId"":1,""BoardId"":8}",_raw_jira_api_issues,1725,2020-06-12T08:48:53.279+0800
+jira:JiraIssue:1:10095,"{""ConnectionId"":1,""BoardId"":8}",_raw_jira_api_issues,1726,2020-06-12T08:50:45.674+0800
+jira:JiraIssue:1:10096,"{""ConnectionId"":1,""BoardId"":8}",_raw_jira_api_issues,1727,2020-06-12T08:51:57.807+0800
+jira:JiraIssue:1:10097,"{""ConnectionId"":1,""BoardId"":8}",_raw_jira_api_issues,1728,2020-06-12T08:52:04.767+0800
+jira:JiraIssue:1:10098,"{""ConnectionId"":1,""BoardId"":8}",_raw_jira_api_issues,1729,2020-06-12T08:52:12.678+0800
+jira:JiraIssue:1:10099,"{""ConnectionId"":1,""BoardId"":8}",_raw_jira_api_issues,1730,2020-06-12T08:52:23.456+0800
diff --git a/plugins/customize/impl/impl.go b/plugins/customize/impl/impl.go
new file mode 100644
index 00000000..e5e5abdc
--- /dev/null
+++ b/plugins/customize/impl/impl.go
@@ -0,0 +1,88 @@
+/*
+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 impl
+
+import (
+	"fmt"
+
+	"github.com/apache/incubator-devlake/plugins/core"
+	"github.com/apache/incubator-devlake/plugins/customize/api"
+	"github.com/apache/incubator-devlake/plugins/customize/tasks"
+	"github.com/apache/incubator-devlake/plugins/helper"
+	"github.com/mitchellh/mapstructure"
+	"github.com/spf13/viper"
+	"gorm.io/gorm"
+)
+
+var _ core.PluginMeta = (*Customize)(nil)
+var _ core.PluginInit = (*Customize)(nil)
+var _ core.PluginApi = (*Customize)(nil)
+
+type Customize struct {
+	handlers *api.Handlers
+}
+
+func (plugin *Customize) Init(config *viper.Viper, logger core.Logger, db *gorm.DB) error {
+	basicRes := helper.NewDefaultBasicRes(config, logger, db)
+	plugin.handlers = api.NewHandlers(basicRes.GetDal())
+	return nil
+}
+
+func (plugin Customize) SubTaskMetas() []core.SubTaskMeta {
+	return []core.SubTaskMeta{
+		tasks.ExtractCustomizedFieldsMeta,
+	}
+}
+
+func (plugin Customize) MakePipelinePlan(connectionId uint64, scope []*core.BlueprintScopeV100) (core.PipelinePlan, error) {
+	return api.MakePipelinePlan(plugin.SubTaskMetas(), connectionId, scope)
+}
+func (plugin Customize) PrepareTaskData(taskCtx core.TaskContext, options map[string]interface{}) (interface{}, error) {
+	var op tasks.Options
+	var err error
+	logger := taskCtx.GetLogger()
+	logger.Debug("%v", options)
+	err = mapstructure.Decode(options, &op)
+	if err != nil {
+		return nil, fmt.Errorf("could not decode Jira options: %v", err)
+	}
+	taskData := &tasks.TaskData{
+		Options: &op,
+	}
+	return taskData, nil
+}
+
+func (plugin Customize) Description() string {
+	return "To customize table fields"
+}
+
+func (plugin Customize) RootPkgPath() string {
+	return "github.com/apache/incubator-devlake/plugins/customize"
+}
+
+func (plugin *Customize) ApiResources() map[string]map[string]core.ApiResourceHandler {
+	return map[string]map[string]core.ApiResourceHandler{
+		":table/fields": {
+			"GET":  plugin.handlers.ListFields,
+			"POST": plugin.handlers.CreateFields,
+		},
+		":table/fields/:field": {
+			"DELETE": plugin.handlers.DeleteField,
+		},
+	}
+}
diff --git a/plugins/customize/models/model.go b/plugins/customize/models/model.go
new file mode 100644
index 00000000..8a251450
--- /dev/null
+++ b/plugins/customize/models/model.go
@@ -0,0 +1,26 @@
+/*
+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 models
+
+type Table struct {
+	Name string
+}
+
+func (t *Table) TableName() string {
+	return t.Name
+}
diff --git a/plugins/customize/tasks/customized_fields_extractor.go b/plugins/customize/tasks/customized_fields_extractor.go
new file mode 100644
index 00000000..6bf1f9ce
--- /dev/null
+++ b/plugins/customize/tasks/customized_fields_extractor.go
@@ -0,0 +1,134 @@
+/*
+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 tasks
+
+import (
+	"context"
+	"fmt"
+	"github.com/apache/incubator-devlake/errors"
+	"strings"
+
+	"github.com/apache/incubator-devlake/plugins/core"
+	"github.com/apache/incubator-devlake/plugins/core/dal"
+	"github.com/apache/incubator-devlake/plugins/customize/models"
+	"github.com/tidwall/gjson"
+)
+
+var _ core.SubTaskEntryPoint = ExtractCustomizedFields
+
+var ExtractCustomizedFieldsMeta = core.SubTaskMeta{Name: "extractCustomizedFields",
+	EntryPoint:       ExtractCustomizedFields,
+	EnabledByDefault: true,
+	Description:      "extract customized fields",
+}
+
+// ExtractCustomizedFields extracts fields from raw data tables and assigns to domain layer tables
+func ExtractCustomizedFields(taskCtx core.SubTaskContext) error {
+	data := taskCtx.GetData().(*TaskData)
+	if data == nil || data.Options == nil {
+		return nil
+	}
+	d := taskCtx.GetDal()
+	var err error
+	for _, rule := range data.Options.TransformationRules {
+		err = extractCustomizedFields(taskCtx.GetContext(), d, rule.Table, rule.RawDataTable, rule.RawDataParams, rule.Mapping)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func extractCustomizedFields(ctx context.Context, d dal.Dal, table, rawTable, rawDataParams string, extractor map[string]string) error {
+	pkFields, err := dal.GetPrimarykeyColumns(d, &models.Table{table})
+	if err != nil {
+		return err
+	}
+	rawDataField := fmt.Sprintf("%s.data", rawTable)
+	// `fields` only include `_raw_data_id` and primary keys coming from the domain layer table, and `data` coming from the raw layer
+	fields := []string{fmt.Sprintf("%s.%s", table, "_raw_data_id")}
+	fields = append(fields, rawDataField)
+	for _, field := range pkFields {
+		fields = append(fields, fmt.Sprintf("%s.%s", table, field.Name()))
+	}
+	clauses := []dal.Clause{
+		dal.Select(strings.Join(fields, ", ")),
+		dal.From(table),
+		dal.Join(fmt.Sprintf(" LEFT JOIN %s ON %s._raw_data_id = %s.id", rawTable, table, rawTable)),
+		dal.Where("_raw_data_table = ? AND _raw_data_params = ?", rawTable, rawDataParams),
+	}
+	rows, err := d.Cursor(clauses...)
+	if err != nil {
+		return err
+	}
+	defer rows.Close()
+
+	for rows.Next() {
+		select {
+		case <-ctx.Done():
+			return ctx.Err()
+		default:
+		}
+		row := make(map[string]interface{})
+		updates := make(map[string]string)
+		err = d.Fetch(rows, &row)
+		if err != nil {
+			return err
+		}
+		switch blob := row["data"].(type) {
+		case []byte:
+			for field, path := range extractor {
+				updates[field] = gjson.GetBytes(blob, path).String()
+			}
+		case string:
+			for field, path := range extractor {
+				updates[field] = gjson.Get(blob, path).String()
+			}
+		default:
+			return nil
+		}
+
+		if len(updates) > 0 {
+			// remove columns that are not primary key
+			delete(row, "_raw_data_id")
+			delete(row, "data")
+			query, params := mkUpdate(table, updates, row)
+			err = d.Exec(query, params...)
+			if err != nil {
+				return errors.Default.Wrap(err, "Exec SQL error")
+			}
+		}
+	}
+	return nil
+}
+
+func mkUpdate(table string, updates map[string]string, pk map[string]interface{}) (string, []interface{}) {
+	var params []interface{}
+	stat := fmt.Sprintf("UPDATE %s SET ", table)
+	var uu []string
+	for field, value := range updates {
+		uu = append(uu, fmt.Sprintf("%s = ?", field))
+		params = append(params, value)
+	}
+	var ww []string
+	for field, value := range pk {
+		ww = append(ww, fmt.Sprintf("%s = ?", field))
+		params = append(params, value)
+	}
+	return stat + strings.Join(uu, ", ") + " WHERE " + strings.Join(ww, " AND "), params
+}
diff --git a/plugins/customize/tasks/task_data.go b/plugins/customize/tasks/task_data.go
new file mode 100644
index 00000000..62b69d7e
--- /dev/null
+++ b/plugins/customize/tasks/task_data.go
@@ -0,0 +1,33 @@
+/*
+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 tasks
+
+type MappingRules struct {
+	Table         string            `json:"table" example:"issues"`
+	RawDataTable  string            `json:"_raw_data_table" example:"_raw_jira_api_issues"`
+	RawDataParams string            `json:"_raw_data_params" example:"{\"ConnectionId\":1,\"BoardId\":8}"`
+	Mapping       map[string]string `json:"mapping" example:"x_text:fields.created"`
+}
+
+type Options struct {
+	TransformationRules []MappingRules `json:"transformationRules"`
+}
+
+type TaskData struct {
+	Options *Options
+}