From b75f274885f5c591196aad6122212bd65830f813 Mon Sep 17 00:00:00 2001 From: Grendgi Date: Thu, 18 Jun 2026 11:35:38 +0300 Subject: [PATCH] feat: search files across folders --- internal/handler/node.go | 3 +- internal/repository/node.go | 70 ++++++++++++++++++++++++++++++++++++- 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/internal/handler/node.go b/internal/handler/node.go index dd89a10..e37456a 100644 --- a/internal/handler/node.go +++ b/internal/handler/node.go @@ -36,7 +36,8 @@ func (h *NodeHandler) List(w http.ResponseWriter, r *http.Request) { userID := commonmw.GetUserID(r.Context()) scope := r.URL.Query().Get("scope") parentID := emptyToNil(r.URL.Query().Get("parent_id")) - nodes, err := h.repo.List(r.Context(), userID, subordinates(r), scope, parentID) + query := strings.TrimSpace(r.URL.Query().Get("q")) + nodes, err := h.repo.List(r.Context(), userID, subordinates(r), scope, parentID, query) if err != nil { writeInternalError(w, r, err, "failed to list files") return diff --git a/internal/repository/node.go b/internal/repository/node.go index f157db9..5a6ac66 100644 --- a/internal/repository/node.go +++ b/internal/repository/node.go @@ -39,7 +39,10 @@ func scanNode(scan func(dest ...any) error) (*model.Node, error) { return &n, err } -func (r *NodeRepository) List(ctx context.Context, userID string, subordinateIDs []string, scope string, parentID *string) ([]model.Node, error) { +func (r *NodeRepository) List(ctx context.Context, userID string, subordinateIDs []string, scope string, parentID *string, search string) ([]model.Node, error) { + if strings.TrimSpace(search) != "" { + return r.Search(ctx, userID, subordinateIDs, scope, search) + } args := []any{userID, subordinateIDs} where := []string{"n.deleted_at IS NULL"} if scope == "trash" { @@ -127,6 +130,71 @@ func (r *NodeRepository) List(ctx context.Context, userID string, subordinateIDs return out, rows.Err() } +func (r *NodeRepository) Search(ctx context.Context, userID string, subordinateIDs []string, scope string, search string) ([]model.Node, error) { + pattern := "%" + strings.ToLower(strings.TrimSpace(search)) + "%" + args := []any{userID, subordinateIDs, pattern} + where := []string{`( + lower(n.title) LIKE $3 + OR lower(COALESCE(n.original_filename, '')) LIKE $3 + OR lower(COALESCE(n.extension, '')) LIKE $3 + OR lower(COALESCE(n.mime_type, '')) LIKE $3 + )`} + accessExpr := "effective_node_access(n.id, $1, $2::text[])" + + if scope == "trash" { + where = append(where, "n.deleted_at IS NOT NULL") + where = append(where, `( + n.owner_user_id = $1 + OR n.owner_user_id::text = ANY($2::text[]) + OR EXISTS ( + SELECT 1 FROM files_access a + WHERE a.node_id = n.id + AND a.user_id = $1 + AND a.access_level = 'edit' + ) + )`) + accessExpr = "'edit'" + } else { + where = append(where, "n.deleted_at IS NULL") + switch scope { + case "shared": + where = append(where, "n.owner_user_id <> $1") + where = append(where, `( + has_node_access(n.id, $1) + OR n.owner_user_id::text = ANY($2::text[]) + )`) + default: + where = append(where, "n.owner_user_id = $1") + } + } + + query := ` + SELECT n.id, n.parent_id, n.node_type, n.title, n.owner_user_id, n.owner_department_id, + n.created_by, n.updated_by, n.storage_key, n.original_filename, n.mime_type, + n.extension, n.size_bytes, n.office_format, n.external_url, n.version, + ` + accessExpr + `, + n.created_at, n.updated_at, n.trashed_at, n.purge_after, n.deleted_at + FROM files_nodes n + WHERE ` + strings.Join(where, " AND ") + ` + ORDER BY CASE WHEN n.node_type = 'folder' THEN 0 ELSE 1 END, lower(n.title), n.created_at DESC` + + rows, err := r.pool.Query(ctx, query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + out := make([]model.Node, 0) + for rows.Next() { + n, err := scanNode(rows.Scan) + if err != nil { + return nil, err + } + out = append(out, *n) + } + return out, rows.Err() +} + func (r *NodeRepository) ListChildrenForPublic(ctx context.Context, parentID string) ([]model.Node, error) { rows, err := r.pool.Query(ctx, ` SELECT n.id, n.parent_id, n.node_type, n.title, n.owner_user_id, n.owner_department_id,