Skip to content

Commit

Permalink
added a fields attribute to schema (#114)
Browse files Browse the repository at this point in the history
* added a fields attribute to schema

* fixing wrong code

* make it work with max_joins

Co-authored-by: Daniel Townsend <[email protected]>
  • Loading branch information
sinisaos and dantownsend authored Dec 9, 2021
1 parent 7bb179f commit ad54533
Show file tree
Hide file tree
Showing 3 changed files with 197 additions and 11 deletions.
52 changes: 50 additions & 2 deletions piccolo_api/crud/endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,45 @@ class Params:
visible_fields: str = field(default="")


def get_visible_fields_options(
table: t.Type[Table],
exclude_secrets: bool = False,
max_joins: int = 0,
prefix: str = "",
) -> t.Tuple[str, ...]:
"""
In the schema, we tell the user which fields are allowed with the
``__visible_fields`` GET parameter. This function extracts the column
names, and names of related columns too.
:param prefix:
Used internally by this function - the user doesn't need to set this.
"""
fields = []

for column in table._meta.columns:
if exclude_secrets and column._meta.secret:
continue

column_name = (
f"{prefix}.{column._meta.name}" if prefix else column._meta.name
)
fields.append(column_name)

if isinstance(column, ForeignKey) and max_joins > 0:
fields.extend(
get_visible_fields_options(
table=column._foreign_key_meta.resolved_references,
exclude_secrets=exclude_secrets,
max_joins=max_joins - 1,
prefix=column_name,
)
)

return tuple(fields)


class PiccoloCRUD(Router):
"""
Wraps a Piccolo table with CRUD methods for use in a REST API.
Expand All @@ -93,7 +132,7 @@ def __init__(
page_size: int = 15,
exclude_secrets: bool = True,
validators: Validators = Validators(),
schema_extra: t.Dict[str, t.Any] = {},
schema_extra: t.Optional[t.Dict[str, t.Any]] = None,
max_joins: int = 0,
) -> None:
"""
Expand Down Expand Up @@ -145,16 +184,25 @@ def __init__(
sensitive nature and the client is highly trusted. Consider using
it with ``exclude_secrets=True``.
To see which fields can be filtered in this way, you can check
the ``visible_fields_options`` value returned by the ``/schema``
endpoint.
""" # noqa: E501
self.table = table
self.page_size = page_size
self.read_only = read_only
self.allow_bulk_delete = allow_bulk_delete
self.exclude_secrets = exclude_secrets
self.validators = validators
self.schema_extra = schema_extra
self.max_joins = max_joins

schema_extra = schema_extra if isinstance(schema_extra, dict) else {}
schema_extra["visible_fields_options"] = get_visible_fields_options(
table=table, exclude_secrets=exclude_secrets, max_joins=max_joins
)
self.schema_extra = schema_extra

root_methods = ["GET"]
if not read_only:
root_methods += (
Expand Down
89 changes: 82 additions & 7 deletions tests/crud/test_crud_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@
from starlette.datastructures import QueryParams
from starlette.testclient import TestClient

from piccolo_api.crud.endpoints import GreaterThan, PiccoloCRUD
from piccolo_api.crud.endpoints import (
GreaterThan,
PiccoloCRUD,
get_visible_fields_options,
)


class Movie(Table):
Expand All @@ -30,6 +34,19 @@ class TopSecret(Table):
confidential = Secret()


class TestGetVisibleFieldsOptions(TestCase):
def test_without_joins(self):
response = get_visible_fields_options(table=Role, max_joins=0)
self.assertEqual(response, ("id", "movie", "name"))

def test_with_joins(self):
response = get_visible_fields_options(table=Role, max_joins=1)
self.assertEqual(
response,
("id", "movie", "movie.id", "movie.name", "movie.rating", "name"),
)


class TestParams(TestCase):
def test_split_params(self):
"""
Expand Down Expand Up @@ -270,10 +287,12 @@ def test_get_references(self):

class TestSchema(TestCase):
def setUp(self):
Movie.create_table(if_not_exists=True).run_sync()
for table in (Movie, Role):
table.create_table(if_not_exists=True).run_sync()

def tearDown(self):
Movie.alter().drop_table().run_sync()
for table in (Role, Movie):
table.alter().drop_table().run_sync()

def test_get_schema(self):
"""
Expand All @@ -284,9 +303,8 @@ def test_get_schema(self):
response = client.get("/schema/")
self.assertTrue(response.status_code == 200)

response_json = response.json()
self.assertEqual(
response_json,
response.json(),
{
"title": "MovieIn",
"type": "object",
Expand All @@ -307,6 +325,11 @@ def test_get_schema(self):
},
"required": ["name"],
"help_text": None,
"visible_fields_options": [
"id",
"name",
"rating",
],
},
)

Expand All @@ -330,9 +353,8 @@ class Rating(Enum):
response = client.get("/schema/")
self.assertTrue(response.status_code == 200)

response_json = response.json()
self.assertEqual(
response_json,
response.json(),
{
"title": "ReviewIn",
"type": "object",
Expand All @@ -356,6 +378,59 @@ class Rating(Enum):
}
},
"help_text": None,
"visible_fields_options": [
"id",
"score",
],
},
)

def test_get_schema_with_joins(self):
"""
Make sure that if a Table has columns with joins specified, they
appear in the schema.
"""
client = TestClient(
PiccoloCRUD(table=Role, read_only=False, max_joins=1)
)

response = client.get("/schema/")
self.assertTrue(response.status_code == 200)

self.assertEqual(
response.json(),
{
"title": "RoleIn",
"type": "object",
"properties": {
"movie": {
"title": "Movie",
"extra": {
"foreign_key": True,
"to": "movie",
"help_text": None,
"choices": None,
},
"nullable": True,
"type": "integer",
},
"name": {
"title": "Name",
"extra": {"help_text": None, "choices": None},
"nullable": False,
"maxLength": 100,
"type": "string",
},
},
"help_text": None,
"visible_fields_options": [
"id",
"movie",
"movie.id",
"movie.name",
"movie.rating",
"name",
],
},
)

Expand Down
67 changes: 65 additions & 2 deletions tests/fastapi/test_fastapi_endpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from unittest import TestCase

from fastapi import FastAPI
from piccolo.columns import Integer, Varchar
from piccolo.columns import ForeignKey, Integer, Varchar
from piccolo.columns.readable import Readable
from piccolo.table import Table
from starlette.testclient import TestClient
Expand All @@ -20,6 +20,11 @@ def get_readable(cls) -> Readable:
return Readable(template="%s", columns=[cls.name])


class Role(Table):
movie = ForeignKey(Movie)
name = Varchar(length=100)


app = FastAPI()


Expand All @@ -31,6 +36,14 @@ def get_readable(cls) -> Readable:
),
)

FastAPIWrapper(
root_url="/roles/",
fastapi_app=app,
piccolo_crud=PiccoloCRUD(
table=Role, read_only=False, allow_bulk_delete=True, max_joins=1
),
)


class TestOpenAPI(TestCase):
def setUp(self):
Expand Down Expand Up @@ -106,6 +119,53 @@ def test_schema(self):
},
},
"help_text": None,
"visible_fields_options": [
"id",
"name",
"rating",
],
},
)

def test_schema_joins(self):
client = TestClient(app)
response = client.get("/roles/schema/")

self.assertEqual(response.status_code, 200)
self.assertEqual(
response.json(),
{
"title": "RoleIn",
"type": "object",
"properties": {
"movie": {
"title": "Movie",
"extra": {
"foreign_key": True,
"to": "movie",
"help_text": None,
"choices": None,
},
"nullable": True,
"type": "integer",
},
"name": {
"title": "Name",
"extra": {"help_text": None, "choices": None},
"nullable": False,
"maxLength": 100,
"type": "string",
},
},
"help_text": None,
"visible_fields_options": [
"id",
"movie",
"movie.id",
"movie.name",
"movie.rating",
"name",
],
},
)

Expand All @@ -128,7 +188,10 @@ def test_references(self):
client = TestClient(app)
response = client.get("/movies/references/")
self.assertEqual(response.status_code, 200)
self.assertEqual(response.json(), {"references": []})
self.assertEqual(
response.json(),
{"references": [{"columnName": "movie", "tableName": "role"}]},
)

def test_delete(self):
client = TestClient(app)
Expand Down

0 comments on commit ad54533

Please sign in to comment.