You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@devlake.apache.org by kl...@apache.org on 2022/11/16 12:51:41 UTC

[incubator-devlake] branch main updated: feat: add project to support resource classification (#3679)

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

klesh 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 fb6c5e2e6 feat: add project to support resource classification (#3679)
fb6c5e2e6 is described below

commit fb6c5e2e6131cd8c868183430b57a84e9c89c827
Author: mappjzc <zh...@merico.dev>
AuthorDate: Wed Nov 16 20:51:36 2022 +0800

    feat: add project to support resource classification (#3679)
    
    * feat: add project api and tables
    
    Add GET POST PATCH Project
    Add GET POST PATCH ProjectMetric
    Add tables
    
    Nddtfjiang <zh...@merico.dev>
    
    * feat: add plugin metric interface
    
    Add PluginMetric for all plugin witch need it.
    
    Nddtfjiang <zh...@merico.dev>
    
    * fix: fix description for review
    
    Nddtfjiang <zh...@merico.dev>
    
    * fix: fix error for test
    
    fix some error for testing.
    
    Nddtfjiang <zh...@merico.dev>
    
    * feat: fix for review
    
    move out the GetTablesInfo from PluginMetric to PluginModel.
    Changed api ProjectMetric api path.
    add metric data in api plugin.
    
    Nddtfjiang <zh...@merico.dev>
    
    * feat: uninstall metric interface implementation
    
    Uninstall PluginMetric interface implementation for unuse plugins.
    
    Nddtfjiang <zh...@merico.dev>
    
    * fix: fix for review
    
    fix for review
    
    Nddtfjiang <zh...@merico.dev>
    
    * fix: fix for sepll review
    
    fix for sepll review
    
    Nddtfjiang <zh...@merico.dev>
---
 api/plugininfo/plugininifo.go                      |  63 +++++-
 api/project/project.go                             | 243 +++++++++++++++++++++
 api/router.go                                      |  16 ++
 .../20221109_add_project_tables.go                 |  69 ++++++
 models/migrationscripts/register.go                |   1 +
 plugins/core/plugin_model.go => models/project.go  |  28 ++-
 plugins/ae/impl/impl.go                            |   2 +
 plugins/azure/impl/impl.go                         |  10 +
 plugins/bitbucket/impl/impl.go                     |   1 +
 plugins/core/{plugin_model.go => plugin_metric.go} |  24 +-
 plugins/core/plugin_model.go                       |   1 +
 plugins/customize/impl/impl.go                     |   5 +
 plugins/dbt/dbt.go                                 |   5 +-
 plugins/dora/impl/impl.go                          |  30 +++
 plugins/feishu/impl/impl.go                        |   1 +
 plugins/gitee/impl/impl.go                         |   1 +
 plugins/gitextractor/gitextractor.go               |   1 +
 plugins/github/impl/impl.go                        |   1 +
 plugins/github_graphql/plugin_main.go              |  12 +-
 plugins/gitlab/impl/impl.go                        |   2 +-
 plugins/icla/plugin_main.go                        |  13 +-
 plugins/jenkins/impl/impl.go                       |   1 +
 plugins/jira/impl/impl.go                          |   5 +-
 plugins/org/impl/impl.go                           |   1 +
 plugins/refdiff/refdiff.go                         |  18 ++
 plugins/starrocks/starrocks.go                     |   5 +
 plugins/tapd/impl/impl.go                          |   1 +
 plugins/webhook/impl/impl.go                       |   5 +
 services/project.go                                | 159 ++++++++++++++
 services/project_helper.go                         | 168 ++++++++++++++
 30 files changed, 868 insertions(+), 24 deletions(-)

diff --git a/api/plugininfo/plugininifo.go b/api/plugininfo/plugininifo.go
index b36803568..4868f883f 100644
--- a/api/plugininfo/plugininifo.go
+++ b/api/plugininfo/plugininifo.go
@@ -18,11 +18,13 @@ limitations under the License.
 package plugininfo
 
 import (
-	"github.com/apache/incubator-devlake/errors"
+	"fmt"
 	"net/http"
 	"reflect"
 	"sync"
 
+	"github.com/apache/incubator-devlake/errors"
+
 	"github.com/apache/incubator-devlake/api/shared"
 	"github.com/apache/incubator-devlake/models/domainlayer/domaininfo"
 	"github.com/apache/incubator-devlake/plugins/core"
@@ -67,6 +69,19 @@ type TableInfos struct {
 	Error     *string      `json:"error"`
 }
 
+type PluginMetric struct {
+	RequiredDataEntities []map[string]interface{} `json:"requiredDataEntities"`
+	RunAfter             []string                 `json:"runAfter"`
+	IsProjectMetric      bool                     `json:"isProjectMetric"`
+}
+
+type PluginMeta struct {
+	Plugin string       `json:"plugin"`
+	Metric PluginMetric `json:"metric"`
+}
+
+type PluginMetas []PluginMeta
+
 func NewTableInfos(table core.Tabler) *TableInfos {
 	tableInfos := &TableInfos{
 		TableName: table.TableName(),
@@ -178,3 +193,49 @@ func Get(c *gin.Context) {
 
 	shared.ApiOutputSuccess(c, info, http.StatusOK)
 }
+
+// @Get name list of plugins
+// @Description GET /plugins
+// @Description RETURN SAMPLE
+// @Tags framework/plugins
+// @Success 200  {object} PluginMetas
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Router /plugins [get]
+func GetPluginMetas(c *gin.Context) {
+	var metas PluginMetas
+	err := core.TraversalPlugin(func(name string, plugin core.PluginMeta) errors.Error {
+		pluginMeta := PluginMeta{
+			Plugin: name,
+		}
+
+		// if this plugin has the plugin task info
+		if pt, ok := plugin.(core.PluginMetric); ok {
+			var err1 errors.Error
+			metric := PluginMetric{}
+
+			metric.RequiredDataEntities, err1 = pt.RequiredDataEntities()
+			if err1 != nil {
+				return errors.Default.Wrap(err1, fmt.Sprintf("failed to get RequiredDataEntities on plugin %s", name))
+			}
+
+			metric.RunAfter, err1 = pt.RunAfter()
+			if err1 != nil {
+				return errors.Default.Wrap(err1, fmt.Sprintf("failed to get RunAfter on plugin %s", name))
+			}
+
+			metric.IsProjectMetric = pt.IsProjectMetric()
+
+			pluginMeta.Metric = metric
+		}
+
+		metas = append(metas, pluginMeta)
+
+		return nil
+	})
+
+	if err != nil {
+		shared.ApiOutputError(c, errors.Default.Wrap(err, "error getting plugin info of plugins"))
+	}
+
+	shared.ApiOutputSuccess(c, metas, http.StatusOK)
+}
diff --git a/api/project/project.go b/api/project/project.go
new file mode 100644
index 000000000..a684b70c4
--- /dev/null
+++ b/api/project/project.go
@@ -0,0 +1,243 @@
+/*
+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 project
+
+import (
+	"net/http"
+
+	"github.com/apache/incubator-devlake/api/shared"
+	"github.com/apache/incubator-devlake/errors"
+	"github.com/apache/incubator-devlake/models"
+	"github.com/apache/incubator-devlake/services"
+	"github.com/gin-gonic/gin"
+)
+
+type PaginatedProjects struct {
+	Projects []*models.Project
+	Count    int64
+}
+
+// @Summary Create and run a new project
+// @Description Create and run a new project
+// @Tags framework/projects
+// @Accept application/json
+// @Param project body models.Project true "json"
+// @Success 200  {object} models.Project
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internal Error"
+// @Router /projects/:projectName [get]
+func GetProject(c *gin.Context) {
+	projectName := c.Param("projectName")
+
+	project, err := services.GetProject(projectName)
+	if err != nil {
+		shared.ApiOutputError(c, errors.Default.Wrap(err, "error getting project"))
+		return
+	}
+
+	shared.ApiOutputSuccess(c, project, http.StatusOK)
+}
+
+// @Summary Get list of projects
+// @Description GET /projects?page=1&pagesize=10
+// @Tags framework/projects
+// @Param page query int true "query"
+// @Param pagesize query int true "query"
+// @Success 200  {object} PaginatedProjects
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internel Error"
+// @Router /projects [get]
+func GetProjects(c *gin.Context) {
+	var query services.ProjectQuery
+	err := c.ShouldBindQuery(&query)
+	if err != nil {
+		shared.ApiOutputError(c, errors.BadInput.Wrap(err, shared.BadRequestBody))
+		return
+	}
+	projects, count, err := services.GetProjects(&query)
+	if err != nil {
+		shared.ApiOutputAbort(c, errors.Default.Wrap(err, "error getting projects"))
+		return
+	}
+	shared.ApiOutputSuccess(c, PaginatedProjects{
+		Projects: projects,
+		Count:    count,
+	}, http.StatusOK)
+}
+
+// @Summary Create a new project
+// @Description Create a new project
+// @Tags framework/projects
+// @Accept application/json
+// @Param project body models.Project true "json"
+// @Success 200  {object} models.Project
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internal Error"
+// @Router /projects [post]
+func PostProject(c *gin.Context) {
+	project := &models.Project{}
+
+	err := c.ShouldBind(project)
+	if err != nil {
+		shared.ApiOutputError(c, errors.BadInput.Wrap(err, shared.BadRequestBody))
+		return
+	}
+
+	err = services.CreateProject(project)
+	if err != nil {
+		shared.ApiOutputError(c, errors.Default.Wrap(err, "error creating project"))
+		return
+	}
+
+	shared.ApiOutputSuccess(c, project, http.StatusCreated)
+}
+
+// @Summary Patch a project
+// @Description Patch a project
+// @Tags framework/projects
+// @Accept application/json
+// @Param project body models.Project true "json"
+// @Success 200  {object} models.Project
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internal Error"
+// @Router /projects/:projectName [patch]
+func PatchProject(c *gin.Context) {
+	projectName := c.Param("projectName")
+
+	var body map[string]interface{}
+	err := c.ShouldBind(&body)
+	if err != nil {
+		shared.ApiOutputError(c, errors.BadInput.Wrap(err, shared.BadRequestBody))
+		return
+	}
+
+	project, err := services.PatchProject(projectName, body)
+	if err != nil {
+		shared.ApiOutputError(c, errors.Default.Wrap(err, "error patch project"))
+		return
+	}
+
+	shared.ApiOutputSuccess(c, project, http.StatusCreated)
+}
+
+// @Cancel a project
+// @Description Cancel a project
+// @Tags framework/projects
+// @Success 200
+// @Failure 400  {string} er2rcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internel Error"
+// @Router /projects/:projectName [delete]
+//func DeleteProject(c *gin.Context) {
+//}
+
+// @Summary Get a ProjectMetrics
+// @Description Get a ProjectMetrics
+// @Tags framework/ProjectMetrics
+// @Param page query int true "query"
+// @Param pagesize query int true "query"
+// @Success 200  {object} models.ProjectMetric
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internel Error"
+// @Router /projects/:projectName/metrics/:pluginName [get]
+func GetProjectMetrics(c *gin.Context) {
+	projectName := c.Param("projectName")
+	pluginName := c.Param("pluginName")
+
+	projectMetric, err := services.GetProjectMetrics(projectName, pluginName)
+	if err != nil {
+		shared.ApiOutputError(c, errors.Default.Wrap(err, "error getting project metric"))
+		return
+	}
+
+	shared.ApiOutputSuccess(c, projectMetric, http.StatusOK)
+}
+
+// @Summary Create a new ProjectMetrics
+// @Description Create  a new ProjectMetrics
+// @Tags framework/ProjectMetrics
+// @Accept application/json
+// @Param project body models.Project true "json"
+// @Success 200  {object} models.ProjectMetric
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internal Error"
+// @Router /projects/:projectName/metrics [post]
+func PostProjectMetrics(c *gin.Context) {
+	projectMetric := &models.ProjectMetric{}
+
+	projectName := c.Param("projectName")
+
+	_, err1 := services.GetProject(projectName)
+	if err1 != nil {
+		shared.ApiOutputError(c, errors.BadInput.Wrap(err1, shared.BadRequestBody))
+		return
+	}
+
+	err := c.ShouldBind(projectMetric)
+	if err != nil {
+		shared.ApiOutputError(c, errors.BadInput.Wrap(err, shared.BadRequestBody))
+		return
+	}
+
+	projectMetric.ProjectName = projectName
+	err = services.CreateProjectMetric(projectMetric)
+	if err != nil {
+		shared.ApiOutputError(c, errors.Default.Wrap(err, "error creating project"))
+		return
+	}
+
+	shared.ApiOutputSuccess(c, projectMetric, http.StatusCreated)
+}
+
+// @Summary Patch a ProjectMetrics
+// @Description Patch a ProjectMetrics
+// @Tags framework/ProjectMetrics
+// @Accept application/json
+// @Param ProjectMetrics body models.ProjectMetric true "json"
+// @Success 200  {object} models.ProjectMetric
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internal Error"
+// @Router /projects/:projectName/metrics/:pluginName  [patch]
+func PatchProjectMetrics(c *gin.Context) {
+	projectName := c.Param("projectName")
+	pluginName := c.Param("pluginName")
+
+	var body map[string]interface{}
+	err := c.ShouldBind(&body)
+	if err != nil {
+		shared.ApiOutputError(c, errors.BadInput.Wrap(err, shared.BadRequestBody))
+		return
+	}
+
+	projectMetric, err := services.PatchProjectMetric(projectName, pluginName, body)
+	if err != nil {
+		shared.ApiOutputError(c, errors.Default.Wrap(err, "error patch project"))
+		return
+	}
+
+	shared.ApiOutputSuccess(c, projectMetric, http.StatusCreated)
+}
+
+// @delete a ProjectMetrics
+// @Description delete a ProjectMetrics
+// @Tags framework/ProjectMetrics
+// @Success 200
+// @Failure 400  {string} errcode.Error "Bad Request"
+// @Failure 500  {string} errcode.Error "Internel Error"
+// @Router /project_metrics/:projectName/:pluginName [delete]
+//func DeleteProjectMetrics(c *gin.Context) {
+//}
diff --git a/api/router.go b/api/router.go
index 0122a185a..1c50f6236 100644
--- a/api/router.go
+++ b/api/router.go
@@ -27,6 +27,7 @@ import (
 	"github.com/apache/incubator-devlake/api/ping"
 	"github.com/apache/incubator-devlake/api/pipelines"
 	"github.com/apache/incubator-devlake/api/plugininfo"
+	"github.com/apache/incubator-devlake/api/project"
 	"github.com/apache/incubator-devlake/api/push"
 	"github.com/apache/incubator-devlake/api/shared"
 	"github.com/apache/incubator-devlake/api/task"
@@ -59,7 +60,22 @@ func RegisterRouter(r *gin.Engine) {
 	r.POST("/push/:tableName", push.Post)
 	r.GET("/domainlayer/repos", domainlayer.ReposIndex)
 
+	// plugin api
 	r.GET("/plugininfo", plugininfo.Get)
+	r.GET("/plugins", plugininfo.GetPluginMetas)
+
+	// project api
+	r.GET("/projects/:projectName", project.GetProject)
+	r.PATCH("/projects/:projectName", project.PatchProject)
+	//r.DELETE("/projects/:projectName", project.DeleteProject)
+	r.POST("/projects", project.PostProject)
+	r.GET("/projects", project.GetProjects)
+
+	// project metric api
+	r.GET("/projects/:projectName/metrics/:pluginName", project.GetProjectMetrics)
+	r.PATCH("/projects/:projectName/metrics/:pluginName", project.PatchProjectMetrics)
+	//r.DELETE("/projects/:projectName/metrics/:pluginName", project.DeleteProjectMetrics)
+	r.POST("/projects/:projectName/metrics", project.PostProjectMetrics)
 
 	// mount all api resources for all plugins
 	pluginsApiResources, err := services.GetPluginsApiResources()
diff --git a/models/migrationscripts/20221109_add_project_tables.go b/models/migrationscripts/20221109_add_project_tables.go
new file mode 100644
index 000000000..1752ace1a
--- /dev/null
+++ b/models/migrationscripts/20221109_add_project_tables.go
@@ -0,0 +1,69 @@
+/*
+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/errors"
+	"github.com/apache/incubator-devlake/helpers/migrationhelper"
+	"github.com/apache/incubator-devlake/models/migrationscripts/archived"
+	"github.com/apache/incubator-devlake/plugins/core"
+)
+
+var _ core.MigrationScript = (*addProjectTables)(nil)
+
+type addProjectTables struct{}
+
+type Project struct {
+	Name        string `gorm:"primaryKey;type:varchar(255)"`
+	Description string `gorm:"type:text"`
+
+	archived.NoPKModel
+}
+
+func (Project) TableName() string {
+	return "projects"
+}
+
+type ProjectMetric struct {
+	ProjectName  string `gorm:"primaryKey;type:varchar(255)"`
+	PluginName   string `gorm:"primaryKey;type:varchar(255)"`
+	PluginOption string `gorm:"type:text"`
+
+	archived.NoPKModel
+}
+
+func (ProjectMetric) TableName() string {
+	return "project_metrics"
+}
+
+func (script *addProjectTables) Up(basicRes core.BasicRes) errors.Error {
+	// To create multiple tables with migrationhelper
+	return migrationhelper.AutoMigrateTables(
+		basicRes,
+		&Project{},
+		&ProjectMetric{},
+	)
+}
+
+func (*addProjectTables) Version() uint64 {
+	return 20221109194734
+}
+
+func (*addProjectTables) Name() string {
+	return "add project tables"
+}
diff --git a/models/migrationscripts/register.go b/models/migrationscripts/register.go
index 7acf5e669..78a1f890e 100644
--- a/models/migrationscripts/register.go
+++ b/models/migrationscripts/register.go
@@ -56,5 +56,6 @@ func All() []core.MigrationScript {
 		new(addCicdScope),
 		new(addSkipOnFail),
 		new(modifyCommitsDiffs),
+		new(addProjectTables),
 	}
 }
diff --git a/plugins/core/plugin_model.go b/models/project.go
similarity index 56%
copy from plugins/core/plugin_model.go
copy to models/project.go
index 5ddec7f61..a9edfcc54 100644
--- a/plugins/core/plugin_model.go
+++ b/models/project.go
@@ -15,12 +15,30 @@ See the License for the specific language governing permissions and
 limitations under the License.
 */
 
-package core
+package models
 
-type Tabler interface {
-	TableName() string
+import "github.com/apache/incubator-devlake/models/common"
+
+type Project struct {
+	Name        string `gorm:"primaryKey;type:varchar(255)"`
+	Description string `gorm:"type:text"`
+
+	common.NoPKModel
+}
+
+func (Project) TableName() string {
+	return "projects"
+}
+
+type ProjectMetric struct {
+	ProjectName  string `gorm:"primaryKey;type:varchar(255)"`
+	PluginName   string `gorm:"primaryKey;type:varchar(255)"`
+	PluginOption string `gorm:"type:text"`
+	Enable       bool   `gorm:"type:boolean"`
+
+	common.NoPKModel
 }
 
-type PluginModel interface {
-	GetTablesInfo() []Tabler
+func (ProjectMetric) TableName() string {
+	return "project_metrics"
 }
diff --git a/plugins/ae/impl/impl.go b/plugins/ae/impl/impl.go
index da5c9bfde..3532720e3 100644
--- a/plugins/ae/impl/impl.go
+++ b/plugins/ae/impl/impl.go
@@ -35,6 +35,7 @@ var _ core.PluginMeta = (*AE)(nil)
 var _ core.PluginInit = (*AE)(nil)
 var _ core.PluginTask = (*AE)(nil)
 var _ core.PluginApi = (*AE)(nil)
+var _ core.PluginModel = (*AE)(nil)
 var _ core.PluginMigration = (*AE)(nil)
 var _ core.CloseablePluginTask = (*AE)(nil)
 
@@ -44,6 +45,7 @@ func (plugin AE) Init(config *viper.Viper, logger core.Logger, db *gorm.DB) erro
 	api.Init(config, logger, db)
 	return nil
 }
+
 func (plugin AE) GetTablesInfo() []core.Tabler {
 	return []core.Tabler{
 		&models.AECommit{},
diff --git a/plugins/azure/impl/impl.go b/plugins/azure/impl/impl.go
index 5b55d4884..29328c3a6 100644
--- a/plugins/azure/impl/impl.go
+++ b/plugins/azure/impl/impl.go
@@ -36,6 +36,7 @@ var _ core.PluginMeta = (*Azure)(nil)
 var _ core.PluginInit = (*Azure)(nil)
 var _ core.PluginTask = (*Azure)(nil)
 var _ core.PluginApi = (*Azure)(nil)
+var _ core.PluginModel = (*Azure)(nil)
 var _ core.CloseablePluginTask = (*Azure)(nil)
 var _ core.PluginMigration = (*Azure)(nil)
 
@@ -53,6 +54,15 @@ func (plugin Azure) Init(config *viper.Viper, logger core.Logger, db *gorm.DB) e
 	return nil
 }
 
+func (plugin Azure) GetTablesInfo() []core.Tabler {
+	return []core.Tabler{
+		&models.AzureBuild{},
+		&models.AzureBuildDefinition{},
+		&models.AzureConnection{},
+		&models.AzureRepo{},
+	}
+}
+
 func (plugin Azure) SubTaskMetas() []core.SubTaskMeta {
 	return []core.SubTaskMeta{
 		tasks.CollectApiRepoMeta,
diff --git a/plugins/bitbucket/impl/impl.go b/plugins/bitbucket/impl/impl.go
index cfa6c7fb9..37e53edc9 100644
--- a/plugins/bitbucket/impl/impl.go
+++ b/plugins/bitbucket/impl/impl.go
@@ -35,6 +35,7 @@ var _ core.PluginMeta = (*Bitbucket)(nil)
 var _ core.PluginInit = (*Bitbucket)(nil)
 var _ core.PluginTask = (*Bitbucket)(nil)
 var _ core.PluginApi = (*Bitbucket)(nil)
+var _ core.PluginModel = (*Bitbucket)(nil)
 var _ core.PluginMigration = (*Bitbucket)(nil)
 var _ core.PluginBlueprintV100 = (*Bitbucket)(nil)
 var _ core.CloseablePluginTask = (*Bitbucket)(nil)
diff --git a/plugins/core/plugin_model.go b/plugins/core/plugin_metric.go
similarity index 51%
copy from plugins/core/plugin_model.go
copy to plugins/core/plugin_metric.go
index 5ddec7f61..2297051c8 100644
--- a/plugins/core/plugin_model.go
+++ b/plugins/core/plugin_metric.go
@@ -17,10 +17,24 @@ limitations under the License.
 
 package core
 
-type Tabler interface {
-	TableName() string
-}
+import (
+	"github.com/apache/incubator-devlake/errors"
+)
+
+type PluginMetric interface {
+	// returns a list of required data entities and expected features.
+	// [{ "model": "cicd_tasks", "requiredFields": {"column": "type", "execptedValue": "Deployment"}}, ...]
+	RequiredDataEntities() (data []map[string]interface{}, err errors.Error)
+
+	// returns if the metric depends on Project for calculation.
+	// Currently, only dora would return true.
+	IsProjectMetric() bool
+
+	// indicates which plugins must be executed before executing this one.
+	// declare a set of dependencies with this
+	RunAfter() ([]string, errors.Error)
 
-type PluginModel interface {
-	GetTablesInfo() []Tabler
+	// returns an empty pointer of the plugin setting struct.
+	// (no concrete usage at this point)
+	Settings() (p interface{})
 }
diff --git a/plugins/core/plugin_model.go b/plugins/core/plugin_model.go
index 5ddec7f61..5fdb7de06 100644
--- a/plugins/core/plugin_model.go
+++ b/plugins/core/plugin_model.go
@@ -22,5 +22,6 @@ type Tabler interface {
 }
 
 type PluginModel interface {
+	// This method returns all models of the current plugin
 	GetTablesInfo() []Tabler
 }
diff --git a/plugins/customize/impl/impl.go b/plugins/customize/impl/impl.go
index edb526725..9f196714a 100644
--- a/plugins/customize/impl/impl.go
+++ b/plugins/customize/impl/impl.go
@@ -32,6 +32,7 @@ import (
 var _ core.PluginMeta = (*Customize)(nil)
 var _ core.PluginInit = (*Customize)(nil)
 var _ core.PluginApi = (*Customize)(nil)
+var _ core.PluginModel = (*Customize)(nil)
 
 type Customize struct {
 	handlers *api.Handlers
@@ -43,6 +44,10 @@ func (plugin *Customize) Init(config *viper.Viper, logger core.Logger, db *gorm.
 	return nil
 }
 
+func (plugin Customize) GetTablesInfo() []core.Tabler {
+	return []core.Tabler{}
+}
+
 func (plugin Customize) SubTaskMetas() []core.SubTaskMeta {
 	return []core.SubTaskMeta{
 		tasks.ExtractCustomizedFieldsMeta,
diff --git a/plugins/dbt/dbt.go b/plugins/dbt/dbt.go
index 1a5e4b7b3..00784fe78 100644
--- a/plugins/dbt/dbt.go
+++ b/plugins/dbt/dbt.go
@@ -28,8 +28,9 @@ import (
 )
 
 var (
-	_ core.PluginMeta = (*Dbt)(nil)
-	_ core.PluginTask = (*Dbt)(nil)
+	_ core.PluginMeta  = (*Dbt)(nil)
+	_ core.PluginTask  = (*Dbt)(nil)
+	_ core.PluginModel = (*Dbt)(nil)
 )
 
 type Dbt struct{}
diff --git a/plugins/dora/impl/impl.go b/plugins/dora/impl/impl.go
index aa13a6c70..394d4d9cc 100644
--- a/plugins/dora/impl/impl.go
+++ b/plugins/dora/impl/impl.go
@@ -32,6 +32,8 @@ import (
 var _ core.PluginMeta = (*Dora)(nil)
 var _ core.PluginInit = (*Dora)(nil)
 var _ core.PluginTask = (*Dora)(nil)
+var _ core.PluginModel = (*Dora)(nil)
+var _ core.PluginMetric = (*Dora)(nil)
 var _ core.CloseablePluginTask = (*Dora)(nil)
 var _ core.PluginMigration = (*Dora)(nil)
 
@@ -56,6 +58,34 @@ func (plugin Dora) Init(config *viper.Viper, logger core.Logger, db *gorm.DB) er
 	return nil
 }
 
+func (plugin Dora) RequiredDataEntities() (data []map[string]interface{}, err errors.Error) {
+	return []map[string]interface{}{
+		{
+			"model": "cicd_tasks",
+			"requiredFields": map[string]string{
+				"column":        "type",
+				"execptedValue": "Deployment",
+			},
+		},
+	}, nil
+}
+
+func (plugin Dora) GetTablesInfo() []core.Tabler {
+	return []core.Tabler{}
+}
+
+func (plugin Dora) IsProjectMetric() bool {
+	return true
+}
+
+func (plugin Dora) RunAfter() ([]string, errors.Error) {
+	return []string{}, nil
+}
+
+func (plugin Dora) Settings() interface{} {
+	return nil
+}
+
 func (plugin Dora) SubTaskMetas() []core.SubTaskMeta {
 	// TODO add your sub task here
 	return []core.SubTaskMeta{
diff --git a/plugins/feishu/impl/impl.go b/plugins/feishu/impl/impl.go
index 09ab960d3..98d61102d 100644
--- a/plugins/feishu/impl/impl.go
+++ b/plugins/feishu/impl/impl.go
@@ -37,6 +37,7 @@ var _ core.PluginMeta = (*Feishu)(nil)
 var _ core.PluginInit = (*Feishu)(nil)
 var _ core.PluginTask = (*Feishu)(nil)
 var _ core.PluginApi = (*Feishu)(nil)
+var _ core.PluginModel = (*Feishu)(nil)
 var _ core.PluginMigration = (*Feishu)(nil)
 var _ core.CloseablePluginTask = (*Feishu)(nil)
 
diff --git a/plugins/gitee/impl/impl.go b/plugins/gitee/impl/impl.go
index a8f5957a4..38d512788 100644
--- a/plugins/gitee/impl/impl.go
+++ b/plugins/gitee/impl/impl.go
@@ -36,6 +36,7 @@ var _ core.PluginMeta = (*Gitee)(nil)
 var _ core.PluginInit = (*Gitee)(nil)
 var _ core.PluginTask = (*Gitee)(nil)
 var _ core.PluginApi = (*Gitee)(nil)
+var _ core.PluginModel = (*Gitee)(nil)
 var _ core.PluginMigration = (*Gitee)(nil)
 var _ core.CloseablePluginTask = (*Gitee)(nil)
 
diff --git a/plugins/gitextractor/gitextractor.go b/plugins/gitextractor/gitextractor.go
index 5a3866e3d..cf73279f6 100644
--- a/plugins/gitextractor/gitextractor.go
+++ b/plugins/gitextractor/gitextractor.go
@@ -31,6 +31,7 @@ import (
 
 var _ core.PluginMeta = (*GitExtractor)(nil)
 var _ core.PluginTask = (*GitExtractor)(nil)
+var _ core.PluginModel = (*GitExtractor)(nil)
 
 type GitExtractor struct{}
 
diff --git a/plugins/github/impl/impl.go b/plugins/github/impl/impl.go
index 3091e085e..84ef2151b 100644
--- a/plugins/github/impl/impl.go
+++ b/plugins/github/impl/impl.go
@@ -36,6 +36,7 @@ var _ core.PluginMeta = (*Github)(nil)
 var _ core.PluginInit = (*Github)(nil)
 var _ core.PluginTask = (*Github)(nil)
 var _ core.PluginApi = (*Github)(nil)
+var _ core.PluginModel = (*Github)(nil)
 var _ core.PluginBlueprintV100 = (*Github)(nil)
 var _ core.CloseablePluginTask = (*Github)(nil)
 
diff --git a/plugins/github_graphql/plugin_main.go b/plugins/github_graphql/plugin_main.go
index ddce4e944..12e780e0f 100644
--- a/plugins/github_graphql/plugin_main.go
+++ b/plugins/github_graphql/plugin_main.go
@@ -20,6 +20,10 @@ package main
 import (
 	"context"
 	"fmt"
+	"reflect"
+	"strings"
+	"time"
+
 	"github.com/apache/incubator-devlake/errors"
 	"github.com/apache/incubator-devlake/plugins/core"
 	"github.com/apache/incubator-devlake/plugins/github/models"
@@ -32,9 +36,6 @@ import (
 	"github.com/spf13/viper"
 	"golang.org/x/oauth2"
 	"gorm.io/gorm"
-	"reflect"
-	"strings"
-	"time"
 )
 
 // make sure interface is implemented
@@ -42,6 +43,7 @@ var _ core.PluginMeta = (*GithubGraphql)(nil)
 var _ core.PluginInit = (*GithubGraphql)(nil)
 var _ core.PluginTask = (*GithubGraphql)(nil)
 var _ core.PluginApi = (*GithubGraphql)(nil)
+var _ core.PluginModel = (*GithubGraphql)(nil)
 var _ core.CloseablePluginTask = (*GithubGraphql)(nil)
 
 // PluginEntry exports a symbol for Framework to load
@@ -57,6 +59,10 @@ func (plugin GithubGraphql) Init(config *viper.Viper, logger core.Logger, db *go
 	return nil
 }
 
+func (plugin GithubGraphql) GetTablesInfo() []core.Tabler {
+	return []core.Tabler{}
+}
+
 func (plugin GithubGraphql) SubTaskMetas() []core.SubTaskMeta {
 	return []core.SubTaskMeta{
 		tasks.CollectRepoMeta,
diff --git a/plugins/gitlab/impl/impl.go b/plugins/gitlab/impl/impl.go
index af16454b1..350656e23 100644
--- a/plugins/gitlab/impl/impl.go
+++ b/plugins/gitlab/impl/impl.go
@@ -34,9 +34,9 @@ import (
 
 var _ core.PluginMeta = (*Gitlab)(nil)
 var _ core.PluginInit = (*Gitlab)(nil)
-var _ core.PluginModel = (*Gitlab)(nil)
 var _ core.PluginTask = (*Gitlab)(nil)
 var _ core.PluginApi = (*Gitlab)(nil)
+var _ core.PluginModel = (*Gitlab)(nil)
 var _ core.PluginMigration = (*Gitlab)(nil)
 var _ core.PluginBlueprintV100 = (*Gitlab)(nil)
 var _ core.CloseablePluginTask = (*Gitlab)(nil)
diff --git a/plugins/icla/plugin_main.go b/plugins/icla/plugin_main.go
index e9125ccff..1298f3e15 100644
--- a/plugins/icla/plugin_main.go
+++ b/plugins/icla/plugin_main.go
@@ -37,6 +37,7 @@ var _ core.PluginMeta = (*Icla)(nil)
 var _ core.PluginInit = (*Icla)(nil)
 var _ core.PluginTask = (*Icla)(nil)
 var _ core.PluginApi = (*Icla)(nil)
+var _ core.PluginModel = (*Icla)(nil)
 var _ core.PluginMigration = (*Icla)(nil)
 var _ core.CloseablePluginTask = (*Icla)(nil)
 
@@ -45,12 +46,6 @@ var PluginEntry Icla //nolint
 
 type Icla struct{}
 
-func (plugin Icla) GetTablesInfo() []core.Tabler {
-	return []core.Tabler{
-		&models.IclaCommitter{},
-	}
-}
-
 func (plugin Icla) Description() string {
 	return "collect some Icla data"
 }
@@ -59,6 +54,12 @@ func (plugin Icla) Init(config *viper.Viper, logger core.Logger, db *gorm.DB) er
 	return nil
 }
 
+func (plugin Icla) GetTablesInfo() []core.Tabler {
+	return []core.Tabler{
+		&models.IclaCommitter{},
+	}
+}
+
 func (plugin Icla) SubTaskMetas() []core.SubTaskMeta {
 	return []core.SubTaskMeta{
 		tasks.CollectCommitterMeta,
diff --git a/plugins/jenkins/impl/impl.go b/plugins/jenkins/impl/impl.go
index 34b589113..6d1813d14 100644
--- a/plugins/jenkins/impl/impl.go
+++ b/plugins/jenkins/impl/impl.go
@@ -36,6 +36,7 @@ var _ core.PluginMeta = (*Jenkins)(nil)
 var _ core.PluginInit = (*Jenkins)(nil)
 var _ core.PluginTask = (*Jenkins)(nil)
 var _ core.PluginApi = (*Jenkins)(nil)
+var _ core.PluginModel = (*Jenkins)(nil)
 var _ core.PluginMigration = (*Jenkins)(nil)
 var _ core.CloseablePluginTask = (*Jenkins)(nil)
 
diff --git a/plugins/jira/impl/impl.go b/plugins/jira/impl/impl.go
index f54e2d67c..c7705aa5a 100644
--- a/plugins/jira/impl/impl.go
+++ b/plugins/jira/impl/impl.go
@@ -19,10 +19,11 @@ package impl
 
 import (
 	"fmt"
-	"github.com/apache/incubator-devlake/errors"
 	"net/http"
 	"time"
 
+	"github.com/apache/incubator-devlake/errors"
+
 	"github.com/apache/incubator-devlake/plugins/core"
 	"github.com/apache/incubator-devlake/plugins/helper"
 	"github.com/apache/incubator-devlake/plugins/jira/api"
@@ -37,6 +38,7 @@ var _ core.PluginMeta = (*Jira)(nil)
 var _ core.PluginInit = (*Jira)(nil)
 var _ core.PluginTask = (*Jira)(nil)
 var _ core.PluginApi = (*Jira)(nil)
+var _ core.PluginModel = (*Jira)(nil)
 var _ core.PluginMigration = (*Jira)(nil)
 var _ core.PluginBlueprintV100 = (*Jira)(nil)
 var _ core.CloseablePluginTask = (*Jira)(nil)
@@ -47,6 +49,7 @@ func (plugin Jira) Init(config *viper.Viper, logger core.Logger, db *gorm.DB) er
 	api.Init(config, logger, db)
 	return nil
 }
+
 func (plugin Jira) GetTablesInfo() []core.Tabler {
 	return []core.Tabler{
 		&models.ApiMyselfResponse{},
diff --git a/plugins/org/impl/impl.go b/plugins/org/impl/impl.go
index b6c8b01c9..76ed1ba9b 100644
--- a/plugins/org/impl/impl.go
+++ b/plugins/org/impl/impl.go
@@ -31,6 +31,7 @@ import (
 var _ core.PluginMeta = (*Org)(nil)
 var _ core.PluginInit = (*Org)(nil)
 var _ core.PluginTask = (*Org)(nil)
+var _ core.PluginModel = (*Org)(nil)
 
 type Org struct {
 	handlers *api.Handlers
diff --git a/plugins/refdiff/refdiff.go b/plugins/refdiff/refdiff.go
index 8a4e7fad8..342b9e006 100644
--- a/plugins/refdiff/refdiff.go
+++ b/plugins/refdiff/refdiff.go
@@ -34,6 +34,8 @@ var _ core.PluginMeta = (*RefDiff)(nil)
 var _ core.PluginInit = (*RefDiff)(nil)
 var _ core.PluginTask = (*RefDiff)(nil)
 var _ core.PluginApi = (*RefDiff)(nil)
+var _ core.PluginModel = (*RefDiff)(nil)
+var _ core.PluginMetric = (*RefDiff)(nil)
 
 // PluginEntry is a variable exported for Framework to search and load
 var PluginEntry RefDiff //nolint
@@ -44,10 +46,26 @@ func (plugin RefDiff) Description() string {
 	return "Calculate commits diff for specified ref pairs based on `commits` and `commit_parents` tables"
 }
 
+func (plugin RefDiff) RequiredDataEntities() (data []map[string]interface{}, err errors.Error) {
+	return []map[string]interface{}{}, nil
+}
+
 func (plugin RefDiff) GetTablesInfo() []core.Tabler {
 	return []core.Tabler{}
 }
 
+func (plugin RefDiff) IsProjectMetric() bool {
+	return false
+}
+
+func (plugin RefDiff) RunAfter() ([]string, errors.Error) {
+	return []string{}, nil
+}
+
+func (plugin RefDiff) Settings() interface{} {
+	return nil
+}
+
 func (plugin RefDiff) Init(config *viper.Viper, logger core.Logger, db *gorm.DB) errors.Error {
 	return nil
 }
diff --git a/plugins/starrocks/starrocks.go b/plugins/starrocks/starrocks.go
index 930bc7750..0db79922f 100644
--- a/plugins/starrocks/starrocks.go
+++ b/plugins/starrocks/starrocks.go
@@ -27,6 +27,11 @@ import (
 
 type StarRocks string
 
+// make sure interface is implemented
+var _ core.PluginMeta = (*StarRocks)(nil)
+var _ core.PluginTask = (*StarRocks)(nil)
+var _ core.PluginModel = (*StarRocks)(nil)
+
 func (s StarRocks) SubTaskMetas() []core.SubTaskMeta {
 	return []core.SubTaskMeta{
 		LoadDataTaskMeta,
diff --git a/plugins/tapd/impl/impl.go b/plugins/tapd/impl/impl.go
index af9d9572f..86f8f1461 100644
--- a/plugins/tapd/impl/impl.go
+++ b/plugins/tapd/impl/impl.go
@@ -37,6 +37,7 @@ var _ core.PluginMeta = (*Tapd)(nil)
 var _ core.PluginInit = (*Tapd)(nil)
 var _ core.PluginTask = (*Tapd)(nil)
 var _ core.PluginApi = (*Tapd)(nil)
+var _ core.PluginModel = (*Tapd)(nil)
 var _ core.PluginMigration = (*Tapd)(nil)
 var _ core.CloseablePluginTask = (*Tapd)(nil)
 
diff --git a/plugins/webhook/impl/impl.go b/plugins/webhook/impl/impl.go
index e555d6f96..372c3453e 100644
--- a/plugins/webhook/impl/impl.go
+++ b/plugins/webhook/impl/impl.go
@@ -30,6 +30,7 @@ import (
 var _ core.PluginMeta = (*Webhook)(nil)
 var _ core.PluginInit = (*Webhook)(nil)
 var _ core.PluginApi = (*Webhook)(nil)
+var _ core.PluginModel = (*Webhook)(nil)
 var _ core.PluginMigration = (*Webhook)(nil)
 
 type Webhook struct{}
@@ -43,6 +44,10 @@ func (plugin Webhook) Init(config *viper.Viper, logger core.Logger, db *gorm.DB)
 	return nil
 }
 
+func (plugin Webhook) GetTablesInfo() []core.Tabler {
+	return []core.Tabler{}
+}
+
 // PkgPath information lost when compiled as plugin(.so)
 func (plugin Webhook) RootPkgPath() string {
 	return "github.com/apache/incubator-devlake/plugins/webhook"
diff --git a/services/project.go b/services/project.go
new file mode 100644
index 000000000..29a7f506a
--- /dev/null
+++ b/services/project.go
@@ -0,0 +1,159 @@
+/*
+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 services
+
+import (
+	"github.com/apache/incubator-devlake/errors"
+	"github.com/apache/incubator-devlake/models"
+	"github.com/apache/incubator-devlake/plugins/helper"
+)
+
+// ProjectQuery used to query projects as the api project input
+type ProjectQuery struct {
+	Page     int `form:"page"`
+	PageSize int `form:"pageSize"`
+}
+
+// CreateProject accepts a project instance and insert it to database
+func CreateProject(project *models.Project) errors.Error {
+	/*project, err := encryptProject(project)
+	if err != nil {
+		return err
+	}*/
+	err := CreateDbProject(project)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+// CreateProjectMetric accepts a ProjectMetric instance and insert it to database
+func CreateProjectMetric(projectMetric *models.ProjectMetric) errors.Error {
+	/*enProjectMetric, err := encryptProjectMetric(projectMetric)
+	if err != nil {
+		return err
+	}*/
+	err := CreateDbProjectMetric(projectMetric)
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+// GetProject returns a Project
+func GetProject(name string) (*models.Project, errors.Error) {
+	project, err := GetDbProject(name)
+	if err != nil {
+		return nil, errors.Convert(err)
+	}
+
+	/*project, err = decryptProject(project)
+	if err != nil {
+		return nil, errors.Convert(err)
+	}*/
+
+	return project, nil
+}
+
+// GetProjectMetrics returns a ProjectMetric
+func GetProjectMetrics(projectName string, pluginName string) (*models.ProjectMetric, errors.Error) {
+	projectMetric, err := GetDbProjectMetrics(projectName, pluginName)
+	if err != nil {
+		return nil, errors.Convert(err)
+	}
+
+	/*projectMetric, err = decryptProjectMetric(projectMetric)
+	if err != nil {
+		return nil, errors.Convert(err)
+	}*/
+
+	return projectMetric, nil
+}
+
+// GetProjects returns a paginated list of Projects based on `query`
+func GetProjects(query *ProjectQuery) ([]*models.Project, int64, errors.Error) {
+	projects, count, err := GetDbProjects(query)
+	if err != nil {
+		return nil, 0, errors.Convert(err)
+	}
+
+	/*for i, project := range projects {
+		projects[i], err = decryptProject(project)
+		if err != nil {
+			return nil, 0, err
+		}
+	}*/
+
+	return projects, count, nil
+}
+
+// PatchProject FIXME ...
+func PatchProject(name string, body map[string]interface{}) (*models.Project, errors.Error) {
+	// load record from db
+	project, err := GetProject(name)
+	if err != nil {
+		return nil, err
+	}
+
+	err = helper.DecodeMapStruct(body, project)
+	if err != nil {
+		return nil, err
+	}
+
+	/*enProject, err := encryptProject(project)
+	if err != nil {
+		return nil, err
+	}*/
+
+	// save
+	err = SaveDbProject(project)
+	if err != nil {
+		return nil, errors.Internal.Wrap(err, "error saving project")
+	}
+
+	// done
+	return project, nil
+}
+
+// PatchProjectMetric FIXME ...
+func PatchProjectMetric(projectName string, pluginName string, body map[string]interface{}) (*models.ProjectMetric, errors.Error) {
+	// load record from db
+	projectMetric, err := GetDbProjectMetrics(projectName, pluginName)
+	if err != nil {
+		return nil, err
+	}
+
+	err = helper.DecodeMapStruct(body, projectMetric)
+	if err != nil {
+		return nil, err
+	}
+
+	/*enProjectMetric, err := encryptProjectMetric(projectMetric)
+	if err != nil {
+		return nil, err
+	}*/
+
+	// save
+	err = SaveDbProjectMetric(projectMetric)
+	if err != nil {
+		return nil, errors.Internal.Wrap(err, "error saving project")
+	}
+
+	// done
+	return projectMetric, nil
+}
diff --git a/services/project_helper.go b/services/project_helper.go
new file mode 100644
index 000000000..bde6b545d
--- /dev/null
+++ b/services/project_helper.go
@@ -0,0 +1,168 @@
+/*
+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 services
+
+import (
+	goerror "errors"
+	"fmt"
+
+	"github.com/apache/incubator-devlake/errors"
+	"github.com/apache/incubator-devlake/models"
+	"gorm.io/gorm"
+)
+
+// CreateDbProject accepts a project instance and insert it to database
+func CreateDbProject(project *models.Project) errors.Error {
+	err := db.Create(project).Error
+	if err != nil {
+		return errors.Default.Wrap(err, "error creating DB project")
+	}
+	return nil
+}
+
+// CreateDbProjectMetric accepts a project metric instance and insert it to database
+func CreateDbProjectMetric(projectMetric *models.ProjectMetric) errors.Error {
+	err := db.Create(projectMetric).Error
+	if err != nil {
+		return errors.Default.Wrap(err, "error creating DB project metric")
+	}
+	return nil
+}
+
+// SaveDbProject save a project instance and update it to database
+func SaveDbProject(project *models.Project) errors.Error {
+	err := db.Save(project).Error
+	if err != nil {
+		return errors.Default.Wrap(err, "error saving DB project")
+	}
+	return nil
+}
+
+// SaveDbProjectMetric save a project instance and update it to database
+func SaveDbProjectMetric(projectMetric *models.ProjectMetric) errors.Error {
+	err := db.Save(projectMetric).Error
+	if err != nil {
+		return errors.Default.Wrap(err, "error saving DB project metric")
+	}
+	return nil
+}
+
+// GetDbProjects returns a paginated list of Project based on `query`
+func GetDbProjects(query *ProjectQuery) ([]*models.Project, int64, errors.Error) {
+	projects := make([]*models.Project, 0)
+	db := db.Model(projects).Order("created_at desc")
+
+	var count int64
+	err := db.Count(&count).Error
+	if err != nil {
+		return nil, 0, errors.Default.Wrap(err, "error getting DB count of project")
+	}
+	db = processDbClausesWithPager(db, query.PageSize, query.Page)
+
+	err = db.Find(&projects).Error
+	if err != nil {
+		return nil, 0, errors.Default.Wrap(err, "error finding DB project")
+	}
+
+	return projects, count, nil
+}
+
+// GetDbProject returns the detail of a given project name
+func GetDbProject(name string) (*models.Project, errors.Error) {
+	project := &models.Project{}
+	project.Name = name
+
+	err := db.First(project).Error
+	if err != nil {
+		if goerror.Is(err, gorm.ErrRecordNotFound) {
+			return nil, errors.NotFound.Wrap(err, fmt.Sprintf("could not find project [%s] in DB", name))
+		}
+		return nil, errors.Default.Wrap(err, "error getting project from DB")
+	}
+
+	return project, nil
+}
+
+// GetDbProjectMetrics returns the detail of a given project name
+func GetDbProjectMetrics(projectName string, pluginName string) (*models.ProjectMetric, errors.Error) {
+	projectMetric := &models.ProjectMetric{}
+	projectMetric.ProjectName = projectName
+	projectMetric.PluginName = pluginName
+
+	err := db.First(projectMetric).Error
+	if err != nil {
+		if goerror.Is(err, gorm.ErrRecordNotFound) {
+			return nil, errors.NotFound.Wrap(err, fmt.Sprintf("could not find project metric [%s][%s] in DB", projectName, pluginName))
+		}
+		return nil, errors.Default.Wrap(err, "error getting project metric from DB")
+	}
+
+	return projectMetric, nil
+}
+
+// encryptProject
+/*func encryptProject(project *models.Project) (*models.Project, errors.Error) {
+	encKey := config.GetConfig().GetString(core.EncodeKeyEnvStr)
+
+	describeEncrypt, err := core.Encrypt(encKey, project.Description)
+	if err != nil {
+		return nil, err
+	}
+	project.Description = describeEncrypt
+
+	return project, nil
+}
+
+// encryptProjectMetric
+func encryptProjectMetric(projectMetric *models.ProjectMetric) (*models.ProjectMetric, errors.Error) {
+	encKey := config.GetConfig().GetString(core.EncodeKeyEnvStr)
+
+	pluginOption, err := core.Encrypt(encKey, projectMetric.PluginOption)
+	if err != nil {
+		return nil, err
+	}
+	projectMetric.PluginOption = pluginOption
+
+	return projectMetric, nil
+}*/
+
+// decryptProject
+/*func decryptProject(project *models.Project) (*models.Project, errors.Error) {
+	encKey := config.GetConfig().GetString(core.EncodeKeyEnvStr)
+
+	describe, err := core.Decrypt(encKey, project.Description)
+	if err != nil {
+		return nil, err
+	}
+	project.Description = describe
+
+	return project, nil
+}
+
+// decryptProjectMetric
+func decryptProjectMetric(projectMetric *models.ProjectMetric) (*models.ProjectMetric, errors.Error) {
+	encKey := config.GetConfig().GetString(core.EncodeKeyEnvStr)
+
+	pluginOption, err := core.Decrypt(encKey, projectMetric.PluginOption)
+	if err != nil {
+		return nil, err
+	}
+	projectMetric.PluginOption = pluginOption
+
+	return projectMetric, nil
+}*/