coze-studio/backend/application/memory/database.go

1034 lines
29 KiB
Go

/*
* Copyright 2025 coze-dev Authors
*
* Licensed 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 memory
import (
"context"
"fmt"
"github.com/coze-dev/coze-studio/backend/api/model/base"
model "github.com/coze-dev/coze-studio/backend/api/model/crossdomain/database"
"github.com/coze-dev/coze-studio/backend/api/model/knowledge/document"
resCommon "github.com/coze-dev/coze-studio/backend/api/model/resource/common"
"github.com/coze-dev/coze-studio/backend/api/model/table"
"github.com/coze-dev/coze-studio/backend/application/base/ctxutil"
"github.com/coze-dev/coze-studio/backend/application/search"
"github.com/coze-dev/coze-studio/backend/crossdomain/contract/crossuser"
"github.com/coze-dev/coze-studio/backend/domain/memory/database/entity"
databaseEntity "github.com/coze-dev/coze-studio/backend/domain/memory/database/entity"
database "github.com/coze-dev/coze-studio/backend/domain/memory/database/service"
searchEntity "github.com/coze-dev/coze-studio/backend/domain/search/entity"
"github.com/coze-dev/coze-studio/backend/pkg/errorx"
"github.com/coze-dev/coze-studio/backend/pkg/lang/conv"
"github.com/coze-dev/coze-studio/backend/pkg/lang/ptr"
"github.com/coze-dev/coze-studio/backend/pkg/lang/slices"
"github.com/coze-dev/coze-studio/backend/pkg/logs"
"github.com/coze-dev/coze-studio/backend/types/consts"
"github.com/coze-dev/coze-studio/backend/types/errno"
)
type DatabaseApplicationService struct {
DomainSVC database.Database
eventbus search.ResourceEventBus
}
var DatabaseApplicationSVC = DatabaseApplicationService{}
func (d *DatabaseApplicationService) GetModeConfig(ctx context.Context, req *table.GetModeConfigRequest) (*table.GetModeConfigResponse, error) {
return &table.GetModeConfigResponse{
Code: 0,
Msg: "success",
BotID: req.BotID,
Mode: "expert",
MaxTableNum: 3,
MaxColumnNum: 20,
MaxCapacityKb: 512000,
MaxRowNum: 100000,
}, nil
}
func (d *DatabaseApplicationService) ListDatabase(ctx context.Context, req *table.ListDatabaseRequest) (*table.ListDatabaseResponse, error) {
uid := ctxutil.GetUIDFromCtx(ctx)
if uid == nil {
return nil, errorx.New(errno.ErrMemoryPermissionCode, errorx.KV("msg", "session required"))
}
if req.SpaceID == nil {
return nil, errorx.New(errno.ErrMemoryPermissionCode, errorx.KV("msg", "space id is required"))
}
spaces, err := crossuser.DefaultSVC().GetUserSpaceList(ctx, *uid)
if err != nil {
return nil, err
}
if len(spaces) == 0 || spaces[0].ID != *req.SpaceID {
return nil, errorx.New(errno.ErrMemoryPermissionCode, errorx.KV("msg", "space id is invalid"))
}
res, err := d.DomainSVC.ListDatabase(ctx, convertListDatabase(req))
if err != nil {
return nil, err
}
bindDatabases := make([]*databaseEntity.Database, 0)
if req.GetBotID() != 0 {
resp, err := d.DomainSVC.MGetDatabaseByAgentID(ctx, &database.MGetDatabaseByAgentIDRequest{
AgentID: req.GetBotID(),
TableType: req.GetTableType(),
NeedSysFields: false,
})
if err != nil {
return nil, err
}
bindDatabases = resp.Databases
}
return convertListDatabaseRes(res, bindDatabases), nil
}
func (d *DatabaseApplicationService) GetDatabaseByID(ctx context.Context, req *table.SingleDatabaseRequest) (*table.SingleDatabaseResponse, error) {
basics := make([]*model.DatabaseBasic, 1)
b := &model.DatabaseBasic{
ID: req.ID,
}
if req.IsDraft {
b.TableType = table.TableType_DraftTable
} else {
b.TableType = table.TableType_OnlineTable
}
b.NeedSysFields = req.NeedSysFields
basics[0] = b
res, err := d.DomainSVC.MGetDatabase(ctx, &database.MGetDatabaseRequest{
Basics: basics,
})
if err != nil {
return nil, err
}
if len(res.Databases) == 0 {
return nil, fmt.Errorf("database %d not found", req.GetID())
}
return ConvertDatabaseRes(res.Databases[0]), nil
}
func (d *DatabaseApplicationService) AddDatabase(ctx context.Context, req *table.AddDatabaseRequest) (*table.SingleDatabaseResponse, error) {
uid := ctxutil.GetUIDFromCtx(ctx)
if uid == nil {
return nil, errorx.New(errno.ErrMemoryPermissionCode, errorx.KV("msg", "session required"))
}
if *uid != req.CreatorID {
return nil, errorx.New(errno.ErrMemoryPermissionCode, errorx.KV("msg", "creator id is invalid"))
}
if req.GetTableName() == "database" {
return nil, errorx.New(errno.ErrMemoryDatabaseNameInvalid)
}
spaces, err := crossuser.DefaultSVC().GetUserSpaceList(ctx, *uid)
if err != nil {
return nil, err
}
if len(spaces) == 0 || spaces[0].ID != req.SpaceID {
return nil, errorx.New(errno.ErrMemoryPermissionCode, errorx.KV("msg", "space id is invalid"))
}
res, err := d.DomainSVC.CreateDatabase(ctx, convertAddDatabase(req))
if err != nil {
return nil, err
}
databaseRes := res.Database
var ptrAppID *int64
if databaseRes.AppID != 0 {
ptrAppID = ptr.Of(databaseRes.AppID)
}
err = d.eventbus.PublishResources(ctx, &searchEntity.ResourceDomainEvent{
OpType: searchEntity.Created,
Resource: &searchEntity.ResourceDocument{
ResType: resCommon.ResType_Database,
ResID: databaseRes.ID,
Name: &databaseRes.TableName,
APPID: ptrAppID,
SpaceID: &databaseRes.SpaceID,
OwnerID: &databaseRes.CreatorID,
PublishStatus: ptr.Of(resCommon.PublishStatus_Published),
CreateTimeMS: ptr.Of(databaseRes.CreatedAtMs),
UpdateTimeMS: ptr.Of(databaseRes.UpdatedAtMs),
},
})
if err != nil {
return nil, fmt.Errorf("publish resource failed, err=%w", err)
}
return ConvertDatabaseRes(databaseRes), nil
}
func (d *DatabaseApplicationService) UpdateDatabase(ctx context.Context, req *table.UpdateDatabaseRequest) (*table.SingleDatabaseResponse, error) {
err := d.ValidateAccess(ctx, req.ID, table.TableType_OnlineTable)
if err != nil {
return nil, err
}
res, err := d.DomainSVC.UpdateDatabase(ctx, ConvertUpdateDatabase(req))
if err != nil {
return nil, err
}
databaseRes := res.Database
err = d.eventbus.PublishResources(ctx, &searchEntity.ResourceDomainEvent{
OpType: searchEntity.Updated,
Resource: &searchEntity.ResourceDocument{
ResType: resCommon.ResType_Database,
ResID: databaseRes.ID,
Name: &databaseRes.TableName,
UpdateTimeMS: ptr.Of(databaseRes.UpdatedAtMs),
},
})
if err != nil {
return nil, fmt.Errorf("publish resource failed, err=%w", err)
}
return convertUpdateDatabaseResult(res), nil
}
func (d *DatabaseApplicationService) DeleteDatabase(ctx context.Context, req *table.DeleteDatabaseRequest) (*table.DeleteDatabaseResponse, error) {
err := d.ValidateAccess(ctx, req.ID, table.TableType_OnlineTable)
if err != nil {
return nil, err
}
err = d.DomainSVC.DeleteDatabase(ctx, &database.DeleteDatabaseRequest{
ID: req.ID,
})
if err != nil {
return nil, err
}
err = d.eventbus.PublishResources(ctx, &searchEntity.ResourceDomainEvent{
OpType: searchEntity.Deleted,
Resource: &searchEntity.ResourceDocument{
ResType: resCommon.ResType_Database,
ResID: req.ID,
},
})
if err != nil {
return nil, err
}
return &table.DeleteDatabaseResponse{
Code: 0,
Msg: "success",
BaseResp: base.NewBaseResp(),
}, nil
}
func (d *DatabaseApplicationService) ListDatabaseRecords(ctx context.Context, req *table.ListDatabaseRecordsRequest) (*table.ListDatabaseRecordsResponse, error) {
tableType := table.TableType_OnlineTable
if req.GetBotID() > 0 {
tableType = table.TableType_DraftTable
}
err := d.ValidateAccess(ctx, req.DatabaseID, tableType)
if err != nil {
return nil, err
}
databaseID := req.DatabaseID
if req.GetBotID() == 0 {
databaseID, err = getDatabaseID(ctx, req.TableType, req.DatabaseID)
if err != nil {
return nil, err
}
}
uid := ctxutil.GetUIDFromCtx(ctx)
if uid == nil {
return nil, errorx.New(errno.ErrMemoryPermissionCode, errorx.KV("msg", "session required"))
}
domainReq := &database.ListDatabaseRecordRequest{
DatabaseID: databaseID,
TableType: req.TableType,
Limit: int(req.Limit),
Offset: int(req.Offset),
UserID: *uid,
}
// FilterCriterion, NotFilterByUserID, OrderByList not use
res, err := d.DomainSVC.ListDatabaseRecord(ctx, domainReq)
if err != nil {
return nil, err
}
return convertListDatabaseRecordsRes(res), nil
}
func (d *DatabaseApplicationService) UpdateDatabaseRecords(ctx context.Context, req *table.UpdateDatabaseRecordsRequest) (*table.UpdateDatabaseRecordsResponse, error) {
uid := ctxutil.GetUIDFromCtx(ctx)
if uid == nil {
return nil, errorx.New(errno.ErrMemoryPermissionCode, errorx.KV("msg", "session required"))
}
err := d.ValidateAccess(ctx, req.DatabaseID, table.TableType_OnlineTable)
if err != nil {
return nil, err
}
databaseID, err := getDatabaseID(ctx, req.GetTableType(), req.GetDatabaseID())
if err != nil {
return nil, err
}
dataRes := make([]map[string]string, 0)
if len(req.GetRecordDataAdd()) > 0 {
err := d.DomainSVC.AddDatabaseRecord(ctx, &database.AddDatabaseRecordRequest{
DatabaseID: databaseID,
TableType: req.GetTableType(),
Records: req.GetRecordDataAdd(),
UserID: *uid,
})
if err != nil {
return nil, err
}
dataRes = append(dataRes, req.GetRecordDataAdd()...)
}
if len(req.GetRecordDataAlter()) > 0 {
err := d.DomainSVC.UpdateDatabaseRecord(ctx, &database.UpdateDatabaseRecordRequest{
DatabaseID: databaseID,
TableType: req.GetTableType(),
Records: req.GetRecordDataAlter(),
UserID: *uid,
})
if err != nil {
return nil, err
}
dataRes = append(dataRes, req.GetRecordDataAlter()...)
}
if len(req.GetRecordDataDelete()) > 0 {
err := d.DomainSVC.DeleteDatabaseRecord(ctx, &database.DeleteDatabaseRecordRequest{
DatabaseID: databaseID,
TableType: req.GetTableType(),
Records: req.GetRecordDataDelete(),
UserID: *uid,
})
if err != nil {
return nil, err
}
dataRes = append(dataRes, req.GetRecordDataDelete()...)
}
return &table.UpdateDatabaseRecordsResponse{
Data: dataRes,
Code: 0,
Msg: "success",
BaseResp: &base.BaseResp{
StatusCode: 0,
StatusMessage: "success",
},
}, nil
}
func (d *DatabaseApplicationService) GetOnlineDatabaseId(ctx context.Context, req *table.GetOnlineDatabaseIdRequest) (*table.GetOnlineDatabaseIdResponse, error) {
basics := make([]*model.DatabaseBasic, 1)
basics[0] = &model.DatabaseBasic{
ID: req.ID,
TableType: table.TableType_DraftTable,
}
res, err := d.DomainSVC.MGetDatabase(ctx, &database.MGetDatabaseRequest{
Basics: basics,
})
if err != nil {
return nil, err
}
if len(res.Databases) == 0 {
return nil, fmt.Errorf("database %d not found", req.ID)
}
return &table.GetOnlineDatabaseIdResponse{
ID: res.Databases[0].OnlineID,
Code: 0,
Msg: "success",
}, nil
}
func (d *DatabaseApplicationService) ResetBotTable(ctx context.Context, req *table.ResetBotTableRequest) (*table.ResetBotTableResponse, error) {
uid := ctxutil.GetUIDFromCtx(ctx)
if uid == nil {
return nil, errorx.New(errno.ErrMemoryPermissionCode, errorx.KV("msg", "session required"))
}
tableType := table.TableType_OnlineTable
if req.GetBotID() > 0 {
tableType = table.TableType_DraftTable
}
err := d.ValidateAccess(ctx, req.GetDatabaseInfoID(), tableType)
if err != nil {
return nil, err
}
databaseID := req.GetDatabaseInfoID()
if req.GetBotID() == 0 {
databaseID, err = getDatabaseID(ctx, req.TableType, req.GetDatabaseInfoID())
if err != nil {
return nil, err
}
}
executeDeleteReq := &database.ExecuteSQLRequest{
DatabaseID: databaseID,
TableType: req.GetTableType(),
OperateType: model.OperateType_Delete,
UserID: conv.Int64ToStr(*uid),
Condition: &model.ComplexCondition{
Conditions: []*model.Condition{
{
Left: model.DefaultIDColName,
Operation: model.Operation_GREATER_THAN,
Right: "?",
},
},
Logic: model.Logic_And,
},
SQLParams: []*model.SQLParamVal{
{
ValueType: table.FieldItemType_Number,
Value: ptr.Of("0"),
},
},
}
_, err = d.DomainSVC.ExecuteSQL(ctx, executeDeleteReq)
if err != nil {
return nil, err
}
return &table.ResetBotTableResponse{
Code: ptr.Of(int64(0)),
Msg: ptr.Of("success"),
BaseResp: base.NewBaseResp(),
}, nil
}
func (d *DatabaseApplicationService) GetDatabaseTemplate(ctx context.Context, req *table.GetDatabaseTemplateRequest) (*table.GetDatabaseTemplateResponse, error) {
uid := ctxutil.GetUIDFromCtx(ctx)
if uid == nil {
return nil, errorx.New(errno.ErrMemoryPermissionCode, errorx.KV("msg", "session required"))
}
databaseID, err := getDatabaseID(ctx, req.TableType, req.DatabaseID)
if err != nil {
return nil, err
}
err = d.ValidateAccess(ctx, req.DatabaseID, table.TableType_OnlineTable)
if err != nil {
return nil, err
}
var fields []*model.FieldItem
var tableName string
if req.GetTableType() == table.TableType_DraftTable {
basics := make([]*model.DatabaseBasic, 1)
basics[0] = &model.DatabaseBasic{
ID: databaseID,
TableType: table.TableType_DraftTable,
}
info, err := d.DomainSVC.MGetDatabase(ctx, &database.MGetDatabaseRequest{
Basics: basics,
})
if err != nil {
return nil, err
}
fields = info.Databases[0].FieldList
tableName = info.Databases[0].TableName
} else {
basics := make([]*model.DatabaseBasic, 1)
basics[0] = &model.DatabaseBasic{
ID: databaseID,
TableType: table.TableType_OnlineTable,
}
info, err := d.DomainSVC.MGetDatabase(ctx, &database.MGetDatabaseRequest{
Basics: basics,
})
if err != nil {
return nil, err
}
fields = info.Databases[0].FieldList
tableName = info.Databases[0].TableName
}
items := make([]*table.FieldItem, 0, len(fields))
for _, field := range fields {
items = append(items, &table.FieldItem{
Name: field.Name,
Desc: field.Desc,
Type: field.Type,
MustRequired: field.MustRequired,
})
}
resp, err := d.DomainSVC.GetDatabaseTemplate(ctx, &database.GetDatabaseTemplateRequest{
UserID: *uid,
TableName: tableName,
FieldItems: items,
})
if err != nil {
return nil, err
}
return &table.GetDatabaseTemplateResponse{
TosUrl: resp.Url,
Code: 0,
Msg: "success",
BaseResp: &base.BaseResp{
StatusCode: 0,
StatusMessage: "success",
},
}, nil
}
func (d *DatabaseApplicationService) GetConnectorName(ctx context.Context, req *table.GetSpaceConnectorListRequest) (*table.GetSpaceConnectorListResponse, error) {
return &table.GetSpaceConnectorListResponse{
ConnectorList: []*table.ConnectorInfo{
{
ConnectorID: consts.CozeConnectorID,
ConnectorName: "Coze",
},
{
ConnectorID: consts.WebSDKConnectorID,
ConnectorName: "Chat SDK",
},
{
ConnectorID: consts.APIConnectorID,
ConnectorName: "API",
},
},
Code: 0,
Msg: "success",
BaseResp: &base.BaseResp{
StatusCode: 0,
StatusMessage: "success",
},
}, nil
}
func (d *DatabaseApplicationService) GetBotDatabase(ctx context.Context, req *table.GetBotTableRequest) (*table.GetBotTableResponse, error) {
relationResp, err := d.DomainSVC.MGetRelationsByAgentID(ctx, &database.MGetRelationsByAgentIDRequest{
AgentID: req.GetBotID(),
TableType: req.GetTableType(),
})
if err != nil {
return nil, err
}
relationMap := slices.ToMap(relationResp.Relations, func(d *model.AgentToDatabase) (int64, *model.AgentToDatabase) {
return d.DatabaseID, d
})
resp, err := d.DomainSVC.MGetDatabaseByAgentID(ctx, &database.MGetDatabaseByAgentIDRequest{
AgentID: req.GetBotID(),
TableType: req.GetTableType(),
NeedSysFields: false,
})
if err != nil {
return nil, err
}
return &table.GetBotTableResponse{
BotTableList: convertToBotTableList(resp.Databases, req.GetBotID(), relationMap),
Code: 0,
Msg: "success",
BaseResp: base.NewBaseResp(),
}, nil
}
func (d *DatabaseApplicationService) ValidateDatabaseTableSchema(ctx context.Context, req *table.ValidateTableSchemaRequest) (*table.ValidateTableSchemaResponse, error) {
uid := ctxutil.GetUIDFromCtx(ctx)
if uid == nil {
return nil, errorx.New(errno.ErrMemoryPermissionCode, errorx.KV("msg", "session required"))
}
if req.GetSourceInfo() == nil || req.GetTableSheet() == nil {
return nil, fmt.Errorf("source file and table sheet required")
}
databaseID, err := getDatabaseID(ctx, req.TableType, req.DatabaseID)
if err != nil {
return nil, err
}
err = d.ValidateAccess(ctx, req.DatabaseID, table.TableType_OnlineTable)
if err != nil {
return nil, err
}
basics := make([]*model.DatabaseBasic, 1)
basics[0] = &model.DatabaseBasic{
ID: databaseID,
TableType: req.TableType,
}
info, err := d.DomainSVC.MGetDatabase(ctx, &database.MGetDatabaseRequest{
Basics: basics,
})
if err != nil {
return nil, err
}
if len(info.Databases) == 0 {
return nil, fmt.Errorf("database %d not found", req.DatabaseID)
}
res, err := d.DomainSVC.ValidateDatabaseTableSchema(ctx, &database.ValidateDatabaseTableSchemaRequest{
DatabaseID: req.GetDatabaseID(),
UserID: *uid,
TosURL: req.GetSourceInfo().GetTosURI(),
TableSheet: databaseEntity.TableSheet{
SheetID: req.GetTableSheet().GetSheetID(),
HeaderLineIdx: req.GetTableSheet().GetHeaderLineIdx(),
StartLineIdx: req.GetTableSheet().GetStartLineIdx(),
},
Fields: info.Databases[0].FieldList,
})
if err != nil {
return nil, err
}
if !res.Valid {
return nil, errorx.New(errno.ErrMemoryInvalidParamCode,
errorx.KV("msg", res.GetInvalidMsg()))
}
return &table.ValidateTableSchemaResponse{
Code: 0,
Msg: "success",
BaseResp: base.NewBaseResp(),
}, nil
}
func (d *DatabaseApplicationService) GetDatabaseTableSchema(ctx context.Context, req *table.GetTableSchemaRequest) (*document.GetTableSchemaInfoResponse, error) {
uid := ctxutil.GetUIDFromCtx(ctx)
if uid == nil {
return nil, errorx.New(errno.ErrMemoryPermissionCode, errorx.KV("msg", "session required"))
}
if req.GetSourceFile() == nil || req.GetTableSheet() == nil {
return nil, fmt.Errorf("source file and table sheet required")
}
tableType := table.TableDataType_AllData
if req.TableDataType != nil {
tableType = req.GetTableDataType()
}
schema, err := d.DomainSVC.GetDatabaseTableSchema(ctx, &database.GetDatabaseTableSchemaRequest{
DatabaseID: req.GetDatabaseID(),
UserID: *uid,
TosURL: req.GetSourceFile().GetTosURI(),
TableSheet: databaseEntity.TableSheet{
SheetID: req.GetTableSheet().GetSheetID(),
HeaderLineIdx: req.GetTableSheet().GetHeaderLineIdx(),
StartLineIdx: req.GetTableSheet().GetStartLineIdx(),
},
// 不传默认返回所有数据
TableDataType: tableType,
})
if err != nil {
return nil, err
}
return &document.GetTableSchemaInfoResponse{
TableMeta: schema.TableMeta,
SheetList: schema.SheetList,
PreviewData: schema.PreviewData,
BaseResp: base.NewBaseResp(),
}, nil
}
func (d *DatabaseApplicationService) SubmitDatabaseInsertTask(ctx context.Context, req *table.SubmitDatabaseInsertRequest) (*table.SubmitDatabaseInsertResponse, error) {
uid := ctxutil.GetUIDFromCtx(ctx)
if uid == nil {
return nil, errorx.New(errno.ErrMemoryPermissionCode, errorx.KV("msg", "session required"))
}
databaseID, err := getDatabaseID(ctx, req.TableType, req.DatabaseID)
if err != nil {
return nil, err
}
err = d.ValidateAccess(ctx, req.DatabaseID, table.TableType_OnlineTable)
if err != nil {
return nil, err
}
err = d.DomainSVC.SubmitDatabaseInsertTask(ctx, &database.SubmitDatabaseInsertTaskRequest{
DatabaseID: databaseID,
UserID: *uid,
FileURI: req.GetFileURI(),
TableSheet: databaseEntity.TableSheet{
SheetID: req.GetTableSheet().GetSheetID(),
HeaderLineIdx: req.GetTableSheet().GetHeaderLineIdx(),
StartLineIdx: req.GetTableSheet().GetStartLineIdx(),
},
ConnectorID: req.ConnectorID,
TableType: req.TableType,
})
if err != nil {
return nil, err
}
return &table.SubmitDatabaseInsertResponse{
Code: 0,
Msg: "success",
BaseResp: base.NewBaseResp(),
}, nil
}
func (d *DatabaseApplicationService) DatabaseFileProgressData(ctx context.Context, req *table.GetDatabaseFileProgressRequest) (*table.GetDatabaseFileProgressResponse, error) {
uid := ctxutil.GetUIDFromCtx(ctx)
if uid == nil {
return nil, errorx.New(errno.ErrMemoryPermissionCode, errorx.KV("msg", "session required"))
}
databaseID, err := getDatabaseID(ctx, req.TableType, req.DatabaseID)
if err != nil {
return nil, err
}
res, err := d.DomainSVC.GetDatabaseFileProgressData(ctx, &database.GetDatabaseFileProgressDataRequest{
DatabaseID: databaseID,
UserID: *uid,
TableType: req.TableType,
})
if err != nil {
return nil, err
}
return &table.GetDatabaseFileProgressResponse{
Data: &table.DatabaseFileProgressData{
FileName: res.FileName,
Progress: res.Progress,
StatusDescript: res.StatusDescript,
},
Code: 0,
Msg: "success",
BaseResp: base.NewBaseResp(),
}, nil
}
func getDatabaseID(ctx context.Context, tableType table.TableType, onlineID int64) (int64, error) {
if tableType == table.TableType_OnlineTable {
return onlineID, nil
}
online, err := DatabaseApplicationSVC.DomainSVC.MGetDatabase(ctx, &database.MGetDatabaseRequest{
Basics: []*model.DatabaseBasic{
{
ID: onlineID,
TableType: table.TableType_OnlineTable,
},
},
})
if err != nil {
return -1, err
}
if len(online.Databases) == 0 {
return -1, fmt.Errorf("online table not found, id: %d", onlineID)
}
return online.Databases[0].GetDraftID(), nil
}
func (d *DatabaseApplicationService) ValidateAccess(ctx context.Context, databaseID int64, tableType table.TableType) error {
uid := ctxutil.GetUIDFromCtx(ctx)
if uid == nil {
return errorx.New(errno.ErrMemoryPermissionCode, errorx.KV("msg", "session uid not found"))
}
do, err := d.DomainSVC.MGetDatabase(ctx, &database.MGetDatabaseRequest{
Basics: []*model.DatabaseBasic{
{
ID: databaseID,
TableType: tableType,
},
},
})
if err != nil {
return err
}
if len(do.Databases) == 0 {
return errorx.New(errno.ErrMemoryPermissionCode, errorx.KV("msg", "database not found"))
}
if do.Databases[0].CreatorID != *uid {
logs.CtxErrorf(ctx, "user(%d) is not the creator(%d) of the database(%d)", *uid, do.Databases[0].CreatorID, databaseID)
return errorx.New(errno.ErrMemoryPermissionCode, errorx.KV("detail", "you are not the creator of the database"))
}
return nil
}
func (d *DatabaseApplicationService) DeleteDatabaseByAppID(ctx context.Context, appID int64) error {
resp, err := d.DomainSVC.DeleteDatabaseByAppID(ctx, &database.DeleteDatabaseByAppIDRequest{
AppID: appID,
})
if err != nil {
return err
}
deletedIDs := resp.DeletedDatabaseIDs
for _, deletedID := range deletedIDs {
err = d.eventbus.PublishResources(ctx, &searchEntity.ResourceDomainEvent{
OpType: searchEntity.Deleted,
Resource: &searchEntity.ResourceDocument{
ResType: resCommon.ResType_Database,
ResID: deletedID,
},
})
if err != nil {
return err
}
}
return nil
}
func (d *DatabaseApplicationService) CopyDatabase(ctx context.Context, req *CopyDatabaseRequest) (*CopyDatabaseResponse, error) {
var err error
basics := make([]*model.DatabaseBasic, 0, len(req.DatabaseIDs))
for _, id := range req.DatabaseIDs {
basics = append(basics, &model.DatabaseBasic{
ID: id,
TableType: req.TableType,
})
}
res, err := d.DomainSVC.MGetDatabase(ctx, &database.MGetDatabaseRequest{Basics: basics})
if err != nil {
return nil, err
}
copyDatabases := make(map[int64]*entity.Database, len(res.Databases))
draftMaps := make(map[int64]int64)
onlineMaps := make(map[int64]int64)
for _, srcDB := range res.Databases {
if req.Suffix != nil {
srcDB.TableName += *req.Suffix
} else {
srcDB.TableName += "_copy"
}
if req.TargetSpaceID != nil {
srcDB.SpaceID = *req.TargetSpaceID
}
originalID := srcDB.ID
originalDraftID := srcDB.GetDraftID()
originalOnlineID := srcDB.GetOnlineID()
srcDB.AppID = req.TargetAppID
srcDB.CreatorID = req.CreatorID
if req.TargetSpaceID != nil {
}
createDatabaseResp, err := d.DomainSVC.CreateDatabase(ctx, &database.CreateDatabaseRequest{
Database: srcDB,
})
if err != nil {
return nil, err
}
onlineDatabase := createDatabaseResp.Database
draftResp, err := d.DomainSVC.GetDraftDatabaseByOnlineID(ctx, &database.GetDraftDatabaseByOnlineIDRequest{
OnlineID: onlineDatabase.ID,
})
if err != nil {
return nil, err
}
copyDatabases[originalID] = onlineDatabase
draftDatabase := draftResp.Database
draftMaps[originalDraftID] = draftDatabase.ID
onlineMaps[originalOnlineID] = onlineDatabase.ID
err = d.eventbus.PublishResources(ctx, &searchEntity.ResourceDomainEvent{
OpType: searchEntity.Created,
Resource: &searchEntity.ResourceDocument{
ResType: resCommon.ResType_Database,
ResID: onlineDatabase.ID,
Name: &onlineDatabase.TableName,
APPID: &onlineDatabase.AppID,
SpaceID: &onlineDatabase.SpaceID,
OwnerID: &onlineDatabase.CreatorID,
PublishStatus: ptr.Of(resCommon.PublishStatus_Published),
CreateTimeMS: ptr.Of(onlineDatabase.CreatedAtMs),
UpdateTimeMS: ptr.Of(onlineDatabase.UpdatedAtMs),
},
})
if err != nil {
return nil, fmt.Errorf("publish resource failed, err=%w", err)
}
}
if !req.IsCopyData {
return &CopyDatabaseResponse{
Databases: copyDatabases,
}, nil
}
for srcID, targetID := range draftMaps {
err = d.duplicateRecords(ctx, srcID, targetID, req.CreatorID, table.TableType_DraftTable)
if err != nil {
return nil, err
}
}
for srcID, targetID := range onlineMaps {
err = d.duplicateRecords(ctx, srcID, targetID, req.CreatorID, table.TableType_OnlineTable)
if err != nil {
return nil, err
}
}
return &CopyDatabaseResponse{
Databases: copyDatabases,
}, nil
}
func (d *DatabaseApplicationService) duplicateRecords(ctx context.Context, srcDatabaseID, targetDatabaseID, userID int64, tableType table.TableType) error {
listReq := &database.ListDatabaseRecordRequest{
DatabaseID: srcDatabaseID,
TableType: tableType,
UserID: userID,
Limit: 1000,
Offset: 0,
}
for {
listResp, err := d.DomainSVC.ListDatabaseRecord(ctx, listReq)
if err != nil {
return fmt.Errorf("list source database records failed: %v", err)
}
if len(listResp.Records) == 0 {
break
}
addReq := &database.AddDatabaseRecordRequest{
DatabaseID: targetDatabaseID,
TableType: tableType,
UserID: userID,
Records: listResp.Records,
}
err = d.DomainSVC.AddDatabaseRecord(ctx, addReq)
if err != nil {
return fmt.Errorf("copy data failed: %v", err)
}
if !listResp.HasMore {
break
}
listReq.Offset += listReq.Limit
}
return nil
}
type CopyDatabaseRequest struct {
DatabaseIDs []int64
TableType table.TableType // table type of the source databases
CreatorID int64
IsCopyData bool // is need to copy data
TargetSpaceID *int64 // if is nil, it will be set to the same space as the original database
TargetAppID int64 // if is nil, it will be set to the same app as the original database; if copy to resource, set to 0
Suffix *string // table name suffix for the copied table, default is "_copy"
}
type CopyDatabaseResponse struct {
Databases map[int64]*entity.Database // key is original database id (online id or draft id), value is the new online database
}
func (d *DatabaseApplicationService) MoveDatabaseToLibrary(ctx context.Context, req *MoveDatabaseToLibraryRequest) (*MoveDatabaseToLibraryResponse, error) {
basics := make([]*model.DatabaseBasic, 0, len(req.DatabaseIDs))
for _, id := range req.DatabaseIDs {
basics = append(basics, &model.DatabaseBasic{
ID: id,
TableType: req.TableType,
})
}
res, err := d.DomainSVC.MGetDatabase(ctx, &database.MGetDatabaseRequest{Basics: basics})
if err != nil {
return nil, err
}
moveDatabases := make([]*entity.Database, 0, len(req.DatabaseIDs))
for _, srcDB := range res.Databases {
srcDB.AppID = 0
moveDatabaseResp, err := d.DomainSVC.UpdateDatabase(ctx, &database.UpdateDatabaseRequest{
Database: srcDB,
})
if err != nil {
return nil, err
}
onlineDatabase := moveDatabaseResp.Database
moveDatabases = append(moveDatabases, onlineDatabase)
// publish resource event
err = d.eventbus.PublishResources(ctx, &searchEntity.ResourceDomainEvent{
OpType: searchEntity.Updated,
Resource: &searchEntity.ResourceDocument{
ResType: resCommon.ResType_Database,
ResID: onlineDatabase.ID,
Name: &onlineDatabase.TableName,
APPID: &onlineDatabase.AppID,
SpaceID: &onlineDatabase.SpaceID,
UpdateTimeMS: &onlineDatabase.UpdatedAtMs,
},
})
if err != nil {
return nil, fmt.Errorf("publish resource failed, err=%w", err)
}
}
return &MoveDatabaseToLibraryResponse{
Databases: moveDatabases,
}, nil
}
type MoveDatabaseToLibraryRequest struct {
DatabaseIDs []int64
TableType table.TableType // table type of the source databases
}
type MoveDatabaseToLibraryResponse struct {
Databases []*entity.Database // the online databases after move
}