-
Notifications
You must be signed in to change notification settings - Fork 228
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Co-authored-by: Paul Schweizer <[email protected]> Co-authored-by: Erik Wrede <[email protected]>
- Loading branch information
1 parent
b94230e
commit c927ada
Showing
24 changed files
with
2,635 additions
and
75 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -71,5 +71,8 @@ target/ | |
*.sqlite3 | ||
.vscode | ||
|
||
# Schema | ||
*.gql | ||
|
||
# mypy cache | ||
.mypy_cache/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,213 @@ | ||
======= | ||
Filters | ||
======= | ||
|
||
Starting in graphene-sqlalchemy version 3, the SQLAlchemyConnectionField class implements filtering by default. The query utilizes a ``filter`` keyword to specify a filter class that inherits from ``graphene.InputObjectType``. | ||
|
||
Migrating from graphene-sqlalchemy-filter | ||
--------------------------------------------- | ||
|
||
If like many of us, you have been using |graphene-sqlalchemy-filter|_ to implement filters and would like to use the in-built mechanism here, there are a couple key differences to note. Mainly, in an effort to simplify the generated schema, filter keywords are nested under their respective fields instead of concatenated. For example, the filter partial ``{usernameIn: ["moderator", "cool guy"]}`` would be represented as ``{username: {in: ["moderator", "cool guy"]}}``. | ||
|
||
.. |graphene-sqlalchemy-filter| replace:: ``graphene-sqlalchemy-filter`` | ||
.. _graphene-sqlalchemy-filter: https://github.com/art1415926535/graphene-sqlalchemy-filter | ||
|
||
Further, some of the constructs found in libraries like `DGraph's DQL <https://dgraph.io/docs/query-language/>`_ have been implemented, so if you have created custom implementations for these features, you may want to take a look at the examples below. | ||
|
||
|
||
Example model | ||
------------- | ||
|
||
Take as example a Pet model similar to that in the sorting example. We will use variations on this arrangement for the following examples. | ||
|
||
.. code:: | ||
class Pet(Base): | ||
__tablename__ = 'pets' | ||
id = Column(Integer(), primary_key=True) | ||
name = Column(String(30)) | ||
age = Column(Integer()) | ||
class PetNode(SQLAlchemyObjectType): | ||
class Meta: | ||
model = Pet | ||
class Query(graphene.ObjectType): | ||
allPets = SQLAlchemyConnectionField(PetNode.connection) | ||
Simple filter example | ||
--------------------- | ||
|
||
Filters are defined at the object level through the ``BaseTypeFilter`` class. The ``BaseType`` encompasses both Graphene ``ObjectType``\ s and ``Interface``\ s. Each ``BaseTypeFilter`` instance may define fields via ``FieldFilter`` and relationships via ``RelationshipFilter``. Here's a basic example querying a single field on the Pet model: | ||
|
||
.. code:: | ||
allPets(filter: {name: {eq: "Fido"}}){ | ||
edges { | ||
node { | ||
name | ||
} | ||
} | ||
} | ||
This will return all pets with the name "Fido". | ||
|
||
|
||
Custom filter types | ||
------------------- | ||
|
||
If you'd like to implement custom behavior for filtering a field, you can do so by extending one of the base filter classes in ``graphene_sqlalchemy.filters``. For example, if you'd like to add a ``divisible_by`` keyword to filter the age attribute on the ``Pet`` model, you can do so as follows: | ||
|
||
.. code:: python | ||
class MathFilter(FloatFilter): | ||
class Meta: | ||
graphene_type = graphene.Float | ||
@classmethod | ||
def divisible_by_filter(cls, query, field, val: int) -> bool: | ||
return is_(field % val, 0) | ||
class PetType(SQLAlchemyObjectType): | ||
... | ||
age = ORMField(filter_type=MathFilter) | ||
class Query(graphene.ObjectType): | ||
pets = SQLAlchemyConnectionField(PetType.connection) | ||
Filtering over relationships with RelationshipFilter | ||
---------------------------------------------------- | ||
|
||
When a filter class field refers to another object in a relationship, you may nest filters on relationship object attributes. This happens directly for 1:1 and m:1 relationships and through the ``contains`` and ``containsExactly`` keywords for 1:n and m:n relationships. | ||
|
||
|
||
:1 relationships | ||
^^^^^^^^^^^^^^^^ | ||
|
||
When an object or interface defines a singular relationship, relationship object attributes may be filtered directly like so: | ||
|
||
Take the following SQLAlchemy model definition as an example: | ||
|
||
.. code:: python | ||
class Pet | ||
... | ||
person_id = Column(Integer(), ForeignKey("people.id")) | ||
class Person | ||
... | ||
pets = relationship("Pet", backref="person") | ||
Then, this query will return all pets whose person is named "Ada": | ||
|
||
.. code:: | ||
allPets(filter: { | ||
person: {name: {eq: "Ada"}} | ||
}) { | ||
... | ||
} | ||
:n relationships | ||
^^^^^^^^^^^^^^^^ | ||
|
||
However, for plural relationships, relationship object attributes must be filtered through either ``contains`` or ``containsExactly``: | ||
|
||
Now, using a many-to-many model definition: | ||
|
||
.. code:: python | ||
people_pets_table = sqlalchemy.Table( | ||
"people_pets", | ||
Base.metadata, | ||
Column("person_id", ForeignKey("people.id")), | ||
Column("pet_id", ForeignKey("pets.id")), | ||
) | ||
class Pet | ||
... | ||
class Person | ||
... | ||
pets = relationship("Pet", backref="people") | ||
this query will return all pets which have a person named "Ben" in their ``people`` list. | ||
|
||
.. code:: | ||
allPets(filter: { | ||
people: { | ||
contains: [{name: {eq: "Ben"}}], | ||
} | ||
}) { | ||
... | ||
} | ||
and this one will return all pets which hvae a person list that contains exactly the people "Ada" and "Ben" and no fewer or people with other names. | ||
|
||
.. code:: | ||
allPets(filter: { | ||
articles: { | ||
containsExactly: [ | ||
{name: {eq: "Ada"}}, | ||
{name: {eq: "Ben"}}, | ||
], | ||
} | ||
}) { | ||
... | ||
} | ||
And/Or Logic | ||
------------ | ||
|
||
Filters can also be chained together logically using `and` and `or` keywords nested under `filter`. Clauses are passed directly to `sqlalchemy.and_` and `slqlalchemy.or_`, respectively. To return all pets named "Fido" or "Spot", use: | ||
|
||
|
||
.. code:: | ||
allPets(filter: { | ||
or: [ | ||
{name: {eq: "Fido"}}, | ||
{name: {eq: "Spot"}}, | ||
] | ||
}) { | ||
... | ||
} | ||
And to return all pets that are named "Fido" or are 5 years old and named "Spot", use: | ||
|
||
.. code:: | ||
allPets(filter: { | ||
or: [ | ||
{name: {eq: "Fido"}}, | ||
{ and: [ | ||
{name: {eq: "Spot"}}, | ||
{age: {eq: 5}} | ||
} | ||
] | ||
}) { | ||
... | ||
} | ||
Hybrid Property support | ||
----------------------- | ||
|
||
Filtering over SQLAlchemy `hybrid properties <https://docs.sqlalchemy.org/en/20/orm/extensions/hybrid.html>`_ is fully supported. | ||
|
||
|
||
Reporting feedback and bugs | ||
--------------------------- | ||
|
||
Filtering is a new feature to graphene-sqlalchemy, so please `post an issue on Github <https://github.com/graphql-python/graphene-sqlalchemy/issues>`_ if you run into any problems or have ideas on how to improve the implementation. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,6 +10,7 @@ Contents: | |
inheritance | ||
relay | ||
tips | ||
filters | ||
examples | ||
tutorial | ||
api |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
Example Filters Project | ||
================================ | ||
|
||
This example highlights the ability to filter queries in graphene-sqlalchemy. | ||
|
||
The project contains two models, one named `Department` and another | ||
named `Employee`. | ||
|
||
Getting started | ||
--------------- | ||
|
||
First you'll need to get the source of the project. Do this by cloning the | ||
whole Graphene-SQLAlchemy repository: | ||
|
||
```bash | ||
# Get the example project code | ||
git clone https://github.com/graphql-python/graphene-sqlalchemy.git | ||
cd graphene-sqlalchemy/examples/filters | ||
``` | ||
|
||
It is recommended to create a virtual environment | ||
for this project. We'll do this using | ||
[virtualenv](http://docs.python-guide.org/en/latest/dev/virtualenvs/) | ||
to keep things simple, | ||
but you may also find something like | ||
[virtualenvwrapper](https://virtualenvwrapper.readthedocs.org/en/latest/) | ||
to be useful: | ||
|
||
```bash | ||
# Create a virtualenv in which we can install the dependencies | ||
virtualenv env | ||
source env/bin/activate | ||
``` | ||
|
||
Install our dependencies: | ||
|
||
```bash | ||
pip install -r requirements.txt | ||
``` | ||
|
||
The following command will setup the database, and start the server: | ||
|
||
```bash | ||
python app.py | ||
``` | ||
|
||
Now head over to your favorite GraphQL client, POST to [http://127.0.0.1:5000/graphql](http://127.0.0.1:5000/graphql) and run some queries! |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
from database import init_db | ||
from fastapi import FastAPI | ||
from schema import schema | ||
from starlette_graphene3 import GraphQLApp, make_playground_handler | ||
|
||
|
||
def create_app() -> FastAPI: | ||
init_db() | ||
app = FastAPI() | ||
|
||
app.mount("/graphql", GraphQLApp(schema, on_get=make_playground_handler())) | ||
|
||
return app | ||
|
||
|
||
app = create_app() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
from sqlalchemy import create_engine | ||
from sqlalchemy.ext.declarative import declarative_base | ||
from sqlalchemy.orm import sessionmaker | ||
|
||
Base = declarative_base() | ||
engine = create_engine( | ||
"sqlite://", connect_args={"check_same_thread": False}, echo=True | ||
) | ||
session_factory = sessionmaker(autocommit=False, autoflush=False, bind=engine) | ||
|
||
from sqlalchemy.orm import scoped_session as scoped_session_factory | ||
|
||
scoped_session = scoped_session_factory(session_factory) | ||
|
||
Base.query = scoped_session.query_property() | ||
Base.metadata.bind = engine | ||
|
||
|
||
def init_db(): | ||
from models import Person, Pet, Toy | ||
|
||
Base.metadata.create_all() | ||
scoped_session.execute("PRAGMA foreign_keys=on") | ||
db = scoped_session() | ||
|
||
person1 = Person(name="A") | ||
person2 = Person(name="B") | ||
|
||
pet1 = Pet(name="Spot") | ||
pet2 = Pet(name="Milo") | ||
|
||
toy1 = Toy(name="disc") | ||
toy2 = Toy(name="ball") | ||
|
||
person1.pet = pet1 | ||
person2.pet = pet2 | ||
|
||
pet1.toys.append(toy1) | ||
pet2.toys.append(toy1) | ||
pet2.toys.append(toy2) | ||
|
||
db.add(person1) | ||
db.add(person2) | ||
db.add(pet1) | ||
db.add(pet2) | ||
db.add(toy1) | ||
db.add(toy2) | ||
|
||
db.commit() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
import sqlalchemy | ||
from database import Base | ||
from sqlalchemy import Column, ForeignKey, Integer, String | ||
from sqlalchemy.orm import relationship | ||
|
||
|
||
class Pet(Base): | ||
__tablename__ = "pets" | ||
id = Column(Integer(), primary_key=True) | ||
name = Column(String(30)) | ||
age = Column(Integer()) | ||
person_id = Column(Integer(), ForeignKey("people.id")) | ||
|
||
|
||
class Person(Base): | ||
__tablename__ = "people" | ||
id = Column(Integer(), primary_key=True) | ||
name = Column(String(100)) | ||
pets = relationship("Pet", backref="person") | ||
|
||
|
||
pets_toys_table = sqlalchemy.Table( | ||
"pets_toys", | ||
Base.metadata, | ||
Column("pet_id", ForeignKey("pets.id")), | ||
Column("toy_id", ForeignKey("toys.id")), | ||
) | ||
|
||
|
||
class Toy(Base): | ||
__tablename__ = "toys" | ||
id = Column(Integer(), primary_key=True) | ||
name = Column(String(30)) | ||
pets = relationship("Pet", secondary=pets_toys_table, backref="toys") |
Oops, something went wrong.