strawberry-graphql / strawberry

A GraphQL library for Python that leverages type annotations 🍓
https://strawberry.rocks
MIT License
4.01k stars 533 forks source link

Private input args are exposed #2676

Open hcharley opened 1 year ago

hcharley commented 1 year ago

Describe the Bug

    @strawberry.field(permission_classes=[IsAuthenticated])
    def movie(
            self,
            info: Info,
            my_private_field: strawberry.Private[str] = "this is private"
    ) -> Movie:
        ...

^ This code will still expose myPrivateField to the GraphQL schema. It should not expose it.

System Information

Additional Context

Upvote & Fund

Fund with Polar

patrick91 commented 1 year ago

Hi @hcharley! This is expected, but i think we can add a feature to support private arguments :)

What's your use case for them?

ThirVondukr commented 1 year ago

In this case my_private_field has a static value (so it could be replaced with something else, for example with a constant), are you using some kind of decorator to do something with that field?

hcharley commented 1 year ago

My usecase is that we're calling a query internally from inside another query, but we need an argument that can be changed so that the info selector's name aligns with the calling query's name.


@strawberry.type
class ResourceQuery:
    @strawberry.field(permission_classes=[IsAuthenticated])
    def resources(
            self, info: Info,
            filters: Optional[ResourceFilterInput] = None,
            pagination: Optional[PaginationRequest] = None,
            sort: Optional[PaginationSort] = None,
            selector_field_override: strawberry.Private[str] = "resources"
    ) -> Edges[Resource]:

        filters = filters if filters else ResourceFilterInput()
        resource_search_map = get_input_map(filters, remove_null_values=True)
        if "resource_uuid" in resource_search_map:
            resource_search_map["uuid"] = (
                resource_search_map.pop("resource_uuid")
            )

        results = info.orm.get_resources(
            name="res",
            selector=selector_field_override,
            relationship_args=[],
        )

        # ...

        page_info = PageInfo(total_results=results.total, pagination=pagination)
        edges = Edges(edges=results.items, page_info=page_info)
        return edges

    @strawberry.field(permission_classes=[IsAuthenticated])
    def resource(
            self, info: Info, uuid: str
    ) -> Optional[Resource]:
        resources = ResourceQuery.resources(
            self=self,
            info=info,
            filters=ResourceFilterInput(
                resource_uuid=uuid
            ),
            pagination=PaginationRequest(
                first=1,
                after=0,
            ),
            selector_field_override="resource"
        )

        if len(resources.edges) == 0:
            return None

        return resources.edges[0].node
kkjot88 commented 1 year ago

Hi @hcharley! This is expected, but i think we can add a feature to support private arguments :)

What's your use case for them?

In my case I would love to have the ability to mark arguments as private (excluded from schema completely) for dependency injection purposes. So I can do something like this using the dependcy-injector library:

@strawberry.type
class Query:
    @strawberry.field
    @inject
    def get_books(
        self,
        author: str,
        search_service: strawberry.Private[SearchService] = Closing[
            Provide[Container.service]
        ],
    ) -> list[Book]:
        return search_service.get_by_author(author)

Technically it is currently possible with code like this:

@strawberry.type
class Query:
    @strawberry.field
    @inject
    def get_books(
        self,
        author: str,
        search_service: str = Closing[Provide[Container.service]],
    ) -> list[Book]:
        return search_service.get_by_author(author)

I can't type hint the search_service arg as SearchService because of "Unexpected type '<class 'main.SearchService'>'". It has to be type hinted as some strawberry scalar eg. str. It is being visible as valid argument from docs/graphiql etc. and can be passed with query, overriding the injected service.

I've managed to hack it but it requires external boilerplate code which other team members would have to know about. I would rather avoid that and use only strawberries' native code.

class GetBooks:
    def __new__(cls, *args, **kwargs):
        self = super().__new__(cls)
        signature = inspect.signature(self.__call__.__func__.func_dict["__wrapped__"])
        args_to_ignore = []
        for k, v in signature.parameters.items():
            if isinstance(v.default, _Marker):
                args_to_ignore.append(ReservedName(k))
        self.__annotations__ = {**self.__call__.__annotations__}
        self = StrawberryResolver(self)
        self.RESERVED_PARAMSPEC = self.RESERVED_PARAMSPEC + tuple(args_to_ignore)
        self = strawberry.field(self)
        return self

    @inject
    @staticmethod
    def __call__(
        self,
        author: str,
        search_service: SearchService = Closing[Provide[Container.service]],
    ) -> list[Book]:
        return search_service.get_by_author(author)

@strawberry.type
class Query:
    get_books = GetBooks()