From 95fffb14ba47a31197c4250a8d7142b6f6e46d59 Mon Sep 17 00:00:00 2001 From: sinisaos Date: Sat, 4 Jun 2022 13:15:58 +0200 Subject: [PATCH 01/10] Added deletion in bulk --- docs/source/crud/piccolo_crud.rst | 36 ++++++ piccolo_api/crud/endpoints.py | 36 ++++-- piccolo_api/crud/validators.py | 4 +- piccolo_api/fastapi/endpoints.py | 18 +++ tests/crud/test_crud_endpoints.py | 142 ++++++++++++++++++++++-- tests/fastapi/test_fastapi_endpoints.py | 2 +- 6 files changed, 217 insertions(+), 21 deletions(-) diff --git a/docs/source/crud/piccolo_crud.rst b/docs/source/crud/piccolo_crud.rst index c44d5aae..94348729 100644 --- a/docs/source/crud/piccolo_crud.rst +++ b/docs/source/crud/piccolo_crud.rst @@ -244,6 +244,42 @@ For example, to return results 11 to 20: ------------------------------------------------------------------------------- +Bulk delete +----------- + +To specify which records you want to delete in bulk, pass a query parameter +like this ``__ids=1,2,3``, and you be able to delete all results whose ``id`` +is in the query params. + +.. hint:: You can also use this method with ``UUID`` primary keys and + the usage is the same. + +A query which delete movies with ``id`` pass in query parameter: + +.. code-block:: + + DELETE https://demo1.piccolo-orm.com/api/tables/movie/?__ids=1,2,3 + +You can delete rows in bulk with any filter params. A query which +delete movies with ``name`` pass in query parameter: + +.. code-block:: + + DELETE https://demo1.piccolo-orm.com/api/tables/movie/?name=Star + +Or you can combine multiple query params for additional security. +A query to delete records with name ``Star``, but with +specific ``id`` you can pass query like this: + +.. code-block:: + + DELETE https://demo1.piccolo-orm.com/api/tables/movie/?name=Star&__ids=1,2 + +.. warning:: To be able to provide a bulk delete action, we must set + ``allow_bulk_delete`` to ``True``. + +------------------------------------------------------------------------------- + Readable -------- diff --git a/piccolo_api/crud/endpoints.py b/piccolo_api/crud/endpoints.py index cdf8e6a2..37cf7565 100644 --- a/piccolo_api/crud/endpoints.py +++ b/piccolo_api/crud/endpoints.py @@ -86,6 +86,7 @@ class Params: include_readable: bool = False page: int = 1 page_size: t.Optional[int] = None + ids: str = field(default="") visible_fields: str = field(default="") range_header: bool = False range_header_name: str = field(default="") @@ -155,8 +156,8 @@ def __init__( :param read_only: If ``True``, only the GET method is allowed. :param allow_bulk_delete: - If ``True``, allows a delete request to the root to delete all - matching records. It is dangerous, so is disabled by default. + If True, allows a delete request to the root and delete all + matching records with values in ``__ids`` query params. :param page_size: The number of results shown on each page by default. :param exclude_secrets: @@ -521,7 +522,7 @@ async def root(self, request: Request) -> Response: return await self.post_single(request, data) elif request.method == "DELETE": params = dict(request.query_params) - return await self.delete_all(request, params=params) + return await self.delete_bulk(request, params=params) else: return Response(status_code=405) @@ -548,6 +549,9 @@ def _split_params(params: t.Dict[str, t.Any]) -> Params: And can specify which page: {'__page': 2}. + You can specify witch records want to delete from rows: + {'__ids': '1,2,3'}. + You can specify which fields want to display in rows: {'__visible_fields': 'id,name'}. @@ -601,6 +605,10 @@ def _split_params(params: t.Dict[str, t.Any]) -> Params: response.page_size = page_size continue + if key == "__ids": + response.ids = value + continue + if key == "__visible_fields": response.visible_fields = value continue @@ -820,19 +828,31 @@ async def post_single( return Response("Unable to save the resource.", status_code=500) @apply_validators - async def delete_all( + async def delete_bulk( self, request: Request, params: t.Optional[t.Dict[str, t.Any]] = None ) -> Response: """ - Deletes all rows - query parameters are used for filtering. + Bulk deletes rows - query parameters are used for filtering. """ params = self._clean_data(params) if params else {} split_params = self._split_params(params) + split_params_ids = split_params.ids.split(",") try: - query = self._apply_filters( - self.table.delete(force=True), split_params - ) + query: t.Union[ + Select, Count, Objects, Delete + ] = self.table.delete() + try: + ids = [ + int(item) if len(item) < len(str(uuid.uuid4())) else item + for item in split_params_ids + ] + query_ids = query.where( + self.table._meta.primary_key.is_in(ids) + ) + query = self._apply_filters(query_ids, split_params) + except ValueError: + query = self._apply_filters(query, split_params) except MalformedQuery as exception: return Response(str(exception), status_code=400) diff --git a/piccolo_api/crud/validators.py b/piccolo_api/crud/validators.py index b05e3268..8bb09865 100644 --- a/piccolo_api/crud/validators.py +++ b/piccolo_api/crud/validators.py @@ -34,7 +34,7 @@ def __init__( delete_single: t.List[ValidatorFunction] = [], post_single: t.List[ValidatorFunction] = [], get_all: t.List[ValidatorFunction] = [], - delete_all: t.List[ValidatorFunction] = [], + delete_bulk: t.List[ValidatorFunction] = [], get_references: t.List[ValidatorFunction] = [], get_ids: t.List[ValidatorFunction] = [], get_new: t.List[ValidatorFunction] = [], @@ -49,7 +49,7 @@ def __init__( self.delete_single = delete_single self.post_single = post_single self.get_all = get_all - self.delete_all = delete_all + self.delete_bulk = delete_bulk self.get_references = get_references self.get_ids = get_ids self.get_new = get_new diff --git a/piccolo_api/fastapi/endpoints.py b/piccolo_api/fastapi/endpoints.py index 973b1d07..a29bd101 100644 --- a/piccolo_api/fastapi/endpoints.py +++ b/piccolo_api/fastapi/endpoints.py @@ -454,6 +454,24 @@ def modify_signature( ) ) + if http_method == HTTPMethod.delete: + parameters.extend( + [ + Parameter( + name="__ids", + kind=Parameter.POSITIONAL_OR_KEYWORD, + annotation=str, + default=Query( + default=None, + description=( + "Specifies which rows you want to delete " + "in bulk (default ' ')." + ), + ), + ), + ] + ) + if http_method == HTTPMethod.get: if allow_ordering: parameters.extend( diff --git a/tests/crud/test_crud_endpoints.py b/tests/crud/test_crud_endpoints.py index 3a6629b1..be4045fb 100644 --- a/tests/crud/test_crud_endpoints.py +++ b/tests/crud/test_crud_endpoints.py @@ -1,7 +1,7 @@ from enum import Enum from unittest import TestCase -from piccolo.columns import ForeignKey, Integer, Secret, Varchar +from piccolo.columns import UUID, ForeignKey, Integer, Secret, Varchar from piccolo.columns.readable import Readable from piccolo.table import Table from starlette.datastructures import QueryParams @@ -33,6 +33,11 @@ class TopSecret(Table): confidential = Secret() +class Studio(Table): + pk = UUID(primary_key=True) + name = Varchar() + + class TestGetVisibleFieldsOptions(TestCase): def test_without_joins(self): response = get_visible_fields_options(table=Role, max_joins=0) @@ -1075,9 +1080,11 @@ def test_get_404(self): class TestBulkDelete(TestCase): def setUp(self): Movie.create_table(if_not_exists=True).run_sync() + Studio.create_table(if_not_exists=True).run_sync() def tearDown(self): Movie.alter().drop_table().run_sync() + Studio.alter().drop_table().run_sync() def test_no_bulk_delete(self): """ @@ -1097,25 +1104,27 @@ def test_no_bulk_delete(self): movie_count = Movie.count().run_sync() self.assertEqual(movie_count, 1) - def test_bulk_delete(self): + def test_bulk_delete_pk_serial(self): """ - Make sure that bulk deletes are only allowed is allow_bulk_delete is - True. + Make sure that bulk deletes are only allowed if ``allow_bulk_delete`` + is True. """ client = TestClient( PiccoloCRUD(table=Movie, read_only=False, allow_bulk_delete=True) ) - movie = Movie(name="Star Wars", rating=93) - movie.save().run_sync() + Movie.insert( + Movie(name="Star Wars", rating=93), + Movie(name="Lord of the Rings", rating=90), + ).run_sync() - response = client.delete("/") + response = client.delete("/", params={"__ids": "1,2"}) self.assertEqual(response.status_code, 204) movie_count = Movie.count().run_sync() self.assertEqual(movie_count, 0) - def test_bulk_delete_filtering(self): + def test_bulk_delete_pk_serial_filtering(self): """ Make sure filtering works with bulk deletes. """ @@ -1128,13 +1137,126 @@ def test_bulk_delete_filtering(self): Movie(name="Lord of the Rings", rating=90), ).run_sync() - response = client.delete("/?name=Star%20Wars") + response = client.delete( + "/", params={"__ids": "1", "name": "Star Wars"} + ) + self.assertEqual(response.status_code, 204) + + movies = Movie.select().run_sync() + self.assertEqual(len(movies), 1) + self.assertEqual(movies[0]["name"], "Lord of the Rings") + + def test_bulk_delete_pk_serial_filtering_without_ids(self): + """ + Make sure filtering works with bulk deletes and + without ``__ids`` query params. + """ + client = TestClient( + PiccoloCRUD(table=Movie, read_only=False, allow_bulk_delete=True) + ) + + Movie.insert( + Movie(name="Star Wars", rating=93), + Movie(name="Lord of the Rings", rating=90), + ).run_sync() + + response = client.delete("/", params={"name": "Star Wars"}) self.assertEqual(response.status_code, 204) movies = Movie.select().run_sync() self.assertEqual(len(movies), 1) self.assertEqual(movies[0]["name"], "Lord of the Rings") + def test_bulk_delete_pk_uuid(self): + """ + Make sure that bulk deletes are only allowed if ``allow_bulk_delete`` + is True. + """ + client = TestClient( + PiccoloCRUD(table=Studio, read_only=False, allow_bulk_delete=True) + ) + + Studio.insert( + Studio( + pk="af5dc416-2784-4d63-87a1-c987f7ad57fc", name="Blasting Room" + ), + Studio( + pk="708a7531-b1cf-4ff8-a25d-3287fad1bac4", name="JHOC Studio" + ), + ).run_sync() + + studios = Studio.select().run_sync() + response = client.delete( + "/", + params={"__ids": f"{studios[0]['pk']},{studios[1]['pk']}"}, + ) + self.assertEqual(response.status_code, 204) + + studio_count = Studio.count().run_sync() + self.assertEqual(studio_count, 0) + + def test_bulk_delete_pk_uuid_filtering(self): + """ + Make sure filtering works with bulk deletes. + """ + client = TestClient( + PiccoloCRUD(table=Studio, read_only=False, allow_bulk_delete=True) + ) + + Studio.insert( + Studio( + pk="af5dc416-2784-4d63-87a1-c987f7ad57fc", name="Blasting Room" + ), + Studio( + pk="708a7531-b1cf-4ff8-a25d-3287fad1bac4", name="JHOC Studio" + ), + ).run_sync() + + studios = Studio.select().run_sync() + response = client.delete( + "/", + params={ + "__ids": f"{studios[0]['pk']}", + "name": f"{studios[0]['name']}", + }, + ) + + studios = Studio.select().run_sync() + self.assertEqual(response.status_code, 204) + self.assertEqual(len(studios), 1) + self.assertEqual(studios[0]["name"], "JHOC Studio") + + def test_bulk_delete_pk_uuid_filtering_without_ids(self): + """ + Make sure filtering works with bulk deletes and + without ``__ids`` query params. + """ + client = TestClient( + PiccoloCRUD(table=Studio, read_only=False, allow_bulk_delete=True) + ) + + Studio.insert( + Studio( + pk="af5dc416-2784-4d63-87a1-c987f7ad57fc", name="Blasting Room" + ), + Studio( + pk="708a7531-b1cf-4ff8-a25d-3287fad1bac4", name="JHOC Studio" + ), + ).run_sync() + + studios = Studio.select().run_sync() + response = client.delete( + "/", + params={ + "name": f"{studios[0]['name']}", + }, + ) + + studios = Studio.select().run_sync() + self.assertEqual(response.status_code, 204) + self.assertEqual(len(studios), 1) + self.assertEqual(studios[0]["name"], "JHOC Studio") + def test_read_only(self): """ In read_only mode, no HTTP verbs should be allowed which modify data. @@ -1200,7 +1322,7 @@ def test_malformed_query(self): response = client.get("/count/", params={"foobar": "1"}) self.assertEqual(response.status_code, 400) - response = client.delete("/", params={"foobar": "1"}) + response = client.delete("/", params={"__ids": "1", "foobar": "1"}) self.assertEqual(response.status_code, 400) diff --git a/tests/fastapi/test_fastapi_endpoints.py b/tests/fastapi/test_fastapi_endpoints.py index 1284a163..ef7ec567 100644 --- a/tests/fastapi/test_fastapi_endpoints.py +++ b/tests/fastapi/test_fastapi_endpoints.py @@ -197,7 +197,7 @@ def test_references(self): def test_delete(self): client = TestClient(app) - response = client.delete("/movies/?id=1") + response = client.delete("/movies/1/") self.assertEqual(response.status_code, 204) self.assertEqual(response.content, b"") From 9826dc3457cea14dc729617638adb9214923d195 Mon Sep 17 00:00:00 2001 From: sinisaos Date: Sun, 5 Jun 2022 19:29:11 +0200 Subject: [PATCH 02/10] make proposed changes --- piccolo_api/crud/endpoints.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/piccolo_api/crud/endpoints.py b/piccolo_api/crud/endpoints.py index 37cf7565..54cf6efd 100644 --- a/piccolo_api/crud/endpoints.py +++ b/piccolo_api/crud/endpoints.py @@ -156,7 +156,7 @@ def __init__( :param read_only: If ``True``, only the GET method is allowed. :param allow_bulk_delete: - If True, allows a delete request to the root and delete all + If ``True``, allows a delete request to the root and delete all matching records with values in ``__ids`` query params. :param page_size: The number of results shown on each page by default. @@ -549,7 +549,7 @@ def _split_params(params: t.Dict[str, t.Any]) -> Params: And can specify which page: {'__page': 2}. - You can specify witch records want to delete from rows: + You can specify which records want to delete from rows: {'__ids': '1,2,3'}. You can specify which fields want to display in rows: @@ -843,10 +843,8 @@ async def delete_bulk( Select, Count, Objects, Delete ] = self.table.delete() try: - ids = [ - int(item) if len(item) < len(str(uuid.uuid4())) else item - for item in split_params_ids - ] + value_type = self.table._meta.primary_key.value_type + ids = [value_type(item) for item in split_params_ids] query_ids = query.where( self.table._meta.primary_key.is_in(ids) ) From 0210a2c55043a43c9d32aeff01f93c3d4abf5800 Mon Sep 17 00:00:00 2001 From: sinisaos Date: Tue, 7 Jun 2022 05:54:19 +0200 Subject: [PATCH 03/10] add comment --- piccolo_api/crud/endpoints.py | 1 + 1 file changed, 1 insertion(+) diff --git a/piccolo_api/crud/endpoints.py b/piccolo_api/crud/endpoints.py index 54cf6efd..86673049 100644 --- a/piccolo_api/crud/endpoints.py +++ b/piccolo_api/crud/endpoints.py @@ -843,6 +843,7 @@ async def delete_bulk( Select, Count, Objects, Delete ] = self.table.delete() try: + # Serial or UUID primary keys enabled in query params value_type = self.table._meta.primary_key.value_type ids = [value_type(item) for item in split_params_ids] query_ids = query.where( From 01c20db7ddbca8c6d988f3a9963b894ff2d83989 Mon Sep 17 00:00:00 2001 From: sinisaos Date: Thu, 30 Jun 2022 20:40:53 +0200 Subject: [PATCH 04/10] added bulk update --- piccolo_api/crud/endpoints.py | 52 ++++- piccolo_api/crud/validators.py | 2 + piccolo_api/fastapi/endpoints.py | 40 +++- tests/crud/test_crud_endpoints.py | 248 +++++++++++++++++++++--- tests/fastapi/test_fastapi_endpoints.py | 50 ++++- 5 files changed, 355 insertions(+), 37 deletions(-) diff --git a/piccolo_api/crud/endpoints.py b/piccolo_api/crud/endpoints.py index 86673049..35a753f8 100644 --- a/piccolo_api/crud/endpoints.py +++ b/piccolo_api/crud/endpoints.py @@ -143,6 +143,7 @@ def __init__( table: t.Type[Table], read_only: bool = True, allow_bulk_delete: bool = False, + allow_bulk_update: bool = False, page_size: int = 15, exclude_secrets: bool = True, validators: t.Optional[Validators] = None, @@ -208,6 +209,7 @@ def __init__( self.page_size = page_size self.read_only = read_only self.allow_bulk_delete = allow_bulk_delete + self.allow_bulk_update = allow_bulk_update self.exclude_secrets = exclude_secrets self.validators = validators self.max_joins = max_joins @@ -232,7 +234,9 @@ def __init__( root_methods = ["GET"] if not read_only: root_methods += ( - ["POST", "DELETE"] if allow_bulk_delete else ["POST"] + ["POST", "DELETE", "PATCH"] + if allow_bulk_delete or allow_bulk_update + else ["POST"] ) routes: t.List[BaseRoute] = [ @@ -514,6 +518,7 @@ def _parse_params(self, params: QueryParams) -> t.Dict[str, t.Any]: return output async def root(self, request: Request) -> Response: + rows_ids = request.query_params.get("rows_ids", None) if request.method == "GET": params = self._parse_params(request.query_params) return await self.get_all(request, params=params) @@ -523,6 +528,9 @@ async def root(self, request: Request) -> Response: elif request.method == "DELETE": params = dict(request.query_params) return await self.delete_bulk(request, params=params) + elif request.method == "PATCH": + data = await request.json() + return await self.patch_bulk(request, data, rows_ids=rows_ids) else: return Response(status_code=405) @@ -827,6 +835,48 @@ async def post_single( except ValueError: return Response("Unable to save the resource.", status_code=500) + @apply_validators + async def patch_bulk( + self, + request: Request, + data: t.Dict[str, t.Any], + rows_ids: str, + ) -> Response: + """ + Bulk update of rows whose primary keys are in the ``rows_ids`` + query param. + """ + cleaned_data = self._clean_data(data) + + try: + model = self.pydantic_model_optional(**cleaned_data) + except Exception as exception: + return Response(str(exception), status_code=400) + + values = { + getattr(self.table, key): getattr(model, key) + for key in data.keys() + } + + # Serial or UUID primary keys enabled in query params + value_type = self.table._meta.primary_key.value_type + split_rows_ids = rows_ids.split(",") + ids = [value_type(item) for item in split_rows_ids] + + await self.table.update(values).where( + self.table._meta.primary_key.is_in(ids) + ).run() + updated_rows = ( + await self.table.select( + exclude_secrets=self.exclude_secrets, + ) + .where(self.table._meta.primary_key.is_in(ids)) + .run() + ) + json = self.pydantic_model_plural()(rows=updated_rows).json() + + return CustomJSONResponse(json) + @apply_validators async def delete_bulk( self, request: Request, params: t.Optional[t.Dict[str, t.Any]] = None diff --git a/piccolo_api/crud/validators.py b/piccolo_api/crud/validators.py index 8bb09865..5b12a8e9 100644 --- a/piccolo_api/crud/validators.py +++ b/piccolo_api/crud/validators.py @@ -35,6 +35,7 @@ def __init__( post_single: t.List[ValidatorFunction] = [], get_all: t.List[ValidatorFunction] = [], delete_bulk: t.List[ValidatorFunction] = [], + patch_bulk: t.List[ValidatorFunction] = [], get_references: t.List[ValidatorFunction] = [], get_ids: t.List[ValidatorFunction] = [], get_new: t.List[ValidatorFunction] = [], @@ -50,6 +51,7 @@ def __init__( self.post_single = post_single self.get_all = get_all self.delete_bulk = delete_bulk + self.patch_bulk = patch_bulk self.get_references = get_references self.get_ids = get_ids self.get_new = get_new diff --git a/piccolo_api/fastapi/endpoints.py b/piccolo_api/fastapi/endpoints.py index a29bd101..d60462e7 100644 --- a/piccolo_api/fastapi/endpoints.py +++ b/piccolo_api/fastapi/endpoints.py @@ -34,19 +34,21 @@ def __init__( self, all_routes: t.Dict[str, t.Any] = {}, get: t.Dict[str, t.Any] = {}, - delete: t.Dict[str, t.Any] = {}, + delete_bulk: t.Dict[str, t.Any] = {}, post: t.Dict[str, t.Any] = {}, put: t.Dict[str, t.Any] = {}, patch: t.Dict[str, t.Any] = {}, + patch_bulk: t.Dict[str, t.Any] = {}, get_single: t.Dict[str, t.Any] = {}, delete_single: t.Dict[str, t.Any] = {}, ): self.all_routes = all_routes self.get = get - self.delete = delete + self.delete_bulk = delete_bulk self.post = post self.put = put self.patch = patch + self.patch_bulk = patch_bulk self.get_single = get_single self.delete_single = delete_single @@ -244,28 +246,52 @@ async def references(request: Request): ) ####################################################################### - # Root - DELETE + # Root - DELETE BULK if not piccolo_crud.read_only and piccolo_crud.allow_bulk_delete: - async def delete(request: Request, **kwargs): + async def delete_bulk(request: Request, **kwargs): """ Deletes all rows matching the given query. """ return await piccolo_crud.root(request=request) self.modify_signature( - endpoint=delete, + endpoint=delete_bulk, model=self.ModelOut, http_method=HTTPMethod.delete, ) fastapi_app.add_api_route( path=root_url, - endpoint=delete, + endpoint=delete_bulk, response_model=None, methods=["DELETE"], - **fastapi_kwargs.get_kwargs("delete"), + **fastapi_kwargs.get_kwargs("delete_bulk"), + ) + + ####################################################################### + # Root - PATCH BULK + + if not piccolo_crud.read_only and piccolo_crud.allow_bulk_update: + + async def patch_bulk(rows_ids: str, request: Request, model): + """ + Bulk update of rows whose primary keys are in the ``rows_ids`` + query param. + """ + return await piccolo_crud.root(request=request) + + patch_bulk.__annotations__[ + "model" + ] = f"ANNOTATIONS['{self.alias}']['ModelOptional']" + + fastapi_app.add_api_route( + path=root_url, + endpoint=patch_bulk, + response_model=self.ModelOut, + methods=["PATCH"], + **fastapi_kwargs.get_kwargs("patch_bulk"), ) ####################################################################### diff --git a/tests/crud/test_crud_endpoints.py b/tests/crud/test_crud_endpoints.py index be4045fb..15363ba3 100644 --- a/tests/crud/test_crud_endpoints.py +++ b/tests/crud/test_crud_endpoints.py @@ -1,7 +1,7 @@ from enum import Enum from unittest import TestCase -from piccolo.columns import UUID, ForeignKey, Integer, Secret, Varchar +from piccolo.columns import UUID, Boolean, ForeignKey, Integer, Secret, Varchar from piccolo.columns.readable import Readable from piccolo.table import Table from starlette.datastructures import QueryParams @@ -36,6 +36,7 @@ class TopSecret(Table): class Studio(Table): pk = UUID(primary_key=True) name = Varchar() + opened = Boolean() class TestGetVisibleFieldsOptions(TestCase): @@ -1178,10 +1179,14 @@ def test_bulk_delete_pk_uuid(self): Studio.insert( Studio( - pk="af5dc416-2784-4d63-87a1-c987f7ad57fc", name="Blasting Room" + pk="af5dc416-2784-4d63-87a1-c987f7ad57fc", + name="Blasting Room", + opened=True, ), Studio( - pk="708a7531-b1cf-4ff8-a25d-3287fad1bac4", name="JHOC Studio" + pk="708a7531-b1cf-4ff8-a25d-3287fad1bac4", + name="JHOC Studio", + opened=True, ), ).run_sync() @@ -1205,10 +1210,14 @@ def test_bulk_delete_pk_uuid_filtering(self): Studio.insert( Studio( - pk="af5dc416-2784-4d63-87a1-c987f7ad57fc", name="Blasting Room" + pk="af5dc416-2784-4d63-87a1-c987f7ad57fc", + name="Blasting Room", + opened=True, ), Studio( - pk="708a7531-b1cf-4ff8-a25d-3287fad1bac4", name="JHOC Studio" + pk="708a7531-b1cf-4ff8-a25d-3287fad1bac4", + name="JHOC Studio", + opened=True, ), ).run_sync() @@ -1237,10 +1246,14 @@ def test_bulk_delete_pk_uuid_filtering_without_ids(self): Studio.insert( Studio( - pk="af5dc416-2784-4d63-87a1-c987f7ad57fc", name="Blasting Room" + pk="af5dc416-2784-4d63-87a1-c987f7ad57fc", + name="Blasting Room", + opened=True, ), Studio( - pk="708a7531-b1cf-4ff8-a25d-3287fad1bac4", name="JHOC Studio" + pk="708a7531-b1cf-4ff8-a25d-3287fad1bac4", + name="JHOC Studio", + opened=True, ), ).run_sync() @@ -1275,6 +1288,211 @@ def test_read_only(self): self.assertEqual(movie_count, 1) +class TestBulkUpdate(TestCase): + def setUp(self): + Movie.create_table(if_not_exists=True).run_sync() + Studio.create_table(if_not_exists=True).run_sync() + + def tearDown(self): + Movie.alter().drop_table().run_sync() + Studio.alter().drop_table().run_sync() + + def test_no_bulk_update(self): + """ + Make sure that updates aren't allowed when ``allow_bulk_update`` is + False. + """ + client = TestClient( + PiccoloCRUD(table=Movie, read_only=False, allow_bulk_update=False) + ) + + movie = Movie(name="Star Wars", rating=93) + movie.save().run_sync() + + params = {"rows_ids": "1"} + + response = client.patch("/", params=params, json={"rating": 98}) + self.assertEqual(response.status_code, 405) + + movie_count = Movie.count().run_sync() + self.assertEqual(movie_count, 1) + + def test_bulk_update_pk_serial_multiple_columns(self): + """ + Make sure that we can update multiple columns in bulk. + """ + client = TestClient( + PiccoloCRUD(table=Movie, read_only=False, allow_bulk_update=True) + ) + + Movie.insert( + Movie(name="Star Wars", rating=93), + Movie(name="Lord of the Rings", rating=90), + ).run_sync() + + params = {"rows_ids": "1,2"} + json = {"name": "Alien", "rating": 98} + + response = client.patch("/", params=params, json=json) + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.json(), + { + "rows": [ + {"id": 1, "name": "Alien", "rating": 98}, + {"id": 2, "name": "Alien", "rating": 98}, + ] + }, + ) + + def test_bulk_update_pk_serial_single_column(self): + """ + Make sure to update only one column in bulk without changes + to other columns. + """ + client = TestClient( + PiccoloCRUD(table=Movie, read_only=False, allow_bulk_update=True) + ) + + Movie.insert( + Movie(name="Star Wars", rating=93), + Movie(name="Lord of the Rings", rating=90), + ).run_sync() + + params = {"rows_ids": "1,2"} + json = {"rating": 95} + + response = client.patch("/", params=params, json=json) + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.json(), + { + "rows": [ + {"id": 1, "name": "Star Wars", "rating": 95}, + {"id": 2, "name": "Lord of the Rings", "rating": 95}, + ] + }, + ) + + def test_bulk_update_pk_serial_non_existing_column(self): + """ + Make sure if we pass non-existent values ​​in the `` rows_ids`` + query params, the rows do not change. + """ + client = TestClient( + PiccoloCRUD(table=Movie, read_only=False, allow_bulk_update=True) + ) + + Movie.insert( + Movie(name="Star Wars", rating=93), + Movie(name="Lord of the Rings", rating=90), + ).run_sync() + # non existing values + params = {"rows_ids": "3,4"} + json = {"rating": 95} + + response = client.patch("/", params=params, json=json) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), {"rows": []}) + + def test_bulk_update_pk_uuid_multiple_columns(self): + """ + Make sure that we can update multiple columns in bulk + with uuid primary key. + """ + client = TestClient( + PiccoloCRUD(table=Studio, read_only=False, allow_bulk_update=True) + ) + + Studio.insert( + Studio( + pk="af5dc416-2784-4d63-87a1-c987f7ad57fc", + name="Blasting Room", + opened=True, + ), + Studio( + pk="708a7531-b1cf-4ff8-a25d-3287fad1bac4", + name="JHOC Studio", + opened=True, + ), + ).run_sync() + + params = { + "rows_ids": ( + "af5dc416-2784-4d63-87a1-c987f7ad57fc," + "708a7531-b1cf-4ff8-a25d-3287fad1bac4" + ) + } + json = {"name": "New Studio", "opened": False} + + response = client.patch("/", params=params, json=json) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json()["rows"][0]["name"], "New Studio") + self.assertEqual(response.json()["rows"][0]["opened"], False) + + def test_bulk_update_pk_uuid_single_column(self): + """ + Make sure to update only one column in bulk without changes + to other columns. + """ + client = TestClient( + PiccoloCRUD(table=Studio, read_only=False, allow_bulk_update=True) + ) + + Studio.insert( + Studio( + pk="af5dc416-2784-4d63-87a1-c987f7ad57fc", + name="Blasting Room", + opened=True, + ), + Studio( + pk="708a7531-b1cf-4ff8-a25d-3287fad1bac4", + name="JHOC Studio", + opened=True, + ), + ).run_sync() + + params = { + "rows_ids": ( + "af5dc416-2784-4d63-87a1-c987f7ad57fc," + "708a7531-b1cf-4ff8-a25d-3287fad1bac4" + ) + } + json = {"opened": False} + + response = client.patch("/", params=params, json=json) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json()["rows"][0]["opened"], False) + + def test_bulk_update_uuid_serial_non_existing_column(self): + """ + Make sure if we pass non-existent values ​​in the `` rows_ids`` + query params, the rows do not change. + """ + client = TestClient( + PiccoloCRUD(table=Studio, read_only=False, allow_bulk_update=True) + ) + + Studio.insert( + Studio( + pk="af5dc416-2784-4d63-87a1-c987f7ad57fc", + name="Blasting Room", + opened=True, + ), + Studio( + pk="708a7531-b1cf-4ff8-a25d-3287fad1bac4", + name="JHOC Studio", + opened=True, + ), + ).run_sync() + + params = {"rows_ids": "708a7531-b1cf-4ff8-87a1-c987f7ad57fc"} + json = {"opened": False} + response = client.patch("/", params=params, json=json) + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), {"rows": []}) + + class TestNew(TestCase): def setUp(self): Movie.create_table(if_not_exists=True).run_sync() @@ -1326,22 +1544,6 @@ def test_malformed_query(self): self.assertEqual(response.status_code, 400) -class TestIncorrectVerbs(TestCase): - def setUp(self): - Movie.create_table(if_not_exists=True).run_sync() - - def tearDown(self): - Movie.alter().drop_table().run_sync() - - def test_incorrect_verbs(self): - client = TestClient( - PiccoloCRUD(table=Movie, read_only=False, allow_bulk_delete=True) - ) - - response = client.patch("/", params={}) - self.assertEqual(response.status_code, 405) - - class TestParseParams(TestCase): def test_parsing(self): app = PiccoloCRUD(table=Movie) diff --git a/tests/fastapi/test_fastapi_endpoints.py b/tests/fastapi/test_fastapi_endpoints.py index ef7ec567..607bf749 100644 --- a/tests/fastapi/test_fastapi_endpoints.py +++ b/tests/fastapi/test_fastapi_endpoints.py @@ -31,7 +31,10 @@ class Role(Table): root_url="/movies/", fastapi_app=app, piccolo_crud=PiccoloCRUD( - table=Movie, read_only=False, allow_bulk_delete=True + table=Movie, + read_only=False, + allow_bulk_delete=True, + allow_bulk_update=True, ), ) @@ -62,6 +65,7 @@ class TestResponses(TestCase): def setUp(self): Movie.create_table(if_not_exists=True).run_sync() Movie(name="Star Wars", rating=93).save().run_sync() + Movie(name="Alien", rating=95).save().run_sync() def tearDown(self): Movie.alter().drop_table().run_sync() @@ -72,7 +76,12 @@ def test_get(self): self.assertEqual(response.status_code, 200) self.assertEqual( response.json(), - {"rows": [{"id": 1, "name": "Star Wars", "rating": 93}]}, + { + "rows": [ + {"id": 2, "name": "Alien", "rating": 95}, + {"id": 1, "name": "Star Wars", "rating": 93}, + ] + }, ) def test_get_single(self): @@ -90,7 +99,7 @@ def test_count(self): self.assertEqual(response.status_code, 200) self.assertEqual( response.json(), - {"count": 1, "page_size": 15}, + {"count": 2, "page_size": 15}, ) def test_schema(self): @@ -175,7 +184,13 @@ def test_get_ids(self): client = TestClient(app) response = client.get("/movies/ids/") self.assertEqual(response.status_code, 200) - self.assertEqual(response.json(), {"1": "Star Wars"}) + self.assertEqual( + response.json(), + { + "1": "Star Wars", + "2": "Alien", + }, + ) def test_new(self): client = TestClient(app) @@ -204,10 +219,10 @@ def test_delete(self): def test_post(self): client = TestClient(app) response = client.post( - "/movies/", json={"name": "Star Wars", "rating": 93} + "/movies/", json={"name": "Alien 2", "rating": 94} ) self.assertEqual(response.status_code, 201) - self.assertEqual(response.json(), [{"id": 2}]) + self.assertEqual(response.json(), [{"id": 3}]) def test_put(self): client = TestClient(app) @@ -228,3 +243,26 @@ def test_patch(self): self.assertEqual( response.json(), {"id": 1, "name": "Star Wars", "rating": 90} ) + + def test_patch_bulk(self): + client = TestClient(app) + params = {"rows_ids": "1,2"} + json = {"rating": 99} + response = client.patch("/movies/", params=params, json=json) + self.assertEqual(response.status_code, 200) + self.assertEqual( + response.json(), + { + "rows": [ + {"id": 1, "name": "Star Wars", "rating": 99}, + {"id": 2, "name": "Alien", "rating": 99}, + ] + }, + ) + + def test_delete_bulk(self): + client = TestClient(app) + params = {"__ids": "1,2"} + response = client.delete("/movies/", params=params) + self.assertEqual(response.status_code, 204) + self.assertEqual(response.content, b"") From 6e072d29da05da5eb0284d98cf8c0389198bda89 Mon Sep 17 00:00:00 2001 From: sinisaos Date: Fri, 1 Jul 2022 18:32:03 +0200 Subject: [PATCH 05/10] add docs for bulk update --- docs/source/crud/piccolo_crud.rst | 44 ++++++++++++++++++++++++------- piccolo_api/crud/endpoints.py | 3 +++ 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/docs/source/crud/piccolo_crud.rst b/docs/source/crud/piccolo_crud.rst index 94348729..a0554f8b 100644 --- a/docs/source/crud/piccolo_crud.rst +++ b/docs/source/crud/piccolo_crud.rst @@ -11,16 +11,16 @@ API. Endpoints --------- -========== ======================= ========================================================================================================== -Path Methods Description -========== ======================= ========================================================================================================== -/ GET, POST, DELETE Get all rows, post a new row, or delete all matching rows. -// GET, PUT, DELETE, PATCH Get, update or delete a single row. -/schema/ GET Returns a JSON schema for the table. This allows clients to auto generate forms. -/ids/ GET Returns a mapping of all row ids to a description of the row. -/count/ GET Returns the number of matching rows. -/new/ GET Returns all of the default values for a new row - can be used to dynamically generate forms by the client. -========== ======================= ========================================================================================================== +========== ======================== ========================================================================================================== +Path Methods Description +========== ======================== ========================================================================================================== +/ GET, POST, DELETE, PATCH Get all rows, post a new row, delete all matching rows or update all matching rows. +// GET, PUT, DELETE, PATCH Get, update or delete a single row. +/schema/ GET Returns a JSON schema for the table. This allows clients to auto generate forms. +/ids/ GET Returns a mapping of all row ids to a description of the row. +/count/ GET Returns the number of matching rows. +/new/ GET Returns all of the default values for a new row - can be used to dynamically generate forms by the client. +========== ======================== ========================================================================================================== ------------------------------------------------------------------------------- @@ -280,6 +280,30 @@ specific ``id`` you can pass query like this: ------------------------------------------------------------------------------- +Bulk update +----------- + +To specify which records you want to update in bulk, pass a query parameter +like this ``rows_ids=1,2,3``, and you be able to update all results whose ``id`` +is in the query params. + +.. hint:: You can also use this method with ``UUID`` primary keys and + the usage is the same. + +A query which update movies with ``id`` pass in query parameter: + +.. code-block:: + + PATCH https://demo1.piccolo-orm.com/api/tables/movie/?rows_ids=1,2,3 + +If you pass a wrong or non-existent value to the query parameters ``rows_ids``, +no record will be changed and api response will be empty list. + +.. warning:: To be able to provide a bulk update action, we must set + ``allow_bulk_update`` to ``True``. + +------------------------------------------------------------------------------- + Readable -------- diff --git a/piccolo_api/crud/endpoints.py b/piccolo_api/crud/endpoints.py index 35a753f8..3b01a953 100644 --- a/piccolo_api/crud/endpoints.py +++ b/piccolo_api/crud/endpoints.py @@ -159,6 +159,9 @@ def __init__( :param allow_bulk_delete: If ``True``, allows a delete request to the root and delete all matching records with values in ``__ids`` query params. + :param allow_bulk_update: + If ``True``, allows a update request to the root and update all + matching records with values in ``rows_ids`` query params. :param page_size: The number of results shown on each page by default. :param exclude_secrets: From e6bde0fc8802ec7adc297128b1922f432ead6e5f Mon Sep 17 00:00:00 2001 From: sinisaos Date: Mon, 3 Oct 2022 22:28:15 +0200 Subject: [PATCH 06/10] proposed changes --- docs/source/crud/piccolo_crud.rst | 6 +++--- piccolo_api/crud/endpoints.py | 20 +++++++++----------- piccolo_api/fastapi/endpoints.py | 7 ++++--- tests/crud/test_crud_endpoints.py | 20 ++++++++++---------- tests/fastapi/test_fastapi_endpoints.py | 2 +- 5 files changed, 27 insertions(+), 28 deletions(-) diff --git a/docs/source/crud/piccolo_crud.rst b/docs/source/crud/piccolo_crud.rst index 4986a455..f8834871 100644 --- a/docs/source/crud/piccolo_crud.rst +++ b/docs/source/crud/piccolo_crud.rst @@ -283,7 +283,7 @@ Bulk update ----------- To specify which records you want to update in bulk, pass a query parameter -like this ``rows_ids=1,2,3``, and you be able to update all results whose ``id`` +like this ``__ids=1,2,3``, and you be able to update all results whose ``id`` is in the query params. .. hint:: You can also use this method with ``UUID`` primary keys and @@ -293,9 +293,9 @@ A query which update movies with ``id`` pass in query parameter: .. code-block:: - PATCH https://demo1.piccolo-orm.com/api/tables/movie/?rows_ids=1,2,3 + PATCH https://demo1.piccolo-orm.com/api/tables/movie/?__ids=1,2,3 -If you pass a wrong or non-existent value to the query parameters ``rows_ids``, +If you pass a wrong or non-existent value to the query parameters ``__ids``, no record will be changed and api response will be empty list. .. warning:: To be able to provide a bulk update action, we must set diff --git a/piccolo_api/crud/endpoints.py b/piccolo_api/crud/endpoints.py index e2cc25d2..01a4ba31 100644 --- a/piccolo_api/crud/endpoints.py +++ b/piccolo_api/crud/endpoints.py @@ -162,7 +162,7 @@ def __init__( matching records with values in ``__ids`` query params. :param allow_bulk_update: If ``True``, allows a update request to the root and update all - matching records with values in ``rows_ids`` query params. + matching records with values in ``__ids`` query params. :param page_size: The number of results shown on each page by default. :param exclude_secrets: @@ -237,11 +237,11 @@ def __init__( root_methods = ["GET"] if not read_only: - root_methods += ( - ["POST", "DELETE", "PATCH"] - if allow_bulk_delete or allow_bulk_update - else ["POST"] - ) + root_methods.append("POST") + if allow_bulk_delete: + root_methods.append("DELETE") + if allow_bulk_update: + root_methods.append("PATCH") routes: t.List[BaseRoute] = [ Route(path="/", endpoint=self.root, methods=root_methods), @@ -513,7 +513,7 @@ def _parse_params(self, params: QueryParams) -> t.Dict[str, t.Any]: return output async def root(self, request: Request) -> Response: - rows_ids = request.query_params.get("rows_ids", None) + rows_ids = request.query_params.get("__ids", None) if request.method == "GET": params = self._parse_params(request.query_params) return await self.get_all(request, params=params) @@ -852,7 +852,7 @@ async def patch_bulk( rows_ids: str, ) -> Response: """ - Bulk update of rows whose primary keys are in the ``rows_ids`` + Bulk update of rows whose primary keys are in the ``__ids`` query param. """ cleaned_data = self._clean_data(data) @@ -898,9 +898,7 @@ async def delete_bulk( split_params_ids = split_params.ids.split(",") try: - query: t.Union[ - Select, Count, Objects, Delete - ] = self.table.delete() + query: t.Any = self.table.delete() try: # Serial or UUID primary keys enabled in query params value_type = self.table._meta.primary_key.value_type diff --git a/piccolo_api/fastapi/endpoints.py b/piccolo_api/fastapi/endpoints.py index d60462e7..882f305e 100644 --- a/piccolo_api/fastapi/endpoints.py +++ b/piccolo_api/fastapi/endpoints.py @@ -23,6 +23,7 @@ class HTTPMethod(str, Enum): get = "GET" delete = "DELETE" + patch = "PATCH" class FastAPIKwargs: @@ -275,9 +276,9 @@ async def delete_bulk(request: Request, **kwargs): if not piccolo_crud.read_only and piccolo_crud.allow_bulk_update: - async def patch_bulk(rows_ids: str, request: Request, model): + async def patch_bulk(request: Request, model): """ - Bulk update of rows whose primary keys are in the ``rows_ids`` + Bulk update of rows whose primary keys are in the ``__ids`` query param. """ return await piccolo_crud.root(request=request) @@ -480,7 +481,7 @@ def modify_signature( ) ) - if http_method == HTTPMethod.delete: + if http_method == HTTPMethod.delete or http_method == HTTPMethod.patch: parameters.extend( [ Parameter( diff --git a/tests/crud/test_crud_endpoints.py b/tests/crud/test_crud_endpoints.py index bc4922a8..25b11521 100644 --- a/tests/crud/test_crud_endpoints.py +++ b/tests/crud/test_crud_endpoints.py @@ -48,7 +48,7 @@ class Studio(Table): name = Varchar() opened = Boolean() - + class Company(Table): name = Varchar() contact_email = Email() @@ -1547,7 +1547,7 @@ def test_no_bulk_update(self): movie = Movie(name="Star Wars", rating=93) movie.save().run_sync() - params = {"rows_ids": "1"} + params = {"__ids": "1"} response = client.patch("/", params=params, json={"rating": 98}) self.assertEqual(response.status_code, 405) @@ -1568,7 +1568,7 @@ def test_bulk_update_pk_serial_multiple_columns(self): Movie(name="Lord of the Rings", rating=90), ).run_sync() - params = {"rows_ids": "1,2"} + params = {"__ids": "1,2"} json = {"name": "Alien", "rating": 98} response = client.patch("/", params=params, json=json) @@ -1597,7 +1597,7 @@ def test_bulk_update_pk_serial_single_column(self): Movie(name="Lord of the Rings", rating=90), ).run_sync() - params = {"rows_ids": "1,2"} + params = {"__ids": "1,2"} json = {"rating": 95} response = client.patch("/", params=params, json=json) @@ -1614,7 +1614,7 @@ def test_bulk_update_pk_serial_single_column(self): def test_bulk_update_pk_serial_non_existing_column(self): """ - Make sure if we pass non-existent values ​​in the `` rows_ids`` + Make sure if we pass non-existent values ​​in the `` __ids`` query params, the rows do not change. """ client = TestClient( @@ -1626,7 +1626,7 @@ def test_bulk_update_pk_serial_non_existing_column(self): Movie(name="Lord of the Rings", rating=90), ).run_sync() # non existing values - params = {"rows_ids": "3,4"} + params = {"__ids": "3,4"} json = {"rating": 95} response = client.patch("/", params=params, json=json) @@ -1656,7 +1656,7 @@ def test_bulk_update_pk_uuid_multiple_columns(self): ).run_sync() params = { - "rows_ids": ( + "__ids": ( "af5dc416-2784-4d63-87a1-c987f7ad57fc," "708a7531-b1cf-4ff8-a25d-3287fad1bac4" ) @@ -1691,7 +1691,7 @@ def test_bulk_update_pk_uuid_single_column(self): ).run_sync() params = { - "rows_ids": ( + "__ids": ( "af5dc416-2784-4d63-87a1-c987f7ad57fc," "708a7531-b1cf-4ff8-a25d-3287fad1bac4" ) @@ -1704,7 +1704,7 @@ def test_bulk_update_pk_uuid_single_column(self): def test_bulk_update_uuid_serial_non_existing_column(self): """ - Make sure if we pass non-existent values ​​in the `` rows_ids`` + Make sure if we pass non-existent values ​​in the `` __ids`` query params, the rows do not change. """ client = TestClient( @@ -1724,7 +1724,7 @@ def test_bulk_update_uuid_serial_non_existing_column(self): ), ).run_sync() - params = {"rows_ids": "708a7531-b1cf-4ff8-87a1-c987f7ad57fc"} + params = {"__ids": "708a7531-b1cf-4ff8-87a1-c987f7ad57fc"} json = {"opened": False} response = client.patch("/", params=params, json=json) self.assertEqual(response.status_code, 200) diff --git a/tests/fastapi/test_fastapi_endpoints.py b/tests/fastapi/test_fastapi_endpoints.py index 607bf749..88b85517 100644 --- a/tests/fastapi/test_fastapi_endpoints.py +++ b/tests/fastapi/test_fastapi_endpoints.py @@ -246,7 +246,7 @@ def test_patch(self): def test_patch_bulk(self): client = TestClient(app) - params = {"rows_ids": "1,2"} + params = {"__ids": "1,2"} json = {"rating": 99} response = client.patch("/movies/", params=params, json=json) self.assertEqual(response.status_code, 200) From f8c80288e7031be2dde7645689b73c3f533b2126 Mon Sep 17 00:00:00 2001 From: sinisaos Date: Tue, 4 Oct 2022 06:52:24 +0200 Subject: [PATCH 07/10] update parameter description --- piccolo_api/fastapi/endpoints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/piccolo_api/fastapi/endpoints.py b/piccolo_api/fastapi/endpoints.py index 882f305e..07281113 100644 --- a/piccolo_api/fastapi/endpoints.py +++ b/piccolo_api/fastapi/endpoints.py @@ -492,7 +492,7 @@ def modify_signature( default=None, description=( "Specifies which rows you want to delete " - "in bulk (default ' ')." + "or update in bulk (default ' ')." ), ), ), From 524c1ac9c7611bdb68e2030c4dd99ff7268c2889 Mon Sep 17 00:00:00 2001 From: sinisaos Date: Wed, 5 Oct 2022 19:58:11 +0200 Subject: [PATCH 08/10] update docstring --- piccolo_api/crud/endpoints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/piccolo_api/crud/endpoints.py b/piccolo_api/crud/endpoints.py index 01a4ba31..144dee9f 100644 --- a/piccolo_api/crud/endpoints.py +++ b/piccolo_api/crud/endpoints.py @@ -552,7 +552,7 @@ def _split_params(params: t.Dict[str, t.Any]) -> Params: And can specify which page: {'__page': 2}. - You can specify which records want to delete from rows: + You can specify which records want to delete or update from rows: {'__ids': '1,2,3'}. You can specify which fields want to display in rows: From 1541cdd1a9e0cbddcac7d488447edf47669e4326 Mon Sep 17 00:00:00 2001 From: Daniel Townsend Date: Fri, 7 Oct 2022 14:23:00 +0100 Subject: [PATCH 09/10] shorten example URLs --- docs/source/crud/piccolo_crud.rst | 8 ++++---- piccolo_api/crud/endpoints.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/source/crud/piccolo_crud.rst b/docs/source/crud/piccolo_crud.rst index f8834871..73afd188 100644 --- a/docs/source/crud/piccolo_crud.rst +++ b/docs/source/crud/piccolo_crud.rst @@ -257,14 +257,14 @@ A query which delete movies with ``id`` pass in query parameter: .. code-block:: - DELETE https://demo1.piccolo-orm.com/api/tables/movie/?__ids=1,2,3 + DELETE /movie/?__ids=1,2,3 You can delete rows in bulk with any filter params. A query which delete movies with ``name`` pass in query parameter: .. code-block:: - DELETE https://demo1.piccolo-orm.com/api/tables/movie/?name=Star + DELETE /movie/?name=Star Or you can combine multiple query params for additional security. A query to delete records with name ``Star``, but with @@ -272,7 +272,7 @@ specific ``id`` you can pass query like this: .. code-block:: - DELETE https://demo1.piccolo-orm.com/api/tables/movie/?name=Star&__ids=1,2 + DELETE /movie/?name=Star&__ids=1,2 .. warning:: To be able to provide a bulk delete action, we must set ``allow_bulk_delete`` to ``True``. @@ -293,7 +293,7 @@ A query which update movies with ``id`` pass in query parameter: .. code-block:: - PATCH https://demo1.piccolo-orm.com/api/tables/movie/?__ids=1,2,3 + PATCH /movie/?__ids=1,2,3 If you pass a wrong or non-existent value to the query parameters ``__ids``, no record will be changed and api response will be empty list. diff --git a/piccolo_api/crud/endpoints.py b/piccolo_api/crud/endpoints.py index 144dee9f..4cf2afd5 100644 --- a/piccolo_api/crud/endpoints.py +++ b/piccolo_api/crud/endpoints.py @@ -476,11 +476,11 @@ def _parse_params(self, params: QueryParams) -> t.Dict[str, t.Any]: The GET params may contain multiple values for each parameter name. For example: - /tables/movie?tag=horror&tag=scifi + /movie?tag=horror&tag=scifi Some clients, such as Axios, will use this convention: - /tables/movie?tag[]=horror&tag[]=scifi + /movie?tag[]=horror&tag[]=scifi This method normalises the parameter name, removing square brackets if present (tag[] -> tag), and will return a list of values if From f42cc90bccaba406f26b5d834424c9102fe4efae Mon Sep 17 00:00:00 2001 From: Daniel Townsend Date: Fri, 7 Oct 2022 14:23:56 +0100 Subject: [PATCH 10/10] document `MalformedQuery` --- piccolo_api/crud/endpoints.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/piccolo_api/crud/endpoints.py b/piccolo_api/crud/endpoints.py index 4cf2afd5..cb4a3b0c 100644 --- a/piccolo_api/crud/endpoints.py +++ b/piccolo_api/crud/endpoints.py @@ -642,6 +642,10 @@ def _apply_filters( Works on any queries which support `where` clauses - Select, Count, Objects etc. + + :raises MalformedQuery: + If the filters reference columns which don't exist. + """ fields = params.fields if fields: