django-flex 26.1.8__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_flex-26.1.8/CHANGELOG.md +42 -0
- django_flex-26.1.8/LICENSE +21 -0
- django_flex-26.1.8/MANIFEST.in +7 -0
- django_flex-26.1.8/PKG-INFO +572 -0
- django_flex-26.1.8/README.md +525 -0
- django_flex-26.1.8/django_flex/__init__.py +92 -0
- django_flex-26.1.8/django_flex/conf.py +101 -0
- django_flex-26.1.8/django_flex/decorators.py +172 -0
- django_flex-26.1.8/django_flex/fields.py +360 -0
- django_flex-26.1.8/django_flex/filters.py +207 -0
- django_flex-26.1.8/django_flex/middleware.py +378 -0
- django_flex-26.1.8/django_flex/permissions.py +494 -0
- django_flex-26.1.8/django_flex/query.py +414 -0
- django_flex-26.1.8/django_flex/ratelimit.py +188 -0
- django_flex-26.1.8/django_flex/response.py +338 -0
- django_flex-26.1.8/django_flex/tests/__init__.py +1 -0
- django_flex-26.1.8/django_flex/tests/conftest.py +43 -0
- django_flex-26.1.8/django_flex/tests/test_fields.py +79 -0
- django_flex-26.1.8/django_flex/tests/test_filters.py +168 -0
- django_flex-26.1.8/django_flex/tests/test_jsonfield.py +142 -0
- django_flex-26.1.8/django_flex/tests/test_middleware.py +244 -0
- django_flex-26.1.8/django_flex/tests/test_permissions.py +129 -0
- django_flex-26.1.8/django_flex/tests/test_query_crud.py +624 -0
- django_flex-26.1.8/django_flex/tests/test_ratelimit.py +242 -0
- django_flex-26.1.8/django_flex/tests/test_response.py +304 -0
- django_flex-26.1.8/django_flex/tests/test_role_resolver.py +336 -0
- django_flex-26.1.8/django_flex/tests/test_security.py +160 -0
- django_flex-26.1.8/django_flex/tests/test_security_h1.py +183 -0
- django_flex-26.1.8/django_flex/tests/test_security_h2.py +239 -0
- django_flex-26.1.8/django_flex/tests/test_security_h3.py +321 -0
- django_flex-26.1.8/django_flex/tests/test_security_h4.py +230 -0
- django_flex-26.1.8/django_flex/tests/test_security_h5.py +242 -0
- django_flex-26.1.8/django_flex/tests/test_security_medium.py +369 -0
- django_flex-26.1.8/django_flex/tests/test_utils.py +201 -0
- django_flex-26.1.8/django_flex/views.py +254 -0
- django_flex-26.1.8/django_flex.egg-info/PKG-INFO +572 -0
- django_flex-26.1.8/django_flex.egg-info/SOURCES.txt +51 -0
- django_flex-26.1.8/django_flex.egg-info/dependency_links.txt +1 -0
- django_flex-26.1.8/django_flex.egg-info/requires.txt +11 -0
- django_flex-26.1.8/django_flex.egg-info/top_level.txt +1 -0
- django_flex-26.1.8/docs/api_reference.md +411 -0
- django_flex-26.1.8/docs/examples/basic_usage.md +388 -0
- django_flex-26.1.8/docs/examples/filtering.md +411 -0
- django_flex-26.1.8/docs/installation.md +86 -0
- django_flex-26.1.8/docs/permissions.md +365 -0
- django_flex-26.1.8/docs/quickstart.md +236 -0
- django_flex-26.1.8/pyproject.toml +111 -0
- django_flex-26.1.8/setup.cfg +4 -0
- django_flex-26.1.8/tests/test_fields.py +79 -0
- django_flex-26.1.8/tests/test_filters.py +168 -0
- django_flex-26.1.8/tests/test_permissions.py +77 -0
- django_flex-26.1.8/tests/test_ratelimit.py +239 -0
- django_flex-26.1.8/tests/test_response.py +94 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2024-01-10
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Initial release of django-flex
|
|
13
|
+
- Core query engine with field selection and filtering
|
|
14
|
+
- Permission system with row-level, field-level, and operation-level access control
|
|
15
|
+
- `FlexQueryView` class-based view for easy integration
|
|
16
|
+
- `flex_query` decorator for function-based views
|
|
17
|
+
- Optional middleware for centralized endpoint
|
|
18
|
+
- Comprehensive documentation and examples
|
|
19
|
+
- Support for Django 3.2, 4.0, 4.1, 4.2, 5.0, and 6.0
|
|
20
|
+
- Support for Python 3.8, 3.9, 3.10, 3.11, 3.12, and 3.14
|
|
21
|
+
|
|
22
|
+
### CRUD Action Names
|
|
23
|
+
|
|
24
|
+
- `get` - Retrieve single object by ID
|
|
25
|
+
- `query` - Query multiple objects with filters/pagination
|
|
26
|
+
- `create` - Create new objects
|
|
27
|
+
- `update` - Update existing objects
|
|
28
|
+
- `delete` - Delete objects
|
|
29
|
+
|
|
30
|
+
### Features
|
|
31
|
+
|
|
32
|
+
- **Field Selection**: Use comma-separated field strings with dot notation for relations
|
|
33
|
+
- Wildcards: `*` for all fields, `customer.*` for all customer fields
|
|
34
|
+
- Nested relations: `customer.address.city`
|
|
35
|
+
- **Filtering**: Full Django ORM operator support
|
|
36
|
+
- Comparison: `lt`, `lte`, `gt`, `gte`, `exact`, `in`, `isnull`, `range`
|
|
37
|
+
- Text: `contains`, `icontains`, `startswith`, `endswith`, `regex`
|
|
38
|
+
- Date/Time: `date`, `year`, `month`, `day`, `hour`, `minute`, `second`
|
|
39
|
+
- Composable: `and`, `or`, `not` for complex conditions
|
|
40
|
+
- **Pagination**: Limit/offset with smart cursor-based continuation
|
|
41
|
+
- **Security**: Principle of least privilege with deny-by-default
|
|
42
|
+
- **Performance**: Automatic `select_related` for N+1 prevention
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Nehemiah Jacob
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: django-flex
|
|
3
|
+
Version: 26.1.8
|
|
4
|
+
Summary: A flexible query language for Django. Enable the frontend to fetch exactly what it needs
|
|
5
|
+
Author: Nehemiah Jacob
|
|
6
|
+
Maintainer: Nehemiah Jacob
|
|
7
|
+
License: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/n3h3m/django-flex
|
|
9
|
+
Project-URL: Documentation, https://github.com/n3h3m/django-flex#readme
|
|
10
|
+
Project-URL: Repository, https://github.com/n3h3m/django-flex.git
|
|
11
|
+
Project-URL: Issues, https://github.com/n3h3m/django-flex/issues
|
|
12
|
+
Project-URL: Changelog, https://github.com/n3h3m/django-flex/blob/main/CHANGELOG.md
|
|
13
|
+
Keywords: django,query,api,flexible,dynamic,graphql-alternative,rest,orm
|
|
14
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
15
|
+
Classifier: Environment :: Web Environment
|
|
16
|
+
Classifier: Framework :: Django
|
|
17
|
+
Classifier: Framework :: Django :: 3.2
|
|
18
|
+
Classifier: Framework :: Django :: 4.0
|
|
19
|
+
Classifier: Framework :: Django :: 4.1
|
|
20
|
+
Classifier: Framework :: Django :: 4.2
|
|
21
|
+
Classifier: Framework :: Django :: 5.0
|
|
22
|
+
Classifier: Intended Audience :: Developers
|
|
23
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
24
|
+
Classifier: Operating System :: OS Independent
|
|
25
|
+
Classifier: Programming Language :: Python :: 3
|
|
26
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
27
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
28
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
29
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
30
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
31
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
32
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
33
|
+
Requires-Python: >=3.8
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
License-File: LICENSE
|
|
36
|
+
Requires-Dist: django>=3.2
|
|
37
|
+
Provides-Extra: dev
|
|
38
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
39
|
+
Requires-Dist: pytest-django>=4.5; extra == "dev"
|
|
40
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
41
|
+
Requires-Dist: black>=23.0; extra == "dev"
|
|
42
|
+
Requires-Dist: isort>=5.12; extra == "dev"
|
|
43
|
+
Requires-Dist: flake8>=6.0; extra == "dev"
|
|
44
|
+
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
45
|
+
Requires-Dist: django-stubs>=4.0; extra == "dev"
|
|
46
|
+
Dynamic: license-file
|
|
47
|
+
|
|
48
|
+
# Django-Flex
|
|
49
|
+
|
|
50
|
+
<p align="center">
|
|
51
|
+
<em>A flexible query language for Django — let your frontend dynamically construct database queries</em>
|
|
52
|
+
</p>
|
|
53
|
+
|
|
54
|
+
<p align="center">
|
|
55
|
+
<a href="https://pypi.org/project/django-flex/">
|
|
56
|
+
<img src="https://img.shields.io/pypi/v/django-flex.svg" alt="PyPI version">
|
|
57
|
+
</a>
|
|
58
|
+
<a href="https://pypi.org/project/django-flex/">
|
|
59
|
+
<img src="https://img.shields.io/pypi/pyversions/django-flex.svg" alt="Python versions">
|
|
60
|
+
</a>
|
|
61
|
+
<a href="https://github.com/your-org/django-flex/blob/main/LICENSE">
|
|
62
|
+
<img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License">
|
|
63
|
+
</a>
|
|
64
|
+
</p>
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
**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.
|
|
69
|
+
|
|
70
|
+
## Features
|
|
71
|
+
|
|
72
|
+
- 🎯 **Field Selection** — Request only the fields you need, including nested relations
|
|
73
|
+
- 🔍 **Dynamic Filtering** — Full Django ORM operator support with composable AND/OR/NOT
|
|
74
|
+
- 📄 **Smart Pagination** — Limit/offset with cursor-based continuation
|
|
75
|
+
- 🔒 **Built-in Security** — Row-level, field-level, and operation-level permissions
|
|
76
|
+
- ⚡ **Automatic Optimization** — N+1 prevention with smart `select_related`
|
|
77
|
+
- 🐍 **Django-Native** — Feels like a natural extension of Django
|
|
78
|
+
|
|
79
|
+
## Installation
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
pip install django-flex
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Add to your Django settings:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
# settings.py
|
|
89
|
+
INSTALLED_APPS = [
|
|
90
|
+
...
|
|
91
|
+
'django_flex',
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
# Optional: Configure permissions and defaults
|
|
95
|
+
DJANGO_FLEX = {
|
|
96
|
+
'DEFAULT_LIMIT': 50,
|
|
97
|
+
'MAX_LIMIT': 200,
|
|
98
|
+
'PERMISSIONS': {
|
|
99
|
+
# See Permission Configuration below
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Quick Start
|
|
105
|
+
|
|
106
|
+
### 1. Class-Based View (Recommended)
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
# views.py
|
|
110
|
+
from django_flex import FlexQueryView
|
|
111
|
+
from myapp.models import Booking
|
|
112
|
+
|
|
113
|
+
class BookingQueryView(FlexQueryView):
|
|
114
|
+
model = Booking
|
|
115
|
+
|
|
116
|
+
# Define permissions for this view
|
|
117
|
+
flex_permissions = {
|
|
118
|
+
'authenticated': {
|
|
119
|
+
'rows': lambda user: Q(team__members=user),
|
|
120
|
+
'fields': ['id', 'status', 'customer.name', 'customer.email'],
|
|
121
|
+
'filters': ['status', 'status.in', 'customer.name.icontains'],
|
|
122
|
+
'order_by': ['created_at', '-created_at'],
|
|
123
|
+
'ops': ['get', 'query'],
|
|
124
|
+
},
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
# urls.py
|
|
130
|
+
from django.urls import path
|
|
131
|
+
from myapp.views import BookingQueryView
|
|
132
|
+
|
|
133
|
+
urlpatterns = [
|
|
134
|
+
path('api/bookings/', BookingQueryView.as_view()),
|
|
135
|
+
path('api/bookings/<int:pk>/', BookingQueryView.as_view()), # Single object by ID
|
|
136
|
+
]
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### 2. Make Queries from Frontend
|
|
140
|
+
|
|
141
|
+
```javascript
|
|
142
|
+
// List bookings with field selection and filtering
|
|
143
|
+
const response = await fetch('/api/bookings/', {
|
|
144
|
+
method: 'GET',
|
|
145
|
+
headers: {'Content-Type': 'application/json'},
|
|
146
|
+
body: JSON.stringify({
|
|
147
|
+
fields: 'id, status, customer.name, customer.email',
|
|
148
|
+
filters: {
|
|
149
|
+
'status.in': ['confirmed', 'completed'],
|
|
150
|
+
'customer.name.icontains': 'khan'
|
|
151
|
+
},
|
|
152
|
+
order_by: '-created_at',
|
|
153
|
+
limit: 20
|
|
154
|
+
})
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const data = await response.json();
|
|
158
|
+
// {
|
|
159
|
+
// "pagination": {"offset": 0, "limit": 20, "has_more": true},
|
|
160
|
+
// "results": {
|
|
161
|
+
// "1": {"id": 1, "status": "confirmed", "customer": {"name": "Aisha Khan", "email": "aisha@example.com"}},
|
|
162
|
+
// "2": {"id": 2, "status": "completed", "customer": {"name": "Omar Khan", "email": "omar@example.com"}}
|
|
163
|
+
// }
|
|
164
|
+
// }
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
```javascript
|
|
168
|
+
// Get single object by ID (using URL)
|
|
169
|
+
const booking = await fetch('/api/bookings/1/', {
|
|
170
|
+
method: 'GET',
|
|
171
|
+
headers: {'Content-Type': 'application/json'},
|
|
172
|
+
body: JSON.stringify({
|
|
173
|
+
fields: 'id, status, customer.*, address.*'
|
|
174
|
+
})
|
|
175
|
+
});
|
|
176
|
+
// Returns: {"id": 1, "status": "confirmed", "customer": {...}, "address": {...}}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Query Language Reference
|
|
180
|
+
|
|
181
|
+
### Field Selection
|
|
182
|
+
|
|
183
|
+
```javascript
|
|
184
|
+
// All fields on the model
|
|
185
|
+
{ fields: '*' }
|
|
186
|
+
|
|
187
|
+
// Specific fields
|
|
188
|
+
{ fields: 'id, name, email' }
|
|
189
|
+
|
|
190
|
+
// Nested relation fields (dot notation)
|
|
191
|
+
{ fields: 'id, customer.name, customer.email' }
|
|
192
|
+
|
|
193
|
+
// Relation wildcards
|
|
194
|
+
{ fields: 'id, status, customer.*, address.*' }
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Filtering
|
|
198
|
+
|
|
199
|
+
```javascript
|
|
200
|
+
// Simple equality
|
|
201
|
+
{ filters: { status: 'confirmed' } }
|
|
202
|
+
|
|
203
|
+
// With operators
|
|
204
|
+
{ filters: { 'price.gte': 100, 'price.lte': 500 } }
|
|
205
|
+
|
|
206
|
+
// Text search
|
|
207
|
+
{ filters: { 'name.icontains': 'khan' } }
|
|
208
|
+
|
|
209
|
+
// List membership
|
|
210
|
+
{ filters: { 'status.in': ['pending', 'confirmed', 'completed'] } }
|
|
211
|
+
|
|
212
|
+
// OR conditions
|
|
213
|
+
{ filters: { or: { status: 'pending', 'customer.vip': true } } }
|
|
214
|
+
|
|
215
|
+
// NOT conditions
|
|
216
|
+
{ filters: { not: { status: 'cancelled' } } }
|
|
217
|
+
|
|
218
|
+
// Complex composition
|
|
219
|
+
{
|
|
220
|
+
filters: {
|
|
221
|
+
'created_at.gte': '2024-01-01',
|
|
222
|
+
or: [
|
|
223
|
+
{ status: 'confirmed' },
|
|
224
|
+
{ and: { status: 'pending', 'urgent': true } }
|
|
225
|
+
]
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
**Supported Operators:**
|
|
231
|
+
|
|
232
|
+
| Category | Operators |
|
|
233
|
+
|----------|-----------|
|
|
234
|
+
| Comparison | `lt`, `lte`, `gt`, `gte`, `exact`, `iexact`, `in`, `isnull`, `range` |
|
|
235
|
+
| Text | `contains`, `icontains`, `startswith`, `istartswith`, `endswith`, `iendswith`, `regex`, `iregex` |
|
|
236
|
+
| Date/Time | `date`, `year`, `month`, `day`, `week_day`, `hour`, `minute`, `second` |
|
|
237
|
+
|
|
238
|
+
### Pagination
|
|
239
|
+
|
|
240
|
+
```javascript
|
|
241
|
+
{
|
|
242
|
+
limit: 20, // Number of results (default: 50, max: 200)
|
|
243
|
+
offset: 0, // Starting position
|
|
244
|
+
order_by: '-created_at' // Sort order (prefix with - for descending)
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Response includes pagination info:
|
|
249
|
+
|
|
250
|
+
```javascript
|
|
251
|
+
{
|
|
252
|
+
"pagination": {
|
|
253
|
+
"offset": 0,
|
|
254
|
+
"limit": 20,
|
|
255
|
+
"has_more": true,
|
|
256
|
+
"next": {
|
|
257
|
+
"fields": "...",
|
|
258
|
+
"filters": {...},
|
|
259
|
+
"limit": 20,
|
|
260
|
+
"offset": 20
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## Permission Configuration
|
|
267
|
+
|
|
268
|
+
Django-Flex uses a **deny-by-default** security model. You must explicitly grant access.
|
|
269
|
+
|
|
270
|
+
```python
|
|
271
|
+
# settings.py
|
|
272
|
+
DJANGO_FLEX = {
|
|
273
|
+
'PERMISSIONS': {
|
|
274
|
+
'booking': {
|
|
275
|
+
# Fields excluded from wildcard expansion (security)
|
|
276
|
+
'exclude': ['internal_notes', 'stripe_payment_id'],
|
|
277
|
+
|
|
278
|
+
# Role-based permissions
|
|
279
|
+
'owner': {
|
|
280
|
+
# Row-level: which rows can this role see?
|
|
281
|
+
'rows': lambda user: Q(created_by=user),
|
|
282
|
+
|
|
283
|
+
# Field-level: which fields can they access?
|
|
284
|
+
'fields': ['*', 'customer.*', 'address.*'],
|
|
285
|
+
|
|
286
|
+
# Filter-level: which fields can they filter on?
|
|
287
|
+
'filters': [
|
|
288
|
+
'id', 'status', 'status.in',
|
|
289
|
+
'customer.name', 'customer.name.icontains',
|
|
290
|
+
'created_at.gte', 'created_at.lte',
|
|
291
|
+
],
|
|
292
|
+
|
|
293
|
+
# Order-level: which fields can they sort by?
|
|
294
|
+
'order_by': ['id', '-id', 'created_at', '-created_at', 'customer.name'],
|
|
295
|
+
|
|
296
|
+
# Operation-level: which actions can they perform?
|
|
297
|
+
'ops': ['get', 'query', 'create', 'update', 'delete'],
|
|
298
|
+
},
|
|
299
|
+
|
|
300
|
+
'staff': {
|
|
301
|
+
'rows': lambda user: Q(team__members=user),
|
|
302
|
+
'fields': ['id', 'status', 'customer.name', 'address.city'],
|
|
303
|
+
'filters': ['status', 'status.in'],
|
|
304
|
+
'order_by': ['created_at', '-created_at'],
|
|
305
|
+
'ops': ['get', 'query'],
|
|
306
|
+
},
|
|
307
|
+
|
|
308
|
+
# Roles not listed have NO ACCESS
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### Custom Role Resolution
|
|
315
|
+
|
|
316
|
+
Django-Flex uses Django's built-in groups for role resolution:
|
|
317
|
+
|
|
318
|
+
```python
|
|
319
|
+
from django_flex import FlexPermission
|
|
320
|
+
|
|
321
|
+
class MyPermission(FlexPermission):
|
|
322
|
+
def get_user_role(self, user):
|
|
323
|
+
if user.is_superuser:
|
|
324
|
+
return 'superuser'
|
|
325
|
+
if user.groups.filter(name='Managers').exists():
|
|
326
|
+
return 'manager'
|
|
327
|
+
return 'staff'
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
## Usage Patterns
|
|
331
|
+
|
|
332
|
+
### 1. Class-Based View (Recommended)
|
|
333
|
+
|
|
334
|
+
```python
|
|
335
|
+
from django_flex import FlexQueryView
|
|
336
|
+
|
|
337
|
+
class BookingQueryView(FlexQueryView):
|
|
338
|
+
model = Booking
|
|
339
|
+
require_auth = True
|
|
340
|
+
allowed_actions = ['get', 'query']
|
|
341
|
+
flex_permissions = {...}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### 2. Function Decorator
|
|
345
|
+
|
|
346
|
+
```python
|
|
347
|
+
from django_flex import flex_query
|
|
348
|
+
from django.http import JsonResponse
|
|
349
|
+
|
|
350
|
+
@flex_query(
|
|
351
|
+
model=Booking,
|
|
352
|
+
allowed_fields=['id', 'status', 'customer.name'],
|
|
353
|
+
allowed_filters=['status', 'status.in'],
|
|
354
|
+
)
|
|
355
|
+
def booking_list(request, result, query_spec):
|
|
356
|
+
return JsonResponse(result.to_dict())
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
### 3. Programmatic Usage
|
|
360
|
+
|
|
361
|
+
```python
|
|
362
|
+
from django_flex import FlexQuery
|
|
363
|
+
|
|
364
|
+
def my_view(request):
|
|
365
|
+
result = FlexQuery(Booking).execute({
|
|
366
|
+
'fields': 'id, customer.name',
|
|
367
|
+
'filters': {'status': 'confirmed'},
|
|
368
|
+
'limit': 20,
|
|
369
|
+
}, user=request.user)
|
|
370
|
+
|
|
371
|
+
return JsonResponse(result.to_dict())
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### 4. Middleware (Single Endpoint)
|
|
375
|
+
|
|
376
|
+
```python
|
|
377
|
+
# settings.py
|
|
378
|
+
MIDDLEWARE = [
|
|
379
|
+
...
|
|
380
|
+
'django_flex.middleware.FlexQueryMiddleware',
|
|
381
|
+
]
|
|
382
|
+
|
|
383
|
+
DJANGO_FLEX = {
|
|
384
|
+
'MIDDLEWARE_PATH': '/api/',
|
|
385
|
+
...
|
|
386
|
+
}
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
Then query any configured model:
|
|
390
|
+
|
|
391
|
+
```javascript
|
|
392
|
+
fetch('/api/', {
|
|
393
|
+
method: 'POST',
|
|
394
|
+
body: JSON.stringify({
|
|
395
|
+
_model: 'booking',
|
|
396
|
+
_action: 'query',
|
|
397
|
+
fields: 'id, status',
|
|
398
|
+
limit: 20
|
|
399
|
+
})
|
|
400
|
+
});
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
## Configuration Reference
|
|
404
|
+
|
|
405
|
+
```python
|
|
406
|
+
DJANGO_FLEX = {
|
|
407
|
+
# Pagination
|
|
408
|
+
'DEFAULT_LIMIT': 50, # Default page size
|
|
409
|
+
'MAX_LIMIT': 200, # Maximum page size (hard cap)
|
|
410
|
+
|
|
411
|
+
# Security
|
|
412
|
+
'MAX_RELATION_DEPTH': 2, # Max depth for nested fields/filters
|
|
413
|
+
'REQUIRE_AUTHENTICATION': True, # Require auth by default
|
|
414
|
+
'AUDIT_QUERIES': False, # Log all queries (for debugging)
|
|
415
|
+
|
|
416
|
+
# Middleware
|
|
417
|
+
'MIDDLEWARE_PATH': '/api/', # Path for middleware endpoint
|
|
418
|
+
|
|
419
|
+
# Optional: versioned APIs with independent settings
|
|
420
|
+
'VERSIONS': {
|
|
421
|
+
'v1': {'path': '/api/v1/', 'PERMISSIONS': {...}},
|
|
422
|
+
'v2': {'path': '/api/v2/', 'PERMISSIONS': {...}},
|
|
423
|
+
},
|
|
424
|
+
|
|
425
|
+
# Model permissions (see Rate Limiting section below)
|
|
426
|
+
'PERMISSIONS': {...},
|
|
427
|
+
}
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
### API Versioning
|
|
431
|
+
|
|
432
|
+
Run unversioned `/api/` alongside versioned `/api/v1/`, `/api/v2/` with different settings per version:
|
|
433
|
+
|
|
434
|
+
```python
|
|
435
|
+
DJANGO_FLEX = {
|
|
436
|
+
'MIDDLEWARE_PATH': '/api/', # Unversioned endpoint
|
|
437
|
+
'PERMISSIONS': {...}, # Top-level = unversioned settings
|
|
438
|
+
'MAX_LIMIT': 200,
|
|
439
|
+
|
|
440
|
+
'VERSIONS': {
|
|
441
|
+
'v1': {
|
|
442
|
+
'path': '/api/v1/',
|
|
443
|
+
'PERMISSIONS': {...}, # v1-specific permissions
|
|
444
|
+
'MAX_LIMIT': 100, # v1-specific limit
|
|
445
|
+
},
|
|
446
|
+
'v2': {
|
|
447
|
+
'path': '/api/v2/',
|
|
448
|
+
'PERMISSIONS': {...}, # v2-specific permissions
|
|
449
|
+
'MAX_LIMIT': 200,
|
|
450
|
+
},
|
|
451
|
+
},
|
|
452
|
+
}
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
## Rate Limiting
|
|
456
|
+
|
|
457
|
+
Rate limits can be configured at multiple levels (most specific wins):
|
|
458
|
+
|
|
459
|
+
```python
|
|
460
|
+
DJANGO_FLEX = {
|
|
461
|
+
'PERMISSIONS': {
|
|
462
|
+
'booking': {
|
|
463
|
+
# Model-level: integer = same for all ops
|
|
464
|
+
'rate_limit': 60,
|
|
465
|
+
|
|
466
|
+
# OR dict for per-operation limits
|
|
467
|
+
# 'rate_limit': {'default': 60, 'query': 30, 'get': 120},
|
|
468
|
+
|
|
469
|
+
# Anonymous users - very restricted
|
|
470
|
+
'anon': {
|
|
471
|
+
'fields': ['id', 'status'],
|
|
472
|
+
'ops': ['query'],
|
|
473
|
+
'rate_limit': 5, # Only 5 requests/minute for anon
|
|
474
|
+
},
|
|
475
|
+
|
|
476
|
+
'authenticated': {
|
|
477
|
+
'fields': ['*'],
|
|
478
|
+
'ops': ['get', 'query'],
|
|
479
|
+
'rate_limit': 50,
|
|
480
|
+
},
|
|
481
|
+
|
|
482
|
+
'staff': {
|
|
483
|
+
'fields': ['*'],
|
|
484
|
+
'ops': ['get', 'query'],
|
|
485
|
+
'rate_limit': 200, # Staff gets higher limits
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
},
|
|
489
|
+
}
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
### Behind a Proxy
|
|
493
|
+
|
|
494
|
+
By default, anonymous rate limiting uses `REMOTE_ADDR` (not spoofable). If you are
|
|
495
|
+
behind a trusted reverse proxy that sets `X-Forwarded-For`, enable:
|
|
496
|
+
|
|
497
|
+
```python
|
|
498
|
+
DJANGO_FLEX = {
|
|
499
|
+
'RATE_LIMIT_USE_FORWARDED_IP': True,
|
|
500
|
+
}
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
WARNING: Only enable this if your proxy is properly configured to set
|
|
504
|
+
`X-Forwarded-For`. Otherwise attackers can spoof their IP and bypass limits.
|
|
505
|
+
|
|
506
|
+
When rate limit is exceeded, returns HTTP 429 with `Retry-After` header:
|
|
507
|
+
|
|
508
|
+
```json
|
|
509
|
+
{"error": "Rate limit exceeded", "retry_after": 45}
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
## Response Format
|
|
513
|
+
|
|
514
|
+
Responses use HTTP status codes (200, 400, 401, 403, 404) to indicate success/failure.
|
|
515
|
+
|
|
516
|
+
### Successful Single Object (get) - HTTP 200
|
|
517
|
+
|
|
518
|
+
```json
|
|
519
|
+
{
|
|
520
|
+
"id": 1,
|
|
521
|
+
"status": "confirmed",
|
|
522
|
+
"customer": {
|
|
523
|
+
"name": "Aisha Khan",
|
|
524
|
+
"email": "aisha@example.com"
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
### Successful Query (query) - HTTP 200
|
|
530
|
+
|
|
531
|
+
```json
|
|
532
|
+
{
|
|
533
|
+
"pagination": {
|
|
534
|
+
"offset": 0,
|
|
535
|
+
"limit": 20,
|
|
536
|
+
"has_more": true,
|
|
537
|
+
"next": {...}
|
|
538
|
+
},
|
|
539
|
+
"results": {
|
|
540
|
+
"1": {...},
|
|
541
|
+
"2": {...}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
### Error Response - HTTP 400/401/403/404
|
|
547
|
+
|
|
548
|
+
```json
|
|
549
|
+
{
|
|
550
|
+
"error": "Access denied: field 'secret_field' not accessible"
|
|
551
|
+
}
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
## Why Django-Flex?
|
|
555
|
+
|
|
556
|
+
| Feature | Django-Flex | GraphQL | REST |
|
|
557
|
+
|---------|-------------|---------|------|
|
|
558
|
+
| Learning curve | Low (Django-native) | High | Low |
|
|
559
|
+
| Field selection | ✅ | ✅ | ❌ (fixed endpoints) |
|
|
560
|
+
| Dynamic filtering | ✅ | ✅ | Limited |
|
|
561
|
+
| Built-in security | ✅ | Manual | Manual |
|
|
562
|
+
| Django integration | Native | Requires graphene | Native |
|
|
563
|
+
| Schema definition | Optional | Required | N/A |
|
|
564
|
+
| N+1 prevention | Automatic | Manual | Manual |
|
|
565
|
+
|
|
566
|
+
## Contributing
|
|
567
|
+
|
|
568
|
+
Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
|
569
|
+
|
|
570
|
+
## License
|
|
571
|
+
|
|
572
|
+
MIT License — see [LICENSE](LICENSE) for details.
|