@atlashub/smartstack-mcp 1.10.0 → 1.14.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 +418 -132
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/templates/migrations/seed-permissions.cs.hbs +0 -108
package/dist/index.js
CHANGED
|
@@ -558,7 +558,7 @@ var ConfigSchema = z.object({
|
|
|
558
558
|
});
|
|
559
559
|
var ValidateConventionsInputSchema = z.object({
|
|
560
560
|
path: z.string().optional().describe("Project path to validate (default: SmartStack.app path)"),
|
|
561
|
-
checks: z.array(z.enum(["tables", "migrations", "services", "namespaces", "entities", "tenants", "controllers", "all"])).default(["all"]).describe("Types of checks to perform")
|
|
561
|
+
checks: z.array(z.enum(["tables", "migrations", "services", "namespaces", "entities", "tenants", "controllers", "layouts", "tabs", "all"])).default(["all"]).describe("Types of checks to perform")
|
|
562
562
|
});
|
|
563
563
|
var CheckMigrationsInputSchema = z.object({
|
|
564
564
|
projectPath: z.string().optional().describe("EF Core project path"),
|
|
@@ -914,7 +914,7 @@ var validateConventionsTool = {
|
|
|
914
914
|
type: "array",
|
|
915
915
|
items: {
|
|
916
916
|
type: "string",
|
|
917
|
-
enum: ["tables", "migrations", "services", "namespaces", "entities", "tenants", "controllers", "all"]
|
|
917
|
+
enum: ["tables", "migrations", "services", "namespaces", "entities", "tenants", "controllers", "layouts", "tabs", "all"]
|
|
918
918
|
},
|
|
919
919
|
description: "Types of checks to perform",
|
|
920
920
|
default: ["all"]
|
|
@@ -925,7 +925,7 @@ var validateConventionsTool = {
|
|
|
925
925
|
async function handleValidateConventions(args, config) {
|
|
926
926
|
const input = ValidateConventionsInputSchema.parse(args);
|
|
927
927
|
const projectPath = input.path || config.smartstack.projectPath;
|
|
928
|
-
const checks = input.checks.includes("all") ? ["tables", "migrations", "services", "namespaces", "entities", "tenants", "controllers"] : input.checks;
|
|
928
|
+
const checks = input.checks.includes("all") ? ["tables", "migrations", "services", "namespaces", "entities", "tenants", "controllers", "layouts", "tabs"] : input.checks;
|
|
929
929
|
logger.info("Validating conventions", { projectPath, checks });
|
|
930
930
|
const result = {
|
|
931
931
|
valid: true,
|
|
@@ -955,6 +955,12 @@ async function handleValidateConventions(args, config) {
|
|
|
955
955
|
if (checks.includes("controllers")) {
|
|
956
956
|
await validateControllerRoutes(structure, config, result);
|
|
957
957
|
}
|
|
958
|
+
if (checks.includes("layouts")) {
|
|
959
|
+
await validateLayouts(structure, config, result);
|
|
960
|
+
}
|
|
961
|
+
if (checks.includes("tabs")) {
|
|
962
|
+
await validateTabs(structure, config, result);
|
|
963
|
+
}
|
|
958
964
|
result.valid = result.errors.length === 0;
|
|
959
965
|
result.summary = generateSummary(result, checks);
|
|
960
966
|
return formatResult(result);
|
|
@@ -1385,6 +1391,132 @@ async function validateControllerRoutes(structure, _config, result) {
|
|
|
1385
1391
|
});
|
|
1386
1392
|
}
|
|
1387
1393
|
}
|
|
1394
|
+
async function validateLayouts(structure, _config, result) {
|
|
1395
|
+
if (!structure.web) {
|
|
1396
|
+
result.warnings.push({
|
|
1397
|
+
type: "warning",
|
|
1398
|
+
category: "layouts",
|
|
1399
|
+
message: "Web project not found, skipping layout validation"
|
|
1400
|
+
});
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
const layoutFiles = await findFiles("**/layouts/**/*.tsx", { cwd: structure.web });
|
|
1404
|
+
if (layoutFiles.length === 0) {
|
|
1405
|
+
result.warnings.push({
|
|
1406
|
+
type: "warning",
|
|
1407
|
+
category: "layouts",
|
|
1408
|
+
message: "No layout files found in web/src/layouts/"
|
|
1409
|
+
});
|
|
1410
|
+
return;
|
|
1411
|
+
}
|
|
1412
|
+
for (const file of layoutFiles) {
|
|
1413
|
+
const content = await readText(file);
|
|
1414
|
+
const fileName = path6.basename(file);
|
|
1415
|
+
const lines = content.split("\n");
|
|
1416
|
+
let hasMaxWidth = false;
|
|
1417
|
+
for (const line of lines) {
|
|
1418
|
+
if (line.trim().startsWith("//") || line.trim().startsWith("*")) continue;
|
|
1419
|
+
if (/className.*max-w-/.test(line)) {
|
|
1420
|
+
hasMaxWidth = true;
|
|
1421
|
+
break;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
if (hasMaxWidth) {
|
|
1425
|
+
result.errors.push({
|
|
1426
|
+
type: "error",
|
|
1427
|
+
category: "layouts",
|
|
1428
|
+
message: `Layout "${fileName}" uses max-w-* constraint which limits content width`,
|
|
1429
|
+
file: path6.relative(structure.root, file),
|
|
1430
|
+
suggestion: "Remove max-w-* class. Content should occupy full available width."
|
|
1431
|
+
});
|
|
1432
|
+
}
|
|
1433
|
+
let hasStandardPadding = false;
|
|
1434
|
+
for (const line of lines) {
|
|
1435
|
+
if (line.trim().startsWith("//") || line.trim().startsWith("*")) continue;
|
|
1436
|
+
if (/className.*lg:px-10/.test(line)) {
|
|
1437
|
+
hasStandardPadding = true;
|
|
1438
|
+
break;
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
if (!hasStandardPadding) {
|
|
1442
|
+
result.warnings.push({
|
|
1443
|
+
type: "warning",
|
|
1444
|
+
category: "layouts",
|
|
1445
|
+
message: `Layout "${fileName}" may be missing standard horizontal padding`,
|
|
1446
|
+
file: path6.relative(structure.root, file),
|
|
1447
|
+
suggestion: "Use lg:px-10 for consistent horizontal padding across layouts"
|
|
1448
|
+
});
|
|
1449
|
+
}
|
|
1450
|
+
let hasScrollPattern = false;
|
|
1451
|
+
for (const line of lines) {
|
|
1452
|
+
if (line.trim().startsWith("//") || line.trim().startsWith("*")) continue;
|
|
1453
|
+
if (/className.*h-full/.test(line) && /className.*overflow-auto/.test(line)) {
|
|
1454
|
+
hasScrollPattern = true;
|
|
1455
|
+
break;
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
if (!hasScrollPattern) {
|
|
1459
|
+
result.warnings.push({
|
|
1460
|
+
type: "warning",
|
|
1461
|
+
category: "layouts",
|
|
1462
|
+
message: `Layout "${fileName}" may be missing scroll container pattern`,
|
|
1463
|
+
file: path6.relative(structure.root, file),
|
|
1464
|
+
suggestion: "Use h-full overflow-auto for proper internal scrolling"
|
|
1465
|
+
});
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
async function validateTabs(structure, _config, result) {
|
|
1470
|
+
if (!structure.web) {
|
|
1471
|
+
result.warnings.push({
|
|
1472
|
+
type: "warning",
|
|
1473
|
+
category: "tabs",
|
|
1474
|
+
message: "Web project not found, skipping tab validation"
|
|
1475
|
+
});
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
const pageFiles = await findFiles("**/pages/**/*.tsx", { cwd: structure.web });
|
|
1479
|
+
let tabPagesCount = 0;
|
|
1480
|
+
let hookUsageCount = 0;
|
|
1481
|
+
for (const file of pageFiles) {
|
|
1482
|
+
const content = await readText(file);
|
|
1483
|
+
const fileName = path6.basename(file);
|
|
1484
|
+
const lines = content.split("\n");
|
|
1485
|
+
let hasTabPattern = false;
|
|
1486
|
+
for (const line of lines) {
|
|
1487
|
+
const trimmed = line.trim();
|
|
1488
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*") || trimmed === "") {
|
|
1489
|
+
continue;
|
|
1490
|
+
}
|
|
1491
|
+
if (/<Tabs|<TabList|activeTab\s*[,=})]|setActiveTab\s*[({]/.test(line)) {
|
|
1492
|
+
hasTabPattern = true;
|
|
1493
|
+
break;
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
if (hasTabPattern) {
|
|
1497
|
+
tabPagesCount++;
|
|
1498
|
+
const usesHook = content.includes("useTabNavigation");
|
|
1499
|
+
if (usesHook) {
|
|
1500
|
+
hookUsageCount++;
|
|
1501
|
+
} else {
|
|
1502
|
+
result.errors.push({
|
|
1503
|
+
type: "error",
|
|
1504
|
+
category: "tabs",
|
|
1505
|
+
message: `Page "${fileName}" has tabs but doesn't use useTabNavigation hook`,
|
|
1506
|
+
file: path6.relative(structure.root, file),
|
|
1507
|
+
suggestion: "Use useTabNavigation hook to sync tab state with URL: const { activeTab, setActiveTab } = useTabNavigation(defaultTab, VALID_TABS)"
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
if (tabPagesCount > 0) {
|
|
1513
|
+
result.warnings.push({
|
|
1514
|
+
type: "warning",
|
|
1515
|
+
category: "tabs",
|
|
1516
|
+
message: `Tab summary: ${hookUsageCount}/${tabPagesCount} pages with tabs use useTabNavigation hook`
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1388
1520
|
function generateSummary(result, checks) {
|
|
1389
1521
|
const parts = [];
|
|
1390
1522
|
parts.push(`Checks performed: ${checks.join(", ")}`);
|
|
@@ -1822,6 +1954,10 @@ var scaffoldExtensionTool = {
|
|
|
1822
1954
|
type: "boolean",
|
|
1823
1955
|
description: "For feature type: generate repository pattern"
|
|
1824
1956
|
},
|
|
1957
|
+
withSeedData: {
|
|
1958
|
+
type: "boolean",
|
|
1959
|
+
description: "For entity type: generate centralized SeedData file in Seeding/Data/{Domain}/"
|
|
1960
|
+
},
|
|
1825
1961
|
entityProperties: {
|
|
1826
1962
|
type: "array",
|
|
1827
1963
|
items: {
|
|
@@ -2346,6 +2482,119 @@ public class {{name}}Configuration : IEntityTypeConfiguration<{{name}}>
|
|
|
2346
2482
|
result.instructions.push("- CreatedAt, UpdatedAt, CreatedBy, UpdatedBy (audit)");
|
|
2347
2483
|
result.instructions.push("- IsDeleted, DeletedAt, DeletedBy (soft delete)");
|
|
2348
2484
|
result.instructions.push("- RowVersion (concurrency)");
|
|
2485
|
+
if (options?.withSeedData) {
|
|
2486
|
+
result.instructions.push("");
|
|
2487
|
+
result.instructions.push("### Seed Data");
|
|
2488
|
+
await scaffoldSeedData(name, options, structure, config, result, dryRun);
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
async function scaffoldSeedData(name, options, structure, config, result, dryRun = false) {
|
|
2492
|
+
const tablePrefix = options?.tablePrefix || "ref_";
|
|
2493
|
+
const domainMap = {
|
|
2494
|
+
"auth_": "Authorization",
|
|
2495
|
+
"nav_": "Navigation",
|
|
2496
|
+
"usr_": "User",
|
|
2497
|
+
"wkf_": "Communications",
|
|
2498
|
+
"cfg_": "Configuration",
|
|
2499
|
+
"ai_": "AI",
|
|
2500
|
+
"entra_": "Entra",
|
|
2501
|
+
"ref_": "Reference",
|
|
2502
|
+
"support_": "Support",
|
|
2503
|
+
"loc_": "Localization"
|
|
2504
|
+
};
|
|
2505
|
+
const domain = options?.seedDataDomain || domainMap[tablePrefix] || "Reference";
|
|
2506
|
+
const isSystemEntity = options?.isSystemEntity || false;
|
|
2507
|
+
const seedDataTemplate = `using SmartStack.Domain.{{domainNamespace}};
|
|
2508
|
+
|
|
2509
|
+
namespace SmartStack.Infrastructure.Persistence.Seeding.Data.{{domain}};
|
|
2510
|
+
|
|
2511
|
+
/// <summary>
|
|
2512
|
+
/// Donnees seed pour {{name}}.
|
|
2513
|
+
/// Centralise les IDs et donnees d'initialisation.
|
|
2514
|
+
/// </summary>
|
|
2515
|
+
public static class {{name}}SeedData
|
|
2516
|
+
{
|
|
2517
|
+
// ============================================================
|
|
2518
|
+
// IDs - Documenter chaque ID avec son role
|
|
2519
|
+
// ============================================================
|
|
2520
|
+
|
|
2521
|
+
/// <summary>ID exemple - A remplacer par vos IDs</summary>
|
|
2522
|
+
public static readonly Guid ExampleId = Guid.Parse("{{exampleGuid}}");
|
|
2523
|
+
|
|
2524
|
+
// ============================================================
|
|
2525
|
+
// CODES / CONSTANTS
|
|
2526
|
+
// ============================================================
|
|
2527
|
+
|
|
2528
|
+
public const string ExampleCode = "example";
|
|
2529
|
+
|
|
2530
|
+
// ============================================================
|
|
2531
|
+
// SEED DATA
|
|
2532
|
+
// ============================================================
|
|
2533
|
+
|
|
2534
|
+
/// <summary>
|
|
2535
|
+
/// Retourne toutes les donnees seed pour {{name}}.
|
|
2536
|
+
/// Appel\xE9 depuis {{name}}Configuration.HasData()
|
|
2537
|
+
/// </summary>
|
|
2538
|
+
public static object[] GetSeedData()
|
|
2539
|
+
{
|
|
2540
|
+
var seedDate = SeedConstants.SeedDate;
|
|
2541
|
+
|
|
2542
|
+
return new object[]
|
|
2543
|
+
{
|
|
2544
|
+
// Exemple - A remplacer par vos donnees
|
|
2545
|
+
new
|
|
2546
|
+
{
|
|
2547
|
+
Id = ExampleId,
|
|
2548
|
+
{{#unless isSystemEntity}}
|
|
2549
|
+
TenantId = (Guid?)null, // Seed data systeme sans tenant
|
|
2550
|
+
{{/unless}}
|
|
2551
|
+
Code = ExampleCode,
|
|
2552
|
+
// TODO: Ajouter les proprietes specifiques
|
|
2553
|
+
IsDeleted = false,
|
|
2554
|
+
CreatedAt = seedDate
|
|
2555
|
+
}
|
|
2556
|
+
};
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
`;
|
|
2560
|
+
const exampleGuid = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
2561
|
+
const r = Math.random() * 16 | 0;
|
|
2562
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
2563
|
+
return v.toString(16);
|
|
2564
|
+
});
|
|
2565
|
+
const context = {
|
|
2566
|
+
name,
|
|
2567
|
+
domain,
|
|
2568
|
+
domainNamespace: domain,
|
|
2569
|
+
isSystemEntity,
|
|
2570
|
+
exampleGuid
|
|
2571
|
+
};
|
|
2572
|
+
const seedDataContent = Handlebars.compile(seedDataTemplate)(context);
|
|
2573
|
+
const projectRoot = config.smartstack.projectPath;
|
|
2574
|
+
const infraPath = structure.infrastructure || path8.join(projectRoot, "Infrastructure");
|
|
2575
|
+
const seedDataPath = path8.join(infraPath, "Persistence", "Seeding", "Data", domain);
|
|
2576
|
+
const seedDataFilePath = path8.join(seedDataPath, `${name}SeedData.cs`);
|
|
2577
|
+
validatePathSecurity(seedDataFilePath, projectRoot);
|
|
2578
|
+
if (!dryRun) {
|
|
2579
|
+
await ensureDirectory(seedDataPath);
|
|
2580
|
+
await writeText(seedDataFilePath, seedDataContent);
|
|
2581
|
+
}
|
|
2582
|
+
result.files.push({ path: seedDataFilePath, content: seedDataContent, type: "created" });
|
|
2583
|
+
result.instructions.push(`SeedData file generated in Seeding/Data/${domain}/`);
|
|
2584
|
+
result.instructions.push("");
|
|
2585
|
+
result.instructions.push("Update your Configuration to use centralized SeedData:");
|
|
2586
|
+
result.instructions.push("```csharp");
|
|
2587
|
+
result.instructions.push(`// In ${name}Configuration.cs`);
|
|
2588
|
+
result.instructions.push(`using SmartStack.Infrastructure.Persistence.Seeding.Data.${domain};`);
|
|
2589
|
+
result.instructions.push("");
|
|
2590
|
+
result.instructions.push(`builder.HasData(${name}SeedData.GetSeedData());`);
|
|
2591
|
+
result.instructions.push("```");
|
|
2592
|
+
result.instructions.push("");
|
|
2593
|
+
result.instructions.push("Pattern to follow:");
|
|
2594
|
+
result.instructions.push("1. Define public static readonly Guid IDs");
|
|
2595
|
+
result.instructions.push("2. Define const string codes");
|
|
2596
|
+
result.instructions.push("3. Implement GetSeedData() returning object[]");
|
|
2597
|
+
result.instructions.push("4. Reference other SeedData IDs for foreign keys");
|
|
2349
2598
|
}
|
|
2350
2599
|
async function scaffoldController(name, options, structure, config, result, dryRun = false) {
|
|
2351
2600
|
const namespace = options?.namespace || `${config.conventions.namespaces.api}.Controllers`;
|
|
@@ -3658,7 +3907,7 @@ async function handleSuggestMigration(args, config) {
|
|
|
3658
3907
|
async function findExistingMigrations(structure, config, context) {
|
|
3659
3908
|
const migrations = [];
|
|
3660
3909
|
const infraPath = structure.infrastructure || path10.join(config.smartstack.projectPath, "Infrastructure");
|
|
3661
|
-
const migrationsPath = path10.join(infraPath, "Migrations");
|
|
3910
|
+
const migrationsPath = path10.join(infraPath, "Persistence", "Migrations");
|
|
3662
3911
|
try {
|
|
3663
3912
|
const migrationFiles = await findFiles("*.cs", { cwd: migrationsPath });
|
|
3664
3913
|
const migrationPattern = /^(\w+)_v(\d+\.\d+\.\d+)_(\d+)_(\w+)\.cs$/;
|
|
@@ -3705,7 +3954,6 @@ function compareVersions2(a, b) {
|
|
|
3705
3954
|
}
|
|
3706
3955
|
|
|
3707
3956
|
// src/tools/generate-permissions.ts
|
|
3708
|
-
import Handlebars2 from "handlebars";
|
|
3709
3957
|
import path11 from "path";
|
|
3710
3958
|
var HTTP_METHOD_TO_ACTION = {
|
|
3711
3959
|
"GET": "read",
|
|
@@ -3722,17 +3970,21 @@ var generatePermissionsTool = {
|
|
|
3722
3970
|
name: "generate_permissions",
|
|
3723
3971
|
description: `Generate RBAC permissions for SmartStack controllers.
|
|
3724
3972
|
|
|
3725
|
-
|
|
3973
|
+
Analyzes NavRoute attributes and outputs HasData() C# code to add to PermissionConfiguration.cs.
|
|
3974
|
+
|
|
3975
|
+
IMPORTANT: This tool does NOT generate migrations with raw SQL (forbidden by SmartStack conventions).
|
|
3976
|
+
Instead, it outputs HasData() code that must be manually added to the Configuration file.
|
|
3726
3977
|
|
|
3727
3978
|
Example:
|
|
3728
3979
|
navRoute: "platform.administration.entra"
|
|
3729
|
-
|
|
3980
|
+
Outputs HasData() code for:
|
|
3981
|
+
- platform.administration.entra.*
|
|
3730
3982
|
- platform.administration.entra.read
|
|
3731
3983
|
- platform.administration.entra.create
|
|
3732
3984
|
- platform.administration.entra.update
|
|
3733
3985
|
- platform.administration.entra.delete
|
|
3734
3986
|
|
|
3735
|
-
|
|
3987
|
+
After adding to PermissionConfiguration.cs, run: dotnet ef migrations add <MigrationName>`,
|
|
3736
3988
|
inputSchema: {
|
|
3737
3989
|
type: "object",
|
|
3738
3990
|
properties: {
|
|
@@ -3750,15 +4002,10 @@ Can also generate EF Core migration to seed permissions in database.`,
|
|
|
3750
4002
|
default: true,
|
|
3751
4003
|
description: "Include standard CRUD actions (read, create, update, delete)"
|
|
3752
4004
|
},
|
|
3753
|
-
|
|
4005
|
+
includeWildcard: {
|
|
3754
4006
|
type: "boolean",
|
|
3755
4007
|
default: true,
|
|
3756
|
-
description: "
|
|
3757
|
-
},
|
|
3758
|
-
dryRun: {
|
|
3759
|
-
type: "boolean",
|
|
3760
|
-
default: false,
|
|
3761
|
-
description: "Preview without writing files or creating migration"
|
|
4008
|
+
description: "Include wildcard permission (e.g., personal.myspace.tenants.*)"
|
|
3762
4009
|
}
|
|
3763
4010
|
}
|
|
3764
4011
|
}
|
|
@@ -3769,8 +4016,7 @@ async function handleGeneratePermissions(args, config) {
|
|
|
3769
4016
|
navRoute: args.navRoute,
|
|
3770
4017
|
actions: args.actions || [],
|
|
3771
4018
|
includeStandardActions: args.includeStandardActions !== false,
|
|
3772
|
-
|
|
3773
|
-
dryRun: args.dryRun === true
|
|
4019
|
+
includeWildcard: args.includeWildcard !== false
|
|
3774
4020
|
};
|
|
3775
4021
|
const structure = await findSmartStackStructure(config.smartstack.projectPath);
|
|
3776
4022
|
if (!structure) {
|
|
@@ -3782,16 +4028,17 @@ async function handleGeneratePermissions(args, config) {
|
|
|
3782
4028
|
permissions = generatePermissionsForNavRoute(
|
|
3783
4029
|
options.navRoute,
|
|
3784
4030
|
options.actions || [],
|
|
3785
|
-
options.includeStandardActions || false
|
|
4031
|
+
options.includeStandardActions || false,
|
|
4032
|
+
options.includeWildcard || false
|
|
3786
4033
|
);
|
|
3787
|
-
report = `## Permissions
|
|
4034
|
+
report = `## Permissions for NavRoute: ${options.navRoute}
|
|
3788
4035
|
|
|
3789
4036
|
`;
|
|
3790
4037
|
report += formatPermissionsReport(permissions);
|
|
3791
4038
|
} else {
|
|
3792
4039
|
const scannedPermissions = await scanControllersForPermissions(structure.api || structure.root);
|
|
3793
4040
|
permissions = scannedPermissions;
|
|
3794
|
-
report = `## Permissions
|
|
4041
|
+
report = `## Permissions from All Controllers
|
|
3795
4042
|
|
|
3796
4043
|
`;
|
|
3797
4044
|
report += `Total controllers scanned: ${getUniqueNavRouteCount(permissions)}
|
|
@@ -3801,53 +4048,51 @@ async function handleGeneratePermissions(args, config) {
|
|
|
3801
4048
|
`;
|
|
3802
4049
|
report += formatPermissionsReport(permissions);
|
|
3803
4050
|
}
|
|
3804
|
-
|
|
3805
|
-
const migrationResult = await generatePermissionMigration(
|
|
3806
|
-
structure.api || structure.root,
|
|
3807
|
-
permissions,
|
|
3808
|
-
config
|
|
3809
|
-
);
|
|
3810
|
-
report += `
|
|
4051
|
+
report += `
|
|
3811
4052
|
|
|
3812
|
-
##
|
|
4053
|
+
## HasData() Code for PermissionConfiguration.cs
|
|
3813
4054
|
|
|
3814
4055
|
`;
|
|
3815
|
-
|
|
4056
|
+
report += `\u26A0\uFE0F **IMPORTANT**: Do NOT use migrationBuilder.Sql() - it's forbidden by SmartStack conventions.
|
|
3816
4057
|
`;
|
|
3817
|
-
|
|
4058
|
+
report += `Add this code to \`PermissionConfiguration.cs\` in the HasData() section, then create a migration.
|
|
3818
4059
|
|
|
3819
4060
|
`;
|
|
3820
|
-
|
|
4061
|
+
report += generateHasDataCode(permissions, options.navRoute || "custom");
|
|
4062
|
+
report += `
|
|
4063
|
+
|
|
4064
|
+
### Next Steps
|
|
3821
4065
|
|
|
3822
4066
|
`;
|
|
3823
|
-
|
|
3824
|
-
`;
|
|
3825
|
-
report += `2. Run: \`dotnet ef database update\`
|
|
4067
|
+
report += `1. Add the module ID variable in GetSeedData() method
|
|
3826
4068
|
`;
|
|
3827
|
-
|
|
4069
|
+
report += `2. Add the HasData entries to the return array
|
|
3828
4070
|
`;
|
|
3829
|
-
|
|
3830
|
-
report += `
|
|
3831
|
-
|
|
3832
|
-
## Dry Run Mode
|
|
3833
|
-
|
|
4071
|
+
report += `3. Run: \`dotnet ef migrations add <MigrationName> -o Persistence/Migrations\`
|
|
3834
4072
|
`;
|
|
3835
|
-
|
|
4073
|
+
report += `4. Run: \`dotnet ef database update\`
|
|
3836
4074
|
`;
|
|
3837
|
-
}
|
|
3838
4075
|
return report;
|
|
3839
4076
|
} catch (error) {
|
|
3840
4077
|
logger.error("Error generating permissions:", error);
|
|
3841
4078
|
throw error;
|
|
3842
4079
|
}
|
|
3843
4080
|
}
|
|
3844
|
-
function generatePermissionsForNavRoute(navRoute, customActions, includeStandardActions) {
|
|
4081
|
+
function generatePermissionsForNavRoute(navRoute, customActions, includeStandardActions, includeWildcard = true) {
|
|
3845
4082
|
const permissions = [];
|
|
3846
4083
|
const parts = navRoute.split(".");
|
|
3847
4084
|
const context = parts[0];
|
|
3848
4085
|
if (parts.length < 3) {
|
|
3849
4086
|
throw new Error(`Invalid NavRoute format: ${navRoute}. Expected format: context.application.module`);
|
|
3850
4087
|
}
|
|
4088
|
+
if (includeWildcard) {
|
|
4089
|
+
permissions.push({
|
|
4090
|
+
code: `${navRoute}.*`,
|
|
4091
|
+
name: formatPermissionName(navRoute, "Full Access"),
|
|
4092
|
+
description: `Full ${parts[parts.length - 1]} management`,
|
|
4093
|
+
category: context
|
|
4094
|
+
});
|
|
4095
|
+
}
|
|
3851
4096
|
const actions = includeStandardActions ? [...STANDARD_ACTIONS, ...customActions] : customActions;
|
|
3852
4097
|
for (const action of actions) {
|
|
3853
4098
|
const code = `${navRoute}.${action}`;
|
|
@@ -3986,87 +4231,54 @@ function getUniqueNavRouteCount(permissions) {
|
|
|
3986
4231
|
);
|
|
3987
4232
|
return navRoutes.size;
|
|
3988
4233
|
}
|
|
3989
|
-
|
|
3990
|
-
const
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
try {
|
|
3998
|
-
template = await readText(templatePath);
|
|
3999
|
-
} catch {
|
|
4000
|
-
template = getSeedPermissionsTemplate();
|
|
4001
|
-
}
|
|
4002
|
-
const handlebars = Handlebars2.create();
|
|
4003
|
-
const compiled = handlebars.compile(template);
|
|
4004
|
-
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:T.]/g, "").slice(0, 14);
|
|
4005
|
-
const migrationName = `SeedPermissions_${timestamp}`;
|
|
4006
|
-
const content = compiled({
|
|
4007
|
-
migrationName,
|
|
4008
|
-
permissions,
|
|
4009
|
-
timestamp
|
|
4010
|
-
});
|
|
4011
|
-
const migrationsPath = path11.join(
|
|
4012
|
-
backendRoot,
|
|
4013
|
-
"Infrastructure",
|
|
4014
|
-
"Data",
|
|
4015
|
-
"Migrations",
|
|
4016
|
-
"Core"
|
|
4017
|
-
);
|
|
4018
|
-
await ensureDirectory(migrationsPath);
|
|
4019
|
-
const filePath = path11.join(migrationsPath, `${migrationName}.cs`);
|
|
4020
|
-
validatePathSecurity(filePath, config.smartstack.projectPath);
|
|
4021
|
-
await writeText(filePath, content);
|
|
4022
|
-
logger.info(`Generated permission migration: ${filePath}`);
|
|
4023
|
-
return {
|
|
4024
|
-
filePath,
|
|
4025
|
-
migrationName
|
|
4026
|
-
};
|
|
4027
|
-
}
|
|
4028
|
-
function getSeedPermissionsTemplate() {
|
|
4029
|
-
return `using Microsoft.EntityFrameworkCore.Migrations;
|
|
4030
|
-
|
|
4031
|
-
namespace SmartStack.Infrastructure.Data.Migrations.Core;
|
|
4234
|
+
function generateHasDataCode(permissions, navRoute) {
|
|
4235
|
+
const parts = navRoute.split(".");
|
|
4236
|
+
const moduleName = parts.length >= 3 ? parts[parts.length - 1] : "custom";
|
|
4237
|
+
const moduleVarName = `${moduleName}ModuleId`;
|
|
4238
|
+
let code = "```csharp\n";
|
|
4239
|
+
code += `// 1. Add module ID variable (get from NavigationModuleSeedData.cs)
|
|
4240
|
+
`;
|
|
4241
|
+
code += `var ${moduleVarName} = Guid.Parse("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"); // TODO: Replace with actual ID
|
|
4032
4242
|
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
{
|
|
4041
|
-
|
|
4042
|
-
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
VALUES (
|
|
4048
|
-
NEWID(),
|
|
4049
|
-
'{{code}}',
|
|
4050
|
-
'{{name}}',
|
|
4051
|
-
'{{description}}',
|
|
4052
|
-
0,
|
|
4053
|
-
GETUTCDATE(),
|
|
4054
|
-
GETUTCDATE()
|
|
4055
|
-
);
|
|
4056
|
-
END
|
|
4057
|
-
");
|
|
4058
|
-
{{/each}}
|
|
4059
|
-
}
|
|
4060
|
-
|
|
4061
|
-
protected override void Down(MigrationBuilder migrationBuilder)
|
|
4062
|
-
{
|
|
4063
|
-
// Remove seeded permissions
|
|
4064
|
-
{{#each permissions}}
|
|
4065
|
-
migrationBuilder.Sql(@"DELETE FROM core.auth_Permissions WHERE Code = '{{code}}'");
|
|
4066
|
-
{{/each}}
|
|
4243
|
+
`;
|
|
4244
|
+
code += `// 2. Add these entries to the HasData() return array:
|
|
4245
|
+
`;
|
|
4246
|
+
for (const perm of permissions) {
|
|
4247
|
+
const isWildcard = perm.code.endsWith(".*");
|
|
4248
|
+
const action = perm.code.split(".").pop();
|
|
4249
|
+
const guidPlaceholder = generatePlaceholderGuid();
|
|
4250
|
+
if (isWildcard) {
|
|
4251
|
+
code += `new { Id = Guid.Parse("${guidPlaceholder}"), Path = "${perm.code}", Level = PermissionLevel.Module, IsWildcard = true, ModuleId = ${moduleVarName}, Description = "${perm.description}", CreatedAt = seedDate },
|
|
4252
|
+
`;
|
|
4253
|
+
} else {
|
|
4254
|
+
const actionEnum = getActionEnum(action || "read");
|
|
4255
|
+
code += `new { Id = Guid.Parse("${guidPlaceholder}"), Path = "${perm.code}", Level = PermissionLevel.Module, Action = PermissionAction.${actionEnum}, IsWildcard = false, ModuleId = ${moduleVarName}, Description = "${perm.description}", CreatedAt = seedDate },
|
|
4256
|
+
`;
|
|
4067
4257
|
}
|
|
4068
|
-
}
|
|
4258
|
+
}
|
|
4259
|
+
code += "```\n";
|
|
4260
|
+
code += "\n\u26A0\uFE0F **IMPORTANT**: Replace all GUIDs with randomly generated ones using:\n";
|
|
4261
|
+
code += "```powershell\n";
|
|
4262
|
+
code += `1..${permissions.length} | ForEach-Object { [guid]::NewGuid().ToString() }
|
|
4069
4263
|
`;
|
|
4264
|
+
code += "```\n";
|
|
4265
|
+
return code;
|
|
4266
|
+
}
|
|
4267
|
+
function generatePlaceholderGuid() {
|
|
4268
|
+
return "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX";
|
|
4269
|
+
}
|
|
4270
|
+
function getActionEnum(action) {
|
|
4271
|
+
const actionMap = {
|
|
4272
|
+
"read": "Read",
|
|
4273
|
+
"create": "Create",
|
|
4274
|
+
"update": "Update",
|
|
4275
|
+
"delete": "Delete",
|
|
4276
|
+
"assign": "Assign",
|
|
4277
|
+
"execute": "Execute",
|
|
4278
|
+
"export": "Export",
|
|
4279
|
+
"import": "Import"
|
|
4280
|
+
};
|
|
4281
|
+
return actionMap[action.toLowerCase()] || action.charAt(0).toUpperCase() + action.slice(1);
|
|
4070
4282
|
}
|
|
4071
4283
|
async function readDirectoryRecursive(dir) {
|
|
4072
4284
|
const files = [];
|
|
@@ -4086,7 +4298,7 @@ async function readDirectoryRecursive(dir) {
|
|
|
4086
4298
|
}
|
|
4087
4299
|
|
|
4088
4300
|
// src/tools/scaffold-tests.ts
|
|
4089
|
-
import
|
|
4301
|
+
import Handlebars2 from "handlebars";
|
|
4090
4302
|
import path12 from "path";
|
|
4091
4303
|
var scaffoldTestsTool = {
|
|
4092
4304
|
name: "scaffold_tests",
|
|
@@ -4166,15 +4378,15 @@ var scaffoldTestsTool = {
|
|
|
4166
4378
|
required: ["target", "name"]
|
|
4167
4379
|
}
|
|
4168
4380
|
};
|
|
4169
|
-
|
|
4381
|
+
Handlebars2.registerHelper("pascalCase", (str) => {
|
|
4170
4382
|
if (!str) return "";
|
|
4171
4383
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
4172
4384
|
});
|
|
4173
|
-
|
|
4385
|
+
Handlebars2.registerHelper("camelCase", (str) => {
|
|
4174
4386
|
if (!str) return "";
|
|
4175
4387
|
return str.charAt(0).toLowerCase() + str.slice(1);
|
|
4176
4388
|
});
|
|
4177
|
-
|
|
4389
|
+
Handlebars2.registerHelper("unless", function(conditional, options) {
|
|
4178
4390
|
if (!conditional) {
|
|
4179
4391
|
return options.fn(this);
|
|
4180
4392
|
}
|
|
@@ -5516,7 +5728,7 @@ async function scaffoldEntityTests(name, options, testTypes, structure, config,
|
|
|
5516
5728
|
...options
|
|
5517
5729
|
};
|
|
5518
5730
|
if (testTypes.includes("unit")) {
|
|
5519
|
-
const content =
|
|
5731
|
+
const content = Handlebars2.compile(entityTestTemplate)(context);
|
|
5520
5732
|
const testPath = path12.join(structure.root, "Tests", "Unit", "Domain", `${name}Tests.cs`);
|
|
5521
5733
|
validatePathSecurity(testPath, structure.root);
|
|
5522
5734
|
if (!dryRun) {
|
|
@@ -5530,7 +5742,7 @@ async function scaffoldEntityTests(name, options, testTypes, structure, config,
|
|
|
5530
5742
|
});
|
|
5531
5743
|
}
|
|
5532
5744
|
if (testTypes.includes("security")) {
|
|
5533
|
-
const securityContent =
|
|
5745
|
+
const securityContent = Handlebars2.compile(securityTestTemplate)({
|
|
5534
5746
|
...context,
|
|
5535
5747
|
nameLower: name.charAt(0).toLowerCase() + name.slice(1),
|
|
5536
5748
|
apiNamespace: config.conventions.namespaces.api
|
|
@@ -5562,7 +5774,7 @@ async function scaffoldServiceTests(name, options, testTypes, structure, config,
|
|
|
5562
5774
|
...options
|
|
5563
5775
|
};
|
|
5564
5776
|
if (testTypes.includes("unit")) {
|
|
5565
|
-
const content =
|
|
5777
|
+
const content = Handlebars2.compile(serviceTestTemplate)(context);
|
|
5566
5778
|
const testPath = path12.join(structure.root, "Tests", "Unit", "Services", `${name}ServiceTests.cs`);
|
|
5567
5779
|
validatePathSecurity(testPath, structure.root);
|
|
5568
5780
|
if (!dryRun) {
|
|
@@ -5592,7 +5804,7 @@ async function scaffoldControllerTests(name, options, testTypes, structure, conf
|
|
|
5592
5804
|
...options
|
|
5593
5805
|
};
|
|
5594
5806
|
if (testTypes.includes("integration")) {
|
|
5595
|
-
const content =
|
|
5807
|
+
const content = Handlebars2.compile(controllerTestTemplate)(context);
|
|
5596
5808
|
const testPath = path12.join(structure.root, "Tests", "Integration", "Controllers", `${name}ControllerTests.cs`);
|
|
5597
5809
|
validatePathSecurity(testPath, structure.root);
|
|
5598
5810
|
if (!dryRun) {
|
|
@@ -5606,7 +5818,7 @@ async function scaffoldControllerTests(name, options, testTypes, structure, conf
|
|
|
5606
5818
|
});
|
|
5607
5819
|
}
|
|
5608
5820
|
if (testTypes.includes("security")) {
|
|
5609
|
-
const securityContent =
|
|
5821
|
+
const securityContent = Handlebars2.compile(securityTestTemplate)(context);
|
|
5610
5822
|
const securityPath = path12.join(structure.root, "Tests", "Security", `${name}SecurityTests.cs`);
|
|
5611
5823
|
validatePathSecurity(securityPath, structure.root);
|
|
5612
5824
|
if (!dryRun) {
|
|
@@ -5631,7 +5843,7 @@ async function scaffoldValidatorTests(name, options, testTypes, structure, confi
|
|
|
5631
5843
|
...options
|
|
5632
5844
|
};
|
|
5633
5845
|
if (testTypes.includes("unit")) {
|
|
5634
|
-
const content =
|
|
5846
|
+
const content = Handlebars2.compile(validatorTestTemplate)(context);
|
|
5635
5847
|
const testPath = path12.join(structure.root, "Tests", "Unit", "Validators", `${name}ValidatorTests.cs`);
|
|
5636
5848
|
validatePathSecurity(testPath, structure.root);
|
|
5637
5849
|
if (!dryRun) {
|
|
@@ -5658,7 +5870,7 @@ async function scaffoldRepositoryTests(name, options, testTypes, structure, conf
|
|
|
5658
5870
|
...options
|
|
5659
5871
|
};
|
|
5660
5872
|
if (testTypes.includes("integration")) {
|
|
5661
|
-
const content =
|
|
5873
|
+
const content = Handlebars2.compile(repositoryTestTemplate)(context);
|
|
5662
5874
|
const testPath = path12.join(structure.root, "Tests", "Integration", "Repositories", `${name}RepositoryTests.cs`);
|
|
5663
5875
|
validatePathSecurity(testPath, structure.root);
|
|
5664
5876
|
if (!dryRun) {
|
|
@@ -10388,6 +10600,76 @@ All tenant-related tables use the \`tenant_\` prefix:
|
|
|
10388
10600
|
|
|
10389
10601
|
---
|
|
10390
10602
|
|
|
10603
|
+
## 10. Frontend Conventions
|
|
10604
|
+
|
|
10605
|
+
### Layout Standards
|
|
10606
|
+
|
|
10607
|
+
All context layouts MUST follow the AdminLayout pattern for consistent user experience.
|
|
10608
|
+
|
|
10609
|
+
#### Standard Structure
|
|
10610
|
+
|
|
10611
|
+
\`\`\`tsx
|
|
10612
|
+
<main className={\`pt-16 transition-all duration-300 \${
|
|
10613
|
+
showSidebar ? (isCollapsed ? 'lg:pl-16' : 'lg:pl-64') : ''
|
|
10614
|
+
}\`}>
|
|
10615
|
+
<div className="p-4 sm:p-6 lg:px-10 h-full overflow-auto">
|
|
10616
|
+
<Outlet />
|
|
10617
|
+
</div>
|
|
10618
|
+
</main>
|
|
10619
|
+
\`\`\`
|
|
10620
|
+
|
|
10621
|
+
#### Layout Rules
|
|
10622
|
+
|
|
10623
|
+
| Rule | Description |
|
|
10624
|
+
|------|-------------|
|
|
10625
|
+
| No \`max-w-*\` | Content MUST occupy full available width |
|
|
10626
|
+
| Horizontal padding | Use \`lg:px-10\` for consistent padding |
|
|
10627
|
+
| Scroll container | Use \`h-full overflow-auto\` for internal scrolling |
|
|
10628
|
+
| Sidebar conditional | \`showSidebar = !!currentAppCode\` |
|
|
10629
|
+
|
|
10630
|
+
### Tab Navigation with URL
|
|
10631
|
+
|
|
10632
|
+
Pages with tabs MUST synchronize tab state with URL for shareable links and browser navigation.
|
|
10633
|
+
|
|
10634
|
+
#### useTabNavigation Hook
|
|
10635
|
+
|
|
10636
|
+
\`\`\`tsx
|
|
10637
|
+
import { useTabNavigation } from '@/hooks/useTabNavigation';
|
|
10638
|
+
|
|
10639
|
+
// 1. Define valid tabs
|
|
10640
|
+
const VALID_TABS = ['info', 'settings', 'permissions'] as const;
|
|
10641
|
+
type TabId = typeof VALID_TABS[number];
|
|
10642
|
+
|
|
10643
|
+
// 2. Use the hook
|
|
10644
|
+
const { activeTab, setActiveTab } = useTabNavigation<TabId>('info', VALID_TABS);
|
|
10645
|
+
|
|
10646
|
+
// 3. Use in JSX
|
|
10647
|
+
<button onClick={() => setActiveTab('settings')}>Settings</button>
|
|
10648
|
+
\`\`\`
|
|
10649
|
+
|
|
10650
|
+
#### URL Behavior
|
|
10651
|
+
|
|
10652
|
+
| URL | Active Tab |
|
|
10653
|
+
|-----|------------|
|
|
10654
|
+
| \`/page\` | Default tab (e.g., \`info\`) |
|
|
10655
|
+
| \`/page?tab=settings\` | \`settings\` |
|
|
10656
|
+
| \`/page?tab=invalid\` | Default tab (fallback) |
|
|
10657
|
+
|
|
10658
|
+
#### Hook Characteristics
|
|
10659
|
+
|
|
10660
|
+
- URL is clean for default tab (no \`?tab=\` parameter)
|
|
10661
|
+
- Invalid tabs fallback to default
|
|
10662
|
+
- Uses \`replace: true\` to avoid polluting browser history
|
|
10663
|
+
|
|
10664
|
+
### Frontend Validation Checks
|
|
10665
|
+
|
|
10666
|
+
| Check | Description |
|
|
10667
|
+
|-------|-------------|
|
|
10668
|
+
| \`layouts\` | Verify no \`max-w-*\` in content wrapper, standard padding present |
|
|
10669
|
+
| \`tabs\` | Verify \`useTabNavigation\` hook usage for pages with tabs |
|
|
10670
|
+
|
|
10671
|
+
---
|
|
10672
|
+
|
|
10391
10673
|
## Quick Reference
|
|
10392
10674
|
|
|
10393
10675
|
| Category | Convention | Example |
|
|
@@ -10420,6 +10702,10 @@ All tenant-related tables use the \`tenant_\` prefix:
|
|
|
10420
10702
|
| Validation | \`withValidation: true\` | FluentValidation validators |
|
|
10421
10703
|
| Repository | \`withRepository: true\` | Interface + Implementation |
|
|
10422
10704
|
| Migration naming | \`suggest_migration\` | {context}_v{version}_{seq}_{Desc} |
|
|
10705
|
+
| Layout wrapper | No \`max-w-*\` | Content fills available width |
|
|
10706
|
+
| Layout padding | \`lg:px-10\` | Standard horizontal padding |
|
|
10707
|
+
| Tab navigation | \`useTabNavigation\` hook | Sync tabs with URL |
|
|
10708
|
+
| Tab URL | \`?tab=name\` | Shareable deep links to tabs |
|
|
10423
10709
|
|
|
10424
10710
|
---
|
|
10425
10711
|
|