1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-14 17:18:33 +00:00

Prevent triggering webhook if query param is provided on row create, update, or delete endpoints

This commit is contained in:
Przemyslaw Kukulski 2024-11-19 19:33:32 +00:00 committed by Bram Wiepjes
parent 27e24b7a8b
commit 851826141e
13 changed files with 507 additions and 10 deletions
backend
src/baserow/contrib/database
tests/baserow/contrib/database/api
changelog/entries/unreleased/feature
web-frontend

View file

@ -54,6 +54,7 @@ from baserow.contrib.database.api.tokens.authentications import TokenAuthenticat
from baserow.contrib.database.api.tokens.errors import ERROR_NO_PERMISSION_TO_TABLE from baserow.contrib.database.api.tokens.errors import ERROR_NO_PERMISSION_TO_TABLE
from baserow.contrib.database.api.utils import ( from baserow.contrib.database.api.utils import (
extract_link_row_joins_from_request, extract_link_row_joins_from_request,
extract_send_webhook_events_from_params,
extract_user_field_names_from_params, extract_user_field_names_from_params,
get_include_exclude_fields, get_include_exclude_fields,
) )
@ -445,6 +446,16 @@ class RowsView(APIView):
"field names (e.g., field_123)." "field names (e.g., field_123)."
), ),
), ),
OpenApiParameter(
name="send_webhook_events",
location=OpenApiParameter.QUERY,
type=OpenApiTypes.BOOL,
description=(
"A flag query parameter that triggers webhooks after the operation,"
" if set to `y`, `yes`, `true`, `t`, `on`, `1`, `or` left empty. "
"Defaults to `true`"
),
),
CLIENT_SESSION_ID_SCHEMA_PARAMETER, CLIENT_SESSION_ID_SCHEMA_PARAMETER,
CLIENT_UNDO_REDO_ACTION_GROUP_ID_SCHEMA_PARAMETER, CLIENT_UNDO_REDO_ACTION_GROUP_ID_SCHEMA_PARAMETER,
], ],
@ -514,6 +525,7 @@ class RowsView(APIView):
) )
user_field_names = extract_user_field_names_from_params(request.GET) user_field_names = extract_user_field_names_from_params(request.GET)
send_webhook_events = extract_send_webhook_events_from_params(request.GET)
model = table.get_model() model = table.get_model()
@ -537,6 +549,7 @@ class RowsView(APIView):
model=model, model=model,
before_row=before_row, before_row=before_row,
user_field_names=user_field_names, user_field_names=user_field_names,
send_webhook_events=send_webhook_events,
) )
except ValidationError as e: except ValidationError as e:
raise RequestBodyValidationException(detail=e.message) raise RequestBodyValidationException(detail=e.message)
@ -771,6 +784,16 @@ class RowView(APIView):
"field names (e.g., field_123)." "field names (e.g., field_123)."
), ),
), ),
OpenApiParameter(
name="send_webhook_events",
location=OpenApiParameter.QUERY,
type=OpenApiTypes.BOOL,
description=(
"A flag query parameter that triggers webhooks after the operation,"
" if set to `y`, `yes`, `true`, `t`, `on`, `1`, `or` left empty. "
"Defaults to `true`"
),
),
CLIENT_SESSION_ID_SCHEMA_PARAMETER, CLIENT_SESSION_ID_SCHEMA_PARAMETER,
CLIENT_UNDO_REDO_ACTION_GROUP_ID_SCHEMA_PARAMETER, CLIENT_UNDO_REDO_ACTION_GROUP_ID_SCHEMA_PARAMETER,
], ],
@ -833,6 +856,7 @@ class RowView(APIView):
TokenHandler().check_table_permissions(request, "update", table, False) TokenHandler().check_table_permissions(request, "update", table, False)
user_field_names = extract_user_field_names_from_params(request.GET) user_field_names = extract_user_field_names_from_params(request.GET)
send_webhook_events = extract_send_webhook_events_from_params(request.GET)
field_ids, field_names = None, None field_ids, field_names = None, None
if user_field_names: if user_field_names:
@ -852,7 +876,11 @@ class RowView(APIView):
try: try:
data["id"] = int(row_id) data["id"] = int(row_id)
row = action_type_registry.get_by_type(UpdateRowsActionType).do( row = action_type_registry.get_by_type(UpdateRowsActionType).do(
request.user, table, [data], model request.user,
table,
[data],
model=model,
send_webhook_events=send_webhook_events,
)[0] )[0]
except ValidationError as exc: except ValidationError as exc:
raise RequestBodyValidationException(detail=exc.message) from exc raise RequestBodyValidationException(detail=exc.message) from exc
@ -877,6 +905,16 @@ class RowView(APIView):
type=OpenApiTypes.INT, type=OpenApiTypes.INT,
description="Deletes the row related to the value.", description="Deletes the row related to the value.",
), ),
OpenApiParameter(
name="send_webhook_events",
location=OpenApiParameter.QUERY,
type=OpenApiTypes.BOOL,
description=(
"A flag query parameter that triggers webhooks after the operation,"
" if set to `y`, `yes`, `true`, `t`, `on`, `1`, `or` left empty. "
"Defaults to `true`"
),
),
CLIENT_SESSION_ID_SCHEMA_PARAMETER, CLIENT_SESSION_ID_SCHEMA_PARAMETER,
CLIENT_UNDO_REDO_ACTION_GROUP_ID_SCHEMA_PARAMETER, CLIENT_UNDO_REDO_ACTION_GROUP_ID_SCHEMA_PARAMETER,
], ],
@ -913,11 +951,13 @@ class RowView(APIView):
table_id. table_id.
""" """
send_webhook_events = extract_send_webhook_events_from_params(request.GET)
table = TableHandler().get_table(table_id) table = TableHandler().get_table(table_id)
TokenHandler().check_table_permissions(request, "delete", table, False) TokenHandler().check_table_permissions(request, "delete", table, False)
action_type_registry.get_by_type(DeleteRowActionType).do( action_type_registry.get_by_type(DeleteRowActionType).do(
request.user, table, row_id request.user, table, row_id, send_webhook_events=send_webhook_events
) )
return Response(status=204) return Response(status=204)
@ -961,6 +1001,16 @@ class RowMoveView(APIView):
"field names (e.g., field_123)." "field names (e.g., field_123)."
), ),
), ),
OpenApiParameter(
name="send_webhook_events",
location=OpenApiParameter.QUERY,
type=OpenApiTypes.BOOL,
description=(
"A flag query parameter that triggers webhooks after the operation,"
" if set to `y`, `yes`, `true`, `t`, `on`, `1`, `or` left empty. "
"Defaults to `true`"
),
),
CLIENT_SESSION_ID_SCHEMA_PARAMETER, CLIENT_SESSION_ID_SCHEMA_PARAMETER,
CLIENT_UNDO_REDO_ACTION_GROUP_ID_SCHEMA_PARAMETER, CLIENT_UNDO_REDO_ACTION_GROUP_ID_SCHEMA_PARAMETER,
], ],
@ -1001,6 +1051,7 @@ class RowMoveView(APIView):
TokenHandler().check_table_permissions(request, "update", table, False) TokenHandler().check_table_permissions(request, "update", table, False)
user_field_names = extract_user_field_names_from_params(request.GET) user_field_names = extract_user_field_names_from_params(request.GET)
send_webhook_events = extract_send_webhook_events_from_params(request.GET)
model = table.get_model() model = table.get_model()
@ -1014,7 +1065,12 @@ class RowMoveView(APIView):
) )
row = action_type_registry.get_by_type(MoveRowActionType).do( row = action_type_registry.get_by_type(MoveRowActionType).do(
request.user, table, row_id, before_row=before_row, model=model request.user,
table,
row_id,
before_row=before_row,
model=model,
send_webhook_events=send_webhook_events,
) )
serializer_class = get_row_serializer_class( serializer_class = get_row_serializer_class(
@ -1055,6 +1111,16 @@ class BatchRowsView(APIView):
"field names (e.g., field_123)." "field names (e.g., field_123)."
), ),
), ),
OpenApiParameter(
name="send_webhook_events",
location=OpenApiParameter.QUERY,
type=OpenApiTypes.BOOL,
description=(
"A flag query parameter that triggers webhooks after the operation,"
" if set to `y`, `yes`, `true`, `t`, `on`, `1`, `or` left empty. "
"Defaults to `true`"
),
),
CLIENT_SESSION_ID_SCHEMA_PARAMETER, CLIENT_SESSION_ID_SCHEMA_PARAMETER,
CLIENT_UNDO_REDO_ACTION_GROUP_ID_SCHEMA_PARAMETER, CLIENT_UNDO_REDO_ACTION_GROUP_ID_SCHEMA_PARAMETER,
], ],
@ -1120,6 +1186,7 @@ class BatchRowsView(APIView):
model = table.get_model() model = table.get_model()
user_field_names = extract_user_field_names_from_params(request.GET) user_field_names = extract_user_field_names_from_params(request.GET)
send_webhook_events = extract_send_webhook_events_from_params(request.GET)
before_id = query_params.get("before") before_id = query_params.get("before")
before_row = ( before_row = (
RowHandler().get_row(request.user, table, before_id, model) RowHandler().get_row(request.user, table, before_id, model)
@ -1139,7 +1206,12 @@ class BatchRowsView(APIView):
try: try:
rows = action_type_registry.get_by_type(CreateRowsActionType).do( rows = action_type_registry.get_by_type(CreateRowsActionType).do(
request.user, table, data["items"], before_row, model request.user,
table,
data["items"],
before_row,
model=model,
send_webhook_events=send_webhook_events,
) )
except ValidationError as exc: except ValidationError as exc:
raise RequestBodyValidationException(detail=exc.message) raise RequestBodyValidationException(detail=exc.message)
@ -1173,6 +1245,16 @@ class BatchRowsView(APIView):
"field names (e.g., field_123)." "field names (e.g., field_123)."
), ),
), ),
OpenApiParameter(
name="send_webhook_events",
location=OpenApiParameter.QUERY,
type=OpenApiTypes.BOOL,
description=(
"A flag query parameter that triggers webhooks after the operation,"
" if set to `y`, `yes`, `true`, `t`, `on`, `1`, `or` left empty. "
"Defaults to `true`"
),
),
CLIENT_SESSION_ID_SCHEMA_PARAMETER, CLIENT_SESSION_ID_SCHEMA_PARAMETER,
CLIENT_UNDO_REDO_ACTION_GROUP_ID_SCHEMA_PARAMETER, CLIENT_UNDO_REDO_ACTION_GROUP_ID_SCHEMA_PARAMETER,
], ],
@ -1237,6 +1319,7 @@ class BatchRowsView(APIView):
model = table.get_model() model = table.get_model()
user_field_names = extract_user_field_names_from_params(request.GET) user_field_names = extract_user_field_names_from_params(request.GET)
send_webhook_events = extract_send_webhook_events_from_params(request.GET)
row_validation_serializer = get_row_serializer_class( row_validation_serializer = get_row_serializer_class(
model, model,
@ -1253,7 +1336,11 @@ class BatchRowsView(APIView):
try: try:
rows = action_type_registry.get_by_type(UpdateRowsActionType).do( rows = action_type_registry.get_by_type(UpdateRowsActionType).do(
request.user, table, data["items"], model request.user,
table,
data["items"],
model=model,
send_webhook_events=send_webhook_events,
) )
except ValidationError as e: except ValidationError as e:
raise RequestBodyValidationException(detail=e.message) raise RequestBodyValidationException(detail=e.message)
@ -1280,6 +1367,16 @@ class BatchDeleteRowsView(APIView):
type=OpenApiTypes.INT, type=OpenApiTypes.INT,
description="Deletes the rows in the table related to the value.", description="Deletes the rows in the table related to the value.",
), ),
OpenApiParameter(
name="send_webhook_events",
location=OpenApiParameter.QUERY,
type=OpenApiTypes.BOOL,
description=(
"A flag query parameter that triggers webhooks after the operation,"
" if set to `y`, `yes`, `true`, `t`, `on`, `1`, `or` left empty. "
"Defaults to `true`"
),
),
CLIENT_SESSION_ID_SCHEMA_PARAMETER, CLIENT_SESSION_ID_SCHEMA_PARAMETER,
CLIENT_UNDO_REDO_ACTION_GROUP_ID_SCHEMA_PARAMETER, CLIENT_UNDO_REDO_ACTION_GROUP_ID_SCHEMA_PARAMETER,
], ],
@ -1327,10 +1424,13 @@ class BatchDeleteRowsView(APIView):
table = TableHandler().get_table(table_id) table = TableHandler().get_table(table_id)
TokenHandler().check_table_permissions(request, "delete", table, False) TokenHandler().check_table_permissions(request, "delete", table, False)
send_webhook_events = extract_send_webhook_events_from_params(request.GET)
action_type_registry.get_by_type(DeleteRowsActionType).do( action_type_registry.get_by_type(DeleteRowsActionType).do(
request.user, request.user,
table, table,
row_ids=data["items"], row_ids=data["items"],
send_webhook_events=send_webhook_events,
) )
return Response(status=204) return Response(status=204)

View file

@ -174,6 +174,20 @@ def extract_user_field_names_from_params(query_params):
return str_to_bool(value) return str_to_bool(value)
def extract_send_webhook_events_from_params(query_params) -> bool:
"""
Extracts the send_webhook_events parameter from the query_params and returns
boolean value. Defaults to true if not provided or empty.
"""
value = query_params.get("send_webhook_events")
if value is None or value == "":
return True
return str_to_bool(value)
@dataclass @dataclass
class LinkedTargetField: class LinkedTargetField:
field_id: int field_id: int

View file

@ -58,6 +58,7 @@ class CreateRowActionType(UndoableActionType):
model: Optional[Type[GeneratedTableModel]] = None, model: Optional[Type[GeneratedTableModel]] = None,
before_row: Optional[GeneratedTableModel] = None, before_row: Optional[GeneratedTableModel] = None,
user_field_names: bool = False, user_field_names: bool = False,
send_webhook_events: bool = True,
) -> GeneratedTableModel: ) -> GeneratedTableModel:
""" """
Creates a new row for a given table with the provided values if the user Creates a new row for a given table with the provided values if the user
@ -76,6 +77,8 @@ class CreateRowActionType(UndoableActionType):
instance. instance.
:param user_field_names: Whether or not the values are keyed by the internal :param user_field_names: Whether or not the values are keyed by the internal
Baserow field name (field_1,field_2 etc) or by the user field names. Baserow field name (field_1,field_2 etc) or by the user field names.
:param send_webhook_events: If set the false then the webhooks will not be
triggered. Defaults to true.
:return: The created row instance. :return: The created row instance.
""" """
@ -91,6 +94,7 @@ class CreateRowActionType(UndoableActionType):
model=model, model=model,
before_row=before_row, before_row=before_row,
user_field_names=user_field_names, user_field_names=user_field_names,
send_webhook_events=send_webhook_events,
) )
workspace = table.database.workspace workspace = table.database.workspace
@ -148,6 +152,7 @@ class CreateRowsActionType(UndoableActionType):
rows_values: List[Dict[str, Any]], rows_values: List[Dict[str, Any]],
before_row: Optional[GeneratedTableModel] = None, before_row: Optional[GeneratedTableModel] = None,
model: Optional[Type[GeneratedTableModel]] = None, model: Optional[Type[GeneratedTableModel]] = None,
send_webhook_events: bool = True,
) -> List[GeneratedTableModel]: ) -> List[GeneratedTableModel]:
""" """
Creates rows for a given table with the provided values if the user Creates rows for a given table with the provided values if the user
@ -163,6 +168,8 @@ class CreateRowsActionType(UndoableActionType):
the row with this id. the row with this id.
:param model: If the correct model has already been generated it can be :param model: If the correct model has already been generated it can be
provided so that it does not have to be generated for a second time. provided so that it does not have to be generated for a second time.
:param send_webhook_events: If set the false then the webhooks will not be
triggered. Defaults to true.
:return: The created list of rows instances. :return: The created list of rows instances.
""" """
@ -177,6 +184,7 @@ class CreateRowsActionType(UndoableActionType):
rows_values, rows_values,
before_row=before_row, before_row=before_row,
model=model, model=model,
send_webhook_events=send_webhook_events,
) )
workspace = table.database.workspace workspace = table.database.workspace
@ -327,6 +335,7 @@ class DeleteRowActionType(UndoableActionType):
table: Table, table: Table,
row_id: int, row_id: int,
model: Optional[Type[GeneratedTableModel]] = None, model: Optional[Type[GeneratedTableModel]] = None,
send_webhook_events: bool = True,
): ):
""" """
Deletes an existing row of the given table and with row_id. Deletes an existing row of the given table and with row_id.
@ -339,6 +348,8 @@ class DeleteRowActionType(UndoableActionType):
:param row_id: The id of the row that must be deleted. :param row_id: The id of the row that must be deleted.
:param model: If the correct model has already been generated, it can be :param model: If the correct model has already been generated, it can be
provided so that it does not have to be generated for a second time. provided so that it does not have to be generated for a second time.
:param send_webhook_events: If set the false then the webhooks will not be
triggered. Defaults to true.
:raises RowDoesNotExist: When the row with the provided id does not exist. :raises RowDoesNotExist: When the row with the provided id does not exist.
""" """
@ -347,7 +358,9 @@ class DeleteRowActionType(UndoableActionType):
"Can't delete rows because it has a data sync." "Can't delete rows because it has a data sync."
) )
RowHandler().delete_row_by_id(user, table, row_id, model=model) RowHandler().delete_row_by_id(
user, table, row_id, model=model, send_webhook_events=send_webhook_events
)
database = table.database database = table.database
params = cls.Params(table.id, table.name, database.id, database.name, row_id) params = cls.Params(table.id, table.name, database.id, database.name, row_id)
@ -399,6 +412,7 @@ class DeleteRowsActionType(UndoableActionType):
table: Table, table: Table,
row_ids: List[int], row_ids: List[int],
model: Optional[Type[GeneratedTableModel]] = None, model: Optional[Type[GeneratedTableModel]] = None,
send_webhook_events: bool = True,
): ):
""" """
Deletes rows of the given table with the given row_ids. Deletes rows of the given table with the given row_ids.
@ -411,6 +425,8 @@ class DeleteRowsActionType(UndoableActionType):
:param row_ids: The id of the row that must be deleted. :param row_ids: The id of the row that must be deleted.
:param model: If the correct model has already been generated, it can be :param model: If the correct model has already been generated, it can be
provided so that it does not have to be generated for a second time. provided so that it does not have to be generated for a second time.
:param send_webhook_events: If set the false then the webhooks will not be
triggered. Defaults to true.
:raises RowDoesNotExist: When the row with the provided id does not exist. :raises RowDoesNotExist: When the row with the provided id does not exist.
""" """
@ -419,7 +435,9 @@ class DeleteRowsActionType(UndoableActionType):
"Can't delete rows because it has a data sync." "Can't delete rows because it has a data sync."
) )
trashed_rows_entry = RowHandler().delete_rows(user, table, row_ids, model=model) trashed_rows_entry = RowHandler().delete_rows(
user, table, row_ids, model=model, send_webhook_events=send_webhook_events
)
workspace = table.database.workspace workspace = table.database.workspace
params = cls.Params( params = cls.Params(
@ -548,6 +566,7 @@ class MoveRowActionType(UndoableActionType):
row_id: int, row_id: int,
before_row: Optional[GeneratedTableModel] = None, before_row: Optional[GeneratedTableModel] = None,
model: Optional[Type[GeneratedTableModel]] = None, model: Optional[Type[GeneratedTableModel]] = None,
send_webhook_events: bool = True,
) -> GeneratedTableModelForUpdate: ) -> GeneratedTableModelForUpdate:
""" """
Moves the row before another row or to the end if no before row is provided. Moves the row before another row or to the end if no before row is provided.
@ -566,6 +585,8 @@ class MoveRowActionType(UndoableActionType):
instance. Otherwise the row will be moved to the end. instance. Otherwise the row will be moved to the end.
:param model: If the correct model has already been generated, it can be :param model: If the correct model has already been generated, it can be
provided so that it does not have to be generated for a second time. provided so that it does not have to be generated for a second time.
:param send_webhook_events: If set the false then the webhooks will not be
triggered. Defaults to true.
""" """
if model is None: if model is None:
@ -577,7 +598,12 @@ class MoveRowActionType(UndoableActionType):
original_row_order = row.order original_row_order = row.order
updated_row = row_handler.move_row( updated_row = row_handler.move_row(
user, table, row, before_row=before_row, model=model user,
table,
row,
before_row=before_row,
model=model,
send_webhook_events=send_webhook_events,
) )
rows_displacement = get_rows_displacement( rows_displacement = get_rows_displacement(
@ -762,6 +788,7 @@ class UpdateRowsActionType(UndoableActionType):
table: Table, table: Table,
rows_values: List[Dict[str, Any]], rows_values: List[Dict[str, Any]],
model: Optional[Type[GeneratedTableModel]] = None, model: Optional[Type[GeneratedTableModel]] = None,
send_webhook_events: bool = True,
) -> List[GeneratedTableModelForUpdate]: ) -> List[GeneratedTableModelForUpdate]:
""" """
Updates field values in batch based on provided rows with the new values. Updates field values in batch based on provided rows with the new values.
@ -776,6 +803,8 @@ class UpdateRowsActionType(UndoableActionType):
field ids plus the id of the row. field ids plus the id of the row.
:param model: If the correct model has already been generated it can be :param model: If the correct model has already been generated it can be
provided so that it does not have to be generated for a second time. provided so that it does not have to be generated for a second time.
:param send_webhook_events: If set the false then the webhooks will not be
triggered. Defaults to true.
:return: The updated rows. :return: The updated rows.
""" """
@ -786,6 +815,7 @@ class UpdateRowsActionType(UndoableActionType):
table, table,
rows_values, rows_values,
model=model, model=model,
send_webhook_events=send_webhook_events,
) )
updated_rows = result.updated_rows updated_rows = result.updated_rows

View file

@ -667,6 +667,7 @@ class RowHandler(metaclass=baserow_trace_methods(tracer)):
before_row: Optional[GeneratedTableModel] = None, before_row: Optional[GeneratedTableModel] = None,
user_field_names: bool = False, user_field_names: bool = False,
values_already_prepared: bool = False, values_already_prepared: bool = False,
send_webhook_events: bool = True,
) -> GeneratedTableModel: ) -> GeneratedTableModel:
""" """
Creates a new row for a given table with the provided values if the user Creates a new row for a given table with the provided values if the user
@ -685,6 +686,8 @@ class RowHandler(metaclass=baserow_trace_methods(tracer)):
:param values_already_prepared: Whether or not the values are already sanitized :param values_already_prepared: Whether or not the values are already sanitized
and validated for every field and can be used directly by the handler and validated for every field and can be used directly by the handler
without any further check. without any further check.
:param send_webhook_events: If set the false then the webhooks will not be
triggered. Defaults to true.
:return: The created row instance. :return: The created row instance.
""" """
@ -706,6 +709,7 @@ class RowHandler(metaclass=baserow_trace_methods(tracer)):
before_row, before_row,
user_field_names, user_field_names,
values_already_prepared=values_already_prepared, values_already_prepared=values_already_prepared,
send_webhook_events=send_webhook_events,
) )
def force_create_row( def force_create_row(
@ -717,6 +721,7 @@ class RowHandler(metaclass=baserow_trace_methods(tracer)):
before: Optional[GeneratedTableModel] = None, before: Optional[GeneratedTableModel] = None,
user_field_names: bool = False, user_field_names: bool = False,
values_already_prepared: bool = False, values_already_prepared: bool = False,
send_webhook_events: bool = True,
): ):
""" """
Creates a new row for a given table with the provided values. Creates a new row for a given table with the provided values.
@ -735,6 +740,8 @@ class RowHandler(metaclass=baserow_trace_methods(tracer)):
:param values_already_prepared: Whether or not the values are already sanitized :param values_already_prepared: Whether or not the values are already sanitized
and validated for every field and can be used directly by the handler and validated for every field and can be used directly by the handler
without any further check. without any further check.
:param send_webhook_events: If set the false then the webhooks will not be
triggered. Defaults to true.
:return: The created row instance. :return: The created row instance.
:rtype: Model :rtype: Model
""" """
@ -806,7 +813,7 @@ class RowHandler(metaclass=baserow_trace_methods(tracer)):
table=table, table=table,
model=model, model=model,
send_realtime_update=True, send_realtime_update=True,
send_webhook_events=True, send_webhook_events=send_webhook_events,
rows_values_refreshed_from_db=False, rows_values_refreshed_from_db=False,
m2m_change_tracker=m2m_change_tracker, m2m_change_tracker=m2m_change_tracker,
) )
@ -2028,6 +2035,7 @@ class RowHandler(metaclass=baserow_trace_methods(tracer)):
row: GeneratedTableModelForUpdate, row: GeneratedTableModelForUpdate,
before_row: Optional[GeneratedTableModel] = None, before_row: Optional[GeneratedTableModel] = None,
model: Optional[Type[GeneratedTableModel]] = None, model: Optional[Type[GeneratedTableModel]] = None,
send_webhook_events: bool = True,
) -> GeneratedTableModelForUpdate: ) -> GeneratedTableModelForUpdate:
""" """
Updates the row order value. Updates the row order value.
@ -2039,6 +2047,8 @@ class RowHandler(metaclass=baserow_trace_methods(tracer)):
instance. Otherwise the row will be moved to the end. instance. Otherwise the row will be moved to the end.
:param model: If the correct model has already been generated, it can be :param model: If the correct model has already been generated, it can be
provided so that it does not have to be generated for a second time. provided so that it does not have to be generated for a second time.
:param send_webhook_events: If set the false then the webhooks will not be
triggered. Defaults to true.
""" """
workspace = table.database.workspace workspace = table.database.workspace
@ -2090,6 +2100,7 @@ class RowHandler(metaclass=baserow_trace_methods(tracer)):
before_return=before_return, before_return=before_return,
updated_field_ids=[], updated_field_ids=[],
prepared_rows_values=None, prepared_rows_values=None,
send_webhook_events=send_webhook_events,
) )
return row return row

View file

@ -1,4 +1,5 @@
from decimal import Decimal from decimal import Decimal
from unittest.mock import patch
from django.conf import settings from django.conf import settings
from django.db import connection from django.db import connection
@ -289,6 +290,57 @@ def test_batch_create_rows(api_client, data_fixture):
assert row_2.needs_background_update assert row_2.needs_background_update
@pytest.mark.django_db(transaction=True)
@pytest.mark.api_rows
def test_batch_create_rows_with_disabled_webhook_events(api_client, data_fixture):
user, jwt_token = data_fixture.create_user_and_token()
table = data_fixture.create_database_table(user=user)
text_field = data_fixture.create_text_field(
table=table, order=0, name="Color", text_default="white"
)
number_field = data_fixture.create_number_field(
table=table, order=1, name="Horsepower"
)
boolean_field = data_fixture.create_boolean_field(
table=table, order=2, name="For sale"
)
data_fixture.create_table_webhook(
table=table,
user=user,
request_method="POST",
url="http://localhost",
events=[],
)
url = reverse("api:database:rows:batch", kwargs={"table_id": table.id})
request_body = {
"items": [
{
f"field_{text_field.id}": "green",
f"field_{number_field.id}": 120,
f"field_{boolean_field.id}": True,
},
{
f"field_{text_field.id}": "yellow",
f"field_{number_field.id}": 240,
f"field_{boolean_field.id}": False,
},
]
}
with patch("baserow.contrib.database.webhooks.registries.call_webhook.delay") as m:
response = api_client.post(
f"{url}?send_webhook_events=false",
request_body,
format="json",
HTTP_AUTHORIZATION=f"JWT {jwt_token}",
)
assert response.status_code == HTTP_200_OK
m.assert_not_called()
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.api_rows @pytest.mark.api_rows
def test_batch_create_rows_id_field_ignored(api_client, data_fixture): def test_batch_create_rows_id_field_ignored(api_client, data_fixture):
@ -1184,6 +1236,62 @@ def test_batch_update_rows(api_client, data_fixture):
assert row_2.needs_background_update assert row_2.needs_background_update
@pytest.mark.django_db(transaction=True)
@pytest.mark.api_rows
def test_batch_update_rows_with_disabled_webhook_events(api_client, data_fixture):
user, jwt_token = data_fixture.create_user_and_token()
table = data_fixture.create_database_table(user=user)
text_field = data_fixture.create_text_field(
table=table, order=0, name="Color", text_default="white"
)
number_field = data_fixture.create_number_field(
table=table, order=1, name="Horsepower"
)
boolean_field = data_fixture.create_boolean_field(
table=table, order=2, name="For sale"
)
model = table.get_model()
row_1 = model.objects.create()
row_2 = model.objects.create()
model.objects.update(needs_background_update=False)
data_fixture.create_table_webhook(
table=table,
user=user,
request_method="POST",
url="http://localhost",
events=[],
)
url = reverse("api:database:rows:batch", kwargs={"table_id": table.id})
request_body = {
"items": [
{
f"id": row_1.id,
f"field_{text_field.id}": "green",
f"field_{number_field.id}": 120,
f"field_{boolean_field.id}": True,
},
{
f"id": row_2.id,
f"field_{text_field.id}": "yellow",
f"field_{number_field.id}": 240,
f"field_{boolean_field.id}": False,
},
]
}
with patch("baserow.contrib.database.webhooks.registries.call_webhook.delay") as m:
response = api_client.patch(
f"{url}?send_webhook_events=false",
request_body,
format="json",
HTTP_AUTHORIZATION=f"JWT {jwt_token}",
)
assert response.status_code == HTTP_200_OK
m.assert_not_called()
@pytest.mark.django_db @pytest.mark.django_db
@pytest.mark.api_rows @pytest.mark.api_rows
def test_batch_update_rows_last_modified_field(api_client, data_fixture): def test_batch_update_rows_last_modified_field(api_client, data_fixture):
@ -2275,3 +2383,34 @@ def test_batch_delete_rows_num_of_queries(api_client, data_fixture):
assert len(delete_one_row_ctx.captured_queries) == len( assert len(delete_one_row_ctx.captured_queries) == len(
delete_multiple_rows_ctx.captured_queries delete_multiple_rows_ctx.captured_queries
) )
@pytest.mark.django_db(transaction=True)
@pytest.mark.api_rows
def test_batch_delete_rows_disabled_webhook_events(api_client, data_fixture):
user, jwt_token = data_fixture.create_user_and_token()
table = data_fixture.create_database_table(user=user)
model = table.get_model()
row_1 = model.objects.create()
row_2 = model.objects.create()
model.objects.create()
url = reverse("api:database:rows:batch-delete", kwargs={"table_id": table.id})
request_body = {"items": [row_1.id, row_2.id]}
data_fixture.create_table_webhook(
table=table,
user=user,
request_method="POST",
url="http://localhost",
events=[],
)
with patch("baserow.contrib.database.webhooks.registries.call_webhook.delay") as m:
response = api_client.post(
f"{url}?send_webhook_events=false",
request_body,
format="json",
HTTP_AUTHORIZATION=f"JWT {jwt_token}",
)
assert response.status_code == HTTP_204_NO_CONTENT
m.assert_not_called()

View file

@ -1,6 +1,7 @@
import json import json
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from decimal import Decimal from decimal import Decimal
from unittest.mock import patch
from urllib.parse import quote from urllib.parse import quote
from django.db import connection from django.db import connection
@ -1840,6 +1841,35 @@ def test_create_row(api_client, data_fixture):
} }
@pytest.mark.django_db(transaction=True)
def test_create_row_with_disabled_webhook_events(api_client, data_fixture):
user, token = data_fixture.create_user_and_token()
table = data_fixture.create_database_table(user=user)
text_field = data_fixture.create_text_field(
table=table, order=0, name="Color", text_default="white"
)
data_fixture.create_table_webhook(
table=table,
user=user,
request_method="POST",
url="http://localhost",
events=[],
)
url = reverse("api:database:rows:list", kwargs={"table_id": table.id})
with patch("baserow.contrib.database.webhooks.registries.call_webhook.delay") as m:
response = api_client.post(
f"{url}?send_webhook_events=false",
{f"field_{text_field.id}": "Test 1"},
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
assert response.status_code == HTTP_200_OK
m.assert_not_called()
@pytest.mark.django_db @pytest.mark.django_db
def test_create_row_with_read_only_field(api_client, data_fixture): def test_create_row_with_read_only_field(api_client, data_fixture):
user, jwt_token = data_fixture.create_user_and_token() user, jwt_token = data_fixture.create_user_and_token()
@ -2290,6 +2320,40 @@ def test_update_row(api_client, data_fixture):
assert getattr(row_2, f"field_{boolean_field.id}") is False assert getattr(row_2, f"field_{boolean_field.id}") is False
@pytest.mark.django_db(transaction=True)
def test_update_row_with_disabled_webhook_events(api_client, data_fixture):
user, token = data_fixture.create_user_and_token()
table = data_fixture.create_database_table(user=user)
text_field = data_fixture.create_text_field(
table=table, order=0, name="Color", text_default="white"
)
model = table.get_model()
row_1 = model.objects.create()
data_fixture.create_table_webhook(
table=table,
user=user,
request_method="POST",
url="http://localhost",
events=[],
)
url = reverse(
"api:database:rows:item", kwargs={"table_id": table.id, "row_id": row_1.id}
)
with patch("baserow.contrib.database.webhooks.registries.call_webhook.delay") as m:
response = api_client.patch(
f"{url}?send_webhook_events=false",
{f"field_{text_field.id}": "Test 1"},
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
assert response.status_code == HTTP_200_OK
m.assert_not_called()
@pytest.mark.django_db @pytest.mark.django_db
def test_update_row_with_read_only_field(api_client, data_fixture): def test_update_row_with_read_only_field(api_client, data_fixture):
user, jwt_token = data_fixture.create_user_and_token() user, jwt_token = data_fixture.create_user_and_token()
@ -2472,6 +2536,40 @@ def test_move_row(api_client, data_fixture):
) )
@pytest.mark.django_db(transaction=True)
def test_move_row_with_disabled_webhook_events(api_client, data_fixture):
user, token = data_fixture.create_user_and_token()
table = data_fixture.create_database_table(user=user)
data_fixture.create_text_field(
table=table, order=0, name="Color", text_default="white"
)
model = table.get_model()
row_1 = model.objects.create()
row_2 = model.objects.create()
data_fixture.create_table_webhook(
table=table,
user=user,
request_method="POST",
url="http://localhost",
events=[],
)
url = reverse(
"api:database:rows:move", kwargs={"table_id": table.id, "row_id": row_2.id}
)
with patch("baserow.contrib.database.webhooks.registries.call_webhook.delay") as m:
response = api_client.patch(
f"{url}?before_id={row_1.id}&send_webhook_events=false",
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
assert response.status_code == HTTP_200_OK
m.assert_not_called()
@pytest.mark.django_db @pytest.mark.django_db
def test_cannot_delete_row_by_id_with_data_sync(api_client, data_fixture): def test_cannot_delete_row_by_id_with_data_sync(api_client, data_fixture):
user, jwt_token = data_fixture.create_user_and_token() user, jwt_token = data_fixture.create_user_and_token()
@ -2566,6 +2664,39 @@ def test_delete_row_by_id(api_client, data_fixture):
assert model.objects.count() == 0 assert model.objects.count() == 0
@pytest.mark.django_db(transaction=True)
def test_delete_row_by_id_with_disabled_webhook_events(api_client, data_fixture):
user, token = data_fixture.create_user_and_token()
table = data_fixture.create_database_table(user=user)
data_fixture.create_text_field(
table=table, order=0, name="Color", text_default="white"
)
model = table.get_model()
row_1 = model.objects.create()
data_fixture.create_table_webhook(
table=table,
user=user,
request_method="POST",
url="http://localhost",
events=[],
)
url = reverse(
"api:database:rows:item", kwargs={"table_id": table.id, "row_id": row_1.id}
)
with patch("baserow.contrib.database.webhooks.registries.call_webhook.delay") as m:
response = api_client.delete(
f"{url}?send_webhook_events=false",
format="json",
HTTP_AUTHORIZATION=f"JWT {token}",
)
assert response.status_code == HTTP_204_NO_CONTENT
m.assert_not_called()
@pytest.mark.django_db @pytest.mark.django_db
def test_list_rows_with_attribute_names(api_client, data_fixture): def test_list_rows_with_attribute_names(api_client, data_fixture):
user, jwt_token = data_fixture.create_user_and_token( user, jwt_token = data_fixture.create_user_and_token(

View file

@ -229,6 +229,7 @@ def test_create_form_view_with_webhooks(api_client, data_fixture):
HTTP_AUTHORIZATION=f"JWT {token}", HTTP_AUTHORIZATION=f"JWT {token}",
) )
assert m.called assert m.called
print("CALL ARGS", m.call_args)
response_json = response.json() response_json = response.json()
assert response.status_code == HTTP_200_OK assert response.status_code == HTTP_200_OK

View file

@ -0,0 +1,7 @@
{
"type": "feature",
"message": "Prevent triggering webhook if query param is provided on row create update or delete endpoints",
"issue_number": 3085,
"bullet_points": [],
"created_at": "2024-11-19"
}

View file

@ -432,6 +432,7 @@
"pathParameters": "Path parameters", "pathParameters": "Path parameters",
"requestBodySchema": "Request body schema", "requestBodySchema": "Request body schema",
"userFieldNamesDescription": "When the `user_field_names` GET parameter is provided and its value is one of the following: `y`, `yes`, `true`, `t`, `on`, `1`, or empty string, the field names returned by this endpoint will be the actual names of the fields.\n\nIf the `user_field_names` GET parameter is not provided, or if it does not match any of the above values, then all returned field names will be `field_` followed by the id of the field. For example `field_1` refers to the field with an id of `1`.", "userFieldNamesDescription": "When the `user_field_names` GET parameter is provided and its value is one of the following: `y`, `yes`, `true`, `t`, `on`, `1`, or empty string, the field names returned by this endpoint will be the actual names of the fields.\n\nIf the `user_field_names` GET parameter is not provided, or if it does not match any of the above values, then all returned field names will be `field_` followed by the id of the field. For example `field_1` refers to the field with an id of `1`.",
"sendWebhookEventsDescription": "A flag query parameter that triggers webhooks after the operation, if set to `y`, `yes`, `true`, `t`, `on`, `1`, `or` left empty. Defaults to `true`",
"singleRow": "Single", "singleRow": "Single",
"batchRows": "Batch", "batchRows": "Batch",
"fileUploads": "File uploads" "fileUploads": "File uploads"

View file

@ -39,6 +39,16 @@
<APIDocsParameter :optional="true" name="before" type="integer"> <APIDocsParameter :optional="true" name="before" type="integer">
{{ $t('apiDocsTableCreateRow.before') }} {{ $t('apiDocsTableCreateRow.before') }}
</APIDocsParameter> </APIDocsParameter>
<APIDocsParameter
name="send_webhook_events"
:optional="true"
type="any"
>
<MarkdownIt
class="api-docs__content"
:content="$t('apiDocs.sendWebhookEventsDescription')"
/>
</APIDocsParameter>
</ul> </ul>
<h4 class="api-docs__heading-4"> <h4 class="api-docs__heading-4">
{{ $t('apiDocs.requestBodySchema') }} {{ $t('apiDocs.requestBodySchema') }}
@ -72,6 +82,16 @@
<APIDocsParameter :optional="true" name="before" type="integer"> <APIDocsParameter :optional="true" name="before" type="integer">
{{ $t('apiDocsTableCreateRows.before') }} {{ $t('apiDocsTableCreateRows.before') }}
</APIDocsParameter> </APIDocsParameter>
<APIDocsParameter
name="send_webhook_events"
:optional="true"
type="any"
>
<MarkdownIt
class="api-docs__content"
:content="$t('apiDocs.sendWebhookEventsDescription')"
/>
</APIDocsParameter>
</ul> </ul>
<h4 class="api-docs__heading-4"> <h4 class="api-docs__heading-4">
{{ $t('apiDocs.requestBodySchema') }} {{ $t('apiDocs.requestBodySchema') }}

View file

@ -33,7 +33,20 @@
</APIDocsParameter> </APIDocsParameter>
</ul> </ul>
</div> </div>
<div v-else> <h4 class="api-docs__heading-4">{{ $t('apiDocs.queryParameters') }}</h4>
<ul class="api-docs__parameters">
<APIDocsParameter
name="send_webhook_events"
:optional="true"
type="any"
>
<MarkdownIt
class="api-docs__content"
:content="$t('apiDocs.sendWebhookEventsDescription')"
/>
</APIDocsParameter>
</ul>
<div v-if="batchMode === true">
<h4 class="api-docs__heading-4"> <h4 class="api-docs__heading-4">
{{ $t('apiDocs.requestBodySchema') }} {{ $t('apiDocs.requestBodySchema') }}
</h4> </h4>

View file

@ -28,6 +28,16 @@
<APIDocsParameter name="before_id" type="integer" :optional="true"> <APIDocsParameter name="before_id" type="integer" :optional="true">
{{ $t('apiDocsTableMoveRow.before') }} {{ $t('apiDocsTableMoveRow.before') }}
</APIDocsParameter> </APIDocsParameter>
<APIDocsParameter
name="send_webhook_events"
:optional="true"
type="any"
>
<MarkdownIt
class="api-docs__content"
:content="$t('apiDocs.sendWebhookEventsDescription')"
/>
</APIDocsParameter>
</ul> </ul>
</div> </div>
<div class="api-docs__right"> <div class="api-docs__right">

View file

@ -40,6 +40,16 @@
:content="$t('apiDocs.userFieldNamesDescription')" :content="$t('apiDocs.userFieldNamesDescription')"
/> />
</APIDocsParameter> </APIDocsParameter>
<APIDocsParameter
name="send_webhook_events"
:optional="true"
type="any"
>
<MarkdownIt
class="api-docs__content"
:content="$t('apiDocs.sendWebhookEventsDescription')"
/>
</APIDocsParameter>
</ul> </ul>
<h4 class="api-docs__heading-4"> <h4 class="api-docs__heading-4">
{{ $t('apiDocs.requestBodySchema') }} {{ $t('apiDocs.requestBodySchema') }}
@ -68,6 +78,16 @@
:content="$t('apiDocs.userFieldNamesDescription')" :content="$t('apiDocs.userFieldNamesDescription')"
/> />
</APIDocsParameter> </APIDocsParameter>
<APIDocsParameter
name="send_webhook_events"
:optional="true"
type="any"
>
<MarkdownIt
class="api-docs__content"
:content="$t('apiDocs.sendWebhookEventsDescription')"
/>
</APIDocsParameter>
</ul> </ul>
<h4 class="api-docs__heading-4"> <h4 class="api-docs__heading-4">
{{ $t('apiDocs.requestBodySchema') }} {{ $t('apiDocs.requestBodySchema') }}