diff --git a/MycroForge.CLI/CodeGen/RequestClassGenerator.cs b/MycroForge.CLI/CodeGen/RequestClassGenerator.cs index d9a5f2e..96ae6d3 100644 --- a/MycroForge.CLI/CodeGen/RequestClassGenerator.cs +++ b/MycroForge.CLI/CodeGen/RequestClassGenerator.cs @@ -134,8 +134,8 @@ public class RequestClassGenerator var fullMatch = match.Groups[0].Value; // Ignore primary_key fields - if (fullMatch.IndexOf("primary_key", StringComparison.Ordinal) < - fullMatch.IndexOf("=", StringComparison.Ordinal)) continue; + if (fullMatch.IndexOf("=", StringComparison.Ordinal) < + fullMatch.IndexOf("primary_key", StringComparison.Ordinal)) continue; // Ignore relationship fields, these need to be done manually if (fullMatch.IndexOf("=", StringComparison.Ordinal) < diff --git a/MycroForge.CLI/Extensions/ServiceCollectionExtensions.cs b/MycroForge.CLI/Extensions/ServiceCollectionExtensions.cs index 302f5e1..421f627 100644 --- a/MycroForge.CLI/Extensions/ServiceCollectionExtensions.cs +++ b/MycroForge.CLI/Extensions/ServiceCollectionExtensions.cs @@ -33,7 +33,7 @@ public static class ServiceCollectionExtensions services.AddScoped, Commands.MycroForge.Api.Generate.Router>(); services.AddScoped, Commands.MycroForge.Api.Generate.Crud>(); - // Register "m4g orm" + // Register "m4g db" services.AddScoped, Commands.MycroForge.Db>(); services.AddScoped, Commands.MycroForge.Db.Migrate>(); services.AddScoped, Commands.MycroForge.Db.Rollback>(); diff --git a/docs/todo-example/README.md b/docs/todo-example/README.md new file mode 100644 index 0000000..0c33296 --- /dev/null +++ b/docs/todo-example/README.md @@ -0,0 +1,231 @@ +# Quick tutorial + +Summary: We're gonna build a todo app to demonstrate how the `m4g` command works.
+TODO: Supplement this section. + +## Initialize the project + +Run `m4g init todo-example` to initialize a new project. + +Open the newly created project by running `code todo-example`, make sure you have `vscode` on you machine before running +this command. If you prefer using another editor, then manually open the generated folder. + +## Setup the database + +Summary: This section explains how to interact with the database. +TODO: Supplement this section. + +### Create a docker compose file for the database + +Create a file called `docker-compose.yml` in the root of your project with the following content. + +```yaml +version: '3.8' + +services: + # MariaDB for the database + todo_db: + image: 'mariadb:10.4' + container_name: 'todo_db' + restart: unless-stopped + networks: + - default + ports: + - '3306:3306' + environment: + MYSQL_ROOT_PASSWORD: 'root' + MYSQL_USER: 'root' + MYSQL_PASSWORD: 'root' + MYSQL_DATABASE: 'todo' + volumes: + - 'todo_db:/var/lib/mysql' + + # PhpMyAdmin for the database UI + phpmyadmin: + image: phpmyadmin/phpmyadmin + container_name: phpmyadmin + restart: unless-stopped + ports: + - 8888:80 + networks: + - default + environment: + PMA_HOST: todo_db + PMA_PORT: 3306 + PMA_ARBITRARY: 1 + links: + - todo_db + +volumes: + todo_db: +``` + +Run `docker compose up -d` to start the database containers.
+You should now be able to log into the database server at http://localhost:8888, with the following credentials.
+**username**: root, **password**: root + +### Update the database connectionstring + +Replace the contents of `db/settings.py` with the following. + +```python +class DbSettings: + def get_connectionstring() -> str: + connectionstring = "mysql+asyncmy://root:root@localhost:3306/todo" + return connectionstring +``` + +The connectionstring has been hardcoded for demo purposes. +In a production app, you would use some kind of secret manager to retrieve it. + +### Create the entities + +`m4g db generate entity Tag -c "description:str:String(255)"` + +`m4g db generate entity Todo -c "description:str:String(255)" -c "is_done:bool:Boolean()"` + +### Define a many to many relation between Todo & Tag + +`m4g db link many Todo --to-many Tag` + +### Generate the migration + +`m4g db generate migration initial_migration` + +### Apply the migration + +`m4g db migrate` + +If you inspect the database in [PhpMyAdmin](http://localhost:8888), you should now see a populated schema. + +## Setup the API + +### Generate CRUD for Tag & Todo + +`m4g api generate crud Tag` + +`m4g api generate crud Todo` + +### Modify generated TodoService + +Add the following import in `api/services/todo_service.py`. + +`from sqlalchemy.orm import selectinload` + +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 +```