# 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)](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](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. `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`. ```python # 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`. ```python # 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`. ```python from sqlalchemy.orm import selectinload from db.entities.tag import Tag ``` Modify `TodoService.list` ```python # 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` ```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 ``` At this point, the app should be ready to test. TODO: Elaborate!