Skip to main content

Expose Django models over GraphQL in the Hasura convention — the stock @refinedev/hasura refine data provider drives Strawberry/Django, composing strawberry-django + strawberry-django-aggregates (aggregates for free).

Project description

strawberry-django-hasura

Expose Django models over GraphQL in the Hasura convention, so the stock @refinedev/hasura refine data provider drives a Strawberry/Django backend with no patching.

It is a thin adapter: it composes strawberry-django (types, the ORM seam) and strawberry-django-aggregates and emits the exact GraphQL shape the refine provider speaks. One unmodified frontend data provider, any Django model. The precise target SDL is in CONTRACT.md.

Why

refine's Hasura provider expects a specific GraphQL shape per resource: a notes(where, order_by, limit, offset): [Note!] list, a notes_by_pk(id): Note detail, insert_notes_one / update_notes_by_pk / delete_notes_by_pk mutations, notes_bool_exp operator objects, notes_order_by + the order_by enum, and a notes_aggregate { aggregate, nodes } surface. This library emits all of it from your Strawberry types — you keep the stock provider, no custom mapping layer.

The aggregate is free. Hasura's aggregate { count, sum {…}, avg {…}, min {…}, max {…} } is the native <Model>Aggregate type that strawberry-django-aggregates already emits — so there is no reshape layer (no flat→nested glue to maintain). You wire it; you don't rebuild it.

Install

pip install strawberry-django-hasura
# or
uv add strawberry-django-hasura

Requires Python 3.14+, Django 6.0+, and a Strawberry/strawberry-django stack (installed transitively).

The frontend side — one provider option

Construct the stock provider with idType: "String" and the Hasura naming convention. idType declares the id variable type verbatim ($id: String!), so an opaque string id (a sqid) binds through every pk-centric op without a patch; the refine default is uuid, so this option is required for a string id:

import dataProvider, { GraphQLClient } from "@refinedev/hasura";

const client = new GraphQLClient("https://your.api/graphql");
const dp = dataProvider(client, {
  idType: "String",            // opaque sqid binds as $id: String!
  namingConvention: "hasura-default",
});

Quickstart (backend)

Declare the per-resource Hasura inputs from your Strawberry type, then compose the adapter's helpers in plain resolvers. (Condensed from tests/demo_schema.py / examples/demo_schema.py, which exercise every surface — including the opaque-id (sqid) boundary.)

import strawberry, strawberry_django
from django.db import models, transaction
from strawberry import UNSET, auto

from strawberry_django_hasura import (
    OrderBy, apply_ordering, build_aggregate_type, hasura_config, input_to_dict,
    make_aggregate_container, make_aggregate_resolver, paginate, where_to_q,
)
from strawberry_django_hasura.comparisons import (
    IDComparison, IntComparison, StringComparison,
)
from .models import Note  # your Django model


@strawberry_django.type(Note)
class NoteType:           # GraphQL type name `Note`
    id: auto
    title: auto
    word_count: auto
    status: auto


@strawberry.input(name="notes_bool_exp")
class NoteBoolExp:
    id: IDComparison | None = UNSET
    title: StringComparison | None = UNSET
    word_count: IntComparison | None = UNSET
    status: StringComparison | None = UNSET
    and_: list["NoteBoolExp"] | None = strawberry.field(name="_and", default=UNSET)
    or_: list["NoteBoolExp"] | None = strawberry.field(name="_or", default=UNSET)
    not_: "NoteBoolExp | None" = strawberry.field(name="_not", default=UNSET)


@strawberry.input(name="notes_order_by")
class NoteOrderBy:                 # per-field input of the `order_by` enum
    title: OrderBy | None = UNSET
    word_count: OrderBy | None = UNSET


@strawberry.input(name="notes_pk_columns_input")
class NotePkColumns:
    id: str                        # String (not ID) — matches idType: "String"


def base_qs():
    # Apply your row-level (e.g. REBAC) scoping here — reads run on this.
    return Note.objects.all()


def filtered(info, where):
    return base_qs().filter(where_to_q(where))


# The free aggregate — the native <Model>Aggregate, wired into the container.
NoteAggregate = build_aggregate_type(Note, name="Note",
                                     aggregate_fields=["word_count"])
aggregate_resolver = make_aggregate_resolver(NoteAggregate)
NoteAggregateContainer = make_aggregate_container(
    "notes_aggregate", NoteType, NoteAggregate,
    filtered_queryset=filtered, aggregate_resolver=aggregate_resolver,
)


@strawberry.type
class Query:
    @strawberry.field(name="notes")
    def notes(self, info: strawberry.Info, where: NoteBoolExp | None = None,
              order_by: list[NoteOrderBy] | None = None,
              limit: int | None = None, offset: int | None = None) -> list[NoteType]:
        qs = apply_ordering(filtered(info, where), order_by)
        return list(paginate(qs, limit, offset))

    @strawberry.field(name="notes_aggregate")
    def notes_aggregate(self, where: NoteBoolExp | None = None) -> NoteAggregateContainer:
        return NoteAggregateContainer(where=where)

    @strawberry.field(name="notes_by_pk")
    def notes_by_pk(self, id: str) -> NoteType | None:   # String to match idType
        return base_qs().filter(pk=id).first()


schema = strawberry.Schema(query=Query, config=hasura_config())

insert_notes_one / update_notes_by_pk / delete_notes_by_pk follow the same pattern, using input_to_dict to translate the Hasura object: / _set: envelope into model kwargs. Your <resource>_set_input is the authoritative writable-field allowlist (keep server-owned columns out of it); run the update_by_pk read-modify-write inside transaction.atomic() and save(update_fields=…) so a patch touches only the columns it set — see tests/demo_schema.py.

Opaque ids (sqid)

If your public id is an opaque sqid (not the raw pk), keep the output id: ID! field encoded, type every pk-arg as String (matching idType: "String"), and pass an id_decode hook so where: { id: { _eq } } decodes before the lookup:

qs = base_qs().filter(where_to_q(where, id_decode=decode_sqid))
# ...and decode at notes_by_pk / pk_columns: objects.get(pk=decode_sqid(id))

The encode/decode stays your concern — the adapter never inspects a value to guess whether it is a sqid.

The surfaces

Surface Module What it emits / does
Filtering comparisons, filtering <resource>_bool_exp operator objects → a Django Q
Ordering ordering [<resource>_order_by!] + the order_by enum → .order_by()
Pagination connection bare limit / offset → a queryset slice
Aggregation aggregation, connection the free <resource>_aggregate { aggregate, nodes } — the native <Model>Aggregate, zero reshape
Mutations mutations insert/update/delete-by-pk envelope → model kwargs
Naming naming hasura_config() — snake_case verbatim on the wire

Proof: the stock provider drives it

examples/ is a runnable proof that the unmodified @refinedev/hasura provider drives a schema built with this library (getList filter + sort + paging, getOne, create, update, deleteOne, and the aggregate), no patching — using only the idType: "String" option.

Status

Beta (v0.1.0). The public API (__init__ exports) and the emitted SDL shape follow CONTRACT.md and are stable for early adopters; minor iteration is expected before a 1.0 stability commitment. Runtime: Python 3.14, Django 6.0.

Documentation

License

AGPL-3.0-or-later. See LICENSE.

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

strawberry_django_hasura-0.1.0.tar.gz (33.3 kB view details)

Uploaded Source

Built Distribution

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

strawberry_django_hasura-0.1.0-py3-none-any.whl (28.9 kB view details)

Uploaded Python 3

File details

Details for the file strawberry_django_hasura-0.1.0.tar.gz.

File metadata

  • Download URL: strawberry_django_hasura-0.1.0.tar.gz
  • Upload date:
  • Size: 33.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for strawberry_django_hasura-0.1.0.tar.gz
Algorithm Hash digest
SHA256 b89c8df626dea18bf179a2c10c9c8191026d3fcbcd5fb53dfabd9faccda55cdf
MD5 2e54b563321786415624b255f332cabe
BLAKE2b-256 4009e5cdcd95b01f835f462f350e9240ecd38450dc2e82b6bc8bc3f2789ee5b3

See more details on using hashes here.

File details

Details for the file strawberry_django_hasura-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for strawberry_django_hasura-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 9b9b2987a70ddd3694b3f64395cd57e04c982aef7db73e61d0a4db85f5e354a9
MD5 ff31a53320c433b7278b9ab445b6e34b
BLAKE2b-256 9ecafcbf505d2610c962b9315221be8a514e95ed4d3755c2a2df7fe404d1bc05

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