mycroforge/readme/todo-example/README.md
mdnapo 02a82589ae - Refactored init & features
- Extended documentation
2024-07-14 22:27:32 +02:00

10 KiB

Quick tutorial

We're going to build a simple todo app, to demonstrate the capabilities of the MycroForge CLI. After this tutorial, you should have a solid foundation to start exploring and using MycroForge to develop your projects.

General notes

The commands in this tutorial assume that you're running them from a MycroForge root directory.

Initialize the project

Open a terminal and cd into the directory where your project should be created. Run m4g init todo-app to initialize a new project and open the newly created project by running code todo-app. Make sure you have vscode on you machine before running this command. If you prefer using another editor, then manually open the generated todo-app folder.

Setup the database

Our todo app needs to keep track of todos, so it needs a storage mechanism of some sorts. A database should be one of the first things, if not THE first thing, that comes to mind. Luckily, MycroForge provides you with a locally hosted database for you project out of the box. This setup, is powered by docker compose and can be examined by opening the db.docker-compose.yml file in the project. Follow along to learn how to use the docker based database when developing locally.

Run the database

The first step is to start the database, you can do this by running the following command in a terminal.

m4g db run

This command starts the services defined in the db.docker-compose.yml file. You can verify that the services are up by running docker container ls. If everything went well, then the previous command should output the service names defined in db.docker-compose.yml.

If you go to PhpMyAdmin (i.e. http://localhost:5051), you should now be able to login with the following credentials.

  • user: root
  • pass: password

When you're done developing, you can shut down the local database by running m4g db stop

Create the entities

Now that the database is running, we can start to create our entities. Run the commands below to create the Todo & Tag entities.

m4g db generate entity Tag --column "description:str:String(255)"

m4g db generate entity Todo --column "description:str:String(255)" -c "is_done:bool:Boolean()"

After running these commands, you should find the generated entities in the db/entities folder of your project. You should also see that the main.py & db/env.py files have been modified to include the newly generated entity.

For more information about the m4g db generate entity command, you can run m4g db generate entity -?.

Define a many-to-many relation between Todo & Tag

To allow for relations between Todo & Tag, we'll define a many-to-many relation between the two entities. This relation makes sense, because a Todo can have many Tags and a Tag could belong to many Todos. You can generate this relation by running the following command from you terminal. Creating a one-to-many relation would also make sense, but for the purpose of demonstration we're going to demonstrate the many-to-many relation, because this one is the most complex, since it requires an additional mapping to be included in the database schema.

m4g db link many Todo --to-many Tag

After running this command you should see that both the Todo and Tag entities now have a new field referencing the a List containing instances of the other entity.

For more information about the m4g db link command try running m4g db link -?. Note that you can do the same thing for all sub commands, so if you want to know more about m4g db link many you can simply run m4g db link many -? to examine the command. The same is true for all the other commands as well.

Generate the migration

Now that we've generated our entities, it's time to generate a migration that will apply these changes in the database. Generate the initial migration by running the following command.

m4g db generate migration initial_migration

After running this command, you should see the new migration in the db/version directory.

Apply the migration

The last step for the database setup is to actually apply the new migration to the database. This can be done by running the following command.

m4g db migrate

After running this command, you should now see a populated schema when visiting PhpMyAdmin. If for whatever reason you want to undo the last migration, you can simply run m4g db rollback.

Setup the API

Generate CRUD for Tag & Todo

Our API should provide use with basic endpoint to manage the Todo & Tag entities, i.e. CRUD functionality. Writing this code can be boring, since it's pretty much boilerplate with some custom additions sprinkled here and there. Fortunately, MycroForge can generate a good chunk of this boring code on your behalf. Run the following commands to generate CRUD functionality for the Todo & Tag classes.

m4g api generate crud Tag

m4g api generate crud Todo

After running this command you should see that the api/requests,api/routers & api/services now contain the relevant classes need to support the generated CRUD functionality. This could should be relatively straightforward, so we won't dive into it, but feel free to take a break and explore what the generated code actually does. Another thing to note, is that the generated routers are also automatically included in main.py.

Modify the generated Todo request classes

Since we have a many-to-many relationship between Todo & Tag, the generated CRUD functionality isn't quite ready yet. We need to be able to specify which Tags to add to a Todo when creating or updating it. To do this, we will allow for a tag_ids field in both the CreateTodoRequest & the UpdateTodoRequest. This field will contain the ids of the Tags that are associated with a Todo.

Modify CreateTodoRequest in api/requests/create_todo_request.py, you might need to import List from typing.

# Before
class CreateTodoRequest(BaseModel):
    description: str = None
    is_done: bool = None
    
# After
class CreateTodoRequest(BaseModel):
    description: str = None
    is_done: bool = None
    tag_ids: Optional[List[int]] = []

Modify UpdateTodoRequest in api/requests/update_todo_request.py, you might need to import List from typing.

# Before
class UpdateTodoRequest(BaseModel):
    description: Optional[str] = None
    is_done: Optional[bool] = None
    tag_ids: Optional[List[int]] = []

# After
class UpdateTodoRequest(BaseModel):
    description: Optional[str] = None
    is_done: Optional[bool] = None
    tag_ids: Optional[List[int]] = []

Modify generated TodoService

The TodoService will also need to be updated to accomodate the management of tag_ids.

Add the following imports in api/services/todo_service.py.

from sqlalchemy.orm import selectinload
from db.entities.tag import Tag

Modify TodoService.list

# Before
    async with async_session() as session:
        stmt = select(Todo)
        results = (await session.scalars(stmt)).all()
        return results

# After
    async with async_session() as session:
        stmt = select(Todo).options(selectinload(Todo.tags))
        results = (await session.scalars(stmt)).all()
        return results

Modify TodoService.get_by_id

# Before
    async def get_by_id(self, id: int) -> Optional[Todo]:
        async with async_session() as session:
            stmt = select(Todo).where(Todo.id == id)
            result = (await session.scalars(stmt)).first()
            return result

# After
    async def get_by_id(self, id: int) -> Optional[Todo]:
        async with async_session() as session:
            stmt = select(Todo).where(Todo.id == id).options(selectinload(Todo.tags))
            result = (await session.scalars(stmt)).first()
            return result

Modify TodoService.create

# Before
    async def create(self, data: Dict[str, Any]) -> None:
        async with async_session() as session:
            entity = Todo(**data)
            session.add(entity)
            await session.commit()

# After
    async def create(self, data: Dict[str, Any]) -> None:
        tag_ids = []
    
        if "tag_ids" in data.keys():
            tag_ids = data["tag_ids"]
            del data["tag_ids"]
    
        async with async_session() as session:
            entity = Todo(**data)
    
            if len(tag_ids) > 0:
                stmt = select(Tag).where(Tag.id.in_(tag_ids))
                result = (await session.scalars(stmt)).all()
                entity.tags = list(result)
    
            session.add(entity)
            await session.commit()

Modify TodoService.update

# Before
    async def update(self, id: int, data: Dict[str, Any]) -> bool:
        tag_ids = []
    
        if "tag_ids" in data.keys():
            tag_ids = data["tag_ids"]
            del data["tag_ids"]
    
        async with async_session() as session:
            stmt = select(Todo).where(Todo.id == id)
            entity = (await session.scalars(stmt)).first()
    
            if entity is None:
                return False
            else:
                for key, value in data.items():
                    setattr(entity, key, value)
                await session.commit()
                return True

# After
    async def update(self, id: int, data: Dict[str, Any]) -> bool:
        tag_ids = []
    
        if "tag_ids" in data.keys():
            tag_ids = data["tag_ids"]
            del data["tag_ids"]
    
        async with async_session() as session:
            stmt = select(Todo).where(Todo.id == id).options(selectinload(Todo.tags))
            entity = (await session.scalars(stmt)).first()
    
            if entity is None:
                return False
            else:
                for key, value in data.items():
                    setattr(entity, key, value)
    
                if len(tag_ids) > 0:
                    stmt = select(Tag).where(Tag.id.in_(tag_ids))
                    result = (await session.scalars(stmt)).all()
                    entity.tags = list(result)
                else:
                    entity.tags = []
    
                await session.commit()
                return True

At this point, the app should be ready to test. TODO: Elaborate!