Skip to main content

Dynamic GraphQL query builder

Project description

About this project

This project emerged from the frustrating experience when trying to write gql queries with dynamic filters. In some cases, think of a UI to beautifully display database contents, one desires to be able to set filters dynamically if some value is provided, yet just ignore it if not.

This behavior was guaranteed by Hasura versions 1.3.3v and below - but disabled by default in later versions, as described here.

The env var HASURA_GRAPHQL_V1_BOOLEAN_NULL_COLLAPSE preserves this functionality globally. However, this as well is not always desired, when a more fine-grained control over when a value is strictly necessary for where conditions, and when not, is of importance.

This project therefore provides a lightweight query builder that takes a simple gql query string as input and inserts the respective clauses if a value is presented, and does nothing, if not. This avoids nasty, error-prone, string concatenations while avoiding heavy-weight ASTs and the necessity of schema declarations as with gql-DSL.

New in v1.2.0

Support for _and & _or operations:

DSL:

result = (
    dynamic_query(query)
    .table('product')
    ._or([
        _and((
            where('name', is_optional=True)
            ._eq('abc'),
            where('price')
            ._gt(10)
        )),
        where('name')
        ._ilike('def'),
        where('brand')
        ._eq('my_favorite_brand'),
        where('tag')
        ._in(['technology', 'houselhold']),
        ]
    )
    .build()
)

Note: With the is_optional flag set to True in a where subquery, it is not included in the generated query-string if the provided value is 'None'.

builder:

    builder.with_where_clause(
        'product',
        (
            ('name', 'price'),
            'name',
            'brand',
            'tag'
        ),
        (('abc', 10), 'def', 'my_favorite_brand', ['technology', 'houselhold']),
        (('_eq', '_gt'), '_ilike', '_eq', '_in')),
        skip_if_none=((True, False), False, False, False)
        wrap_in_or=True,
    )

The builder works by alternatingly apply _or & _and operations based on the tuple structure, beginning with _or. E.g. (field_1, (field_2, (field_3, field_4)) results in a clause like:

_or {[
    field_1 ...
    _and {[
        field_2 ...
        _or {[
            field_3 ...
            field_4 ...
        ]}
    ]}
    ]}

This is a working system, yet too unreadable for production code. Therefore, it is highly encouraged to use the DSL for complicated queries.

Usage

All a user should be interacting with is located in the src.api package. As of now it only contains the GQLDynamicQueryBuilder which is the heart of the project and provides all the necessary functionality, while api.dsl provides a more user-friendly and convenient wrapper around the builder, as described here. To extend a query by an optional where clause one can do the following:

    query = """
        query TestQuery {
            product {
                name
                brand
            }
        }
    """
    
    builder = GQLDynamicQueryBuilder(query)
    builder = builder.with_where_clause(
                table_name='product', 
                field_name='name', 
                value='tomato', 
                operation='_ilike',
                skip_if_none=True
              )
    result = builder.build() # returns the transformed query as a string

This also works for queries with existing where clauses and other query parameters (e.g. limit). To access nested fields, simply use '.' as the delimiter:

    builder.with_where_clause(
        'product', 
        'brand.name', # will build brand { name: ...
        'ABC', 
        '_eq',
        skip_if_none=True
    )
    result = builder.build()

Furthermore, it is also possible to provide the following to with_where_clause:

  • values: list and operation: str which allows for set-operations like _in
  • values: list and operation: list[str] which allows for multiple operations for the same field (e.g. timestamp {_gte: <ts1> _lt: <ts2>})

As a fallback also explicit where clauses are supported via table_name: clause dictionaries:

    builder.with_where_clauses(
        {'product': 'name: {_ilike : "tomato"}'}
    )
    result = builder.build()

DSL

Since version 1.1.0 a more user-friendly DSL is provided under api.dsl. The DSL is a wrapper around the builder pattern that enables a continuous flow of information, such as the table name for which a filter is to be set. E.g. look at this builder-based query:

    result = (GQLDynamicQueryBuilder(query)
                  .with_where_clause('product', 'name', ['a', 'b', 'c'], '_in', skip_if_none=True)
                  .with_offset('product', 15)
                  .with_limit('product', 25)
                  .with_where_clause('product', 'price', 5, '_gte')
                  ).build()

All function calls in the above examples return a GQLDynamicQueryBuilder object, whose state is modified along the way. Thereby, the table name is repeated 4 times and the code is only marginally readable. Using the DSL on the other hand the same functionality can be achieved through the following:

    result = (dynamic_query(query).table('subquery_to_test')
                  .where('test', is_optional=True)
                  ._in(['a', 'b', 'c'])
                  .offset(15)
                  .limit(25)
                  .where('test2')
                  ._gte(5)
                  .build()
                  )

This way the query construction becomes much more readable and concise. Furthermore, the method chaining is guarded by the type system, i.e. there is no need to worry to e.g. chain build() to a where() as the dsl is designed to only allow cohesive queries.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

gql_dynamic_query_builder-1.2.0.tar.gz (12.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

gql_dynamic_query_builder-1.2.0-py3-none-any.whl (14.2 kB view details)

Uploaded Python 3

File details

Details for the file gql_dynamic_query_builder-1.2.0.tar.gz.

File metadata

File hashes

Hashes for gql_dynamic_query_builder-1.2.0.tar.gz
Algorithm Hash digest
SHA256 1e77fccc2538e36bcc64fb59b894f68368e005f6aac9ed9951a216b301e9f5f6
MD5 09e9e0f951dfa35843b84df0b02d7c05
BLAKE2b-256 336310291a5588501106af00006a3a8a04fcc313eeb4536b8859832acb6faa9e

See more details on using hashes here.

File details

Details for the file gql_dynamic_query_builder-1.2.0-py3-none-any.whl.

File metadata

File hashes

Hashes for gql_dynamic_query_builder-1.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 21efd5cd04784c79ccb09f3f66d48edbeb405b3a791cc21c7cff9cf6c4c13c5c
MD5 148e6844aa758743ba13749ae6871aec
BLAKE2b-256 90d2a470475f4002a2481e58ce24607eb840d4089201a4221549ce870a6b03b4

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page