Skip to content
Closed
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
80 changes: 70 additions & 10 deletions docs/source/crud/piccolo_crud.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@ methods on your Piccolo table, as well as some extras, via a `REST API <https://
Endpoints
---------

========== ======================= ==========================================================================================================
Path Methods Description
========== ======================= ==========================================================================================================
/ GET, POST, DELETE Get all rows, post a new row, or delete all matching rows.
/<id>/ 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.
/<id>/ 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.
========== ======================== ==========================================================================================================

-------------------------------------------------------------------------------

Expand Down Expand Up @@ -265,6 +265,66 @@ 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 /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 /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 /movie/?name=Star&__ids=1,2

.. warning:: To be able to provide a bulk delete action, we must set
``allow_bulk_delete`` to ``True``.

-------------------------------------------------------------------------------

Bulk update
-----------

To specify which records you want to update in bulk, pass a query parameter
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
the usage is the same.

A query which update movies with ``id`` pass in query parameter:

.. code-block::

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.

.. warning:: To be able to provide a bulk update action, we must set
``allow_bulk_update`` to ``True``.

-------------------------------------------------------------------------------

Readable
--------

Expand Down
104 changes: 87 additions & 17 deletions piccolo_api/crud/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ class Params:
include_readable: bool = False
page: int = 1
page_size: t.Optional[int] = None
ids: str = field(default="")
visible_fields: t.Optional[t.List[Column]] = None
range_header: bool = False
range_header_name: str = field(default="")
Expand Down Expand Up @@ -179,6 +180,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,
Expand All @@ -192,8 +194,11 @@ 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 allow_bulk_update:
If ``True``, allows a update request to the root and update 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:
Expand Down Expand Up @@ -244,6 +249,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
Expand All @@ -267,9 +273,11 @@ def __init__(

root_methods = ["GET"]
if not read_only:
root_methods += (
["POST", "DELETE"] if allow_bulk_delete 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),
Expand Down Expand Up @@ -508,11 +516,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
Expand Down Expand Up @@ -545,6 +553,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("__ids", None)
if request.method == "GET":
params = self._parse_params(request.query_params)
return await self.get_all(request, params=params)
Expand All @@ -553,7 +562,10 @@ 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)
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)

Expand All @@ -579,6 +591,9 @@ def _split_params(self, params: t.Dict[str, t.Any]) -> Params:

And can specify which page: {'__page': 2}.

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:
{'__visible_fields': 'id,name'}.

Expand Down Expand Up @@ -664,6 +679,10 @@ def _split_params(self, params: t.Dict[str, t.Any]) -> Params:
response.page_size = page_size
continue

if key == "__ids":
response.ids = value
continue

if key == "__visible_fields":
column_names: t.List[str]

Expand Down Expand Up @@ -714,6 +733,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:
Expand Down Expand Up @@ -928,23 +951,70 @@ async def post_single(
)

@apply_validators
async def delete_all(
self, request: Request, params: t.Optional[t.Dict[str, t.Any]] = None
async def patch_bulk(
self,
request: Request,
data: t.Dict[str, t.Any],
rows_ids: str,
) -> Response:
"""
Deletes all rows - query parameters are used for filtering.
Bulk update of rows whose primary keys are in the ``__ids``
query param.
"""
params = self._clean_data(params) if params else {}
cleaned_data = self._clean_data(data)

try:
split_params = self._split_params(params)
except ParamException as exception:
model = self.pydantic_model_optional(**cleaned_data)
except Exception as exception:
return Response(str(exception), status_code=400)

try:
query = self._apply_filters(
self.table.delete(force=True), split_params
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
) -> Response:
"""
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: t.Any = 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(
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)

Expand Down
6 changes: 4 additions & 2 deletions piccolo_api/crud/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ 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] = [],
patch_bulk: t.List[ValidatorFunction] = [],
get_references: t.List[ValidatorFunction] = [],
get_ids: t.List[ValidatorFunction] = [],
get_new: t.List[ValidatorFunction] = [],
Expand All @@ -70,7 +71,8 @@ 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.patch_bulk = patch_bulk
self.get_references = get_references
self.get_ids = get_ids
self.get_new = get_new
Expand Down
Loading