Go: update search (#14023)

### What problem does this PR solve?

Update search

### Type of change

- [x] New Feature (non-breaking change which adds functionality)

Signed-off-by: Jin Hai <haijin.chn@gmail.com>
This commit is contained in:
Jin Hai
2026-04-13 15:07:04 +08:00
committed by GitHub
parent 1638083e18
commit 3e787b3b09
4 changed files with 229 additions and 19 deletions

View File

@@ -155,8 +155,25 @@ func (dao *SearchDAO) DeleteByID(id string) error {
// Accessible4Deletion checks if a search can be deleted by a specific user
// Reference: Python search_service.py::accessible4deletion
// Returns true if the search exists, is valid, and was created by the user
func (dao *SearchDAO) Accessible4Deletion(searchID string, userID string) bool {
func (dao *SearchDAO) Accessible4Deletion(searchID string, userID string) (bool, error) {
var search entity.Search
err := DB.Where("id = ? AND created_by = ? AND status = ?", searchID, userID, "1").First(&search).Error
return err == nil
return err == nil, err
}
// GetByTenantIDAndID gets search by tenant ID and search ID
// Reference: Python SearchService.query(tenant_id=tenant_id, id=search_id)
func (dao *SearchDAO) GetByTenantIDAndID(tenantID string, searchID string) (*entity.Search, error) {
var search entity.Search
err := DB.Where("tenant_id = ? AND id = ? AND status = ?", tenantID, searchID, "1").First(&search).Error
if err != nil {
return nil, err
}
return &search, nil
}
// UpdateByID updates search by ID
// Reference: Python common_service.py::update_by_id
func (dao *SearchDAO) UpdateByID(id string, updates map[string]interface{}) error {
return DB.Model(&entity.Search{}).Where("id = ?", id).Updates(updates).Error
}

View File

@@ -309,3 +309,115 @@ func (h *SearchHandler) DeleteSearch(c *gin.Context) {
"message": "success",
})
}
// UpdateSearch update a search app
// @Summary Update Search App
// @Description Update a search app by ID
// @Tags search
// @Accept json
// @Produce json
// @Param search_id path string true "search app ID"
// @Param request body service.UpdateSearchRequest true "search update parameters"
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/searches/{search_id} [put]
func (h *SearchHandler) UpdateSearch(c *gin.Context) {
// Get current user from context (same as Python current_user)
user, errorCode, errorMessage := GetUser(c)
if errorCode != common.CodeSuccess {
jsonError(c, errorCode, errorMessage)
return
}
userID := user.ID
// Get search_id from path parameter (same as Python <search_id>)
searchID := c.Param("search_id")
if searchID == "" {
c.JSON(http.StatusBadRequest, gin.H{
"code": common.CodeBadRequest,
"data": nil,
"message": "search_id is required",
})
return
}
// Parse request body
var req service.UpdateSearchRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"code": common.CodeBadRequest,
"data": nil,
"message": "Invalid request body: " + err.Error(),
})
return
}
// Validate name (same as Python validation)
if err := common.ValidateName(req.Name); err != nil {
c.JSON(http.StatusOK, gin.H{
"code": common.CodeDataError,
"data": nil,
"message": err.Error(),
})
return
}
// Update search
updatedSearch, err := h.searchService.UpdateSearch(userID, searchID, &req)
if err != nil {
errMsg := err.Error()
switch errMsg {
case "no authorization":
c.JSON(http.StatusOK, gin.H{
"code": common.CodeAuthenticationError,
"data": false,
"message": "No authorization.",
})
case "duplicated search name":
c.JSON(http.StatusOK, gin.H{
"code": common.CodeDataError,
"data": nil,
"message": "Duplicated search name.",
})
default:
// Check if it's a "cannot find search" error
if len(errMsg) > 18 && errMsg[:18] == "cannot find search" {
c.JSON(http.StatusOK, gin.H{
"code": common.CodeDataError,
"data": false,
"message": errMsg,
})
} else {
c.JSON(http.StatusOK, gin.H{
"code": common.CodeDataError,
"data": nil,
"message": errMsg,
})
}
}
return
}
// Convert to response format (same as Python updated_search.to_dict())
result := map[string]interface{}{
"id": updatedSearch.ID,
"tenant_id": updatedSearch.TenantID,
"name": updatedSearch.Name,
"description": updatedSearch.Description,
"created_by": updatedSearch.CreatedBy,
"status": updatedSearch.Status,
"create_time": updatedSearch.CreateTime,
"update_time": updatedSearch.UpdateTime,
"search_config": updatedSearch.SearchConfig,
}
if updatedSearch.Avatar != nil {
result["avatar"] = *updatedSearch.Avatar
}
// Return success response
c.JSON(http.StatusOK, gin.H{
"code": common.CodeSuccess,
"data": result,
"message": "success",
})
}

View File

@@ -175,17 +175,18 @@ func (r *Router) Setup(engine *gin.Engine) {
// message.GET("/:memory_id/:message_id/content", r.memoryHandler.GetMessageContent)
// }
chats := v1.Group("/chats")
{
chats.GET("", r.chatHandler.ListChats)
chats.GET("/:chat_id", r.chatHandler.GetChat)
}
chats := v1.Group("/chats")
{
chats.GET("", r.chatHandler.ListChats)
chats.GET("/:chat_id", r.chatHandler.GetChat)
}
searches := v1.Group("/searches")
{
searches.GET("", r.searchHandler.ListSearches)
searches.POST("", r.searchHandler.CreateSearch)
searches.GET("/:search_id", r.searchHandler.GetSearch)
searches.PUT("/:search_id", r.searchHandler.UpdateSearch)
searches.DELETE("/:search_id", r.searchHandler.DeleteSearch)
}

View File

@@ -149,16 +149,6 @@ type CreateSearchResponse struct {
// 5. Set fields: id, name, description, tenant_id, created_by
// 6. Save to database within DB.atomic() transaction
// 7. Return {search_id: id} on success
//
// Error handling from Python:
// - Name not string: "Search name must be string."
// - Name empty: "Search name can't be empty."
// - Name too long: "Search name length is X which is larger than 255."
// - Tenant not found: "Authorized identity."
// - Save failure: generic get_data_error_result()
//
// Note: Go implementation validates these in handler layer for cleaner separation
// Note: Similar pattern in: CreateMemory (memory.go), CreateDataset (datasets.go)
func (s *SearchService) CreateSearch(userID string, name string, description *string) (*CreateSearchResponse, error) {
// Generate UUID for search ID (same as Python get_uuid())
searchID := common.GenerateUUID()
@@ -238,15 +228,105 @@ func (s *SearchService) GetSearchDetail(userID string, searchID string) (*entity
func (s *SearchService) DeleteSearch(userID string, searchID string) error {
// Step 1: Check deletion permission (same as Python SearchService.accessible4deletion)
// Python: cls.model.select().where(cls.model.id == search_id, cls.model.created_by == user_id, cls.model.status == StatusEnum.VALID.value).first()
if !s.searchDAO.Accessible4Deletion(searchID, userID) {
status, err := s.searchDAO.Accessible4Deletion(searchID, userID)
if err != nil {
return fmt.Errorf("failed to check deletion permission: %w", err)
}
if !status {
return fmt.Errorf("no authorization")
}
// Step 2: Execute delete (same as Python SearchService.delete_by_id)
// Python: cls.model.delete().where(cls.model.id == pid).execute()
if err := s.searchDAO.DeleteByID(searchID); err != nil {
if err = s.searchDAO.DeleteByID(searchID); err != nil {
return fmt.Errorf("failed to delete search App %s: %w", searchID, err)
}
return nil
}
// UpdateSearchRequest update search request
// Reference: api/apps/restful_apis/search_api.py::update
// Required fields: name, search_config
// Optional fields: description
// Immutable fields: search_id, tenant_id, created_by, update_time, id (will be removed)
type UpdateSearchRequest struct {
Name string `json:"name" binding:"required"`
Description *string `json:"description,omitempty"`
SearchConfig map[string]interface{} `json:"search_config" binding:"required"`
}
func (s *SearchService) UpdateSearch(userID string, searchID string, req *UpdateSearchRequest) (*entity.Search, error) {
// Step 1: Check update permission (same as delete - uses accessible4deletion)
// Only creator can update
status, err := s.searchDAO.Accessible4Deletion(searchID, userID)
if err != nil {
return nil, fmt.Errorf("failed to check deletion permission: %w", err)
}
if !status {
return nil, fmt.Errorf("no authorization")
}
// Step 2: Get existing search
// Python: search_app = SearchService.query(tenant_id=current_user.id, id=search_id)[0]
search, err := s.searchDAO.GetByTenantIDAndID(userID, searchID)
if err != nil {
return nil, fmt.Errorf("cannot find search %s", searchID)
}
// Step 3: Check for duplicate name (if name changed)
// Python: if req["name"].lower() != search_app.name.lower() and len(SearchService.query(...)) >= 1
trimmedName := req.Name
if search.Name != trimmedName {
existing, _ := s.searchDAO.GetByNameAndTenant(trimmedName, userID)
if len(existing) > 0 {
return nil, fmt.Errorf("duplicated search name")
}
}
// Step 4: Merge search_config
// Python: req["search_config"] = {**current_config, **new_config}
currentConfig := search.SearchConfig
if currentConfig == nil {
currentConfig = make(entity.JSONMap)
}
mergedConfig := make(entity.JSONMap)
// Copy current config
for k, v := range currentConfig {
mergedConfig[k] = v
}
// Merge new config
for k, v := range req.SearchConfig {
mergedConfig[k] = v
}
// Step 5: Prepare updates (excluding immutable fields)
// Python removes: search_id, tenant_id, created_by, update_time, id
updates := map[string]interface{}{
"name": trimmedName,
"search_config": mergedConfig,
}
if req.Description != nil {
updates["description"] = *req.Description
}
// Step 6: Execute update
// Python: SearchService.update_by_id(search_id, req)
if err = s.searchDAO.UpdateByID(searchID, updates); err != nil {
return nil, fmt.Errorf("failed to update search: %w", err)
}
// Step 7: Fetch updated search
// Python: e, updated_search = SearchService.get_by_id(search_id)
updatedSearch, err := s.searchDAO.GetByID(searchID)
if err != nil {
return nil, fmt.Errorf("failed to fetch updated search: %w", err)
}
return updatedSearch, nil
}