@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/package.json
CHANGED
|
@@ -174,7 +174,27 @@ export const {{name}}: React.FC<{{name}}Props> = ({
|
|
|
174
174
|
|
|
175
175
|
{/* Form */}
|
|
176
176
|
<form onSubmit={handleSubmit} className="p-6 space-y-6">
|
|
177
|
-
{/*
|
|
177
|
+
{/*
|
|
178
|
+
TODO: Add form fields based on entity properties.
|
|
179
|
+
IMPORTANT — Field type mapping:
|
|
180
|
+
- string properties → <input type="text" />
|
|
181
|
+
- bool properties → <input type="checkbox" />
|
|
182
|
+
- number properties → <input type="number" />
|
|
183
|
+
- DateTime properties → <input type="date" />
|
|
184
|
+
- Guid FK properties (e.g., EmployeeId, DepartmentId) → <EntityLookup /> (NEVER plain text input!)
|
|
185
|
+
|
|
186
|
+
For FK fields, use EntityLookup from @/components/ui/EntityLookup:
|
|
187
|
+
<EntityLookup
|
|
188
|
+
apiEndpoint="/api/{related-entity-route}"
|
|
189
|
+
value={data.relatedEntityId}
|
|
190
|
+
onChange={(id) => handleChange('relatedEntityId', id)}
|
|
191
|
+
label="Related Entity"
|
|
192
|
+
mapOption={(item) => ({ id: item.id, label: item.name, sublabel: item.code })}
|
|
193
|
+
required
|
|
194
|
+
/>
|
|
195
|
+
|
|
196
|
+
See smartstack-frontend.md section 6 for the full EntityLookup pattern.
|
|
197
|
+
*/}
|
|
178
198
|
<div className="text-[var(--text-secondary)] text-center py-8">
|
|
179
199
|
Add your form fields here
|
|
180
200
|
</div>
|
|
@@ -91,6 +91,7 @@ Execute incremental SmartStack development using the APEX methodology. This skil
|
|
|
91
91
|
|
|
92
92
|
| File | Purpose | Loaded by |
|
|
93
93
|
|------|---------|-----------|
|
|
94
|
+
| `references/smartstack-api.md` | BaseEntity, interfaces, entity/config/controller patterns | step-01, step-03 |
|
|
94
95
|
| `references/smartstack-layers.md` | Layer execution rules, skill/MCP mapping, seed data | step-02, step-03 |
|
|
95
96
|
| `references/agent-teams-protocol.md` | TeamCreate, coordination, shutdown protocol | step-01, step-03 |
|
|
96
97
|
</reference_files>
|
|
@@ -108,6 +109,26 @@ Execute incremental SmartStack development using the APEX methodology. This skil
|
|
|
108
109
|
|
|
109
110
|
</execution_rules>
|
|
110
111
|
|
|
112
|
+
<error_handling>
|
|
113
|
+
|
|
114
|
+
## Error Handling Strategy
|
|
115
|
+
|
|
116
|
+
| Error | Action | Max Retries |
|
|
117
|
+
|-------|--------|-------------|
|
|
118
|
+
| `dotnet build` fails | Identify error, fix via skill/MCP, rebuild | 3 |
|
|
119
|
+
| `npm run typecheck` fails | Fix TypeScript error, retry | 3 |
|
|
120
|
+
| MCP unavailable | Degraded mode: use `smartstack-api.md` as sole reference, no MCP validation | — |
|
|
121
|
+
| File lock (MSB3021) | Auto-use `--output /tmp/{project}_build` for build verification | — |
|
|
122
|
+
| NuGet restore required | Run `dotnet restore` before first `dotnet build` | 1 |
|
|
123
|
+
| Migration fails | Rollback migration (`dotnet ef migrations remove`), fix entity/config, retry | 2 |
|
|
124
|
+
| MCP scaffold output wrong | Verify against `smartstack-api.md` patterns, fix manually | — |
|
|
125
|
+
| POST-CHECK fails | Return to step-03, fix the issue, re-validate | 2 |
|
|
126
|
+
| Stuck after max retries | AskUserQuestion with options: "Try alternative", "Skip", "Discuss" | — |
|
|
127
|
+
|
|
128
|
+
**Principle:** Always fix CODE, never bypass checks. If stuck, escalate to user.
|
|
129
|
+
|
|
130
|
+
</error_handling>
|
|
131
|
+
|
|
111
132
|
<success_criteria>
|
|
112
133
|
- SmartStack context detected (context/app/module)
|
|
113
134
|
- Plan validated with skill/MCP mapped for each file
|
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
# SmartStack Domain API Reference
|
|
2
|
+
|
|
3
|
+
> **Source of truth:** `SmartStack.app/src/SmartStack.Domain/Common/`
|
|
4
|
+
> **Loaded by:** step-01 (analyze), step-03 (execute)
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## BaseEntity
|
|
9
|
+
|
|
10
|
+
```csharp
|
|
11
|
+
namespace SmartStack.Domain.Common;
|
|
12
|
+
|
|
13
|
+
public abstract class BaseEntity
|
|
14
|
+
{
|
|
15
|
+
public Guid Id { get; set; }
|
|
16
|
+
public DateTime CreatedAt { get; set; }
|
|
17
|
+
public DateTime? UpdatedAt { get; set; }
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**ONLY 3 properties.** No Code, no IsDeleted, no RowVersion, no SoftDelete, no CreatedBy/UpdatedBy.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Interfaces
|
|
26
|
+
|
|
27
|
+
### ITenantEntity (mandatory tenant isolation)
|
|
28
|
+
|
|
29
|
+
```csharp
|
|
30
|
+
public interface ITenantEntity
|
|
31
|
+
{
|
|
32
|
+
Guid TenantId { get; }
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### IAuditableEntity (audit trail)
|
|
37
|
+
|
|
38
|
+
```csharp
|
|
39
|
+
public interface IAuditableEntity
|
|
40
|
+
{
|
|
41
|
+
string? CreatedBy { get; set; }
|
|
42
|
+
string? UpdatedBy { get; set; }
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### IOptionalTenantEntity (nullable tenant)
|
|
47
|
+
|
|
48
|
+
```csharp
|
|
49
|
+
public interface IOptionalTenantEntity
|
|
50
|
+
{
|
|
51
|
+
Guid? TenantId { get; }
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### IScopedTenantEntity (tenant + scope visibility)
|
|
56
|
+
|
|
57
|
+
```csharp
|
|
58
|
+
public interface IScopedTenantEntity : IOptionalTenantEntity
|
|
59
|
+
{
|
|
60
|
+
EntityScope Scope { get; }
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### EntityScope enum
|
|
65
|
+
|
|
66
|
+
```csharp
|
|
67
|
+
public enum EntityScope
|
|
68
|
+
{
|
|
69
|
+
Tenant = 0, // Visible only to specific tenant (TenantId required)
|
|
70
|
+
Shared = 1, // Visible to all tenants (TenantId null)
|
|
71
|
+
Platform = 2 // Visible only to platform admins (HasGlobalAccess)
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Entity Pattern (tenant-scoped, most common)
|
|
78
|
+
|
|
79
|
+
```csharp
|
|
80
|
+
using SmartStack.Domain.Common;
|
|
81
|
+
|
|
82
|
+
namespace {ProjectName}.Domain.Entities.{Context}.{App}.{Module};
|
|
83
|
+
|
|
84
|
+
public class {Name} : BaseEntity, ITenantEntity, IAuditableEntity
|
|
85
|
+
{
|
|
86
|
+
// ITenantEntity
|
|
87
|
+
public Guid TenantId { get; private set; }
|
|
88
|
+
|
|
89
|
+
// IAuditableEntity
|
|
90
|
+
public string? CreatedBy { get; set; }
|
|
91
|
+
public string? UpdatedBy { get; set; }
|
|
92
|
+
|
|
93
|
+
// Business properties (add your own)
|
|
94
|
+
public string Code { get; private set; } = null!;
|
|
95
|
+
public string Name { get; private set; } = null!;
|
|
96
|
+
public string? Description { get; private set; }
|
|
97
|
+
public bool IsActive { get; private set; } = true;
|
|
98
|
+
|
|
99
|
+
private {Name}() { }
|
|
100
|
+
|
|
101
|
+
public static {Name} Create(Guid tenantId, string code, string name)
|
|
102
|
+
{
|
|
103
|
+
if (tenantId == Guid.Empty)
|
|
104
|
+
throw new ArgumentException("TenantId is required", nameof(tenantId));
|
|
105
|
+
|
|
106
|
+
return new {Name}
|
|
107
|
+
{
|
|
108
|
+
Id = Guid.NewGuid(),
|
|
109
|
+
TenantId = tenantId,
|
|
110
|
+
Code = code.ToLowerInvariant(),
|
|
111
|
+
Name = name,
|
|
112
|
+
CreatedAt = DateTime.UtcNow
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
public void Update(string name, string? description)
|
|
117
|
+
{
|
|
118
|
+
Name = name;
|
|
119
|
+
Description = description;
|
|
120
|
+
UpdatedAt = DateTime.UtcNow;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Entity Pattern (platform-level, no tenant)
|
|
128
|
+
|
|
129
|
+
```csharp
|
|
130
|
+
public class {Name} : BaseEntity, IAuditableEntity
|
|
131
|
+
{
|
|
132
|
+
public string? CreatedBy { get; set; }
|
|
133
|
+
public string? UpdatedBy { get; set; }
|
|
134
|
+
|
|
135
|
+
// Business properties
|
|
136
|
+
public string Code { get; private set; } = null!;
|
|
137
|
+
public string Name { get; private set; } = null!;
|
|
138
|
+
|
|
139
|
+
private {Name}() { }
|
|
140
|
+
|
|
141
|
+
public static {Name} Create(string code, string name)
|
|
142
|
+
{
|
|
143
|
+
return new {Name}
|
|
144
|
+
{
|
|
145
|
+
Id = Guid.NewGuid(),
|
|
146
|
+
Code = code.ToLowerInvariant(),
|
|
147
|
+
Name = name,
|
|
148
|
+
CreatedAt = DateTime.UtcNow
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## EF Configuration Pattern
|
|
157
|
+
|
|
158
|
+
```csharp
|
|
159
|
+
using Microsoft.EntityFrameworkCore;
|
|
160
|
+
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
|
161
|
+
|
|
162
|
+
public class {Name}Configuration : IEntityTypeConfiguration<{Name}>
|
|
163
|
+
{
|
|
164
|
+
public void Configure(EntityTypeBuilder<{Name}> builder)
|
|
165
|
+
{
|
|
166
|
+
builder.ToTable("{prefix}{Name}s", "{schema}");
|
|
167
|
+
|
|
168
|
+
builder.HasKey(x => x.Id);
|
|
169
|
+
|
|
170
|
+
// Tenant (if ITenantEntity)
|
|
171
|
+
builder.Property(x => x.TenantId).IsRequired();
|
|
172
|
+
builder.HasIndex(x => x.TenantId)
|
|
173
|
+
.HasDatabaseName("IX_{prefix}{Name}s_TenantId");
|
|
174
|
+
|
|
175
|
+
// Business properties
|
|
176
|
+
builder.Property(x => x.Code).HasMaxLength(50).IsRequired();
|
|
177
|
+
builder.Property(x => x.Name).HasMaxLength(100).IsRequired();
|
|
178
|
+
builder.Property(x => x.Description).HasMaxLength(500);
|
|
179
|
+
|
|
180
|
+
// Audit (from IAuditableEntity)
|
|
181
|
+
builder.Property(x => x.CreatedBy).HasMaxLength(256);
|
|
182
|
+
builder.Property(x => x.UpdatedBy).HasMaxLength(256);
|
|
183
|
+
|
|
184
|
+
// Unique indexes
|
|
185
|
+
builder.HasIndex(x => new { x.TenantId, x.Code })
|
|
186
|
+
.IsUnique()
|
|
187
|
+
.HasDatabaseName("IX_{prefix}{Name}s_Tenant_Code");
|
|
188
|
+
|
|
189
|
+
// Relationships
|
|
190
|
+
// builder.HasMany(x => x.Children)
|
|
191
|
+
// .WithOne(x => x.Parent)
|
|
192
|
+
// .HasForeignKey(x => x.ParentId)
|
|
193
|
+
// .OnDelete(DeleteBehavior.Restrict);
|
|
194
|
+
|
|
195
|
+
// Seed data (if applicable)
|
|
196
|
+
// builder.HasData({Name}SeedData.GetSeedData());
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
203
|
+
## Service Pattern (tenant-scoped, MANDATORY)
|
|
204
|
+
|
|
205
|
+
> **CRITICAL:** ALL services MUST inject `ICurrentUser` and filter by `TenantId`. Missing TenantId = OWASP A01 vulnerability.
|
|
206
|
+
|
|
207
|
+
```csharp
|
|
208
|
+
using Microsoft.EntityFrameworkCore;
|
|
209
|
+
using Microsoft.Extensions.Logging;
|
|
210
|
+
using SmartStack.Application.Common.Interfaces.Identity;
|
|
211
|
+
using SmartStack.Application.Common.Interfaces.Persistence;
|
|
212
|
+
|
|
213
|
+
namespace {ProjectName}.Infrastructure.Services.{Context}.{App}.{Module};
|
|
214
|
+
|
|
215
|
+
public class {Name}Service : I{Name}Service
|
|
216
|
+
{
|
|
217
|
+
private readonly IExtensionsDbContext _db;
|
|
218
|
+
private readonly ICurrentUser _currentUser;
|
|
219
|
+
private readonly ILogger<{Name}Service> _logger;
|
|
220
|
+
|
|
221
|
+
public {Name}Service(
|
|
222
|
+
IExtensionsDbContext db,
|
|
223
|
+
ICurrentUser currentUser,
|
|
224
|
+
ILogger<{Name}Service> logger)
|
|
225
|
+
{
|
|
226
|
+
_db = db;
|
|
227
|
+
_currentUser = currentUser;
|
|
228
|
+
_logger = logger;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
public async Task<PaginatedResult<{Name}ResponseDto>> GetAllAsync(
|
|
232
|
+
string? search = null,
|
|
233
|
+
int page = 1,
|
|
234
|
+
int pageSize = 20,
|
|
235
|
+
CancellationToken ct = default)
|
|
236
|
+
{
|
|
237
|
+
var query = _db.{Name}s
|
|
238
|
+
.Where(x => x.TenantId == _currentUser.TenantId) // MANDATORY tenant filter
|
|
239
|
+
.AsNoTracking();
|
|
240
|
+
|
|
241
|
+
// Search filter — enables EntityLookup on frontend
|
|
242
|
+
if (!string.IsNullOrWhiteSpace(search))
|
|
243
|
+
{
|
|
244
|
+
query = query.Where(x =>
|
|
245
|
+
x.Name.Contains(search) ||
|
|
246
|
+
x.Code.Contains(search));
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
var totalCount = await query.CountAsync(ct);
|
|
250
|
+
var items = await query
|
|
251
|
+
.OrderBy(x => x.Name)
|
|
252
|
+
.Skip((page - 1) * pageSize)
|
|
253
|
+
.Take(pageSize)
|
|
254
|
+
.Select(x => new {Name}ResponseDto(x.Id, x.Code, x.Name, x.CreatedAt))
|
|
255
|
+
.ToListAsync(ct);
|
|
256
|
+
|
|
257
|
+
return new PaginatedResult<{Name}ResponseDto>(items, totalCount, page, pageSize);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
public async Task<{Name}ResponseDto?> GetByIdAsync(Guid id, CancellationToken ct)
|
|
261
|
+
{
|
|
262
|
+
return await _db.{Name}s
|
|
263
|
+
.Where(x => x.Id == id && x.TenantId == _currentUser.TenantId) // MANDATORY
|
|
264
|
+
.AsNoTracking()
|
|
265
|
+
.Select(x => new {Name}ResponseDto(x.Id, x.Code, x.Name, x.CreatedAt))
|
|
266
|
+
.FirstOrDefaultAsync(ct);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
public async Task<{Name}ResponseDto> CreateAsync(Create{Name}Dto dto, CancellationToken ct)
|
|
270
|
+
{
|
|
271
|
+
var entity = {Name}.Create(
|
|
272
|
+
tenantId: _currentUser.TenantId, // MANDATORY — never Guid.Empty
|
|
273
|
+
code: dto.Code,
|
|
274
|
+
name: dto.Name);
|
|
275
|
+
|
|
276
|
+
entity.CreatedBy = _currentUser.UserId?.ToString();
|
|
277
|
+
|
|
278
|
+
_db.{Name}s.Add(entity);
|
|
279
|
+
await _db.SaveChangesAsync(ct);
|
|
280
|
+
|
|
281
|
+
_logger.LogInformation("Created {Entity} {Id} for tenant {TenantId}",
|
|
282
|
+
nameof({Name}), entity.Id, _currentUser.TenantId);
|
|
283
|
+
|
|
284
|
+
return new {Name}ResponseDto(entity.Id, entity.Code, entity.Name, entity.CreatedAt);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
public async Task DeleteAsync(Guid id, CancellationToken ct)
|
|
288
|
+
{
|
|
289
|
+
var entity = await _db.{Name}s
|
|
290
|
+
.FirstOrDefaultAsync(x => x.Id == id && x.TenantId == _currentUser.TenantId, ct)
|
|
291
|
+
?? throw new KeyNotFoundException($"{Name} {id} not found");
|
|
292
|
+
|
|
293
|
+
_db.{Name}s.Remove(entity);
|
|
294
|
+
await _db.SaveChangesAsync(ct);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
**Key interfaces:**
|
|
300
|
+
- `ICurrentUser` (from `SmartStack.Application.Common.Interfaces.Identity`): provides `TenantId`, `UserId`, `Email`
|
|
301
|
+
- `IExtensionsDbContext` (for client extensions) or `ICoreDbContext` (for platform)
|
|
302
|
+
|
|
303
|
+
**FORBIDDEN in services:**
|
|
304
|
+
- `tenantId: Guid.Empty` — always use `_currentUser.TenantId`
|
|
305
|
+
- Queries WITHOUT `.Where(x => x.TenantId == _currentUser.TenantId)` — data leak
|
|
306
|
+
- Missing `ILogger<T>` — undiagnosable in production
|
|
307
|
+
|
|
308
|
+
---
|
|
309
|
+
|
|
310
|
+
## Controller Pattern (NavRoute)
|
|
311
|
+
|
|
312
|
+
```csharp
|
|
313
|
+
using Microsoft.AspNetCore.Authorization;
|
|
314
|
+
using Microsoft.AspNetCore.Mvc;
|
|
315
|
+
using SmartStack.Api.Routing;
|
|
316
|
+
using SmartStack.Api.Authorization;
|
|
317
|
+
|
|
318
|
+
namespace {ProjectName}.Api.Controllers.{Context}.{App};
|
|
319
|
+
|
|
320
|
+
[ApiController]
|
|
321
|
+
[NavRoute("{context}.{app}.{module}")]
|
|
322
|
+
[Authorize]
|
|
323
|
+
public class {Name}Controller : ControllerBase
|
|
324
|
+
{
|
|
325
|
+
private readonly I{Name}Service _service;
|
|
326
|
+
private readonly ILogger<{Name}Controller> _logger;
|
|
327
|
+
|
|
328
|
+
public {Name}Controller(I{Name}Service service, ILogger<{Name}Controller> logger)
|
|
329
|
+
{
|
|
330
|
+
_service = service;
|
|
331
|
+
_logger = logger;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
[HttpGet]
|
|
335
|
+
[RequirePermission(Permissions.{Module}.Read)]
|
|
336
|
+
public async Task<ActionResult<PaginatedResult<{Name}ResponseDto>>> GetAll(
|
|
337
|
+
[FromQuery] string? search = null,
|
|
338
|
+
[FromQuery] int page = 1,
|
|
339
|
+
[FromQuery] int pageSize = 20,
|
|
340
|
+
CancellationToken ct = default)
|
|
341
|
+
=> Ok(await _service.GetAllAsync(search, page, pageSize, ct));
|
|
342
|
+
|
|
343
|
+
[HttpGet("{id:guid}")]
|
|
344
|
+
[RequirePermission(Permissions.{Module}.Read)]
|
|
345
|
+
public async Task<ActionResult<{Name}ResponseDto>> GetById(Guid id, CancellationToken ct)
|
|
346
|
+
{
|
|
347
|
+
var result = await _service.GetByIdAsync(id, ct);
|
|
348
|
+
return result is null ? NotFound() : Ok(result);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
[HttpPost]
|
|
352
|
+
[RequirePermission(Permissions.{Module}.Create)]
|
|
353
|
+
public async Task<ActionResult<{Name}ResponseDto>> Create([FromBody] Create{Name}Dto dto, CancellationToken ct)
|
|
354
|
+
{
|
|
355
|
+
var result = await _service.CreateAsync(dto, ct);
|
|
356
|
+
return CreatedAtAction(nameof(GetById), new { id = result.Id }, result);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
[HttpDelete("{id:guid}")]
|
|
360
|
+
[RequirePermission(Permissions.{Module}.Delete)]
|
|
361
|
+
public async Task<ActionResult> Delete(Guid id, CancellationToken ct)
|
|
362
|
+
{
|
|
363
|
+
await _service.DeleteAsync(id, ct);
|
|
364
|
+
return NoContent();
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
**CRITICAL:** Use `[RequirePermission(Permissions.{Module}.{Action})]` on EVERY endpoint — NEVER `[Authorize]` alone (no RBAC enforcement).
|
|
370
|
+
|
|
371
|
+
**Namespace:** `SmartStack.Api.Routing` (NOT `SmartStack.Api.Core.Routing`)
|
|
372
|
+
|
|
373
|
+
**NavRoute resolves at startup from DB:** `platform.administration.users` → `api/platform/administration/users`
|
|
374
|
+
|
|
375
|
+
---
|
|
376
|
+
|
|
377
|
+
## Navigation Seed Data Pattern (CRITICAL — routes must be full paths)
|
|
378
|
+
|
|
379
|
+
> **The navigation seed data defines menu routes stored in DB. These routes MUST be full paths starting with `/`.**
|
|
380
|
+
> Short routes (e.g., `humanresources`) cause 400 Bad Request on application-tracking.
|
|
381
|
+
|
|
382
|
+
### Route Convention
|
|
383
|
+
|
|
384
|
+
| Level | Route Format | Example |
|
|
385
|
+
|-------|-------------|---------|
|
|
386
|
+
| Application | `/{context}/{app-kebab}` | `/business/human-resources` |
|
|
387
|
+
| Module | `/{context}/{app-kebab}/{module-kebab}` | `/business/human-resources/employees` |
|
|
388
|
+
| Section | `/{context}/{app-kebab}/{module-kebab}/{section-kebab}` | `/business/human-resources/employees/list` |
|
|
389
|
+
| Resource | `/{context}/{app-kebab}/{module-kebab}/{section-kebab}/{resource-kebab}` | `/business/human-resources/employees/list/export` |
|
|
390
|
+
|
|
391
|
+
**Rules:**
|
|
392
|
+
- Routes ALWAYS start with `/`
|
|
393
|
+
- Routes ALWAYS include the full hierarchy from context to current level
|
|
394
|
+
- Routes ALWAYS use kebab-case (NOT PascalCase, NOT camelCase)
|
|
395
|
+
- Code identifiers stay PascalCase in C# (`HumanResources`) but routes are kebab-case (`human-resources`)
|
|
396
|
+
|
|
397
|
+
### ToKebabCase Helper (include in SeedConstants or SeedDataProvider)
|
|
398
|
+
|
|
399
|
+
```csharp
|
|
400
|
+
private static string ToKebabCase(string value)
|
|
401
|
+
=> System.Text.RegularExpressions.Regex
|
|
402
|
+
.Replace(value, "(?<!^)([A-Z])", "-$1")
|
|
403
|
+
.ToLowerInvariant();
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
### SeedConstants Pattern
|
|
407
|
+
|
|
408
|
+
```csharp
|
|
409
|
+
public static class SeedConstants
|
|
410
|
+
{
|
|
411
|
+
// Deterministic GUIDs (SHA256-based, reproducible across environments)
|
|
412
|
+
public static readonly Guid ApplicationId = DeterministicGuid("nav:business.humanresources");
|
|
413
|
+
public static readonly Guid ModuleId = DeterministicGuid("nav:business.humanresources.employees");
|
|
414
|
+
public static readonly Guid SectionId = DeterministicGuid("nav:business.humanresources.employees.list");
|
|
415
|
+
|
|
416
|
+
private static Guid DeterministicGuid(string input)
|
|
417
|
+
{
|
|
418
|
+
var hash = System.Security.Cryptography.SHA256.HashData(
|
|
419
|
+
System.Text.Encoding.UTF8.GetBytes(input));
|
|
420
|
+
var bytes = new byte[16];
|
|
421
|
+
Array.Copy(hash, bytes, 16);
|
|
422
|
+
bytes[6] = (byte)((bytes[6] & 0x0F) | 0x50); // version 5
|
|
423
|
+
bytes[8] = (byte)((bytes[8] & 0x3F) | 0x80); // variant
|
|
424
|
+
return new Guid(bytes);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### Navigation Seed Data Example
|
|
430
|
+
|
|
431
|
+
```csharp
|
|
432
|
+
// Application: /business/human-resources
|
|
433
|
+
var app = NavigationApplication.Create(
|
|
434
|
+
businessCtx.Id, "humanresources", "Human Resources", "HR Management",
|
|
435
|
+
"Users", IconType.Lucide,
|
|
436
|
+
"/business/human-resources", // FULL PATH — starts with /, kebab-case
|
|
437
|
+
10);
|
|
438
|
+
|
|
439
|
+
// Module: /business/human-resources/employees
|
|
440
|
+
var module = NavigationModule.Create(
|
|
441
|
+
app.Id, "employees", "Employees", "Employee management",
|
|
442
|
+
"UserCheck", IconType.Lucide,
|
|
443
|
+
"/business/human-resources/employees", // FULL PATH — includes parent
|
|
444
|
+
10);
|
|
445
|
+
|
|
446
|
+
// Section: /business/human-resources/employees/departments
|
|
447
|
+
var section = NavigationSection.Create(
|
|
448
|
+
module.Id, "departments", "Departments", "Manage departments",
|
|
449
|
+
"Building2", IconType.Lucide,
|
|
450
|
+
"/business/human-resources/employees/departments", // FULL PATH
|
|
451
|
+
10);
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
### FORBIDDEN in Seed Data
|
|
455
|
+
|
|
456
|
+
| Mistake | Reality |
|
|
457
|
+
|---------|---------|
|
|
458
|
+
| `"humanresources"` as route | Must be `"/business/human-resources"` (full path, kebab-case) |
|
|
459
|
+
| `"employees"` as route | Must be `"/business/human-resources/employees"` (includes parent) |
|
|
460
|
+
| `Guid.NewGuid()` in seed data | Must use deterministic GUIDs (SHA256) |
|
|
461
|
+
| Missing translations | Must have 4 languages: fr, en, it, de |
|
|
462
|
+
| Missing NavigationApplicationSeedData | Menu invisible without Application level |
|
|
463
|
+
|
|
464
|
+
---
|
|
465
|
+
|
|
466
|
+
## DbContext Pattern (extensions)
|
|
467
|
+
|
|
468
|
+
```csharp
|
|
469
|
+
// In IExtensionsDbContext.cs:
|
|
470
|
+
public DbSet<{Name}> {Name}s => Set<{Name}>();
|
|
471
|
+
|
|
472
|
+
// In ExtensionsDbContext.cs (same line):
|
|
473
|
+
public DbSet<{Name}> {Name}s => Set<{Name}>();
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
---
|
|
477
|
+
|
|
478
|
+
## DI Registration Pattern
|
|
479
|
+
|
|
480
|
+
```csharp
|
|
481
|
+
// In DependencyInjection.cs or ServiceCollectionExtensions.cs:
|
|
482
|
+
services.AddScoped<I{Name}Service, {Name}Service>();
|
|
483
|
+
services.AddValidatorsFromAssemblyContaining<Create{Name}DtoValidator>();
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
---
|
|
487
|
+
|
|
488
|
+
## Common Mistakes to Avoid
|
|
489
|
+
|
|
490
|
+
| Mistake | Reality |
|
|
491
|
+
|---------|---------|
|
|
492
|
+
| `entity.SoftDelete()` | Does NOT exist — no soft delete in BaseEntity |
|
|
493
|
+
| `entity.Code` inherited | Code is a business property — add it yourself |
|
|
494
|
+
| `e.RowVersion` in config | Does NOT exist in BaseEntity |
|
|
495
|
+
| `e.IsDeleted` filter | Does NOT exist — no soft delete |
|
|
496
|
+
| `SmartStack.Api.Core.Routing` | Wrong — use `SmartStack.Api.Routing` |
|
|
497
|
+
| `SystemEntity` base class | Does NOT exist — use `BaseEntity` for all |
|
|
498
|
+
| `[Route] + [NavRoute]` | Only `[NavRoute]` needed (resolves route from DB) |
|
|
499
|
+
| `SmartStack.Domain.Common.Interfaces` | Wrong — interfaces are in `SmartStack.Domain.Common` directly |
|
|
500
|
+
| `[Authorize]` without `[RequirePermission]` | No RBAC enforcement — always use `[RequirePermission]` |
|
|
501
|
+
| `tenantId: Guid.Empty` in services | OWASP A01 — always use `_currentUser.TenantId` |
|
|
502
|
+
| Service without `ICurrentUser` | All tenant data leaks — inject `ICurrentUser` |
|
|
503
|
+
| Route `"humanresources"` in seed data | Must be full path `"/business/human-resources"` |
|
|
504
|
+
| Route without leading `/` | All routes must start with `/` |
|
|
505
|
+
| `Permission.Create()` | Does NOT exist — use `CreateForModule()`, `CreateForSection()`, etc. |
|
|
506
|
+
| `GetAllAsync()` without search param | ALL GetAll endpoints MUST support `?search=` for EntityLookup |
|
|
507
|
+
| FK field as plain text input | Frontend MUST use `EntityLookup` component for Guid FK fields |
|