Compare commits

7 Commits

50 changed files with 876 additions and 335 deletions

View File

@@ -1,4 +1,5 @@
using Humanizer;
using MycroForge.CLI.Commands;
using MycroForge.CLI.Extensions;
using MycroForge.Core;
@@ -6,6 +7,8 @@ namespace MycroForge.CLI.CodeGen;
public class CrudRouterGenerator
{
#region Templates
private static readonly string[] Template =
[
"from typing import Annotated",
@@ -79,6 +82,8 @@ public class CrudRouterGenerator
"\t\treturn JSONResponse(status_code=500, content=str(ex))",
];
#endregion
private readonly ProjectContext _context;
public CrudRouterGenerator(ProjectContext context)
@@ -86,65 +91,49 @@ public class CrudRouterGenerator
_context = context;
}
public async Task Generate(string path, string entity)
public async Task Generate(FullyQualifiedName fqn)
{
var entitySnakeCaseName = entity.Underscore().ToLower();
var entityClassName = entity.Pascalize();
var serviceClassName = $"{entityClassName}Service";
var entityRoutePrefix = entity.Kebaberize().Pluralize().ToLower();
var serviceClassName = $"{fqn.PascalizedName}Service";
var entityRoutePrefix = fqn.PascalizedName.Kebaberize().Pluralize().ToLower();
var servicesFolderPath = $"{Features.Api.FeatureName}/services/{path}";
var serviceFilePath = $"{servicesFolderPath}/{entitySnakeCaseName}_service.py";
var serviceImportPath = serviceFilePath
.Replace('/', '.')
.Replace('\\', '.')
.Replace(".py", string.Empty)
.DeduplicateDots()
.Trim();
var serviceFilePath = Path.Join(
Features.Api.FeatureName, "services", fqn.FolderPath, $"{fqn.SnakeCasedName}_service"
);
var serviceImportPath = serviceFilePath.SlashesToDots();
var routerFolderPath = Path.Join(Features.Api.FeatureName, "routers", fqn.FolderPath);
var routerFilePath = Path.Join(routerFolderPath, $"{fqn.SnakeCasedName}");
var routerImportPath = routerFolderPath.SlashesToDots();
var requestsFolderPath = Path.Join(Features.Api.FeatureName, "requests", fqn.FolderPath);
var routersFolderPath = $"{Features.Api.FeatureName}/routers/{path}";
var routerFilePath = $"{routersFolderPath}/{entitySnakeCaseName}.py";
var routerImportPath = routersFolderPath
.Replace('/', '.')
.Replace('\\', '.')
.Replace(".py", "")
.DeduplicateDots()
.Trim();
var requestsFolderPath = $"{Features.Api.FeatureName}/requests/{path}";
var createRequestImportPath = $"{requestsFolderPath}/Create{entityClassName}Request"
.Replace('/', '.')
.Replace('\\', '.')
.DeduplicateDots()
var createRequestImportPath = Path.Join(requestsFolderPath, $"Create{fqn.PascalizedName}Request")
.SlashesToDots()
.Underscore()
.ToLower();
var createRequestClassName = $"Create{entityClassName}Request";
var createRequestClassName = $"Create{fqn.PascalizedName}Request";
var updateRequestImportPath = $"{requestsFolderPath}/Update{entityClassName}Request"
.Replace('/', '.')
.Replace('\\', '.')
.DeduplicateDots()
var updateRequestImportPath = Path.Join(requestsFolderPath, $"Update{fqn.PascalizedName}Request")
.SlashesToDots()
.Underscore()
.ToLower();
var updateRequestClassName = $"Update{entityClassName}Request";
var updateRequestClassName = $"Update{fqn.PascalizedName}Request";
var router = string.Join("\n", Template)
.Replace("%service_import_path%", serviceImportPath)
.Replace("%entity_class_name%", entityClassName)
.Replace("%entity_class_name%", fqn.PascalizedName)
.Replace("%service_class_name%", serviceClassName)
.Replace("%create_entity_request_import_path%", createRequestImportPath)
.Replace("%create_entity_request_class_name%", createRequestClassName)
.Replace("%update_entity_request_import_path%", updateRequestImportPath)
.Replace("%update_entity_request_class_name%", updateRequestClassName);
await _context.CreateFile(routerFilePath, router);
await _context.CreateFile($"{routerFilePath}.py", router);
var main = await _context.ReadFile("main.py");
main = new MainModifier(main).Initialize()
.Import(from: routerImportPath, import: entitySnakeCaseName)
.IncludeRouter(prefix: entityRoutePrefix, router: entitySnakeCaseName)
.Import(from: routerImportPath, import: fqn.SnakeCasedName)
.IncludeRouter(prefix: entityRoutePrefix, router: fqn.SnakeCasedName)
.Rewrite();
await _context.WriteFile("main.py", main);

View File

@@ -1,5 +1,4 @@
using Humanizer;
using MycroForge.CLI.Extensions;
using MycroForge.CLI.Commands;
using MycroForge.Core;
namespace MycroForge.CLI.CodeGen;
@@ -65,26 +64,15 @@ public class CrudServiceGenerator
_context = context;
}
public async Task Generate(string path, string entity)
public async Task Generate(FullyQualifiedName fqn)
{
var entitySnakeCaseName = entity.Underscore().ToLower();
var entityClassName = entity.Pascalize();
var entityImportPath = fqn.GetImportPath(root: [Features.Db.FeatureName, "entities"]);
var entitiesFolderPath = $"{Features.Db.FeatureName}/entities/{path}";
var entityFilePath = $"{entitiesFolderPath}/{entitySnakeCaseName}.py";
var entityImportPath = entityFilePath
.Replace('/', '.')
.Replace('\\', '.')
.Replace(".py", string.Empty)
.DeduplicateDots()
.Trim();
var servicesFolderPath = $"{Features.Api.FeatureName}/services/{path}";
var serviceFilePath = $"{servicesFolderPath}/{entity.Underscore().ToLower()}_service.py";
var serviceFilePath = Path.Join(Features.Api.FeatureName, "services", $"{fqn.FilePath}_service.py");
var service = string.Join("\n", Template)
.Replace("%entity_import_path%", entityImportPath)
.Replace("%entity_class_name%", entityClassName)
.Replace("%entity_class_name%", fqn.PascalizedName)
;
await _context.CreateFile(serviceFilePath, service);

View File

@@ -148,7 +148,7 @@ public partial class EntityLinker
var path = $"{Features.Db.FeatureName}/entities";
if (fqn.HasPath)
path = Path.Combine(path, fqn.Path);
path = Path.Combine(path, fqn.FolderPath);
path = Path.Combine(path, $"{fqn.SnakeCasedName}.py");
var entity = new EntityModel(fqn.PascalizedName, path, await _context.ReadFile(path));

View File

@@ -38,12 +38,14 @@ public class MainModifier
public MainModifier IncludeRouter(string prefix, string router)
{
_routerIncludeBuffer.Add($"\napp.include_router(prefix=\"/{prefix}\", router={router}.router)\n");
_routerIncludeBuffer.Add($"app.include_router(prefix=\"/{prefix}\", router={router}.router)");
return this;
}
public string Rewrite()
{
// Make sure to insert the includes before the imports, if done the other way around,
// the insertions of the includes will change the indexes of the imports.
InsertIncludes();
InsertImports();
@@ -54,7 +56,7 @@ public class MainModifier
private void InsertImports()
{
if (_importsBuffer.Count == 0) return;
if (_lastImport is not null)
{
_source.InsertMultiLine(_lastImport.EndIndex, _importsBuffer.ToArray());
@@ -69,15 +71,17 @@ public class MainModifier
{
if (_routerIncludeBuffer.Count == 0) return;
// Prepend an empty string to the router include buffer,
// this will ensure that the new entries are all on separate lines.
var content = _routerIncludeBuffer.Prepend(string.Empty).ToArray();
if (_lastRouterInclude is not null)
{
_source.InsertMultiLine(
_lastRouterInclude.EndIndex, _routerIncludeBuffer.ToArray()
);
_source.InsertMultiLine(_lastRouterInclude.EndIndex, content);
}
else
{
_source.InsertMultiLineAtEnd(_routerIncludeBuffer.ToArray());
_source.InsertMultiLineAtEnd(content);
}
}
}

View File

@@ -1,5 +1,6 @@
using System.Text.RegularExpressions;
using Humanizer;
using MycroForge.CLI.Commands;
using MycroForge.Core;
namespace MycroForge.CLI.CodeGen;
@@ -40,29 +41,30 @@ public class RequestClassGenerator
_context = context;
}
public async Task Generate(string path, string entity, Type type)
public async Task Generate(FullyQualifiedName fqn, Type type)
{
var entitySnakeCaseName = entity.Underscore().ToLower();
var entityClassName = entity.Pascalize();
var entitiesFolderPath = $"{Features.Db.FeatureName}/entities/{path}";
var entityFilePath = $"{entitiesFolderPath}/{entitySnakeCaseName}.py";
var entityFilePath = Path.Join(Features.Db.FeatureName, "entities", $"{fqn.FilePath}.py");
var entitySource = await _context.ReadFile(entityFilePath);
var fieldInfo = ReadFields(entitySource);
var fields = string.Join('\n', fieldInfo.Select(x => ToFieldString(x, type)));
var requestsFolderPath = $"{Features.Api.FeatureName}/requests/{path}";
var updateRequestFilePath =
$"{requestsFolderPath}/{type.ToString().ToLower()}_{entitySnakeCaseName}_request.py";
var requestFilePath = Path.Join(
Features.Api.FeatureName,
"requests",
fqn.FolderPath,
// requestsFolderPath,
$"{type.ToString().ToLower()}_{fqn.SnakeCasedName}_request.py"
);
var service = string.Join("\n", Template)
.Replace("%imports%", GetImportString(entitySource, fieldInfo, type))
.Replace("%request_type%", type.ToString().Pascalize())
.Replace("%entity_class_name%", entityClassName)
.Replace("%entity_class_name%", fqn.PascalizedName)
// .Replace("%entity_class_name%", entityClassName)
.Replace("%fields%", fields)
;
await _context.CreateFile(updateRequestFilePath, service);
await _context.CreateFile(requestFilePath, service);
}
private string ToFieldString(Field field, Type type)
@@ -104,7 +106,7 @@ public class RequestClassGenerator
.Replace("]", "")
.Replace(" ", "")
.Split();
foreach (var dissectedType in dissectedTypes)
{
if (imports.FirstOrDefault(i => i.Match(dissectedType)) is Import import)

View File

@@ -1,28 +1,48 @@
using Humanizer;
using MycroForge.CLI.Extensions;
namespace MycroForge.CLI.Commands;
public class FullyQualifiedName
{
public string Path { get; }
public string FolderPath { get; }
public string PascalizedName { get; }
public string SnakeCasedName { get; }
public bool HasPath => Path.Length > 0;
public string FilePath =>
string.IsNullOrEmpty(FolderPath.Trim())
? SnakeCasedName
: Path.Join(FolderPath, SnakeCasedName);
public bool HasPath => FolderPath.Length > 0;
public FullyQualifiedName(string name)
{
var path = string.Empty;
if (name.Split(':').Select(s => s.Trim()).ToArray() is { Length: 2 } fullName)
{
path = fullName[0];
name = fullName[1];
}
Path = path;
FolderPath = path;
PascalizedName = name.Pascalize();
SnakeCasedName = name.Underscore().ToLower();
SnakeCasedName = SnakeCase(name);
}
}
public string GetImportPath(params string[] root)
{
if (root.Length == 0)
return string.Join('.', FilePath).SlashesToDots();
var importRoot = string.Join('.', root);
return string.Join('.', SnakeCase(importRoot), FilePath).SlashesToDots();
}
private static string SnakeCase(string value) => value.Underscore().ToLower();
// private static string SlashesToDots(string value) => value.Replace('\\', '.').Replace('/', '.');
}

View File

@@ -21,6 +21,11 @@ public partial class MycroForge
description: "The database UI port"
);
private static readonly Option<ProjectConfig.DbConfig.DbuPlatformOptions> DbuPlatformOption = new(
aliases: ["--database-ui-platform", "--dbu-platform"],
description: "The docker platform for the PhpMyAdmin image"
);
private readonly ProjectContext _context;
private readonly OptionsContainer _optionsContainer;
private readonly List<IFeature> _features;
@@ -34,12 +39,22 @@ public partial class MycroForge
AddOption(DbhPortOption);
AddOption(DbuPortOption);
this.SetHandler(ExecuteAsync, DbhPortOption, DbuPortOption);
AddOption(DbuPlatformOption);
this.SetHandler(ExecuteAsync, DbhPortOption, DbuPortOption, DbuPlatformOption);
}
private async Task ExecuteAsync(int dbhPort, int dbuPort)
private async Task ExecuteAsync(
int dbhPort,
int dbuPort,
ProjectConfig.DbConfig.DbuPlatformOptions dbuPlatform
)
{
_optionsContainer.Set(new Features.Db.Options { DbhPort = dbhPort, DbuPort = dbuPort });
_optionsContainer.Set(new Features.Db.Options
{
DbhPort = dbhPort,
DbuPort = dbuPort,
DbuPlatform = dbuPlatform
});
var feature = _features.First(f => f.Name == Features.Db.FeatureName);
await feature.ExecuteAsync(_context);
}

View File

@@ -29,11 +29,10 @@ public partial class MycroForge
private async Task ExecuteAsync(string entity)
{
var fqn = new FullyQualifiedName(entity);
await new CrudServiceGenerator(_context).Generate(fqn.Path, fqn.PascalizedName);
await new RequestClassGenerator(_context).Generate(fqn.Path, fqn.PascalizedName, RequestClassGenerator.Type.Create);
await new RequestClassGenerator(_context).Generate(fqn.Path, fqn.PascalizedName, RequestClassGenerator.Type.Update);
await new CrudRouterGenerator(_context).Generate(fqn.Path, fqn.PascalizedName);
await new CrudServiceGenerator(_context).Generate(fqn);
await new RequestClassGenerator(_context).Generate(fqn, RequestClassGenerator.Type.Create);
await new RequestClassGenerator(_context).Generate(fqn, RequestClassGenerator.Type.Update);
await new CrudRouterGenerator(_context).Generate(fqn);
}
}
}

View File

@@ -49,7 +49,7 @@ public partial class MycroForge
_context.AssertDirectoryExists(folderPath);
if (fqn.HasPath)
folderPath = Path.Combine(folderPath, fqn.Path);
folderPath = Path.Combine(folderPath, fqn.FolderPath);
var fileName = $"{fqn.SnakeCasedName}.py";
var filePath = Path.Combine(folderPath, fileName);

View File

@@ -7,11 +7,11 @@ public partial class MycroForge
{
public partial class Api : Command, ISubCommandOf<MycroForge>
{
public Api(IEnumerable<ISubCommandOf<Api>> subCommands) :
public Api(IEnumerable<ISubCommandOf<Api>> commands) :
base("api", "API related commands")
{
foreach (var subCommandOf in subCommands)
AddCommand((subCommandOf as Command)!);
foreach (var command in commands)
AddCommand((command as Command)!);
}
}
}

View File

@@ -75,7 +75,7 @@ public partial class MycroForge
_context.AssertDirectoryExists(Features.Db.FeatureName);
if (fqn.HasPath)
folderPath = Path.Combine(folderPath, fqn.Path);
folderPath = Path.Combine(folderPath, fqn.FolderPath);
var _columns = GetColumnDefinitions(columns.ToArray());
var typeImports = string.Join(", ", _columns.Select(c => c.OrmType.Split('(').First()).Distinct());

View File

@@ -23,7 +23,8 @@ public partial class MycroForge
{
var config = await _context.LoadConfig();
var env = $"DBH_PORT={config.Db.DbhPort} DBU_PORT={config.Db.DbuPort}";
await _context.Bash($"{env} docker compose -f {Features.Db.FeatureName}.docker-compose.yml up -d");
var command = $"{env} docker compose -f {Features.Db.FeatureName}.docker-compose.yml up -d";
await _context.Bash(command);
}
}
}

View File

@@ -21,9 +21,10 @@ public partial class MycroForge
private async Task ExecuteAsync()
{
var config = await _context.LoadConfig();
var env = $"DB_PORT={config.Db.DbhPort} PMA_PORT={config.Db.DbuPort}";
await _context.Bash($"{env} docker compose -f {Features.Db.FeatureName}.docker-compose.yml down");
await _context.Bash(
// Set the log level to ERROR to prevent warnings concerning environment variables not being set.
$"docker --log-level ERROR compose -f {Features.Db.FeatureName}.docker-compose.yml down"
);
}
}
}

View File

@@ -61,7 +61,7 @@ public partial class MycroForge
var folderPath = string.Empty;
if (fqn.HasPath)
folderPath = Path.Combine(folderPath, fqn.Path);
folderPath = Path.Combine(folderPath, fqn.FolderPath);
var filePath = Path.Combine(folderPath, $"{fqn.SnakeCasedName}.py");
var template = withSession ? WithSessionTemplate : DefaultTemplate;

View File

@@ -15,6 +15,7 @@ public partial class MycroForge
ApiPort = ctx.ParseResult.GetValueForOption(ApiPortOption),
DbhPort = ctx.ParseResult.GetValueForOption(DbhPortOption),
DbuPort = ctx.ParseResult.GetValueForOption(DbuPortOption),
DbuPlatform = ctx.ParseResult.GetValueForOption(DbuPlatformOption),
};
}
}

View File

@@ -1,4 +1,6 @@
namespace MycroForge.CLI.Commands;
using MycroForge.Core;
namespace MycroForge.CLI.Commands;
public partial class MycroForge
{
@@ -11,6 +13,7 @@ public partial class MycroForge
public int? ApiPort { get; set; }
public int? DbhPort { get; set; }
public int? DbuPort { get; set; }
public ProjectConfig.DbConfig.DbuPlatformOptions DbuPlatform { get; set; }
public Features.Api.Options ApiOptions => new()
{
@@ -20,7 +23,8 @@ public partial class MycroForge
public Features.Db.Options DbOptions => new()
{
DbhPort = DbhPort <= 0 ? 5050 : DbhPort,
DbuPort = DbuPort <= 0 ? 5051 : DbhPort
DbuPort = DbuPort <= 0 ? 5051 : DbhPort,
DbuPlatform = DbuPlatform
};
}
}

View File

@@ -39,6 +39,11 @@ public partial class MycroForge
description: "The database UI port"
);
private static readonly Option<ProjectConfig.DbConfig.DbuPlatformOptions> DbuPlatformOption = new(
aliases: ["--database-ui-platform", "--dbu-platform"],
description: "The docker platform for the PhpMyAdmin image"
);
private readonly ProjectContext _context;
private readonly List<IFeature> _features;
private readonly OptionsContainer _optionsContainer;
@@ -55,6 +60,7 @@ public partial class MycroForge
AddOption(ApiPortOption);
AddOption(DbhPortOption);
AddOption(DbuPortOption);
AddOption(DbuPlatformOption);
this.SetHandler(ExecuteAsync, new Binder());
}

View File

@@ -2,11 +2,8 @@
public static class StringExtensions
{
public static string DeduplicateDots(this string path)
{
while (path.Contains(".."))
path = path.Replace("..", ".");
return path.Trim('.');
}
public static string SlashesToDots(this string path) =>
path.Replace('/', '.')
.Replace('\\', '.')
.Trim();
}

View File

@@ -1,10 +1,15 @@
namespace MycroForge.CLI.Features;
using MycroForge.Core;
namespace MycroForge.CLI.Features;
public sealed partial class Db
{
public class Options
{
public int? DbhPort { get; set; }
public int? DbuPort { get; set; }
public ProjectConfig.DbConfig.DbuPlatformOptions DbuPlatform { get; set; }
}
}

View File

@@ -1,6 +1,7 @@
using MycroForge.CLI.CodeGen;
using MycroForge.CLI.Commands;
using MycroForge.Core;
using MycroForge.Core.Extensions;
namespace MycroForge.CLI.Features;
@@ -37,7 +38,6 @@ public sealed partial class Db : IFeature
private static readonly string[] DockerCompose =
[
"version: '3.8'",
"# Access the database UI at http://localhost:${DBU_PORT}.",
"# Login: username = root & password = password",
"",
@@ -59,6 +59,7 @@ public sealed partial class Db : IFeature
"",
" %app_name%_phpmyadmin:",
" image: phpmyadmin/phpmyadmin",
" platform: %dbu_platform%",
" container_name: %app_name%_phpmyadmin",
" ports:",
" - '${DBU_PORT}:80'",
@@ -87,15 +88,16 @@ public sealed partial class Db : IFeature
{
_optionsContainer = optionsContainer;
}
public async Task ExecuteAsync(ProjectContext context)
{
var options = _optionsContainer.Get<Options>();
var config = await context.LoadConfig(create: true);
config.Db = new()
{
DbhPort = options.DbhPort ?? 5050,
DbuPort = options.DbuPort ?? 5051
DbhPort = options.DbhPort ?? 5050,
DbuPort = options.DbuPort ?? 5051,
DbuPlatform = options.DbuPlatform
};
await context.SaveConfig(config);
@@ -123,7 +125,10 @@ public sealed partial class Db : IFeature
await context.CreateFile($"{FeatureName}/entities/entity_base.py", EntityBase);
var dockerCompose = string.Join('\n', DockerCompose).Replace("%app_name%", appName);
var dockerCompose = string.Join('\n', DockerCompose)
.Replace("%app_name%", appName)
.Replace("%dbu_platform%", options.DbuPlatform.ToDockerPlatformString())
;
await context.CreateFile($"{FeatureName}.docker-compose.yml", dockerCompose);
}

View File

@@ -1,4 +1,4 @@
#!/usr/bin/bash
#!/usr/bin/bash
dotnet pack -v d

View File

@@ -0,0 +1,6 @@
namespace MycroForge.Core.Attributes;
public class DockerPlatformAttribute : Attribute
{
public string Platform { get; set; } = string.Empty;
}

View File

@@ -48,7 +48,7 @@ public class Source
public Source InsertMultiLineAtEnd(params string[] text)
{
_text += (string.Join('\n', text));
_text += string.Join('\n', text);
return this;
}

View File

@@ -0,0 +1,19 @@
using System.Reflection;
using MycroForge.Core.Attributes;
namespace MycroForge.Core.Extensions;
public static class EnumExtensions
{
/// <summary>
/// A generic extension method that aids in reflecting
/// and retrieving any attribute that is applied to an `Enum`.
/// </summary>
public static string ToDockerPlatformString(this ProjectConfig.DbConfig.DbuPlatformOptions value)
{
return value.GetType()
.GetMember(value.ToString())
.FirstOrDefault()!
.GetCustomAttribute<DockerPlatformAttribute>()!.Platform;
}
}

View File

@@ -0,0 +1,27 @@
using MycroForge.Core.Attributes;
namespace MycroForge.Core;
public partial class ProjectConfig
{
public partial class DbConfig
{
public enum DbuPlatformOptions
{
[DockerPlatform(Platform = "linux/amd64")]
linux_amd64,
[DockerPlatform(Platform = "linux/arm32/v5")]
linux_arm32v5,
[DockerPlatform(Platform = "linux/arm32/v6")]
linux_arm32v6,
[DockerPlatform(Platform = "linux/arm32/v7")]
linux_arm32v7,
[DockerPlatform(Platform = "linux/arm64/v8")]
linux_arm64v8
}
}
}

View File

@@ -1,10 +1,16 @@
namespace MycroForge.Core;
using System.Text.Json.Serialization;
namespace MycroForge.Core;
public partial class ProjectConfig
{
public class DbConfig
public partial class DbConfig
{
public int DbhPort { get; set; }
public int DbuPort { get; set; }
[JsonIgnore]
public DbuPlatformOptions DbuPlatform { get; set; }
}
}

View File

@@ -8,7 +8,7 @@ https://learn.microsoft.com/en-us/dotnet/core/tutorials/cli-templates-create-tem
### Build the package
`dotnet pack`
### Push to local nuget
### Push to devdisciples nuget
`dotnet nuget push bin/Release/MycroForge.PluginTemplate.Package.1.0.0.nupkg --source devdisciples`
### Install template package from local nuget

View File

View File

@@ -26,4 +26,13 @@ Run the install script in the same directory as the downloaded zip. See the exam
```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`
- Mention `--dbu-platform` option for `m4g init`
- Research if System.CommandLine middleware can be used to safeguard commands like `m4g add` or `m4g api`.
- Fix up exception handling
- Clean up README files
-

3
docs/.dockerignore Normal file
View File

@@ -0,0 +1,3 @@
.docusaurus/
node_modules/
.k8s/

3
docs/.gitignore vendored
View File

@@ -18,3 +18,6 @@
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.k8s/remote*.yml
!.k8s/remote.example.yml

61
docs/.k8s/local.yml Normal file
View File

@@ -0,0 +1,61 @@
# =========================================
# App manifest
# =========================================
---
# App Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: m4g-docs-deployment
spec:
replicas: 1
selector:
matchLabels:
app: m4g-docs
template:
metadata:
labels:
app: m4g-docs
spec:
containers:
- name: m4g-docs
image: m4gdocs:latest
imagePullPolicy: Never
ports:
- containerPort: 80
---
# App Service
apiVersion: v1
kind: Service
metadata:
name: m4g-docs-service
spec:
selector:
app: m4g-docs
ports:
- protocol: TCP
port: 80
targetPort: 80
type: NodePort
---
# App Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: m4g-docs-ingress
spec:
ingressClassName: caddy
rules:
- host: m4g.docs.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: m4g-docs-service
port:
number: 80

View File

@@ -0,0 +1,67 @@
# =========================================
# App manifest
# =========================================
---
# App Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: m4g-docs-deployment
spec:
replicas: 1
selector:
matchLabels:
app: m4g-docs
template:
metadata:
labels:
app: m4g-docs
spec:
containers:
- name: m4g-docs
image: git.devdisciples.com/devdisciples/m4gdocs:latest
imagePullPolicy: Always
ports:
- containerPort: 80
---
# App Service
apiVersion: v1
kind: Service
metadata:
name: m4g-docs-service
spec:
selector:
app: m4g-docs
ports:
- protocol: TCP
port: 80
targetPort: 80
type: NodePort
---
# App Ingress
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: m4g-docs-ingress
annotations:
cert-manager.io/cluster-issuer: lets-encrypt
spec:
ingressClassName: public
tls:
- hosts:
- m4g.example.com
secretName: example-tls-secret
rules:
- host: m4g.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: m4g-docs-service
port:
number: 80

27
docs/Dockerfile Normal file
View File

@@ -0,0 +1,27 @@
# syntax=docker/dockerfile:1
# Stage 1: Base image.
## Start with a base image containing NodeJS so we can build Docusaurus.
FROM node:lts AS base
## Disable colour output from yarn to make logs easier to read.
ENV FORCE_COLOR=0
## Enable corepack.
RUN corepack enable
## Set the working directory to `/opt/docusaurus`.
WORKDIR /opt/docusaurus
# Stage 2b: Production build mode.
FROM base AS prod
## Set the working directory to `/opt/docusaurus`.
WORKDIR /opt/docusaurus
## Copy over the source code.
COPY . /opt/docusaurus/
## Install dependencies with `--immutable` to ensure reproducibility.
RUN npm ci
## Build the static site.
RUN npm run build
## Use a stable nginx image
FROM nginx:stable-alpine AS deploy
WORKDIR /home/node/app
COPY --chown=node:node --from=prod /opt/docusaurus/build/ /usr/share/nginx/html/

View File

@@ -39,3 +39,9 @@ $ 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

25
docs/docs.sln Normal file
View File

@@ -0,0 +1,25 @@

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

View File

@@ -1,3 +1,7 @@
---
sidebar_position: 5
---
# Commands
```

View File

@@ -12,7 +12,8 @@ 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
-?, -h, --help Show help and usage information
--database-host-port, --dbh-port <database-host-port> The database host port
--database-ui-port, --dbu-port <database-ui-port> The database UI port
--database-ui-platform, --dbu-platform <linux_amd64|linux_arm32v5|linux_arm32v6|linux_arm32v7|linux_arm64v8> The docker platform for the PhpMyAdmin image
-?, -h, --help Show help and usage information
```

View File

@@ -15,9 +15,10 @@ 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
-?, -h, --help Show help and usage information
--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 <linux_amd64|linux_arm32v5|linux_arm32v6|linux_arm32v7|linux_arm64v8> The docker platform for the PhpMyAdmin image
-?, -h, --help Show help and usage information
```

View File

@@ -0,0 +1,174 @@
---
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.

View File

@@ -0,0 +1,68 @@
---
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.

View File

@@ -1,21 +0,0 @@
---
sidebar_position: 2
---
# Install
## Requirements
MycroForge has the following dependencies.
- bash
- git
- Python3 (3.10)
- Docker
- .NET 8
### Windows
To simplify the implementation of this tool, it assumes that it's running in a POSIX compliant environment.
MycroForge has been developed and tested on Windows in WSL2 Ubuntu 22.04.03.
So when running on Windows, it's recommended to run MycroForge in the same environment or atleast in a similar WSL2 distro.

View File

@@ -2,144 +2,15 @@
sidebar_position: 1
---
# Intro
## What is MycroForge?
Welcome to **MycroForge** an opinionated CLI tool designed to streamline the development of FastAPI and SQLAlchemy-based backends. With MycroForge, you can effortlessly create backend projects through a convenient command line interface.
MycroForge is an opinionated CLI tool that is meant to facilitate the development of FastAPI & SQLAlchemy based backends.
It provides a command line interface that allows users to generate a skeleton project and other common items like
database entities, migrations, routers and even basic CRUD functionality. The main purpose of this tool is to generate
boilerplate code and to provide a unified interface for performing recurrent development activities.
## Key Features
## Generating a project
To generate a project you can run the following command.
`m4g init <name>`
```
Description:
Initialize a new project
Usage:
m4g init <name> [options]
Arguments:
<name> The name of your project
Options:
--without <api|db|git> Features to exclude
-?, -h, --help Show help and usage information
```
Running this command will generate the following project structure.
```
📦<project_name>
┣ 📂.git
┣ 📂.venv
┣ 📂api
┃ ┗ 📂routers
┃ ┃ ┗ 📜hello.py
┣ 📂db
┃ ┣ 📂engine
┃ ┃ ┗ 📜async_session.py
┃ ┣ 📂entities
┃ ┃ ┗ 📜entity_base.py
┃ ┣ 📂versions
┃ ┣ 📜README
┃ ┣ 📜env.py
┃ ┣ 📜script.py.mako
┃ ┗ 📜settings.py
┣ 📜.gitignore
┣ 📜alembic.ini
┣ 📜db.docker-compose.yml
┣ 📜m4g.json
┣ 📜main.py
┗ 📜requirements.txt
```
Let's go through these one by one.
### .git
The `m4g init` command will initialize new projects with git by default.
If you don't want to use git you can pass the option `--without git` to `m4g init`.
### .venv
To promote isolation of Python dependencies, new projects are initialized with a virtual environment by default.
TODO: This is a good section to introduce the `m4g hydrate` command.
### api/routers/hello.py
This file defines a basic example router, which is imported and mapped in `main.py`. This router is just an example and
can be removed or modified at you discretion.
### db/engine/async_session.py
This file defines the `async_session` function, which can be used to open an asynchronous session to a database.
### db/entities/entity_base.py
This file contains an automatically generated entity base class that derives from the DeclarativeBase.
All entities must inherit from this class, so that SQLAlchemy & alembic can track them. The entities directory is also
where all newly generated entities will be stored.
### db/versions
This is where the generated database migrations will be stored.
### db/README
This README file is automatically generated by the alembic init command.
### db/env.py
This is the database environment file that is used by alembic to interact with the database.
If you take a closer look at the imports, you'll see that the file has been modified to assign `EntityBase.metadata` to
a variable called `target_metadata`, this will allow alembic to track changes in your entities. You'll also find that
the `DbSettings` class is used to get the connectionstring. Any time you generate a new database entity, or create a
many-to-many relation between two entities, this file will also be modified to include the generated classes.
### db/script.py.mako
This file is automatically generated by the alembic init command.
### db/settings.py
This file defines the `DbSettings` class, that is responsible for retrieving the database connectionstring.
You will probably want to modify this class to retrieve the connectionstring from a secret manager at some point.
### .gitignore
The default .gitignore file that is generated by the `m4g init` command. Modify this file at your discretion.
### alembic.ini
This file is automatically generated by the alembic init command.
### db.docker-compose.yml
A docker compose file for running a database locally.
### m4g.json
This file contains some configs that are used by the CLI, for example the ports to map to the API and database.
### main.py
The entrypoint for the application. When generating entities, many-to-many relations or routers, this file will be
modified to include the generated files.
### requirements.txt
The requirements file containing the Python dependencies.
TODO: introduce the `m4g install` & `m4g uninstall` commands.
## Plugin system
TODO: Dedicate a section to the Plugin system
- **Project Skeleton Generation:** Quickly generate a well-structured project skeleton tailored for FastAPI and SQLAlchemy, ensuring you start with best practices.
- **Database Entities:** Easily create and manage database entities, simplifying your database interactions.
- **Migrations:** Handle database migrations seamlessly, allowing for smooth transitions and updates.
- **Routers:** Generate and manage routers to keep your application modular and organized.
- **CRUD Functionality:** Automatically generate basic CRUD (Create, Read, Update, Delete) operations to accelerate your development process.

111
docs/docs/project_layout.md Normal file
View File

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

View File

@@ -4,20 +4,17 @@ sidebar_position: 3
# Tutorial
We're going to build a simple todo app to demonstrate the capabilities of the MycroForge CLI.
After this tutorial, you should have a solid foundation to start exploring and using MycroForge to develop your
projects.
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're running them from a MycroForge root directory.
The commands in this tutorial assume that you are running them from the root directory of your MycroForge project.
## Initialize the project
## Initialize the Project
Open a terminal and `cd` into the directory where your project should be created.
Run `m4g init todo-app` to initialize a new project and open the newly created project by running `code todo-app`.
Make sure you have `vscode` on you machine before running this command. If you prefer using another editor, then
manually open the generated `todo-app` folder.
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
@@ -31,7 +28,9 @@ locally.
The first step is to start the database, you can do this by running the following command in a terminal.
`m4g db run`
```bash
m4g db run
```
This command starts the services defined in the `db.docker-compose.yml` file.
You can verify that the services are up by running `docker container ls`. If everything went well, then the previous
@@ -50,9 +49,13 @@ When you're done developing, you can shut down the local database by running `m4
Now that the database is running, we can start to create our entities. Run the commands below to create the `Todo` &
`Tag` entities.
`m4g db generate entity Tag --column "description:str:String(255)"`
```bash
m4g db generate entity Tag --column "description:str:String(255)"
```
`m4g db generate entity Todo --column "description:str:String(255)" -c "is_done:bool:Boolean()"`
```bash
m4g db generate entity Todo --column "description:str:String(255)" -c "is_done:bool:Boolean()"
```
After running these commands, you should find the generated entities in the `db/entities` folder of your project.
You should also see that the `main.py` & `db/env.py` files have been modified to include the newly generated entity.
@@ -68,7 +71,9 @@ Creating a one-to-many relation would also make sense, but for the purpose of de
the many-to-many relation, because this one is the most complex, since it requires an additional mapping to be included
in the database schema.
`m4g db link many Todo --to-many Tag`
```bash
m4g db link many Todo --to-many Tag
```
After running this command you should see that both the `Todo` and `Tag` entities now have a new field referencing the
a `List` containing instances of the other entity.
@@ -82,7 +87,9 @@ examine the command. The same is true for all the other commands as well.
Now that we've generated our entities, it's time to generate a migration that will apply these changes in the database.
Generate the initial migration by running the following command.
`m4g db generate migration initial_migration`
```bash
m4g db generate migration initial_migration
```
After running this command, you should see the new migration in the `db/version` directory.
@@ -91,7 +98,9 @@ After running this command, you should see the new migration in the `db/version`
The last step for the database setup is to actually apply the new migration to the database. This can be done by running
the following command.
`m4g db migrate`
```bash
m4g db migrate
```
After running this command, you should now see a populated schema when visiting [PhpMyAdmin](http://localhost:5051).
If for whatever reason you want to undo the last migration, you can simply run `m4g db rollback`.
@@ -105,9 +114,13 @@ Writing this code can be boring, since it's pretty much boilerplate with some cu
Fortunately, MycroForge can generate a good chunk of this boring code on your behalf. Run the following commands to
generate CRUD functionality for the `Todo` & `Tag` classes.
`m4g api generate crud Tag`
```bash
m4g api generate crud Tag
```
`m4g api generate crud Todo`
```bash
m4g api generate crud Todo
```
After running this command you should see that the `api/requests`,`api/routers` & `api/services` now contain the
relevant classes need to support the generated CRUD functionality. This could should be relatively straightforward, so
@@ -121,15 +134,22 @@ yet. We need to be able to specify which `Tags` to add to a `Todo` when creating
To do this, we will allow for a `tag_ids` field in both the `CreateTodoRequest` & the `UpdateTodoRequest`.
This field will contain the ids of the `Tags` that are associated with a `Todo`.
Modify `CreateTodoRequest` in `api/requests/create_todo_request.py`, you might need to import `List` from `typing`.
Modify `CreateTodoRequest` in `api/requests/create_todo_request.py`.
```python
# Before
from pydantic import BaseModel
class CreateTodoRequest(BaseModel):
description: str = None
is_done: bool = None
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
@@ -140,12 +160,19 @@ Modify `UpdateTodoRequest` in `api/requests/update_todo_request.py`, you might n
```python
# Before
from pydantic import BaseModel
from typing import Optional
class UpdateTodoRequest(BaseModel):
description: Optional[str] = None
is_done: Optional[bool] = None
tag_ids: Optional[List[int]] = []
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
@@ -167,16 +194,18 @@ Modify `TodoService.list`
```python
# Before
async with async_session() as session:
stmt = select(Todo)
results = (await session.scalars(stmt)).all()
return results
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 with async_session() as session:
stmt = select(Todo).options(selectinload(Todo.tags))
results = (await session.scalars(stmt)).all()
return results
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`
@@ -279,5 +308,11 @@ Modify `TodoService.update`
return True
```
At this point, the app should be ready to test.
TODO: Elaborate!
## Test the API!
Run the following command.
```bash
m4g api run
```
Go to http://localhost:5000/docs and test your Todo API!

View File

@@ -77,8 +77,9 @@ const config: Config = {
copyright: `Copyright © ${new Date().getFullYear()} DevDisciples`,
},
prism: {
theme: prismThemes.github,
darkTheme: prismThemes.dracula,
theme: prismThemes.oneLight,
darkTheme: prismThemes.oneDark,
additionalLanguages: ["csharp"]
},
} satisfies Preset.ThemeConfig,
};

View File

@@ -4,7 +4,7 @@
"private": true,
"scripts": {
"docusaurus": "docusaurus",
"start": "docusaurus start",
"start": "docusaurus start --host 0.0.0.0",
"build": "docusaurus build",
"swizzle": "docusaurus swizzle",
"deploy": "docusaurus deploy",

View File

@@ -10,22 +10,22 @@ type FeatureItem = {
const FeatureList: FeatureItem[] = [
{
title: 'Initialize a skeleton project quickly',
title: 'Initialize a Skeleton Project Quickly',
Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default,
description: (
<>
Initialize a skeleton project that supports FastAPI and SQLAlchemy with a single command.
Here is an example. <code>m4g init todo-app</code>
Start a new FastAPI and SQLAlchemy project with a single command.
For example: <code>m4g init todo-app</code>
</>
),
},
{
title: 'Generate common items',
title: 'Generate Common Items',
Svg: require('@site/static/img/undraw_docusaurus_tree.svg').default,
description: (
<>
MycroForge allows you to generate boilerplate code for common items like entities, service & routers.
It can even generate a basic CRUD setup around an entity!
Use MycroForge to generate boilerplate code for entities, services, and routers.
It even supports basic CRUD setup!
</>
),
},
@@ -34,15 +34,15 @@ const FeatureList: FeatureItem[] = [
Svg: require('@site/static/img/undraw_docusaurus_react.svg').default,
description: (
<>
Extend MycroForge with your own commands by creating a plugin!
Create plugins to extend MycroForge with your own custom commands.
</>
),
},
];
function Feature({title, Svg, description}: FeatureItem) {
function Feature({ title, Svg, description }: FeatureItem) {
return (
<div className={clsx('col col--4')}>
<div className={clsx('col col--4', styles.feature)}>
<div className="text--center">
<Svg className={styles.featureSvg} role="img" />
</div>

View File

@@ -20,7 +20,7 @@ function HomepageHeader() {
<Link
className="button button--secondary button--lg"
to="/docs/intro">
MycroForge Tutorial
Get started now!
</Link>
</div>
</div>