Skip to content
Jean Marie Daniel Vianney Guedegbe edited this page Nov 19, 2025 · 1 revision

DynAPI-Studio – Premium Technical Documentation

Tagline
“Turn any PostgreSQL schema into a live, permission-aware REST API with zero boilerplate.”


Table of Contents

  1. Introduction
    1.1 What is DynAPI-Studio?
    1.2 Key Features
    1.3 Typical Use Cases

  2. Project Structure & Architecture
    2.1 High-Level Layout
    2.2 Core Components

  3. Installation & Initial Setup
    3.1 Requirements
    3.2 Environment Variables
    3.3 Database & Redis
    3.4 Running the Project

  4. Configuration Deep Dive
    4.1 Django Settings
    4.2 REST Framework & JWT
    4.3 Channels & WebSockets
    4.4 URL Routing

  5. Domain Model: Users, Roles & Permissions
    5.1 User
    5.2 Role
    5.3 UserRole

  6. 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

  7. Permission System & Security
    7.1 TablePermission
    7.2 User-Level Table ACLs
    7.3 Throttling

  8. Authentication & User Management
    8.1 JWT Login
    8.2 User Registration
    8.3 Password Change & Admin Reset

  9. Admin Integration & UI
    9.1 Admin Overview Page
    9.2 Admin Permissions UI
    9.3 Inline Permissions on User

  10. Real-Time Updates via WebSockets
    10.1 WebSocket Consumer
    10.2 Server-Side Notifications
    10.3 Frontend Consumption Example

  11. Custom Hooks & Custom Actions
    11.1 Validation Hooks
    11.2 Custom Actions per Table

  12. Multi-Database Routing

  13. Management Commands

  14. Testing & Quality Assurance

  15. End-to-End Usage Scenario
    15.1 From Table Creation to API Consumption
    15.2 Bulk Permission Management

  16. Deployment Considerations

  17. Limitations & Future Improvements


1. Introduction

1.1 What is DynAPI-Studio?

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:

  1. Introspects your PostgreSQL public schema,
  2. Generates dynamic Django models (unmanaged),
  3. Creates DRF serializers and ModelViewSets on the fly,
  4. Registers REST routes under a dynamic namespace (/api/dyn/<table_name>/),
  5. Enforces per-user, per-table permissions stored in the database,
  6. Emits real-time WebSocket events on CRUD operations.

It is essentially a “database-driven API generator.”


1.2 Key Features

  • 🚀 Automatic CRUD generation for every business table (except excluded system tables).
  • 🧬 Dynamic models generated at runtime using PostgreSQL introspection.
  • 🔐 Fine-grained permissions: UserRole controls can_create, can_read, can_update, can_delete per 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.

1.3 Typical Use Cases

  • 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.

2. Project Structure & Architecture

2.1 High-Level Layout

The project uses a classic Django project/app structure:

  • Project: dynapi_studio/

    • settings.py – Django/DRF/Channels configuration
    • urls.py – root URL configuration
    • asgi.py – ASGI entrypoint (HTTP + WebSockets)
    • wsgi.py – WSGI entrypoint (HTTP only)
  • Core App: dynapi_core/

    • models.pyUser, Role, UserRole
    • db_introspection.py – PostgreSQL introspection services
    • generation.py – dynamic model/serializer/viewset generator
    • dynamic_models.py – namespace module for dynamic models
    • dynamic_registry.py – registry of dynamic model classes
    • api_router.py – DRF router setup and dynamic route registration
    • permissions.py – table-level permission class
    • views.py – REST views & viewsets (auth, user/role/userrole, admin config)
    • serializers.py & auth_serializers.py – serializers for static models & auth flows
    • admin.py – admin integration, custom views & UI injection
    • routing.py, ws_consumers.py – WebSocket routing and consumer
    • throttling.py – per-user throttle class
    • management/commands/* – CLI commands for introspection & route loading
    • tests.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).


2.2 Core Components

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()

3. Installation & Initial Setup

3.1 Requirements

  • 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

3.2 Environment Variables

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.


3.3 Database & Redis

  1. Create the primary PostgreSQL database (e.g. dynapi_studio_db).
  2. Optionally create the secondary database if you plan to route some dynamic tables there.
  3. 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 createsuperuser

3.4 Running the Project

For development:

# HTTP + WebSockets via ASGI server (e.g. daphne or uvicorn)
daphne dynapi_studio.asgi:application
# or
uvicorn dynapi_studio.asgi:application --reload

For classic WSGI (no WebSockets), you can still use:

python manage.py runserver

But WebSocket features will only work through the ASGI stack.


4. Configuration Deep Dive

4.1 Django Settings

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
  • DATABASES

    • default: PostgreSQL main DB
    • secondary: PostgreSQL secondary DB
    • DATABASE_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 extra contact field.

  • DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"


4.2 REST Framework & JWT

REST_FRAMEWORK:

  • Authentication:

    • JWTAuthentication (SimpleJWT)
    • SessionAuthentication (for browsable API & admin)
  • Permissions:

    • Default: IsAuthenticated – all API endpoints require authentication unless they explicitly override with AllowAny (e.g. health, login, register).
  • Schema:

    • DEFAULT_SCHEMA_CLASS = "drf_spectacular.openapi.AutoSchema"
    • OpenAPI schema at /api/schema/ and docs at /api/docs/, /api/redoc/.
  • Throttling:

    • DEFAULT_THROTTLE_CLASSESDynAPIPerUserThrottle
    • DEFAULT_THROTTLE_RATESdynapi: env or "1000/day".

SIMPLE_JWT defines token lifetimes & header behavior.


4.3 Channels & WebSockets

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))

4.4 URL Routing

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/ → includes dynapi_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

5. Domain Model: Users, Roles & Permissions

5.1 User

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.


5.2 Role

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.


5.3 UserRole

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_create
    • can_read
    • can_update
    • can_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.


6. Dynamic API Pipeline

6.1 Database Introspection

dynapi_core/db_introspection.py defines:

  • ColumnInfo dataclass:

    • name, data_type, is_nullable, is_primary_key, foreign_table, foreign_column.
  • TableInfo dataclass:

    • name
    • columns: List[ColumnInfo]
  • DatabaseIntrospector service:

    • list_tables()

      • Queries pg_catalog.pg_tables where schemaname = 'public'.
    • _get_foreign_keys(table_name)

      • Uses information_schema to detect FK relationships.
    • get_table_columns(table_name)

      • Uses pg_attribute, pg_class, pg_namespace, pg_index to gather column info.
    • get_all_tables_info()

      • Returns TableInfo for every table in the public schema.

This is the foundation for later steps: DynAPI must understand the schema before generating any API.


6.2 Dynamic Model Generation

dynapi_core/generation.py is the core engine.

Important classes & functions:

  • GeneratedResources dataclass:

    • model: models.Model
    • serializer: ModelSerializer
    • viewset: 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).

APIGenerator.build_all()

Steps:

  1. Fetch all TableInfo from DatabaseIntrospector.

  2. Clear the dynamic_registry to avoid stale classes.

  3. 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.
  4. 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.

_create_model_skeleton(table_info)

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.name
    • managed = 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).

_enhance_model_with_fk(...)

For each column where col.foreign_table is known:

  • Creates a ForeignKey to the corresponding temporary model using _create_foreign_key_field.
  • on_delete = DO_NOTHING, db_column = col.name.

_django_field_from_column(...)

Maps PostgreSQL types to Django field classes:

  • int, integer, etc. → IntegerField, BigIntegerField, SmallIntegerField
  • booleanBooleanField
  • varchar, character varying(n)CharField(max_length=n)
  • textTextField
  • numeric, decimalDecimalField(max_digits=20, decimal_places=6)
  • timestamp, date, timeDateTimeField, DateField, TimeField
  • uuidUUIDField
  • Fallback: TextField for unknown types or enums.

6.3 Dynamic Serializers

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.


6.4 Dynamic ViewSets

Each dynamic table is exposed via a tailored ModelViewSet:

Key properties:

  • queryset = model_cls.objects.select_related(*fk_fields).all()

    • fk_fields is 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 = all CharField and TextFields.
  • ordering_fields = filterset_fields, default ordering = ["-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).
  • perform_destroy:

    • Notifies "deleted" events before calling super().

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).

6.5 Dynamic Router & Route Registration

dynapi_core/api_router.py defines:

  • A global DefaultRouter instance.

  • 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.

_clear_dynamic_routes()

Removes any previously registered dynamic routes from the router registry, while keeping static ones.

_register_dynamic_routes()

  1. Ensures database is ready via is_database_ready().

  2. Gets all dynamic_resources = get_dynamic_resources().

  3. 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"
  },
  ...
]

reload_dynamic_routes()

  • 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_reload view in admin.py for admin usage.

6.6 Caching & Reloading

get_dynamic_resources():

  • Uses _DYNAMIC_RESOURCES_CACHE to avoid regeneration on every request.
  • Ensures DB connection is available.
  • Instantiates an APIGenerator and 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.

7. Permission System & Security

7.1 TablePermission

Located in dynapi_core/permissions.py.

For every request to a dynamic viewset:

  1. If the user is not authenticated → deny.

  2. If the view doesn’t define table_nameallow (used for non-dynamic views).

  3. Fetch UserRole for (user, table_name).

    • If not found → deny.
  4. Map HTTP method → permission:

    • Safe methods (GET, HEAD, OPTIONS) → can_read.
    • POSTcan_create.
    • PUT / PATCHcan_update.
    • DELETEcan_delete.

If the flag is False → request is denied.


7.2 User-Level Table ACLs

The UserRole model allows:

  • Per table, per user granular ACLs.
  • Optional association to a Role for 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 UserRole IDs.
  • Bulk assign a set of tables for a user (bulk-assign).

7.3 Throttling

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.


8. Authentication & User Management

8.1 JWT Login

  • DynAPITokenObtainPairSerializer extends SimpleJWT’s TokenObtainPairSerializer:

    • Adds username and email to JWT payload.
    • Logs last login via update_last_login.
    • Returns token pair + user info in the response.
  • LoginView (in views.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
  }
}

8.2 User Registration

RegisterView:

  • Endpoint: POST /api/auth/register/

  • Uses RegisterSerializer (auth_serializers.py), which:

    • Validates password + password_confirm.
    • Uses Django’s password_validation.
    • Creates a User and sets hashed password.

The view then issues JWT tokens (RefreshToken.for_user(user)) and returns them along with user info.


8.3 Password Change & Admin Reset

  • ChangePasswordView:

    • Endpoint: POST /api/auth/password/change/

    • Requires authenticated user.

    • Uses ChangePasswordSerializer to validate:

      • old_password matches current password.
      • new_password and new_password_confirm match and pass Django validation.
    • Saves the new password.

  • AdminResetPasswordView:

    • Endpoint: POST /api/auth/password/reset/admin/

    • Restricted to IsAdminUser.

    • Uses AdminResetPasswordSerializer:

      • Validates user_id exists.
      • Validates new password.
      • Sets and saves new password.

9. Admin Integration & UI

9.1 Admin Overview Page

dynapi_core/admin.py:

  • Defines custom views dynapi_overview and dynapi_permissions_view.
  • Hooks them into the admin using a monkey-patched admin.site.get_urls.

dynapi_overview:

  • Uses DatabaseIntrospector to list all tables.

  • Uses get_dynamic_resources() to see which tables are exposed.

  • Counts UserRole entries 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.

9.2 Admin Permissions UI

dynapi_permissions_view:

  • Provides a rich admin UI for managing UserRole entries:

    • Filtering bar: q, table, user.

    • Table of UserRole entries with:

      • user, role, table, each permission flag, and API endpoint path.
      • checkboxes to select multiple rows.
  • Two panels (with JS logic):

    1. Bulk update selected permissions:

      • Calls /api/user-roles/bulk-update/ via fetch.
      • Fields: can_create, can_read, can_update, can_delete (each can be “no change”, “allow”, “deny”).
    2. Bulk assign permissions for a user across multiple tables:

      • Calls /api/user-roles/bulk-assign/.

      • Inputs:

        • user_id, optional role_id,
        • list of table names (comma-separated),
        • permission flags.

The UI uses CSRF tokens and reloads the page after successful operation.


9.3 Inline Permissions on User

In UserAdmin (extends DjangoUserAdmin):

  • Adds extra fieldset “Informations supplémentaires” with contact.

  • Registers UserRoleInline as a TabularInline:

    • Allows editing of UserRole in the user’s detail page directly.

This is convenient for per-user permission administration.


10. Real-Time Updates via WebSockets

10.1 WebSocket Consumer

dynapi_core/ws_consumers.py:

DynAPITableConsumer(AsyncJsonWebsocketConsumer):

  • Connect:

    • Reads table_name from URL: /ws/dynapi/<table_name>/.
    • Joins group dynapi_<table_name>.
  • Disconnect:

    • Leaves the group.
  • dynapi_event(self, event):

    • Called when group_send emits a type="dynapi.event" message.

    • Sends JSON to the client:

      {
        "event": "created" | "updated" | "deleted",
        "table": "<table_name>",
        "pk": <primary_key>
      }

10.2 Server-Side Notifications

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
        pass

Called on:

  • perform_create: "created"
  • perform_update: "updated"
  • perform_destroy: "deleted"

10.3 Frontend Consumption Example

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.


11. Custom Hooks & Custom Actions

11.1 Validation Hooks

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 combined ValidationError if any.

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.


11.2 Custom Actions per Table

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 body payload.
  • Returns the result of the registered function.

This mechanism allows you to attach business-specific behaviors to dynamic resources without changing the generator itself.


12. Multi-Database Routing

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.
    • Else → None (use default).

  • db_for_write mirrors db_for_read.

  • allow_migrate always returns None to avoid migrations for dynamic models (managed = False).

This is handy when some external or special tables live in a different database.


13. Management Commands

Two custom commands:

  1. 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.
  2. test_introspection

    • Usage: python manage.py test_introspection
    • Uses DatabaseIntrospector to list tables and their columns, excluding Django and auth tables.
    • Useful for debugging schema issues.

14. Testing & Quality Assurance

dynapi_core/tests.py provides an end-to-end test class: DynamicAPIPipelineTests.

Test flow:

  1. Setup:

    • Creates two tables via raw SQL:

      • test_dynamic_table
      • test_dynamic_child with parent_id FK.
    • Calls reset_dynamic_resources_cache() and api_router.reload_dynamic_routes().

    • Retrieves _resources = get_dynamic_resources() for inspection.

    • Creates:

      • user with UserRole entries for both tables.
      • admin superuser.
  2. test_dynamic_models_generated:

    • Asserts that both tables exist in _resources.
    • Verifies model fields (including that parent_id is a ForeignKey with db_column="parent_id").
  3. 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 403 because can_delete=False).
      • Delete child (allowed).
  4. test_dynamic_reload_endpoint:

    • Authenticates as admin.
    • Calls POST /api/dyn/reload/.
    • Verifies the response contains the dynamic tables.

These tests validate:

  • Introspection still works.
  • Dynamic models & relationships are correctly generated.
  • Permissions are enforced as expected.
  • Reload endpoint functions correctly.

15. End-to-End Usage Scenario

15.1 From Table Creation to API Consumption

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.


15.2 Bulk Permission Management

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.


16. Deployment Considerations

  • ASGI server: Use daphne or uvicorn to serve dynapi_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=0 in production.
    • Configure ALLOWED_HOSTS.
    • Consider HTTPS termination (via reverse proxy or load balancer).
    • Carefully manage UserRole entries, especially can_delete=True and can_update=True.

17. Limitations & Future Improvements

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 TextField for 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.

Clone this wiki locally