@atlashub/smartstack-cli 3.22.0 → 3.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/mcp-entry.mjs +143 -174
- package/dist/mcp-entry.mjs.map +1 -1
- package/package.json +1 -1
- package/templates/mcp-scaffolding/component.tsx.hbs +21 -1
- package/templates/skills/apex/SKILL.md +21 -0
- package/templates/skills/apex/references/smartstack-api.md +507 -0
- package/templates/skills/apex/references/smartstack-frontend.md +1081 -0
- package/templates/skills/apex/references/smartstack-layers.md +166 -20
- package/templates/skills/apex/steps/step-00-init.md +27 -14
- package/templates/skills/apex/steps/step-01-analyze.md +45 -3
- package/templates/skills/apex/steps/step-02-plan.md +5 -1
- package/templates/skills/apex/steps/step-03-execute.md +51 -9
- package/templates/skills/apex/steps/step-04-validate.md +251 -0
- package/templates/skills/apex/steps/step-05-examine.md +7 -0
- package/templates/skills/apex/steps/step-07-tests.md +48 -5
- package/templates/skills/business-analyse/_shared.md +6 -6
- package/templates/skills/business-analyse/patterns/suggestion-catalog.md +1 -1
- package/templates/skills/business-analyse/questionnaire/07-ui.md +3 -3
- package/templates/skills/business-analyse/references/cadrage-pre-analysis.md +1 -1
- package/templates/skills/business-analyse/references/entity-architecture-decision.md +3 -3
- package/templates/skills/business-analyse/references/handoff-file-templates.md +13 -5
- package/templates/skills/business-analyse/references/spec-auto-inference.md +14 -14
- package/templates/skills/business-analyse/steps/step-01-cadrage.md +2 -2
- package/templates/skills/business-analyse/steps/step-02-decomposition.md +1 -1
- package/templates/skills/business-analyse/steps/step-03a1-setup.md +2 -2
- package/templates/skills/business-analyse/steps/step-03b-ui.md +2 -1
- package/templates/skills/business-analyse/steps/step-05a-handoff.md +15 -4
- package/templates/skills/business-analyse/templates/tpl-frd.md +2 -2
- package/templates/skills/business-analyse/templates-frd.md +2 -2
- package/templates/skills/ralph-loop/references/category-rules.md +45 -7
- package/templates/skills/ralph-loop/references/compact-loop.md +2 -2
- package/templates/skills/ralph-loop/references/core-seed-data.md +10 -0
- package/templates/skills/ralph-loop/steps/step-02-execute.md +110 -1
- package/templates/skills/validate-feature/steps/step-05-db-validation.md +86 -1
package/dist/mcp-entry.mjs
CHANGED
|
@@ -26901,12 +26901,16 @@ async function validateNamespaces(structure, config2, result) {
|
|
|
26901
26901
|
if (namespaceMatch) {
|
|
26902
26902
|
const namespace = namespaceMatch[1];
|
|
26903
26903
|
if (!namespace.startsWith(layer.expected)) {
|
|
26904
|
+
const lastDot = namespace.lastIndexOf(".");
|
|
26905
|
+
const namespaceSuffix = lastDot > 0 ? namespace.substring(lastDot + 1) : "";
|
|
26906
|
+
const isValidLayerPattern = ["Domain", "Application", "Infrastructure", "Api"].some((l) => namespace.includes(`.${l}`) || namespace.endsWith(`.${l}`));
|
|
26907
|
+
const severity = isValidLayerPattern ? "warning" : "error";
|
|
26904
26908
|
result.errors.push({
|
|
26905
|
-
type:
|
|
26909
|
+
type: severity,
|
|
26906
26910
|
category: "namespaces",
|
|
26907
|
-
message: `${layer.name} file has
|
|
26911
|
+
message: `${layer.name} file has namespace "${namespace}" (expected prefix: "${layer.expected}")`,
|
|
26908
26912
|
file: path8.relative(structure.root, file),
|
|
26909
|
-
suggestion: `Should start with "${layer.expected}"`
|
|
26913
|
+
suggestion: severity === "warning" ? `Client extension project detected. Namespace "${namespace}" follows Clean Architecture pattern but differs from config.` : `Should start with "${layer.expected}"`
|
|
26910
26914
|
});
|
|
26911
26915
|
}
|
|
26912
26916
|
}
|
|
@@ -34496,19 +34500,32 @@ public interface I{{name}}Service
|
|
|
34496
34500
|
const implementationTemplate = `using System.Threading;
|
|
34497
34501
|
using System.Threading.Tasks;
|
|
34498
34502
|
using System.Collections.Generic;
|
|
34503
|
+
using System.Linq;
|
|
34504
|
+
using Microsoft.EntityFrameworkCore;
|
|
34499
34505
|
using Microsoft.Extensions.Logging;
|
|
34506
|
+
using SmartStack.Application.Common.Interfaces.Identity;
|
|
34507
|
+
using SmartStack.Application.Common.Interfaces.Persistence;
|
|
34500
34508
|
|
|
34501
34509
|
namespace {{implNamespace}};
|
|
34502
34510
|
|
|
34503
34511
|
/// <summary>
|
|
34504
|
-
/// Service implementation for {{name}} operations
|
|
34512
|
+
/// Service implementation for {{name}} operations.
|
|
34513
|
+
/// IMPORTANT: All queries MUST filter by _currentUser.TenantId for multi-tenant isolation.
|
|
34514
|
+
/// IMPORTANT: GetAllAsync MUST support search parameter for frontend EntityLookup component.
|
|
34505
34515
|
/// </summary>
|
|
34506
34516
|
public class {{name}}Service : I{{name}}Service
|
|
34507
34517
|
{
|
|
34518
|
+
private readonly IExtensionsDbContext _db;
|
|
34519
|
+
private readonly ICurrentUser _currentUser;
|
|
34508
34520
|
private readonly ILogger<{{name}}Service> _logger;
|
|
34509
34521
|
|
|
34510
|
-
public {{name}}Service(
|
|
34522
|
+
public {{name}}Service(
|
|
34523
|
+
IExtensionsDbContext db,
|
|
34524
|
+
ICurrentUser currentUser,
|
|
34525
|
+
ILogger<{{name}}Service> logger)
|
|
34511
34526
|
{
|
|
34527
|
+
_db = db;
|
|
34528
|
+
_currentUser = currentUser;
|
|
34512
34529
|
_logger = logger;
|
|
34513
34530
|
}
|
|
34514
34531
|
|
|
@@ -34516,8 +34533,12 @@ public class {{name}}Service : I{{name}}Service
|
|
|
34516
34533
|
/// <inheritdoc />
|
|
34517
34534
|
public async Task<object> {{this}}(CancellationToken cancellationToken = default)
|
|
34518
34535
|
{
|
|
34519
|
-
_logger.LogInformation("Executing {{this}}");
|
|
34520
|
-
// TODO: Implement {{this}}
|
|
34536
|
+
_logger.LogInformation("Executing {{this}} for tenant {TenantId}", _currentUser.TenantId);
|
|
34537
|
+
// TODO: Implement {{this}} \u2014 ALL queries must filter by _currentUser.TenantId
|
|
34538
|
+
// IMPORTANT: GetAllAsync MUST accept (string? search, int page, int pageSize) parameters
|
|
34539
|
+
// to enable EntityLookup search on the frontend. Example:
|
|
34540
|
+
// if (!string.IsNullOrWhiteSpace(search))
|
|
34541
|
+
// query = query.Where(x => x.Name.Contains(search) || x.Code.Contains(search));
|
|
34521
34542
|
await Task.CompletedTask;
|
|
34522
34543
|
throw new NotImplementedException();
|
|
34523
34544
|
}
|
|
@@ -34561,7 +34582,6 @@ async function scaffoldEntity(name, options, structure, config2, result, dryRun
|
|
|
34561
34582
|
const schema = options?.schema || config2.conventions.schemas.platform;
|
|
34562
34583
|
const entityTemplate = `using System;
|
|
34563
34584
|
using SmartStack.Domain.Common;
|
|
34564
|
-
using SmartStack.Domain.Common.Interfaces;
|
|
34565
34585
|
|
|
34566
34586
|
namespace {{namespace}};
|
|
34567
34587
|
|
|
@@ -34570,14 +34590,10 @@ namespace {{namespace}};
|
|
|
34570
34590
|
{{#unless isSystemEntity}}
|
|
34571
34591
|
/// Tenant-scoped: data is isolated per tenant
|
|
34572
34592
|
{{else}}
|
|
34573
|
-
///
|
|
34593
|
+
/// Platform-level entity: no tenant isolation
|
|
34574
34594
|
{{/unless}}
|
|
34575
34595
|
/// </summary>
|
|
34576
|
-
{{#
|
|
34577
|
-
public class {{name}} : SystemEntity
|
|
34578
|
-
{{else}}
|
|
34579
|
-
public class {{name}} : BaseEntity, ITenantEntity
|
|
34580
|
-
{{/if}}
|
|
34596
|
+
public class {{name}} : BaseEntity{{#unless isSystemEntity}}, ITenantEntity, IAuditableEntity{{else}}, IAuditableEntity{{/unless}}
|
|
34581
34597
|
{
|
|
34582
34598
|
{{#unless isSystemEntity}}
|
|
34583
34599
|
// === MULTI-TENANT ===
|
|
@@ -34588,6 +34604,14 @@ public class {{name}} : BaseEntity, ITenantEntity
|
|
|
34588
34604
|
public Guid TenantId { get; private set; }
|
|
34589
34605
|
|
|
34590
34606
|
{{/unless}}
|
|
34607
|
+
// === AUDIT ===
|
|
34608
|
+
|
|
34609
|
+
/// <summary>User who created this entity</summary>
|
|
34610
|
+
public string? CreatedBy { get; set; }
|
|
34611
|
+
|
|
34612
|
+
/// <summary>User who last updated this entity</summary>
|
|
34613
|
+
public string? UpdatedBy { get; set; }
|
|
34614
|
+
|
|
34591
34615
|
{{#if baseEntity}}
|
|
34592
34616
|
// === RELATIONSHIPS ===
|
|
34593
34617
|
|
|
@@ -34599,7 +34623,7 @@ public class {{name}} : BaseEntity, ITenantEntity
|
|
|
34599
34623
|
/// <summary>
|
|
34600
34624
|
/// Navigation property to {{baseEntity}}
|
|
34601
34625
|
/// </summary>
|
|
34602
|
-
public
|
|
34626
|
+
public {{baseEntity}}? {{baseEntity}} { get; private set; }
|
|
34603
34627
|
|
|
34604
34628
|
{{/if}}
|
|
34605
34629
|
// === BUSINESS PROPERTIES ===
|
|
@@ -34613,71 +34637,38 @@ public class {{name}} : BaseEntity, ITenantEntity
|
|
|
34613
34637
|
/// <summary>
|
|
34614
34638
|
/// Factory method to create a new {{name}}
|
|
34615
34639
|
/// </summary>
|
|
34616
|
-
{{#
|
|
34640
|
+
{{#unless isSystemEntity}}
|
|
34641
|
+
/// <param name="tenantId">Required tenant identifier</param>
|
|
34617
34642
|
public static {{name}} Create(
|
|
34618
|
-
|
|
34619
|
-
string? createdBy = null)
|
|
34643
|
+
Guid tenantId)
|
|
34620
34644
|
{
|
|
34645
|
+
if (tenantId == Guid.Empty)
|
|
34646
|
+
throw new ArgumentException("TenantId is required", nameof(tenantId));
|
|
34647
|
+
|
|
34621
34648
|
return new {{name}}
|
|
34622
34649
|
{
|
|
34623
34650
|
Id = Guid.NewGuid(),
|
|
34624
|
-
|
|
34625
|
-
CreatedAt = DateTime.UtcNow
|
|
34626
|
-
CreatedBy = createdBy
|
|
34651
|
+
TenantId = tenantId,
|
|
34652
|
+
CreatedAt = DateTime.UtcNow
|
|
34627
34653
|
};
|
|
34628
34654
|
}
|
|
34629
34655
|
{{else}}
|
|
34630
|
-
|
|
34631
|
-
/// <param name="code">Unique code within tenant (will be lowercased)</param>
|
|
34632
|
-
/// <param name="createdBy">User who created this entity</param>
|
|
34633
|
-
public static {{name}} Create(
|
|
34634
|
-
Guid tenantId,
|
|
34635
|
-
string code,
|
|
34636
|
-
string? createdBy = null)
|
|
34656
|
+
public static {{name}} Create()
|
|
34637
34657
|
{
|
|
34638
|
-
if (tenantId == Guid.Empty)
|
|
34639
|
-
throw new ArgumentException("TenantId is required", nameof(tenantId));
|
|
34640
|
-
|
|
34641
34658
|
return new {{name}}
|
|
34642
34659
|
{
|
|
34643
34660
|
Id = Guid.NewGuid(),
|
|
34644
|
-
|
|
34645
|
-
Code = code.ToLowerInvariant(),
|
|
34646
|
-
CreatedAt = DateTime.UtcNow,
|
|
34647
|
-
CreatedBy = createdBy
|
|
34661
|
+
CreatedAt = DateTime.UtcNow
|
|
34648
34662
|
};
|
|
34649
34663
|
}
|
|
34650
|
-
{{/
|
|
34664
|
+
{{/unless}}
|
|
34651
34665
|
|
|
34652
34666
|
/// <summary>
|
|
34653
34667
|
/// Update the entity
|
|
34654
34668
|
/// </summary>
|
|
34655
|
-
public void Update(
|
|
34669
|
+
public void Update()
|
|
34656
34670
|
{
|
|
34657
34671
|
UpdatedAt = DateTime.UtcNow;
|
|
34658
|
-
UpdatedBy = updatedBy;
|
|
34659
|
-
}
|
|
34660
|
-
|
|
34661
|
-
/// <summary>
|
|
34662
|
-
/// Soft delete the entity
|
|
34663
|
-
/// </summary>
|
|
34664
|
-
public void SoftDelete(string? deletedBy = null)
|
|
34665
|
-
{
|
|
34666
|
-
IsDeleted = true;
|
|
34667
|
-
DeletedAt = DateTime.UtcNow;
|
|
34668
|
-
DeletedBy = deletedBy;
|
|
34669
|
-
}
|
|
34670
|
-
|
|
34671
|
-
/// <summary>
|
|
34672
|
-
/// Restore a soft-deleted entity
|
|
34673
|
-
/// </summary>
|
|
34674
|
-
public void Restore(string? restoredBy = null)
|
|
34675
|
-
{
|
|
34676
|
-
IsDeleted = false;
|
|
34677
|
-
DeletedAt = null;
|
|
34678
|
-
DeletedBy = null;
|
|
34679
|
-
UpdatedAt = DateTime.UtcNow;
|
|
34680
|
-
UpdatedBy = restoredBy;
|
|
34681
34672
|
}
|
|
34682
34673
|
}
|
|
34683
34674
|
`;
|
|
@@ -34689,85 +34680,38 @@ namespace {{infrastructureNamespace}}.Persistence.Configurations{{#if configName
|
|
|
34689
34680
|
|
|
34690
34681
|
/// <summary>
|
|
34691
34682
|
/// EF Core configuration for {{name}}
|
|
34692
|
-
{{#unless isSystemEntity}}
|
|
34693
|
-
/// Tenant-aware: includes tenant isolation query filter
|
|
34694
|
-
{{else}}
|
|
34695
|
-
/// System entity: no tenant isolation
|
|
34696
|
-
{{/unless}}
|
|
34697
34683
|
/// </summary>
|
|
34698
34684
|
public class {{name}}Configuration : IEntityTypeConfiguration<{{name}}>
|
|
34699
34685
|
{
|
|
34700
34686
|
public void Configure(EntityTypeBuilder<{{name}}> builder)
|
|
34701
34687
|
{
|
|
34702
|
-
// Table name with schema and domain prefix
|
|
34703
34688
|
builder.ToTable("{{tablePrefix}}{{name}}s", "{{schema}}");
|
|
34704
34689
|
|
|
34705
|
-
// Primary key
|
|
34706
34690
|
builder.HasKey(e => e.Id);
|
|
34707
34691
|
|
|
34708
34692
|
{{#unless isSystemEntity}}
|
|
34709
|
-
//
|
|
34710
|
-
// MULTI-TENANT CONFIGURATION
|
|
34711
|
-
// ============================================
|
|
34712
|
-
|
|
34713
|
-
// TenantId is required for tenant isolation
|
|
34693
|
+
// Multi-tenant
|
|
34714
34694
|
builder.Property(e => e.TenantId).IsRequired();
|
|
34715
34695
|
builder.HasIndex(e => e.TenantId)
|
|
34716
34696
|
.HasDatabaseName("IX_{{tablePrefix}}{{name}}s_TenantId");
|
|
34717
34697
|
|
|
34718
|
-
// Tenant relationship (configured in Tenant configuration)
|
|
34719
|
-
// builder.HasOne<Tenant>().WithMany().HasForeignKey(e => e.TenantId);
|
|
34720
|
-
|
|
34721
|
-
// Code: lowercase, unique per tenant (filtered for soft delete)
|
|
34722
|
-
builder.Property(e => e.Code).HasMaxLength(100).IsRequired();
|
|
34723
|
-
builder.HasIndex(e => new { e.TenantId, e.Code })
|
|
34724
|
-
.IsUnique()
|
|
34725
|
-
.HasFilter("[IsDeleted] = 0")
|
|
34726
|
-
.HasDatabaseName("IX_{{tablePrefix}}{{name}}s_Tenant_Code_Unique");
|
|
34727
|
-
{{else}}
|
|
34728
|
-
// Code: lowercase, unique globally (filtered for soft delete)
|
|
34729
|
-
builder.Property(e => e.Code).HasMaxLength(100).IsRequired();
|
|
34730
|
-
builder.HasIndex(e => e.Code)
|
|
34731
|
-
.IsUnique()
|
|
34732
|
-
.HasFilter("[IsDeleted] = 0")
|
|
34733
|
-
.HasDatabaseName("IX_{{tablePrefix}}{{name}}s_Code_Unique");
|
|
34734
34698
|
{{/unless}}
|
|
34735
|
-
|
|
34736
|
-
// Concurrency token
|
|
34737
|
-
builder.Property(e => e.RowVersion).IsRowVersion();
|
|
34738
|
-
|
|
34739
|
-
// Audit fields
|
|
34699
|
+
// Audit fields (from IAuditableEntity)
|
|
34740
34700
|
builder.Property(e => e.CreatedBy).HasMaxLength(256);
|
|
34741
34701
|
builder.Property(e => e.UpdatedBy).HasMaxLength(256);
|
|
34742
|
-
builder.Property(e => e.DeletedBy).HasMaxLength(256);
|
|
34743
34702
|
|
|
34744
34703
|
{{#if baseEntity}}
|
|
34745
|
-
//
|
|
34746
|
-
// RELATIONSHIPS
|
|
34747
|
-
// ============================================
|
|
34748
|
-
|
|
34749
|
-
// Relationship to {{baseEntity}} (1:1)
|
|
34704
|
+
// Relationship to {{baseEntity}}
|
|
34750
34705
|
builder.HasOne(e => e.{{baseEntity}})
|
|
34751
|
-
.
|
|
34752
|
-
.HasForeignKey
|
|
34753
|
-
.OnDelete(DeleteBehavior.
|
|
34706
|
+
.WithMany()
|
|
34707
|
+
.HasForeignKey(e => e.{{baseEntity}}Id)
|
|
34708
|
+
.OnDelete(DeleteBehavior.Restrict);
|
|
34754
34709
|
|
|
34755
|
-
// Index on foreign key
|
|
34756
34710
|
builder.HasIndex(e => e.{{baseEntity}}Id)
|
|
34757
|
-
.
|
|
34711
|
+
.HasDatabaseName("IX_{{tablePrefix}}{{name}}s_{{baseEntity}}Id");
|
|
34758
34712
|
{{/if}}
|
|
34759
34713
|
|
|
34760
|
-
//
|
|
34761
|
-
// QUERY FILTERS
|
|
34762
|
-
// ============================================
|
|
34763
|
-
// Note: Global query filters are applied in DbContext.OnModelCreating
|
|
34764
|
-
// - Soft delete: .HasQueryFilter(e => !e.IsDeleted)
|
|
34765
|
-
{{#unless isSystemEntity}}
|
|
34766
|
-
// - Tenant isolation: .HasQueryFilter(e => e.TenantId == _tenantId)
|
|
34767
|
-
// Combined filter applied via ITenantEntity interface check
|
|
34768
|
-
{{/unless}}
|
|
34769
|
-
|
|
34770
|
-
// TODO: Add additional business-specific configuration
|
|
34714
|
+
// TODO: Add business property configurations (HasMaxLength, IsRequired, indexes)
|
|
34771
34715
|
}
|
|
34772
34716
|
}
|
|
34773
34717
|
`;
|
|
@@ -34813,11 +34757,11 @@ public class {{name}}Configuration : IEntityTypeConfiguration<{{name}}>
|
|
|
34813
34757
|
result.instructions.push("Create migration:");
|
|
34814
34758
|
result.instructions.push(`dotnet ef migrations add ${migrationPrefix}_vX.X.X_XXX_Add${name} --context ${dbContextName}`);
|
|
34815
34759
|
result.instructions.push("");
|
|
34816
|
-
result.instructions.push("
|
|
34817
|
-
result.instructions.push(`- Id (Guid),
|
|
34818
|
-
result.instructions.push("-
|
|
34819
|
-
result.instructions.push("-
|
|
34820
|
-
result.instructions.push("-
|
|
34760
|
+
result.instructions.push("BaseEntity fields (inherited):");
|
|
34761
|
+
result.instructions.push(`- Id (Guid), CreatedAt (DateTime), UpdatedAt (DateTime?)`);
|
|
34762
|
+
result.instructions.push(`${isSystemEntity ? "" : "- TenantId (Guid) \u2014 from ITenantEntity"}`);
|
|
34763
|
+
result.instructions.push("- CreatedBy, UpdatedBy (string?) \u2014 from IAuditableEntity");
|
|
34764
|
+
result.instructions.push("- Add your own business properties (Code, Name, etc.) as needed");
|
|
34821
34765
|
if (options?.withSeedData) {
|
|
34822
34766
|
result.instructions.push("");
|
|
34823
34767
|
result.instructions.push("### Seed Data");
|
|
@@ -34889,9 +34833,7 @@ public static class {{name}}SeedData
|
|
|
34889
34833
|
{{#unless isSystemEntity}}
|
|
34890
34834
|
TenantId = (Guid?)null, // Seed data systeme sans tenant
|
|
34891
34835
|
{{/unless}}
|
|
34892
|
-
Code = ExampleCode,
|
|
34893
34836
|
// TODO: Ajouter les proprietes specifiques
|
|
34894
|
-
IsDeleted = false,
|
|
34895
34837
|
CreatedAt = seedDate
|
|
34896
34838
|
}
|
|
34897
34839
|
};
|
|
@@ -35116,11 +35058,8 @@ async function scaffoldController(name, options, structure, config2, result, dry
|
|
|
35116
35058
|
const namespace = options?.namespace || (hierarchy.controllerArea ? `${config2.conventions.namespaces.api}.Controllers.${hierarchy.controllerArea}` : `${config2.conventions.namespaces.api}.Controllers`);
|
|
35117
35059
|
const navRoute = options?.navRoute;
|
|
35118
35060
|
const navRouteSuffix = options?.navRouteSuffix;
|
|
35119
|
-
const
|
|
35120
|
-
const
|
|
35121
|
-
[NavRoute("${navRoute}", Suffix = "${navRouteSuffix}")]` : `[Route("${apiRoute}")]
|
|
35122
|
-
[NavRoute("${navRoute}")]` : `[Route("api/[controller]")]`;
|
|
35123
|
-
const navRouteUsing = navRoute ? "using SmartStack.Api.Core.Routing;\n" : "";
|
|
35061
|
+
const routeAttribute = navRoute ? navRouteSuffix ? `[NavRoute("${navRoute}", Suffix = "${navRouteSuffix}")]` : `[NavRoute("${navRoute}")]` : `[Route("api/[controller]")]`;
|
|
35062
|
+
const navRouteUsing = navRoute ? "using SmartStack.Api.Routing;\n" : "";
|
|
35124
35063
|
const controllerTemplate = `using Microsoft.AspNetCore.Authorization;
|
|
35125
35064
|
using Microsoft.AspNetCore.Mvc;
|
|
35126
35065
|
using Microsoft.Extensions.Logging;
|
|
@@ -35128,17 +35067,20 @@ ${navRouteUsing}
|
|
|
35128
35067
|
namespace {{namespace}};
|
|
35129
35068
|
|
|
35130
35069
|
/// <summary>
|
|
35131
|
-
/// API controller for {{name}} operations
|
|
35070
|
+
/// API controller for {{name}} operations.
|
|
35071
|
+
/// IMPORTANT: Use [RequirePermission] on each endpoint for RBAC enforcement.
|
|
35132
35072
|
/// </summary>
|
|
35133
35073
|
[ApiController]
|
|
35134
35074
|
{{routeAttribute}}
|
|
35135
35075
|
[Authorize]
|
|
35136
35076
|
public class {{name}}Controller : ControllerBase
|
|
35137
35077
|
{
|
|
35078
|
+
private readonly I{{name}}Service _service;
|
|
35138
35079
|
private readonly ILogger<{{name}}Controller> _logger;
|
|
35139
35080
|
|
|
35140
|
-
public {{name}}Controller(ILogger<{{name}}Controller> logger)
|
|
35081
|
+
public {{name}}Controller(I{{name}}Service service, ILogger<{{name}}Controller> logger)
|
|
35141
35082
|
{
|
|
35083
|
+
_service = service;
|
|
35142
35084
|
_logger = logger;
|
|
35143
35085
|
}
|
|
35144
35086
|
|
|
@@ -35146,21 +35088,21 @@ public class {{name}}Controller : ControllerBase
|
|
|
35146
35088
|
/// Get all {{nameLower}}s
|
|
35147
35089
|
/// </summary>
|
|
35148
35090
|
[HttpGet]
|
|
35149
|
-
|
|
35091
|
+
// TODO: Add [RequirePermission(Permissions.{Module}.Read)] for RBAC
|
|
35092
|
+
public async Task<ActionResult<IEnumerable<object>>> GetAll(CancellationToken ct)
|
|
35150
35093
|
{
|
|
35151
|
-
|
|
35152
|
-
|
|
35153
|
-
return Ok(Array.Empty<{{name}}Dto>());
|
|
35094
|
+
// TODO: Call _service.GetAllAsync(ct)
|
|
35095
|
+
return Ok(Array.Empty<object>());
|
|
35154
35096
|
}
|
|
35155
35097
|
|
|
35156
35098
|
/// <summary>
|
|
35157
35099
|
/// Get {{nameLower}} by ID
|
|
35158
35100
|
/// </summary>
|
|
35159
35101
|
[HttpGet("{id:guid}")]
|
|
35160
|
-
|
|
35102
|
+
// TODO: Add [RequirePermission(Permissions.{Module}.Read)] for RBAC
|
|
35103
|
+
public async Task<ActionResult<object>> GetById(Guid id, CancellationToken ct)
|
|
35161
35104
|
{
|
|
35162
|
-
|
|
35163
|
-
// TODO: Implement
|
|
35105
|
+
// TODO: Call _service.GetByIdAsync(id, ct)
|
|
35164
35106
|
return NotFound();
|
|
35165
35107
|
}
|
|
35166
35108
|
|
|
@@ -35168,10 +35110,10 @@ public class {{name}}Controller : ControllerBase
|
|
|
35168
35110
|
/// Create new {{nameLower}}
|
|
35169
35111
|
/// </summary>
|
|
35170
35112
|
[HttpPost]
|
|
35171
|
-
|
|
35113
|
+
// TODO: Add [RequirePermission(Permissions.{Module}.Create)] for RBAC
|
|
35114
|
+
public async Task<ActionResult<object>> Create([FromBody] object request, CancellationToken ct)
|
|
35172
35115
|
{
|
|
35173
|
-
|
|
35174
|
-
// TODO: Implement
|
|
35116
|
+
// TODO: Call _service.CreateAsync(dto, ct)
|
|
35175
35117
|
return CreatedAtAction(nameof(GetById), new { id = Guid.NewGuid() }, null);
|
|
35176
35118
|
}
|
|
35177
35119
|
|
|
@@ -35179,10 +35121,10 @@ public class {{name}}Controller : ControllerBase
|
|
|
35179
35121
|
/// Update {{nameLower}}
|
|
35180
35122
|
/// </summary>
|
|
35181
35123
|
[HttpPut("{id:guid}")]
|
|
35182
|
-
|
|
35124
|
+
// TODO: Add [RequirePermission(Permissions.{Module}.Update)] for RBAC
|
|
35125
|
+
public async Task<ActionResult> Update(Guid id, [FromBody] object request, CancellationToken ct)
|
|
35183
35126
|
{
|
|
35184
|
-
|
|
35185
|
-
// TODO: Implement
|
|
35127
|
+
// TODO: Call _service.UpdateAsync(id, dto, ct)
|
|
35186
35128
|
return NoContent();
|
|
35187
35129
|
}
|
|
35188
35130
|
|
|
@@ -35190,18 +35132,13 @@ public class {{name}}Controller : ControllerBase
|
|
|
35190
35132
|
/// Delete {{nameLower}}
|
|
35191
35133
|
/// </summary>
|
|
35192
35134
|
[HttpDelete("{id:guid}")]
|
|
35193
|
-
|
|
35135
|
+
// TODO: Add [RequirePermission(Permissions.{Module}.Delete)] for RBAC
|
|
35136
|
+
public async Task<ActionResult> Delete(Guid id, CancellationToken ct)
|
|
35194
35137
|
{
|
|
35195
|
-
|
|
35196
|
-
// TODO: Implement
|
|
35138
|
+
// TODO: Call _service.DeleteAsync(id, ct)
|
|
35197
35139
|
return NoContent();
|
|
35198
35140
|
}
|
|
35199
35141
|
}
|
|
35200
|
-
|
|
35201
|
-
// DTOs
|
|
35202
|
-
public record {{name}}Dto(Guid Id, DateTime CreatedAt);
|
|
35203
|
-
public record Create{{name}}Request();
|
|
35204
|
-
public record Update{{name}}Request();
|
|
35205
35142
|
`;
|
|
35206
35143
|
const context = {
|
|
35207
35144
|
namespace,
|
|
@@ -35221,14 +35158,12 @@ public record Update{{name}}Request();
|
|
|
35221
35158
|
}
|
|
35222
35159
|
result.files.push({ path: controllerFilePath, content: controllerContent, type: "created" });
|
|
35223
35160
|
if (navRoute) {
|
|
35224
|
-
result.instructions.push("Controller created with NavRoute
|
|
35161
|
+
result.instructions.push("Controller created with NavRoute (route resolved from DB at startup).");
|
|
35225
35162
|
result.instructions.push(`NavRoute: ${navRoute}${navRouteSuffix ? ` (Suffix: ${navRouteSuffix})` : ""}`);
|
|
35226
|
-
result.instructions.push(`API Route: ${apiRoute}`);
|
|
35227
35163
|
result.instructions.push("");
|
|
35228
|
-
result.instructions.push("
|
|
35229
|
-
result.instructions.push("
|
|
35230
|
-
result.instructions.push(
|
|
35231
|
-
result.instructions.push(` Context > Application > Module > Section matching "${navRoute}"`);
|
|
35164
|
+
result.instructions.push("NavRoute resolves API routes from Navigation entities in the database.");
|
|
35165
|
+
result.instructions.push("Ensure the navigation path exists (seed data required):");
|
|
35166
|
+
result.instructions.push(` Context > Application > Module matching "${navRoute}"`);
|
|
35232
35167
|
} else {
|
|
35233
35168
|
result.instructions.push("Controller created with traditional routing.");
|
|
35234
35169
|
result.instructions.push("");
|
|
@@ -35416,6 +35351,27 @@ export function use{{name}}(options: Use{{name}}Options = {}) {
|
|
|
35416
35351
|
const componentImportPath = hierarchy.context && hierarchy.module ? `@/components/${hierarchy.context.toLowerCase()}/${hierarchy.module.toLowerCase()}/${name}` : `./components/${name}`;
|
|
35417
35352
|
result.instructions.push(`import { ${name} } from '${componentImportPath}';`);
|
|
35418
35353
|
result.instructions.push(`import { use${name} } from '@/hooks/use${name}';`);
|
|
35354
|
+
result.instructions.push("");
|
|
35355
|
+
result.instructions.push("### IMPORTANT: Foreign Key Fields in Forms");
|
|
35356
|
+
result.instructions.push("If this entity has FK fields (e.g., EmployeeId, DepartmentId):");
|
|
35357
|
+
result.instructions.push("- NEVER render FK Guid fields as plain text inputs");
|
|
35358
|
+
result.instructions.push("- ALWAYS use EntityLookup component from @/components/ui/EntityLookup");
|
|
35359
|
+
result.instructions.push("- EntityLookup provides searchable dropdown with API-backed search");
|
|
35360
|
+
result.instructions.push("- See smartstack-frontend.md section 6 for the full EntityLookup pattern");
|
|
35361
|
+
result.instructions.push("");
|
|
35362
|
+
result.instructions.push("Example:");
|
|
35363
|
+
result.instructions.push("```tsx");
|
|
35364
|
+
result.instructions.push('import { EntityLookup } from "@/components/ui/EntityLookup";');
|
|
35365
|
+
result.instructions.push("");
|
|
35366
|
+
result.instructions.push("<EntityLookup");
|
|
35367
|
+
result.instructions.push(' apiEndpoint="/api/{context}/{app}/{related-entity}"');
|
|
35368
|
+
result.instructions.push(" value={formData.relatedEntityId}");
|
|
35369
|
+
result.instructions.push(' onChange={(id) => handleChange("relatedEntityId", id)}');
|
|
35370
|
+
result.instructions.push(' label="Related Entity"');
|
|
35371
|
+
result.instructions.push(" mapOption={(item) => ({ id: item.id, label: item.name, sublabel: item.code })}");
|
|
35372
|
+
result.instructions.push(" required");
|
|
35373
|
+
result.instructions.push("/>");
|
|
35374
|
+
result.instructions.push("```");
|
|
35419
35375
|
}
|
|
35420
35376
|
async function scaffoldTest(name, options, structure, config2, result, dryRun = false) {
|
|
35421
35377
|
const isSystemEntity = options?.isSystemEntity || false;
|
|
@@ -35810,7 +35766,7 @@ public interface I{{name}}Repository
|
|
|
35810
35766
|
/// <summary>Update entity</summary>
|
|
35811
35767
|
Task UpdateAsync({{name}} entity, CancellationToken ct = default);
|
|
35812
35768
|
|
|
35813
|
-
/// <summary>
|
|
35769
|
+
/// <summary>Delete entity</summary>
|
|
35814
35770
|
Task DeleteAsync({{name}} entity, CancellationToken ct = default);
|
|
35815
35771
|
}
|
|
35816
35772
|
`;
|
|
@@ -35885,8 +35841,8 @@ public class {{name}}Repository : I{{name}}Repository
|
|
|
35885
35841
|
/// <inheritdoc />
|
|
35886
35842
|
public async Task DeleteAsync({{name}} entity, CancellationToken ct = default)
|
|
35887
35843
|
{
|
|
35888
|
-
|
|
35889
|
-
await
|
|
35844
|
+
_context.{{name}}s.Remove(entity);
|
|
35845
|
+
await _context.SaveChangesAsync(ct);
|
|
35890
35846
|
}
|
|
35891
35847
|
}
|
|
35892
35848
|
`;
|
|
@@ -57054,7 +57010,12 @@ async function scaffoldRoutes(input, config2) {
|
|
|
57054
57010
|
result.instructions.push("");
|
|
57055
57011
|
result.instructions.push("Add routes to `contextRoutes.{context}[]` with **RELATIVE** paths (no leading `/`):");
|
|
57056
57012
|
result.instructions.push("");
|
|
57013
|
+
result.instructions.push("**IMPORTANT:** Pages are lazy-loaded. Use `<Suspense fallback={<PageLoader />}>` wrapper.");
|
|
57014
|
+
result.instructions.push("");
|
|
57057
57015
|
result.instructions.push("```tsx");
|
|
57016
|
+
result.instructions.push("import { lazy, Suspense } from 'react';");
|
|
57017
|
+
result.instructions.push("import { PageLoader } from '@/components/ui/PageLoader';");
|
|
57018
|
+
result.instructions.push("");
|
|
57058
57019
|
result.instructions.push("const contextRoutes: ContextRouteExtensions = {");
|
|
57059
57020
|
for (const [context, applications] of Object.entries(routeTree)) {
|
|
57060
57021
|
result.instructions.push(` ${context}: [`);
|
|
@@ -57063,7 +57024,7 @@ async function scaffoldRoutes(input, config2) {
|
|
|
57063
57024
|
const modulePath = route.navRoute.split(".").slice(1).join("/");
|
|
57064
57025
|
const pageEntry = pageFiles.get(route.navRoute);
|
|
57065
57026
|
const component = pageEntry?.[0]?.componentName || `${route.navRoute.split(".").map(capitalize).join("")}Page`;
|
|
57066
|
-
result.instructions.push(` { path: '${modulePath}', element:
|
|
57027
|
+
result.instructions.push(` { path: '${modulePath}', element: <Suspense fallback={<PageLoader />}><${component} /></Suspense> },`);
|
|
57067
57028
|
}
|
|
57068
57029
|
}
|
|
57069
57030
|
result.instructions.push(" ],");
|
|
@@ -57076,7 +57037,8 @@ async function scaffoldRoutes(input, config2) {
|
|
|
57076
57037
|
result.instructions.push("");
|
|
57077
57038
|
result.instructions.push('### Pattern B: JSX Routes (if App.tsx uses `<Route path="/{context}" element={<{Layout} />}>`)');
|
|
57078
57039
|
result.instructions.push("");
|
|
57079
|
-
result.instructions.push("Insert `<Route>` children INSIDE the appropriate Layout wrapper
|
|
57040
|
+
result.instructions.push("Insert `<Route>` children INSIDE the appropriate Layout wrapper.");
|
|
57041
|
+
result.instructions.push("**IMPORTANT:** Use `<Suspense fallback={<PageLoader />}>` for lazy-loaded pages.");
|
|
57080
57042
|
result.instructions.push("");
|
|
57081
57043
|
for (const [context, applications] of Object.entries(routeTree)) {
|
|
57082
57044
|
const layoutName = getLayoutName(context);
|
|
@@ -57088,7 +57050,7 @@ async function scaffoldRoutes(input, config2) {
|
|
|
57088
57050
|
const modulePath = route.navRoute.split(".").slice(1).join("/");
|
|
57089
57051
|
const pageEntry = pageFiles.get(route.navRoute);
|
|
57090
57052
|
const component = pageEntry?.[0]?.componentName || `${route.navRoute.split(".").map(capitalize).join("")}Page`;
|
|
57091
|
-
result.instructions.push(`<Route path="${modulePath}" element={
|
|
57053
|
+
result.instructions.push(`<Route path="${modulePath}" element={<Suspense fallback={<PageLoader />}><${component} /></Suspense>} />`);
|
|
57092
57054
|
}
|
|
57093
57055
|
}
|
|
57094
57056
|
result.instructions.push("```");
|
|
@@ -57327,27 +57289,30 @@ function generateRouterConfig(routes, includeGuards) {
|
|
|
57327
57289
|
" */",
|
|
57328
57290
|
"",
|
|
57329
57291
|
"import { createBrowserRouter, RouteObject } from 'react-router-dom';",
|
|
57330
|
-
"import {
|
|
57292
|
+
"import { lazy, Suspense } from 'react';",
|
|
57293
|
+
"import { ROUTES } from './navRoutes.generated';",
|
|
57294
|
+
"import { PageLoader } from '@/components/ui/PageLoader';"
|
|
57331
57295
|
];
|
|
57332
57296
|
if (includeGuards) {
|
|
57333
57297
|
lines.push("import { ProtectedRoute, PermissionGuard } from './guards';");
|
|
57334
57298
|
}
|
|
57335
57299
|
const contexts = Object.keys(routeTree);
|
|
57336
57300
|
for (const context of contexts) {
|
|
57337
|
-
|
|
57301
|
+
const name = `${capitalize(context)}Layout`;
|
|
57302
|
+
lines.push(`const ${name} = lazy(() => import('../layouts/${name}').then(m => ({ default: m.${name} })));`);
|
|
57338
57303
|
}
|
|
57339
57304
|
lines.push("");
|
|
57340
|
-
lines.push("// Page imports - customize
|
|
57305
|
+
lines.push("// Page imports - lazy loaded (customize paths)");
|
|
57341
57306
|
for (const route of routes) {
|
|
57342
57307
|
const pageName = route.navRoute.split(".").map(capitalize).join("");
|
|
57343
|
-
lines.push(`//
|
|
57308
|
+
lines.push(`// const ${pageName}Page = lazy(() => import('../pages/${pageName}Page').then(m => ({ default: m.${pageName}Page })));`);
|
|
57344
57309
|
}
|
|
57345
57310
|
lines.push("");
|
|
57346
57311
|
lines.push("const routes: RouteObject[] = [");
|
|
57347
57312
|
for (const [context, applications] of Object.entries(routeTree)) {
|
|
57348
57313
|
lines.push(" {");
|
|
57349
57314
|
lines.push(` path: '${context}',`);
|
|
57350
|
-
lines.push(` element:
|
|
57315
|
+
lines.push(` element: <Suspense fallback={<PageLoader />}><${capitalize(context)}Layout /></Suspense>,`);
|
|
57351
57316
|
lines.push(" children: [");
|
|
57352
57317
|
for (const [app, modules] of Object.entries(applications)) {
|
|
57353
57318
|
lines.push(" {");
|
|
@@ -57575,7 +57540,9 @@ function generateClientRoutesConfig(routes, pageFiles, includeGuards) {
|
|
|
57575
57540
|
"",
|
|
57576
57541
|
"import type { RouteObject } from 'react-router-dom';",
|
|
57577
57542
|
"import type { ContextRouteExtensions } from '@atlashub/smartstack';",
|
|
57578
|
-
"import {
|
|
57543
|
+
"import { lazy, Suspense } from 'react';",
|
|
57544
|
+
"import { Navigate } from 'react-router-dom';",
|
|
57545
|
+
"import { PageLoader } from '@/components/ui/PageLoader';"
|
|
57579
57546
|
];
|
|
57580
57547
|
if (includeGuards) {
|
|
57581
57548
|
lines.push("import { ROUTES } from './navRoutes.generated';");
|
|
@@ -57588,7 +57555,9 @@ function generateClientRoutesConfig(routes, pageFiles, includeGuards) {
|
|
|
57588
57555
|
if (pageEntries) {
|
|
57589
57556
|
for (const entry of pageEntries) {
|
|
57590
57557
|
if (!importedComponents.has(entry.componentName)) {
|
|
57591
|
-
lines.push(`
|
|
57558
|
+
lines.push(`const ${entry.componentName} = lazy(() =>`);
|
|
57559
|
+
lines.push(` import('${entry.importPath}').then(m => ({ default: m.${entry.componentName} }))`);
|
|
57560
|
+
lines.push(");");
|
|
57592
57561
|
importedComponents.add(entry.componentName);
|
|
57593
57562
|
}
|
|
57594
57563
|
}
|
|
@@ -57597,7 +57566,7 @@ function generateClientRoutesConfig(routes, pageFiles, includeGuards) {
|
|
|
57597
57566
|
for (const route of routes) {
|
|
57598
57567
|
if (!pageFiles.has(route.navRoute)) {
|
|
57599
57568
|
const pageName = route.navRoute.split(".").map(capitalize).join("") + "Page";
|
|
57600
|
-
lines.push(`// TODO:
|
|
57569
|
+
lines.push(`// TODO: const ${pageName} = lazy(() => import('@/pages/...').then(m => ({ default: m.${pageName} })));`);
|
|
57601
57570
|
}
|
|
57602
57571
|
}
|
|
57603
57572
|
lines.push("");
|
|
@@ -57626,7 +57595,7 @@ function generateClientRoutesConfig(routes, pageFiles, includeGuards) {
|
|
|
57626
57595
|
lines.push(` {`);
|
|
57627
57596
|
lines.push(` path: '${modulePath}',`);
|
|
57628
57597
|
if (hasRealPage) {
|
|
57629
|
-
lines.push(` element: <PermissionGuard permissions={ROUTES['${route.navRoute}'].permissions}><${component} /></PermissionGuard>,`);
|
|
57598
|
+
lines.push(` element: <Suspense fallback={<PageLoader />}><PermissionGuard permissions={ROUTES['${route.navRoute}'].permissions}><${component} /></PermissionGuard></Suspense>,`);
|
|
57630
57599
|
} else {
|
|
57631
57600
|
lines.push(` element: <PermissionGuard permissions={ROUTES['${route.navRoute}'].permissions}><div>TODO: ${component}</div></PermissionGuard>,`);
|
|
57632
57601
|
}
|
|
@@ -57634,7 +57603,7 @@ function generateClientRoutesConfig(routes, pageFiles, includeGuards) {
|
|
|
57634
57603
|
} else {
|
|
57635
57604
|
lines.push(` {`);
|
|
57636
57605
|
lines.push(` path: '${modulePath}',`);
|
|
57637
|
-
lines.push(` element: ${hasRealPage ?
|
|
57606
|
+
lines.push(` element: ${hasRealPage ? `<Suspense fallback={<PageLoader />}><${component} /></Suspense>` : `<div>TODO: ${component}</div>`},`);
|
|
57638
57607
|
lines.push(` },`);
|
|
57639
57608
|
}
|
|
57640
57609
|
}
|
|
@@ -57651,7 +57620,7 @@ function generateClientRoutesConfig(routes, pageFiles, includeGuards) {
|
|
|
57651
57620
|
lines.push(` {`);
|
|
57652
57621
|
lines.push(` path: '${fullPath}',`);
|
|
57653
57622
|
if (hasRealPage) {
|
|
57654
|
-
lines.push(` element: <PermissionGuard permissions={ROUTES['${route.navRoute}'].permissions}><${component} /></PermissionGuard>,`);
|
|
57623
|
+
lines.push(` element: <Suspense fallback={<PageLoader />}><PermissionGuard permissions={ROUTES['${route.navRoute}'].permissions}><${component} /></PermissionGuard></Suspense>,`);
|
|
57655
57624
|
} else {
|
|
57656
57625
|
lines.push(` element: <PermissionGuard permissions={ROUTES['${route.navRoute}'].permissions}><div>TODO: ${component}</div></PermissionGuard>,`);
|
|
57657
57626
|
}
|
|
@@ -57659,7 +57628,7 @@ function generateClientRoutesConfig(routes, pageFiles, includeGuards) {
|
|
|
57659
57628
|
} else {
|
|
57660
57629
|
lines.push(` {`);
|
|
57661
57630
|
lines.push(` path: '${fullPath}',`);
|
|
57662
|
-
lines.push(` element: ${hasRealPage ?
|
|
57631
|
+
lines.push(` element: ${hasRealPage ? `<Suspense fallback={<PageLoader />}><${component} /></Suspense>` : `<div>TODO: ${component}</div>`},`);
|
|
57663
57632
|
lines.push(` },`);
|
|
57664
57633
|
}
|
|
57665
57634
|
}
|