Compare commits
29 Commits
plugin-ref
...
bc1f0fb943
| Author | SHA1 | Date | |
|---|---|---|---|
| bc1f0fb943 | |||
| e95bd759c3 | |||
| 2918f87911 | |||
| 33a86882ac | |||
| d95ee39c0b | |||
| 9109e9a4c7 | |||
| 67a693b916 | |||
| b8373f6908 | |||
| b14a2e7117 | |||
| 207345b54b | |||
| dc8327289f | |||
| a88f8a1f11 | |||
| f676f236b1 | |||
| 4f322e56c7 | |||
| 4b0d1df34f | |||
| 128ae21088 | |||
| 6301bd438a | |||
| 3f33035611 | |||
| 32b7a3c01c | |||
| 91431fd996 | |||
| 5ccb40bb44 | |||
| d210c6ac7c | |||
| aa1c2422ef | |||
| 5698b504e9 | |||
| e2b2c82ff7 | |||
| 7badcc333b | |||
| bf27a344e1 | |||
| 577d61ed42 | |||
| 777b0fccc8 |
19
.dockerignore
Normal file
19
.dockerignore
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# directories
|
||||||
|
**/bin/
|
||||||
|
**/obj/
|
||||||
|
**/out/
|
||||||
|
.git
|
||||||
|
.idea
|
||||||
|
docs
|
||||||
|
.gitignore
|
||||||
|
.dockerignore
|
||||||
|
MycroForge.sln.DotSettings.user
|
||||||
|
nuget.docker-compose.yml
|
||||||
|
README.md
|
||||||
|
|
||||||
|
# files
|
||||||
|
Dockerfile*
|
||||||
|
**/*.md
|
||||||
|
|
||||||
|
#MycroForge.PluginTemplate
|
||||||
|
#MycroForge.PluginTemplate.Package
|
||||||
21
.gitea/workflows/gitea-ci.yml
Normal file
21
.gitea/workflows/gitea-ci.yml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
name: Test and build
|
||||||
|
run-name: ${{ gitea.actor }} is building MycroForge.CLI
|
||||||
|
on: [ push ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: https://github.com/actions/checkout@v4
|
||||||
|
- uses: https://github.com/actions/setup-dotnet@v4
|
||||||
|
with:
|
||||||
|
dotnet-version: '8.x'
|
||||||
|
# - name: "Run dotnet restore"
|
||||||
|
# run: dotnet restore
|
||||||
|
# - name: "Run dotnet build"
|
||||||
|
# run: dotnet build --no-restore
|
||||||
|
- name: "Reference MycroForge.Core in MycroForge.PluginTemplate"
|
||||||
|
run: dotnet add MycroForge.PluginTemplate reference MycroForge.Core
|
||||||
|
- name: "Run MycroForge.CLI.Tests"
|
||||||
|
# run: dotnet test --no-build --verbosity normal
|
||||||
|
run: dotnet test
|
||||||
30
Dockerfile
Normal file
30
Dockerfile
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
FROM mcr.microsoft.com/dotnet/sdk:8.0-bookworm-slim
|
||||||
|
|
||||||
|
# Copy the files to the /tool directory.
|
||||||
|
WORKDIR /tool
|
||||||
|
COPY . .
|
||||||
|
# Manually add a reference to MycroForge.Core in the MycroForge.PluginTemplate project,
|
||||||
|
# otherwise it will try to fetch it from a nuget repository.
|
||||||
|
RUN dotnet add MycroForge.PluginTemplate reference MycroForge.Core
|
||||||
|
|
||||||
|
# Install the tools required for testing
|
||||||
|
RUN apt update -y && \
|
||||||
|
apt upgrade -y && \
|
||||||
|
apt install -y git && \
|
||||||
|
apt install -y bash && \
|
||||||
|
apt install -y python3 && \
|
||||||
|
apt install -y python3-pip && \
|
||||||
|
apt install -y python3-venv
|
||||||
|
|
||||||
|
# Publish the CLI as a global tool and add the .dotnet/tools folder the the PATH variable.
|
||||||
|
WORKDIR /tool/MycroForge.CLI
|
||||||
|
RUN ./scripts/publish-tool.sh
|
||||||
|
ENV PATH="$PATH:/root/.dotnet/tools"
|
||||||
|
|
||||||
|
WORKDIR /test
|
||||||
|
|
||||||
|
SHELL ["/bin/bash", "-c"]
|
||||||
|
|
||||||
|
ENV PATH="$PATH:/root/.dotnet/tools"
|
||||||
|
|
||||||
|
CMD ["sleep", "infinity"]
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using DotNet.Testcontainers.Containers;
|
||||||
|
|
||||||
|
namespace MycroForge.CLI.Tests.Extensions;
|
||||||
|
|
||||||
|
internal static class ContainerInterfaceExtensions
|
||||||
|
{
|
||||||
|
public static Task<byte[]> ReadFileFromRootAsync(this IContainer container, string file)
|
||||||
|
{
|
||||||
|
return container.ReadFileAsync($"/test/todo/{file}");
|
||||||
|
}
|
||||||
|
}
|
||||||
1
MycroForge.CLI.Tests/GlobalUsings.cs
Normal file
1
MycroForge.CLI.Tests/GlobalUsings.cs
Normal file
@@ -0,0 +1 @@
|
|||||||
|
global using NUnit.Framework;
|
||||||
222
MycroForge.CLI.Tests/InitCommandTests.cs
Normal file
222
MycroForge.CLI.Tests/InitCommandTests.cs
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using DotNet.Testcontainers.Builders;
|
||||||
|
using DotNet.Testcontainers.Containers;
|
||||||
|
using DotNet.Testcontainers.Images;
|
||||||
|
using MycroForge.CLI.Tests.Extensions;
|
||||||
|
using MycroForge.Core;
|
||||||
|
|
||||||
|
namespace MycroForge.CLI.Tests;
|
||||||
|
|
||||||
|
public class InitCommandTests
|
||||||
|
{
|
||||||
|
private IFutureDockerImage Image = null!;
|
||||||
|
private IContainer Container = null!;
|
||||||
|
|
||||||
|
private async Task CreateImage()
|
||||||
|
{
|
||||||
|
Image = new ImageFromDockerfileBuilder()
|
||||||
|
.WithDockerfileDirectory(CommonDirectoryPath.GetSolutionDirectory(), string.Empty)
|
||||||
|
.WithDockerfile("Dockerfile")
|
||||||
|
.WithCleanUp(true)
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
await Image.CreateAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CreateContainer()
|
||||||
|
{
|
||||||
|
Container = new ContainerBuilder()
|
||||||
|
.WithImage(Image)
|
||||||
|
.WithCleanUp(true)
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
await Container.StartAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
[OneTimeSetUp]
|
||||||
|
public async Task SetupOnce()
|
||||||
|
{
|
||||||
|
await CreateImage();
|
||||||
|
|
||||||
|
await CreateContainer();
|
||||||
|
|
||||||
|
await Container.ExecAsync(["m4g", "init", "todo"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Creates_m4g_json()
|
||||||
|
{
|
||||||
|
var bytes = await Container.ReadFileFromRootAsync("m4g.json");
|
||||||
|
using var stream = new MemoryStream(bytes);
|
||||||
|
var config = await JsonSerializer.DeserializeAsync<ProjectConfig>(stream, DefaultJsonSerializerOptions.Default);
|
||||||
|
|
||||||
|
Assert.That(config, Is.Not.Null);
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(config!.Api, Is.Not.Null);
|
||||||
|
Assert.That(config.Api.Port, Is.EqualTo(8000));
|
||||||
|
Assert.That(config.Db, Is.Not.Null);
|
||||||
|
Assert.That(config.Db.DbhPort, Is.EqualTo(5050));
|
||||||
|
Assert.That(config.Db.DbuPort, Is.EqualTo(5051));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Creates_main_py()
|
||||||
|
{
|
||||||
|
var bytes = await Container.ReadFileFromRootAsync("main.py");
|
||||||
|
var text = Encoding.UTF8.GetString(bytes);
|
||||||
|
var lines = text.Split('\n');
|
||||||
|
|
||||||
|
// TestContext.WriteLine(text);
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(text, Is.Not.Empty);
|
||||||
|
Assert.That(lines, Does.Contain("from fastapi import FastAPI"));
|
||||||
|
Assert.That(lines, Does.Contain("from api.routers import hello"));
|
||||||
|
Assert.That(lines, Does.Contain("app = FastAPI()"));
|
||||||
|
Assert.That(lines, Does.Contain("app.include_router(prefix=\"/hello\", router=hello.router)"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Creates_gitignore()
|
||||||
|
{
|
||||||
|
var bytes = await Container.ReadFileFromRootAsync(".gitignore");
|
||||||
|
var text = Encoding.UTF8.GetString(bytes);
|
||||||
|
var lines = text.Split('\n');
|
||||||
|
|
||||||
|
// TestContext.WriteLine(text);
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(text, Is.Not.Empty);
|
||||||
|
Assert.That(lines, Does.Contain(".venv"));
|
||||||
|
Assert.That(lines, Does.Contain("__pycache__/"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Creates_alembic_ini()
|
||||||
|
{
|
||||||
|
var bytes = await Container.ReadFileFromRootAsync("alembic.ini");
|
||||||
|
var text = Encoding.UTF8.GetString(bytes);
|
||||||
|
var lines = text.Split('\n');
|
||||||
|
|
||||||
|
// TestContext.WriteLine(text);
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(text, Is.Not.Empty);
|
||||||
|
Assert.That(lines, Does.Contain("[alembic]"));
|
||||||
|
Assert.That(lines, Does.Contain("sqlalchemy.url = driver://user:pass@localhost/dbname"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Creates_db_docker_compose_yml()
|
||||||
|
{
|
||||||
|
var bytes = await Container.ReadFileFromRootAsync("db.docker-compose.yml");
|
||||||
|
var text = Encoding.UTF8.GetString(bytes);
|
||||||
|
var lines = text.Split('\n');
|
||||||
|
|
||||||
|
// TestContext.WriteLine(text);
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(text, Is.Not.Empty);
|
||||||
|
Assert.That(lines, Does.Contain(" todo_mariadb:"));
|
||||||
|
Assert.That(lines, Does.Contain(" container_name: 'todo_mariadb'"));
|
||||||
|
Assert.That(lines, Does.Contain(" - '${DBH_PORT}:3306'"));
|
||||||
|
Assert.That(lines, Does.Contain(" MYSQL_DATABASE: 'todo'"));
|
||||||
|
Assert.That(lines, Does.Contain(" - 'todo_mariadb:/var/lib/mysql'"));
|
||||||
|
Assert.That(lines, Does.Contain(" todo_phpmyadmin:"));
|
||||||
|
Assert.That(lines, Does.Contain(" container_name: todo_phpmyadmin"));
|
||||||
|
Assert.That(lines, Does.Contain(" PMA_HOST: todo_mariadb"));
|
||||||
|
Assert.That(lines, Does.Contain(" - todo_mariadb"));
|
||||||
|
Assert.That(lines, Does.Contain(" todo_mariadb:"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Creates_api__router_hello_py()
|
||||||
|
{
|
||||||
|
var bytes = await Container.ReadFileFromRootAsync("api/routers/hello.py");
|
||||||
|
var text = Encoding.UTF8.GetString(bytes);
|
||||||
|
var lines = text.Split('\n');
|
||||||
|
|
||||||
|
// TestContext.WriteLine(text);
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(text, Is.Not.Empty);
|
||||||
|
Assert.That(lines, Does.Contain("from fastapi import APIRouter"));
|
||||||
|
Assert.That(lines, Does.Contain("from fastapi.responses import JSONResponse"));
|
||||||
|
Assert.That(lines, Does.Contain("from fastapi.encoders import jsonable_encoder"));
|
||||||
|
Assert.That(lines, Does.Contain("router = APIRouter()"));
|
||||||
|
Assert.That(lines, Does.Contain("@router.get(\"/{name}\")"));
|
||||||
|
Assert.That(lines, Does.Contain("async def hello(name: str):"));
|
||||||
|
Assert.That(lines, Does.Contain("\treturn JSONResponse(status_code=200, content=jsonable_encoder({'greeting': f\"Hello, {name}!\"}))"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Creates_db__engine__async_session_py()
|
||||||
|
{
|
||||||
|
var bytes = await Container.ReadFileFromRootAsync("db/engine/async_session.py");
|
||||||
|
var text = Encoding.UTF8.GetString(bytes);
|
||||||
|
var lines = text.Split('\n');
|
||||||
|
|
||||||
|
// TestContext.WriteLine(text);
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(text, Is.Not.Empty);
|
||||||
|
Assert.That(lines, Does.Contain("from sqlalchemy.ext.asyncio import create_async_engine, AsyncEngine, AsyncSession"));
|
||||||
|
Assert.That(lines, Does.Contain("from db.settings import DbSettings"));
|
||||||
|
Assert.That(lines, Does.Contain("async_engine: AsyncEngine = create_async_engine(DbSettings.get_connectionstring())"));
|
||||||
|
Assert.That(lines, Does.Contain("def async_session() -> AsyncSession:"));
|
||||||
|
Assert.That(lines, Does.Contain("\treturn AsyncSession(async_engine, expire_on_commit=False)"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Creates_db__entities__entity_base_py()
|
||||||
|
{
|
||||||
|
var bytes = await Container.ReadFileFromRootAsync("db/entities/entity_base.py");
|
||||||
|
var text = Encoding.UTF8.GetString(bytes);
|
||||||
|
var lines = text.Split('\n');
|
||||||
|
|
||||||
|
// TestContext.WriteLine(text);
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(text, Is.Not.Empty);
|
||||||
|
Assert.That(lines, Does.Contain("from sqlalchemy.orm import DeclarativeBase"));
|
||||||
|
Assert.That(lines, Does.Contain("class EntityBase(DeclarativeBase):"));
|
||||||
|
Assert.That(lines, Does.Contain("\tpass"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Creates_db__settings_py()
|
||||||
|
{
|
||||||
|
var bytes = await Container.ReadFileFromRootAsync("db/settings.py");
|
||||||
|
var text = Encoding.UTF8.GetString(bytes);
|
||||||
|
var lines = text.Split('\n');
|
||||||
|
|
||||||
|
// TestContext.WriteLine(text);
|
||||||
|
|
||||||
|
Assert.Multiple(() =>
|
||||||
|
{
|
||||||
|
Assert.That(text, Is.Not.Empty);
|
||||||
|
Assert.That(lines, Does.Contain("class DbSettings:"));
|
||||||
|
Assert.That(lines, Does.Contain("\tdef get_connectionstring() -> str:"));
|
||||||
|
Assert.That(lines, Does.Contain("\t\tconnectionstring = \"mysql+asyncmy://root:password@localhost:5050/todo\""));
|
||||||
|
Assert.That(lines, Does.Contain("\t\treturn connectionstring"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
25
MycroForge.CLI.Tests/MycroForge.CLI.Tests.csproj
Normal file
25
MycroForge.CLI.Tests/MycroForge.CLI.Tests.csproj
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
<IsTestProject>true</IsTestProject>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0"/>
|
||||||
|
<PackageReference Include="NUnit" Version="3.13.3"/>
|
||||||
|
<PackageReference Include="NUnit3TestAdapter" Version="4.2.1"/>
|
||||||
|
<PackageReference Include="NUnit.Analyzers" Version="3.6.1"/>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.0"/>
|
||||||
|
<PackageReference Include="Testcontainers" Version="3.10.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\MycroForge.Core\MycroForge.Core.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using Humanizer;
|
using Humanizer;
|
||||||
|
using MycroForge.CLI.Commands;
|
||||||
using MycroForge.CLI.Extensions;
|
using MycroForge.CLI.Extensions;
|
||||||
using MycroForge.Core;
|
using MycroForge.Core;
|
||||||
|
|
||||||
@@ -6,6 +7,8 @@ namespace MycroForge.CLI.CodeGen;
|
|||||||
|
|
||||||
public class CrudRouterGenerator
|
public class CrudRouterGenerator
|
||||||
{
|
{
|
||||||
|
#region Templates
|
||||||
|
|
||||||
private static readonly string[] Template =
|
private static readonly string[] Template =
|
||||||
[
|
[
|
||||||
"from typing import Annotated",
|
"from typing import Annotated",
|
||||||
@@ -79,6 +82,8 @@ public class CrudRouterGenerator
|
|||||||
"\t\treturn JSONResponse(status_code=500, content=str(ex))",
|
"\t\treturn JSONResponse(status_code=500, content=str(ex))",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
private readonly ProjectContext _context;
|
private readonly ProjectContext _context;
|
||||||
|
|
||||||
public CrudRouterGenerator(ProjectContext context)
|
public CrudRouterGenerator(ProjectContext context)
|
||||||
@@ -86,65 +91,49 @@ public class CrudRouterGenerator
|
|||||||
_context = context;
|
_context = context;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Generate(string path, string entity)
|
public async Task Generate(FullyQualifiedName fqn)
|
||||||
{
|
{
|
||||||
var entitySnakeCaseName = entity.Underscore().ToLower();
|
var serviceClassName = $"{fqn.PascalizedName}Service";
|
||||||
var entityClassName = entity.Pascalize();
|
var entityRoutePrefix = fqn.PascalizedName.Kebaberize().Pluralize().ToLower();
|
||||||
var serviceClassName = $"{entityClassName}Service";
|
|
||||||
var entityRoutePrefix = entity.Kebaberize().Pluralize().ToLower();
|
|
||||||
|
|
||||||
var servicesFolderPath = $"{Features.Api.FeatureName}/services/{path}";
|
var serviceFilePath = Path.Join(
|
||||||
var serviceFilePath = $"{servicesFolderPath}/{entitySnakeCaseName}_service.py";
|
Features.Api.FeatureName, "services", fqn.Namespace, $"{fqn.SnakeCasedName}_service"
|
||||||
var serviceImportPath = serviceFilePath
|
);
|
||||||
.Replace('/', '.')
|
|
||||||
.Replace('\\', '.')
|
var serviceImportPath = serviceFilePath.SlashesToDots();
|
||||||
.Replace(".py", string.Empty)
|
var routerFolderPath = Path.Join(Features.Api.FeatureName, "routers", fqn.Namespace);
|
||||||
.DeduplicateDots()
|
var routerFilePath = Path.Join(routerFolderPath, $"{fqn.SnakeCasedName}");
|
||||||
.Trim();
|
var routerImportPath = routerFolderPath.SlashesToDots();
|
||||||
|
var requestsFolderPath = Path.Join(Features.Api.FeatureName, "requests", fqn.Namespace);
|
||||||
|
|
||||||
var routersFolderPath = $"{Features.Api.FeatureName}/routers/{path}";
|
var createRequestImportPath = Path.Join(requestsFolderPath, $"Create{fqn.PascalizedName}Request")
|
||||||
var routerFilePath = $"{routersFolderPath}/{entitySnakeCaseName}.py";
|
.SlashesToDots()
|
||||||
var routerImportPath = routersFolderPath
|
|
||||||
.Replace('/', '.')
|
|
||||||
.Replace('\\', '.')
|
|
||||||
.Replace(".py", "")
|
|
||||||
.DeduplicateDots()
|
|
||||||
.Trim();
|
|
||||||
|
|
||||||
var requestsFolderPath = $"{Features.Api.FeatureName}/requests/{path}";
|
|
||||||
|
|
||||||
var createRequestImportPath = $"{requestsFolderPath}/Create{entityClassName}Request"
|
|
||||||
.Replace('/', '.')
|
|
||||||
.Replace('\\', '.')
|
|
||||||
.DeduplicateDots()
|
|
||||||
.Underscore()
|
.Underscore()
|
||||||
.ToLower();
|
.ToLower();
|
||||||
var createRequestClassName = $"Create{entityClassName}Request";
|
var createRequestClassName = $"Create{fqn.PascalizedName}Request";
|
||||||
|
|
||||||
var updateRequestImportPath = $"{requestsFolderPath}/Update{entityClassName}Request"
|
var updateRequestImportPath = Path.Join(requestsFolderPath, $"Update{fqn.PascalizedName}Request")
|
||||||
.Replace('/', '.')
|
.SlashesToDots()
|
||||||
.Replace('\\', '.')
|
|
||||||
.DeduplicateDots()
|
|
||||||
.Underscore()
|
.Underscore()
|
||||||
.ToLower();
|
.ToLower();
|
||||||
var updateRequestClassName = $"Update{entityClassName}Request";
|
var updateRequestClassName = $"Update{fqn.PascalizedName}Request";
|
||||||
|
|
||||||
var router = string.Join("\n", Template)
|
var router = string.Join("\n", Template)
|
||||||
.Replace("%service_import_path%", serviceImportPath)
|
.Replace("%service_import_path%", serviceImportPath)
|
||||||
.Replace("%entity_class_name%", entityClassName)
|
.Replace("%entity_class_name%", fqn.PascalizedName)
|
||||||
.Replace("%service_class_name%", serviceClassName)
|
.Replace("%service_class_name%", serviceClassName)
|
||||||
.Replace("%create_entity_request_import_path%", createRequestImportPath)
|
.Replace("%create_entity_request_import_path%", createRequestImportPath)
|
||||||
.Replace("%create_entity_request_class_name%", createRequestClassName)
|
.Replace("%create_entity_request_class_name%", createRequestClassName)
|
||||||
.Replace("%update_entity_request_import_path%", updateRequestImportPath)
|
.Replace("%update_entity_request_import_path%", updateRequestImportPath)
|
||||||
.Replace("%update_entity_request_class_name%", updateRequestClassName);
|
.Replace("%update_entity_request_class_name%", updateRequestClassName);
|
||||||
|
|
||||||
await _context.CreateFile(routerFilePath, router);
|
await _context.CreateFile($"{routerFilePath}.py", router);
|
||||||
|
|
||||||
var main = await _context.ReadFile("main.py");
|
var main = await _context.ReadFile("main.py");
|
||||||
|
|
||||||
main = new MainModifier(main).Initialize()
|
main = new MainModifier(main).Initialize()
|
||||||
.Import(from: routerImportPath, import: entitySnakeCaseName)
|
.Import(from: routerImportPath, import: fqn.SnakeCasedName)
|
||||||
.IncludeRouter(prefix: entityRoutePrefix, router: entitySnakeCaseName)
|
.IncludeRouter(prefix: entityRoutePrefix, router: fqn.SnakeCasedName)
|
||||||
.Rewrite();
|
.Rewrite();
|
||||||
|
|
||||||
await _context.WriteFile("main.py", main);
|
await _context.WriteFile("main.py", main);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using Humanizer;
|
using MycroForge.CLI.Commands;
|
||||||
using MycroForge.CLI.Extensions;
|
|
||||||
using MycroForge.Core;
|
using MycroForge.Core;
|
||||||
|
|
||||||
namespace MycroForge.CLI.CodeGen;
|
namespace MycroForge.CLI.CodeGen;
|
||||||
@@ -65,26 +64,15 @@ public class CrudServiceGenerator
|
|||||||
_context = context;
|
_context = context;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Generate(string path, string entity)
|
public async Task Generate(FullyQualifiedName fqn)
|
||||||
{
|
{
|
||||||
var entitySnakeCaseName = entity.Underscore().ToLower();
|
var entityImportPath = fqn.GetImportPath(root: [Features.Db.FeatureName, "entities"]);
|
||||||
var entityClassName = entity.Pascalize();
|
|
||||||
|
|
||||||
var entitiesFolderPath = $"{Features.Db.FeatureName}/entities/{path}";
|
var serviceFilePath = Path.Join(Features.Api.FeatureName, "services", $"{fqn.FilePath}_service.py");
|
||||||
var entityFilePath = $"{entitiesFolderPath}/{entitySnakeCaseName}.py";
|
|
||||||
var entityImportPath = entityFilePath
|
|
||||||
.Replace('/', '.')
|
|
||||||
.Replace('\\', '.')
|
|
||||||
.Replace(".py", string.Empty)
|
|
||||||
.DeduplicateDots()
|
|
||||||
.Trim();
|
|
||||||
|
|
||||||
var servicesFolderPath = $"{Features.Api.FeatureName}/services/{path}";
|
|
||||||
var serviceFilePath = $"{servicesFolderPath}/{entity.Underscore().ToLower()}_service.py";
|
|
||||||
|
|
||||||
var service = string.Join("\n", Template)
|
var service = string.Join("\n", Template)
|
||||||
.Replace("%entity_import_path%", entityImportPath)
|
.Replace("%entity_import_path%", entityImportPath)
|
||||||
.Replace("%entity_class_name%", entityClassName)
|
.Replace("%entity_class_name%", fqn.PascalizedName)
|
||||||
;
|
;
|
||||||
|
|
||||||
await _context.CreateFile(serviceFilePath, service);
|
await _context.CreateFile(serviceFilePath, service);
|
||||||
|
|||||||
@@ -96,14 +96,18 @@ public partial class EntityLinker
|
|||||||
var left = await LoadEntity(_left);
|
var left = await LoadEntity(_left);
|
||||||
var right = await LoadEntity(_right);
|
var right = await LoadEntity(_right);
|
||||||
|
|
||||||
var associationTable = string.Join('\n', AssociationTable);
|
var associationTable = string.Join('\n', AssociationTable)
|
||||||
associationTable = associationTable
|
|
||||||
.Replace("%left_entity%", left.ClassName.Underscore().ToLower())
|
.Replace("%left_entity%", left.ClassName.Underscore().ToLower())
|
||||||
.Replace("%right_entity%", right.ClassName.Underscore().ToLower())
|
.Replace("%right_entity%", right.ClassName.Underscore().ToLower())
|
||||||
.Replace("%left_table%", left.TableName)
|
.Replace("%left_table%", left.TableName)
|
||||||
.Replace("%right_table%", right.TableName);
|
.Replace("%right_table%", right.TableName);
|
||||||
var associationTablePath =
|
|
||||||
$"{Features.Db.FeatureName}/entities/associations/{left.TableName.Singularize()}_{right.TableName.Singularize()}_mapping.py";
|
var associationTablePath = Path.Join(
|
||||||
|
Features.Db.FeatureName,
|
||||||
|
"entities",
|
||||||
|
"associations",
|
||||||
|
$"{left.TableName.Singularize()}_{right.TableName.Singularize()}_mapping.py"
|
||||||
|
);
|
||||||
|
|
||||||
await _context.CreateFile(associationTablePath, associationTable);
|
await _context.CreateFile(associationTablePath, associationTable);
|
||||||
|
|
||||||
@@ -136,21 +140,25 @@ public partial class EntityLinker
|
|||||||
var env = await _context.ReadFile($"{Features.Db.FeatureName}/env.py");
|
var env = await _context.ReadFile($"{Features.Db.FeatureName}/env.py");
|
||||||
env = new DbEnvModifier(env, associationTableImportPath, associationTableImportName).Rewrite();
|
env = new DbEnvModifier(env, associationTableImportPath, associationTableImportName).Rewrite();
|
||||||
await _context.WriteFile($"{Features.Db.FeatureName}/env.py", env);
|
await _context.WriteFile($"{Features.Db.FeatureName}/env.py", env);
|
||||||
|
|
||||||
var main = await _context.ReadFile("main.py");
|
var main = await _context.ReadFile("main.py");
|
||||||
main = new MainModifier(main).Initialize().Import(associationTableImportPath, associationTableImportName).Rewrite();
|
main = new MainModifier(main)
|
||||||
|
.Initialize()
|
||||||
|
.Import(associationTableImportPath, associationTableImportName)
|
||||||
|
.Rewrite();
|
||||||
|
|
||||||
await _context.WriteFile("main.py", main);
|
await _context.WriteFile("main.py", main);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<EntityModel> LoadEntity(string name)
|
private async Task<EntityModel> LoadEntity(string name)
|
||||||
{
|
{
|
||||||
var fqn = new FullyQualifiedName(name);
|
var fqn = new FullyQualifiedName(name);
|
||||||
var path = $"{Features.Db.FeatureName}/entities";
|
var path = Path.Join(Features.Db.FeatureName, "entities");
|
||||||
|
|
||||||
if (fqn.HasPath)
|
if (fqn.HasNamespace)
|
||||||
path = Path.Combine(path, fqn.Path);
|
path = Path.Join(path, fqn.Namespace);
|
||||||
|
|
||||||
path = Path.Combine(path, $"{fqn.SnakeCasedName}.py");
|
path = Path.Join(path, $"{fqn.SnakeCasedName}.py");
|
||||||
var entity = new EntityModel(fqn.PascalizedName, path, await _context.ReadFile(path));
|
var entity = new EntityModel(fqn.PascalizedName, path, await _context.ReadFile(path));
|
||||||
entity.Initialize();
|
entity.Initialize();
|
||||||
return entity;
|
return entity;
|
||||||
|
|||||||
@@ -38,12 +38,14 @@ public class MainModifier
|
|||||||
|
|
||||||
public MainModifier IncludeRouter(string prefix, string router)
|
public MainModifier IncludeRouter(string prefix, string router)
|
||||||
{
|
{
|
||||||
_routerIncludeBuffer.Add($"\napp.include_router(prefix=\"/{prefix}\", router={router}.router)\n");
|
_routerIncludeBuffer.Add($"app.include_router(prefix=\"/{prefix}\", router={router}.router)");
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Rewrite()
|
public string Rewrite()
|
||||||
{
|
{
|
||||||
|
// Make sure to insert the includes before the imports, if done the other way around,
|
||||||
|
// the insertions of the includes will change the indexes of the imports.
|
||||||
InsertIncludes();
|
InsertIncludes();
|
||||||
|
|
||||||
InsertImports();
|
InsertImports();
|
||||||
@@ -54,7 +56,7 @@ public class MainModifier
|
|||||||
private void InsertImports()
|
private void InsertImports()
|
||||||
{
|
{
|
||||||
if (_importsBuffer.Count == 0) return;
|
if (_importsBuffer.Count == 0) return;
|
||||||
|
|
||||||
if (_lastImport is not null)
|
if (_lastImport is not null)
|
||||||
{
|
{
|
||||||
_source.InsertMultiLine(_lastImport.EndIndex, _importsBuffer.ToArray());
|
_source.InsertMultiLine(_lastImport.EndIndex, _importsBuffer.ToArray());
|
||||||
@@ -69,15 +71,17 @@ public class MainModifier
|
|||||||
{
|
{
|
||||||
if (_routerIncludeBuffer.Count == 0) return;
|
if (_routerIncludeBuffer.Count == 0) return;
|
||||||
|
|
||||||
|
// Prepend an empty string to the router include buffer,
|
||||||
|
// this will ensure that the new entries are all on separate lines.
|
||||||
|
var content = _routerIncludeBuffer.Prepend(string.Empty).ToArray();
|
||||||
|
|
||||||
if (_lastRouterInclude is not null)
|
if (_lastRouterInclude is not null)
|
||||||
{
|
{
|
||||||
_source.InsertMultiLine(
|
_source.InsertMultiLine(_lastRouterInclude.EndIndex, content);
|
||||||
_lastRouterInclude.EndIndex, _routerIncludeBuffer.ToArray()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
_source.InsertMultiLineAtEnd(_routerIncludeBuffer.ToArray());
|
_source.InsertMultiLineAtEnd(content);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,19 +1,22 @@
|
|||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using Humanizer;
|
using Humanizer;
|
||||||
|
using MycroForge.CLI.Commands;
|
||||||
using MycroForge.Core;
|
using MycroForge.Core;
|
||||||
|
|
||||||
namespace MycroForge.CLI.CodeGen;
|
namespace MycroForge.CLI.CodeGen;
|
||||||
|
|
||||||
public class RequestClassGenerator
|
public class RequestClassGenerator
|
||||||
{
|
{
|
||||||
public record Import(string Name, List<string> Types)
|
private static readonly List<string> PythonTypingImports = ["Any", "Dict", "List", "Optional"];
|
||||||
|
|
||||||
|
private record Import(string Name, List<string> Types)
|
||||||
{
|
{
|
||||||
public bool Match(string type) => Types.Any(t => type == t || type.StartsWith(t));
|
public bool Match(string type) => Types.Any(t => type == t || type.StartsWith(t));
|
||||||
|
|
||||||
public string FindType(string type) => Types.First(t => type == t || type.StartsWith(t));
|
public string FindType(string type) => Types.First(t => type == t || type.StartsWith(t));
|
||||||
};
|
};
|
||||||
|
|
||||||
public record Field(string Name, string Type);
|
private record Field(string Name, string Type);
|
||||||
|
|
||||||
public enum Type
|
public enum Type
|
||||||
{
|
{
|
||||||
@@ -40,29 +43,28 @@ public class RequestClassGenerator
|
|||||||
_context = context;
|
_context = context;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Generate(string path, string entity, Type type)
|
public async Task Generate(FullyQualifiedName fqn, Type type)
|
||||||
{
|
{
|
||||||
var entitySnakeCaseName = entity.Underscore().ToLower();
|
var entityFilePath = Path.Join(Features.Db.FeatureName, "entities", $"{fqn.FilePath}.py");
|
||||||
var entityClassName = entity.Pascalize();
|
|
||||||
var entitiesFolderPath = $"{Features.Db.FeatureName}/entities/{path}";
|
|
||||||
var entityFilePath = $"{entitiesFolderPath}/{entitySnakeCaseName}.py";
|
|
||||||
var entitySource = await _context.ReadFile(entityFilePath);
|
var entitySource = await _context.ReadFile(entityFilePath);
|
||||||
|
|
||||||
var fieldInfo = ReadFields(entitySource);
|
var fieldInfo = ReadFields(entitySource);
|
||||||
var fields = string.Join('\n', fieldInfo.Select(x => ToFieldString(x, type)));
|
var fields = string.Join('\n', fieldInfo.Select(x => ToFieldString(x, type)));
|
||||||
|
|
||||||
var requestsFolderPath = $"{Features.Api.FeatureName}/requests/{path}";
|
var requestFilePath = Path.Join(
|
||||||
var updateRequestFilePath =
|
Features.Api.FeatureName,
|
||||||
$"{requestsFolderPath}/{type.ToString().ToLower()}_{entitySnakeCaseName}_request.py";
|
"requests",
|
||||||
|
fqn.Namespace,
|
||||||
|
$"{type.ToString().ToLower()}_{fqn.SnakeCasedName}_request.py"
|
||||||
|
);
|
||||||
|
|
||||||
var service = string.Join("\n", Template)
|
var service = string.Join("\n", Template)
|
||||||
.Replace("%imports%", GetImportString(entitySource, fieldInfo, type))
|
.Replace("%imports%", GetImportString(entitySource, fieldInfo, type))
|
||||||
.Replace("%request_type%", type.ToString().Pascalize())
|
.Replace("%request_type%", type.ToString().Pascalize())
|
||||||
.Replace("%entity_class_name%", entityClassName)
|
.Replace("%entity_class_name%", fqn.PascalizedName)
|
||||||
.Replace("%fields%", fields)
|
.Replace("%fields%", fields)
|
||||||
;
|
;
|
||||||
|
|
||||||
await _context.CreateFile(updateRequestFilePath, service);
|
await _context.CreateFile(requestFilePath, service);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string ToFieldString(Field field, Type type)
|
private string ToFieldString(Field field, Type type)
|
||||||
@@ -100,11 +102,12 @@ public class RequestClassGenerator
|
|||||||
.Replace(" ", "");
|
.Replace(" ", "");
|
||||||
Console.WriteLine(str); // = "List,Dict,str,Any"
|
Console.WriteLine(str); // = "List,Dict,str,Any"
|
||||||
*/
|
*/
|
||||||
var dissectedTypes = field.Type.Replace("[", ",")
|
var dissectedTypes = field.Type
|
||||||
|
.Replace("[", ",")
|
||||||
.Replace("]", "")
|
.Replace("]", "")
|
||||||
.Replace(" ", "")
|
.Replace(" ", "")
|
||||||
.Split();
|
.Split(',');
|
||||||
|
|
||||||
foreach (var dissectedType in dissectedTypes)
|
foreach (var dissectedType in dissectedTypes)
|
||||||
{
|
{
|
||||||
if (imports.FirstOrDefault(i => i.Match(dissectedType)) is Import import)
|
if (imports.FirstOrDefault(i => i.Match(dissectedType)) is Import import)
|
||||||
@@ -164,16 +167,16 @@ public class RequestClassGenerator
|
|||||||
.Split(',')
|
.Split(',')
|
||||||
.Select(s => s.Trim())
|
.Select(s => s.Trim())
|
||||||
.ToArray();
|
.ToArray();
|
||||||
imports.Add(new Import(name, [..types]));
|
imports.Add(new Import(name, new List<string>(types)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (imports.FirstOrDefault(i => i.Name == "typing") is Import typingImport)
|
if (imports.FirstOrDefault(i => i.Name == "typing") is Import typingImport)
|
||||||
{
|
{
|
||||||
typingImport.Types.AddRange(["Any", "Dict", "List", "Optional"]);
|
typingImport.Types.AddRange(PythonTypingImports);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
imports.Add(new("typing", ["Any", "Dict", "List", "Optional"]));
|
imports.Add(new Import("typing", PythonTypingImports));
|
||||||
}
|
}
|
||||||
|
|
||||||
return imports;
|
return imports;
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
namespace MycroForge.CLI.Commands.Attributes;
|
||||||
|
|
||||||
|
public class RequiresFeatureAttribute : Attribute
|
||||||
|
{
|
||||||
|
public string FeatureName { get; }
|
||||||
|
|
||||||
|
public RequiresFeatureAttribute(string featureName)
|
||||||
|
{
|
||||||
|
FeatureName = featureName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RequireFeature(string command)
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(Path.Join(Environment.CurrentDirectory, FeatureName)))
|
||||||
|
throw new($"Command '{command}' requires feature {FeatureName}");
|
||||||
|
}
|
||||||
|
}
|
||||||
17
MycroForge.CLI/Commands/Attributes/RequiresFileAttribute.cs
Normal file
17
MycroForge.CLI/Commands/Attributes/RequiresFileAttribute.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
namespace MycroForge.CLI.Commands.Attributes;
|
||||||
|
|
||||||
|
public class RequiresFileAttribute : Attribute
|
||||||
|
{
|
||||||
|
public string FilePath { get; }
|
||||||
|
|
||||||
|
public RequiresFileAttribute(string filePath)
|
||||||
|
{
|
||||||
|
FilePath = filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RequireFile(string command)
|
||||||
|
{
|
||||||
|
if (!File.Exists(Path.Join(Environment.CurrentDirectory, FilePath)))
|
||||||
|
throw new($"Command '{command}' requires file {FilePath}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using Microsoft.Extensions.FileSystemGlobbing;
|
||||||
|
using Microsoft.Extensions.FileSystemGlobbing.Abstractions;
|
||||||
|
|
||||||
|
namespace MycroForge.CLI.Commands.Attributes;
|
||||||
|
|
||||||
|
public class RequiresPluginAttribute : Attribute
|
||||||
|
{
|
||||||
|
public void RequirePluginProject(string command)
|
||||||
|
{
|
||||||
|
var currentDirectoryInfo = new DirectoryInfo(Environment.CurrentDirectory);
|
||||||
|
var matcher = new Matcher()
|
||||||
|
.AddInclude("*.csproj")
|
||||||
|
.Execute(new DirectoryInfoWrapper(currentDirectoryInfo));
|
||||||
|
|
||||||
|
if (!matcher.HasMatches)
|
||||||
|
throw new($"Command '{command}' must be run in a command plugin project.");
|
||||||
|
|
||||||
|
var csprojFileName = $"{new DirectoryInfo(Environment.CurrentDirectory).Name}.csproj";
|
||||||
|
|
||||||
|
bool IsCsprojFile(FilePatternMatch file)
|
||||||
|
{
|
||||||
|
return Path.GetFileName(file.Path) == csprojFileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasCsprojFile = matcher.Files.Any(IsCsprojFile);
|
||||||
|
|
||||||
|
if (!hasCsprojFile)
|
||||||
|
throw new($"File '{csprojFileName}' was not found, make sure you're in a command plugin project.");
|
||||||
|
}
|
||||||
|
}
|
||||||
12
MycroForge.CLI/Commands/Attributes/RequiresVenvAttribute.cs
Normal file
12
MycroForge.CLI/Commands/Attributes/RequiresVenvAttribute.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
namespace MycroForge.CLI.Commands.Attributes;
|
||||||
|
|
||||||
|
public class RequiresVenvAttribute : Attribute
|
||||||
|
{
|
||||||
|
public string Path => System.IO.Path.Join(Environment.CurrentDirectory, ".venv");
|
||||||
|
|
||||||
|
public void RequireVenv(string command)
|
||||||
|
{
|
||||||
|
if (!File.Exists(System.IO.Path.Join(Environment.CurrentDirectory, Path)))
|
||||||
|
throw new($"Command '{command}' requires directory {Path}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,28 +1,48 @@
|
|||||||
using Humanizer;
|
using Humanizer;
|
||||||
|
using MycroForge.CLI.Extensions;
|
||||||
|
|
||||||
namespace MycroForge.CLI.Commands;
|
namespace MycroForge.CLI.Commands;
|
||||||
|
|
||||||
public class FullyQualifiedName
|
public class FullyQualifiedName
|
||||||
{
|
{
|
||||||
public string Path { get; }
|
public string Namespace { get; }
|
||||||
public string PascalizedName { get; }
|
public string PascalizedName { get; }
|
||||||
public string SnakeCasedName { get; }
|
public string SnakeCasedName { get; }
|
||||||
|
|
||||||
public bool HasPath => Path.Length > 0;
|
public string FilePath =>
|
||||||
|
string.IsNullOrEmpty(Namespace.Trim())
|
||||||
|
? SnakeCasedName
|
||||||
|
: Path.Join(Namespace, SnakeCasedName);
|
||||||
|
|
||||||
|
public bool HasNamespace => Namespace.Length > 0;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public FullyQualifiedName(string name)
|
public FullyQualifiedName(string name)
|
||||||
{
|
{
|
||||||
var path = string.Empty;
|
var path = string.Empty;
|
||||||
|
|
||||||
if (name.Split(':').Select(s => s.Trim()).ToArray() is { Length: 2 } fullName)
|
if (name.Split(':').Select(s => s.Trim()).ToArray() is { Length: 2 } fullName)
|
||||||
{
|
{
|
||||||
path = fullName[0];
|
path = fullName[0];
|
||||||
name = fullName[1];
|
name = fullName[1];
|
||||||
}
|
}
|
||||||
|
|
||||||
Path = path;
|
Namespace = path;
|
||||||
PascalizedName = name.Pascalize();
|
PascalizedName = name.Pascalize();
|
||||||
SnakeCasedName = name.Underscore().ToLower();
|
SnakeCasedName = SnakeCase(name);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
public string GetImportPath(params string[] root)
|
||||||
|
{
|
||||||
|
if (root.Length == 0)
|
||||||
|
return string.Join('.', FilePath).SlashesToDots();
|
||||||
|
|
||||||
|
var importRoot = string.Join('.', root);
|
||||||
|
|
||||||
|
return string.Join('.', SnakeCase(importRoot), FilePath).SlashesToDots();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string SnakeCase(string value) => value.Underscore().ToLower();
|
||||||
|
|
||||||
|
// private static string SlashesToDots(string value) => value.Replace('\\', '.').Replace('/', '.');
|
||||||
|
}
|
||||||
@@ -21,6 +21,11 @@ public partial class MycroForge
|
|||||||
description: "The database UI port"
|
description: "The database UI port"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
private static readonly Option<ProjectConfig.DbConfig.DbuPlatformOptions> DbuPlatformOption = new(
|
||||||
|
aliases: ["--database-ui-platform", "--dbu-platform"],
|
||||||
|
description: "The docker platform for the PhpMyAdmin image"
|
||||||
|
);
|
||||||
|
|
||||||
private readonly ProjectContext _context;
|
private readonly ProjectContext _context;
|
||||||
private readonly OptionsContainer _optionsContainer;
|
private readonly OptionsContainer _optionsContainer;
|
||||||
private readonly List<IFeature> _features;
|
private readonly List<IFeature> _features;
|
||||||
@@ -34,12 +39,22 @@ public partial class MycroForge
|
|||||||
|
|
||||||
AddOption(DbhPortOption);
|
AddOption(DbhPortOption);
|
||||||
AddOption(DbuPortOption);
|
AddOption(DbuPortOption);
|
||||||
this.SetHandler(ExecuteAsync, DbhPortOption, DbuPortOption);
|
AddOption(DbuPlatformOption);
|
||||||
|
this.SetHandler(ExecuteAsync, DbhPortOption, DbuPortOption, DbuPlatformOption);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ExecuteAsync(int dbhPort, int dbuPort)
|
private async Task ExecuteAsync(
|
||||||
|
int dbhPort,
|
||||||
|
int dbuPort,
|
||||||
|
ProjectConfig.DbConfig.DbuPlatformOptions dbuPlatform
|
||||||
|
)
|
||||||
{
|
{
|
||||||
_optionsContainer.Set(new Features.Db.Options { DbhPort = dbhPort, DbuPort = dbuPort });
|
_optionsContainer.Set(new Features.Db.Options
|
||||||
|
{
|
||||||
|
DbhPort = dbhPort,
|
||||||
|
DbuPort = dbuPort,
|
||||||
|
DbuPlatform = dbuPlatform
|
||||||
|
});
|
||||||
var feature = _features.First(f => f.Name == Features.Db.FeatureName);
|
var feature = _features.First(f => f.Name == Features.Db.FeatureName);
|
||||||
await feature.ExecuteAsync(_context);
|
await feature.ExecuteAsync(_context);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,11 +29,10 @@ public partial class MycroForge
|
|||||||
private async Task ExecuteAsync(string entity)
|
private async Task ExecuteAsync(string entity)
|
||||||
{
|
{
|
||||||
var fqn = new FullyQualifiedName(entity);
|
var fqn = new FullyQualifiedName(entity);
|
||||||
|
await new CrudServiceGenerator(_context).Generate(fqn);
|
||||||
await new CrudServiceGenerator(_context).Generate(fqn.Path, fqn.PascalizedName);
|
await new RequestClassGenerator(_context).Generate(fqn, RequestClassGenerator.Type.Create);
|
||||||
await new RequestClassGenerator(_context).Generate(fqn.Path, fqn.PascalizedName, RequestClassGenerator.Type.Create);
|
await new RequestClassGenerator(_context).Generate(fqn, RequestClassGenerator.Type.Update);
|
||||||
await new RequestClassGenerator(_context).Generate(fqn.Path, fqn.PascalizedName, RequestClassGenerator.Type.Update);
|
await new CrudRouterGenerator(_context).Generate(fqn);
|
||||||
await new CrudRouterGenerator(_context).Generate(fqn.Path, fqn.PascalizedName);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ using System.CommandLine;
|
|||||||
using Humanizer;
|
using Humanizer;
|
||||||
using MycroForge.CLI.CodeGen;
|
using MycroForge.CLI.CodeGen;
|
||||||
using MycroForge.Core.Contract;
|
using MycroForge.Core.Contract;
|
||||||
using MycroForge.CLI.Extensions;
|
|
||||||
using MycroForge.Core;
|
using MycroForge.Core;
|
||||||
|
|
||||||
namespace MycroForge.CLI.Commands;
|
namespace MycroForge.CLI.Commands;
|
||||||
@@ -44,19 +43,17 @@ public partial class MycroForge
|
|||||||
private async Task ExecuteAsync(string name)
|
private async Task ExecuteAsync(string name)
|
||||||
{
|
{
|
||||||
var fqn = new FullyQualifiedName(name);
|
var fqn = new FullyQualifiedName(name);
|
||||||
var folderPath = $"{Features.Api.FeatureName}/routers";
|
var routersFolderPath = Path.Join(Features.Api.FeatureName, "routers");
|
||||||
|
|
||||||
_context.AssertDirectoryExists(folderPath);
|
if (fqn.HasNamespace)
|
||||||
|
routersFolderPath = Path.Join(routersFolderPath, fqn.Namespace);
|
||||||
|
|
||||||
if (fqn.HasPath)
|
|
||||||
folderPath = Path.Combine(folderPath, fqn.Path);
|
|
||||||
|
|
||||||
var fileName = $"{fqn.SnakeCasedName}.py";
|
var fileName = $"{fqn.SnakeCasedName}.py";
|
||||||
var filePath = Path.Combine(folderPath, fileName);
|
var filePath = Path.Join(routersFolderPath, fileName);
|
||||||
|
|
||||||
await _context.CreateFile(filePath, Template);
|
await _context.CreateFile(filePath, Template);
|
||||||
|
|
||||||
var moduleImportPath = folderPath
|
var moduleImportPath = routersFolderPath
|
||||||
.Replace('\\', '.')
|
.Replace('\\', '.')
|
||||||
.Replace('/', '.');
|
.Replace('/', '.');
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
using System.CommandLine;
|
using System.CommandLine;
|
||||||
|
using MycroForge.CLI.Commands.Attributes;
|
||||||
using MycroForge.Core.Contract;
|
using MycroForge.Core.Contract;
|
||||||
|
|
||||||
namespace MycroForge.CLI.Commands;
|
namespace MycroForge.CLI.Commands;
|
||||||
|
|
||||||
public partial class MycroForge
|
public partial class MycroForge
|
||||||
{
|
{
|
||||||
|
[RequiresFeature(Features.Api.FeatureName)]
|
||||||
public partial class Api : Command, ISubCommandOf<MycroForge>
|
public partial class Api : Command, ISubCommandOf<MycroForge>
|
||||||
{
|
{
|
||||||
public Api(IEnumerable<ISubCommandOf<Api>> subCommands) :
|
public Api(IEnumerable<ISubCommandOf<Api>> commands) :
|
||||||
base("api", "API related commands")
|
base("api", "API related commands")
|
||||||
{
|
{
|
||||||
foreach (var subCommandOf in subCommands)
|
foreach (var command in commands)
|
||||||
AddCommand((subCommandOf as Command)!);
|
AddCommand((command as Command)!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
using System.CommandLine;
|
using System.CommandLine;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using Humanizer;
|
using Humanizer;
|
||||||
using MycroForge.CLI.CodeGen;
|
using MycroForge.CLI.CodeGen;
|
||||||
using MycroForge.Core.Contract;
|
using MycroForge.Core.Contract;
|
||||||
using MycroForge.CLI.Extensions;
|
|
||||||
using MycroForge.Core;
|
using MycroForge.Core;
|
||||||
|
|
||||||
namespace MycroForge.CLI.Commands;
|
namespace MycroForge.CLI.Commands;
|
||||||
@@ -15,11 +15,65 @@ public partial class MycroForge
|
|||||||
{
|
{
|
||||||
public class Entity : Command, ISubCommandOf<Generate>
|
public class Entity : Command, ISubCommandOf<Generate>
|
||||||
{
|
{
|
||||||
private record ColumnDefinition(string Name, string NativeType, string OrmType);
|
#region Hidden region
|
||||||
|
|
||||||
|
private static string[] SqlAlchemyTypes =
|
||||||
|
[
|
||||||
|
"BigInteger",
|
||||||
|
"Boolean",
|
||||||
|
"Date",
|
||||||
|
"DateTime",
|
||||||
|
"Enum",
|
||||||
|
"Double",
|
||||||
|
"Float",
|
||||||
|
"Integer",
|
||||||
|
"Interval",
|
||||||
|
"LargeBinary",
|
||||||
|
"MatchType",
|
||||||
|
"Numeric",
|
||||||
|
"PickleType",
|
||||||
|
"SchemaType",
|
||||||
|
"SmallInteger",
|
||||||
|
"String",
|
||||||
|
"Text",
|
||||||
|
"Time",
|
||||||
|
"Unicode",
|
||||||
|
"UnicodeText",
|
||||||
|
"Uuid",
|
||||||
|
"ARRAY",
|
||||||
|
"BIGINT",
|
||||||
|
"BINARY",
|
||||||
|
"BLOB",
|
||||||
|
"BOOLEAN",
|
||||||
|
"CHAR",
|
||||||
|
"CLOB",
|
||||||
|
"DATE",
|
||||||
|
"DATETIME",
|
||||||
|
"DECIMAL",
|
||||||
|
"DOUBLE",
|
||||||
|
"DOUBLE_PRECISION",
|
||||||
|
"FLOAT",
|
||||||
|
"INT",
|
||||||
|
"JSON",
|
||||||
|
"INTEGER",
|
||||||
|
"NCHAR",
|
||||||
|
"NVARCHAR",
|
||||||
|
"NUMERIC",
|
||||||
|
"REAL",
|
||||||
|
"SMALLINT",
|
||||||
|
"TEXT",
|
||||||
|
"TIME",
|
||||||
|
"TIMESTAMP",
|
||||||
|
"UUID",
|
||||||
|
"VARBINARY",
|
||||||
|
"VARCHAR"
|
||||||
|
];
|
||||||
|
|
||||||
|
private static readonly Regex SqlAlchemyTypeRegex = new(@".*\(.*\)");
|
||||||
|
|
||||||
private static readonly string[] Template =
|
private static readonly string[] Template =
|
||||||
[
|
[
|
||||||
"from sqlalchemy import %type_imports%",
|
"from sqlalchemy import %sqlalchemy_imports%",
|
||||||
"from sqlalchemy.orm import Mapped, mapped_column",
|
"from sqlalchemy.orm import Mapped, mapped_column",
|
||||||
$"from {Features.Db.FeatureName}.entities.entity_base import EntityBase",
|
$"from {Features.Db.FeatureName}.entities.entity_base import EntityBase",
|
||||||
"",
|
"",
|
||||||
@@ -56,6 +110,10 @@ public partial class MycroForge
|
|||||||
"\tfirst_name:str:String(255)",
|
"\tfirst_name:str:String(255)",
|
||||||
])) { AllowMultipleArgumentsPerToken = true };
|
])) { AllowMultipleArgumentsPerToken = true };
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
private record ColumnDefinition(string Name, string NativeType, string SqlAlchemyType);
|
||||||
|
|
||||||
private readonly ProjectContext _context;
|
private readonly ProjectContext _context;
|
||||||
|
|
||||||
public Entity(ProjectContext context) : base("entity", "Generate and database entity")
|
public Entity(ProjectContext context) : base("entity", "Generate and database entity")
|
||||||
@@ -70,25 +128,27 @@ public partial class MycroForge
|
|||||||
private async Task ExecuteAsync(string name, IEnumerable<string> columns)
|
private async Task ExecuteAsync(string name, IEnumerable<string> columns)
|
||||||
{
|
{
|
||||||
var fqn = new FullyQualifiedName(name);
|
var fqn = new FullyQualifiedName(name);
|
||||||
var folderPath = $"{Features.Db.FeatureName}/entities";
|
var folderPath = Path.Join(Features.Db.FeatureName, "entities");
|
||||||
|
|
||||||
_context.AssertDirectoryExists(Features.Db.FeatureName);
|
if (fqn.HasNamespace)
|
||||||
|
folderPath = Path.Join(folderPath, fqn.Namespace);
|
||||||
|
|
||||||
|
var sqlAlchemyColumn = GetColumnDefinitions(columns.ToArray());
|
||||||
|
var distinctSqlAlchemyColumnTypes = sqlAlchemyColumn
|
||||||
|
.Select(c => c.SqlAlchemyType.Split('(').First())
|
||||||
|
.Distinct();
|
||||||
|
|
||||||
if (fqn.HasPath)
|
var sqlAlchemyImport = string.Join(", ", distinctSqlAlchemyColumnTypes);
|
||||||
folderPath = Path.Combine(folderPath, fqn.Path);
|
var columnDefinitions = string.Join("\n ", sqlAlchemyColumn.Select(ColumnToString));
|
||||||
|
|
||||||
var _columns = GetColumnDefinitions(columns.ToArray());
|
|
||||||
var typeImports = string.Join(", ", _columns.Select(c => c.OrmType.Split('(').First()).Distinct());
|
|
||||||
var columnDefinitions = string.Join("\n\t", _columns.Select(ColumnToString));
|
|
||||||
|
|
||||||
var code = string.Join('\n', Template);
|
var code = string.Join('\n', Template);
|
||||||
code = code.Replace("%type_imports%", typeImports);
|
code = code.Replace("%sqlalchemy_imports%", sqlAlchemyImport);
|
||||||
code = code.Replace("%class_name%", fqn.PascalizedName);
|
code = code.Replace("%class_name%", fqn.PascalizedName);
|
||||||
code = code.Replace("%table_name%", fqn.SnakeCasedName.Pluralize());
|
code = code.Replace("%table_name%", fqn.SnakeCasedName.Pluralize());
|
||||||
code = code.Replace("%column_definitions%", columnDefinitions);
|
code = code.Replace("%column_definitions%", columnDefinitions);
|
||||||
|
|
||||||
var fileName = $"{fqn.SnakeCasedName}.py";
|
var fileName = $"{fqn.SnakeCasedName}.py";
|
||||||
var filePath = Path.Combine(folderPath, fileName);
|
var filePath = Path.Join(folderPath, fileName);
|
||||||
await _context.CreateFile(filePath, code);
|
await _context.CreateFile(filePath, code);
|
||||||
|
|
||||||
var importPathParts = new[] { folderPath, fileName.Replace(".py", "") }
|
var importPathParts = new[] { folderPath, fileName.Replace(".py", "") }
|
||||||
@@ -109,25 +169,58 @@ public partial class MycroForge
|
|||||||
await _context.WriteFile("main.py", main);
|
await _context.WriteFile("main.py", main);
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<ColumnDefinition> GetColumnDefinitions(string[] fields)
|
private List<ColumnDefinition> GetColumnDefinitions(string[] columns)
|
||||||
{
|
{
|
||||||
var definitions = new List<ColumnDefinition>();
|
var definitions = new List<ColumnDefinition>();
|
||||||
|
|
||||||
foreach (var field in fields)
|
foreach (var column in columns)
|
||||||
{
|
{
|
||||||
if (field.Split(':') is not { Length: 3 } definition)
|
if (column.Split(':') is not { Length: 3 } definition)
|
||||||
throw new Exception($"Field definition {field} is invalid.");
|
throw new Exception($"Column definition {column} is invalid.");
|
||||||
|
|
||||||
definitions.Add(new ColumnDefinition(definition[0], definition[1], definition[2]));
|
definitions.Add(new ColumnDefinition(definition[0], definition[1], definition[2]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ValidateSqlAlchemyColumnTypes(definitions);
|
||||||
|
|
||||||
return definitions;
|
return definitions;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string ColumnToString(ColumnDefinition definition)
|
private static void ValidateSqlAlchemyColumnTypes(List<ColumnDefinition> definitions)
|
||||||
{
|
{
|
||||||
return $"{definition.Name}: Mapped[{definition.NativeType}] = mapped_column({definition.OrmType})";
|
foreach (var column in definitions)
|
||||||
|
{
|
||||||
|
if (!SqlAlchemyTypeRegex.IsMatch(column.SqlAlchemyType))
|
||||||
|
{
|
||||||
|
var message = new[]
|
||||||
|
{
|
||||||
|
$"SQLAlchemy column definition {column.SqlAlchemyType} was not properly defined.",
|
||||||
|
"Add parentheses and specify parameters if required, an example is provided below.",
|
||||||
|
" String(255)",
|
||||||
|
"",
|
||||||
|
"Available options are:",
|
||||||
|
string.Join(Environment.NewLine, SqlAlchemyTypes.Select(type => $" - {type}"))
|
||||||
|
};
|
||||||
|
|
||||||
|
throw new(string.Join(Environment.NewLine, message));
|
||||||
|
}
|
||||||
|
|
||||||
|
var type = column.SqlAlchemyType.Split('(').First();
|
||||||
|
|
||||||
|
if (!SqlAlchemyTypes.Contains(type))
|
||||||
|
{
|
||||||
|
var message = string.Join(Environment.NewLine, [
|
||||||
|
$"SQLAlchemy column type '{column.SqlAlchemyType}' is not valid, available options are:",
|
||||||
|
string.Join(Environment.NewLine, SqlAlchemyTypes.Select(type => $" - {type}"))
|
||||||
|
]);
|
||||||
|
|
||||||
|
throw new(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string ColumnToString(ColumnDefinition definition) =>
|
||||||
|
$"{definition.Name}: Mapped[{definition.NativeType}] = mapped_column({definition.SqlAlchemyType})";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,8 +27,6 @@ public partial class MycroForge
|
|||||||
|
|
||||||
private async Task ExecuteAsync(string name)
|
private async Task ExecuteAsync(string name)
|
||||||
{
|
{
|
||||||
_context.AssertDirectoryExists($"{Features.Db.FeatureName}/versions");
|
|
||||||
|
|
||||||
await _context.Bash(
|
await _context.Bash(
|
||||||
"source .venv/bin/activate",
|
"source .venv/bin/activate",
|
||||||
$"alembic revision --autogenerate -m \"{name}\" --rev-id $(date -u +\"%Y%m%d%H%M%S\")"
|
$"alembic revision --autogenerate -m \"{name}\" --rev-id $(date -u +\"%Y%m%d%H%M%S\")"
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ public partial class MycroForge
|
|||||||
{
|
{
|
||||||
var config = await _context.LoadConfig();
|
var config = await _context.LoadConfig();
|
||||||
var env = $"DBH_PORT={config.Db.DbhPort} DBU_PORT={config.Db.DbuPort}";
|
var env = $"DBH_PORT={config.Db.DbhPort} DBU_PORT={config.Db.DbuPort}";
|
||||||
await _context.Bash($"{env} docker compose -f {Features.Db.FeatureName}.docker-compose.yml up -d");
|
var command = $"{env} docker compose -f {Features.Db.FeatureName}.docker-compose.yml up -d";
|
||||||
|
await _context.Bash(command);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,9 +21,10 @@ public partial class MycroForge
|
|||||||
|
|
||||||
private async Task ExecuteAsync()
|
private async Task ExecuteAsync()
|
||||||
{
|
{
|
||||||
var config = await _context.LoadConfig();
|
await _context.Bash(
|
||||||
var env = $"DB_PORT={config.Db.DbhPort} PMA_PORT={config.Db.DbuPort}";
|
// Set the log level to ERROR to prevent warnings concerning environment variables not being set.
|
||||||
await _context.Bash($"{env} docker compose -f {Features.Db.FeatureName}.docker-compose.yml down");
|
$"docker --log-level ERROR compose -f {Features.Db.FeatureName}.docker-compose.yml down"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
using System.CommandLine;
|
using System.CommandLine;
|
||||||
|
using MycroForge.CLI.Commands.Attributes;
|
||||||
using MycroForge.Core.Contract;
|
using MycroForge.Core.Contract;
|
||||||
|
|
||||||
namespace MycroForge.CLI.Commands;
|
namespace MycroForge.CLI.Commands;
|
||||||
|
|
||||||
public partial class MycroForge
|
public partial class MycroForge
|
||||||
{
|
{
|
||||||
|
[RequiresFeature(Features.Db.FeatureName)]
|
||||||
public partial class Db : Command, ISubCommandOf<MycroForge>
|
public partial class Db : Command, ISubCommandOf<MycroForge>
|
||||||
{
|
{
|
||||||
public Db(IEnumerable<ISubCommandOf<Db>> commands)
|
public Db(IEnumerable<ISubCommandOf<Db>> commands)
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
using System.CommandLine;
|
using System.CommandLine;
|
||||||
using Humanizer;
|
|
||||||
using MycroForge.Core.Contract;
|
using MycroForge.Core.Contract;
|
||||||
using MycroForge.CLI.Extensions;
|
|
||||||
using MycroForge.Core;
|
using MycroForge.Core;
|
||||||
|
|
||||||
namespace MycroForge.CLI.Commands;
|
namespace MycroForge.CLI.Commands;
|
||||||
@@ -60,10 +58,10 @@ public partial class MycroForge
|
|||||||
var fqn = new FullyQualifiedName(name);
|
var fqn = new FullyQualifiedName(name);
|
||||||
var folderPath = string.Empty;
|
var folderPath = string.Empty;
|
||||||
|
|
||||||
if (fqn.HasPath)
|
if (fqn.HasNamespace)
|
||||||
folderPath = Path.Combine(folderPath, fqn.Path);
|
folderPath = Path.Join(folderPath, fqn.Namespace);
|
||||||
|
|
||||||
var filePath = Path.Combine(folderPath, $"{fqn.SnakeCasedName}.py");
|
var filePath = Path.Join(folderPath, $"{fqn.SnakeCasedName}.py");
|
||||||
var template = withSession ? WithSessionTemplate : DefaultTemplate;
|
var template = withSession ? WithSessionTemplate : DefaultTemplate;
|
||||||
var code = string.Join('\n', template)
|
var code = string.Join('\n', template)
|
||||||
.Replace("%class_name%", fqn.PascalizedName);
|
.Replace("%class_name%", fqn.PascalizedName);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.CommandLine;
|
using System.CommandLine;
|
||||||
|
using MycroForge.CLI.Commands.Attributes;
|
||||||
using MycroForge.Core;
|
using MycroForge.Core;
|
||||||
using MycroForge.Core.Contract;
|
using MycroForge.Core.Contract;
|
||||||
|
|
||||||
@@ -6,11 +7,12 @@ namespace MycroForge.CLI.Commands;
|
|||||||
|
|
||||||
public partial class MycroForge
|
public partial class MycroForge
|
||||||
{
|
{
|
||||||
|
[RequiresFile("requirements.txt")]
|
||||||
public class Hydrate : Command, ISubCommandOf<MycroForge>
|
public class Hydrate : Command, ISubCommandOf<MycroForge>
|
||||||
{
|
{
|
||||||
private readonly ProjectContext _context;
|
private readonly ProjectContext _context;
|
||||||
|
|
||||||
public Hydrate(ProjectContext context)
|
public Hydrate(ProjectContext context)
|
||||||
: base("hydrate", "Initialize venv and install dependencies from requirements.txt")
|
: base("hydrate", "Initialize venv and install dependencies from requirements.txt")
|
||||||
{
|
{
|
||||||
_context = context;
|
_context = context;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ public partial class MycroForge
|
|||||||
ApiPort = ctx.ParseResult.GetValueForOption(ApiPortOption),
|
ApiPort = ctx.ParseResult.GetValueForOption(ApiPortOption),
|
||||||
DbhPort = ctx.ParseResult.GetValueForOption(DbhPortOption),
|
DbhPort = ctx.ParseResult.GetValueForOption(DbhPortOption),
|
||||||
DbuPort = ctx.ParseResult.GetValueForOption(DbuPortOption),
|
DbuPort = ctx.ParseResult.GetValueForOption(DbuPortOption),
|
||||||
|
DbuPlatform = ctx.ParseResult.GetValueForOption(DbuPlatformOption),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
namespace MycroForge.CLI.Commands;
|
using MycroForge.Core;
|
||||||
|
|
||||||
|
namespace MycroForge.CLI.Commands;
|
||||||
|
|
||||||
public partial class MycroForge
|
public partial class MycroForge
|
||||||
{
|
{
|
||||||
@@ -11,6 +13,7 @@ public partial class MycroForge
|
|||||||
public int? ApiPort { get; set; }
|
public int? ApiPort { get; set; }
|
||||||
public int? DbhPort { get; set; }
|
public int? DbhPort { get; set; }
|
||||||
public int? DbuPort { get; set; }
|
public int? DbuPort { get; set; }
|
||||||
|
public ProjectConfig.DbConfig.DbuPlatformOptions DbuPlatform { get; set; }
|
||||||
|
|
||||||
public Features.Api.Options ApiOptions => new()
|
public Features.Api.Options ApiOptions => new()
|
||||||
{
|
{
|
||||||
@@ -20,7 +23,8 @@ public partial class MycroForge
|
|||||||
public Features.Db.Options DbOptions => new()
|
public Features.Db.Options DbOptions => new()
|
||||||
{
|
{
|
||||||
DbhPort = DbhPort <= 0 ? 5050 : DbhPort,
|
DbhPort = DbhPort <= 0 ? 5050 : DbhPort,
|
||||||
DbuPort = DbuPort <= 0 ? 5051 : DbhPort
|
DbuPort = DbuPort <= 0 ? 5051 : DbhPort,
|
||||||
|
DbuPlatform = DbuPlatform
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,11 @@ public partial class MycroForge
|
|||||||
description: "The database UI port"
|
description: "The database UI port"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
private static readonly Option<ProjectConfig.DbConfig.DbuPlatformOptions> DbuPlatformOption = new(
|
||||||
|
aliases: ["--database-ui-platform", "--dbu-platform"],
|
||||||
|
description: "The docker platform for the PhpMyAdmin image"
|
||||||
|
);
|
||||||
|
|
||||||
private readonly ProjectContext _context;
|
private readonly ProjectContext _context;
|
||||||
private readonly List<IFeature> _features;
|
private readonly List<IFeature> _features;
|
||||||
private readonly OptionsContainer _optionsContainer;
|
private readonly OptionsContainer _optionsContainer;
|
||||||
@@ -55,6 +60,7 @@ public partial class MycroForge
|
|||||||
AddOption(ApiPortOption);
|
AddOption(ApiPortOption);
|
||||||
AddOption(DbhPortOption);
|
AddOption(DbhPortOption);
|
||||||
AddOption(DbuPortOption);
|
AddOption(DbuPortOption);
|
||||||
|
AddOption(DbuPlatformOption);
|
||||||
|
|
||||||
this.SetHandler(ExecuteAsync, new Binder());
|
this.SetHandler(ExecuteAsync, new Binder());
|
||||||
}
|
}
|
||||||
@@ -75,7 +81,7 @@ public partial class MycroForge
|
|||||||
await _context.CreateFile("main.py");
|
await _context.CreateFile("main.py");
|
||||||
|
|
||||||
// Create the venv
|
// Create the venv
|
||||||
await _context.Bash($"python3 -m venv {Path.Combine(projectRoot, ".venv")}");
|
await _context.Bash($"python3 -m venv {Path.Join(projectRoot, ".venv")}");
|
||||||
|
|
||||||
// Pass feature arguments to the ArgsContainer
|
// Pass feature arguments to the ArgsContainer
|
||||||
_optionsContainer.Set(options.ApiOptions);
|
_optionsContainer.Set(options.ApiOptions);
|
||||||
@@ -100,7 +106,7 @@ public partial class MycroForge
|
|||||||
|
|
||||||
private async Task<string> CreateDirectory(string name)
|
private async Task<string> CreateDirectory(string name)
|
||||||
{
|
{
|
||||||
var directory = Path.Combine(Directory.GetCurrentDirectory(), name);
|
var directory = Path.Join(Directory.GetCurrentDirectory(), name);
|
||||||
|
|
||||||
if (Directory.Exists(directory))
|
if (Directory.Exists(directory))
|
||||||
throw new Exception($"Directory {directory} already exists.");
|
throw new Exception($"Directory {directory} already exists.");
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.CommandLine;
|
using System.CommandLine;
|
||||||
|
using MycroForge.CLI.Commands.Attributes;
|
||||||
using MycroForge.Core;
|
using MycroForge.Core;
|
||||||
using MycroForge.Core.Contract;
|
using MycroForge.Core.Contract;
|
||||||
|
|
||||||
@@ -6,6 +7,7 @@ namespace MycroForge.CLI.Commands;
|
|||||||
|
|
||||||
public partial class MycroForge
|
public partial class MycroForge
|
||||||
{
|
{
|
||||||
|
[RequiresVenv]
|
||||||
public class Install : Command, ISubCommandOf<MycroForge>
|
public class Install : Command, ISubCommandOf<MycroForge>
|
||||||
{
|
{
|
||||||
private static readonly Argument<IEnumerable<string>> PackagesArgument =
|
private static readonly Argument<IEnumerable<string>> PackagesArgument =
|
||||||
@@ -28,7 +30,7 @@ public partial class MycroForge
|
|||||||
|
|
||||||
if (packs.Length == 0)
|
if (packs.Length == 0)
|
||||||
{
|
{
|
||||||
Console.WriteLine("m4g install requires at least one package.");
|
Console.WriteLine("'m4g install' requires at least one package.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
using Humanizer;
|
using Humanizer;
|
||||||
using Microsoft.Extensions.FileSystemGlobbing;
|
using Microsoft.Extensions.FileSystemGlobbing;
|
||||||
using Microsoft.Extensions.FileSystemGlobbing.Abstractions;
|
using Microsoft.Extensions.FileSystemGlobbing.Abstractions;
|
||||||
|
using MycroForge.CLI.Commands.Attributes;
|
||||||
using MycroForge.Core;
|
using MycroForge.Core;
|
||||||
using MycroForge.Core.Contract;
|
using MycroForge.Core.Contract;
|
||||||
|
|
||||||
@@ -11,6 +12,7 @@ public partial class MycroForge
|
|||||||
{
|
{
|
||||||
public partial class Plugin
|
public partial class Plugin
|
||||||
{
|
{
|
||||||
|
[RequiresPlugin]
|
||||||
public class Install : Command, ISubCommandOf<Plugin>
|
public class Install : Command, ISubCommandOf<Plugin>
|
||||||
{
|
{
|
||||||
public enum TargetPlatform
|
public enum TargetPlatform
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.CommandLine;
|
using System.CommandLine;
|
||||||
|
using MycroForge.CLI.Commands.Attributes;
|
||||||
using MycroForge.Core;
|
using MycroForge.Core;
|
||||||
using MycroForge.Core.Contract;
|
using MycroForge.Core.Contract;
|
||||||
|
|
||||||
@@ -6,6 +7,7 @@ namespace MycroForge.CLI.Commands;
|
|||||||
|
|
||||||
public partial class MycroForge
|
public partial class MycroForge
|
||||||
{
|
{
|
||||||
|
[RequiresVenv]
|
||||||
public class Uninstall : Command, ISubCommandOf<MycroForge>
|
public class Uninstall : Command, ISubCommandOf<MycroForge>
|
||||||
{
|
{
|
||||||
private static readonly Argument<IEnumerable<string>> PackagesArgument =
|
private static readonly Argument<IEnumerable<string>> PackagesArgument =
|
||||||
@@ -27,9 +29,17 @@ public partial class MycroForge
|
|||||||
|
|
||||||
private async Task ExecuteAsync(IEnumerable<string> packages, bool yes)
|
private async Task ExecuteAsync(IEnumerable<string> packages, bool yes)
|
||||||
{
|
{
|
||||||
|
var packs = packages.ToArray();
|
||||||
|
|
||||||
|
if (packs.Length == 0)
|
||||||
|
{
|
||||||
|
Console.WriteLine("'m4g uninstall' requires at least one package.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await _context.Bash(
|
await _context.Bash(
|
||||||
"source .venv/bin/activate",
|
"source .venv/bin/activate",
|
||||||
$"pip uninstall{(yes ? " --yes " : " ")}{string.Join(' ', packages)}",
|
$"pip uninstall{(yes ? " --yes " : " ")}{string.Join(' ', packs)}",
|
||||||
"pip freeze > requirements.txt"
|
"pip freeze > requirements.txt"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
96
MycroForge.CLI/Extensions/CommandExtensions.cs
Normal file
96
MycroForge.CLI/Extensions/CommandExtensions.cs
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
using System.CommandLine;
|
||||||
|
using System.CommandLine.Builder;
|
||||||
|
using System.CommandLine.Invocation;
|
||||||
|
using System.CommandLine.Parsing;
|
||||||
|
using System.Reflection;
|
||||||
|
using MycroForge.CLI.Commands.Attributes;
|
||||||
|
|
||||||
|
namespace MycroForge.CLI.Extensions;
|
||||||
|
|
||||||
|
public static class CommandExtensions
|
||||||
|
{
|
||||||
|
public static async Task ExecuteAsync(this Commands.MycroForge rootCommand, string[] args)
|
||||||
|
{
|
||||||
|
var parser = new CommandLineBuilder(rootCommand)
|
||||||
|
.AddMiddleware()
|
||||||
|
.UseDefaults()
|
||||||
|
.UseExceptionHandler((ex, ctx) =>
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* Use a custom ExceptionHandler to prevent the System.CommandLine library from printing the StackTrace by default.
|
||||||
|
*/
|
||||||
|
Console.WriteLine(ex.Message);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Set the exit code to a non-zero value to indicate to the shell that the process has failed.
|
||||||
|
*/
|
||||||
|
Environment.ExitCode = -1;
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
await parser.InvokeAsync(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CommandLineBuilder AddMiddleware(this CommandLineBuilder builder)
|
||||||
|
{
|
||||||
|
builder.AddMiddleware(async (context, next) =>
|
||||||
|
{
|
||||||
|
var commandChain = context.GetCommandChain();
|
||||||
|
var commandText = string.Join(' ', commandChain.Select(cmd => cmd.Name));
|
||||||
|
|
||||||
|
foreach (var _command in commandChain)
|
||||||
|
{
|
||||||
|
if (_command.GetRequiresFeatureAttribute() is RequiresFeatureAttribute requiresFeatureAttribute)
|
||||||
|
requiresFeatureAttribute.RequireFeature(commandText);
|
||||||
|
|
||||||
|
else if (_command.GetRequiresFileAttribute() is RequiresFileAttribute requiresFileAttribute)
|
||||||
|
requiresFileAttribute.RequireFile(commandText);
|
||||||
|
|
||||||
|
else if (_command.GetRequiresPluginAttribute() is RequiresPluginAttribute requiresPluginAttribute)
|
||||||
|
requiresPluginAttribute.RequirePluginProject(commandText);
|
||||||
|
|
||||||
|
else if (_command.GetRequiresVenvAttribute() is RequiresVenvAttribute requiresVenvAttribute)
|
||||||
|
requiresVenvAttribute.RequireVenv(commandText);
|
||||||
|
}
|
||||||
|
|
||||||
|
await next(context);
|
||||||
|
});
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<Command> GetCommandChain(this InvocationContext context)
|
||||||
|
{
|
||||||
|
var chain = new List<Command>();
|
||||||
|
/*
|
||||||
|
* The CommandResult property refers to the last command in the chain.
|
||||||
|
* So if the command is 'm4g api run' the CommandResult will refer to 'run'.
|
||||||
|
*/
|
||||||
|
SymbolResult? cmd = context.ParseResult.CommandResult;
|
||||||
|
|
||||||
|
while (cmd is CommandResult result)
|
||||||
|
{
|
||||||
|
chain.Add(result.Command);
|
||||||
|
cmd = cmd.Parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Reverse the chain to reflect the actual order of the commands.
|
||||||
|
*/
|
||||||
|
chain.Reverse();
|
||||||
|
|
||||||
|
return chain;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RequiresFeatureAttribute? GetRequiresFeatureAttribute(this Command command) =>
|
||||||
|
command.GetType().GetCustomAttribute<RequiresFeatureAttribute>();
|
||||||
|
|
||||||
|
private static RequiresFileAttribute? GetRequiresFileAttribute(this Command command) =>
|
||||||
|
command.GetType().GetCustomAttribute<RequiresFileAttribute>();
|
||||||
|
|
||||||
|
private static RequiresPluginAttribute? GetRequiresPluginAttribute(this Command command) =>
|
||||||
|
command.GetType().GetCustomAttribute<RequiresPluginAttribute>();
|
||||||
|
|
||||||
|
private static RequiresVenvAttribute? GetRequiresVenvAttribute(this Command command) =>
|
||||||
|
command.GetType().GetCustomAttribute<RequiresVenvAttribute>();
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ namespace MycroForge.CLI.Extensions;
|
|||||||
|
|
||||||
public static class ServiceCollectionExtensions
|
public static class ServiceCollectionExtensions
|
||||||
{
|
{
|
||||||
public static IServiceCollection RegisterCommandDefaults(this IServiceCollection services)
|
public static IServiceCollection RegisterDefaultCommands(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
// Register ProjectContext, OptionsContainer & features
|
// Register ProjectContext, OptionsContainer & features
|
||||||
services.AddScoped<ProjectContext>();
|
services.AddScoped<ProjectContext>();
|
||||||
|
|||||||
@@ -2,11 +2,8 @@
|
|||||||
|
|
||||||
public static class StringExtensions
|
public static class StringExtensions
|
||||||
{
|
{
|
||||||
public static string DeduplicateDots(this string path)
|
public static string SlashesToDots(this string path) =>
|
||||||
{
|
path.Replace('/', '.')
|
||||||
while (path.Contains(".."))
|
.Replace('\\', '.')
|
||||||
path = path.Replace("..", ".");
|
.Trim();
|
||||||
|
|
||||||
return path.Trim('.');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,10 +1,15 @@
|
|||||||
namespace MycroForge.CLI.Features;
|
using MycroForge.Core;
|
||||||
|
|
||||||
|
namespace MycroForge.CLI.Features;
|
||||||
|
|
||||||
public sealed partial class Db
|
public sealed partial class Db
|
||||||
{
|
{
|
||||||
public class Options
|
public class Options
|
||||||
{
|
{
|
||||||
public int? DbhPort { get; set; }
|
public int? DbhPort { get; set; }
|
||||||
|
|
||||||
public int? DbuPort { get; set; }
|
public int? DbuPort { get; set; }
|
||||||
|
|
||||||
|
public ProjectConfig.DbConfig.DbuPlatformOptions DbuPlatform { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -37,7 +37,6 @@ public sealed partial class Db : IFeature
|
|||||||
|
|
||||||
private static readonly string[] DockerCompose =
|
private static readonly string[] DockerCompose =
|
||||||
[
|
[
|
||||||
"version: '3.8'",
|
|
||||||
"# Access the database UI at http://localhost:${DBU_PORT}.",
|
"# Access the database UI at http://localhost:${DBU_PORT}.",
|
||||||
"# Login: username = root & password = password",
|
"# Login: username = root & password = password",
|
||||||
"",
|
"",
|
||||||
@@ -58,7 +57,7 @@ public sealed partial class Db : IFeature
|
|||||||
" - '%app_name%_mariadb:/var/lib/mysql'",
|
" - '%app_name%_mariadb:/var/lib/mysql'",
|
||||||
"",
|
"",
|
||||||
" %app_name%_phpmyadmin:",
|
" %app_name%_phpmyadmin:",
|
||||||
" image: phpmyadmin/phpmyadmin",
|
" image: %dbu_platform%/phpmyadmin",
|
||||||
" container_name: %app_name%_phpmyadmin",
|
" container_name: %app_name%_phpmyadmin",
|
||||||
" ports:",
|
" ports:",
|
||||||
" - '${DBU_PORT}:80'",
|
" - '${DBU_PORT}:80'",
|
||||||
@@ -87,15 +86,16 @@ public sealed partial class Db : IFeature
|
|||||||
{
|
{
|
||||||
_optionsContainer = optionsContainer;
|
_optionsContainer = optionsContainer;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ExecuteAsync(ProjectContext context)
|
public async Task ExecuteAsync(ProjectContext context)
|
||||||
{
|
{
|
||||||
var options = _optionsContainer.Get<Options>();
|
var options = _optionsContainer.Get<Options>();
|
||||||
var config = await context.LoadConfig(create: true);
|
var config = await context.LoadConfig(create: true);
|
||||||
config.Db = new()
|
config.Db = new()
|
||||||
{
|
{
|
||||||
DbhPort = options.DbhPort ?? 5050,
|
DbhPort = options.DbhPort ?? 5050,
|
||||||
DbuPort = options.DbuPort ?? 5051
|
DbuPort = options.DbuPort ?? 5051,
|
||||||
|
DbuPlatform = options.DbuPlatform
|
||||||
};
|
};
|
||||||
await context.SaveConfig(config);
|
await context.SaveConfig(config);
|
||||||
|
|
||||||
@@ -123,7 +123,10 @@ public sealed partial class Db : IFeature
|
|||||||
|
|
||||||
await context.CreateFile($"{FeatureName}/entities/entity_base.py", EntityBase);
|
await context.CreateFile($"{FeatureName}/entities/entity_base.py", EntityBase);
|
||||||
|
|
||||||
var dockerCompose = string.Join('\n', DockerCompose).Replace("%app_name%", appName);
|
var dockerCompose = string.Join('\n', DockerCompose)
|
||||||
|
.Replace("%app_name%", appName)
|
||||||
|
.Replace("%dbu_platform%", options.DbuPlatform.ToString())
|
||||||
|
;
|
||||||
|
|
||||||
await context.CreateFile($"{FeatureName}.docker-compose.yml", dockerCompose);
|
await context.CreateFile($"{FeatureName}.docker-compose.yml", dockerCompose);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,18 @@
|
|||||||
using System.CommandLine;
|
using MycroForge.CLI.Extensions;
|
||||||
using MycroForge.CLI.Extensions;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
|
using RootCommand = MycroForge.CLI.Commands.MycroForge;
|
||||||
|
|
||||||
using var host = Host
|
using var host = Host
|
||||||
.CreateDefaultBuilder()
|
.CreateDefaultBuilder()
|
||||||
.ConfigureServices((_, services) =>
|
.ConfigureServices((_, services) =>
|
||||||
{
|
{
|
||||||
services
|
services
|
||||||
.RegisterCommandDefaults()
|
.RegisterDefaultCommands()
|
||||||
.RegisterCommandPlugins()
|
.RegisterCommandPlugins()
|
||||||
;
|
;
|
||||||
})
|
})
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
try
|
var command = host.Services.GetRequiredService<RootCommand>();
|
||||||
{
|
await command.ExecuteAsync(args.Length == 0 ? ["--help"] : args);
|
||||||
await host.Services.GetRequiredService<MycroForge.CLI.Commands.MycroForge>()
|
|
||||||
.InvokeAsync(args.Length == 0 ? ["--help"] : args);
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
Console.WriteLine(e.Message);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
#!/usr/bin/bash
|
|
||||||
|
|
||||||
TARGET=$1
|
|
||||||
|
|
||||||
if [ -z "$TARGET" ]; then
|
|
||||||
echo "The target platform was not provided."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
dotnet publish --self-contained -r "$TARGET"
|
|
||||||
zip -vr "releases/m4g-$TARGET.zip" "bin/Release/net8.0/$TARGET/"
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
#!/usr/bin/bash
|
|
||||||
|
|
||||||
./scripts/build-executable.sh linux-x64
|
|
||||||
./scripts/build-executable.sh linux-arm
|
|
||||||
./scripts/build-executable.sh linux-arm64
|
|
||||||
./scripts/build-executable.sh osx-x64
|
|
||||||
./scripts/build-executable.sh osx-arm64
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
#!/usr/bin/bash
|
|
||||||
|
|
||||||
ZIP=$1
|
|
||||||
if [ -z "$ZIP" ]; then
|
|
||||||
echo "The zip file was not provided."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
TARGET=${ZIP//"m4g-"/}
|
|
||||||
TARGET=${TARGET//".zip"/}
|
|
||||||
|
|
||||||
DIR="/tmp/m4g"
|
|
||||||
|
|
||||||
rm -rf "$DIR"
|
|
||||||
unzip "$ZIP" -d "$DIR"
|
|
||||||
|
|
||||||
sudo rm -rf /usr/share/m4g
|
|
||||||
sudo cp -r "$DIR/bin/Release/net8.0/$TARGET" /usr/share/m4g
|
|
||||||
|
|
||||||
sudo unlink /usr/local/bin/m4g 2> /dev/null
|
|
||||||
sudo ln -s /usr/share/m4g/MycroForge.CLI /usr/local/bin/m4g
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
#!/usr/bin/bash
|
|
||||||
|
|
||||||
ZIP=$1
|
|
||||||
if [ -z "$ZIP" ]; then
|
|
||||||
echo "The zip file was not provided."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
TARGET=${ZIP//"m4g-"/}
|
|
||||||
TARGET=${TARGET//".zip"/}
|
|
||||||
|
|
||||||
DIR="/tmp/m4g"
|
|
||||||
|
|
||||||
rm -rf "$DIR"
|
|
||||||
unzip "$ZIP" -d "$DIR"
|
|
||||||
|
|
||||||
sudo rm -rf /usr/local/bin/m4g
|
|
||||||
sudo cp -r "$DIR/bin/Release/net8.0/$TARGET" /usr/local/bin/m4g
|
|
||||||
mv /usr/share/m4g/MycroForge.CLI /usr/share/m4g/m4g
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
#!/usr/bin/bash
|
|
||||||
|
|
||||||
# Make sure to run this script from the MycroForge.CLI directory and prefixed with sudo!
|
|
||||||
# Example:
|
|
||||||
# sudo ./scripts/publish-linux.sh
|
|
||||||
|
|
||||||
dotnet publish --self-contained -r linux-x64
|
|
||||||
sudo rm -rf /usr/share/m4g
|
|
||||||
sudo cp -r bin/Release/net8.0/linux-x64 /usr/share/m4g
|
|
||||||
sudo unlink /usr/local/bin/m4g
|
|
||||||
sudo ln -s /usr/share/m4g/MycroForge.CLI /usr/local/bin/m4g
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/bash
|
#!/usr/bin/bash
|
||||||
|
|
||||||
dotnet pack -v d
|
dotnet pack -v d
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
dotnet pack -v d
|
dotnet pack -v d
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ public class Source
|
|||||||
|
|
||||||
public Source InsertMultiLineAtEnd(params string[] text)
|
public Source InsertMultiLineAtEnd(params string[] text)
|
||||||
{
|
{
|
||||||
_text += (string.Join('\n', text));
|
_text += string.Join('\n', text);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
17
MycroForge.Core/DefaultJsonSerializerOptions.cs
Normal file
17
MycroForge.Core/DefaultJsonSerializerOptions.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace MycroForge.Core;
|
||||||
|
|
||||||
|
public static class DefaultJsonSerializerOptions
|
||||||
|
{
|
||||||
|
public static readonly JsonSerializerOptions Default = new()
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||||
|
};
|
||||||
|
|
||||||
|
public static readonly JsonSerializerOptions CamelCasePrettyPrint = new()
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
|
WriteIndented = true
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -4,15 +4,12 @@ namespace MycroForge.Core.Extensions;
|
|||||||
|
|
||||||
public static class ObjectStreamExtensions
|
public static class ObjectStreamExtensions
|
||||||
{
|
{
|
||||||
public static async Task<string> SerializeAsync(
|
public static async Task<string> SerializeAsync(this object @object, JsonSerializerOptions? options = null)
|
||||||
this object @object,
|
|
||||||
JsonSerializerOptions? jsonSerializerOptions = null
|
|
||||||
)
|
|
||||||
{
|
{
|
||||||
using var stream = new MemoryStream();
|
using var stream = new MemoryStream();
|
||||||
using var reader = new StreamReader(stream);
|
using var reader = new StreamReader(stream);
|
||||||
var options = jsonSerializerOptions ?? Serialization.DefaultJsonSerializerOptions.Default;
|
options ??= DefaultJsonSerializerOptions.Default;
|
||||||
|
|
||||||
await JsonSerializer.SerializeAsync(stream, @object, options);
|
await JsonSerializer.SerializeAsync(stream, @object, options);
|
||||||
stream.Position = 0;
|
stream.Position = 0;
|
||||||
return await reader.ReadToEndAsync();
|
return await reader.ReadToEndAsync();
|
||||||
|
|||||||
16
MycroForge.Core/ProjectConfig.DbConfig.DbuPlatformOptions.cs
Normal file
16
MycroForge.Core/ProjectConfig.DbConfig.DbuPlatformOptions.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
namespace MycroForge.Core;
|
||||||
|
|
||||||
|
public partial class ProjectConfig
|
||||||
|
{
|
||||||
|
public partial class DbConfig
|
||||||
|
{
|
||||||
|
public enum DbuPlatformOptions
|
||||||
|
{
|
||||||
|
amd64,
|
||||||
|
arm32v5,
|
||||||
|
arm32v6,
|
||||||
|
arm32v7,
|
||||||
|
arm64v8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,16 @@
|
|||||||
namespace MycroForge.Core;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace MycroForge.Core;
|
||||||
|
|
||||||
public partial class ProjectConfig
|
public partial class ProjectConfig
|
||||||
{
|
{
|
||||||
public class DbConfig
|
public partial class DbConfig
|
||||||
{
|
{
|
||||||
public int DbhPort { get; set; }
|
public int DbhPort { get; set; }
|
||||||
|
|
||||||
public int DbuPort { get; set; }
|
public int DbuPort { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public DbuPlatformOptions DbuPlatform { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,7 +9,7 @@ public class ProjectContext
|
|||||||
{
|
{
|
||||||
public string RootDirectory { get; private set; } = Environment.CurrentDirectory;
|
public string RootDirectory { get; private set; } = Environment.CurrentDirectory;
|
||||||
public string AppName => Path.GetFileNameWithoutExtension(RootDirectory).Underscore().ToLower();
|
public string AppName => Path.GetFileNameWithoutExtension(RootDirectory).Underscore().ToLower();
|
||||||
private string ConfigPath => Path.Combine(RootDirectory, "m4g.json");
|
private string ConfigPath => Path.Join(RootDirectory, "m4g.json");
|
||||||
|
|
||||||
|
|
||||||
public async Task<ProjectConfig> LoadConfig(bool create = false)
|
public async Task<ProjectConfig> LoadConfig(bool create = false)
|
||||||
@@ -21,8 +21,8 @@ public class ProjectContext
|
|||||||
}
|
}
|
||||||
|
|
||||||
var config = await JsonSerializer.DeserializeAsync<ProjectConfig>(
|
var config = await JsonSerializer.DeserializeAsync<ProjectConfig>(
|
||||||
File.OpenRead(ConfigPath),
|
File.OpenRead(ConfigPath),
|
||||||
Serialization.DefaultJsonSerializerOptions.CamelCasePrettyPrint
|
DefaultJsonSerializerOptions.CamelCasePrettyPrint
|
||||||
);
|
);
|
||||||
|
|
||||||
if (config is null)
|
if (config is null)
|
||||||
@@ -37,21 +37,9 @@ public class ProjectContext
|
|||||||
RootDirectory = path;
|
RootDirectory = path;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void AssertDirectoryExists(string path)
|
|
||||||
{
|
|
||||||
var fullPath = Path.Combine(RootDirectory, path);
|
|
||||||
|
|
||||||
if (!Directory.Exists(fullPath))
|
|
||||||
{
|
|
||||||
throw new(string.Join('\n',
|
|
||||||
$"{fullPath} does not exist, make sure you're in the correct directory."
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task CreateFile(string path, params string[] content)
|
public async Task CreateFile(string path, params string[] content)
|
||||||
{
|
{
|
||||||
var fullPath = Path.Combine(RootDirectory, path);
|
var fullPath = Path.Join(RootDirectory, path);
|
||||||
var fileInfo = new FileInfo(fullPath);
|
var fileInfo = new FileInfo(fullPath);
|
||||||
|
|
||||||
if (fileInfo.Exists) return;
|
if (fileInfo.Exists) return;
|
||||||
@@ -59,11 +47,12 @@ public class ProjectContext
|
|||||||
Directory.CreateDirectory(fileInfo.Directory!.FullName);
|
Directory.CreateDirectory(fileInfo.Directory!.FullName);
|
||||||
await File.WriteAllTextAsync(fullPath, string.Join("\n", content));
|
await File.WriteAllTextAsync(fullPath, string.Join("\n", content));
|
||||||
await Bash($"chmod 777 {fullPath}");
|
await Bash($"chmod 777 {fullPath}");
|
||||||
|
Console.WriteLine($"Created file {path}");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string> ReadFile(string path)
|
public async Task<string> ReadFile(string path)
|
||||||
{
|
{
|
||||||
var fullPath = Path.Combine(RootDirectory, path);
|
var fullPath = Path.Join(RootDirectory, path);
|
||||||
var fileInfo = new FileInfo(fullPath);
|
var fileInfo = new FileInfo(fullPath);
|
||||||
|
|
||||||
if (!fileInfo.Exists)
|
if (!fileInfo.Exists)
|
||||||
@@ -74,10 +63,11 @@ public class ProjectContext
|
|||||||
|
|
||||||
public async Task WriteFile(string path, params string[] content)
|
public async Task WriteFile(string path, params string[] content)
|
||||||
{
|
{
|
||||||
var fullPath = Path.Combine(RootDirectory, path);
|
var fullPath = Path.Join(RootDirectory, path);
|
||||||
var fileInfo = new FileInfo(fullPath);
|
var fileInfo = new FileInfo(fullPath);
|
||||||
Directory.CreateDirectory(fileInfo.Directory!.FullName);
|
Directory.CreateDirectory(fileInfo.Directory!.FullName);
|
||||||
await File.WriteAllTextAsync(fullPath, string.Join("\n", content));
|
await File.WriteAllTextAsync(fullPath, string.Join("\n", content));
|
||||||
|
Console.WriteLine($"Modified file {path}");
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Bash(params string[] script)
|
public async Task Bash(params string[] script)
|
||||||
@@ -114,21 +104,19 @@ public class ProjectContext
|
|||||||
process.BeginErrorReadLine();
|
process.BeginErrorReadLine();
|
||||||
|
|
||||||
await using var input = process.StandardInput;
|
await using var input = process.StandardInput;
|
||||||
foreach (var line in script)
|
// Concat with '&&' operator to make sure that script does not continue on failure.
|
||||||
await input.WriteLineAsync(line);
|
await input.WriteAsync(string.Join(" && ", script));
|
||||||
|
|
||||||
await input.FlushAsync();
|
await input.FlushAsync();
|
||||||
input.Close();
|
input.Close();
|
||||||
|
|
||||||
await process.WaitForExitAsync();
|
await process.WaitForExitAsync();
|
||||||
|
Environment.ExitCode = process.ExitCode;
|
||||||
if (process.ExitCode != 0)
|
|
||||||
Console.WriteLine($"Process finished with exit code {process.ExitCode}.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SaveConfig(ProjectConfig config)
|
public async Task SaveConfig(ProjectConfig config)
|
||||||
{
|
{
|
||||||
var json = await config.SerializeAsync(Serialization.DefaultJsonSerializerOptions.CamelCasePrettyPrint);
|
var json = await config.SerializeAsync(DefaultJsonSerializerOptions.CamelCasePrettyPrint);
|
||||||
await File.WriteAllTextAsync(ConfigPath, json);
|
await File.WriteAllTextAsync(ConfigPath, json);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
using System.Text.Json;
|
|
||||||
|
|
||||||
namespace MycroForge.Core;
|
|
||||||
|
|
||||||
public static class Serialization
|
|
||||||
{
|
|
||||||
public static class DefaultJsonSerializerOptions
|
|
||||||
{
|
|
||||||
public static readonly JsonSerializerOptions Default = new()
|
|
||||||
{
|
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
|
||||||
};
|
|
||||||
|
|
||||||
public static readonly JsonSerializerOptions CamelCasePrettyPrint = new()
|
|
||||||
{
|
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
|
||||||
WriteIndented = true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
4
MycroForge.Core/scripts/publish-nuget.sh
Normal file
4
MycroForge.Core/scripts/publish-nuget.sh
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
dotnet build -r Release
|
||||||
|
dotnet nuget push --source devdisciples bin/Release/MycroForge.Core.1.0.0.nupkg
|
||||||
@@ -8,7 +8,7 @@ https://learn.microsoft.com/en-us/dotnet/core/tutorials/cli-templates-create-tem
|
|||||||
### Build the package
|
### Build the package
|
||||||
`dotnet pack`
|
`dotnet pack`
|
||||||
|
|
||||||
### Push to local nuget
|
### Push to devdisciples nuget
|
||||||
`dotnet nuget push bin/Release/MycroForge.PluginTemplate.Package.1.0.0.nupkg --source devdisciples`
|
`dotnet nuget push bin/Release/MycroForge.PluginTemplate.Package.1.0.0.nupkg --source devdisciples`
|
||||||
|
|
||||||
### Install template package from local nuget
|
### Install template package from local nuget
|
||||||
0
MycroForge.PluginTemplate/readme.md
Normal file
0
MycroForge.PluginTemplate/readme.md
Normal file
@@ -8,6 +8,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MycroForge.PluginTemplate",
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MycroForge.PluginTemplate.Package", "MycroForge.PluginTemplate.Package\MycroForge.PluginTemplate.Package.csproj", "{1C5C5B9A-3C90-4FE7-A1AC-2F46C3CD0D69}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MycroForge.PluginTemplate.Package", "MycroForge.PluginTemplate.Package\MycroForge.PluginTemplate.Package.csproj", "{1C5C5B9A-3C90-4FE7-A1AC-2F46C3CD0D69}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MycroForge.CLI.Tests", "MycroForge.CLI.Tests\MycroForge.CLI.Tests.csproj", "{71A7EA9D-3C12-4FDE-BA4F-BDD1961DDA1B}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -30,5 +32,9 @@ Global
|
|||||||
{1C5C5B9A-3C90-4FE7-A1AC-2F46C3CD0D69}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{1C5C5B9A-3C90-4FE7-A1AC-2F46C3CD0D69}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{1C5C5B9A-3C90-4FE7-A1AC-2F46C3CD0D69}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{1C5C5B9A-3C90-4FE7-A1AC-2F46C3CD0D69}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{1C5C5B9A-3C90-4FE7-A1AC-2F46C3CD0D69}.Release|Any CPU.Build.0 = Release|Any CPU
|
{1C5C5B9A-3C90-4FE7-A1AC-2F46C3CD0D69}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{71A7EA9D-3C12-4FDE-BA4F-BDD1961DDA1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{71A7EA9D-3C12-4FDE-BA4F-BDD1961DDA1B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{71A7EA9D-3C12-4FDE-BA4F-BDD1961DDA1B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{71A7EA9D-3C12-4FDE-BA4F-BDD1961DDA1B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|||||||
30
README.md
30
README.md
@@ -1,29 +1,3 @@
|
|||||||
### Dependencies
|
### Documentation
|
||||||
|
|
||||||
- git
|
Go to https://m4g.devdisciples.com for the docs.
|
||||||
- Docker
|
|
||||||
- bash (/bin/bash)
|
|
||||||
- Python 3.10.2 (/usr/bin/python3)
|
|
||||||
- python3-pip
|
|
||||||
- python3-venv
|
|
||||||
|
|
||||||
#### Note
|
|
||||||
The MycroForge CLI assumes a linux compatible environment, so on Windows you'll have to use WSL.
|
|
||||||
`Ubuntu-22.04` is the recommended WSL version to use.
|
|
||||||
|
|
||||||
### TODO
|
|
||||||
- Figure out why BashException cannot be caught, can it be due to the differences in scoping?
|
|
||||||
Because the `Bash` class is static and the services calling `Bash.ExecuteAsync` are in the container.
|
|
||||||
Maybe this in combination with the async nature of the whole thing?
|
|
||||||
|
|
||||||
### Install
|
|
||||||
|
|
||||||
Run the install script in the same directory as the downloaded zip. See the example below for linux-x64.
|
|
||||||
|
|
||||||
`sudo ./install.sh m4g-<platform>.zip <platform>`
|
|
||||||
|
|
||||||
### Add DevDisciples NuGet source
|
|
||||||
|
|
||||||
```bash
|
|
||||||
dotnet nuget add source --name devdisciples --username username --password password https://git.devdisciples.com/api/packages/devdisciples/nuget/index.json --store-password-in-clear-text
|
|
||||||
```
|
|
||||||
3
docs/.dockerignore
Normal file
3
docs/.dockerignore
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.docusaurus/
|
||||||
|
node_modules/
|
||||||
|
.k8s/
|
||||||
3
docs/.gitignore
vendored
3
docs/.gitignore
vendored
@@ -18,3 +18,6 @@
|
|||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
|
|
||||||
|
.k8s/remote*.yml
|
||||||
|
!.k8s/remote.example.yml
|
||||||
61
docs/.k8s/local.yml
Normal file
61
docs/.k8s/local.yml
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
# =========================================
|
||||||
|
# App manifest
|
||||||
|
# =========================================
|
||||||
|
|
||||||
|
---
|
||||||
|
# App Deployment
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: m4g-docs-deployment
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: m4g-docs
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: m4g-docs
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: m4g-docs
|
||||||
|
image: m4gdocs:latest
|
||||||
|
imagePullPolicy: Never
|
||||||
|
ports:
|
||||||
|
- containerPort: 80
|
||||||
|
|
||||||
|
---
|
||||||
|
# App Service
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: m4g-docs-service
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: m4g-docs
|
||||||
|
ports:
|
||||||
|
- protocol: TCP
|
||||||
|
port: 80
|
||||||
|
targetPort: 80
|
||||||
|
type: NodePort
|
||||||
|
|
||||||
|
---
|
||||||
|
# App Ingress
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: m4g-docs-ingress
|
||||||
|
spec:
|
||||||
|
ingressClassName: caddy
|
||||||
|
rules:
|
||||||
|
- host: m4g.docs.local
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: m4g-docs-service
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
67
docs/.k8s/remote.example.yml
Normal file
67
docs/.k8s/remote.example.yml
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# =========================================
|
||||||
|
# App manifest
|
||||||
|
# =========================================
|
||||||
|
|
||||||
|
---
|
||||||
|
# App Deployment
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: m4g-docs-deployment
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: m4g-docs
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: m4g-docs
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: m4g-docs
|
||||||
|
image: git.devdisciples.com/devdisciples/m4gdocs:latest
|
||||||
|
imagePullPolicy: Always
|
||||||
|
ports:
|
||||||
|
- containerPort: 80
|
||||||
|
|
||||||
|
---
|
||||||
|
# App Service
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: m4g-docs-service
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: m4g-docs
|
||||||
|
ports:
|
||||||
|
- protocol: TCP
|
||||||
|
port: 80
|
||||||
|
targetPort: 80
|
||||||
|
type: NodePort
|
||||||
|
|
||||||
|
---
|
||||||
|
# App Ingress
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: m4g-docs-ingress
|
||||||
|
annotations:
|
||||||
|
cert-manager.io/cluster-issuer: lets-encrypt
|
||||||
|
spec:
|
||||||
|
ingressClassName: public
|
||||||
|
tls:
|
||||||
|
- hosts:
|
||||||
|
- m4g.example.com
|
||||||
|
secretName: example-tls-secret
|
||||||
|
rules:
|
||||||
|
- host: m4g.example.com
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: m4g-docs-service
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
27
docs/Dockerfile
Normal file
27
docs/Dockerfile
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
|
# Stage 1: Base image.
|
||||||
|
## Start with a base image containing NodeJS so we can build Docusaurus.
|
||||||
|
FROM node:lts AS base
|
||||||
|
## Disable colour output from yarn to make logs easier to read.
|
||||||
|
ENV FORCE_COLOR=0
|
||||||
|
## Enable corepack.
|
||||||
|
RUN corepack enable
|
||||||
|
## Set the working directory to `/opt/docusaurus`.
|
||||||
|
WORKDIR /opt/docusaurus
|
||||||
|
|
||||||
|
# Stage 2b: Production build mode.
|
||||||
|
FROM base AS prod
|
||||||
|
## Set the working directory to `/opt/docusaurus`.
|
||||||
|
WORKDIR /opt/docusaurus
|
||||||
|
## Copy over the source code.
|
||||||
|
COPY . /opt/docusaurus/
|
||||||
|
## Install dependencies with `--immutable` to ensure reproducibility.
|
||||||
|
RUN npm ci
|
||||||
|
## Build the static site.
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
## Use a stable nginx image
|
||||||
|
FROM nginx:stable-alpine AS deploy
|
||||||
|
WORKDIR /home/node/app
|
||||||
|
COPY --chown=node:node --from=prod /opt/docusaurus/build/ /usr/share/nginx/html/
|
||||||
@@ -39,3 +39,24 @@ $ GIT_USER=<Your GitHub username> yarn deploy
|
|||||||
```
|
```
|
||||||
|
|
||||||
If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
|
If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
|
||||||
|
|
||||||
|
### Build Docker image
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t git.devdisciples.com/devdisciples/m4gdocs:latest -t m4gdocs:latest .
|
||||||
|
```
|
||||||
|
|
||||||
|
### Push Docker image
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker push git.devdisciples.com/devdisciples/m4gdocs:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Update in Kubernetes Cluster
|
||||||
|
|
||||||
|
Update the image by deleting the old pod, this will automatically pull in the latest image and deploy it.
|
||||||
|
Run the following command on the relevant node to achieve this.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kubectl delete pods -l app=m4g-docs
|
||||||
|
```
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 5
|
||||||
|
---
|
||||||
|
|
||||||
# Commands
|
# Commands
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ Usage:
|
|||||||
m4g add db [options]
|
m4g add db [options]
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--database-host-port, --dbh-port <database-host-port> The database host 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-port, --dbu-port <database-ui-port> The database UI port
|
||||||
-?, -h, --help Show help and usage information
|
--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
|
||||||
```
|
```
|
||||||
@@ -15,9 +15,10 @@ Arguments:
|
|||||||
<name> The name of your project
|
<name> The name of your project
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--without <api|db|git|gitignore> Features to exclude
|
--without <api|db|git|gitignore> Features to exclude
|
||||||
--api-port <api-port> The API port
|
--api-port <api-port> The API port
|
||||||
--database-host-port, --dbh-port <database-host-port> The database host 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-port, --dbu-port <database-ui-port> The database UI port
|
||||||
-?, -h, --help Show help and usage information
|
--database-ui-platform, --dbu-platform <amd64|arm32v5|arm32v6|arm32v7|arm64v8> The docker platform for the PhpMyAdmin image
|
||||||
|
-?, -h, --help Show help and usage information
|
||||||
```
|
```
|
||||||
|
|||||||
178
docs/docs/command_plugins.md
Normal file
178
docs/docs/command_plugins.md
Normal 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/docs/getting_started.md
Normal file
78
docs/docs/getting_started.md
Normal 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.
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
---
|
|
||||||
sidebar_position: 2
|
|
||||||
---
|
|
||||||
|
|
||||||
# Install
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
MycroForge has the following dependencies.
|
|
||||||
|
|
||||||
- bash
|
|
||||||
- git
|
|
||||||
- Python3 (3.10)
|
|
||||||
- Docker
|
|
||||||
- .NET 8
|
|
||||||
|
|
||||||
### Windows
|
|
||||||
|
|
||||||
To simplify the implementation of this tool, it assumes that it's running in a POSIX compliant environment.
|
|
||||||
MycroForge has been developed and tested on Windows in WSL2 Ubuntu 22.04.03.
|
|
||||||
So when running on Windows, it's recommended to run MycroForge in the same environment or atleast in a similar WSL2 distro.
|
|
||||||
@@ -2,144 +2,15 @@
|
|||||||
sidebar_position: 1
|
sidebar_position: 1
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
# Intro
|
# Intro
|
||||||
|
|
||||||
## What is MycroForge?
|
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.
|
||||||
|
|
||||||
MycroForge is an opinionated CLI tool that is meant to facilitate the development of FastAPI & SQLAlchemy based backends.
|
## Key Features
|
||||||
It provides a command line interface that allows users to generate a skeleton project and other common items like
|
|
||||||
database entities, migrations, routers and even basic CRUD functionality. The main purpose of this tool is to generate
|
|
||||||
boilerplate code and to provide a unified interface for performing recurrent development activities.
|
|
||||||
|
|
||||||
|
- **Project Skeleton Generation:** Quickly generate a well-structured project skeleton tailored for FastAPI and SQLAlchemy, ensuring you start with best practices.
|
||||||
## Generating a project
|
- **Database Entities:** Easily create and manage database entities, simplifying your database interactions.
|
||||||
|
- **Migrations:** Handle database migrations seamlessly, allowing for smooth transitions and updates.
|
||||||
To generate a project you can run the following command.
|
- **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.
|
||||||
`m4g init <name>`
|
|
||||||
|
|
||||||
```
|
|
||||||
Description:
|
|
||||||
Initialize a new project
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
m4g init <name> [options]
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
<name> The name of your project
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--without <api|db|git> Features to exclude
|
|
||||||
-?, -h, --help Show help and usage information
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Running this command will generate the following project structure.
|
|
||||||
|
|
||||||
```
|
|
||||||
📦<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.
|
|
||||||
TODO: This is a good section to introduce the `m4g hydrate` command.
|
|
||||||
|
|
||||||
### 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.
|
|
||||||
TODO: introduce the `m4g install` & `m4g uninstall` commands.
|
|
||||||
|
|
||||||
|
|
||||||
## Plugin system
|
|
||||||
|
|
||||||
TODO: Dedicate a section to the Plugin system
|
|
||||||
|
|||||||
@@ -1,36 +1,10 @@
|
|||||||
# General introduction
|
---
|
||||||
|
sidebar_position: 4
|
||||||
|
---
|
||||||
|
|
||||||
## What is MycroForge?
|
# Project layout
|
||||||
|
|
||||||
MycroForge is an opinionated CLI tool that is meant to facilitate the development of FastAPI & SQLAlchemy based backends.
|
When you generate a new project with `m4g init <project_name>`, it will create a folder like the example below.
|
||||||
It provides a command line interface that allows users to generate a skeleton project and other common items like
|
|
||||||
database entities, migrations, routers and even basic CRUD functionality. The main purpose of this tool is to generate
|
|
||||||
boilerplate code and to provide a unified interface for performing recurrent development activities.
|
|
||||||
|
|
||||||
|
|
||||||
## Generating a project
|
|
||||||
|
|
||||||
To generate a project you can run the following command.
|
|
||||||
|
|
||||||
`m4g init <name>`
|
|
||||||
|
|
||||||
```
|
|
||||||
Description:
|
|
||||||
Initialize a new project
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
m4g init <name> [options]
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
<name> The name of your project
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--without <api|db|git> Features to exclude
|
|
||||||
-?, -h, --help Show help and usage information
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
Running this command will generate the following project structure.
|
|
||||||
|
|
||||||
```
|
```
|
||||||
📦<project_name>
|
📦<project_name>
|
||||||
@@ -67,7 +41,8 @@ If you don't want to use git you can pass the option `--without git` to `m4g ini
|
|||||||
### .venv
|
### .venv
|
||||||
|
|
||||||
To promote isolation of Python dependencies, new projects are initialized with a virtual environment by default.
|
To promote isolation of Python dependencies, new projects are initialized with a virtual environment by default.
|
||||||
TODO: This is a good section to introduce the `m4g hydrate` command.
|
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
|
### api/routers/hello.py
|
||||||
|
|
||||||
@@ -133,9 +108,4 @@ modified to include the generated files.
|
|||||||
### requirements.txt
|
### requirements.txt
|
||||||
|
|
||||||
The requirements file containing the Python dependencies.
|
The requirements file containing the Python dependencies.
|
||||||
TODO: introduce the `m4g install` & `m4g uninstall` commands.
|
Whenever you run `m4g install` or `m4g uninstall` this file will be updated too.
|
||||||
|
|
||||||
|
|
||||||
## Scripting
|
|
||||||
|
|
||||||
TODO: Dedicate a section to scripting
|
|
||||||
@@ -4,20 +4,21 @@ sidebar_position: 3
|
|||||||
|
|
||||||
# Tutorial
|
# Tutorial
|
||||||
|
|
||||||
We're going to build a simple todo app to demonstrate the capabilities of the MycroForge CLI.
|
In this tutorial, we'll 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
|
By the end, you should have a solid foundation to start exploring and using MycroForge for your projects.
|
||||||
projects.
|
|
||||||
|
|
||||||
## General notes
|
## General notes
|
||||||
|
|
||||||
The commands in this tutorial assume that you're running them from a MycroForge root directory.
|
The commands in this tutorial assume that you are running them from the root directory of your MycroForge project.
|
||||||
|
|
||||||
## Initialize the project
|
## Initialize the Project
|
||||||
|
|
||||||
Open a terminal and `cd` into the directory where your project should be created.
|
Open a terminal and navigate (`cd`) to 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`.
|
Run the following command to initialize a new project and open it in VSCode:
|
||||||
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.
|
```bash
|
||||||
|
m4g init todo-app
|
||||||
|
```
|
||||||
|
|
||||||
## Setup the database
|
## Setup the database
|
||||||
|
|
||||||
@@ -31,28 +32,43 @@ locally.
|
|||||||
|
|
||||||
The first step is to start the database, you can do this by running the following command in a terminal.
|
The first step is to start the database, you can do this by running the following command in a terminal.
|
||||||
|
|
||||||
`m4g db run`
|
```bash
|
||||||
|
m4g db run
|
||||||
|
```
|
||||||
|
|
||||||
This command starts the services defined in the `db.docker-compose.yml` file.
|
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
|
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`.
|
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
|
Go to [PhpMyAdmin (i.e. http://localhost:5051)](http://localhost:5051). You should now be able to login with the
|
||||||
following credentials.
|
following credentials.
|
||||||
- user: root
|
- user: root
|
||||||
- pass: password
|
- pass: password
|
||||||
|
|
||||||
When you're done developing, you can shut down the local database by running `m4g db stop`
|
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
|
### Create the entities
|
||||||
|
|
||||||
Now that the database is running, we can start to create our entities. Run the commands below to create the `Todo` &
|
Now that the database is running, we can start creating our entities. Run the commands below to create the `Todo` &
|
||||||
`Tag` entities.
|
`Tag` entities.
|
||||||
|
|
||||||
`m4g db generate entity Tag --column "description:str:String(255)"`
|
```bash
|
||||||
|
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()"`
|
```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.
|
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.
|
You should also see that the `main.py` & `db/env.py` files have been modified to include the newly generated entity.
|
||||||
@@ -68,7 +84,9 @@ Creating a one-to-many relation would also make sense, but for the purpose of de
|
|||||||
the many-to-many relation, because this one is the most complex, since it requires an additional mapping to be included
|
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.
|
in the database schema.
|
||||||
|
|
||||||
`m4g db link many Todo --to-many Tag`
|
```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
|
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.
|
a `List` containing instances of the other entity.
|
||||||
@@ -82,7 +100,9 @@ examine the command. The same is true for all the other commands as well.
|
|||||||
Now that we've generated our entities, it's time to generate a migration that will apply these changes in the database.
|
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.
|
Generate the initial migration by running the following command.
|
||||||
|
|
||||||
`m4g db generate migration initial_migration`
|
```bash
|
||||||
|
m4g db generate migration initial_migration
|
||||||
|
```
|
||||||
|
|
||||||
After running this command, you should see the new migration in the `db/version` directory.
|
After running this command, you should see the new migration in the `db/version` directory.
|
||||||
|
|
||||||
@@ -91,7 +111,9 @@ After running this command, you should see the new migration in the `db/version`
|
|||||||
The last step for the database setup is to actually apply the new migration to the database. This can be done by running
|
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.
|
the following command.
|
||||||
|
|
||||||
`m4g db migrate`
|
```bash
|
||||||
|
m4g db migrate
|
||||||
|
```
|
||||||
|
|
||||||
After running this command, you should now see a populated schema when visiting [PhpMyAdmin](http://localhost:5051).
|
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`.
|
If for whatever reason you want to undo the last migration, you can simply run `m4g db rollback`.
|
||||||
@@ -105,9 +127,13 @@ Writing this code can be boring, since it's pretty much boilerplate with some cu
|
|||||||
Fortunately, MycroForge can generate a good chunk of this boring code on your behalf. Run the following commands to
|
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.
|
generate CRUD functionality for the `Todo` & `Tag` classes.
|
||||||
|
|
||||||
`m4g api generate crud Tag`
|
```bash
|
||||||
|
m4g api generate crud Tag
|
||||||
|
```
|
||||||
|
|
||||||
`m4g api generate crud Todo`
|
```bash
|
||||||
|
m4g api generate crud Todo
|
||||||
|
```
|
||||||
|
|
||||||
After running this command you should see that the `api/requests`,`api/routers` & `api/services` now contain the
|
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
|
relevant classes need to support the generated CRUD functionality. This could should be relatively straightforward, so
|
||||||
@@ -121,15 +147,22 @@ yet. We need to be able to specify which `Tags` to add to a `Todo` when creating
|
|||||||
To do this, we will allow for a `tag_ids` field in both the `CreateTodoRequest` & the `UpdateTodoRequest`.
|
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`.
|
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`.
|
Modify `CreateTodoRequest` in `api/requests/create_todo_request.py`.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
# Before
|
# Before
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
class CreateTodoRequest(BaseModel):
|
class CreateTodoRequest(BaseModel):
|
||||||
description: str = None
|
description: str = None
|
||||||
is_done: bool = None
|
is_done: bool = None
|
||||||
|
|
||||||
# After
|
# After
|
||||||
|
from typing import List, Optional
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
class CreateTodoRequest(BaseModel):
|
class CreateTodoRequest(BaseModel):
|
||||||
description: str = None
|
description: str = None
|
||||||
is_done: bool = None
|
is_done: bool = None
|
||||||
@@ -140,12 +173,19 @@ Modify `UpdateTodoRequest` in `api/requests/update_todo_request.py`, you might n
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
# Before
|
# Before
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
class UpdateTodoRequest(BaseModel):
|
class UpdateTodoRequest(BaseModel):
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
is_done: Optional[bool] = None
|
is_done: Optional[bool] = None
|
||||||
tag_ids: Optional[List[int]] = []
|
|
||||||
|
|
||||||
# After
|
# After
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
|
||||||
class UpdateTodoRequest(BaseModel):
|
class UpdateTodoRequest(BaseModel):
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
is_done: Optional[bool] = None
|
is_done: Optional[bool] = None
|
||||||
@@ -167,16 +207,18 @@ Modify `TodoService.list`
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
# Before
|
# Before
|
||||||
async with async_session() as session:
|
async def list(self) -> List[Todo]:
|
||||||
stmt = select(Todo)
|
async with async_session() as session:
|
||||||
results = (await session.scalars(stmt)).all()
|
stmt = select(Todo)
|
||||||
return results
|
results = (await session.scalars(stmt)).all()
|
||||||
|
return results
|
||||||
|
|
||||||
# After
|
# After
|
||||||
async with async_session() as session:
|
async def list(self) -> List[Todo]:
|
||||||
stmt = select(Todo).options(selectinload(Todo.tags))
|
async with async_session() as session:
|
||||||
results = (await session.scalars(stmt)).all()
|
stmt = select(Todo).options(selectinload(Todo.tags))
|
||||||
return results
|
results = (await session.scalars(stmt)).all()
|
||||||
|
return results
|
||||||
```
|
```
|
||||||
|
|
||||||
Modify `TodoService.get_by_id`
|
Modify `TodoService.get_by_id`
|
||||||
@@ -279,5 +321,11 @@ Modify `TodoService.update`
|
|||||||
return True
|
return True
|
||||||
```
|
```
|
||||||
|
|
||||||
At this point, the app should be ready to test.
|
## Test the API!
|
||||||
TODO: Elaborate!
|
|
||||||
|
Run the following command.
|
||||||
|
```bash
|
||||||
|
m4g api run
|
||||||
|
```
|
||||||
|
|
||||||
|
Go to http://localhost:5000/docs and test your Todo API!
|
||||||
@@ -4,8 +4,8 @@ import type * as Preset from '@docusaurus/preset-classic';
|
|||||||
|
|
||||||
const config: Config = {
|
const config: Config = {
|
||||||
title: 'MycroForge',
|
title: 'MycroForge',
|
||||||
tagline: 'Your FastAPI & SQLAlchemy assistant!',
|
tagline: 'Your FastAPI & SQLAlchemy toolkit!',
|
||||||
favicon: 'img/favicon.ico',
|
// favicon: 'img/favicon.ico',
|
||||||
|
|
||||||
// Set the production url of your site here
|
// Set the production url of your site here
|
||||||
url: 'https://git.devdisciples.com',
|
url: 'https://git.devdisciples.com',
|
||||||
@@ -52,10 +52,10 @@ const config: Config = {
|
|||||||
image: 'img/docusaurus-social-card.jpg',
|
image: 'img/docusaurus-social-card.jpg',
|
||||||
navbar: {
|
navbar: {
|
||||||
title: 'MycroForge',
|
title: 'MycroForge',
|
||||||
logo: {
|
// logo: {
|
||||||
alt: 'MycroForge Logo',
|
// alt: 'MycroForge Logo',
|
||||||
src: 'img/logo.svg',
|
// src: 'img/logo.svg',
|
||||||
},
|
// },
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
type: 'docSidebar',
|
type: 'docSidebar',
|
||||||
@@ -77,8 +77,9 @@ const config: Config = {
|
|||||||
copyright: `Copyright © ${new Date().getFullYear()} DevDisciples`,
|
copyright: `Copyright © ${new Date().getFullYear()} DevDisciples`,
|
||||||
},
|
},
|
||||||
prism: {
|
prism: {
|
||||||
theme: prismThemes.github,
|
theme: prismThemes.oneLight,
|
||||||
darkTheme: prismThemes.dracula,
|
darkTheme: prismThemes.oneDark,
|
||||||
|
additionalLanguages: ["csharp"]
|
||||||
},
|
},
|
||||||
} satisfies Preset.ThemeConfig,
|
} satisfies Preset.ThemeConfig,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"docusaurus": "docusaurus",
|
"docusaurus": "docusaurus",
|
||||||
"start": "docusaurus start",
|
"start": "docusaurus start --host 0.0.0.0",
|
||||||
"build": "docusaurus build",
|
"build": "docusaurus build",
|
||||||
"swizzle": "docusaurus swizzle",
|
"swizzle": "docusaurus swizzle",
|
||||||
"deploy": "docusaurus deploy",
|
"deploy": "docusaurus deploy",
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
asgiref==3.5.0
|
|
||||||
blinker==1.4
|
|
||||||
click==8.0.3
|
|
||||||
colorama==0.4.4
|
|
||||||
command-not-found==0.3
|
|
||||||
cryptography==3.4.8
|
|
||||||
dbus-python==1.2.18
|
|
||||||
distro==1.7.0
|
|
||||||
distro-info==1.1+ubuntu0.2
|
|
||||||
h11==0.13.0
|
|
||||||
httplib2==0.20.2
|
|
||||||
importlib-metadata==4.6.4
|
|
||||||
jeepney==0.7.1
|
|
||||||
keyring==23.5.0
|
|
||||||
launchpadlib==1.10.16
|
|
||||||
lazr.restfulclient==0.14.4
|
|
||||||
lazr.uri==1.0.6
|
|
||||||
more-itertools==8.10.0
|
|
||||||
netifaces==0.11.0
|
|
||||||
oauthlib==3.2.0
|
|
||||||
PyGObject==3.42.1
|
|
||||||
PyJWT==2.3.0
|
|
||||||
pyparsing==2.4.7
|
|
||||||
python-apt==2.4.0+ubuntu3
|
|
||||||
PyYAML==5.4.1
|
|
||||||
SecretStorage==3.3.1
|
|
||||||
six==1.16.0
|
|
||||||
systemd-python==234
|
|
||||||
ubuntu-pro-client==8001
|
|
||||||
ufw==0.36.1
|
|
||||||
unattended-upgrades==0.1
|
|
||||||
uvicorn==0.15.0
|
|
||||||
wadllib==1.3.6
|
|
||||||
wsproto==1.0.0
|
|
||||||
zipp==1.0.0
|
|
||||||
@@ -4,47 +4,49 @@ import styles from './styles.module.css';
|
|||||||
|
|
||||||
type FeatureItem = {
|
type FeatureItem = {
|
||||||
title: string;
|
title: string;
|
||||||
Svg: React.ComponentType<React.ComponentProps<'svg'>>;
|
// Svg: React.ComponentType<React.ComponentProps<'svg'>>;
|
||||||
description: JSX.Element;
|
description: JSX.Element;
|
||||||
};
|
};
|
||||||
|
|
||||||
const FeatureList: FeatureItem[] = [
|
const FeatureList: FeatureItem[] = [
|
||||||
{
|
{
|
||||||
title: 'Initialize a skeleton project quickly',
|
title: 'Initialize a Skeleton Project Quickly',
|
||||||
Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default,
|
// Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default,
|
||||||
description: (
|
description: (
|
||||||
<>
|
<>
|
||||||
Initialize a skeleton project that supports FastAPI and SQLAlchemy with a single command.
|
Start a new FastAPI and SQLAlchemy project with a single command.
|
||||||
Here is an example. <code>m4g init todo-app</code>
|
For example: <code>m4g init todo-app</code>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Generate common items',
|
title: 'Generate Common Items',
|
||||||
Svg: require('@site/static/img/undraw_docusaurus_tree.svg').default,
|
// Svg: require('@site/static/img/undraw_docusaurus_tree.svg').default,
|
||||||
description: (
|
description: (
|
||||||
<>
|
<>
|
||||||
MycroForge allows you to generate boilerplate code for common items like entities, service & routers.
|
Use MycroForge to generate boilerplate code for entities, services, and routers.
|
||||||
It can even generate a basic CRUD setup around an entity!
|
It even supports basic CRUD setup!
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'Extend MycroForge',
|
title: 'Extend MycroForge',
|
||||||
Svg: require('@site/static/img/undraw_docusaurus_react.svg').default,
|
// Svg: '',
|
||||||
|
// Svg: require('@site/static/img/undraw_docusaurus_react.svg').default,
|
||||||
description: (
|
description: (
|
||||||
<>
|
<>
|
||||||
Extend MycroForge with your own commands by creating a plugin!
|
Create plugins to extend MycroForge with your own custom commands.
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
function Feature({title, Svg, description}: FeatureItem) {
|
function Feature({ title, description }: FeatureItem) {
|
||||||
|
// function Feature({ title, Svg, description }: FeatureItem) {
|
||||||
return (
|
return (
|
||||||
<div className={clsx('col col--4')}>
|
<div className={clsx('col col--4', styles.feature)}>
|
||||||
<div className="text--center">
|
<div className="text--center">
|
||||||
<Svg className={styles.featureSvg} role="img" />
|
{/* <Svg className={styles.featureSvg} role="img" /> */}
|
||||||
</div>
|
</div>
|
||||||
<div className="text--center padding-horiz--md">
|
<div className="text--center padding-horiz--md">
|
||||||
<Heading as="h3">{title}</Heading>
|
<Heading as="h3">{title}</Heading>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ function HomepageHeader() {
|
|||||||
<Link
|
<Link
|
||||||
className="button button--secondary button--lg"
|
className="button button--secondary button--lg"
|
||||||
to="/docs/intro">
|
to="/docs/intro">
|
||||||
MycroForge Tutorial
|
Get started now!
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -32,7 +32,7 @@ export default function Home(): JSX.Element {
|
|||||||
const {siteConfig} = useDocusaurusContext();
|
const {siteConfig} = useDocusaurusContext();
|
||||||
return (
|
return (
|
||||||
<Layout
|
<Layout
|
||||||
title={`Hello from ${siteConfig.title}`}
|
title={`Home page`}
|
||||||
description="Description will go into a meta tag in <head />">
|
description="Description will go into a meta tag in <head />">
|
||||||
<HomepageHeader />
|
<HomepageHeader />
|
||||||
<main>
|
<main>
|
||||||
|
|||||||
8380
docs/yarn.lock
Normal file
8380
docs/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,279 +0,0 @@
|
|||||||
# 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!
|
|
||||||
Reference in New Issue
Block a user