.. | ||
README.md |
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!