mycroforge-docs/docs/tutorial.md
2024-10-06 13:24:20 +02:00

11 KiB

sidebar_position
3

Tutorial

In this tutorial, we'll build a simple todo app to demonstrate the capabilities of the MycroForge CLI. By the end, you should have a solid foundation to start exploring and using MycroForge for your projects.

General notes

The commands in this tutorial assume that you are running them from the root directory of your MycroForge project.

Initialize the Project

Open a terminal and navigate (cd) to the directory where your project should be created. Run the following command to initialize a new project and open it in VSCode:

m4g init todo-app

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.

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

:::info

If you're running on MacOS, Docker might complain about a platform mismatch for PhpMyAdmin. In that case you might need to specify the platform for the PhpMyAdmin image. You can do this by passing the --dbu-platform flag to m4g init. Run m4g init -? for all the available options. If you've already initialized a project, you can also change the platform prefix of the PhpMyAdmin image in the db.docker-compose.yml.

:::

Create the entities

Now that the database is running, we can start creating 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.

# Before
from pydantic import BaseModel


class CreateTodoRequest(BaseModel):
	description: str = None
	is_done: bool = None

# After
from typing import List, Optional
from pydantic import BaseModel


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
from pydantic import BaseModel
from typing import Optional


class UpdateTodoRequest(BaseModel):
	description: Optional[str] = None
	is_done: Optional[bool] = None

# After
from pydantic import BaseModel
from typing import List, Optional


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 def list(self) -> List[Todo]:
		async with async_session() as session:
			stmt = select(Todo)
			results = (await session.scalars(stmt)).all()
			return results

# After
	async def list(self) -> List[Todo]:
		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

Test the API!

Run the following command.

m4g api run

Go to http://localhost:5000/docs and test your Todo API!