@atlashub/smartstack-cli 3.23.0 → 3.25.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/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp-entry.mjs +96 -24
- 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/references/smartstack-api.md +174 -5
- package/templates/skills/apex/references/smartstack-frontend.md +1101 -0
- package/templates/skills/apex/references/smartstack-layers.md +81 -5
- package/templates/skills/apex/steps/step-01-analyze.md +27 -3
- package/templates/skills/apex/steps/step-02-plan.md +5 -1
- package/templates/skills/apex/steps/step-03-execute.md +47 -5
- package/templates/skills/apex/steps/step-04-validate.md +300 -0
- package/templates/skills/apex/steps/step-05-examine.md +7 -0
- package/templates/skills/apex/steps/step-07-tests.md +19 -0
- 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/efcore/steps/migration/step-02-create.md +14 -1
- package/templates/skills/ralph-loop/references/category-rules.md +71 -9
- package/templates/skills/ralph-loop/references/compact-loop.md +3 -3
- package/templates/skills/ralph-loop/references/core-seed-data.md +10 -0
- package/templates/skills/ralph-loop/steps/step-02-execute.md +190 -1
- package/templates/skills/validate-feature/steps/step-01-compile.md +4 -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>
|
|
@@ -228,13 +228,33 @@ public class {Name}Service : I{Name}Service
|
|
|
228
228
|
_logger = logger;
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
-
public async Task<
|
|
231
|
+
public async Task<PaginatedResult<{Name}ResponseDto>> GetAllAsync(
|
|
232
|
+
string? search = null,
|
|
233
|
+
int page = 1,
|
|
234
|
+
int pageSize = 20,
|
|
235
|
+
CancellationToken ct = default)
|
|
232
236
|
{
|
|
233
|
-
|
|
237
|
+
var query = _db.{Name}s
|
|
234
238
|
.Where(x => x.TenantId == _currentUser.TenantId) // MANDATORY tenant filter
|
|
235
|
-
.AsNoTracking()
|
|
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)
|
|
236
254
|
.Select(x => new {Name}ResponseDto(x.Id, x.Code, x.Name, x.CreatedAt))
|
|
237
255
|
.ToListAsync(ct);
|
|
256
|
+
|
|
257
|
+
return new PaginatedResult<{Name}ResponseDto>(items, totalCount, page, pageSize);
|
|
238
258
|
}
|
|
239
259
|
|
|
240
260
|
public async Task<{Name}ResponseDto?> GetByIdAsync(Guid id, CancellationToken ct)
|
|
@@ -313,8 +333,12 @@ public class {Name}Controller : ControllerBase
|
|
|
313
333
|
|
|
314
334
|
[HttpGet]
|
|
315
335
|
[RequirePermission(Permissions.{Module}.Read)]
|
|
316
|
-
public async Task<ActionResult<
|
|
317
|
-
|
|
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));
|
|
318
342
|
|
|
319
343
|
[HttpGet("{id:guid}")]
|
|
320
344
|
[RequirePermission(Permissions.{Module}.Read)]
|
|
@@ -479,3 +503,148 @@ services.AddValidatorsFromAssemblyContaining<Create{Name}DtoValidator>();
|
|
|
479
503
|
| Route `"humanresources"` in seed data | Must be full path `"/business/human-resources"` |
|
|
480
504
|
| Route without leading `/` | All routes must start with `/` |
|
|
481
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 |
|
|
508
|
+
|
|
509
|
+
---
|
|
510
|
+
|
|
511
|
+
## Critical Anti-Patterns (with code examples)
|
|
512
|
+
|
|
513
|
+
> **These are the most common and dangerous mistakes.** Each one has been observed in production code generation.
|
|
514
|
+
|
|
515
|
+
### Anti-Pattern 1: HasQueryFilter with `!= Guid.Empty` (SECURITY — OWASP A01)
|
|
516
|
+
|
|
517
|
+
The `HasQueryFilter` in EF Core should use **runtime tenant resolution**, NOT a static comparison against `Guid.Empty`.
|
|
518
|
+
|
|
519
|
+
**INCORRECT — Does NOT isolate tenants:**
|
|
520
|
+
```csharp
|
|
521
|
+
// WRONG: This only excludes empty GUIDs — ALL tenant data is still visible to everyone!
|
|
522
|
+
public void Configure(EntityTypeBuilder<MyEntity> builder)
|
|
523
|
+
{
|
|
524
|
+
builder.HasQueryFilter(e => e.TenantId != Guid.Empty);
|
|
525
|
+
}
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
**CORRECT — Tenant isolation via service:**
|
|
529
|
+
```csharp
|
|
530
|
+
// CORRECT: In SmartStack, tenant filtering is done in the SERVICE layer, not via HasQueryFilter.
|
|
531
|
+
public async Task<PaginatedResult<MyEntityDto>> GetAllAsync(...)
|
|
532
|
+
{
|
|
533
|
+
var query = _db.MyEntities
|
|
534
|
+
.Where(x => x.TenantId == _currentUser.TenantId) // MANDATORY runtime filter
|
|
535
|
+
.AsNoTracking();
|
|
536
|
+
// ...
|
|
537
|
+
}
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
**Why it's wrong:** `HasQueryFilter(e => e.TenantId != Guid.Empty)` is a **static filter** — it only removes records with empty GUIDs. It does NOT restrict data to the current tenant. This is an **OWASP A01 Broken Access Control** vulnerability.
|
|
541
|
+
|
|
542
|
+
---
|
|
543
|
+
|
|
544
|
+
### Anti-Pattern 2: `List<T>` instead of `PaginatedResult<T>` for GetAll
|
|
545
|
+
|
|
546
|
+
**INCORRECT — No pagination:**
|
|
547
|
+
```csharp
|
|
548
|
+
// WRONG: Returns all records at once — no pagination, no totalCount
|
|
549
|
+
public async Task<List<MyEntityDto>> GetAllAsync(CancellationToken ct)
|
|
550
|
+
{
|
|
551
|
+
return await _db.MyEntities
|
|
552
|
+
.Where(x => x.TenantId == _currentUser.TenantId)
|
|
553
|
+
.Select(x => new MyEntityDto(x.Id, x.Code, x.Name))
|
|
554
|
+
.ToListAsync(ct);
|
|
555
|
+
}
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
**CORRECT — Paginated with search:**
|
|
559
|
+
```csharp
|
|
560
|
+
// CORRECT: Returns PaginatedResult<T> with search, page, pageSize
|
|
561
|
+
public async Task<PaginatedResult<MyEntityDto>> GetAllAsync(
|
|
562
|
+
string? search = null, int page = 1, int pageSize = 20, CancellationToken ct = default)
|
|
563
|
+
{
|
|
564
|
+
var query = _db.MyEntities.Where(x => x.TenantId == _currentUser.TenantId).AsNoTracking();
|
|
565
|
+
if (!string.IsNullOrWhiteSpace(search))
|
|
566
|
+
query = query.Where(x => x.Name.Contains(search) || x.Code.Contains(search));
|
|
567
|
+
var totalCount = await query.CountAsync(ct);
|
|
568
|
+
var items = await query.OrderBy(x => x.Name).Skip((page - 1) * pageSize).Take(pageSize)
|
|
569
|
+
.Select(x => new MyEntityDto(x.Id, x.Code, x.Name, x.CreatedAt)).ToListAsync(ct);
|
|
570
|
+
return new PaginatedResult<MyEntityDto>(items, totalCount, page, pageSize);
|
|
571
|
+
}
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
**Why it's wrong:** `List<T>` loads ALL records into memory. It also breaks `EntityLookup` which requires `{ items, totalCount }` response format.
|
|
575
|
+
|
|
576
|
+
---
|
|
577
|
+
|
|
578
|
+
### Anti-Pattern 3: Missing `IAuditableEntity` on tenant entities
|
|
579
|
+
|
|
580
|
+
**INCORRECT — No audit trail:**
|
|
581
|
+
```csharp
|
|
582
|
+
// WRONG: Tenant entity without IAuditableEntity
|
|
583
|
+
public class MyEntity : BaseEntity, ITenantEntity
|
|
584
|
+
{
|
|
585
|
+
public Guid TenantId { get; private set; }
|
|
586
|
+
public string Code { get; private set; } = null!;
|
|
587
|
+
}
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
**CORRECT — Always pair ITenantEntity with IAuditableEntity:**
|
|
591
|
+
```csharp
|
|
592
|
+
public class MyEntity : BaseEntity, ITenantEntity, IAuditableEntity
|
|
593
|
+
{
|
|
594
|
+
public Guid TenantId { get; private set; }
|
|
595
|
+
public string? CreatedBy { get; set; }
|
|
596
|
+
public string? UpdatedBy { get; set; }
|
|
597
|
+
public string Code { get; private set; } = null!;
|
|
598
|
+
}
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
**Why it's wrong:** Without `IAuditableEntity`, there is no record of who created or modified data. Mandatory for compliance in multi-tenant environments.
|
|
602
|
+
|
|
603
|
+
---
|
|
604
|
+
|
|
605
|
+
### Anti-Pattern 4: Code auto-generation with `Count() + 1`
|
|
606
|
+
|
|
607
|
+
**INCORRECT — Race condition:**
|
|
608
|
+
```csharp
|
|
609
|
+
// WRONG: Two concurrent requests get the same count
|
|
610
|
+
var count = await _db.MyEntities.Where(x => x.TenantId == _currentUser.TenantId).CountAsync(ct);
|
|
611
|
+
return $"ENT{(count + 1):D4}";
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
**CORRECT — Use `GenerateNextCodeAsync()` (atomic):**
|
|
615
|
+
```csharp
|
|
616
|
+
var code = await GenerateNextCodeAsync("MyEntity", _currentUser.TenantId, ct);
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
**Why it's wrong:** `Count() + 1` causes **race conditions** — concurrent requests generate duplicate codes.
|
|
620
|
+
|
|
621
|
+
---
|
|
622
|
+
|
|
623
|
+
### Anti-Pattern 5: Missing Update validator
|
|
624
|
+
|
|
625
|
+
**INCORRECT — Only CreateValidator:**
|
|
626
|
+
```csharp
|
|
627
|
+
public class CreateMyEntityDtoValidator : AbstractValidator<CreateMyEntityDto>
|
|
628
|
+
{
|
|
629
|
+
public CreateMyEntityDtoValidator()
|
|
630
|
+
{
|
|
631
|
+
RuleFor(x => x.Code).NotEmpty().MaximumLength(100);
|
|
632
|
+
RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
// No UpdateMyEntityDtoValidator exists!
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
**CORRECT — Always create validators in pairs:**
|
|
639
|
+
```csharp
|
|
640
|
+
public class CreateMyEntityDtoValidator : AbstractValidator<CreateMyEntityDto> { /* ... */ }
|
|
641
|
+
public class UpdateMyEntityDtoValidator : AbstractValidator<UpdateMyEntityDto>
|
|
642
|
+
{
|
|
643
|
+
public UpdateMyEntityDtoValidator()
|
|
644
|
+
{
|
|
645
|
+
RuleFor(x => x.Name).NotEmpty().MaximumLength(200);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
**Why it's wrong:** Without an `UpdateValidator`, the Update endpoint accepts **any data without validation**.
|