igorbenav / fastcrud

FastCRUD is a Python package for FastAPI, offering robust async CRUD operations and flexible endpoint creation utilities.
MIT License
643 stars 45 forks source link

Add Nested Routes Support and Documentation #20

Open igorbenav opened 7 months ago

igorbenav commented 7 months ago

Describe the bug or question Handling something like this scenario:

router = APIRouter(prefix="/user/{user_id}")

@router.get("/posts/{post_id}")
def get_post(user_id: int, post_id: int):
    ...
    return {"user": user_id, "post:": post_id, "text": post_text}
curl localhost:8000/user/1/posts/2

{
  "user": 1,
  "post:": 2
  "text": "sample text"
}
JakNowy commented 5 months ago

I tried addressing this. After short research I thought that something like this would work out of the box:

class UserCRUD(fastcrud.FastCRUD):
    async def read(self, db: AsyncSession, root_param: int, read_param: int) -> Any:
        return {
            'root_param': root_param,
            'read_param': read_param,
        }

user_router = crud_router(
    crud=UserCRUD(User),
    model=User,
    session=deps.get_async_db,
    create_schema=UserIn,
    update_schema=UserUpdate,
    endpoint_names={'read': 'read/{read_param}'}
)

my_router = APIRouter()
my_router.include_router(user_router, prefix='root_router/{root_param}')

It turns out, that while the route gets properly built, the path params are not resolved dynamically as integers (they are parsed as raw path strings). Only default id gets recognized as a path param. I can see they are all gettin passed in

            self.router.add_api_route(
                f"{self.path}/{endpoint_name}/{_primary_keys_path_suffix}",
                ...
            )

but not sure what magic makes the _primary_keys_path_suffix resolve the params but not endpoint_name. Any thoughts on that? Is that approach ok or we want something else?

image

(note I have one more layer of /user/ nesting in my project but that's irrelevant here)

JakNowy commented 5 months ago

@igorbenav

igorbenav commented 5 months ago

I'll take a proper look at it later today

igorbenav commented 5 months ago

Sorry, had a really long day. I'll try to do it on friday

JakNowy commented 3 months ago

I have tested that even further. Turns out it's just swagger docs not fully recognizing the nesting, but the API works flawlessly out of the box!!!

class MyEndpointCreator(EndpointCreator):
    def _read_item(self):
        """Creates an endpoint for reading a single item from the database."""

        @apply_model_pk(**self._primary_keys_types)
        async def endpoint(root_param, read_param, db: AsyncSession = Depends(self.session), **pkeys,):
            return {
                'root_param': root_param,
                'read_param': read_param,
                'pkes': pkeys,
            }

        return endpoint

user_router = crud_router(
    crud=UserCRUD(User),
    model=User,
    session=deps.get_async_db,
    create_schema=UserIn,
    update_schema=UserYoutubeTOS,
    endpoint_names={'read': 'read/{read_param}'},
    endpoint_creator=MyEndpointCreator,
    path='test/{root_param}',  # this solves even swagger
)

my_router = APIRouter()
my_router.include_router(user_router)

image

UPDATE: passing path to crud_router instead of prefix to my_router.include_router() makes it also work on swagger!

JakNowy commented 3 months ago

@igorbenav

igorbenav commented 3 months ago

That's great!