--- 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: ```bash 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. ```bash 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)](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. ```bash m4g db generate entity Tag --column "description:str:String(255)" ``` ```bash 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. ```bash 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. ```bash 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. ```bash m4g db migrate ``` After running this command, you should now see a populated schema when visiting [PhpMyAdmin](http://localhost:5051). 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. ```bash m4g api generate crud Tag ``` ```bash 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`. ```python # 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`. ```python # 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`. ```python from sqlalchemy.orm import selectinload from db.entities.tag import Tag ``` Modify `TodoService.list` ```python # 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` ```python # 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` ```python # 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` ```python # 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. ```bash m4g api run ``` Go to http://localhost:5000/docs and test your Todo API!