Tagline
“Turn any PostgreSQL schema into a live, permission-aware REST API with zero boilerplate.”
-
Introduction
1.1 What is DynAPI-Studio?
1.2 Key Features
1.3 Typical Use Cases -
Project Structure & Architecture
2.1 High-Level Layout
2.2 Core Components -
Installation & Initial Setup
3.1 Requirements
3.2 Environment Variables
3.3 Database & Redis
3.4 Running the Project -
Configuration Deep Dive
4.1 Django Settings
4.2 REST Framework & JWT
4.3 Channels & WebSockets
4.4 URL Routing -
Domain Model: Users, Roles & Permissions
5.1User
5.2Role
5.3UserRole -
Dynamic API Pipeline
6.1 Database Introspection
6.2 Dynamic Model Generation
6.3 Dynamic Serializers
6.4 Dynamic ViewSets
6.5 Dynamic Router & Route Registration
6.6 Caching & Reloading -
Permission System & Security
7.1TablePermission
7.2 User-Level Table ACLs
7.3 Throttling -
Authentication & User Management
8.1 JWT Login
8.2 User Registration
8.3 Password Change & Admin Reset -
Admin Integration & UI
9.1 Admin Overview Page
9.2 Admin Permissions UI
9.3 Inline Permissions on User -
Real-Time Updates via WebSockets
10.1 WebSocket Consumer
10.2 Server-Side Notifications
10.3 Frontend Consumption Example -
Custom Hooks & Custom Actions
11.1 Validation Hooks
11.2 Custom Actions per Table -
End-to-End Usage Scenario
15.1 From Table Creation to API Consumption
15.2 Bulk Permission Management
DynAPI-Studio is a Django-based framework that automatically turns a PostgreSQL database schema into a set of fully functional, permission-aware REST APIs.
Instead of manually creating Django models, serializers, viewsets, and routes for every table, DynAPI-Studio:
- Introspects your PostgreSQL
publicschema, - Generates dynamic Django models (unmanaged),
- Creates DRF serializers and ModelViewSets on the fly,
- Registers REST routes under a dynamic namespace (
/api/dyn/<table_name>/), - Enforces per-user, per-table permissions stored in the database,
- Emits real-time WebSocket events on CRUD operations.
It is essentially a “database-driven API generator.”
- 🚀 Automatic CRUD generation for every business table (except excluded system tables).
- 🧬 Dynamic models generated at runtime using PostgreSQL introspection.
- 🔐 Fine-grained permissions:
UserRolecontrolscan_create,can_read,can_update,can_deleteper user per table. - 🔑 JWT authentication with registration, login, password change, admin password reset.
- 🌐 Live WebSocket updates per table using Django Channels + Redis.
- 🛡️ Throttling: per-user DynAPI rate limiting via DRF throttling.
- 🧩 Extensible hooks for validation (
validation_hooks) and custom business actions (custom_actions). - ⚙️ Admin UI:
- Overview of detected tables and their exposure,
- Advanced permission management UI with bulk update / bulk assign.
- 🧪 Automated tests validating end-to-end behavior of the dynamic pipeline.
- Quickly exposing a legacy PostgreSQL database via modern REST endpoints.
- Building internal admin tools / back-office UIs against existing schemas.
- Prototyping CRUD apps without writing repetitive model/view code.
- Providing a low-code / no-code backend where business users manipulate tables and permission rules while DynAPI-Studio provides the API interface.
The project uses a classic Django project/app structure:
-
Project:
dynapi_studio/settings.py– Django/DRF/Channels configurationurls.py– root URL configurationasgi.py– ASGI entrypoint (HTTP + WebSockets)wsgi.py– WSGI entrypoint (HTTP only)
-
Core App:
dynapi_core/models.py–User,Role,UserRoledb_introspection.py– PostgreSQL introspection servicesgeneration.py– dynamic model/serializer/viewset generatordynamic_models.py– namespace module for dynamic modelsdynamic_registry.py– registry of dynamic model classesapi_router.py– DRF router setup and dynamic route registrationpermissions.py– table-level permission classviews.py– REST views & viewsets (auth, user/role/userrole, admin config)serializers.py&auth_serializers.py– serializers for static models & auth flowsadmin.py– admin integration, custom views & UI injectionrouting.py,ws_consumers.py– WebSocket routing and consumerthrottling.py– per-user throttle classmanagement/commands/*– CLI commands for introspection & route loadingtests.py– end-to-end tests for the dynamic pipeline- Customization hooks:
hooks.py,custom_hooks.py,custom_actions.py,my_actions.py
Template files (admin overview & permissions pages plus JS helpers) live under templates/admin/dynapi_core/... (the snippet is provided inside the repo text but is intended to be template files).
At a conceptual level, the architecture is:
PostgreSQL Schema
↓ (introspection)
DatabaseIntrospector
↓
APIGenerator
- Dynamic models
- Dynamic serializers
- Dynamic viewsets
↓
get_dynamic_resources()
↓
api_router / DefaultRouter
↓
HTTP API: /api/dyn/<table_name>/
↓ ↑
TablePermission UserRole rules
WebSockets (Channels)
↑
DynAPITableConsumer + _notify_ws()
- Python 3.10+ (example; any modern Python 3 should work)
- Django (as configured in your
requirements.txt) - Django REST Framework (
rest_framework) - drf-spectacular for OpenAPI schema & docs
- djangorestframework-simplejwt for JWT auth
- Django Channels (
channels) - Redis (or compatible) for Channels layer
- PostgreSQL as the primary database engine
The project expects some environment variables (see settings.py):
SECRET_KEY– Django secret key (default:"change-me-in-production").DEBUG–"1"or"0"(default"1").ALLOWED_HOSTS– comma-separated hostnames (default:"localhost,127.0.0.1").
Primary database (DATABASES["default"]):
DB_NAME,DB_USER,DB_PASSWORD,DB_HOST,DB_PORT.
Secondary database (used via DB router for some dynamic tables):
DB2_NAME,DB2_USER,DB2_PASSWORD,DB2_HOST,DB2_PORT.
Redis / Channels:
REDIS_URL– e.g.redis://127.0.0.1:6379/0.
DynAPI throttle rate:
DYNAPI_RATE– e.g."1000/day".
A /.env.develop file (not fully shown) can provide these for local development.
- Create the primary PostgreSQL database (e.g.
dynapi_studio_db). - Optionally create the secondary database if you plan to route some dynamic tables there.
- Ensure Redis is running (default:
redis://127.0.0.1:6379/0).
Run Django migrations for the static models:
python manage.py migrate
python manage.py createsuperuserFor development:
# HTTP + WebSockets via ASGI server (e.g. daphne or uvicorn)
daphne dynapi_studio.asgi:application
# or
uvicorn dynapi_studio.asgi:application --reloadFor classic WSGI (no WebSockets), you can still use:
python manage.py runserverBut WebSocket features will only work through the ASGI stack.
Key sections in dynapi_studio/settings.py:
-
INSTALLED_APPS- Core Django apps (
django.contrib.*) - Third-party:
channels,rest_framework,rest_framework.authtoken,drf_spectacular,rest_framework_simplejwt - Project app:
dynapi_core
- Core Django apps (
-
DATABASESdefault: PostgreSQL main DBsecondary: PostgreSQL secondary DBDATABASE_ROUTERS = ["dynapi_core.db_routers.DynAPIDatabaseRouter"]ensures some dynamic models can be routed to the second DB, based on table name prefix.
-
AUTH_USER_MODEL = "dynapi_core.User"Uses a custom user model with an extracontactfield. -
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
REST_FRAMEWORK:
-
Authentication:
JWTAuthentication(SimpleJWT)SessionAuthentication(for browsable API & admin)
-
Permissions:
- Default:
IsAuthenticated– all API endpoints require authentication unless they explicitly override withAllowAny(e.g. health, login, register).
- Default:
-
Schema:
DEFAULT_SCHEMA_CLASS = "drf_spectacular.openapi.AutoSchema"- OpenAPI schema at
/api/schema/and docs at/api/docs/,/api/redoc/.
-
Throttling:
DEFAULT_THROTTLE_CLASSES→DynAPIPerUserThrottleDEFAULT_THROTTLE_RATES→dynapi: env or"1000/day".
SIMPLE_JWT defines token lifetimes & header behavior.
ASGI_APPLICATION = "dynapi_studio.asgi.application"
CHANNEL_LAYERS uses channels_redis:
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [os.getenv("REDIS_URL", "redis://127.0.0.1:6379/0")],
},
},
}The ASGI app wires:
"http"→ standard Django ASGI app"websocket"→AuthMiddlewareStack(URLRouter(dynapi_core.routing.websocket_urlpatterns))
In dynapi_studio/urls.py:
-
/admin/→ Django admin (with DynAPI admin extensions). -
/api/schema/→ OpenAPI schema. -
/api/docs/,/api/redoc/→ Swagger and Redoc docs via drf-spectacular. -
/api/→ includesdynapi_core.api_router.urlpatterns:/api/health/– health check/api/auth/*– login, register, password change, admin reset/api/users/,/api/roles/,/api/user-roles/– static viewsets/api/dyn/reload/– dynamic route reload endpoint/api/dyn/config/– export/import of DynAPI configuration/api/dyn/<table_name>/– dynamic CRUD endpoints for each table
Located in dynapi_core/models.py, extending AbstractUser:
-
Standard Django user fields:
username,email,password, etc. -
Additional field:
contact: CharField(max_length=50, null=True, blank=True)– phone number or contact.
Used as the AUTH_USER_MODEL and for authentication / JWT tokens.
Represents a logical or business role:
name: CharField(unique=True)– e.g.Admin,Manager,Viewer.description: TextField– optional description.
Used as a grouping concept for UserRole entries.
The heart of DynAPI permission system:
-
user: ForeignKey(User)– the user owning the permission. -
role: ForeignKey(Role, null=True)– optional role reference. -
table_name: CharField– the PostgreSQL table name the permission applies to. -
Boolean flags:
can_createcan_readcan_updatecan_delete
unique_together = ("user", "table_name") ensures one UserRole per user-table pair.
This model is consumed by the TablePermission class to enforce access control.
dynapi_core/db_introspection.py defines:
-
ColumnInfodataclass:name,data_type,is_nullable,is_primary_key,foreign_table,foreign_column.
-
TableInfodataclass:namecolumns: List[ColumnInfo]
-
DatabaseIntrospectorservice:-
list_tables()- Queries
pg_catalog.pg_tableswhereschemaname = 'public'.
- Queries
-
_get_foreign_keys(table_name)- Uses
information_schemato detect FK relationships.
- Uses
-
get_table_columns(table_name)- Uses
pg_attribute,pg_class,pg_namespace,pg_indexto gather column info.
- Uses
-
get_all_tables_info()- Returns
TableInfofor every table in the public schema.
- Returns
-
This is the foundation for later steps: DynAPI must understand the schema before generating any API.
dynapi_core/generation.py is the core engine.
Important classes & functions:
-
GeneratedResourcesdataclass:model: models.Modelserializer: ModelSerializerviewset: ModelViewSet
-
_DYNAMIC_RESOURCES_CACHE– global in-process cache for generated resources. -
EXCLUDED_TABLES– set of tables that are never exposed (e.g. Django system tables, auth tables, DynAPI internal tables).
Steps:
-
Fetch all
TableInfofromDatabaseIntrospector. -
Clear the
dynamic_registryto avoid stale classes. -
First pass: For each table (not excluded, with at least one column):
- Call
_create_model_skeleton(table_info)to build a model without foreign keys. - Store temporary models keyed by table name.
- Call
-
Second pass: For each table again:
-
Call
generate_for_table(table_info, temp_models)to:- Attach FK fields,
- Create serializer,
- Create viewset,
- Register model in the global registry.
-
Builds a Django model:
-
Fields are created via
_django_field_from_column(col, skip_fk=True)for non-FK columns. -
Meta:db_table = table_info.namemanaged = False(no migrations)app_label = "dynapi_core"
-
__module__ = "dynapi_core.dynamic_models"ensures models live in a dedicated namespace. -
Class name:
"Dynamic" + CamelCase(table_name).
For each column where col.foreign_table is known:
- Creates a
ForeignKeyto the corresponding temporary model using_create_foreign_key_field. on_delete = DO_NOTHING,db_column = col.name.
Maps PostgreSQL types to Django field classes:
int,integer, etc. →IntegerField,BigIntegerField,SmallIntegerFieldboolean→BooleanFieldvarchar,character varying(n)→CharField(max_length=n)text→TextFieldnumeric,decimal→DecimalField(max_digits=20, decimal_places=6)timestamp,date,time→DateTimeField,DateField,TimeFielduuid→UUIDField- Fallback:
TextFieldfor unknown types or enums.
For each model:
class DynamicSerializer(serializers.ModelSerializer):
class Meta:
model = model_cls
fields = "__all__"The class is then renamed to e.g. DynamicProductSerializer at runtime.
This provides full CRUD serialization for all columns.
Each dynamic table is exposed via a tailored ModelViewSet:
Key properties:
-
queryset = model_cls.objects.select_related(*fk_fields).all()fk_fieldsis precomputed list of FK fields for optimization.
-
serializer_class = serializer_cls -
permission_classes = [TablePermission] -
table_name = table_info.name– critical for permission checks and hooks.
Filtering & search:
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]filterset_fields= all non-M2M/non-O2M field names.search_fields= allCharFieldandTextFields.ordering_fields = filterset_fields, defaultordering = ["-pk"].
Hooks:
-
perform_create&perform_update:- Call
validation_hooks.run(...)before saving, allowing custom validation per table/field. - Notify WebSocket subscribers via
_notify_ws("created" / "updated", instance).
- Call
-
perform_destroy:- Notifies
"deleted"events before calling super().
- Notifies
Custom actions:
-
@action(detail=True, methods=["post"], url_path="action/(?P<action_name>[^/.]+)")- Endpoint:
/api/dyn/<table>/<pk>/action/<action_name>/ - Uses
custom_actions.get(table_name, action_name)to retrieve the handler, passes(instance, request.user, payload).
- Endpoint:
dynapi_core/api_router.py defines:
-
A global
DefaultRouterinstance. -
Static registrations:
"users"→UserViewSet"roles"→RoleViewSet"user-roles"→UserRoleViewSet
Dynamic prefixes:
DYN_PREFIX = "dyn/"– route prefix for dynamic tables.DYN_BASENAME_PREFIX = "dyn-"– DRF basename prefix.
Removes any previously registered dynamic routes from the router registry, while keeping static ones.
-
Ensures database is ready via
is_database_ready(). -
Gets all
dynamic_resources = get_dynamic_resources(). -
For each table:
route_prefix = "dyn/<table_name>"basename = "dyn-" + table_name.replace("_", "-")router.register(route_prefix, resources.viewset, basename=basename)
This function returns a list of metadata:
[
{
"table": "product",
"route_prefix": "dyn/product",
"basename": "dyn-product"
},
...
]- Resets the dynamic resource cache (
reset_dynamic_resources_cache()). - Clears old dynamic routes.
- Registers new ones.
Exposed via:
DynamicReloadView(POST/api/dyn/reload/) for API usage.dynapi_reloadview inadmin.pyfor admin usage.
get_dynamic_resources():
- Uses
_DYNAMIC_RESOURCES_CACHEto avoid regeneration on every request. - Ensures DB connection is available.
- Instantiates an
APIGeneratorand builds all resources once per process.
reset_dynamic_resources_cache() empties the cache and clears the dynamic model registry, forcing a fresh introspection and generation.
This is essential after:
- Applying migrations,
- Changing schema (adding/dropping tables, modifying columns),
- Updating custom hooks/actions.
Located in dynapi_core/permissions.py.
For every request to a dynamic viewset:
-
If the user is not authenticated → deny.
-
If the view doesn’t define
table_name→ allow (used for non-dynamic views). -
Fetch
UserRolefor(user, table_name).- If not found → deny.
-
Map HTTP method → permission:
- Safe methods (
GET,HEAD,OPTIONS) →can_read. POST→can_create.PUT/PATCH→can_update.DELETE→can_delete.
- Safe methods (
If the flag is False → request is denied.
The UserRole model allows:
- Per table, per user granular ACLs.
- Optional association to a
Rolefor grouping.
The admin interface and API endpoints (UserRoleViewSet) make it easy to:
- Filter permissions by user/table/role/search.
- Bulk update permission flags for multiple
UserRoleIDs. - Bulk assign a set of tables for a user (
bulk-assign).
DynAPIPerUserThrottle (in dynapi_core/throttling.py) is a simple rate limiter:
-
scope = "dynapi". -
get_cache_key(...):- If user is authenticated → use user.pk.
- Otherwise → use client IP (
get_ident).
Configured via REST_FRAMEWORK["DEFAULT_THROTTLE_RATES"]["dynapi"].
This protects the dynamic endpoints from abuse.
-
DynAPITokenObtainPairSerializerextends SimpleJWT’sTokenObtainPairSerializer:- Adds
usernameandemailto JWT payload. - Logs last login via
update_last_login. - Returns token pair + user info in the response.
- Adds
-
LoginView(inviews.py):POST /api/auth/login/permission_classes = [AllowAny].
Response structure:
{
"refresh": "<refresh_token>",
"access": "<access_token>",
"user": {
"id": 1,
"username": "admin",
"email": "admin@example.com",
"contact": null
}
}RegisterView:
-
Endpoint:
POST /api/auth/register/ -
Uses
RegisterSerializer(auth_serializers.py), which:- Validates
password+password_confirm. - Uses Django’s
password_validation. - Creates a
Userand sets hashed password.
- Validates
The view then issues JWT tokens (RefreshToken.for_user(user)) and returns them along with user info.
-
ChangePasswordView:-
Endpoint:
POST /api/auth/password/change/ -
Requires authenticated user.
-
Uses
ChangePasswordSerializerto validate:old_passwordmatches current password.new_passwordandnew_password_confirmmatch and pass Django validation.
-
Saves the new password.
-
-
AdminResetPasswordView:-
Endpoint:
POST /api/auth/password/reset/admin/ -
Restricted to
IsAdminUser. -
Uses
AdminResetPasswordSerializer:- Validates
user_idexists. - Validates new password.
- Sets and saves new password.
- Validates
-
dynapi_core/admin.py:
- Defines custom views
dynapi_overviewanddynapi_permissions_view. - Hooks them into the admin using a monkey-patched
admin.site.get_urls.
dynapi_overview:
-
Uses
DatabaseIntrospectorto list all tables. -
Uses
get_dynamic_resources()to see which tables are exposed. -
Counts
UserRoleentries per table. -
Sets flags:
is_internal(excluded, system, or dynapi tables).is_exposed.
-
Computes
api_url→/api/dyn/<table_name>/when exposed. -
Renders template
admin/dynapi_core/dynapi_overview.html:-
Shows badges (“Internal”, “Business”, “Exposed”, “Not exposed”).
-
Shows columns count, number of permissions, endpoint path.
-
Provides buttons:
- “Manage permissions” →
dynapi_permissions. - “Reload dynamic routes” →
dynapi_reload.
- “Manage permissions” →
-
dynapi_permissions_view:
-
Provides a rich admin UI for managing
UserRoleentries:-
Filtering bar:
q,table,user. -
Table of
UserRoleentries with:- user, role, table, each permission flag, and API endpoint path.
- checkboxes to select multiple rows.
-
-
Two panels (with JS logic):
-
Bulk update selected permissions:
- Calls
/api/user-roles/bulk-update/viafetch. - Fields:
can_create,can_read,can_update,can_delete(each can be “no change”, “allow”, “deny”).
- Calls
-
Bulk assign permissions for a user across multiple tables:
-
Calls
/api/user-roles/bulk-assign/. -
Inputs:
user_id, optionalrole_id,- list of table names (comma-separated),
- permission flags.
-
-
The UI uses CSRF tokens and reloads the page after successful operation.
In UserAdmin (extends DjangoUserAdmin):
-
Adds extra fieldset “Informations supplémentaires” with
contact. -
Registers
UserRoleInlineas aTabularInline:- Allows editing of
UserRolein the user’s detail page directly.
- Allows editing of
This is convenient for per-user permission administration.
dynapi_core/ws_consumers.py:
DynAPITableConsumer(AsyncJsonWebsocketConsumer):
-
Connect:
- Reads
table_namefrom URL:/ws/dynapi/<table_name>/. - Joins group
dynapi_<table_name>.
- Reads
-
Disconnect:
- Leaves the group.
-
dynapi_event(self, event):-
Called when
group_sendemits atype="dynapi.event"message. -
Sends JSON to the client:
{ "event": "created" | "updated" | "deleted", "table": "<table_name>", "pk": <primary_key> }
-
In the dynamic viewset (inside generation.py), _notify_ws():
def _notify_ws(self, event_type: str, instance):
try:
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
channel_layer = get_channel_layer()
if not channel_layer:
return
group_name = f"dynapi_{self.table_name}"
payload = {
"type": "dynapi.event",
"event": event_type,
"table": self.table_name,
"pk": instance.pk,
}
async_to_sync(channel_layer.group_send)(group_name, payload)
except Exception:
# WebSocket failure must not break HTTP request
passCalled on:
perform_create:"created"perform_update:"updated"perform_destroy:"deleted"
JavaScript (browser or SPA):
const tableName = "product";
const ws = new WebSocket(`ws://localhost:8000/ws/dynapi/${tableName}/`);
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log("DynAPI event:", data);
// { event: "created", table: "product", pk: 123 }
// → Update your UI accordingly
};
ws.onopen = () => console.log("WebSocket connected");
ws.onclose = () => console.log("WebSocket disconnected");This enables live UI updates when records are created/updated/deleted via the dynamic APIs.
dynapi_core/hooks.py defines ValidationHookRegistry:
-
Maintains a mapping:
{ "product": { "__table__": [func1, ...], "price": [func2, ...] } } -
Decorator usage:
@validation_hooks.register("product", "price") def check_price(instance, data, action, user): ...
-
run(table_name, instance, data, action, user):- Executes table-level hooks (
"__table__") and field-specific hooks. - Collects
ValidationErrors per field and raises a combinedValidationErrorif any.
- Executes table-level hooks (
Example from custom_hooks.py:
from rest_framework.exceptions import ValidationError
from .hooks import validation_hooks
@validation_hooks.register("product", "price")
def check_positive_price(instance, data, action, user):
price = data.get("price")
if price is not None and price < 0:
raise ValidationError(["Le prix doit être positif."])This hook is run in dynamic viewsets before serializer.save() in perform_create and perform_update.
dynapi_core/custom_actions.py defines CustomActionRegistry:
-
Registry:
custom_actions.register("table_name", "action_name")
-
Retrieval:
custom_actions.get(table_name, action_name)
In my_actions.py:
from .custom_actions import custom_actions
@custom_actions.register("order", "mark_as_paid")
def mark_order_as_paid(instance, user, payload):
instance.status = "paid"
instance.save()
return {"status": "ok", "id": instance.pk}Accessed via dynamic viewset custom_action:
POST /api/dyn/order/<pk>/action/mark_as_paid/with JSON bodypayload.- Returns the result of the registered function.
This mechanism allows you to attach business-specific behaviors to dynamic resources without changing the generator itself.
dynapi_core/db_routers.py:
DynAPIDatabaseRouter routes dynamic models (those in dynapi_core.dynamic_models) based on table name:
-
db_for_read(model, **hints):-
If
model.__module__ == "dynapi_core.dynamic_models":- If
model._meta.db_table.startswith("ext_")→ route to"secondary"DB.
- If
-
Else →
None(use default).
-
-
db_for_writemirrorsdb_for_read. -
allow_migratealways returnsNoneto avoid migrations for dynamic models (managed = False).
This is handy when some external or special tables live in a different database.
Two custom commands:
-
load_dynamic_routes- Usage:
python manage.py load_dynamic_routes - Calls
reload_dynamic_routes()and outputs how many dynamic tables were registered and their route prefixes.
- Usage:
-
test_introspection- Usage:
python manage.py test_introspection - Uses
DatabaseIntrospectorto list tables and their columns, excluding Django and auth tables. - Useful for debugging schema issues.
- Usage:
dynapi_core/tests.py provides an end-to-end test class: DynamicAPIPipelineTests.
Test flow:
-
Setup:
-
Creates two tables via raw SQL:
test_dynamic_tabletest_dynamic_childwithparent_idFK.
-
Calls
reset_dynamic_resources_cache()andapi_router.reload_dynamic_routes(). -
Retrieves
_resources = get_dynamic_resources()for inspection. -
Creates:
userwithUserRoleentries for both tables.adminsuperuser.
-
-
test_dynamic_models_generated:- Asserts that both tables exist in
_resources. - Verifies model fields (including that
parent_idis aForeignKeywithdb_column="parent_id").
- Asserts that both tables exist in
-
test_dynamic_crud_with_permissions:-
Authenticates as
user. -
Performs:
- Create parent record (
POST /api/dyn/test_dynamic_table/). - List parents (
GET /api/dyn/test_dynamic_table/). - Create child with FK.
- Retrieve child detail.
- Update parent (
PATCH). - Attempt to delete parent (should be
403becausecan_delete=False). - Delete child (allowed).
- Create parent record (
-
-
test_dynamic_reload_endpoint:- Authenticates as
admin. - Calls
POST /api/dyn/reload/. - Verifies the response contains the dynamic tables.
- Authenticates as
These tests validate:
- Introspection still works.
- Dynamic models & relationships are correctly generated.
- Permissions are enforced as expected.
- Reload endpoint functions correctly.
Step 1 – Create PostgreSQL Tables
Suppose you create tables:
CREATE TABLE customer (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(255)
);
CREATE TABLE order (
id SERIAL PRIMARY KEY,
customer_id INTEGER NOT NULL REFERENCES customer(id),
status VARCHAR(50),
total NUMERIC(10, 2)
);Step 2 – Reload Dynamic Routes
From Django admin:
- Go to DynAPI overview.
- Click “Reload dynamic routes”; or
From API:
curl -X POST \
-H "Authorization: Bearer <admin_access_token>" \
http://localhost:8000/api/dyn/reload/DynAPI-Studio will introspect and register:
/api/dyn/customer//api/dyn/order/
Step 3 – Assign Permissions
As an admin, create UserRole entries:
-
For user
alice:table_name = "customer",can_create=True,can_read=True,can_update=True,can_delete=False.table_name = "order",can_create=True,can_read=True,can_update=True,can_delete=True.
Optionally use:
- Admin Permissions UI (bulk assign).
- API:
/api/user-roles/bulk-assign/.
Step 4 – Use the API
As alice:
# Create a customer
curl -X POST http://localhost:8000/api/dyn/customer/ \
-H "Authorization: Bearer <alice_token>" \
-H "Content-Type: application/json" \
-d '{"name": "John Doe", "email": "john@example.com"}'# List customers
curl -H "Authorization: Bearer <alice_token>" \
http://localhost:8000/api/dyn/customer/# Create an order
curl -X POST http://localhost:8000/api/dyn/order/ \
-H "Authorization: Bearer <alice_token>" \
-H "Content-Type: application/json" \
-d '{"customer_id": 1, "status": "pending", "total": 100.50}'Everything works without writing any model or viewset for customer or order.
Using the admin “Permissions” page:
- Select lines with checkboxes.
- Use “Bulk update” panel to toggle
can_create,can_read,can_update,can_delete. - Use “Bulk assign” panel to assign the same permissions across many tables for a given user.
Under the hood, the JS calls:
POST /api/user-roles/bulk-update/POST /api/user-roles/bulk-assign/
These endpoints handle validation and apply changes in a single request.
-
ASGI server: Use
daphneoruvicornto servedynapi_studio.asgi:application. -
Redis: Must be reachable from the deployment environment for Channels.
-
PostgreSQL schema changes:
-
After adding/removing tables/columns, call:
python manage.py load_dynamic_routes- or
POST /api/dyn/reload/ - or use the admin “Reload dynamic routes” button.
-
-
Security:
- Use strong
SECRET_KEY. - Set
DEBUG=0in production. - Configure
ALLOWED_HOSTS. - Consider HTTPS termination (via reverse proxy or load balancer).
- Carefully manage
UserRoleentries, especiallycan_delete=Trueandcan_update=True.
- Use strong
Limitations:
-
DynAPI-Studio is tightly coupled to PostgreSQL and uses its system catalogs.
-
Dynamic models are unmanaged (
managed=False):- No migrations; schema changes must be done directly in the DB.
-
No automatic handling of:
- Complex constraints (CHECK, UNIQUE across multiple columns, etc.).
- Advanced types (arrays, JSONB) beyond basic mapping (currently fallback is
TextFieldfor unknown types).
Possible future enhancements:
- Richer type mapping for JSON/ARRAY columns.
- Per-table configuration (e.g. hidden fields, read-only fields, custom labels).
- Multi-tenant support at the table/row level.
- UI for editing custom hooks & custom actions.
- Versioning & audit logging for dynamic CRUD operations.
DynAPI-Studio provides a powerful, extensible foundation for building data-driven applications on top of PostgreSQL, drastically reducing the amount of repetitive CRUD boilerplate while still respecting security, permissions, and real-time needs.