Skip to main content

A flexible query language for Django - enable frontends to dynamically construct database queries

Project description

Django-Flex

A flexible query language for Django — let your frontend dynamically construct database queries

PyPI version Python versions License


Django-Flex enables frontends to send flexible, dynamic queries to your Django backend — think of it as a simpler alternative to GraphQL that feels native to Django.

Features

  • 🎯 Field Selection — Request only the fields you need, including nested relations
  • 🔍 Dynamic Filtering — Full Django ORM operator support with composable AND/OR/NOT
  • 📄 Smart Pagination — Limit/offset with cursor-based continuation
  • 🔒 Built-in Security — Row-level, field-level, and operation-level permissions
  • Automatic Optimization — N+1 prevention with smart select_related
  • 🐍 Django-Native — Feels like a natural extension of Django

Installation

pip install django-flex

Add to your Django settings:

# settings.py
INSTALLED_APPS = [
    ...
    'django_flex',
]

# Optional: Configure permissions and defaults
DJANGO_FLEX = {
    'DEFAULT_LIMIT': 50,
    'MAX_LIMIT': 200,
    'PERMISSIONS': {
        # See Permission Configuration below
    },
}

Quick Start

1. Class-Based View (Recommended)

# views.py
from django_flex import FlexQueryView
from myapp.models import Booking

class BookingQueryView(FlexQueryView):
    model = Booking
    
    # Define permissions for this view
    flex_permissions = {
        'authenticated': {
            'rows': lambda user: Q(team__members=user),
            'fields': ['id', 'status', 'customer.name', 'customer.email'],
            'filters': ['status', 'status.in', 'customer.name.icontains'],
            'order_by': ['created_at', '-created_at'],
            'ops': ['get', 'query'],
        },
    }
# urls.py
from django.urls import path
from myapp.views import BookingQueryView

urlpatterns = [
    path('api/bookings/', BookingQueryView.as_view()),
]

2. Make Queries from Frontend

// List bookings with field selection and filtering
const response = await fetch('/api/bookings/', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify({
        fields: 'id, status, customer.name, customer.email',
        filters: {
            'status.in': ['confirmed', 'completed'],
            'customer.name.icontains': 'khan'
        },
        order_by: '-created_at',
        limit: 20
    })
});

const data = await response.json();
// {
//     "success": true,
//     "code": "FLEX_OK_QUERY",
//     "pagination": {"offset": 0, "limit": 20, "has_more": true},
//     "results": {
//         "1": {"id": 1, "status": "confirmed", "customer": {"name": "Aisha Khan", "email": "aisha@example.com"}},
//         "2": {"id": 2, "status": "completed", "customer": {"name": "Omar Khan", "email": "omar@example.com"}}
//     }
// }
// Get single object by ID
const booking = await fetch('/api/bookings/', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify({
        id: 1,
        fields: 'id, status, customer.*, address.*'
    })
});

Query Language Reference

Field Selection

// All fields on the model
{ fields: '*' }

// Specific fields
{ fields: 'id, name, email' }

// Nested relation fields (dot notation)
{ fields: 'id, customer.name, customer.email' }

// Relation wildcards
{ fields: 'id, status, customer.*, address.*' }

Filtering

// Simple equality
{ filters: { status: 'confirmed' } }

// With operators
{ filters: { 'price.gte': 100, 'price.lte': 500 } }

// Text search
{ filters: { 'name.icontains': 'khan' } }

// List membership
{ filters: { 'status.in': ['pending', 'confirmed', 'completed'] } }

// OR conditions
{ filters: { or: { status: 'pending', 'customer.vip': true } } }

// NOT conditions  
{ filters: { not: { status: 'cancelled' } } }

// Complex composition
{
    filters: {
        'created_at.gte': '2024-01-01',
        or: [
            { status: 'confirmed' },
            { and: { status: 'pending', 'urgent': true } }
        ]
    }
}

Supported Operators:

Category Operators
Comparison lt, lte, gt, gte, exact, iexact, in, isnull, range
Text contains, icontains, startswith, istartswith, endswith, iendswith, regex, iregex
Date/Time date, year, month, day, week_day, hour, minute, second

Pagination

{
    limit: 20,      // Number of results (default: 50, max: 200)
    offset: 0,      // Starting position
    order_by: '-created_at'  // Sort order (prefix with - for descending)
}

Response includes pagination info:

{
    "pagination": {
        "offset": 0,
        "limit": 20,
        "has_more": true,
        "next": {
            "fields": "...",
            "filters": {...},
            "limit": 20,
            "offset": 20
        }
    }
}

Permission Configuration

Django-Flex uses a deny-by-default security model. You must explicitly grant access.

# settings.py
DJANGO_FLEX = {
    'PERMISSIONS': {
        'booking': {
            # Fields excluded from wildcard expansion (security)
            'exclude': ['internal_notes', 'stripe_payment_id'],
            
            # Role-based permissions
            'owner': {
                # Row-level: which rows can this role see?
                'rows': lambda user: Q(created_by=user),
                
                # Field-level: which fields can they access?
                'fields': ['*', 'customer.*', 'address.*'],
                
                # Filter-level: which fields can they filter on?
                'filters': [
                    'id', 'status', 'status.in',
                    'customer.name', 'customer.name.icontains',
                    'created_at.gte', 'created_at.lte',
                ],
                
                # Order-level: which fields can they sort by?
                'order_by': ['id', '-id', 'created_at', '-created_at', 'customer.name'],
                
                # Operation-level: which actions can they perform?
                'ops': ['get', 'query', 'create', 'update', 'delete'],
            },
            
            'staff': {
                'rows': lambda user: Q(team__members=user),
                'fields': ['id', 'status', 'customer.name', 'address.city'],
                'filters': ['status', 'status.in'],
                'order_by': ['created_at', '-created_at'],
                'ops': ['get', 'query'],
            },
            
            # Roles not listed have NO ACCESS
        },
    },
}

Custom Role Resolution

Django-Flex uses Django's built-in groups for role resolution:

from django_flex import FlexPermission

class MyPermission(FlexPermission):
    def get_user_role(self, user):
        if user.is_superuser:
            return 'superuser'
        if user.groups.filter(name='Managers').exists():
            return 'manager'
        return 'staff'

Usage Patterns

1. Class-Based View (Recommended)

from django_flex import FlexQueryView

class BookingQueryView(FlexQueryView):
    model = Booking
    require_auth = True
    allowed_actions = ['get', 'query']
    flex_permissions = {...}

2. Function Decorator

from django_flex import flex_query
from django.http import JsonResponse

@flex_query(
    model=Booking,
    allowed_fields=['id', 'status', 'customer.name'],
    allowed_filters=['status', 'status.in'],
)
def booking_list(request, result, query_spec):
    return JsonResponse(result.to_dict())

3. Programmatic Usage

from django_flex import FlexQuery

def my_view(request):
    result = FlexQuery(Booking).execute({
        'fields': 'id, customer.name',
        'filters': {'status': 'confirmed'},
        'limit': 20,
    }, user=request.user)
    
    return JsonResponse(result.to_dict())

4. Middleware (Single Endpoint)

# settings.py
MIDDLEWARE = [
    ...
    'django_flex.middleware.FlexQueryMiddleware',
]

DJANGO_FLEX = {
    'MIDDLEWARE_PATH': '/api/flex/',
    ...
}

Then query any configured model:

fetch('/api/flex/', {
    method: 'POST',
    body: JSON.stringify({
        _model: 'booking',
        _action: 'query',
        fields: 'id, status',
        limit: 20
    })
});

Configuration Reference

DJANGO_FLEX = {
    # Pagination
    'DEFAULT_LIMIT': 50,        # Default page size
    'MAX_LIMIT': 200,           # Maximum page size (hard cap)
    
    # Security
    'MAX_RELATION_DEPTH': 2,    # Max depth for nested fields/filters
    'REQUIRE_AUTHENTICATION': True,  # Require auth by default
    'AUDIT_QUERIES': False,     # Log all queries (for debugging)
    
    # Middleware
    'MIDDLEWARE_PATH': '/api/flex/',  # Path for middleware endpoint
    
    # Model permissions
    'PERMISSIONS': {...},
    
    # Response codes (customizable)
    'RESPONSE_CODES': {
        'OK': 'FLEX_OK',
        'OK_LIST': 'FLEX_OK_QUERY',
        'LIMIT_CLAMPED': 'FLEX_LIMIT_CLAMPED',
        'NOT_FOUND': 'FLEX_NOT_FOUND',
        'MODEL_NOT_FOUND': 'FLEX_MODEL_NOT_FOUND',
        'PERMISSION_DENIED': 'FLEX_PERMISSION_DENIED',
        'INVALID_FIELD': 'FLEX_INVALID_FIELD',
        'INVALID_FILTER': 'FLEX_INVALID_FILTER',
    },
}

Response Format

Successful Single Object (get)

{
    "success": true,
    "code": "FLEX_OK",
    "id": 1,
    "status": "confirmed",
    "customer": {
        "name": "Aisha Khan",
        "email": "aisha@example.com"
    }
}

Successful Query (query)

{
    "success": true,
    "code": "FLEX_OK_QUERY",
    "pagination": {
        "offset": 0,
        "limit": 20,
        "has_more": true,
        "next": {...}
    },
    "results": {
        "1": {...},
        "2": {...}
    }
}

Error Response

{
    "success": false,
    "code": "FLEX_PERMISSION_DENIED",
    "error": "Access denied: field 'secret_field' not accessible"
}

Why Django-Flex?

Feature Django-Flex GraphQL REST
Learning curve Low (Django-native) High Low
Field selection ❌ (fixed endpoints)
Dynamic filtering Limited
Built-in security Manual Manual
Django integration Native Requires graphene Native
Schema definition Optional Required N/A
N+1 prevention Automatic Manual Manual

Contributing

Contributions are welcome! Please see CONTRIBUTING.md for guidelines.

License

MIT License — see LICENSE for details.

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

django_flex-26.1.3.tar.gz (38.6 kB view details)

Uploaded Source

Built Distribution

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

django_flex-26.1.3-py3-none-any.whl (25.5 kB view details)

Uploaded Python 3

File details

Details for the file django_flex-26.1.3.tar.gz.

File metadata

  • Download URL: django_flex-26.1.3.tar.gz
  • Upload date:
  • Size: 38.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.9.6

File hashes

Hashes for django_flex-26.1.3.tar.gz
Algorithm Hash digest
SHA256 6d6ae79dae31bc9107ffc8a8c7772a8bf5bc74b6bc6aa4eef29780382c97d62c
MD5 00edd9d9b9397652c81d775f711c4c6d
BLAKE2b-256 c6c25b0f8fc35c66c710dcbf165c6264d285b2d2cf1c1e8bc828801f2c14a586

See more details on using hashes here.

File details

Details for the file django_flex-26.1.3-py3-none-any.whl.

File metadata

  • Download URL: django_flex-26.1.3-py3-none-any.whl
  • Upload date:
  • Size: 25.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.9.6

File hashes

Hashes for django_flex-26.1.3-py3-none-any.whl
Algorithm Hash digest
SHA256 7451c793bdf3b0e0d11867bebcb69ba98378a60ebe680999a37310d7a22e5ae3
MD5 98328e413012983c4a57dd76a2b329c3
BLAKE2b-256 b6442951bd58325e25c339944f9b82275fddb959f97e19466baf3bb8d362d205

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