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
Original file line number Diff line number Diff line change
Expand Up @@ -246,10 +246,15 @@ def extract_properties(self, path: List[str], **kwargs) -> Dict[str, List[str]]:
service_type = data_source.service.specific.get_type()

if service_type.returns_list:
# We remove the row id from the path
_, *rest = rest
if rest:
# We remove the row id from the path
_, *rest = rest

return {data_source.service_id: service_type.extract_properties(rest, **kwargs)}
return {
data_source.service_id: service_type.extract_properties(
data_source.service.specific, rest, **kwargs
)
}


class DataSourceContextDataProviderType(BuilderDataProviderType):
Expand Down Expand Up @@ -328,7 +333,11 @@ def extract_properties(self, path: List[str], **kwargs) -> Dict[str, List[str]]:

service_type = data_source.service.specific.get_type()

return {data_source.service_id: service_type.extract_properties(rest, **kwargs)}
return {
data_source.service_id: service_type.extract_properties(
data_source.service.specific, rest, **kwargs
)
}


class CurrentRecordDataProviderType(BuilderDataProviderType):
Expand Down Expand Up @@ -445,7 +454,11 @@ def extract_properties(
else:
path = [schema_property, *path]

return {data_source.service_id: service_type.extract_properties(path, **kwargs)}
return {
data_source.service_id: service_type.extract_properties(
data_source.service.specific, path, **kwargs
)
}


class PreviousActionProviderType(BuilderDataProviderType):
Expand Down Expand Up @@ -584,7 +597,9 @@ def extract_properties(

service_type = previous_action.service.specific.get_type()
return {
previous_action.service.id: service_type.extract_properties(rest, **kwargs)
previous_action.service.id: service_type.extract_properties(
previous_action.service.specific, rest, **kwargs
)
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,9 @@ def formula_generator(
query_param.value = new_formula
yield query_param

def extract_properties(self, path: List[str], **kwargs) -> List[str]:
def extract_properties(
self, service: Service, path: List[str], **kwargs
) -> List[str]:
"""Returns the first path element if any"""

if path:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,9 @@ def import_property_name(
return f"field_{new_field_id}" if new_field_id else None
return property_name

def extract_properties(self, path: List[str], **kwargs) -> List[str]:
def extract_properties(
self, service: Service, path: List[str], **kwargs
) -> List[str]:
"""
Given a list of formula path parts, call the ServiceType's
extract_properties() method and return a set of unique field IDs.
Expand All @@ -387,11 +389,11 @@ def extract_properties(self, path: List[str], **kwargs) -> List[str]:
The path can contain one or more parts, depending on the field type
and the formula. Some examples of `path` are:

An element that specifies a specific a field:
['field_5439']
An element that specifies a specific field:
['field_5439']

An element that uses a Link Row Field formula
['field_5569', '0', 'value']
An element that uses a Link Row Field formula:
['field_5569', '0', 'value']
"""

# If the path length is greater or equal to 1, then we have
Expand All @@ -400,6 +402,14 @@ def extract_properties(self, path: List[str], **kwargs) -> List[str]:
if len(path) >= 1:
field_dbname, *rest = path
else:
# When path is empty, e.g. `get('data_source.606')`, we should
# return all fields since we don't know which specific fields
# are needed.
if field_objects := self.get_table_field_objects(service):
return ["id"] + [
field_object["field"].db_column for field_object in field_objects
]

# In any other scenario, we have a formula that is not a format we
# can currently parse properly, so we return an empty list.
return []
Expand Down Expand Up @@ -1536,7 +1546,9 @@ def dispatch_transform(

return DispatchResult(data={"result": result})

def extract_properties(self, path: List[str], **kwargs) -> List[str]:
def extract_properties(
self, service: Service, path: List[str], **kwargs
) -> List[str]:
"""
Returns the usual properties for this service type.
"""
Expand Down
4 changes: 3 additions & 1 deletion backend/src/baserow/core/services/registries.py
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,9 @@ def import_serialized(

return created_instance

def extract_properties(self, path: List[str], **kwargs) -> List[str]:
def extract_properties(
self, service: Service, path: List[str], **kwargs
) -> List[str]:
return []

def import_property_name(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1517,18 +1517,53 @@ def test_data_source_data_extract_properties_calls_correct_service_type(

assert result == {mocked_data_source.service_id: expected}
mocked_get_data_source.assert_called_once_with(int(data_source_id), with_cache=True)
mocked_service_type.extract_properties.assert_called_once_with([expected])
mocked_service_type.extract_properties.assert_called_once_with(
mocked_data_source.service.specific, [expected]
)

mocked_service_type.returns_list = True
mocked_service_type.extract_properties.reset_mock()

result = DataSourceDataProviderType().extract_properties(
[data_source_id, "1", expected]
)
mocked_service_type.extract_properties.assert_called_once_with([expected])
mocked_service_type.extract_properties.assert_called_once_with(
mocked_data_source.service.specific, [expected]
)
assert result == {mocked_data_source.service_id: expected}


@patch.object(DataSourceHandler, "get_data_source")
@pytest.mark.django_db
def test_data_source_data_extract_properties_returns_all_fields_when_no_row_id_or_field_name(
mocked_get_data_source,
):
"""
Test the DataSourceDataProviderType::extract_properties() method.

Ensure that when there is no row ID or field name, all fields are returned.
"""

mocked_service_type = MagicMock()
mocked_service_type.extract_properties.return_value = ["id", "field_123"]
mocked_data_source = MagicMock()
mocked_data_source.service.specific.get_type = MagicMock(
return_value=mocked_service_type
)
mocked_get_data_source.return_value = mocked_data_source

data_source_id = "1"
# mock a path without any row_id or field name
path = [data_source_id]
result = DataSourceDataProviderType().extract_properties(path)

assert result == {mocked_data_source.service_id: ["id", "field_123"]}
mocked_get_data_source.assert_called_once_with(int(data_source_id), with_cache=True)
mocked_service_type.extract_properties.assert_called_once_with(
mocked_data_source.service.specific, []
)


@pytest.mark.django_db
def test_data_source_data_extract_properties_returns_expected_results(data_fixture):
"""
Expand Down Expand Up @@ -1623,7 +1658,9 @@ def test_data_source_context_extract_properties_calls_correct_service_type(

assert result == {mocked_data_source.service_id: expected}
mocked_get_data_source.assert_called_once_with(int(data_source_id), with_cache=True)
mocked_service_type.extract_properties.assert_called_once_with([expected])
mocked_service_type.extract_properties.assert_called_once_with(
mocked_data_source.service.specific, [expected]
)

mocked_service_type.returns_list = True
mocked_service_type.extract_properties.reset_mock()
Expand All @@ -1633,7 +1670,9 @@ def test_data_source_context_extract_properties_calls_correct_service_type(
[data_source_id, expected]
)

mocked_service_type.extract_properties.assert_called_once_with([expected])
mocked_service_type.extract_properties.assert_called_once_with(
mocked_data_source.service.specific, [expected]
)
assert result == {mocked_data_source.service_id: expected}


Expand Down Expand Up @@ -1822,7 +1861,9 @@ def test_current_record_extract_properties_calls_correct_service_type(

assert result == {mocked_data_source.service_id: expected_field}
mock_get_data_source.assert_called_once_with(fake_element_id, with_cache=True)
mocked_service_type.extract_properties.assert_called_once_with([expected_field])
mocked_service_type.extract_properties.assert_called_once_with(
mocked_data_source.service.specific, [expected_field]
)


@pytest.mark.django_db
Expand Down Expand Up @@ -1883,15 +1924,17 @@ def test_current_record_extract_properties_called_with_correct_path(
if returns_list:
if schema_property:
mock_service_type.extract_properties.assert_called_once_with(
[schema_property, *path]
mock_data_source.service.specific, [schema_property, *path]
)
else:
mock_service_type.extract_properties.assert_called_once_with(path)
mock_service_type.extract_properties.assert_called_once_with(
mock_data_source.service.specific, path
)
assert result == {service_id: ["field_999"]}
else:
if schema_property:
mock_service_type.extract_properties.assert_called_once_with(
[schema_property, *path]
mock_data_source.service.specific, [schema_property, *path]
)
assert result == {service_id: ["field_999"]}
else:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import json
from contextlib import contextmanager
from unittest.mock import Mock, patch
from unittest.mock import MagicMock, Mock, patch

import pytest
from requests import exceptions as request_exceptions
Expand Down Expand Up @@ -365,11 +365,13 @@ def test_core_http_request_formula_generator():

@pytest.mark.django_db
def test_core_http_request_extract_properties(data_fixture):
mock_service = MagicMock()

assert CoreHTTPRequestServiceType().extract_properties(
["headers", "content_type"]
mock_service, ["headers", "content_type"]
) == ["headers"]

assert CoreHTTPRequestServiceType().extract_properties([]) == []
assert CoreHTTPRequestServiceType().extract_properties(mock_service, []) == []


@pytest.mark.django_db
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -803,7 +803,7 @@ def test_dispatch_transform_passes_field_ids(mock_get_serializer, field_names):
[
(
[],
[],
["id"],
),
(
["foo"],
Expand Down Expand Up @@ -836,7 +836,8 @@ def test_extract_properties(path, expected):

service_type = LocalBaserowGetRowUserServiceType()

result = service_type.extract_properties(path)
mock_service = MagicMock()
result = service_type.extract_properties(mock_service, path)

assert result == expected

Expand Down Expand Up @@ -918,3 +919,21 @@ def test_can_dispatch_interesting_table(data_fixture):

dispatch_context = FakeDispatchContext(public_allowed_properties=field_names)
assert len(result.data.keys()) == 1 + 1


@pytest.mark.django_db
def test_extract_properties_with_empty_path_returns_all_fields(data_fixture):
"""
When path is empty, extract_properties should return all field names.
"""

user = data_fixture.create_user()
table = data_fixture.create_database_table(user=user)
field_1 = data_fixture.create_text_field(table=table, name="Fruit")
field_2 = data_fixture.create_number_field(table=table, name="Count")
service = data_fixture.create_local_baserow_get_row_service(table=table)
service_type = LocalBaserowGetRowUserServiceType()

result = service_type.extract_properties(service, [])

assert result == ["id", field_1.db_column, field_2.db_column]
Original file line number Diff line number Diff line change
Expand Up @@ -1156,7 +1156,7 @@ def test_dispatch_transform_passes_field_ids(mock_get_serializer, field_names):
[
(
[],
[],
["id"],
),
(
["foo"],
Expand Down Expand Up @@ -1187,7 +1187,8 @@ def test_extract_properties(path, expected):

service_type = LocalBaserowListRowsUserServiceType()

result = service_type.extract_properties(path)
mock_service = MagicMock()
result = service_type.extract_properties(mock_service, path)

assert result == expected

Expand Down Expand Up @@ -1250,3 +1251,21 @@ def test_search_on_multiple_select_with_list(data_fixture):
)
assert len(dispatch_data["results"]) == 1
assert dispatch_data["results"][0]._primary_field_id == options[0].field_id


@pytest.mark.django_db
def test_extract_properties_with_empty_path_returns_all_fields(data_fixture):
"""
When path is empty, extract_properties should return all field names.
"""

user = data_fixture.create_user()
table = data_fixture.create_database_table(user=user)
field_1 = data_fixture.create_text_field(table=table, name="Fruit")
field_2 = data_fixture.create_number_field(table=table, name="Count")
service = data_fixture.create_local_baserow_list_rows_service(table=table)
service_type = LocalBaserowListRowsUserServiceType()

result = service_type.extract_properties(service, [])

assert result == ["id", field_1.db_column, field_2.db_column]
Original file line number Diff line number Diff line change
Expand Up @@ -809,7 +809,7 @@ def test_dispatch_transform_passes_field_ids(
@pytest.mark.parametrize(
"path,expected",
[
([], []),
([], ["id"]),
([None], []),
([""], []),
(["foo"], []),
Expand All @@ -830,7 +830,8 @@ def test_extract_properties_returns_expected_list(path, expected):

service_type = LocalBaserowUpsertRowServiceType()

result = service_type.extract_properties(path)
mock_service = MagicMock()
result = service_type.extract_properties(mock_service, path)

assert result == expected

Expand Down
4 changes: 3 additions & 1 deletion backend/tests/baserow/core/service/test_service_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,9 @@ def test_extract_properties():
service_type_cls.model_class = MagicMock()
service_type = service_type_cls()

result = service_type.extract_properties(["foo"])
mock_service = MagicMock()
result = service_type.extract_properties(mock_service, ["foo"])

assert result == []


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"type": "bug",
"message": "Fixed a bug where a broken List Rows data source could cause a crash.",
"issue_origin": "github",
"issue_number": null,
"domain": "builder",
"bullet_points": [],
"created_at": "2026-03-25"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"type": "bug",
"message": "Fixed the Slack Bot Form's translations to use the correct placeholders.",
"issue_origin": "github",
"issue_number": null,
"domain": "integration",
"bullet_points": [],
"created_at": "2026-03-26"
}
Loading
Loading