Compare commits
45 Commits
5ccb40bb44
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| c0b2ce44ad | |||
| 21f4e7765b | |||
| 33ba944f8a | |||
| 05051878f2 | |||
| 3fff9c7dd0 | |||
| eda7992c23 | |||
| 457429f7ec | |||
| 8f82360cc7 | |||
| 8396da2e9a | |||
| 5c7d07afb0 | |||
| 52d2507891 | |||
| 7d677d27c6 | |||
| 0f85244681 | |||
| d59bf264b0 | |||
| 9b339c738e | |||
| 165d97245d | |||
| a1ffc51a57 | |||
| 6464b9f8f3 | |||
| 30d2ccba76 | |||
| 7d61f76b94 | |||
| 38b83ac8ac | |||
| f8efd45076 | |||
| c1f618ad19 | |||
| 5d49b9ab2d | |||
| bb3d75521e | |||
| bc1f0fb943 | |||
| e95bd759c3 | |||
| 2918f87911 | |||
| 33a86882ac | |||
| d95ee39c0b | |||
| 9109e9a4c7 | |||
| 67a693b916 | |||
| b8373f6908 | |||
| b14a2e7117 | |||
| 207345b54b | |||
| dc8327289f | |||
| a88f8a1f11 | |||
| f676f236b1 | |||
| 4f322e56c7 | |||
| 4b0d1df34f | |||
| 128ae21088 | |||
| 6301bd438a | |||
| 3f33035611 | |||
| 32b7a3c01c | |||
| 91431fd996 |
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
|
||||
31
.gitea/workflows/build_cli.yml
Normal file
31
.gitea/workflows/build_cli.yml
Normal file
@@ -0,0 +1,31 @@
|
||||
name: Build and publish MycroForge.CLI
|
||||
run-name: ${{ gitea.actor }} triggered a build for the MycroForge.CLI package
|
||||
on: [ push ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
if: gitea.ref == 'refs/heads/main'
|
||||
steps:
|
||||
- uses: https://github.com/actions/checkout@v4
|
||||
- uses: https://github.com/actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '8.x'
|
||||
- name: "Build and publish NuGet package"
|
||||
run: |
|
||||
# Build the NuGet package
|
||||
cd MycroForge.CLI
|
||||
dotnet pack -v m
|
||||
|
||||
# Add the NuGet source
|
||||
dotnet nuget add source --name devdisciples \
|
||||
--username ${{ secrets.NUGET_USER }} \
|
||||
--password ${{ secrets.NUGET_PASS }} \
|
||||
--store-password-in-clear-text \
|
||||
https://git.devdisciples.com/api/packages/devdisciples/nuget/index.json
|
||||
|
||||
# Get the path to the package
|
||||
PACKAGE="nupkg/$(ls nupkg)"
|
||||
|
||||
# Push the package
|
||||
dotnet nuget push "$PACKAGE" --source devdisciples
|
||||
33
.gitea/workflows/build_core.yml
Normal file
33
.gitea/workflows/build_core.yml
Normal file
@@ -0,0 +1,33 @@
|
||||
name: Build and publish MycroForge.Core
|
||||
run-name: ${{ gitea.actor }} triggered a build for the MycroForge.Core package
|
||||
on: [ workflow_dispatch ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
if: gitea.ref == 'refs/heads/main'
|
||||
steps:
|
||||
- uses: https://github.com/actions/checkout@v4
|
||||
- uses: https://github.com/actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '8.x'
|
||||
- name: "Build and publish NuGet package"
|
||||
run: |
|
||||
# Build the NuGet package
|
||||
cd MycroForge.Core
|
||||
dotnet publish
|
||||
dotnet pack -v m
|
||||
|
||||
# Add the NuGet source
|
||||
dotnet nuget add source --name devdisciples \
|
||||
--username ${{ secrets.NUGET_USER }} \
|
||||
--password ${{ secrets.NUGET_PASS }} \
|
||||
--store-password-in-clear-text \
|
||||
https://git.devdisciples.com/api/packages/devdisciples/nuget/index.json
|
||||
|
||||
# Set the path to the package
|
||||
VERSION=$(grep '<Version>' < MycroForge.Core.csproj | sed 's/.*<Version>\(.*\)<\/Version>/\1/' | xargs)
|
||||
PACKAGE="bin/Release/MycroForge.Core.$VERSION.nupkg"
|
||||
|
||||
# Push the package
|
||||
dotnet nuget push "$PACKAGE" --source devdisciples
|
||||
33
.gitea/workflows/build_plugin_template.yml
Normal file
33
.gitea/workflows/build_plugin_template.yml
Normal file
@@ -0,0 +1,33 @@
|
||||
name: Build and publish MycroForge.PluginTemplate package
|
||||
run-name: ${{ gitea.actor }} triggered a build for the MycroForge.PluginTemplate package
|
||||
on: [ workflow_dispatch ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
if: gitea.ref == 'refs/heads/main'
|
||||
steps:
|
||||
- uses: https://github.com/actions/checkout@v4
|
||||
- uses: https://github.com/actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '8.x'
|
||||
- name: "Build and publish NuGet package"
|
||||
run: |
|
||||
# Build the NuGet package
|
||||
cd MycroForge.PluginTemplate.Package
|
||||
dotnet publish
|
||||
dotnet pack -v m
|
||||
|
||||
# Add the NuGet source
|
||||
dotnet nuget add source --name devdisciples \
|
||||
--username ${{ secrets.NUGET_USER }} \
|
||||
--password ${{ secrets.NUGET_PASS }} \
|
||||
--store-password-in-clear-text \
|
||||
https://git.devdisciples.com/api/packages/devdisciples/nuget/index.json
|
||||
|
||||
# Set the path to the package
|
||||
VERSION=$(grep '<PackageVersion>' < MycroForge.PluginTemplate.Package.csproj | sed 's/.*<PackageVersion>\(.*\)<\/PackageVersion>/\1/' | xargs)
|
||||
PACKAGE="bin/Release/MycroForge.PluginTemplate.Package.$VERSION.nupkg"
|
||||
|
||||
# Push the package
|
||||
dotnet nuget push "$PACKAGE" --source devdisciples
|
||||
20
.gitea/workflows/test.yml
Normal file
20
.gitea/workflows/test.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
name: Test MycroForge.CLI
|
||||
run-name: ${{ gitea.actor }} triggered a test for the MycroForge.CLI
|
||||
on: [ push ]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
if: gitea.ref == 'refs/heads/develop'
|
||||
steps:
|
||||
- uses: https://github.com/actions/checkout@v4
|
||||
- uses: https://github.com/actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '8.x'
|
||||
- name: "Reference MycroForge.Core in MycroForge.PluginTemplate"
|
||||
# The MycroForge.PluginTemplate project references MycroForge.Core as a package and not as a reference.
|
||||
# This allows the 'm4g plugin init' command to pull in the core package from a package repository.
|
||||
# To prevent the test command from trying to pull from the package repository, we reference the local project.
|
||||
run: dotnet add MycroForge.PluginTemplate reference MycroForge.Core
|
||||
- name: "Run MycroForge.CLI.Tests"
|
||||
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>
|
||||
@@ -97,14 +97,14 @@ public class CrudRouterGenerator
|
||||
var entityRoutePrefix = fqn.PascalizedName.Kebaberize().Pluralize().ToLower();
|
||||
|
||||
var serviceFilePath = Path.Join(
|
||||
Features.Api.FeatureName, "services", fqn.FolderPath, $"{fqn.SnakeCasedName}_service"
|
||||
Features.Api.FeatureName, "services", fqn.Namespace, $"{fqn.SnakeCasedName}_service"
|
||||
);
|
||||
|
||||
var serviceImportPath = serviceFilePath.SlashesToDots();
|
||||
var routerFolderPath = Path.Join(Features.Api.FeatureName, "routers", fqn.FolderPath);
|
||||
var routerFolderPath = Path.Join(Features.Api.FeatureName, "routers", fqn.Namespace);
|
||||
var routerFilePath = Path.Join(routerFolderPath, $"{fqn.SnakeCasedName}");
|
||||
var routerImportPath = routerFolderPath.SlashesToDots();
|
||||
var requestsFolderPath = Path.Join(Features.Api.FeatureName, "requests", fqn.FolderPath);
|
||||
var requestsFolderPath = Path.Join(Features.Api.FeatureName, "requests", fqn.Namespace);
|
||||
|
||||
var createRequestImportPath = Path.Join(requestsFolderPath, $"Create{fqn.PascalizedName}Request")
|
||||
.SlashesToDots()
|
||||
|
||||
@@ -96,14 +96,18 @@ public partial class EntityLinker
|
||||
var left = await LoadEntity(_left);
|
||||
var right = await LoadEntity(_right);
|
||||
|
||||
var associationTable = string.Join('\n', AssociationTable);
|
||||
associationTable = associationTable
|
||||
var associationTable = string.Join('\n', AssociationTable)
|
||||
.Replace("%left_entity%", left.ClassName.Underscore().ToLower())
|
||||
.Replace("%right_entity%", right.ClassName.Underscore().ToLower())
|
||||
.Replace("%left_table%", left.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);
|
||||
|
||||
@@ -136,21 +140,25 @@ public partial class EntityLinker
|
||||
var env = await _context.ReadFile($"{Features.Db.FeatureName}/env.py");
|
||||
env = new DbEnvModifier(env, associationTableImportPath, associationTableImportName).Rewrite();
|
||||
await _context.WriteFile($"{Features.Db.FeatureName}/env.py", env);
|
||||
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
private async Task<EntityModel> LoadEntity(string name)
|
||||
{
|
||||
var fqn = new FullyQualifiedName(name);
|
||||
var path = $"{Features.Db.FeatureName}/entities";
|
||||
var path = Path.Join(Features.Db.FeatureName, "entities");
|
||||
|
||||
if (fqn.HasPath)
|
||||
path = Path.Combine(path, fqn.FolderPath);
|
||||
if (fqn.HasNamespace)
|
||||
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));
|
||||
entity.Initialize();
|
||||
return entity;
|
||||
|
||||
@@ -7,14 +7,16 @@ namespace MycroForge.CLI.CodeGen;
|
||||
|
||||
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 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
|
||||
{
|
||||
@@ -47,12 +49,11 @@ public class RequestClassGenerator
|
||||
var entitySource = await _context.ReadFile(entityFilePath);
|
||||
var fieldInfo = ReadFields(entitySource);
|
||||
var fields = string.Join('\n', fieldInfo.Select(x => ToFieldString(x, type)));
|
||||
|
||||
|
||||
var requestFilePath = Path.Join(
|
||||
Features.Api.FeatureName,
|
||||
"requests",
|
||||
fqn.FolderPath,
|
||||
// requestsFolderPath,
|
||||
fqn.Namespace,
|
||||
$"{type.ToString().ToLower()}_{fqn.SnakeCasedName}_request.py"
|
||||
);
|
||||
|
||||
@@ -60,7 +61,6 @@ public class RequestClassGenerator
|
||||
.Replace("%imports%", GetImportString(entitySource, fieldInfo, type))
|
||||
.Replace("%request_type%", type.ToString().Pascalize())
|
||||
.Replace("%entity_class_name%", fqn.PascalizedName)
|
||||
// .Replace("%entity_class_name%", entityClassName)
|
||||
.Replace("%fields%", fields)
|
||||
;
|
||||
|
||||
@@ -102,10 +102,11 @@ public class RequestClassGenerator
|
||||
.Replace(" ", "");
|
||||
Console.WriteLine(str); // = "List,Dict,str,Any"
|
||||
*/
|
||||
var dissectedTypes = field.Type.Replace("[", ",")
|
||||
var dissectedTypes = field.Type
|
||||
.Replace("[", ",")
|
||||
.Replace("]", "")
|
||||
.Replace(" ", "")
|
||||
.Split();
|
||||
.Split(',');
|
||||
|
||||
foreach (var dissectedType in dissectedTypes)
|
||||
{
|
||||
@@ -166,16 +167,16 @@ public class RequestClassGenerator
|
||||
.Split(',')
|
||||
.Select(s => s.Trim())
|
||||
.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)
|
||||
{
|
||||
typingImport.Types.AddRange(["Any", "Dict", "List", "Optional"]);
|
||||
typingImport.Types.AddRange(PythonTypingImports);
|
||||
}
|
||||
else
|
||||
{
|
||||
imports.Add(new("typing", ["Any", "Dict", "List", "Optional"]));
|
||||
imports.Add(new Import("typing", PythonTypingImports));
|
||||
}
|
||||
|
||||
return imports;
|
||||
|
||||
@@ -7,15 +7,21 @@ public class RequiresPluginAttribute : Attribute
|
||||
{
|
||||
public void RequirePluginProject(string command)
|
||||
{
|
||||
var currentDirectoryInfo = new DirectoryInfo(Environment.CurrentDirectory);
|
||||
var matcher = new Matcher()
|
||||
.AddInclude("*.csproj")
|
||||
.Execute(new DirectoryInfoWrapper(new DirectoryInfo(Environment.CurrentDirectory)));
|
||||
.Execute(new DirectoryInfoWrapper(currentDirectoryInfo));
|
||||
|
||||
if (!matcher.HasMatches)
|
||||
throw new($"Command '{command}' must be run in a command plugin project.");
|
||||
|
||||
var csprojFileName = $"{Path.GetDirectoryName(Environment.CurrentDirectory)}.csproj";
|
||||
bool IsCsprojFile(FilePatternMatch file) => Path.GetFileNameWithoutExtension(file.Path).Equals(csprojFileName);
|
||||
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)
|
||||
|
||||
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}");
|
||||
}
|
||||
}
|
||||
@@ -5,16 +5,16 @@ namespace MycroForge.CLI.Commands;
|
||||
|
||||
public class FullyQualifiedName
|
||||
{
|
||||
public string FolderPath { get; }
|
||||
public string Namespace { get; }
|
||||
public string PascalizedName { get; }
|
||||
public string SnakeCasedName { get; }
|
||||
|
||||
public string FilePath =>
|
||||
string.IsNullOrEmpty(FolderPath.Trim())
|
||||
string.IsNullOrEmpty(Namespace.Trim())
|
||||
? SnakeCasedName
|
||||
: Path.Join(FolderPath, SnakeCasedName);
|
||||
: Path.Join(Namespace, SnakeCasedName);
|
||||
|
||||
public bool HasPath => FolderPath.Length > 0;
|
||||
public bool HasNamespace => Namespace.Length > 0;
|
||||
|
||||
|
||||
public FullyQualifiedName(string name)
|
||||
@@ -27,7 +27,7 @@ public class FullyQualifiedName
|
||||
name = fullName[1];
|
||||
}
|
||||
|
||||
FolderPath = path;
|
||||
Namespace = path;
|
||||
PascalizedName = name.Pascalize();
|
||||
SnakeCasedName = SnakeCase(name);
|
||||
}
|
||||
|
||||
@@ -43,19 +43,17 @@ public partial class MycroForge
|
||||
private async Task ExecuteAsync(string 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.FolderPath);
|
||||
|
||||
var fileName = $"{fqn.SnakeCasedName}.py";
|
||||
var filePath = Path.Combine(folderPath, fileName);
|
||||
var filePath = Path.Join(routersFolderPath, fileName);
|
||||
|
||||
await _context.CreateFile(filePath, Template);
|
||||
|
||||
var moduleImportPath = folderPath
|
||||
|
||||
var moduleImportPath = routersFolderPath
|
||||
.Replace('\\', '.')
|
||||
.Replace('/', '.');
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.CommandLine;
|
||||
using System.Text.RegularExpressions;
|
||||
using Humanizer;
|
||||
using MycroForge.CLI.CodeGen;
|
||||
using MycroForge.Core.Contract;
|
||||
@@ -14,11 +15,65 @@ public partial class MycroForge
|
||||
{
|
||||
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 =
|
||||
[
|
||||
"from sqlalchemy import %type_imports%",
|
||||
"from sqlalchemy import %sqlalchemy_imports%",
|
||||
"from sqlalchemy.orm import Mapped, mapped_column",
|
||||
$"from {Features.Db.FeatureName}.entities.entity_base import EntityBase",
|
||||
"",
|
||||
@@ -55,6 +110,10 @@ public partial class MycroForge
|
||||
"\tfirst_name:str:String(255)",
|
||||
])) { AllowMultipleArgumentsPerToken = true };
|
||||
|
||||
#endregion
|
||||
|
||||
private record ColumnDefinition(string Name, string NativeType, string SqlAlchemyType);
|
||||
|
||||
private readonly ProjectContext _context;
|
||||
|
||||
public Entity(ProjectContext context) : base("entity", "Generate and database entity")
|
||||
@@ -69,25 +128,27 @@ public partial class MycroForge
|
||||
private async Task ExecuteAsync(string name, IEnumerable<string> columns)
|
||||
{
|
||||
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)
|
||||
folderPath = Path.Combine(folderPath, fqn.FolderPath);
|
||||
|
||||
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 sqlAlchemyImport = string.Join(", ", distinctSqlAlchemyColumnTypes);
|
||||
var columnDefinitions = string.Join("\n ", sqlAlchemyColumn.Select(ColumnToString));
|
||||
|
||||
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("%table_name%", fqn.SnakeCasedName.Pluralize());
|
||||
code = code.Replace("%column_definitions%", columnDefinitions);
|
||||
|
||||
|
||||
var fileName = $"{fqn.SnakeCasedName}.py";
|
||||
var filePath = Path.Combine(folderPath, fileName);
|
||||
var filePath = Path.Join(folderPath, fileName);
|
||||
await _context.CreateFile(filePath, code);
|
||||
|
||||
var importPathParts = new[] { folderPath, fileName.Replace(".py", "") }
|
||||
@@ -108,25 +169,58 @@ public partial class MycroForge
|
||||
await _context.WriteFile("main.py", main);
|
||||
}
|
||||
|
||||
private List<ColumnDefinition> GetColumnDefinitions(string[] fields)
|
||||
private List<ColumnDefinition> GetColumnDefinitions(string[] columns)
|
||||
{
|
||||
var definitions = new List<ColumnDefinition>();
|
||||
|
||||
foreach (var field in fields)
|
||||
foreach (var column in columns)
|
||||
{
|
||||
if (field.Split(':') is not { Length: 3 } definition)
|
||||
throw new Exception($"Field definition {field} is invalid.");
|
||||
if (column.Split(':') is not { Length: 3 } definition)
|
||||
throw new Exception($"Column definition {column} is invalid.");
|
||||
|
||||
definitions.Add(new ColumnDefinition(definition[0], definition[1], definition[2]));
|
||||
}
|
||||
|
||||
ValidateSqlAlchemyColumnTypes(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)
|
||||
{
|
||||
_context.AssertDirectoryExists($"{Features.Db.FeatureName}/versions");
|
||||
|
||||
await _context.Bash(
|
||||
"source .venv/bin/activate",
|
||||
$"alembic revision --autogenerate -m \"{name}\" --rev-id $(date -u +\"%Y%m%d%H%M%S\")"
|
||||
|
||||
@@ -58,10 +58,10 @@ public partial class MycroForge
|
||||
var fqn = new FullyQualifiedName(name);
|
||||
var folderPath = string.Empty;
|
||||
|
||||
if (fqn.HasPath)
|
||||
folderPath = Path.Combine(folderPath, fqn.FolderPath);
|
||||
if (fqn.HasNamespace)
|
||||
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 code = string.Join('\n', template)
|
||||
.Replace("%class_name%", fqn.PascalizedName);
|
||||
|
||||
@@ -81,7 +81,7 @@ public partial class MycroForge
|
||||
await _context.CreateFile("main.py");
|
||||
|
||||
// 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
|
||||
_optionsContainer.Set(options.ApiOptions);
|
||||
@@ -106,7 +106,7 @@ public partial class MycroForge
|
||||
|
||||
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))
|
||||
throw new Exception($"Directory {directory} already exists.");
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.CommandLine;
|
||||
using MycroForge.CLI.Commands.Attributes;
|
||||
using MycroForge.Core;
|
||||
using MycroForge.Core.Contract;
|
||||
|
||||
@@ -6,6 +7,7 @@ namespace MycroForge.CLI.Commands;
|
||||
|
||||
public partial class MycroForge
|
||||
{
|
||||
[RequiresVenv]
|
||||
public class Install : Command, ISubCommandOf<MycroForge>
|
||||
{
|
||||
private static readonly Argument<IEnumerable<string>> PackagesArgument =
|
||||
|
||||
@@ -10,21 +10,29 @@ public partial class MycroForge
|
||||
{
|
||||
public class Init : Command, ISubCommandOf<Plugin>
|
||||
{
|
||||
private static readonly Argument<string> NameArgument =
|
||||
new(name: "name", description: "The name of your project");
|
||||
private static readonly Argument<string> NamespaceArgument =
|
||||
new(name: "namespace", description: "The namespace of your project");
|
||||
|
||||
private static readonly Argument<string> ClassArgument =
|
||||
new(name: "class", description: "The class name of the generated command");
|
||||
|
||||
private static readonly Argument<string> CommandArgument =
|
||||
new(name: "command", description: "The command name that will be added to 'm4g'");
|
||||
|
||||
private readonly ProjectContext _context;
|
||||
|
||||
public Init(ProjectContext context) : base("init", "Initialize a basic plugin project")
|
||||
{
|
||||
_context = context;
|
||||
AddArgument(NameArgument);
|
||||
this.SetHandler(ExecuteAsync, NameArgument);
|
||||
AddArgument(NamespaceArgument);
|
||||
AddArgument(ClassArgument);
|
||||
AddArgument(CommandArgument);
|
||||
this.SetHandler(ExecuteAsync, NamespaceArgument, ClassArgument, CommandArgument);
|
||||
}
|
||||
|
||||
private async Task ExecuteAsync(string name)
|
||||
private async Task ExecuteAsync(string @namespace, string @class, string command)
|
||||
{
|
||||
await _context.Bash($"dotnet new m4gp -n {name}");
|
||||
await _context.Bash($"dotnet new m4gp -n {@namespace} --class {@class} --command {command}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,9 +44,13 @@ public partial class MycroForge
|
||||
var assemblyName = GetAssemblyName();
|
||||
var pluginInstallPath = Path.Join(Plugins.RootDirectory, assemblyName);
|
||||
var platform = target.ToString().Dasherize();
|
||||
await _context.Bash($"dotnet publish -c Release -r {platform} --output {pluginInstallPath}");
|
||||
Console.WriteLine($"Successfully installed plugin {assemblyName}");
|
||||
var exitCode = await _context.Bash(
|
||||
$"dotnet publish -c Release -r {platform} --output {pluginInstallPath}"
|
||||
);
|
||||
|
||||
Console.WriteLine(exitCode == 0
|
||||
? $"Successfully installed plugin {assemblyName}"
|
||||
: $"Could not install {assemblyName}, process exited with code {exitCode}.");
|
||||
}
|
||||
|
||||
private string GetAssemblyName()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.CommandLine;
|
||||
using MycroForge.CLI.Commands.Attributes;
|
||||
using MycroForge.Core;
|
||||
using MycroForge.Core.Contract;
|
||||
|
||||
@@ -6,6 +7,7 @@ namespace MycroForge.CLI.Commands;
|
||||
|
||||
public partial class MycroForge
|
||||
{
|
||||
[RequiresVenv]
|
||||
public class Uninstall : Command, ISubCommandOf<MycroForge>
|
||||
{
|
||||
private static readonly Argument<IEnumerable<string>> PackagesArgument =
|
||||
|
||||
@@ -48,6 +48,9 @@ public static class CommandExtensions
|
||||
|
||||
else if (_command.GetRequiresPluginAttribute() is RequiresPluginAttribute requiresPluginAttribute)
|
||||
requiresPluginAttribute.RequirePluginProject(commandText);
|
||||
|
||||
else if (_command.GetRequiresVenvAttribute() is RequiresVenvAttribute requiresVenvAttribute)
|
||||
requiresVenvAttribute.RequireVenv(commandText);
|
||||
}
|
||||
|
||||
await next(context);
|
||||
@@ -56,15 +59,6 @@ public static class CommandExtensions
|
||||
return builder;
|
||||
}
|
||||
|
||||
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 List<Command> GetCommandChain(this InvocationContext context)
|
||||
{
|
||||
var chain = new List<Command>();
|
||||
@@ -87,4 +81,16 @@ public static class CommandExtensions
|
||||
|
||||
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>();
|
||||
}
|
||||
@@ -52,7 +52,7 @@ public sealed partial class Api : IFeature
|
||||
|
||||
await context.Bash(
|
||||
"source .venv/bin/activate",
|
||||
"python3 -m pip install fastapi",
|
||||
"python3 -m pip install fastapi uvicorn",
|
||||
"python3 -m pip freeze > requirements.txt"
|
||||
);
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Version>0.0.1</Version>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
|
||||
@@ -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
|
||||
@@ -2,4 +2,6 @@
|
||||
|
||||
dotnet pack -v d
|
||||
|
||||
dotnet nuget push nupkg/MycroForge.CLI.1.0.0.nupkg --source devdisciples
|
||||
VERSION="1.0.0"
|
||||
|
||||
dotnet nuget push nupkg/MycroForge.CLI.$VERSION.nupkg --source devdisciples
|
||||
|
||||
2
MycroForge.CLI/scripts/publish-dotnet.sh → MycroForge.CLI/scripts/publish-tool.sh
Normal file → Executable file
2
MycroForge.CLI/scripts/publish-dotnet.sh → MycroForge.CLI/scripts/publish-tool.sh
Normal file → Executable file
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/bash
|
||||
#!/bin/bash
|
||||
|
||||
dotnet pack -v d
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace MycroForge.Core.Attributes;
|
||||
|
||||
public class DockerPlatformAttribute : Attribute
|
||||
{
|
||||
public string Platform { get; set; } = string.Empty;
|
||||
}
|
||||
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 async Task<string> SerializeAsync(
|
||||
this object @object,
|
||||
JsonSerializerOptions? jsonSerializerOptions = null
|
||||
)
|
||||
public static async Task<string> SerializeAsync(this object @object, JsonSerializerOptions? options = null)
|
||||
{
|
||||
using var stream = new MemoryStream();
|
||||
using var reader = new StreamReader(stream);
|
||||
var options = jsonSerializerOptions ?? Serialization.DefaultJsonSerializerOptions.Default;
|
||||
|
||||
options ??= DefaultJsonSerializerOptions.Default;
|
||||
|
||||
await JsonSerializer.SerializeAsync(stream, @object, options);
|
||||
stream.Position = 0;
|
||||
return await reader.ReadToEndAsync();
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<Nullable>enable</Nullable>
|
||||
<PackageId>MycroForge.Core</PackageId>
|
||||
<Description>The MycroForge core package</Description>
|
||||
<Version>1.0.0</Version>
|
||||
<Version>0.0.1</Version>
|
||||
<Authors>Donné Napo</Authors>
|
||||
<Company>Dev Disciples</Company>
|
||||
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
|
||||
|
||||
@@ -9,7 +9,7 @@ public class ProjectContext
|
||||
{
|
||||
public string RootDirectory { get; private set; } = Environment.CurrentDirectory;
|
||||
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)
|
||||
@@ -21,8 +21,8 @@ public class ProjectContext
|
||||
}
|
||||
|
||||
var config = await JsonSerializer.DeserializeAsync<ProjectConfig>(
|
||||
File.OpenRead(ConfigPath),
|
||||
Serialization.DefaultJsonSerializerOptions.CamelCasePrettyPrint
|
||||
File.OpenRead(ConfigPath),
|
||||
DefaultJsonSerializerOptions.CamelCasePrettyPrint
|
||||
);
|
||||
|
||||
if (config is null)
|
||||
@@ -37,21 +37,9 @@ public class ProjectContext
|
||||
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)
|
||||
{
|
||||
var fullPath = Path.Combine(RootDirectory, path);
|
||||
var fullPath = Path.Join(RootDirectory, path);
|
||||
var fileInfo = new FileInfo(fullPath);
|
||||
|
||||
if (fileInfo.Exists) return;
|
||||
@@ -64,7 +52,7 @@ public class ProjectContext
|
||||
|
||||
public async Task<string> ReadFile(string path)
|
||||
{
|
||||
var fullPath = Path.Combine(RootDirectory, path);
|
||||
var fullPath = Path.Join(RootDirectory, path);
|
||||
var fileInfo = new FileInfo(fullPath);
|
||||
|
||||
if (!fileInfo.Exists)
|
||||
@@ -75,14 +63,14 @@ public class ProjectContext
|
||||
|
||||
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);
|
||||
Directory.CreateDirectory(fileInfo.Directory!.FullName);
|
||||
await File.WriteAllTextAsync(fullPath, string.Join("\n", content));
|
||||
Console.WriteLine($"Modified file {path}");
|
||||
}
|
||||
|
||||
public async Task Bash(params string[] script)
|
||||
public async Task<int> Bash(params string[] script)
|
||||
{
|
||||
var info = new ProcessStartInfo
|
||||
{
|
||||
@@ -123,13 +111,14 @@ public class ProjectContext
|
||||
input.Close();
|
||||
|
||||
await process.WaitForExitAsync();
|
||||
|
||||
Environment.ExitCode = process.ExitCode;
|
||||
|
||||
return process.ExitCode;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
7
MycroForge.Core/scripts/publish-nuget.sh
Normal file → Executable file
7
MycroForge.Core/scripts/publish-nuget.sh
Normal file → Executable file
@@ -1,4 +1,5 @@
|
||||
#!/bin/bash
|
||||
#!/bin/bash
|
||||
|
||||
dotnet build -r Release
|
||||
dotnet nuget push --source devdisciples bin/Release/MycroForge.Core.1.0.0.nupkg
|
||||
VERSION=$(grep '<Version>' < MycroForge.Core.csproj | sed 's/.*<Version>\(.*\)<\/Version>/\1/' | tr -d '[:space:]')
|
||||
dotnet build -r Releasedo
|
||||
dotnet nuget push --source devdisciples "bin/Release/MycroForge.Core.$VERSION.nupkg"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<!-- The package metadata. Fill in the properties marked as TODO below -->
|
||||
<!-- Follow the instructions on https://learn.microsoft.com/nuget/create-packages/package-authoring-best-practices -->
|
||||
<PackageId>MycroForge.PluginTemplate.Package</PackageId>
|
||||
<PackageVersion>1.0</PackageVersion>
|
||||
<PackageVersion>0.0.1</PackageVersion>
|
||||
<Title>A template for generating MycroForge plugins</Title>
|
||||
<Authors>Donné Napo</Authors>
|
||||
<Description>Template to use when creating a plugin for the MycroForge CLI.</Description>
|
||||
|
||||
2
MycroForge.PluginTemplate.Package/scripts/build_package.sh → MycroForge.PluginTemplate.Package/scripts/build-package.sh
Normal file → Executable file
2
MycroForge.PluginTemplate.Package/scripts/build_package.sh → MycroForge.PluginTemplate.Package/scripts/build-package.sh
Normal file → Executable file
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
#!/bin/env bash
|
||||
|
||||
rm -rf templates/MycroForge.PluginTemplate
|
||||
cp -R ../MycroForge.PluginTemplate templates/MycroForge.PluginTemplate
|
||||
12
MycroForge.PluginTemplate.Package/scripts/publish-nuget.sh
Executable file
12
MycroForge.PluginTemplate.Package/scripts/publish-nuget.sh
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/bin/env bash
|
||||
|
||||
rm -rf templates/MycroForge.PluginTemplate
|
||||
cp -R ../MycroForge.PluginTemplate templates/MycroForge.PluginTemplate
|
||||
dotnet pack
|
||||
|
||||
# Set the path to the package
|
||||
VERSION=$(grep '<PackageVersion>' < MycroForge.PluginTemplate.Package.csproj | sed 's/.*<PackageVersion>\(.*\)<\/PackageVersion>/\1/' | tr -d '[:space:]')
|
||||
PACKAGE="bin/Release/MycroForge.PluginTemplate.Package.$VERSION.nupkg"
|
||||
|
||||
# Push the package
|
||||
dotnet nuget push "$PACKAGE" --source devdisciples
|
||||
@@ -17,5 +17,16 @@
|
||||
"language": "C#",
|
||||
"type": "project"
|
||||
},
|
||||
"preferNameDirectory": true
|
||||
"preferNameDirectory": true,
|
||||
"symbols": {
|
||||
"class": {
|
||||
"type": "parameter",
|
||||
"replaces": "ExampleCommand",
|
||||
"fileRename": "ExampleCommand"
|
||||
},
|
||||
"command": {
|
||||
"type": "parameter",
|
||||
"replaces": "example"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ using RootCommand = MycroForge.Core.RootCommand;
|
||||
|
||||
namespace MycroForge.PluginTemplate;
|
||||
|
||||
public class HelloWorldCommand : Command, ISubCommandOf<RootCommand>
|
||||
public class ExampleCommand : Command, ISubCommandOf<RootCommand>
|
||||
{
|
||||
private readonly Argument<string> NameArgument =
|
||||
new(name: "name", description: "The name of the person to greet");
|
||||
@@ -15,8 +15,8 @@ public class HelloWorldCommand : Command, ISubCommandOf<RootCommand>
|
||||
|
||||
private readonly ProjectContext _context;
|
||||
|
||||
public HelloWorldCommand(ProjectContext context) :
|
||||
base("hello", "An example command generated by dotnet new using the m4gp template")
|
||||
public ExampleCommand(ProjectContext context) :
|
||||
base("example", "A basic command plugin generated by the 'm4g plugin init' command")
|
||||
{
|
||||
_context = context;
|
||||
AddArgument(NameArgument);
|
||||
@@ -28,9 +28,9 @@ public class HelloWorldCommand : Command, ISubCommandOf<RootCommand>
|
||||
{
|
||||
name = allCaps ? name.ToUpper() : name;
|
||||
|
||||
await _context.CreateFile("hello_world.txt",
|
||||
await _context.CreateFile("example.txt",
|
||||
$"Hello {name}!",
|
||||
"This file was generated by your custom command!"
|
||||
"This file was generated by the 'm4g example' command!"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -4,12 +4,12 @@ using RootCommand = MycroForge.Core.RootCommand;
|
||||
|
||||
namespace MycroForge.PluginTemplate;
|
||||
|
||||
public class HelloWorldCommandPlugin : ICommandPlugin
|
||||
public class ExampleCommandPlugin : ICommandPlugin
|
||||
{
|
||||
public string Name => "MycroForge.PluginTemplate";
|
||||
|
||||
public void RegisterServices(IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<ISubCommandOf<RootCommand>, HelloWorldCommand>();
|
||||
services.AddScoped<ISubCommandOf<RootCommand>, ExampleCommand>();
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="MycroForge.Core" Version="1.0.0" />
|
||||
<PackageReference Include="MycroForge.Core" Version="0.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -8,6 +8,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MycroForge.PluginTemplate",
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MycroForge.PluginTemplate.Package", "MycroForge.PluginTemplate.Package\MycroForge.PluginTemplate.Package.csproj", "{1C5C5B9A-3C90-4FE7-A1AC-2F46C3CD0D69}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MycroForge.CLI.Tests", "MycroForge.CLI.Tests\MycroForge.CLI.Tests.csproj", "{71A7EA9D-3C12-4FDE-BA4F-BDD1961DDA1B}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
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}.Release|Any CPU.ActiveCfg = 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
|
||||
EndGlobal
|
||||
|
||||
38
README.md
38
README.md
@@ -1,37 +1,3 @@
|
||||
### Dependencies
|
||||
### Documentation
|
||||
|
||||
- git
|
||||
- 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
|
||||
```
|
||||
|
||||
### TODO
|
||||
|
||||
- Fix `-c` option for `m4g db generate entity`
|
||||
- Add a CLI UI library
|
||||
- Clean up README files
|
||||
- Theme the site with a custom color scheme and icon/logos
|
||||
-
|
||||
Go to https://m4g.devdisciples.com for the docs.
|
||||
@@ -1,3 +0,0 @@
|
||||
.docusaurus/
|
||||
node_modules/
|
||||
.k8s/
|
||||
23
docs/.gitignore
vendored
23
docs/.gitignore
vendored
@@ -1,23 +0,0 @@
|
||||
# Dependencies
|
||||
/node_modules
|
||||
|
||||
# Production
|
||||
/build
|
||||
|
||||
# Generated files
|
||||
.docusaurus
|
||||
.cache-loader
|
||||
|
||||
# Misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
.k8s/remote*.yml
|
||||
!.k8s/remote.example.yml
|
||||
@@ -1,61 +0,0 @@
|
||||
# =========================================
|
||||
# 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
|
||||
@@ -1,67 +0,0 @@
|
||||
# =========================================
|
||||
# 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
|
||||
@@ -1,27 +0,0 @@
|
||||
# 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/
|
||||
@@ -1,47 +0,0 @@
|
||||
# Website
|
||||
|
||||
This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator.
|
||||
|
||||
### Installation
|
||||
|
||||
```
|
||||
$ yarn
|
||||
```
|
||||
|
||||
### Local Development
|
||||
|
||||
```
|
||||
$ yarn start
|
||||
```
|
||||
|
||||
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
|
||||
|
||||
### Build
|
||||
|
||||
```
|
||||
$ yarn build
|
||||
```
|
||||
|
||||
This command generates static content into the `build` directory and can be served using any static contents hosting service.
|
||||
|
||||
### Deployment
|
||||
|
||||
Using SSH:
|
||||
|
||||
```
|
||||
$ USE_SSH=true yarn deploy
|
||||
```
|
||||
|
||||
Not using SSH:
|
||||
|
||||
```
|
||||
$ 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.
|
||||
|
||||
|
||||
### Custom
|
||||
|
||||
kubectl --kubeconfig ~/.kube/main.k8s.config apply -f .k8s/remote.yml
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = {
|
||||
presets: [require.resolve('@docusaurus/core/lib/babel/preset')],
|
||||
};
|
||||
@@ -1,25 +0,0 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.5.002.0
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyTest.Plugin", "MyTest.Plugin\MyTest.Plugin.csproj", "{C93CD889-7228-4DA2-B0E2-5273F2FAAFE6}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{C93CD889-7228-4DA2-B0E2-5273F2FAAFE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{C93CD889-7228-4DA2-B0E2-5273F2FAAFE6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{C93CD889-7228-4DA2-B0E2-5273F2FAAFE6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{C93CD889-7228-4DA2-B0E2-5273F2FAAFE6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {241D0F32-CE9B-40CA-BEA2-A2554CA22824}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -1,28 +0,0 @@
|
||||
---
|
||||
sidebar_position: 5
|
||||
---
|
||||
|
||||
# Commands
|
||||
|
||||
```
|
||||
Description:
|
||||
The MycroForge CLI tool.
|
||||
|
||||
Usage:
|
||||
m4g [command] [options]
|
||||
|
||||
Options:
|
||||
--version Show version information
|
||||
-?, -h, --help Show help and usage information
|
||||
|
||||
Commands:
|
||||
init <name> Initialize a new project
|
||||
i, install <packages> Install packages and update the requirements.txt
|
||||
u, uninstall <packages> Uninstall packages and update the requirements.txt
|
||||
hydrate Initialize venv and install dependencies from requirements.txt
|
||||
add Add features to the project
|
||||
g, generate Generate common items
|
||||
api API related commands
|
||||
db Database related commands
|
||||
p, plugin Plugin related commands
|
||||
```
|
||||
@@ -1,22 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g add
|
||||
|
||||
```
|
||||
Description:
|
||||
Add features to the project
|
||||
|
||||
Usage:
|
||||
m4g add [command] [options]
|
||||
|
||||
Options:
|
||||
-?, -h, --help Show help and usage information
|
||||
|
||||
Commands:
|
||||
api Add FastAPI to the project
|
||||
db Add SQLAlchemy & Alembic to the project
|
||||
git Add git to the project
|
||||
gitignore Add a default .gitignore file to the project
|
||||
```
|
||||
@@ -1,17 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g add api
|
||||
|
||||
```
|
||||
Description:
|
||||
Add FastAPI to the project
|
||||
|
||||
Usage:
|
||||
m4g add api [options]
|
||||
|
||||
Options:
|
||||
--api-port <api-port> The API port
|
||||
-?, -h, --help Show help and usage information
|
||||
```
|
||||
@@ -1,19 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g add db
|
||||
|
||||
```
|
||||
Description:
|
||||
Add SQLAlchemy & Alembic to the project
|
||||
|
||||
Usage:
|
||||
m4g add db [options]
|
||||
|
||||
Options:
|
||||
--database-host-port, --dbh-port <database-host-port> The database host port
|
||||
--database-ui-port, --dbu-port <database-ui-port> The database UI port
|
||||
--database-ui-platform, --dbu-platform <linux_amd64|linux_arm32v5|linux_arm32v6|linux_arm32v7|linux_arm64v8> The docker platform for the PhpMyAdmin image
|
||||
-?, -h, --help Show help and usage information
|
||||
```
|
||||
@@ -1,16 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g add git
|
||||
|
||||
```
|
||||
Description:
|
||||
Add git to the project
|
||||
|
||||
Usage:
|
||||
m4g add git [options]
|
||||
|
||||
Options:
|
||||
-?, -h, --help Show help and usage information
|
||||
```
|
||||
@@ -1,16 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g add gitignore
|
||||
|
||||
```
|
||||
Description:
|
||||
Add a default .gitignore file to the project
|
||||
|
||||
Usage:
|
||||
m4g add gitignore [options]
|
||||
|
||||
Options:
|
||||
-?, -h, --help Show help and usage information
|
||||
```
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g api
|
||||
|
||||
```
|
||||
Description:
|
||||
API related commands
|
||||
|
||||
Usage:
|
||||
m4g api [command] [options]
|
||||
|
||||
Options:
|
||||
-?, -h, --help Show help and usage information
|
||||
|
||||
Commands:
|
||||
run Run your app
|
||||
g, generate Generate an API item
|
||||
```
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g api generate
|
||||
|
||||
```
|
||||
Description:
|
||||
Generate an API item
|
||||
|
||||
Usage:
|
||||
m4g api generate [command] [options]
|
||||
|
||||
Options:
|
||||
-?, -h, --help Show help and usage information
|
||||
|
||||
Commands:
|
||||
r, router <name> Generate an api router
|
||||
crud <entity> Generated CRUD functionality for an entity
|
||||
```
|
||||
@@ -1,19 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g api generate crud
|
||||
|
||||
```
|
||||
Description:
|
||||
Generated CRUD functionality for an entity
|
||||
|
||||
Usage:
|
||||
m4g api generate crud <entity> [options]
|
||||
|
||||
Arguments:
|
||||
<entity> The entity to target
|
||||
|
||||
Options:
|
||||
-?, -h, --help Show help and usage information
|
||||
```
|
||||
@@ -1,19 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g api generate router
|
||||
|
||||
```
|
||||
Description:
|
||||
Generate an api router
|
||||
|
||||
Usage:
|
||||
m4g api generate router <name> [options]
|
||||
|
||||
Arguments:
|
||||
<name> The name of the api router
|
||||
|
||||
Options:
|
||||
-?, -h, --help Show help and usage information
|
||||
```
|
||||
@@ -1,16 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g api run
|
||||
|
||||
```
|
||||
Description:
|
||||
Run your app
|
||||
|
||||
Usage:
|
||||
m4g api run [options]
|
||||
|
||||
Options:
|
||||
-?, -h, --help Show help and usage information
|
||||
```
|
||||
@@ -1,24 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g db
|
||||
|
||||
```
|
||||
Description:
|
||||
Database related commands
|
||||
|
||||
Usage:
|
||||
m4g db [command] [options]
|
||||
|
||||
Options:
|
||||
-?, -h, --help Show help and usage information
|
||||
|
||||
Commands:
|
||||
run Runs the services defined in db.docker-compose.yml
|
||||
stop Stops db.docker-compose.yml
|
||||
migrate Apply migrations to the database
|
||||
rollback Rollback the last migration
|
||||
g, generate Generate a database item
|
||||
link Define relationships between entities
|
||||
```
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g db generate
|
||||
|
||||
```
|
||||
Description:
|
||||
Generate a database item
|
||||
|
||||
Usage:
|
||||
m4g db generate [command] [options]
|
||||
|
||||
Options:
|
||||
-?, -h, --help Show help and usage information
|
||||
|
||||
Commands:
|
||||
e, entity <name> Generate and database entity
|
||||
m, migration <name> Generate a migration
|
||||
```
|
||||
@@ -1,35 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g db generate entity
|
||||
|
||||
```
|
||||
Description:
|
||||
Generate and database entity
|
||||
|
||||
Usage:
|
||||
m4g db generate entity <name> [options]
|
||||
|
||||
Arguments:
|
||||
<name> The name of the database entity
|
||||
|
||||
Supported formats:
|
||||
Entity
|
||||
path/relative/to/entities:Entity
|
||||
|
||||
Options:
|
||||
-c, --column <column> Specify the fields to add.
|
||||
|
||||
Format:
|
||||
<name>:<native_type>:<orm_type>
|
||||
|
||||
<name> = Name of the column
|
||||
<native_type> = The native Python type
|
||||
<orm_type> = The SQLAlchemy type
|
||||
|
||||
Example:
|
||||
first_name:str:String(255)
|
||||
-?, -h, --help Show help and usage information
|
||||
|
||||
```
|
||||
@@ -1,19 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g db generate migration
|
||||
|
||||
```
|
||||
Description:
|
||||
Generate a migration
|
||||
|
||||
Usage:
|
||||
m4g db generate migration <name> [options]
|
||||
|
||||
Arguments:
|
||||
<name> The name of the migration
|
||||
|
||||
Options:
|
||||
-?, -h, --help Show help and usage information
|
||||
```
|
||||
@@ -1,21 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g db link
|
||||
|
||||
```
|
||||
Description:
|
||||
Define relationships between entities
|
||||
|
||||
Usage:
|
||||
m4g db link [command] [options]
|
||||
|
||||
Options:
|
||||
-?, -h, --help Show help and usage information
|
||||
|
||||
Commands:
|
||||
one <entity> Define a 1:n relation
|
||||
many <entity> Define a n:m relation
|
||||
|
||||
```
|
||||
@@ -1,21 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g db link many
|
||||
|
||||
```
|
||||
Description:
|
||||
Define a n:m relation
|
||||
|
||||
Usage:
|
||||
m4g db link many <entity> [options]
|
||||
|
||||
Arguments:
|
||||
<entity> The left side of the relation
|
||||
|
||||
Options:
|
||||
--to-one <to-one> The right side of the relation
|
||||
--to-many <to-many> The right side of the relation
|
||||
-?, -h, --help Show help and usage information
|
||||
```
|
||||
@@ -1,21 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g db link one
|
||||
|
||||
```
|
||||
Description:
|
||||
Define a 1:n relation
|
||||
|
||||
Usage:
|
||||
m4g db link one <entity> [options]
|
||||
|
||||
Arguments:
|
||||
<entity> The left side of the relation
|
||||
|
||||
Options:
|
||||
--to-one <to-one> The right side of the relation
|
||||
--to-many <to-many> The right side of the relation
|
||||
-?, -h, --help Show help and usage information
|
||||
```
|
||||
@@ -1,16 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g db migrate
|
||||
|
||||
```
|
||||
Description:
|
||||
Apply migrations to the database
|
||||
|
||||
Usage:
|
||||
m4g db migrate [options]
|
||||
|
||||
Options:
|
||||
-?, -h, --help Show help and usage information
|
||||
```
|
||||
@@ -1,16 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g db rollback
|
||||
|
||||
```
|
||||
Description:
|
||||
Rollback the last migration
|
||||
|
||||
Usage:
|
||||
m4g db rollback [options]
|
||||
|
||||
Options:
|
||||
-?, -h, --help Show help and usage information
|
||||
```
|
||||
@@ -1,16 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g db run
|
||||
|
||||
```
|
||||
Description:
|
||||
Runs the services defined in db.docker-compose.yml
|
||||
|
||||
Usage:
|
||||
m4g db run [options]
|
||||
|
||||
Options:
|
||||
-?, -h, --help Show help and usage information
|
||||
```
|
||||
@@ -1,16 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g db stop
|
||||
|
||||
```
|
||||
Description:
|
||||
Stops db.docker-compose.yml
|
||||
|
||||
Usage:
|
||||
m4g db stop [options]
|
||||
|
||||
Options:
|
||||
-?, -h, --help Show help and usage information
|
||||
```
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g generate
|
||||
|
||||
```
|
||||
Description:
|
||||
Generate common items
|
||||
|
||||
Usage:
|
||||
m4g generate [command] [options]
|
||||
|
||||
Options:
|
||||
-?, -h, --help Show help and usage information
|
||||
|
||||
Commands:
|
||||
s, service <name> Generate a service
|
||||
venv Generate a venv
|
||||
```
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g generate service
|
||||
|
||||
```
|
||||
Description:
|
||||
Generate a service
|
||||
|
||||
Usage:
|
||||
m4g generate service <name> [options]
|
||||
|
||||
Arguments:
|
||||
<name> The name of the service
|
||||
|
||||
Options:
|
||||
--with-session Create a service that uses database sessions
|
||||
-?, -h, --help Show help and usage information
|
||||
```
|
||||
@@ -1,16 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g generate venv
|
||||
|
||||
```
|
||||
Description:
|
||||
Generate a venv
|
||||
|
||||
Usage:
|
||||
m4g generate venv [options]
|
||||
|
||||
Options:
|
||||
-?, -h, --help Show help and usage information
|
||||
```
|
||||
@@ -1,16 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g hydrate
|
||||
|
||||
```
|
||||
Description:
|
||||
Initialize venv and install dependencies from requirements.txt
|
||||
|
||||
Usage:
|
||||
m4g hydrate [options]
|
||||
|
||||
Options:
|
||||
-?, -h, --help Show help and usage information
|
||||
```
|
||||
@@ -1,24 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g init
|
||||
|
||||
```
|
||||
Description:
|
||||
Initialize a new project
|
||||
|
||||
Usage:
|
||||
m4g init <name> [options]
|
||||
|
||||
Arguments:
|
||||
<name> The name of your project
|
||||
|
||||
Options:
|
||||
--without <api|db|git|gitignore> Features to exclude
|
||||
--api-port <api-port> The API port
|
||||
--database-host-port, --dbh-port <database-host-port> The database host port
|
||||
--database-ui-port, --dbu-port <database-ui-port> The database UI port
|
||||
--database-ui-platform, --dbu-platform <amd64|arm32v5|arm32v6|arm32v7|arm64v8> The docker platform for the PhpMyAdmin image
|
||||
-?, -h, --help Show help and usage information
|
||||
```
|
||||
@@ -1,19 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g install
|
||||
|
||||
```
|
||||
Description:
|
||||
Install packages and update the requirements.txt
|
||||
|
||||
Usage:
|
||||
m4g install [<packages>...] [options]
|
||||
|
||||
Arguments:
|
||||
<packages> The names of the packages to install
|
||||
|
||||
Options:
|
||||
-?, -h, --help Show help and usage information
|
||||
```
|
||||
@@ -1,22 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g plugin
|
||||
|
||||
```
|
||||
Description:
|
||||
Plugin related commands
|
||||
|
||||
Usage:
|
||||
m4g plugin [command] [options]
|
||||
|
||||
Options:
|
||||
-?, -h, --help Show help and usage information
|
||||
|
||||
Commands:
|
||||
init <name> Initialize a basic plugin project
|
||||
l, list, ls List all installed plugins
|
||||
i, install Install a plugin
|
||||
u, uninstall <name> Uninstall a plugin
|
||||
```
|
||||
@@ -1,19 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g plugin init
|
||||
|
||||
```
|
||||
Description:
|
||||
Initialize a basic plugin project
|
||||
|
||||
Usage:
|
||||
m4g plugin init <name> [options]
|
||||
|
||||
Arguments:
|
||||
<name> The name of your project
|
||||
|
||||
Options:
|
||||
-?, -h, --help Show help and usage information
|
||||
```
|
||||
@@ -1,17 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g plugin install
|
||||
|
||||
```
|
||||
Description:
|
||||
Install a plugin
|
||||
|
||||
Usage:
|
||||
m4g plugin install [options]
|
||||
|
||||
Options:
|
||||
-p, --platform <linux_arm|linux_arm64|linux_x64|osx_arm64|osx_x64> (REQUIRED) The platform to target when building the plugin
|
||||
-?, -h, --help Show help and usage information
|
||||
```
|
||||
@@ -1,16 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g plugin list
|
||||
|
||||
```
|
||||
Description:
|
||||
List all installed plugins
|
||||
|
||||
Usage:
|
||||
m4g plugin list [options]
|
||||
|
||||
Options:
|
||||
-?, -h, --help Show help and usage information
|
||||
```
|
||||
@@ -1,19 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g plugin uninstall
|
||||
|
||||
```
|
||||
Description:
|
||||
Uninstall a plugin
|
||||
|
||||
Usage:
|
||||
m4g plugin uninstall [<name>...] [options]
|
||||
|
||||
Arguments:
|
||||
<name> The names of the plugins you want to uninstall
|
||||
|
||||
Options:
|
||||
-?, -h, --help Show help and usage information
|
||||
```
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
# m4g uninstall
|
||||
|
||||
```
|
||||
Description:
|
||||
Uninstall packages and update the requirements.txt
|
||||
|
||||
Usage:
|
||||
m4g uninstall [<packages>...] [options]
|
||||
|
||||
Arguments:
|
||||
<packages> The names of the packages to uninstall
|
||||
|
||||
Options:
|
||||
-y, --yes Don’t ask for confirmation of uninstall deletions
|
||||
-?, -h, --help Show help and usage information
|
||||
```
|
||||
@@ -1,174 +0,0 @@
|
||||
---
|
||||
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 the 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");
|
||||
var exists = File.Exists(path);
|
||||
|
||||
if (exists && !overwrite)
|
||||
{
|
||||
Console.WriteLine($"File {path} already exists, add the -o or --overwrite flag to overwrite it.");
|
||||
return;
|
||||
}
|
||||
|
||||
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.
|
||||
@@ -1,68 +0,0 @@
|
||||
---
|
||||
sidebar_position: 2
|
||||
---
|
||||
|
||||
# Getting Started
|
||||
|
||||
## Requirements
|
||||
|
||||
To use MycroForge, ensure you have the following dependencies installed:
|
||||
|
||||
- **bash**
|
||||
- **git**
|
||||
- **Python 3.10**
|
||||
- **Docker**
|
||||
- **.NET 8**
|
||||
- **XCode Command Line Tools (MacOS only)**
|
||||
|
||||
### 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
|
||||
|
||||
If you see a blank screen when opening the FastAPI Swagger documentation, then make sure you've activated the Safari developer tools.
|
||||
@@ -1,16 +0,0 @@
|
||||
---
|
||||
sidebar_position: 1
|
||||
---
|
||||
|
||||
|
||||
# Intro
|
||||
|
||||
Welcome to **MycroForge** – an opinionated CLI tool designed to streamline the development of FastAPI and SQLAlchemy-based backends. With MycroForge, you can effortlessly create backend projects through a convenient command line interface.
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Project Skeleton Generation:** Quickly generate a well-structured project skeleton tailored for FastAPI and SQLAlchemy, ensuring you start with best practices.
|
||||
- **Database Entities:** Easily create and manage database entities, simplifying your database interactions.
|
||||
- **Migrations:** Handle database migrations seamlessly, allowing for smooth transitions and updates.
|
||||
- **Routers:** Generate and manage routers to keep your application modular and organized.
|
||||
- **CRUD Functionality:** Automatically generate basic CRUD (Create, Read, Update, Delete) operations to accelerate your development process.
|
||||
@@ -1,111 +0,0 @@
|
||||
---
|
||||
sidebar_position: 4
|
||||
---
|
||||
|
||||
# Project layout
|
||||
|
||||
When you generate a new project with `m4g init <project_name>`, it will create a folder like the example below.
|
||||
|
||||
```
|
||||
📦<project_name>
|
||||
┣ 📂.git
|
||||
┣ 📂.venv
|
||||
┣ 📂api
|
||||
┃ ┗ 📂routers
|
||||
┃ ┃ ┗ 📜hello.py
|
||||
┣ 📂db
|
||||
┃ ┣ 📂engine
|
||||
┃ ┃ ┗ 📜async_session.py
|
||||
┃ ┣ 📂entities
|
||||
┃ ┃ ┗ 📜entity_base.py
|
||||
┃ ┣ 📂versions
|
||||
┃ ┣ 📜README
|
||||
┃ ┣ 📜env.py
|
||||
┃ ┣ 📜script.py.mako
|
||||
┃ ┗ 📜settings.py
|
||||
┣ 📜.gitignore
|
||||
┣ 📜alembic.ini
|
||||
┣ 📜db.docker-compose.yml
|
||||
┣ 📜m4g.json
|
||||
┣ 📜main.py
|
||||
┗ 📜requirements.txt
|
||||
```
|
||||
|
||||
Let's go through these one by one.
|
||||
|
||||
### .git
|
||||
|
||||
The `m4g init` command will initialize new projects with git by default.
|
||||
If you don't want to use git you can pass the option `--without git` to `m4g init`.
|
||||
|
||||
### .venv
|
||||
|
||||
To promote isolation of Python dependencies, new projects are initialized with a virtual environment by default.
|
||||
When you clone a MycroForge repository from git, it won't have a `.venv` folder yet.
|
||||
You can run `m4g hydrate` in the root folder of the project to restore the dependencies.
|
||||
|
||||
### api/routers/hello.py
|
||||
|
||||
This file defines a basic example router, which is imported and mapped in `main.py`. This router is just an example and
|
||||
can be removed or modified at you discretion.
|
||||
|
||||
### db/engine/async_session.py
|
||||
|
||||
This file defines the `async_session` function, which can be used to open an asynchronous session to a database.
|
||||
|
||||
### db/entities/entity_base.py
|
||||
|
||||
This file contains an automatically generated entity base class that derives from the DeclarativeBase.
|
||||
All entities must inherit from this class, so that SQLAlchemy & alembic can track them. The entities directory is also
|
||||
where all newly generated entities will be stored.
|
||||
|
||||
### db/versions
|
||||
|
||||
This is where the generated database migrations will be stored.
|
||||
|
||||
### db/README
|
||||
|
||||
This README file is automatically generated by the alembic init command.
|
||||
|
||||
### db/env.py
|
||||
|
||||
This is the database environment file that is used by alembic to interact with the database.
|
||||
If you take a closer look at the imports, you'll see that the file has been modified to assign `EntityBase.metadata` to
|
||||
a variable called `target_metadata`, this will allow alembic to track changes in your entities. You'll also find that
|
||||
the `DbSettings` class is used to get the connectionstring. Any time you generate a new database entity, or create a
|
||||
many-to-many relation between two entities, this file will also be modified to include the generated classes.
|
||||
|
||||
### db/script.py.mako
|
||||
|
||||
This file is automatically generated by the alembic init command.
|
||||
|
||||
### db/settings.py
|
||||
|
||||
This file defines the `DbSettings` class, that is responsible for retrieving the database connectionstring.
|
||||
You will probably want to modify this class to retrieve the connectionstring from a secret manager at some point.
|
||||
|
||||
### .gitignore
|
||||
|
||||
The default .gitignore file that is generated by the `m4g init` command. Modify this file at your discretion.
|
||||
|
||||
### alembic.ini
|
||||
|
||||
This file is automatically generated by the alembic init command.
|
||||
|
||||
### db.docker-compose.yml
|
||||
|
||||
A docker compose file for running a database locally.
|
||||
|
||||
### m4g.json
|
||||
|
||||
This file contains some configs that are used by the CLI, for example the ports to map to the API and database.
|
||||
|
||||
### main.py
|
||||
|
||||
The entrypoint for the application. When generating entities, many-to-many relations or routers, this file will be
|
||||
modified to include the generated files.
|
||||
|
||||
### requirements.txt
|
||||
|
||||
The requirements file containing the Python dependencies.
|
||||
Whenever you run `m4g install` or `m4g uninstall` this file will be updated too.
|
||||
@@ -1,327 +0,0 @@
|
||||
---
|
||||
sidebar_position: 3
|
||||
---
|
||||
|
||||
# Tutorial
|
||||
|
||||
In this tutorial, we'll build a simple todo app to demonstrate the capabilities of the MycroForge CLI.
|
||||
By the end, you should have a solid foundation to start exploring and using MycroForge for your projects.
|
||||
|
||||
## General notes
|
||||
|
||||
The commands in this tutorial assume that you are running them from the root directory of your MycroForge project.
|
||||
|
||||
## Initialize the Project
|
||||
|
||||
Open a terminal and navigate (`cd`) to the directory where your project should be created.
|
||||
Run the following command to initialize a new project and open it in VSCode:
|
||||
|
||||
## Setup the database
|
||||
|
||||
Our todo app needs to keep track of todos, so it needs a storage mechanism of some sorts. A database should be one
|
||||
of the first things, if not THE first thing, that comes to mind. Luckily, MycroForge provides you with a locally hosted
|
||||
database for you project out of the box. This setup, is powered by docker compose and can be examined by opening the
|
||||
`db.docker-compose.yml` file in the project. Follow along to learn how to use the docker based database when developing
|
||||
locally.
|
||||
|
||||
### Run the database
|
||||
|
||||
The first step is to start the database, you can do this by running the following command in a terminal.
|
||||
|
||||
```bash
|
||||
m4g db run
|
||||
```
|
||||
|
||||
This command starts the services defined in the `db.docker-compose.yml` file.
|
||||
You can verify that the services are up by running `docker container ls`. If everything went well, then the previous
|
||||
command should output the service names defined in `db.docker-compose.yml`.
|
||||
|
||||
Go to [PhpMyAdmin (i.e. http://localhost:5051)](http://localhost:5051). You should now be able to login with the
|
||||
following credentials.
|
||||
- user: root
|
||||
- pass: password
|
||||
|
||||
When you're done developing, you can shut down the local database by running `m4g db stop`
|
||||
|
||||
:::info
|
||||
|
||||
If you're running on MacOS, Docker might complain about a platform mismatch for PhpMyAdmin.
|
||||
In that case you might need to specify the platform for the PhpMyAdmin image.
|
||||
You can do this by passing the `--dbu-platform` flag to `m4g init`.
|
||||
Run `m4g init -?` for all the available options.
|
||||
If you've already initialized a project, you can also change the platform prefix of the PhpMyAdmin image in the `db.docker-compose.yml`.
|
||||
|
||||
:::
|
||||
|
||||
### Create the entities
|
||||
|
||||
Now that the database is running, we can start to create our entities. Run the commands below to create the `Todo` &
|
||||
`Tag` entities.
|
||||
|
||||
```bash
|
||||
m4g db generate entity Tag --column "description:str:String(255)"
|
||||
```
|
||||
|
||||
```bash
|
||||
m4g db generate entity Todo --column "description:str:String(255)" -c "is_done:bool:Boolean()"
|
||||
```
|
||||
|
||||
After running these commands, you should find the generated entities in the `db/entities` folder of your project.
|
||||
You should also see that the `main.py` & `db/env.py` files have been modified to include the newly generated entity.
|
||||
|
||||
For more information about the `m4g db generate entity` command, you can run `m4g db generate entity -?`.
|
||||
|
||||
### Define a many-to-many relation between Todo & Tag
|
||||
|
||||
To allow for relations between `Todo` & `Tag`, we'll define a many-to-many relation between the two entities.
|
||||
This relation makes sense, because a `Todo` can have many `Tags` and a `Tag` could belong to many `Todos`.
|
||||
You can generate this relation by running the following command from you terminal.
|
||||
Creating a one-to-many relation would also make sense, but for the purpose of demonstration we're going to demonstrate
|
||||
the many-to-many relation, because this one is the most complex, since it requires an additional mapping to be included
|
||||
in the database schema.
|
||||
|
||||
```bash
|
||||
m4g db link many Todo --to-many Tag
|
||||
```
|
||||
|
||||
After running this command you should see that both the `Todo` and `Tag` entities now have a new field referencing the
|
||||
a `List` containing instances of the other entity.
|
||||
|
||||
For more information about the `m4g db link` command try running `m4g db link -?`. Note that you can do the same thing
|
||||
for all sub commands, so if you want to know more about `m4g db link many` you can simply run `m4g db link many -?` to
|
||||
examine the command. The same is true for all the other commands as well.
|
||||
|
||||
### Generate the migration
|
||||
|
||||
Now that we've generated our entities, it's time to generate a migration that will apply these changes in the database.
|
||||
Generate the initial migration by running the following command.
|
||||
|
||||
```bash
|
||||
m4g db generate migration initial_migration
|
||||
```
|
||||
|
||||
After running this command, you should see the new migration in the `db/version` directory.
|
||||
|
||||
### Apply the migration
|
||||
|
||||
The last step for the database setup is to actually apply the new migration to the database. This can be done by running
|
||||
the following command.
|
||||
|
||||
```bash
|
||||
m4g db migrate
|
||||
```
|
||||
|
||||
After running this command, you should now see a populated schema when visiting [PhpMyAdmin](http://localhost:5051).
|
||||
If for whatever reason you want to undo the last migration, you can simply run `m4g db rollback`.
|
||||
|
||||
## Setup the API
|
||||
|
||||
### Generate CRUD for Tag & Todo
|
||||
|
||||
Our API should provide use with basic endpoint to manage the `Todo` & `Tag` entities, i.e. CRUD functionality.
|
||||
Writing this code can be boring, since it's pretty much boilerplate with some custom additions sprinkled here and there.
|
||||
Fortunately, MycroForge can generate a good chunk of this boring code on your behalf. Run the following commands to
|
||||
generate CRUD functionality for the `Todo` & `Tag` classes.
|
||||
|
||||
```bash
|
||||
m4g api generate crud Tag
|
||||
```
|
||||
|
||||
```bash
|
||||
m4g api generate crud Todo
|
||||
```
|
||||
|
||||
After running this command you should see that the `api/requests`,`api/routers` & `api/services` now contain the
|
||||
relevant classes need to support the generated CRUD functionality. This could should be relatively straightforward, so
|
||||
we won't dive into it, but feel free to take a break and explore what the generated code actually does. Another thing to
|
||||
note, is that the generated routers are also automatically included in `main.py`.
|
||||
|
||||
### Modify the generated Todo request classes
|
||||
|
||||
Since we have a many-to-many relationship between `Todo` & `Tag`, the generated CRUD functionality isn't quite ready
|
||||
yet. We need to be able to specify which `Tags` to add to a `Todo` when creating or updating it.
|
||||
To do this, we will allow for a `tag_ids` field in both the `CreateTodoRequest` & the `UpdateTodoRequest`.
|
||||
This field will contain the ids of the `Tags` that are associated with a `Todo`.
|
||||
|
||||
Modify `CreateTodoRequest` in `api/requests/create_todo_request.py`.
|
||||
|
||||
```python
|
||||
# Before
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class CreateTodoRequest(BaseModel):
|
||||
description: str = None
|
||||
is_done: bool = None
|
||||
|
||||
# After
|
||||
from typing import List, Optional
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class CreateTodoRequest(BaseModel):
|
||||
description: str = None
|
||||
is_done: bool = None
|
||||
tag_ids: Optional[List[int]] = []
|
||||
```
|
||||
|
||||
Modify `UpdateTodoRequest` in `api/requests/update_todo_request.py`, you might need to import `List` from `typing`.
|
||||
|
||||
```python
|
||||
# Before
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class UpdateTodoRequest(BaseModel):
|
||||
description: Optional[str] = None
|
||||
is_done: Optional[bool] = None
|
||||
|
||||
# After
|
||||
from pydantic import BaseModel
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
class UpdateTodoRequest(BaseModel):
|
||||
description: Optional[str] = None
|
||||
is_done: Optional[bool] = None
|
||||
tag_ids: Optional[List[int]] = []
|
||||
```
|
||||
|
||||
### Modify generated TodoService
|
||||
|
||||
The `TodoService` will also need to be updated to accomodate the management of `tag_ids`.
|
||||
|
||||
Add the following imports in `api/services/todo_service.py`.
|
||||
|
||||
```python
|
||||
from sqlalchemy.orm import selectinload
|
||||
from db.entities.tag import Tag
|
||||
```
|
||||
|
||||
Modify `TodoService.list`
|
||||
|
||||
```python
|
||||
# Before
|
||||
async def list(self) -> List[Todo]:
|
||||
async with async_session() as session:
|
||||
stmt = select(Todo)
|
||||
results = (await session.scalars(stmt)).all()
|
||||
return results
|
||||
|
||||
# After
|
||||
async def list(self) -> List[Todo]:
|
||||
async with async_session() as session:
|
||||
stmt = select(Todo).options(selectinload(Todo.tags))
|
||||
results = (await session.scalars(stmt)).all()
|
||||
return results
|
||||
```
|
||||
|
||||
Modify `TodoService.get_by_id`
|
||||
|
||||
```python
|
||||
# Before
|
||||
async def get_by_id(self, id: int) -> Optional[Todo]:
|
||||
async with async_session() as session:
|
||||
stmt = select(Todo).where(Todo.id == id)
|
||||
result = (await session.scalars(stmt)).first()
|
||||
return result
|
||||
|
||||
# After
|
||||
async def get_by_id(self, id: int) -> Optional[Todo]:
|
||||
async with async_session() as session:
|
||||
stmt = select(Todo).where(Todo.id == id).options(selectinload(Todo.tags))
|
||||
result = (await session.scalars(stmt)).first()
|
||||
return result
|
||||
```
|
||||
|
||||
Modify `TodoService.create`
|
||||
|
||||
```python
|
||||
# Before
|
||||
async def create(self, data: Dict[str, Any]) -> None:
|
||||
async with async_session() as session:
|
||||
entity = Todo(**data)
|
||||
session.add(entity)
|
||||
await session.commit()
|
||||
|
||||
# After
|
||||
async def create(self, data: Dict[str, Any]) -> None:
|
||||
tag_ids = []
|
||||
|
||||
if "tag_ids" in data.keys():
|
||||
tag_ids = data["tag_ids"]
|
||||
del data["tag_ids"]
|
||||
|
||||
async with async_session() as session:
|
||||
entity = Todo(**data)
|
||||
|
||||
if len(tag_ids) > 0:
|
||||
stmt = select(Tag).where(Tag.id.in_(tag_ids))
|
||||
result = (await session.scalars(stmt)).all()
|
||||
entity.tags = list(result)
|
||||
|
||||
session.add(entity)
|
||||
await session.commit()
|
||||
```
|
||||
|
||||
Modify `TodoService.update`
|
||||
|
||||
```python
|
||||
# Before
|
||||
async def update(self, id: int, data: Dict[str, Any]) -> bool:
|
||||
tag_ids = []
|
||||
|
||||
if "tag_ids" in data.keys():
|
||||
tag_ids = data["tag_ids"]
|
||||
del data["tag_ids"]
|
||||
|
||||
async with async_session() as session:
|
||||
stmt = select(Todo).where(Todo.id == id)
|
||||
entity = (await session.scalars(stmt)).first()
|
||||
|
||||
if entity is None:
|
||||
return False
|
||||
else:
|
||||
for key, value in data.items():
|
||||
setattr(entity, key, value)
|
||||
await session.commit()
|
||||
return True
|
||||
|
||||
# After
|
||||
async def update(self, id: int, data: Dict[str, Any]) -> bool:
|
||||
tag_ids = []
|
||||
|
||||
if "tag_ids" in data.keys():
|
||||
tag_ids = data["tag_ids"]
|
||||
del data["tag_ids"]
|
||||
|
||||
async with async_session() as session:
|
||||
stmt = select(Todo).where(Todo.id == id).options(selectinload(Todo.tags))
|
||||
entity = (await session.scalars(stmt)).first()
|
||||
|
||||
if entity is None:
|
||||
return False
|
||||
else:
|
||||
for key, value in data.items():
|
||||
setattr(entity, key, value)
|
||||
|
||||
if len(tag_ids) > 0:
|
||||
stmt = select(Tag).where(Tag.id.in_(tag_ids))
|
||||
result = (await session.scalars(stmt)).all()
|
||||
entity.tags = list(result)
|
||||
else:
|
||||
entity.tags = []
|
||||
|
||||
await session.commit()
|
||||
return True
|
||||
```
|
||||
|
||||
## Test the API!
|
||||
|
||||
Run the following command.
|
||||
```bash
|
||||
m4g api run
|
||||
```
|
||||
|
||||
Go to http://localhost:5000/docs and test your Todo API!
|
||||
@@ -1,87 +0,0 @@
|
||||
import {themes as prismThemes} from 'prism-react-renderer';
|
||||
import type {Config} from '@docusaurus/types';
|
||||
import type * as Preset from '@docusaurus/preset-classic';
|
||||
|
||||
const config: Config = {
|
||||
title: 'MycroForge',
|
||||
tagline: 'Your FastAPI & SQLAlchemy assistant!',
|
||||
favicon: 'img/favicon.ico',
|
||||
|
||||
// Set the production url of your site here
|
||||
url: 'https://git.devdisciples.com',
|
||||
// Set the /<baseUrl>/ pathname under which your site is served
|
||||
// For GitHub pages deployment, it is often '/<projectName>/'
|
||||
baseUrl: '/',
|
||||
|
||||
// GitHub pages deployment config.
|
||||
// If you aren't using GitHub pages, you don't need these.
|
||||
organizationName: 'devdisciples', // Usually your GitHub org/user name.
|
||||
projectName: 'mycroforge', // Usually your repo name.
|
||||
|
||||
onBrokenLinks: 'throw',
|
||||
onBrokenMarkdownLinks: 'warn',
|
||||
|
||||
// Even if you don't use internationalization, you can use this field to set
|
||||
// useful metadata like html lang. For example, if your site is Chinese, you
|
||||
// may want to replace "en" with "zh-Hans".
|
||||
i18n: {
|
||||
defaultLocale: 'en',
|
||||
locales: ['en'],
|
||||
},
|
||||
|
||||
presets: [
|
||||
[
|
||||
'classic',
|
||||
{
|
||||
docs: {
|
||||
sidebarPath: './sidebars.ts',
|
||||
// Please change this to your repo.
|
||||
// Remove this to remove the "edit this page" links.
|
||||
editUrl:
|
||||
'https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/',
|
||||
},
|
||||
theme: {
|
||||
customCss: './src/css/custom.css',
|
||||
},
|
||||
} satisfies Preset.Options,
|
||||
],
|
||||
],
|
||||
|
||||
themeConfig: {
|
||||
// Replace with your project's social card
|
||||
image: 'img/docusaurus-social-card.jpg',
|
||||
navbar: {
|
||||
title: 'MycroForge',
|
||||
logo: {
|
||||
alt: 'MycroForge Logo',
|
||||
src: 'img/logo.svg',
|
||||
},
|
||||
items: [
|
||||
{
|
||||
type: 'docSidebar',
|
||||
sidebarId: 'tutorialSidebar',
|
||||
position: 'left',
|
||||
label: 'Docs',
|
||||
},
|
||||
{
|
||||
href: 'https://git.devdisciples.com/devdisciples/mycroforge',
|
||||
label: 'Git',
|
||||
position: 'right',
|
||||
},
|
||||
],
|
||||
},
|
||||
footer: {
|
||||
style: 'dark',
|
||||
links: [
|
||||
],
|
||||
copyright: `Copyright © ${new Date().getFullYear()} DevDisciples`,
|
||||
},
|
||||
prism: {
|
||||
theme: prismThemes.oneLight,
|
||||
darkTheme: prismThemes.oneDark,
|
||||
additionalLanguages: ["csharp"]
|
||||
},
|
||||
} satisfies Preset.ThemeConfig,
|
||||
};
|
||||
|
||||
export default config;
|
||||
14543
docs/package-lock.json
generated
14543
docs/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user