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/05/25 14:28:38 UTC

[incubator-devlake] branch main updated: feat(tapd): add new entities (#2008)

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 3a46af43 feat(tapd): add new entities (#2008)
3a46af43 is described below

commit 3a46af43749c3f8a09332064631ab7fa19291ac0
Author: Warren Chen <yi...@merico.dev>
AuthorDate: Wed May 25 22:28:33 2022 +0800

    feat(tapd): add new entities (#2008)
    
    closes #2007
---
 plugins/tapd/models/bug_custom_field.go            | 21 +++++
 .../migrationscripts/archived/bug_custom_field.go  | 21 +++++
 .../archived/story_custom_field.go                 | 21 +++++
 .../migrationscripts/archived/task_custom_field.go | 21 +++++
 .../models/migrationscripts/archived/workspace.go  |  1 +
 .../tapd/models/migrationscripts/init_schema.go    |  3 +
 plugins/tapd/models/story_custom_field.go          | 21 +++++
 plugins/tapd/models/task_custom_field.go           | 21 +++++
 plugins/tapd/models/workspace.go                   |  1 +
 plugins/tapd/tapd.go                               | 98 +++++++++++-----------
 plugins/tapd/tasks/company_collector.go            | 76 +++++++++++++++++
 plugins/tapd/tasks/company_extractor.go            | 67 +++++++++++++++
 .../tapd/tasks/story_custom_fields_collector.go    | 74 ++++++++++++++++
 .../tapd/tasks/story_custom_fields_extractor.go    | 72 ++++++++++++++++
 14 files changed, 470 insertions(+), 48 deletions(-)

diff --git a/plugins/tapd/models/bug_custom_field.go b/plugins/tapd/models/bug_custom_field.go
new file mode 100644
index 00000000..c2c5bbd9
--- /dev/null
+++ b/plugins/tapd/models/bug_custom_field.go
@@ -0,0 +1,21 @@
+package models
+
+import "github.com/apache/incubator-devlake/models/common"
+
+type TapdBugCustomFields struct {
+	ConnectionId uint64 `gorm:"primaryKey;type:BIGINT  NOT NULL"`
+	ID           uint64 `gorm:"primaryKey;type:BIGINT  NOT NULL" json:"id,string"`
+	WorkspaceID  uint64 `json:"workspace_id,string"`
+	EntryType    string `json:"entry_type"`
+	CustomField  string `json:"custom_field"`
+	Type         string `json:"type"`
+	Name         string `json:"name"`
+	Options      string `json:"options"`
+	Enabled      string `json:"enabled"`
+	Sort         string `json:"sort"`
+	common.NoPKModel
+}
+
+func (TapdBugCustomFields) TableName() string {
+	return "_tool_tapd_bug_custom_fields"
+}
diff --git a/plugins/tapd/models/migrationscripts/archived/bug_custom_field.go b/plugins/tapd/models/migrationscripts/archived/bug_custom_field.go
new file mode 100644
index 00000000..41681c2d
--- /dev/null
+++ b/plugins/tapd/models/migrationscripts/archived/bug_custom_field.go
@@ -0,0 +1,21 @@
+package archived
+
+import "github.com/apache/incubator-devlake/models/common"
+
+type TapdBugCustomFields struct {
+	ConnectionId uint64 `gorm:"primaryKey;type:BIGINT  NOT NULL"`
+	ID           uint64 `gorm:"primaryKey;type:BIGINT  NOT NULL" json:"id,string"`
+	WorkspaceID  uint64 `json:"workspace_id,string"`
+	EntryType    string `json:"entry_type"`
+	CustomField  string `json:"custom_field"`
+	Type         string `json:"type"`
+	Name         string `json:"name"`
+	Options      string `json:"options"`
+	Enabled      string `json:"enabled"`
+	Sort         string `json:"sort"`
+	common.NoPKModel
+}
+
+func (TapdBugCustomFields) TableName() string {
+	return "_tool_tapd_bug_custom_fields"
+}
diff --git a/plugins/tapd/models/migrationscripts/archived/story_custom_field.go b/plugins/tapd/models/migrationscripts/archived/story_custom_field.go
new file mode 100644
index 00000000..e10b3943
--- /dev/null
+++ b/plugins/tapd/models/migrationscripts/archived/story_custom_field.go
@@ -0,0 +1,21 @@
+package archived
+
+import "github.com/apache/incubator-devlake/models/common"
+
+type TapdStoryCustomFields struct {
+	ConnectionId uint64 `gorm:"primaryKey;type:BIGINT  NOT NULL"`
+	ID           uint64 `gorm:"primaryKey;type:BIGINT  NOT NULL" json:"id,string"`
+	WorkspaceID  uint64 `json:"workspace_id,string"`
+	EntryType    string `json:"entry_type"`
+	CustomField  string `json:"custom_field"`
+	Type         string `json:"type"`
+	Name         string `json:"name"`
+	Options      string `json:"options"`
+	Enabled      string `json:"enabled"`
+	Sort         string `json:"sort"`
+	common.NoPKModel
+}
+
+func (TapdStoryCustomFields) TableName() string {
+	return "_tool_tapd_story_custom_fields"
+}
diff --git a/plugins/tapd/models/migrationscripts/archived/task_custom_field.go b/plugins/tapd/models/migrationscripts/archived/task_custom_field.go
new file mode 100644
index 00000000..6350bc84
--- /dev/null
+++ b/plugins/tapd/models/migrationscripts/archived/task_custom_field.go
@@ -0,0 +1,21 @@
+package archived
+
+import "github.com/apache/incubator-devlake/models/common"
+
+type TapdTaskCustomFields struct {
+	ConnectionId uint64 `gorm:"primaryKey;type:BIGINT  NOT NULL"`
+	ID           uint64 `gorm:"primaryKey;type:BIGINT  NOT NULL" json:"id,string"`
+	WorkspaceID  uint64 `json:"workspace_id,string"`
+	EntryType    string `json:"entry_type"`
+	CustomField  string `json:"custom_field"`
+	Type         string `json:"type"`
+	Name         string `json:"name"`
+	Options      string `json:"options"`
+	Enabled      string `json:"enabled"`
+	Sort         string `json:"sort"`
+	common.NoPKModel
+}
+
+func (TapdTaskCustomFields) TableName() string {
+	return "_tool_tapd_task_custom_fields"
+}
diff --git a/plugins/tapd/models/migrationscripts/archived/workspace.go b/plugins/tapd/models/migrationscripts/archived/workspace.go
index 7aeaa11c..2c5cb918 100644
--- a/plugins/tapd/models/migrationscripts/archived/workspace.go
+++ b/plugins/tapd/models/migrationscripts/archived/workspace.go
@@ -33,6 +33,7 @@ type TapdWorkspace struct {
 	BeginDate    *helper.CSTTime `json:"begin_date"`
 	EndDate      *helper.CSTTime `json:"end_date"`
 	ExternalOn   string          `gorm:"type:varchar(255)" json:"external_on"`
+	ParentId     uint64          `gorm:"type:BIGINT" json:"parent_id,string"`
 	Creator      string          `gorm:"type:varchar(255)" json:"creator"`
 	Created      *helper.CSTTime `json:"created"`
 	common.NoPKModel
diff --git a/plugins/tapd/models/migrationscripts/init_schema.go b/plugins/tapd/models/migrationscripts/init_schema.go
index 543ffdef..65bf881b 100644
--- a/plugins/tapd/models/migrationscripts/init_schema.go
+++ b/plugins/tapd/models/migrationscripts/init_schema.go
@@ -57,6 +57,9 @@ func (*InitSchemas) Up(ctx context.Context, db *gorm.DB) error {
 		&archived.TapdIterationBug{},
 		&archived.TapdIterationStory{},
 		&archived.TapdIterationTask{},
+		&archived.TapdStoryCustomFields{},
+		&archived.TapdBugCustomFields{},
+		&archived.TapdTaskCustomFields{},
 	)
 }
 
diff --git a/plugins/tapd/models/story_custom_field.go b/plugins/tapd/models/story_custom_field.go
new file mode 100644
index 00000000..02cbe9c5
--- /dev/null
+++ b/plugins/tapd/models/story_custom_field.go
@@ -0,0 +1,21 @@
+package models
+
+import "github.com/apache/incubator-devlake/models/common"
+
+type TapdStoryCustomFields struct {
+	ConnectionId uint64 `gorm:"primaryKey;type:BIGINT  NOT NULL"`
+	ID           uint64 `gorm:"primaryKey;type:BIGINT  NOT NULL" json:"id,string"`
+	WorkspaceID  uint64 `json:"workspace_id,string"`
+	EntryType    string `json:"entry_type"`
+	CustomField  string `json:"custom_field"`
+	Type         string `json:"type"`
+	Name         string `json:"name"`
+	Options      string `json:"options"`
+	Enabled      string `json:"enabled"`
+	Sort         string `json:"sort"`
+	common.NoPKModel
+}
+
+func (TapdStoryCustomFields) TableName() string {
+	return "_tool_tapd_story_custom_fields"
+}
diff --git a/plugins/tapd/models/task_custom_field.go b/plugins/tapd/models/task_custom_field.go
new file mode 100644
index 00000000..4b4bf81b
--- /dev/null
+++ b/plugins/tapd/models/task_custom_field.go
@@ -0,0 +1,21 @@
+package models
+
+import "github.com/apache/incubator-devlake/models/common"
+
+type TapdTaskCustomFields struct {
+	ConnectionId uint64 `gorm:"primaryKey;type:BIGINT  NOT NULL"`
+	ID           uint64 `gorm:"primaryKey;type:BIGINT  NOT NULL" json:"id,string"`
+	WorkspaceID  uint64 `json:"workspace_id,string"`
+	EntryType    string `json:"entry_type"`
+	CustomField  string `json:"custom_field"`
+	Type         string `json:"type"`
+	Name         string `json:"name"`
+	Options      string `json:"options"`
+	Enabled      string `json:"enabled"`
+	Sort         string `json:"sort"`
+	common.NoPKModel
+}
+
+func (TapdTaskCustomFields) TableName() string {
+	return "_tool_tapd_task_custom_fields"
+}
diff --git a/plugins/tapd/models/workspace.go b/plugins/tapd/models/workspace.go
index aa0a817c..64665c9e 100644
--- a/plugins/tapd/models/workspace.go
+++ b/plugins/tapd/models/workspace.go
@@ -33,6 +33,7 @@ type TapdWorkspace struct {
 	BeginDate    *helper.CSTTime `json:"begin_date"`
 	EndDate      *helper.CSTTime `json:"end_date"`
 	ExternalOn   string          `gorm:"type:varchar(255)" json:"external_on"`
+	ParentId     uint64          `gorm:"type:BIGINT" json:"parent_id,string"`
 	Creator      string          `gorm:"type:varchar(255)" json:"creator"`
 	Created      *helper.CSTTime `json:"created"`
 	common.NoPKModel
diff --git a/plugins/tapd/tapd.go b/plugins/tapd/tapd.go
index 3729af3b..f0ffcded 100644
--- a/plugins/tapd/tapd.go
+++ b/plugins/tapd/tapd.go
@@ -54,50 +54,54 @@ func (plugin Tapd) Description() string {
 
 func (plugin Tapd) SubTaskMetas() []core.SubTaskMeta {
 	return []core.SubTaskMeta{
-		tasks.CollectWorkspaceMeta,
-		tasks.ExtractWorkspaceMeta,
-		tasks.CollectBugStatusMeta,
-		tasks.ExtractBugStatusMeta,
-		tasks.CollectUserMeta,
-		tasks.ExtractUserMeta,
-		tasks.CollectIterationMeta,
-		tasks.ExtractIterationMeta,
-		tasks.CollectStoryMeta,
-		tasks.CollectBugMeta,
-		tasks.CollectTaskMeta,
-		tasks.ExtractStoryMeta,
-		tasks.ExtractBugMeta,
-		tasks.ExtractTaskMeta,
-		tasks.CollectBugChangelogMeta,
-		tasks.ExtractBugChangelogMeta,
-		tasks.CollectStoryChangelogMeta,
-		tasks.ExtractStoryChangelogMeta,
-		tasks.CollectTaskChangelogMeta,
-		tasks.ExtractTaskChangelogMeta,
-		tasks.CollectWorklogMeta,
-		tasks.ExtractWorklogMeta,
-		tasks.CollectBugCommitMeta,
-		tasks.ExtractBugCommitMeta,
-		tasks.CollectStoryCommitMeta,
-		tasks.ExtractStoryCommitMeta,
-		tasks.CollectTaskCommitMeta,
-		tasks.ExtractTaskCommitMeta,
-		tasks.ConvertWorkspaceMeta,
-		tasks.ConvertUserMeta,
-		tasks.ConvertIterationMeta,
-		tasks.ConvertStoryMeta,
-		tasks.ConvertBugMeta,
-		tasks.ConvertTaskMeta,
-		tasks.ConvertWorklogMeta,
-		tasks.ConvertBugChangelogMeta,
-		tasks.ConvertStoryChangelogMeta,
-		tasks.ConvertTaskChangelogMeta,
-		tasks.ConvertBugCommitMeta,
-		tasks.ConvertStoryCommitMeta,
-		tasks.ConvertTaskCommitMeta,
-		tasks.ConvertStoryLabelsMeta,
-		tasks.ConvertTaskLabelsMeta,
-		tasks.ConvertBugLabelsMeta,
+		//tasks.CollectCompanyMeta,
+		//tasks.ExtractCompanyMeta,
+		//tasks.CollectWorkspaceMeta,
+		//tasks.ExtractWorkspaceMeta,
+		tasks.CollectStoryCustomFieldsMeta,
+		tasks.ExtractStoryCustomFieldsMeta,
+		//tasks.CollectBugStatusMeta,
+		//tasks.ExtractBugStatusMeta,
+		//tasks.CollectUserMeta,
+		//tasks.ExtractUserMeta,
+		//tasks.CollectIterationMeta,
+		//tasks.ExtractIterationMeta,
+		//tasks.CollectStoryMeta,
+		//tasks.CollectBugMeta,
+		//tasks.CollectTaskMeta,
+		//tasks.ExtractStoryMeta,
+		//tasks.ExtractBugMeta,
+		//tasks.ExtractTaskMeta,
+		//tasks.CollectBugChangelogMeta,
+		//tasks.ExtractBugChangelogMeta,
+		//tasks.CollectStoryChangelogMeta,
+		//tasks.ExtractStoryChangelogMeta,
+		//tasks.CollectTaskChangelogMeta,
+		//tasks.ExtractTaskChangelogMeta,
+		//tasks.CollectWorklogMeta,
+		//tasks.ExtractWorklogMeta,
+		//tasks.CollectBugCommitMeta,
+		//tasks.ExtractBugCommitMeta,
+		//tasks.CollectStoryCommitMeta,
+		//tasks.ExtractStoryCommitMeta,
+		//tasks.CollectTaskCommitMeta,
+		//tasks.ExtractTaskCommitMeta,
+		//tasks.ConvertWorkspaceMeta,
+		//tasks.ConvertUserMeta,
+		//tasks.ConvertIterationMeta,
+		//tasks.ConvertStoryMeta,
+		//tasks.ConvertBugMeta,
+		//tasks.ConvertTaskMeta,
+		//tasks.ConvertWorklogMeta,
+		//tasks.ConvertBugChangelogMeta,
+		//tasks.ConvertStoryChangelogMeta,
+		//tasks.ConvertTaskChangelogMeta,
+		//tasks.ConvertBugCommitMeta,
+		//tasks.ConvertStoryCommitMeta,
+		//tasks.ConvertTaskCommitMeta,
+		//tasks.ConvertStoryLabelsMeta,
+		//tasks.ConvertTaskLabelsMeta,
+		//tasks.ConvertBugLabelsMeta,
 	}
 }
 
@@ -182,14 +186,11 @@ func main() {
 	cmd := &cobra.Command{Use: "tapd"}
 	connectionId := cmd.Flags().Uint64P("connection", "c", 0, "tapd connection id")
 	workspaceId := cmd.Flags().Uint64P("workspace", "w", 0, "tapd workspace id")
+	companyId := cmd.Flags().Uint64P("company", "o", 0, "tapd company id")
 	err := cmd.MarkFlagRequired("connection")
 	if err != nil {
 		panic(err)
 	}
-	//err = cmd.MarkFlagRequired("company")
-	//if err != nil {
-	//	panic(err)
-	//}
 	err = cmd.MarkFlagRequired("workspace")
 	if err != nil {
 		panic(err)
@@ -198,6 +199,7 @@ func main() {
 		runner.DirectRun(c, args, PluginEntry, map[string]interface{}{
 			"connectionId": *connectionId,
 			"workspaceId":  *workspaceId,
+			"companyId":    *companyId,
 		})
 	}
 	runner.RunCmd(cmd)
diff --git a/plugins/tapd/tasks/company_collector.go b/plugins/tapd/tasks/company_collector.go
new file mode 100644
index 00000000..49678fb4
--- /dev/null
+++ b/plugins/tapd/tasks/company_collector.go
@@ -0,0 +1,76 @@
+/*
+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 (
+	"encoding/json"
+	"fmt"
+	"github.com/apache/incubator-devlake/plugins/core"
+	"github.com/apache/incubator-devlake/plugins/helper"
+	"net/http"
+	"net/url"
+)
+
+const RAW_COMPANY_TABLE = "tapd_api_companies"
+
+var _ core.SubTaskEntryPoint = CollectCompanies
+
+func CollectCompanies(taskCtx core.SubTaskContext) error {
+	data := taskCtx.GetData().(*TapdTaskData)
+	logger := taskCtx.GetLogger()
+	logger.Info("collect companies")
+	collector, err := helper.NewApiCollector(helper.ApiCollectorArgs{
+		RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
+			Ctx: taskCtx,
+			Params: TapdApiParams{
+				ConnectionId: data.Connection.ID,
+				CompanyId:    data.Options.CompanyId,
+			},
+			Table: RAW_COMPANY_TABLE,
+		},
+		ApiClient: data.ApiClient,
+		//PageSize:    100,
+		UrlTemplate: "workspaces/projects",
+		Query: func(reqData *helper.RequestData) (url.Values, error) {
+			query := url.Values{}
+			query.Set("company_id", fmt.Sprintf("%v", data.Options.CompanyId))
+			//query.Set("page", fmt.Sprintf("%v", reqData.Pager.Page))
+			//query.Set("limit", fmt.Sprintf("%v", reqData.Pager.Size))
+			return query, nil
+		},
+		ResponseParser: func(res *http.Response) ([]json.RawMessage, error) {
+			var data struct {
+				Companies []json.RawMessage `json:"data"`
+			}
+			err := helper.UnmarshalResponse(res, &data)
+			return data.Companies, err
+		},
+	})
+	if err != nil {
+		logger.Error("collect company error:", err)
+		return err
+	}
+	return collector.Execute()
+}
+
+var CollectCompanyMeta = core.SubTaskMeta{
+	Name:        "collectCompanies",
+	EntryPoint:  CollectCompanies,
+	Required:    true,
+	Description: "collect Tapd companies",
+}
diff --git a/plugins/tapd/tasks/company_extractor.go b/plugins/tapd/tasks/company_extractor.go
new file mode 100644
index 00000000..4dd5eee7
--- /dev/null
+++ b/plugins/tapd/tasks/company_extractor.go
@@ -0,0 +1,67 @@
+/*
+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 (
+	"encoding/json"
+	"github.com/apache/incubator-devlake/plugins/core"
+	"github.com/apache/incubator-devlake/plugins/helper"
+)
+
+var _ core.SubTaskEntryPoint = ExtractCompanies
+
+var ExtractCompanyMeta = core.SubTaskMeta{
+	Name:             "extractCompanies",
+	EntryPoint:       ExtractCompanies,
+	EnabledByDefault: true,
+	Description:      "Extract raw company data into tool layer table _tool_tapd_workspaces",
+}
+
+func ExtractCompanies(taskCtx core.SubTaskContext) error {
+	data := taskCtx.GetData().(*TapdTaskData)
+	extractor, err := helper.NewApiExtractor(helper.ApiExtractorArgs{
+		RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
+			Ctx: taskCtx,
+			Params: TapdApiParams{
+				ConnectionId: data.Connection.ID,
+				CompanyId:    data.Options.CompanyId,
+			},
+			Table: RAW_COMPANY_TABLE,
+		},
+		Extract: func(row *helper.RawData) ([]interface{}, error) {
+			var workspaceRes TapdWorkspaceRes
+			err := json.Unmarshal(row.Data, &workspaceRes)
+			if err != nil {
+				return nil, err
+			}
+
+			ws := workspaceRes.Workspace
+
+			ws.ConnectionId = data.Connection.ID
+			return []interface{}{
+				&ws,
+			}, nil
+		},
+	})
+
+	if err != nil {
+		return err
+	}
+
+	return extractor.Execute()
+}
diff --git a/plugins/tapd/tasks/story_custom_fields_collector.go b/plugins/tapd/tasks/story_custom_fields_collector.go
new file mode 100644
index 00000000..888f8991
--- /dev/null
+++ b/plugins/tapd/tasks/story_custom_fields_collector.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 tasks
+
+import (
+	"encoding/json"
+	"fmt"
+	"github.com/apache/incubator-devlake/plugins/core"
+	"github.com/apache/incubator-devlake/plugins/helper"
+	"net/http"
+	"net/url"
+)
+
+const RAW_STORY_CUSTOM_FIELDS_TABLE = "tapd_api_story_story_custom_fields"
+
+var _ core.SubTaskEntryPoint = CollectStoryCustomFields
+
+func CollectStoryCustomFields(taskCtx core.SubTaskContext) error {
+	data := taskCtx.GetData().(*TapdTaskData)
+	logger := taskCtx.GetLogger()
+	logger.Info("collect story_custom_fields")
+	collector, err := helper.NewApiCollector(helper.ApiCollectorArgs{
+		RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
+			Ctx: taskCtx,
+			Params: TapdApiParams{
+				ConnectionId: data.Connection.ID,
+				WorkspaceID:  data.Options.WorkspaceID,
+			},
+			Table: RAW_STORY_CUSTOM_FIELDS_TABLE,
+		},
+		ApiClient: data.ApiClient,
+		//PageSize:    100,
+		UrlTemplate: "stories/custom_fields_settings",
+		Query: func(reqData *helper.RequestData) (url.Values, error) {
+			query := url.Values{}
+			query.Set("workspace_id", fmt.Sprintf("%v", data.Options.WorkspaceID))
+			return query, nil
+		},
+		ResponseParser: func(res *http.Response) ([]json.RawMessage, error) {
+			var data struct {
+				StoryCustomFields []json.RawMessage `json:"data"`
+			}
+			err := helper.UnmarshalResponse(res, &data)
+			return data.StoryCustomFields, err
+		},
+	})
+	if err != nil {
+		logger.Error("collect story_custom_fields error:", err)
+		return err
+	}
+	return collector.Execute()
+}
+
+var CollectStoryCustomFieldsMeta = core.SubTaskMeta{
+	Name:        "collectStoryCustomFields",
+	EntryPoint:  CollectStoryCustomFields,
+	Required:    true,
+	Description: "collect Tapd StoryCustomFields",
+}
diff --git a/plugins/tapd/tasks/story_custom_fields_extractor.go b/plugins/tapd/tasks/story_custom_fields_extractor.go
new file mode 100644
index 00000000..6549485f
--- /dev/null
+++ b/plugins/tapd/tasks/story_custom_fields_extractor.go
@@ -0,0 +1,72 @@
+/*
+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 (
+	"encoding/json"
+	"github.com/apache/incubator-devlake/plugins/core"
+	"github.com/apache/incubator-devlake/plugins/helper"
+	"github.com/apache/incubator-devlake/plugins/tapd/models"
+)
+
+var _ core.SubTaskEntryPoint = ExtractStoryCustomFields
+
+var ExtractStoryCustomFieldsMeta = core.SubTaskMeta{
+	Name:             "extractStoryCustomFields",
+	EntryPoint:       ExtractStoryCustomFields,
+	EnabledByDefault: true,
+	Description:      "Extract raw company data into tool layer table _tool_tapd_story_custom_fields",
+}
+
+type TapdStoryCustomFieldsRes struct {
+	CustomFieldConfig models.TapdStoryCustomFields
+}
+
+func ExtractStoryCustomFields(taskCtx core.SubTaskContext) error {
+	data := taskCtx.GetData().(*TapdTaskData)
+	extractor, err := helper.NewApiExtractor(helper.ApiExtractorArgs{
+		RawDataSubTaskArgs: helper.RawDataSubTaskArgs{
+			Ctx: taskCtx,
+			Params: TapdApiParams{
+				ConnectionId: data.Connection.ID,
+				WorkspaceID:  data.Options.WorkspaceID,
+			},
+			Table: RAW_STORY_CUSTOM_FIELDS_TABLE,
+		},
+		Extract: func(row *helper.RawData) ([]interface{}, error) {
+			var customFields TapdStoryCustomFieldsRes
+			err := json.Unmarshal(row.Data, &customFields)
+			if err != nil {
+				return nil, err
+			}
+
+			toolL := customFields.CustomFieldConfig
+
+			toolL.ConnectionId = data.Connection.ID
+			return []interface{}{
+				&toolL,
+			}, nil
+		},
+	})
+
+	if err != nil {
+		return err
+	}
+
+	return extractor.Execute()
+}