Multi-App Metadata Decoration
Framework M is designed for "No-Cliff" extensibility. This means multiple independent applications (e.g., a localized Tax App, a WMS App, or an HR App) can inject fields, behaviors, and relationships into core DocTypes (like Company or User) without modifying the core source code.
This is achieved via the MetadataDecoratorRegistry.
1. Registering Dynamic Properties
Apps can inject new fields into existing DocTypes during the framework's bootstrap phase.
from framework_m_core.metadata_decorator import MetadataDecoratorRegistry
registry = MetadataDecoratorRegistry.get_instance()
# Inject a simple string field into the Company DocType
registry.register_property(
doctype_name="Company",
field_name="regional_tax_id",
field_type=str
)
Before the database schema is synchronized, the framework "bakes" these registered properties into the runtime Pydantic models. All standard routing, validation, and serialization will automatically respect the new fields.
2. Agnostic Hybrid Overflow (Persistence)
When injecting fields, developers must choose between high-performance SQL columns and flexible JSON overflow. Framework M handles the pack/unpack routing transparently.
persistence="json" (Default)
By default, injected fields are saved inside a generic custom_fields JSON column on the target table.
- Pros: Zero database migrations required. You can add hundreds of fields without hitting SQL column limits.
- Cons: Slightly slower indexing depending on the database dialect.
registry.register_property("Company", "eu_vat_id", str, persistence="json")
Note: The GenericRepository uses smart path filtering, meaning repository.list(filters=[...]) will automatically translate queries against eu_vat_id into native JSON extraction SQL paths!
persistence="column"
If an injected field requires high-performance indexing, foreign key constraints, or heavy analytical querying, you can force it to be mapped as a physical SQL column.
registry.register_property("Company", "india_gstin", str, persistence="column")
When the app boots, Alembic auto-migrations will detect this new physical requirement and generate an ALTER TABLE statement.
3. Zero-Overhead References
In a monolithic application, linking a StockEntry to a Supplier is easy: you use a Foreign Key.
However, Framework M supports decomposition into Macroservices. The Supplier DocType might physically live in a completely different database than the StockEntry DocType. Hardcoded SQL Foreign Keys will crash in this topology.
To solve this, use the reference_to parameter:
registry.register_property(
doctype_name="StockEntry",
field_name="supplier_id",
field_type=str,
reference_to="Supplier"
)
How it works:
- Database Level: The field is stored as a plain UUID string. No cross-database FK constraints are created, preventing crashes.
- UI Level: The Framework's JSON schema exposes this as a
link, allowing the frontend to render a standard dropdown picker. - Repository Level (Hydration):
- Indie Mode: If
M_MODE=indie, theGenericRepositoryperforms a highly optimized local SQL join/fetch to attachdoc.supplier. - Enterprise Mode: If
M_MODE=enterprise, the repository recognizes the entity lives elsewhere and seamlessly dispatches an RPC request over the NATS Event Bus (rpc.Supplier.get) to fetch and attachdoc.supplier.
- Indie Mode: If
4. Modifying Schema Layouts & Enums
If you need to mutate the actual JSON Schema (e.g., adding an option to an Enum or changing a field's label), you can register a decorator function.
def add_archived_status(schema: dict) -> dict:
# Safely append "Archived" to the existing enum options
schema["properties"]["status"]["enum"].append("Archived")
return schema
registry.register("Invoice", add_archived_status)
Conflict Resolution (Last-In-Wins)
If two apps attempt to override the same scalar value (e.g., title), the framework logs a warning to alert the developers, and applies a Last-In-Wins policy based on the order the apps were loaded during bootstrap.