Step 4 - Defining Models & Permissions
Create standard SQLAlchemy 2.0 models and learn how ZCore auto-generates permission keys.
ZCore uses standard SQLAlchemy 2.0 declarative models. It does not force you to learn a new ORM syntax. Let's define our Task model.
Write the Model
Open tasks/models.py and define the model:
# tasks/models.py
import uuid
from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column
from zcore import Base, SoftDeleteMixin
class Task(Base, SoftDeleteMixin):
__tablename__ = "tasks"
id: Mapped[uuid.UUID] = mapped_column(
primary_key=True,
default=uuid.uuid4,
)
title: Mapped[str] = mapped_column(String(255))
is_completed: Mapped[bool] = mapped_column(default=False)
assignee_email: Mapped[str | None] = mapped_column(String(255), nullable=True)Create an Alembic Migration
ZCore does not use Base.metadata.create_all() as a replacement for database migrations.
For a real application, schema changes should be tracked through Alembic migrations. This gives you a versioned database schema that can be reproduced across development, staging, and production environments.
If your project does not have Alembic initialized yet, initialize it from the project root:
alembic init alembicConfigure Alembic to use your database connection and make sure its env.py can access the application's SQLAlchemy metadata.
Then generate a migration from the Task model:
alembic revision --autogenerate -m "create tasks table"Alembic will generate a migration inside the alembic/versions/ directory.
Review the generated migration before applying it. Autogenerated migrations should always be treated as a starting point rather than blindly trusted.
Finally, apply the migration:
alembic upgrade headWhy Alembic? Base.metadata.create_all() is useful for quick experiments, but it does not provide versioned schema changes, upgrade paths, downgrade paths, or a reliable migration history. Alembic is the appropriate tool for managing a production database schema.
After applying the migration, you can inspect the current database revision with:
alembic currentAnd inspect the complete migration history with:
alembic historyWhen you later modify the Task model, create a new migration instead of modifying the database manually:
alembic revision --autogenerate -m "add task field"Then apply the new migration:
alembic upgrade headThis keeps your database schema synchronized with your application's models through an explicit, version-controlled migration history.
The Magic of model.actions()
Under the hood: The Base class you inherited from provides a special classmethod called actions().
If you call Task.actions(), it returns an Actions dataclass mapping standard operations and lookups to permission strings based on your table name:
CREATE:"tasks:create"VIEW:"tasks:view"LISTVIEW:"tasks:listview"LOOKUP:"tasks:lookup"UPDATE:"tasks:update"DELETE:"tasks:delete"
We will use these strings later to secure our endpoints automatically.
Your database schema is ready. Next, we will define the Pydantic schemas (with security rules) for this model.