331 lines
11 KiB
Markdown
331 lines
11 KiB
Markdown
---
|
|
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! |