Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,4 @@ markers =
websockets: All tests related to handeling web socket connections
import_export_workspace: All tests related to importing and exporting workspaces
data_sync: All tests related to data sync functionality
workspace_search: All tests related to workspace search functionality
1 change: 1 addition & 0 deletions backend/src/baserow/api/search/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DEFAULT_SEARCH_LIMIT = 20
41 changes: 41 additions & 0 deletions backend/src/baserow/api/search/serializers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from rest_framework import serializers

from baserow.api.search.constants import DEFAULT_SEARCH_LIMIT
from baserow.contrib.database.search.handler import ALL_SEARCH_MODES


Expand All @@ -10,3 +11,43 @@ class SearchQueryParamSerializer(serializers.Serializer):
default=None,
choices=ALL_SEARCH_MODES,
)


class WorkspaceSearchSerializer(serializers.Serializer):
"""Serializer for workspace search requests."""

query = serializers.CharField(min_length=1, max_length=100)
limit = serializers.IntegerField(
default=DEFAULT_SEARCH_LIMIT,
min_value=1,
max_value=100,
help_text="Maximum number of results per type",
)
offset = serializers.IntegerField(
default=0, min_value=0, help_text="Number of results to skip"
)


class SearchResultSerializer(serializers.Serializer):
"""Serializer for individual search results."""

type = serializers.CharField()
id = serializers.IntegerField()
title = serializers.CharField()
subtitle = serializers.CharField(required=False, allow_null=True)
description = serializers.CharField(required=False, allow_null=True)
metadata = serializers.DictField(required=False, allow_null=True)
created_on = serializers.CharField(required=False, allow_null=True)
updated_on = serializers.CharField(required=False, allow_null=True)


class WorkspaceSearchResponseSerializer(serializers.Serializer):
"""Serializer for workspace search responses."""

results = serializers.ListField(
child=SearchResultSerializer(),
help_text="Priority-ordered search results",
)
has_more = serializers.BooleanField(
help_text="Whether there are more results available for pagination"
)
17 changes: 17 additions & 0 deletions backend/src/baserow/api/search/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from django.urls import path

from baserow.api.search.views import WorkspaceSearchView
from baserow.core.feature_flags import FF_WORKSPACE_SEARCH, feature_flag_is_enabled

app_name = "baserow.api.search"

urlpatterns = []

if feature_flag_is_enabled(FF_WORKSPACE_SEARCH):
urlpatterns = [
path(
"workspace/<int:workspace_id>/",
WorkspaceSearchView.as_view(),
name="workspace_search",
),
]
74 changes: 74 additions & 0 deletions backend/src/baserow/api/search/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
from drf_spectacular.openapi import OpenApiParameter, OpenApiTypes
from drf_spectacular.utils import extend_schema
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView

from baserow.api.decorators import map_exceptions, validate_query_parameters
from baserow.api.errors import ERROR_GROUP_DOES_NOT_EXIST, ERROR_USER_NOT_IN_GROUP
from baserow.api.schemas import get_error_schema
from baserow.api.search.serializers import (
WorkspaceSearchResponseSerializer,
WorkspaceSearchSerializer,
)
from baserow.core.exceptions import UserNotInWorkspace, WorkspaceDoesNotExist
from baserow.core.handler import CoreHandler
from baserow.core.operations import ReadWorkspaceOperationType
from baserow.core.search.handler import WorkspaceSearchHandler


class WorkspaceSearchView(APIView):
"""
API view for workspace search functionality.
"""

permission_classes = [IsAuthenticated]

@extend_schema(
parameters=[
OpenApiParameter(
name="workspace_id",
location=OpenApiParameter.PATH,
type=OpenApiTypes.INT,
description="Workspace ID to search within",
),
],
request=WorkspaceSearchSerializer,
responses={
200: WorkspaceSearchResponseSerializer,
400: get_error_schema(
["ERROR_USER_NOT_IN_GROUP", "ERROR_INVALID_SEARCH_QUERY"]
),
404: get_error_schema(["ERROR_GROUP_DOES_NOT_EXIST"]),
},
tags=["Search"],
operation_id="workspace_search",
description="Search across all searchable content within a workspace",
)
@map_exceptions(
{
UserNotInWorkspace: ERROR_USER_NOT_IN_GROUP,
WorkspaceDoesNotExist: ERROR_GROUP_DOES_NOT_EXIST,
}
)
@validate_query_parameters(WorkspaceSearchSerializer, return_validated=True)
def get(self, request, workspace_id, query_params):
workspace = CoreHandler().get_workspace(workspace_id)
CoreHandler().check_permissions(
request.user,
ReadWorkspaceOperationType.type,
workspace=workspace,
context=workspace,
)

handler = WorkspaceSearchHandler()
result_data = handler.search_workspace(
user=request.user,
workspace=workspace,
query=query_params["query"],
limit=query_params["limit"],
offset=query_params["offset"],
)
serializer = WorkspaceSearchResponseSerializer(data=result_data)
serializer.is_valid(raise_exception=True)
return Response(serializer.data)
2 changes: 2 additions & 0 deletions backend/src/baserow/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from .jobs import urls as jobs_urls
from .mcp import urls as mcp_urls
from .notifications import urls as notifications_urls
from .search import urls as search_urls
from .settings import urls as settings_urls
from .snapshots import urls as snapshots_urls
from .spectacular.views import CachedSpectacularJSONAPIView
Expand Down Expand Up @@ -50,6 +51,7 @@
path("snapshots/", include(snapshots_urls, namespace="snapshots")),
path("_health/", include(health_urls, namespace="health")),
path("notifications/", include(notifications_urls, namespace="notifications")),
path("search/", include(search_urls, namespace="search")),
path("admin/", include(admin_urls, namespace="admin")),
path("mcp/", include(mcp_urls, namespace="mcp")),
path(
Expand Down
6 changes: 6 additions & 0 deletions backend/src/baserow/contrib/automation/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,3 +213,9 @@ def ready(self):
)

connect_to_node_pre_delete_signal()

from baserow.core.search.registries import workspace_search_registry

from .search_types import AutomationSearchType

workspace_search_registry.register(AutomationSearchType())
17 changes: 11 additions & 6 deletions backend/src/baserow/contrib/automation/formula_importer.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from typing import Dict
from typing import Dict, Union

from baserow.contrib.automation.data_providers.registries import (
automation_data_provider_type_registry,
)
from baserow.core.formula import get_parse_tree_for_formula
from baserow.core.formula import BaserowFormulaObject, get_parse_tree_for_formula
from baserow.core.services.formula_importer import BaserowFormulaImporter


Expand All @@ -18,7 +18,9 @@ def get_data_provider_type_registry(self):
return automation_data_provider_type_registry


def import_formula(formula: str, id_mapping: Dict[str, str], **kwargs) -> str:
def import_formula(
formula: Union[str, BaserowFormulaObject], id_mapping: Dict[str, str], **kwargs
) -> str:
"""
When a formula is used in an automation, it must be migrated when we import it
because it could contain IDs referencing other objects.
Expand All @@ -30,8 +32,11 @@ def import_formula(formula: str, id_mapping: Dict[str, str], **kwargs) -> str:
:return: The updated path.
"""

if not formula:
return formula
# Figure out what our formula string is.
formula_str = formula if isinstance(formula, str) else formula["formula"]

tree = get_parse_tree_for_formula(formula)
if not formula_str:
return formula_str

tree = get_parse_tree_for_formula(formula_str)
return AutomationFormulaImporter(id_mapping, **kwargs).visit(tree)
13 changes: 13 additions & 0 deletions backend/src/baserow/contrib/automation/search_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from baserow.contrib.automation.models import Automation
from baserow.core.search.search_types import ApplicationSearchType


class AutomationSearchType(ApplicationSearchType):
"""
Searchable item type specifically for automations.
"""

type = "automation"
name = "Automation"
model_class = Automation
priority = 4
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ def get_workflow_actions(self, obj: ElementsAndWorkflowActions):

class PageParameterValueSerializer(serializers.Serializer):
name = serializers.CharField()
value = FormulaSerializerField(allow_blank=True)
value = FormulaSerializerField()


@extend_schema_serializer(exclude_fields=("config",))
Expand Down Expand Up @@ -363,7 +363,7 @@ class UpdateCollectionFieldSerializer(serializers.ModelSerializer):
help_text=CollectionField._meta.get_field("type").help_text,
)

value = FormulaSerializerField(allow_blank=True)
value = FormulaSerializerField()


class ChoiceOptionSerializer(serializers.ModelSerializer):
Expand Down Expand Up @@ -408,8 +408,6 @@ class MenuItemSerializer(serializers.ModelSerializer):
)
navigate_to_url = FormulaSerializerField(
help_text=LinkElement._meta.get_field("navigate_to_url").help_text,
default="",
allow_blank=True,
required=False,
)
page_parameters = PageParameterValueSerializer(
Expand Down
5 changes: 5 additions & 0 deletions backend/src/baserow/contrib/builder/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,3 +337,8 @@ def ready(self):
# which need to be filled first.
import baserow.contrib.builder.signals # noqa: F403, F401
import baserow.contrib.builder.ws.signals # noqa: F403, F401
from baserow.core.search.registries import workspace_search_registry

from .search_types import BuilderSearchType

workspace_search_registry.register(BuilderSearchType())
Loading
Loading