@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.
Files changed (34) hide show
  1. package/dist/mcp-entry.mjs +143 -174
  2. package/dist/mcp-entry.mjs.map +1 -1
  3. package/package.json +1 -1
  4. package/templates/mcp-scaffolding/component.tsx.hbs +21 -1
  5. package/templates/skills/apex/SKILL.md +21 -0
  6. package/templates/skills/apex/references/smartstack-api.md +507 -0
  7. package/templates/skills/apex/references/smartstack-frontend.md +1081 -0
  8. package/templates/skills/apex/references/smartstack-layers.md +166 -20
  9. package/templates/skills/apex/steps/step-00-init.md +27 -14
  10. package/templates/skills/apex/steps/step-01-analyze.md +45 -3
  11. package/templates/skills/apex/steps/step-02-plan.md +5 -1
  12. package/templates/skills/apex/steps/step-03-execute.md +51 -9
  13. package/templates/skills/apex/steps/step-04-validate.md +251 -0
  14. package/templates/skills/apex/steps/step-05-examine.md +7 -0
  15. package/templates/skills/apex/steps/step-07-tests.md +48 -5
  16. package/templates/skills/business-analyse/_shared.md +6 -6
  17. package/templates/skills/business-analyse/patterns/suggestion-catalog.md +1 -1
  18. package/templates/skills/business-analyse/questionnaire/07-ui.md +3 -3
  19. package/templates/skills/business-analyse/references/cadrage-pre-analysis.md +1 -1
  20. package/templates/skills/business-analyse/references/entity-architecture-decision.md +3 -3
  21. package/templates/skills/business-analyse/references/handoff-file-templates.md +13 -5
  22. package/templates/skills/business-analyse/references/spec-auto-inference.md +14 -14
  23. package/templates/skills/business-analyse/steps/step-01-cadrage.md +2 -2
  24. package/templates/skills/business-analyse/steps/step-02-decomposition.md +1 -1
  25. package/templates/skills/business-analyse/steps/step-03a1-setup.md +2 -2
  26. package/templates/skills/business-analyse/steps/step-03b-ui.md +2 -1
  27. package/templates/skills/business-analyse/steps/step-05a-handoff.md +15 -4
  28. package/templates/skills/business-analyse/templates/tpl-frd.md +2 -2
  29. package/templates/skills/business-analyse/templates-frd.md +2 -2
  30. package/templates/skills/ralph-loop/references/category-rules.md +45 -7
  31. package/templates/skills/ralph-loop/references/compact-loop.md +2 -2
  32. package/templates/skills/ralph-loop/references/core-seed-data.md +10 -0
  33. package/templates/skills/ralph-loop/steps/step-02-execute.md +110 -1
  34. package/templates/skills/validate-feature/steps/step-05-db-validation.md +86 -1
@@ -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: "error",
26909
+ type: severity,
26906
26910
  category: "namespaces",
26907
- message: `${layer.name} file has incorrect namespace "${namespace}"`,
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(ILogger<{{name}}Service> logger)
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
- /// System entity: platform-level, no tenant isolation
34593
+ /// Platform-level entity: no tenant isolation
34574
34594
  {{/unless}}
34575
34595
  /// </summary>
34576
- {{#if isSystemEntity}}
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 virtual {{baseEntity}}? {{baseEntity}} { get; private set; }
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
- {{#if isSystemEntity}}
34640
+ {{#unless isSystemEntity}}
34641
+ /// <param name="tenantId">Required tenant identifier</param>
34617
34642
  public static {{name}} Create(
34618
- string code,
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
- Code = code.ToLowerInvariant(),
34625
- CreatedAt = DateTime.UtcNow,
34626
- CreatedBy = createdBy
34651
+ TenantId = tenantId,
34652
+ CreatedAt = DateTime.UtcNow
34627
34653
  };
34628
34654
  }
34629
34655
  {{else}}
34630
- /// <param name="tenantId">Required tenant identifier</param>
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
- TenantId = tenantId,
34645
- Code = code.ToLowerInvariant(),
34646
- CreatedAt = DateTime.UtcNow,
34647
- CreatedBy = createdBy
34661
+ CreatedAt = DateTime.UtcNow
34648
34662
  };
34649
34663
  }
34650
- {{/if}}
34664
+ {{/unless}}
34651
34665
 
34652
34666
  /// <summary>
34653
34667
  /// Update the entity
34654
34668
  /// </summary>
34655
- public void Update(string? updatedBy = null)
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
- .WithOne()
34752
- .HasForeignKey<{{name}}>(e => e.{{baseEntity}}Id)
34753
- .OnDelete(DeleteBehavior.Cascade);
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
- .IsUnique();
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("Required fields from BaseEntity:");
34817
- result.instructions.push(`- Id (Guid), ${isSystemEntity ? "" : "TenantId (Guid), "}Code (string, lowercase)`);
34818
- result.instructions.push("- CreatedAt, UpdatedAt, CreatedBy, UpdatedBy (audit)");
34819
- result.instructions.push("- IsDeleted, DeletedAt, DeletedBy (soft delete)");
34820
- result.instructions.push("- RowVersion (concurrency)");
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 apiRoute = navRoute ? `api/${navRoute.replace(/\./g, "/")}${navRouteSuffix ? `/${navRouteSuffix}` : ""}` : null;
35120
- const routeAttribute = navRoute ? navRouteSuffix ? `[Route("${apiRoute}")]
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
- public async Task<ActionResult<IEnumerable<{{name}}Dto>>> GetAll()
35091
+ // TODO: Add [RequirePermission(Permissions.{Module}.Read)] for RBAC
35092
+ public async Task<ActionResult<IEnumerable<object>>> GetAll(CancellationToken ct)
35150
35093
  {
35151
- _logger.LogInformation("Getting all {{nameLower}}s");
35152
- // TODO: Implement
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
- public async Task<ActionResult<{{name}}Dto>> GetById(Guid id)
35102
+ // TODO: Add [RequirePermission(Permissions.{Module}.Read)] for RBAC
35103
+ public async Task<ActionResult<object>> GetById(Guid id, CancellationToken ct)
35161
35104
  {
35162
- _logger.LogInformation("Getting {{nameLower}} {Id}", id);
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
- public async Task<ActionResult<{{name}}Dto>> Create([FromBody] Create{{name}}Request request)
35113
+ // TODO: Add [RequirePermission(Permissions.{Module}.Create)] for RBAC
35114
+ public async Task<ActionResult<object>> Create([FromBody] object request, CancellationToken ct)
35172
35115
  {
35173
- _logger.LogInformation("Creating {{nameLower}}");
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
- public async Task<ActionResult> Update(Guid id, [FromBody] Update{{name}}Request request)
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
- _logger.LogInformation("Updating {{nameLower}} {Id}", id);
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
- public async Task<ActionResult> Delete(Guid id)
35135
+ // TODO: Add [RequirePermission(Permissions.{Module}.Delete)] for RBAC
35136
+ public async Task<ActionResult> Delete(Guid id, CancellationToken ct)
35194
35137
  {
35195
- _logger.LogInformation("Deleting {{nameLower}} {Id}", id);
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 + explicit Route (deterministic routing).");
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("The [Route] attribute ensures deterministic API routing.");
35229
- result.instructions.push("The [NavRoute] attribute integrates with the navigation/permission system.");
35230
- result.instructions.push("Ensure the navigation path exists in the database:");
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>Soft delete entity</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
- entity.SoftDelete();
35889
- await UpdateAsync(entity, ct);
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: <${component} /> },`);
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={<${component} />} />`);
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 { ROUTES } from './navRoutes.generated';"
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
- lines.push(`import { ${capitalize(context)}Layout } from '../layouts/${capitalize(context)}Layout';`);
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 these paths");
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(`// import { ${pageName}Page } from '../pages/${pageName}Page';`);
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: <${capitalize(context)}Layout />,`);
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 { Navigate } from 'react-router-dom';"
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(`import { ${entry.componentName} } from '${entry.importPath}';`);
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: import { ${pageName} } from '@/pages/...';`);
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 ? `<${component} />` : `<div>TODO: ${component}</div>`},`);
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 ? `<${component} />` : `<div>TODO: ${component}</div>`},`);
57631
+ lines.push(` element: ${hasRealPage ? `<Suspense fallback={<PageLoader />}><${component} /></Suspense>` : `<div>TODO: ${component}</div>`},`);
57663
57632
  lines.push(` },`);
57664
57633
  }
57665
57634
  }