Initial commit

This commit is contained in:
2024-10-06 13:24:20 +02:00
commit f547a0e132
65 changed files with 25280 additions and 0 deletions

28
docs/Commands/index.md Normal file
View File

@@ -0,0 +1,28 @@
---
sidebar_position: 5
---
# Commands
```
Description:
The MycroForge CLI tool.
Usage:
m4g [command] [options]
Options:
--version Show version information
-?, -h, --help Show help and usage information
Commands:
init <name> Initialize a new project
i, install <packages> Install packages and update the requirements.txt
u, uninstall <packages> Uninstall packages and update the requirements.txt
hydrate Initialize venv and install dependencies from requirements.txt
add Add features to the project
g, generate Generate common items
api API related commands
db Database related commands
p, plugin Plugin related commands
```

View File

@@ -0,0 +1,22 @@
---
sidebar_position: 1
---
# m4g add
```
Description:
Add features to the project
Usage:
m4g add [command] [options]
Options:
-?, -h, --help Show help and usage information
Commands:
api Add FastAPI to the project
db Add SQLAlchemy & Alembic to the project
git Add git to the project
gitignore Add a default .gitignore file to the project
```

View File

@@ -0,0 +1,17 @@
---
sidebar_position: 1
---
# m4g add api
```
Description:
Add FastAPI to the project
Usage:
m4g add api [options]
Options:
--api-port <api-port> The API port
-?, -h, --help Show help and usage information
```

View File

@@ -0,0 +1,19 @@
---
sidebar_position: 1
---
# m4g add db
```
Description:
Add SQLAlchemy & Alembic to the project
Usage:
m4g add db [options]
Options:
--database-host-port, --dbh-port <database-host-port> The database host port
--database-ui-port, --dbu-port <database-ui-port> The database UI port
--database-ui-platform, --dbu-platform <linux_amd64|linux_arm32v5|linux_arm32v6|linux_arm32v7|linux_arm64v8> The docker platform for the PhpMyAdmin image
-?, -h, --help Show help and usage information
```

View File

@@ -0,0 +1,16 @@
---
sidebar_position: 1
---
# m4g add git
```
Description:
Add git to the project
Usage:
m4g add git [options]
Options:
-?, -h, --help Show help and usage information
```

View File

@@ -0,0 +1,16 @@
---
sidebar_position: 1
---
# m4g add gitignore
```
Description:
Add a default .gitignore file to the project
Usage:
m4g add gitignore [options]
Options:
-?, -h, --help Show help and usage information
```

View File

@@ -0,0 +1,20 @@
---
sidebar_position: 1
---
# m4g api
```
Description:
API related commands
Usage:
m4g api [command] [options]
Options:
-?, -h, --help Show help and usage information
Commands:
run Run your app
g, generate Generate an API item
```

View File

@@ -0,0 +1,20 @@
---
sidebar_position: 1
---
# m4g api generate
```
Description:
Generate an API item
Usage:
m4g api generate [command] [options]
Options:
-?, -h, --help Show help and usage information
Commands:
r, router <name> Generate an api router
crud <entity> Generated CRUD functionality for an entity
```

View File

@@ -0,0 +1,19 @@
---
sidebar_position: 1
---
# m4g api generate crud
```
Description:
Generated CRUD functionality for an entity
Usage:
m4g api generate crud <entity> [options]
Arguments:
<entity> The entity to target
Options:
-?, -h, --help Show help and usage information
```

View File

@@ -0,0 +1,19 @@
---
sidebar_position: 1
---
# m4g api generate router
```
Description:
Generate an api router
Usage:
m4g api generate router <name> [options]
Arguments:
<name> The name of the api router
Options:
-?, -h, --help Show help and usage information
```

View File

@@ -0,0 +1,16 @@
---
sidebar_position: 1
---
# m4g api run
```
Description:
Run your app
Usage:
m4g api run [options]
Options:
-?, -h, --help Show help and usage information
```

View File

@@ -0,0 +1,24 @@
---
sidebar_position: 1
---
# m4g db
```
Description:
Database related commands
Usage:
m4g db [command] [options]
Options:
-?, -h, --help Show help and usage information
Commands:
run Runs the services defined in db.docker-compose.yml
stop Stops db.docker-compose.yml
migrate Apply migrations to the database
rollback Rollback the last migration
g, generate Generate a database item
link Define relationships between entities
```

View File

@@ -0,0 +1,20 @@
---
sidebar_position: 1
---
# m4g db generate
```
Description:
Generate a database item
Usage:
m4g db generate [command] [options]
Options:
-?, -h, --help Show help and usage information
Commands:
e, entity <name> Generate and database entity
m, migration <name> Generate a migration
```

View File

@@ -0,0 +1,35 @@
---
sidebar_position: 1
---
# m4g db generate entity
```
Description:
Generate and database entity
Usage:
m4g db generate entity <name> [options]
Arguments:
<name> The name of the database entity
Supported formats:
Entity
path/relative/to/entities:Entity
Options:
-c, --column <column> Specify the fields to add.
Format:
<name>:<native_type>:<orm_type>
<name> = Name of the column
<native_type> = The native Python type
<orm_type> = The SQLAlchemy type
Example:
first_name:str:String(255)
-?, -h, --help Show help and usage information
```

View File

@@ -0,0 +1,19 @@
---
sidebar_position: 1
---
# m4g db generate migration
```
Description:
Generate a migration
Usage:
m4g db generate migration <name> [options]
Arguments:
<name> The name of the migration
Options:
-?, -h, --help Show help and usage information
```

View File

@@ -0,0 +1,21 @@
---
sidebar_position: 1
---
# m4g db link
```
Description:
Define relationships between entities
Usage:
m4g db link [command] [options]
Options:
-?, -h, --help Show help and usage information
Commands:
one <entity> Define a 1:n relation
many <entity> Define a n:m relation
```

View File

@@ -0,0 +1,21 @@
---
sidebar_position: 1
---
# m4g db link many
```
Description:
Define a n:m relation
Usage:
m4g db link many <entity> [options]
Arguments:
<entity> The left side of the relation
Options:
--to-one <to-one> The right side of the relation
--to-many <to-many> The right side of the relation
-?, -h, --help Show help and usage information
```

View File

@@ -0,0 +1,21 @@
---
sidebar_position: 1
---
# m4g db link one
```
Description:
Define a 1:n relation
Usage:
m4g db link one <entity> [options]
Arguments:
<entity> The left side of the relation
Options:
--to-one <to-one> The right side of the relation
--to-many <to-many> The right side of the relation
-?, -h, --help Show help and usage information
```

View File

@@ -0,0 +1,16 @@
---
sidebar_position: 1
---
# m4g db migrate
```
Description:
Apply migrations to the database
Usage:
m4g db migrate [options]
Options:
-?, -h, --help Show help and usage information
```

View File

@@ -0,0 +1,16 @@
---
sidebar_position: 1
---
# m4g db rollback
```
Description:
Rollback the last migration
Usage:
m4g db rollback [options]
Options:
-?, -h, --help Show help and usage information
```

View File

@@ -0,0 +1,16 @@
---
sidebar_position: 1
---
# m4g db run
```
Description:
Runs the services defined in db.docker-compose.yml
Usage:
m4g db run [options]
Options:
-?, -h, --help Show help and usage information
```

View File

@@ -0,0 +1,16 @@
---
sidebar_position: 1
---
# m4g db stop
```
Description:
Stops db.docker-compose.yml
Usage:
m4g db stop [options]
Options:
-?, -h, --help Show help and usage information
```

View File

@@ -0,0 +1,20 @@
---
sidebar_position: 1
---
# m4g generate
```
Description:
Generate common items
Usage:
m4g generate [command] [options]
Options:
-?, -h, --help Show help and usage information
Commands:
s, service <name> Generate a service
venv Generate a venv
```

View File

@@ -0,0 +1,20 @@
---
sidebar_position: 1
---
# m4g generate service
```
Description:
Generate a service
Usage:
m4g generate service <name> [options]
Arguments:
<name> The name of the service
Options:
--with-session Create a service that uses database sessions
-?, -h, --help Show help and usage information
```

View File

@@ -0,0 +1,16 @@
---
sidebar_position: 1
---
# m4g generate venv
```
Description:
Generate a venv
Usage:
m4g generate venv [options]
Options:
-?, -h, --help Show help and usage information
```

View File

@@ -0,0 +1,16 @@
---
sidebar_position: 1
---
# m4g hydrate
```
Description:
Initialize venv and install dependencies from requirements.txt
Usage:
m4g hydrate [options]
Options:
-?, -h, --help Show help and usage information
```

24
docs/Commands/m4g_init.md Normal file
View File

@@ -0,0 +1,24 @@
---
sidebar_position: 1
---
# m4g init
```
Description:
Initialize a new project
Usage:
m4g init <name> [options]
Arguments:
<name> The name of your project
Options:
--without <api|db|git|gitignore> Features to exclude
--api-port <api-port> The API port
--database-host-port, --dbh-port <database-host-port> The database host port
--database-ui-port, --dbu-port <database-ui-port> The database UI port
--database-ui-platform, --dbu-platform <amd64|arm32v5|arm32v6|arm32v7|arm64v8> The docker platform for the PhpMyAdmin image
-?, -h, --help Show help and usage information
```

View File

@@ -0,0 +1,19 @@
---
sidebar_position: 1
---
# m4g install
```
Description:
Install packages and update the requirements.txt
Usage:
m4g install [<packages>...] [options]
Arguments:
<packages> The names of the packages to install
Options:
-?, -h, --help Show help and usage information
```

View File

@@ -0,0 +1,22 @@
---
sidebar_position: 1
---
# m4g plugin
```
Description:
Plugin related commands
Usage:
m4g plugin [command] [options]
Options:
-?, -h, --help Show help and usage information
Commands:
init <name> Initialize a basic plugin project
l, list, ls List all installed plugins
i, install Install a plugin
u, uninstall <name> Uninstall a plugin
```

View File

@@ -0,0 +1,19 @@
---
sidebar_position: 1
---
# m4g plugin init
```
Description:
Initialize a basic plugin project
Usage:
m4g plugin init <name> [options]
Arguments:
<name> The name of your project
Options:
-?, -h, --help Show help and usage information
```

View File

@@ -0,0 +1,17 @@
---
sidebar_position: 1
---
# m4g plugin install
```
Description:
Install a plugin
Usage:
m4g plugin install [options]
Options:
-p, --platform <linux_arm|linux_arm64|linux_x64|osx_arm64|osx_x64> (REQUIRED) The platform to target when building the plugin
-?, -h, --help Show help and usage information
```

View File

@@ -0,0 +1,16 @@
---
sidebar_position: 1
---
# m4g plugin list
```
Description:
List all installed plugins
Usage:
m4g plugin list [options]
Options:
-?, -h, --help Show help and usage information
```

View File

@@ -0,0 +1,19 @@
---
sidebar_position: 1
---
# m4g plugin uninstall
```
Description:
Uninstall a plugin
Usage:
m4g plugin uninstall [<name>...] [options]
Arguments:
<name> The names of the plugins you want to uninstall
Options:
-?, -h, --help Show help and usage information
```

View File

@@ -0,0 +1,20 @@
---
sidebar_position: 1
---
# m4g uninstall
```
Description:
Uninstall packages and update the requirements.txt
Usage:
m4g uninstall [<packages>...] [options]
Arguments:
<packages> The names of the packages to uninstall
Options:
-y, --yes Dont ask for confirmation of uninstall deletions
-?, -h, --help Show help and usage information
```

178
docs/command_plugins.md Normal file
View File

@@ -0,0 +1,178 @@
---
sidebar_position: 6
---
# Command plugins
MycroForge has a plugin system that allows you to extend the CLI with your own commands.
This section will guide you through the process of creating your own extension to the `m4g` command.
MycroForge is written in C# sharp and this is the same for plugins, so decent knowledge about `C#` & `.NET` is required.
In this tutorial we will create a command plugin that extens the `m4g` command with a `dotenv` sub command.
What this command will do is generate a `.env` file in the current directory and print a message to the console.
## Setup
To start creating command plugins for MycroFroge, make sure you've added the devdisciples package repository.
This can be done by running the following command.
```
dotnet nuget add source --name devdisciples https://git.devdisciples.com/api/packages/devdisciples/nuget/index.json
```
Run the following command to add the `MycroForge.PluginTemplate.Package`.
```
dotnet add package --source devdisciples --version 1.0.0 MycroForge.PluginTemplate.Package
```
## Initialize a plugin package
Generate a template plugin project by running the following command.
```
m4g plugin init My.Dotenv.Plugin
```
This should generate the following folder structure.
```
My.Dotenv.Plugin
┣ 📜HelloWorldCommand.cs
┣ 📜HelloWorldCommandPlugin.cs
┗ 📜My.Dotenv.Plugin.csproj
```
Rename the following files. Also rename the classes in these files, the easiest way in `vscode` is to right click the class name and select the `Rename symbol` action. Note that this action does not (necessarily) rename the files!
```
HelloWorldCommand.cs => DotenvCommand.cs
HelloWorldCommandPlugin.cs => DotenvCommandPlugin.cs
```
Modify `Name` property in `DotenvCommandPlugin.cs`.
```cs
// Before
public class DotenvCommandPlugin : ICommandPlugin
{
public string Name => "My.Plugin";
public void RegisterServices(IServiceCollection services)
{
services.AddScoped<ISubCommandOf<RootCommand>, HelloWorldCommand>();
}
}
// After
public class DotenvCommandPlugin : ICommandPlugin
{
public string Name => "My.Dotenv.Plugin";
public void RegisterServices(IServiceCollection services)
{
services.AddScoped<ISubCommandOf<RootCommand>, HelloWorldCommand>();
}
}
```
Modify `DotenvCommand.cs`.
```cs
// Before
public class DotenvCommand : Command, ISubCommandOf<RootCommand>
{
private readonly Argument<string> NameArgument =
new(name: "name", description: "The name of the person to greet");
private readonly Option<bool> AllCapsOption =
new(aliases: ["-a", "--all-caps"], description: "Print the name in all caps");
private readonly ProjectContext _context;
public DotenvCommand(ProjectContext context) :
base("hello", "An example command generated by dotnet new using the m4gp template")
{
_context = context;
AddArgument(NameArgument);
AddOption(AllCapsOption);
this.SetHandler(ExecuteAsync, NameArgument, AllCapsOption);
}
private async Task ExecuteAsync(string name, bool allCaps)
{
name = allCaps ? name.ToUpper() : name;
await _context.CreateFile("hello_world.txt",
$"Hello {name}!",
"This file was generated by your custom command!"
);
}
}
// After
public class DotenvCommand : Command, ISubCommandOf<RootCommand>
{
private readonly Argument<string> VarsArgument =
new(name: "vars", description: "Env vars to include in the .env file separated by ';'");
private readonly Option<bool> PrintOption =
new(aliases: ["-o", "--overwrite"], description: "Overwrite the .env file if it exists");
private readonly ProjectContext _context;
public DotenvCommand(ProjectContext context) :
// dotenv = the name of the sub command that will be added to the m4g command
base("dotenv", "Generate a .env file in the current directory")
{
_context = context;
AddArgument(VarsArgument);
AddOption(PrintOption);
this.SetHandler(ExecuteAsync, VarsArgument, PrintOption);
}
private async Task ExecuteAsync(string vars, bool overwrite)
{
var path = Path.Join(Environment.CurrentDirectory, ".env");
if (File.Exists(path))
{
if (overwrite)
{
await _context.WriteFile(".env", content);
return;
}
Console.WriteLine($"File {path} already exists, add the -o or --overwrite flag to overwrite it.");
}
var content = string.Join(Environment.NewLine, vars.Split(';'));
await _context.CreateFile(".env", content);
}
}
```
## Install the plugin
Open a terminal an make sure you're in the root directory of the plugin, i.e. the `My.Dotenv.Plugin` folder.
Run the following command to install the plugin.
```
m4g plugin install --platform <platform=linux_arm|linux_arm64|linux_x64|osx_arm64|osx_x64>
```
Make sure to choose the right platform option for your machine.
If everything went well then running `m4g` should now also show a `dotenv` command.
## Test the plugin
Try running `m4g dotenv "FIRSTNAME=JOHN;LASTNAME=JOE"`, this should generate a `.env` in the current directory with the vars you specified.
## Uninstall the plugin
Uninstall the plugin by running `m4g plugin install My.Dotenv.Plugin`.
## Resources
For examples of how the core commands are implemented, you can take a look at the commands in the [MycroForge.CLI.Commands](https://git.devdisciples.com/devdisciples/mycroforge/src/branch/main/MycroForge.CLI/Commands) namespace.
The MycroForge.CLI project uses [SystemCommand.Line](https://learn.microsoft.com/en-us/dotnet/standard/commandline/get-started-tutorial) for the CLI support, check out the Microsoft documentation for more info.

78
docs/getting_started.md Normal file
View File

@@ -0,0 +1,78 @@
---
sidebar_position: 2
---
# Getting Started
## Requirements
To use MycroForge, ensure you have the following dependencies installed:
- [**bash**](https://www.gnu.org/software/bash/)
- [**git**](https://git-scm.com/)
- [**Python 3.10**](https://www.python.org/downloads/release/python-3100/)
- [**Docker**](https://www.docker.com/)
- [**.NET 8**](https://dotnet.microsoft.com/en-us/download/dotnet/8.0)
- [**XCode Command Line Tools (MacOS only)**](https://mac.install.guide/commandlinetools/)
## Prior knowledge
Since this tool is meant to generate FastAPI & SQLAlchemy code, you should have decent knowledge of both frameworks. If not, then please follow the links below to learn more about them.
- [FastAPI](https://fastapi.tiangolo.com/)
- [SQLAlchemy](https://www.sqlalchemy.org/)
### Adding the Package Registry
Before installing MycroForge, add the package registry by running the following command:
```
dotnet nuget add source --name devdisciples https://git.devdisciples.com/api/packages/devdisciples/nuget/index.json
```
### Install
```
dotnet tool install -g MycroForge.CLI
```
### Uninstall
```
dotnet tool install -g MycroForge.CLI
```
### Windows
MycroForge is designed to run in a POSIX compliant environment. It has been tested on Windows using WSL2 with Ubuntu 22.04.03. So it is recommended to run MycroForge within the same or a similar WSL2 distribution for optimal performance.
### MacOS
#### Post install steps
After installing MycroForge, the dotnet CLI will show a message with some instructions to make the `m4g` command available in `zsh`.
It should look similar to the example below.
```sh
Tools directory '/Users/username/.dotnet/tools' is not currently on the PATH environment variable.
If you are using zsh, you can add it to your profile by running the following command:
cat << \EOF >> ~/.zprofile
# Add .NET Core SDK tools
export PATH="$PATH:/Users/username/.dotnet/tools"
EOF
And run zsh -l to make it available for current session.
You can only add it to the current session by running the following command:
export PATH="$PATH:/Users/username/.dotnet/tools"
```
### Known issues
#### FastAPI swagger blank screen (MacOS only)
If you see a blank screen when opening the FastAPI Swagger documentation, then make sure you've activated the Safari developer tools.
#### Database container doesn't start after running `m4g db run`
If the database, i.e. the mariadb container, doesn't successfully start after running `m4g db run` the first time, then try running it again. This often works, but yeah...it's hacky. However, at the time of writing it's unknown what causes this.

16
docs/intro.md Normal file
View File

@@ -0,0 +1,16 @@
---
sidebar_position: 1
---
# Intro
Welcome to **MycroForge** an opinionated CLI tool designed to streamline the development of FastAPI and SQLAlchemy-based backends. With MycroForge, you can effortlessly create backend projects through a convenient command line interface.
## Key Features
- **Project Skeleton Generation:** Quickly generate a well-structured project skeleton tailored for FastAPI and SQLAlchemy, ensuring you start with best practices.
- **Database Entities:** Easily create and manage database entities, simplifying your database interactions.
- **Migrations:** Handle database migrations seamlessly, allowing for smooth transitions and updates.
- **Routers:** Generate and manage routers to keep your application modular and organized.
- **CRUD Functionality:** Automatically generate basic CRUD (Create, Read, Update, Delete) operations to accelerate your development process.

111
docs/project_layout.md Normal file
View File

@@ -0,0 +1,111 @@
---
sidebar_position: 4
---
# Project layout
When you generate a new project with `m4g init <project_name>`, it will create a folder like the example below.
```
📦<project_name>
┣ 📂.git
┣ 📂.venv
┣ 📂api
┃ ┗ 📂routers
┃ ┃ ┗ 📜hello.py
┣ 📂db
┃ ┣ 📂engine
┃ ┃ ┗ 📜async_session.py
┃ ┣ 📂entities
┃ ┃ ┗ 📜entity_base.py
┃ ┣ 📂versions
┃ ┣ 📜README
┃ ┣ 📜env.py
┃ ┣ 📜script.py.mako
┃ ┗ 📜settings.py
┣ 📜.gitignore
┣ 📜alembic.ini
┣ 📜db.docker-compose.yml
┣ 📜m4g.json
┣ 📜main.py
┗ 📜requirements.txt
```
Let's go through these one by one.
### .git
The `m4g init` command will initialize new projects with git by default.
If you don't want to use git you can pass the option `--without git` to `m4g init`.
### .venv
To promote isolation of Python dependencies, new projects are initialized with a virtual environment by default.
When you clone a MycroForge repository from git, it won't have a `.venv` folder yet.
You can run `m4g hydrate` in the root folder of the project to restore the dependencies.
### api/routers/hello.py
This file defines a basic example router, which is imported and mapped in `main.py`. This router is just an example and
can be removed or modified at you discretion.
### db/engine/async_session.py
This file defines the `async_session` function, which can be used to open an asynchronous session to a database.
### db/entities/entity_base.py
This file contains an automatically generated entity base class that derives from the DeclarativeBase.
All entities must inherit from this class, so that SQLAlchemy & alembic can track them. The entities directory is also
where all newly generated entities will be stored.
### db/versions
This is where the generated database migrations will be stored.
### db/README
This README file is automatically generated by the alembic init command.
### db/env.py
This is the database environment file that is used by alembic to interact with the database.
If you take a closer look at the imports, you'll see that the file has been modified to assign `EntityBase.metadata` to
a variable called `target_metadata`, this will allow alembic to track changes in your entities. You'll also find that
the `DbSettings` class is used to get the connectionstring. Any time you generate a new database entity, or create a
many-to-many relation between two entities, this file will also be modified to include the generated classes.
### db/script.py.mako
This file is automatically generated by the alembic init command.
### db/settings.py
This file defines the `DbSettings` class, that is responsible for retrieving the database connectionstring.
You will probably want to modify this class to retrieve the connectionstring from a secret manager at some point.
### .gitignore
The default .gitignore file that is generated by the `m4g init` command. Modify this file at your discretion.
### alembic.ini
This file is automatically generated by the alembic init command.
### db.docker-compose.yml
A docker compose file for running a database locally.
### m4g.json
This file contains some configs that are used by the CLI, for example the ports to map to the API and database.
### main.py
The entrypoint for the application. When generating entities, many-to-many relations or routers, this file will be
modified to include the generated files.
### requirements.txt
The requirements file containing the Python dependencies.
Whenever you run `m4g install` or `m4g uninstall` this file will be updated too.

331
docs/tutorial.md Normal file
View File

@@ -0,0 +1,331 @@
---
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!