@atlashub/smartstack-cli 4.35.0 → 4.37.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 +54 -100
- package/dist/index.js.map +1 -1
- package/dist/mcp-entry.mjs +54 -11
- package/dist/mcp-entry.mjs.map +1 -1
- package/package.json +1 -1
- package/templates/agents/efcore/migration.md +43 -0
- package/templates/agents/efcore/rebase-snapshot.md +36 -0
- package/templates/agents/efcore/squash.md +36 -0
- package/templates/skills/apex/references/checks/seed-checks.sh +1 -1
- package/templates/skills/apex/references/core-seed-data.md +39 -21
- package/templates/skills/application/references/application-roles-template.md +14 -8
- package/templates/skills/application/references/provider-template.md +32 -20
- package/templates/skills/application/templates-frontend.md +294 -2
- package/templates/skills/application/templates-seed.md +23 -11
- package/templates/skills/audit-route/SKILL.md +107 -0
- package/templates/skills/audit-route/references/routing-pattern.md +129 -0
- package/templates/skills/audit-route/steps/step-00-init.md +128 -0
- package/templates/skills/audit-route/steps/step-01-inventory.md +157 -0
- package/templates/skills/audit-route/steps/step-02-conformity.md +193 -0
- package/templates/skills/audit-route/steps/step-03-report.md +201 -0
- package/templates/skills/dev-start/SKILL.md +12 -2
- package/templates/skills/efcore/SKILL.md +219 -67
- package/templates/agents/efcore/conflicts.md +0 -114
- package/templates/agents/efcore/db-deploy.md +0 -86
- package/templates/agents/efcore/db-reset.md +0 -98
- package/templates/agents/efcore/db-seed.md +0 -73
- package/templates/agents/efcore/db-status.md +0 -97
- package/templates/agents/efcore/scan.md +0 -124
- package/templates/skills/efcore/references/both-contexts.md +0 -32
- package/templates/skills/efcore/references/destructive-operations.md +0 -38
- package/templates/skills/efcore/steps/db/step-deploy.md +0 -217
- package/templates/skills/efcore/steps/db/step-reset.md +0 -186
- package/templates/skills/efcore/steps/db/step-seed.md +0 -166
- package/templates/skills/efcore/steps/db/step-status.md +0 -173
- package/templates/skills/efcore/steps/migration/step-00-init.md +0 -102
- package/templates/skills/efcore/steps/migration/step-01-check.md +0 -164
- package/templates/skills/efcore/steps/migration/step-02-create.md +0 -160
- package/templates/skills/efcore/steps/migration/step-03-validate.md +0 -168
- package/templates/skills/efcore/steps/rebase-snapshot/step-00-init.md +0 -173
- package/templates/skills/efcore/steps/rebase-snapshot/step-01-backup.md +0 -100
- package/templates/skills/efcore/steps/rebase-snapshot/step-02-fetch.md +0 -115
- package/templates/skills/efcore/steps/rebase-snapshot/step-03-create.md +0 -112
- package/templates/skills/efcore/steps/rebase-snapshot/step-04-validate.md +0 -157
- package/templates/skills/efcore/steps/shared/step-00-init.md +0 -131
- package/templates/skills/efcore/steps/squash/step-00-init.md +0 -141
- package/templates/skills/efcore/steps/squash/step-01-backup.md +0 -120
- package/templates/skills/efcore/steps/squash/step-02-fetch.md +0 -168
- package/templates/skills/efcore/steps/squash/step-03-create.md +0 -184
- package/templates/skills/efcore/steps/squash/step-04-validate.md +0 -174
package/package.json
CHANGED
|
@@ -114,6 +114,49 @@ rm Persistence/Migrations/*${OLD_NAME}*.cs
|
|
|
114
114
|
|
|
115
115
|
> **Note:** `$MCP_NAME` is obtained via MCP call, never computed locally.
|
|
116
116
|
|
|
117
|
+
## SQL Objects Post-Injection (MANDATORY after every migration creation)
|
|
118
|
+
|
|
119
|
+
**This step is NON-OPTIONAL.** After EVERY `dotnet ef migrations add`, you MUST check for SQL objects and inject them into the generated migration. Skipping this step causes runtime 500 errors on ALL endpoints (TVFs like `fn_GetUserGroupHierarchy` won't exist in the database).
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
# Detect the generated migration file (the main .cs, not .Designer.cs)
|
|
123
|
+
MIGRATION_FILE=$(find "$MIGRATIONS_DIR" -name "*${MCP_NAME}.cs" ! -name "*.Designer.cs" | head -1)
|
|
124
|
+
SQL_OBJECTS_DIR="$INFRA_PROJECT_DIR/Persistence/SqlObjects"
|
|
125
|
+
SQL_FILES=$(find "$SQL_OBJECTS_DIR" -name "*.sql" 2>/dev/null | wc -l)
|
|
126
|
+
|
|
127
|
+
if [ "$SQL_FILES" -gt 0 ]; then
|
|
128
|
+
echo "Found $SQL_FILES SQL object(s) — injecting SqlObjectHelper.ApplyAll()..."
|
|
129
|
+
|
|
130
|
+
# Add using directive if not present
|
|
131
|
+
if ! grep -q "using SmartStack.Infrastructure.Persistence.SqlObjects;" "$MIGRATION_FILE"; then
|
|
132
|
+
sed -i '1s/^/using SmartStack.Infrastructure.Persistence.SqlObjects;\n/' "$MIGRATION_FILE"
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
# Add SqlObjectHelper.ApplyAll at the end of Up() method
|
|
136
|
+
# Use Edit tool to insert before the closing brace of Up()
|
|
137
|
+
# Target: the last " }" before "protected override void Down"
|
|
138
|
+
sed -i '/protected override void Down/i\
|
|
139
|
+
\ // Apply SQL objects (TVF, Views, SP) from embedded resources\
|
|
140
|
+
\ SqlObjectHelper.ApplyAll(migrationBuilder);' "$MIGRATION_FILE"
|
|
141
|
+
|
|
142
|
+
# VERIFY injection succeeded — FAIL if not
|
|
143
|
+
if ! grep -q "SqlObjectHelper.ApplyAll" "$MIGRATION_FILE"; then
|
|
144
|
+
echo "ERROR: Auto-injection failed. Use Edit tool to manually add:"
|
|
145
|
+
echo " 1. Add 'using SmartStack.Infrastructure.Persistence.SqlObjects;' at top"
|
|
146
|
+
echo " 2. Add 'SqlObjectHelper.ApplyAll(migrationBuilder);' at end of Up() method"
|
|
147
|
+
echo " File: $MIGRATION_FILE"
|
|
148
|
+
# DO NOT CONTINUE — fix this before proceeding
|
|
149
|
+
else
|
|
150
|
+
echo " SqlObjectHelper.ApplyAll(migrationBuilder) injected successfully"
|
|
151
|
+
find "$SQL_OBJECTS_DIR" -name "*.sql" -exec basename {} \; | while read f; do echo " - $f"; done
|
|
152
|
+
fi
|
|
153
|
+
else
|
|
154
|
+
echo "No SQL objects found in SqlObjects/ — skipping injection"
|
|
155
|
+
fi
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
> **CRITICAL:** If the bash `sed` injection fails (indentation mismatch), use the **Edit tool** to manually insert the lines. NEVER skip this step — it causes cascading 500 errors at runtime.
|
|
159
|
+
|
|
117
160
|
## Context Detection
|
|
118
161
|
|
|
119
162
|
If unable to auto-detect:
|
|
@@ -70,6 +70,42 @@ mcp__smartstack__suggest_migration({ description: "...", context: DBCONTEXT_TYPE
|
|
|
70
70
|
|
|
71
71
|
dotnet ef migrations add "$MIGRATION_NAME" --context "$DBCONTEXT"
|
|
72
72
|
|
|
73
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
74
|
+
# MANDATORY: SQL Objects Post-Injection
|
|
75
|
+
# Without this, TVFs (fn_GetUserGroupHierarchy etc.) won't exist in DB
|
|
76
|
+
# and ALL endpoints will return 500 at runtime
|
|
77
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
78
|
+
MIGRATION_FILE=$(find Migrations -name "*${MIGRATION_NAME}.cs" ! -name "*.Designer.cs" | head -1)
|
|
79
|
+
SQL_OBJECTS_DIR="$INFRA_PROJECT_DIR/Persistence/SqlObjects"
|
|
80
|
+
SQL_FILES=$(find "$SQL_OBJECTS_DIR" -name "*.sql" 2>/dev/null | wc -l)
|
|
81
|
+
|
|
82
|
+
if [ "$SQL_FILES" -gt 0 ]; then
|
|
83
|
+
echo "Found $SQL_FILES SQL object(s) — injecting SqlObjectHelper.ApplyAll()..."
|
|
84
|
+
|
|
85
|
+
# Add using directive if not present
|
|
86
|
+
if ! grep -q "using SmartStack.Infrastructure.Persistence.SqlObjects;" "$MIGRATION_FILE"; then
|
|
87
|
+
sed -i '1s/^/using SmartStack.Infrastructure.Persistence.SqlObjects;\n/' "$MIGRATION_FILE"
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
# Inject SqlObjectHelper.ApplyAll before Down() method
|
|
91
|
+
sed -i '/protected override void Down/i\
|
|
92
|
+
\ // Apply SQL objects (TVF, Views, SP) from embedded resources\
|
|
93
|
+
\ SqlObjectHelper.ApplyAll(migrationBuilder);' "$MIGRATION_FILE"
|
|
94
|
+
|
|
95
|
+
# VERIFY — FAIL if injection didn't work
|
|
96
|
+
if ! grep -q "SqlObjectHelper.ApplyAll" "$MIGRATION_FILE"; then
|
|
97
|
+
echo "ERROR: Auto-injection failed. Use Edit tool to manually add:"
|
|
98
|
+
echo " 1. 'using SmartStack.Infrastructure.Persistence.SqlObjects;' at top"
|
|
99
|
+
echo " 2. 'SqlObjectHelper.ApplyAll(migrationBuilder);' at end of Up()"
|
|
100
|
+
echo " File: $MIGRATION_FILE"
|
|
101
|
+
else
|
|
102
|
+
echo " SqlObjectHelper.ApplyAll(migrationBuilder) injected"
|
|
103
|
+
find "$SQL_OBJECTS_DIR" -name "*.sql" -exec basename {} \; | while read f; do echo " - $f"; done
|
|
104
|
+
fi
|
|
105
|
+
else
|
|
106
|
+
echo "No SQL objects found — skipping injection"
|
|
107
|
+
fi
|
|
108
|
+
|
|
73
109
|
# Validate
|
|
74
110
|
dotnet build
|
|
75
111
|
```
|
|
@@ -100,6 +100,42 @@ mcp__smartstack__suggest_migration({ description: "...", context: DBCONTEXT_TYPE
|
|
|
100
100
|
|
|
101
101
|
dotnet ef migrations add "$MIGRATION_NAME_FROM_MCP" --context "$DBCONTEXT"
|
|
102
102
|
|
|
103
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
104
|
+
# MANDATORY: SQL Objects Post-Injection
|
|
105
|
+
# Without this, TVFs (fn_GetUserGroupHierarchy etc.) won't exist in DB
|
|
106
|
+
# and ALL endpoints will return 500 at runtime
|
|
107
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
108
|
+
MIGRATION_FILE=$(find Migrations -name "*${MIGRATION_NAME_FROM_MCP}.cs" ! -name "*.Designer.cs" | head -1)
|
|
109
|
+
SQL_OBJECTS_DIR="$INFRA_PROJECT_DIR/Persistence/SqlObjects"
|
|
110
|
+
SQL_FILES=$(find "$SQL_OBJECTS_DIR" -name "*.sql" 2>/dev/null | wc -l)
|
|
111
|
+
|
|
112
|
+
if [ "$SQL_FILES" -gt 0 ]; then
|
|
113
|
+
echo "Found $SQL_FILES SQL object(s) — injecting SqlObjectHelper.ApplyAll()..."
|
|
114
|
+
|
|
115
|
+
# Add using directive if not present
|
|
116
|
+
if ! grep -q "using SmartStack.Infrastructure.Persistence.SqlObjects;" "$MIGRATION_FILE"; then
|
|
117
|
+
sed -i '1s/^/using SmartStack.Infrastructure.Persistence.SqlObjects;\n/' "$MIGRATION_FILE"
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
# Inject SqlObjectHelper.ApplyAll before Down() method
|
|
121
|
+
sed -i '/protected override void Down/i\
|
|
122
|
+
\ // Apply SQL objects (TVF, Views, SP) from embedded resources\
|
|
123
|
+
\ SqlObjectHelper.ApplyAll(migrationBuilder);' "$MIGRATION_FILE"
|
|
124
|
+
|
|
125
|
+
# VERIFY — FAIL if injection didn't work
|
|
126
|
+
if ! grep -q "SqlObjectHelper.ApplyAll" "$MIGRATION_FILE"; then
|
|
127
|
+
echo "ERROR: Auto-injection failed. Use Edit tool to manually add:"
|
|
128
|
+
echo " 1. 'using SmartStack.Infrastructure.Persistence.SqlObjects;' at top"
|
|
129
|
+
echo " 2. 'SqlObjectHelper.ApplyAll(migrationBuilder);' at end of Up()"
|
|
130
|
+
echo " File: $MIGRATION_FILE"
|
|
131
|
+
else
|
|
132
|
+
echo " SqlObjectHelper.ApplyAll(migrationBuilder) injected"
|
|
133
|
+
find "$SQL_OBJECTS_DIR" -name "*.sql" -exec basename {} \; | while read f; do echo " - $f"; done
|
|
134
|
+
fi
|
|
135
|
+
else
|
|
136
|
+
echo "No SQL objects found — skipping injection"
|
|
137
|
+
fi
|
|
138
|
+
|
|
103
139
|
# Validate
|
|
104
140
|
dotnet build && dotnet ef migrations script --idempotent > /dev/null
|
|
105
141
|
```
|
|
@@ -249,7 +249,7 @@ if [ -n "$PROVIDER" ]; then
|
|
|
249
249
|
echo "CRITICAL: Translation seed data inserts without idempotency guard in $PROVIDER"
|
|
250
250
|
echo "Fix: Before each NavigationTranslations.Add block, check existence:"
|
|
251
251
|
echo " if (!await context.NavigationTranslations.AnyAsync("
|
|
252
|
-
echo " t => t.EntityId ==
|
|
252
|
+
echo " t => t.EntityId == mod1Entity.Id // actual DB ID, NOT seed-time GUID"
|
|
253
253
|
echo " && t.EntityType == NavigationEntityType.Module, ct))"
|
|
254
254
|
echo " { foreach (var t in ...) { context.NavigationTranslations.Add(...); } }"
|
|
255
255
|
echo "The unique index IX_nav_Translations_EntityType_EntityId_LanguageCode will crash on duplicates."
|
|
@@ -109,6 +109,7 @@ public static class NavigationApplicationSeedData
|
|
|
109
109
|
return new NavigationApplicationSeedEntry
|
|
110
110
|
{
|
|
111
111
|
Id = ApplicationId,
|
|
112
|
+
Zone = ApplicationZone.Business,
|
|
112
113
|
Code = "{appCode}",
|
|
113
114
|
Label = "{appLabel_en}",
|
|
114
115
|
Description = "{appDesc_en}",
|
|
@@ -198,6 +199,7 @@ public static class NavigationApplicationSeedData
|
|
|
198
199
|
public class NavigationApplicationSeedEntry
|
|
199
200
|
{
|
|
200
201
|
public Guid Id { get; init; }
|
|
202
|
+
public ApplicationZone Zone { get; init; }
|
|
201
203
|
public string Code { get; init; } = null!;
|
|
202
204
|
public string Label { get; init; } = null!;
|
|
203
205
|
public string? Description { get; init; }
|
|
@@ -1007,11 +1009,12 @@ public class RolePermissionSeedEntry
|
|
|
1007
1009
|
|
|
1008
1010
|
| Rule | Description |
|
|
1009
1011
|
|------|-------------|
|
|
1010
|
-
| Factory methods | `
|
|
1012
|
+
| Factory methods | `NavigationApplication.Create(zone, ...)`, `Role.Create(name, shortName, category, ...)`, `Permission.CreateForModule(...)` — NEVER `new Entity()` |
|
|
1011
1013
|
| Idempotence | Each Seed method checks existence before inserting |
|
|
1012
1014
|
| Execution order | Navigation → Roles → Permissions → RolePermissions (roles MUST exist before mapping) |
|
|
1013
1015
|
| SaveChanges per group | Navigation -> save -> Roles -> save -> Permissions -> save -> RolePermissions -> save |
|
|
1014
|
-
| FK resolution by Code | Parent entities (modules, roles) found by `Code
|
|
1016
|
+
| FK resolution by Code | Parent entities (applications, modules, roles) found by `Code` from DB, not seed-time GUID |
|
|
1017
|
+
| Translation EntityId | ALWAYS use actual DB ID (`app.Id`, `mod1Entity.Id`, `actualSection.Id`), NEVER seed-time GUID from SeedData classes |
|
|
1015
1018
|
| DI registration | `services.AddScoped<IClientSeedDataProvider, {AppPascalName}SeedDataProvider>()` |
|
|
1016
1019
|
|
|
1017
1020
|
### Template
|
|
@@ -1054,7 +1057,7 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
|
|
|
1054
1057
|
else
|
|
1055
1058
|
{
|
|
1056
1059
|
app = NavigationApplication.Create(
|
|
1057
|
-
appEntry.Code, appEntry.Label,
|
|
1060
|
+
appEntry.Zone, appEntry.Code, appEntry.Label,
|
|
1058
1061
|
appEntry.Description, appEntry.Icon, appEntry.IconType,
|
|
1059
1062
|
appEntry.Route, appEntry.DisplayOrder);
|
|
1060
1063
|
context.NavigationApplications.Add(app);
|
|
@@ -1064,7 +1067,7 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
|
|
|
1064
1067
|
foreach (var t in NavigationApplicationSeedData.GetTranslationEntries())
|
|
1065
1068
|
{
|
|
1066
1069
|
context.NavigationTranslations.Add(
|
|
1067
|
-
NavigationTranslation.Create(t.EntityType,
|
|
1070
|
+
NavigationTranslation.Create(t.EntityType, app.Id, t.LanguageCode, t.Label, t.Description));
|
|
1068
1071
|
}
|
|
1069
1072
|
await ((DbContext)context).SaveChangesAsync(ct);
|
|
1070
1073
|
}
|
|
@@ -1097,14 +1100,15 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
|
|
|
1097
1100
|
// --- Module translations (idempotent — unique index IX_nav_Translations_EntityType_EntityId_LanguageCode) ---
|
|
1098
1101
|
// Always check existence before inserting translations to avoid duplicate key errors
|
|
1099
1102
|
// on re-runs, partial failures, or DB reset scenarios.
|
|
1103
|
+
// CRITICAL: Use mod1Entity.Id (actual DB ID), NOT seed-time GUID from SeedData class.
|
|
1100
1104
|
if (!await context.NavigationTranslations.AnyAsync(
|
|
1101
|
-
t => t.EntityId ==
|
|
1105
|
+
t => t.EntityId == mod1Entity.Id
|
|
1102
1106
|
&& t.EntityType == NavigationEntityType.Module, ct))
|
|
1103
1107
|
{
|
|
1104
1108
|
foreach (var t in {Module1Pascal}NavigationSeedData.GetTranslationEntries())
|
|
1105
1109
|
{
|
|
1106
1110
|
context.NavigationTranslations.Add(
|
|
1107
|
-
NavigationTranslation.Create(t.EntityType,
|
|
1111
|
+
NavigationTranslation.Create(t.EntityType, mod1Entity.Id, t.LanguageCode, t.Label, t.Description));
|
|
1108
1112
|
}
|
|
1109
1113
|
}
|
|
1110
1114
|
// Repeat for each module...
|
|
@@ -1129,16 +1133,21 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
|
|
|
1129
1133
|
await ((DbContext)context).SaveChangesAsync(ct);
|
|
1130
1134
|
|
|
1131
1135
|
// --- Section translations (idempotent — check before inserting) ---
|
|
1136
|
+
// CRITICAL: secEntry.Id is a seed-time GUID ≠ actual DB ID.
|
|
1137
|
+
// Resolve actual section from DB by Code+ModuleId to get the real ID.
|
|
1132
1138
|
foreach (var secEntry in {Module1Pascal}NavigationSeedData.GetSectionEntries(mod1Entity.Id))
|
|
1133
1139
|
{
|
|
1140
|
+
var actualSection = await context.NavigationSections
|
|
1141
|
+
.FirstAsync(s => s.Code == secEntry.Code && s.ModuleId == mod1Entity.Id, ct);
|
|
1142
|
+
|
|
1134
1143
|
if (!await context.NavigationTranslations.AnyAsync(
|
|
1135
|
-
t => t.EntityId ==
|
|
1144
|
+
t => t.EntityId == actualSection.Id && t.EntityType == NavigationEntityType.Section, ct))
|
|
1136
1145
|
{
|
|
1137
1146
|
foreach (var t in {Module1Pascal}NavigationSeedData.GetSectionTranslationEntries()
|
|
1138
1147
|
.Where(st => st.EntityId == secEntry.Id))
|
|
1139
1148
|
{
|
|
1140
1149
|
context.NavigationTranslations.Add(
|
|
1141
|
-
NavigationTranslation.Create(t.EntityType,
|
|
1150
|
+
NavigationTranslation.Create(t.EntityType, actualSection.Id, t.LanguageCode, t.Label, t.Description));
|
|
1142
1151
|
}
|
|
1143
1152
|
}
|
|
1144
1153
|
}
|
|
@@ -1174,10 +1183,13 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
|
|
|
1174
1183
|
|
|
1175
1184
|
public async Task SeedRolesAsync(ICoreDbContext context, CancellationToken ct)
|
|
1176
1185
|
{
|
|
1177
|
-
//
|
|
1178
|
-
|
|
1186
|
+
// Resolve application from DB by Code — NOT seed-time GUID
|
|
1187
|
+
var app = await context.NavigationApplications
|
|
1188
|
+
.FirstAsync(a => a.Code == "{appCode}", ct);
|
|
1189
|
+
|
|
1190
|
+
// Check idempotence — verify by Code, scoped by actual DB ApplicationId
|
|
1179
1191
|
var existingRoleCodes = await context.Roles
|
|
1180
|
-
.Where(r => r.ApplicationId ==
|
|
1192
|
+
.Where(r => r.ApplicationId == app.Id
|
|
1181
1193
|
&& (r.Code == "admin" || r.Code == "manager" || r.Code == "contributor" || r.Code == "viewer"))
|
|
1182
1194
|
.Select(r => r.Code)
|
|
1183
1195
|
.ToListAsync(ct);
|
|
@@ -1188,11 +1200,13 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
|
|
|
1188
1200
|
if (existingRoleCodes.Contains(entry.Code)) continue; // Skip if already exists
|
|
1189
1201
|
|
|
1190
1202
|
var role = Role.Create(
|
|
1191
|
-
entry.
|
|
1192
|
-
entry.
|
|
1193
|
-
|
|
1194
|
-
entry.
|
|
1195
|
-
entry.IsSystem
|
|
1203
|
+
name: entry.Name,
|
|
1204
|
+
shortName: entry.Code,
|
|
1205
|
+
category: RoleCategory.Application,
|
|
1206
|
+
description: entry.Description,
|
|
1207
|
+
isSystem: entry.IsSystem,
|
|
1208
|
+
applicationId: app.Id,
|
|
1209
|
+
code: entry.Code);
|
|
1196
1210
|
context.Roles.Add(role);
|
|
1197
1211
|
}
|
|
1198
1212
|
await ((DbContext)context).SaveChangesAsync(ct);
|
|
@@ -1231,13 +1245,17 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
|
|
|
1231
1245
|
.AnyAsync(rp => rp.Permission!.Path.StartsWith("{appCode}."), ct);
|
|
1232
1246
|
if (exists) return;
|
|
1233
1247
|
|
|
1248
|
+
// Resolve application from DB by Code — NOT seed-time GUID
|
|
1249
|
+
var app = await context.NavigationApplications
|
|
1250
|
+
.FirstAsync(a => a.Code == "{appCode}", ct);
|
|
1251
|
+
|
|
1234
1252
|
// Resolve roles by Code from DB — do not use hardcoded GUIDs.
|
|
1235
1253
|
// Application-scoped roles (admin, manager, contributor, viewer) are created by
|
|
1236
1254
|
// SeedRolesAsync() above. System roles use their own IDs that do not match
|
|
1237
1255
|
// DeterministicGuid("role:admin").
|
|
1238
1256
|
// Load THIS application's roles + system roles only
|
|
1239
1257
|
var roles = await context.Roles
|
|
1240
|
-
.Where(r => r.ApplicationId ==
|
|
1258
|
+
.Where(r => r.ApplicationId == app.Id || r.IsSystem)
|
|
1241
1259
|
.ToListAsync(ct);
|
|
1242
1260
|
|
|
1243
1261
|
// Resolve permissions
|
|
@@ -1333,11 +1351,11 @@ Before marking the task as completed, verify ALL:
|
|
|
1333
1351
|
|
|
1334
1352
|
**Application-Level (FIRST — before modules):**
|
|
1335
1353
|
- [ ] `NavigationApplicationSeedData.cs` created (once per application, at `Infrastructure/Persistence/Seeding/Data/`)
|
|
1336
|
-
- [ ] Application GUID is
|
|
1337
|
-
- [ ] GetApplicationEntry() takes no parameters (no contextId)
|
|
1338
|
-
- [ ] Application translations created (4 languages: fr, en, it, de, EntityType = Application)
|
|
1354
|
+
- [ ] Application GUID is random (`Guid.NewGuid()`) — FK resolution is by Code lookup, not fixed ID
|
|
1355
|
+
- [ ] GetApplicationEntry() takes no parameters (no contextId), includes `Zone = ApplicationZone.Business`
|
|
1356
|
+
- [ ] Application translations created (4 languages: fr, en, it, de, EntityType = Application), using `app.Id` (actual DB ID) for EntityId
|
|
1339
1357
|
- [ ] `IClientSeedDataProvider.SeedNavigationAsync()` uses `NavigationApplicationSeedData` (NO hardcoded `{appLabel_en}` / `{appIcon}` placeholders)
|
|
1340
|
-
- [ ] `ApplicationRolesSeedData.ApplicationId` references `NavigationApplicationSeedData.ApplicationId` (
|
|
1358
|
+
- [ ] `ApplicationRolesSeedData.ApplicationId` references `NavigationApplicationSeedData.ApplicationId` (DTO only — provider code resolves from DB by Code)
|
|
1341
1359
|
|
|
1342
1360
|
**Module-Level:**
|
|
1343
1361
|
- [ ] Random GUIDs via `Guid.NewGuid()` (no deterministic/sequential/fixed values)
|
|
@@ -147,20 +147,26 @@ Add a new method `SeedRolesAsync()` to the provider:
|
|
|
147
147
|
```csharp
|
|
148
148
|
public async Task SeedRolesAsync(ICoreDbContext context, CancellationToken ct)
|
|
149
149
|
{
|
|
150
|
-
//
|
|
150
|
+
// Resolve application from DB by Code — NOT seed-time GUID
|
|
151
|
+
var app = await context.NavigationApplications
|
|
152
|
+
.FirstAsync(a => a.Code == "{app_code}", ct);
|
|
153
|
+
|
|
154
|
+
// Check idempotence — scoped by actual DB ApplicationId
|
|
151
155
|
var exists = await context.Roles
|
|
152
|
-
.AnyAsync(r => r.ApplicationId ==
|
|
156
|
+
.AnyAsync(r => r.ApplicationId == app.Id, ct);
|
|
153
157
|
if (exists) return;
|
|
154
158
|
|
|
155
159
|
// Create application-scoped roles using factory method
|
|
156
160
|
foreach (var entry in ApplicationRolesSeedData.GetRoleEntries())
|
|
157
161
|
{
|
|
158
162
|
var role = Role.Create(
|
|
159
|
-
entry.
|
|
160
|
-
entry.
|
|
161
|
-
|
|
162
|
-
entry.
|
|
163
|
-
entry.IsSystem
|
|
163
|
+
name: entry.Name,
|
|
164
|
+
shortName: entry.Code,
|
|
165
|
+
category: RoleCategory.Application,
|
|
166
|
+
description: entry.Description,
|
|
167
|
+
isSystem: entry.IsSystem,
|
|
168
|
+
applicationId: app.Id,
|
|
169
|
+
code: entry.Code);
|
|
164
170
|
|
|
165
171
|
context.Roles.Add(role);
|
|
166
172
|
}
|
|
@@ -205,7 +211,7 @@ Before marking the task as completed, verify:
|
|
|
205
211
|
## Notes
|
|
206
212
|
|
|
207
213
|
- **Application ID source:** Read from the navigation application created in `SeedNavigationAsync()` or from `{AppPascal}NavigationSeedData.cs`
|
|
208
|
-
- **Role factory method:** Use `Role.Create(
|
|
214
|
+
- **Role factory method:** Use `Role.Create(name, shortName, category, description, isSystem, applicationId: ..., code: ...)` from SmartStack.Domain
|
|
209
215
|
- **Code uniqueness:** Role codes must be unique within the application scope
|
|
210
216
|
- **System roles:** These are NOT system roles (IsSystem = false) - they are application-scoped roles
|
|
211
217
|
- **Tenant isolation:** Application-scoped roles are automatically tenant-isolated via the Core authorization system
|
|
@@ -43,7 +43,7 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
|
|
|
43
43
|
else
|
|
44
44
|
{
|
|
45
45
|
app = NavigationApplication.Create(
|
|
46
|
-
appEntry.Code, appEntry.Label,
|
|
46
|
+
ApplicationZone.Business, appEntry.Code, appEntry.Label,
|
|
47
47
|
appEntry.Description, appEntry.Icon, appEntry.IconType,
|
|
48
48
|
appEntry.Route, appEntry.DisplayOrder);
|
|
49
49
|
context.NavigationApplications.Add(app);
|
|
@@ -53,7 +53,7 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
|
|
|
53
53
|
foreach (var t in NavigationApplicationSeedData.GetTranslationEntries())
|
|
54
54
|
{
|
|
55
55
|
context.NavigationTranslations.Add(
|
|
56
|
-
NavigationTranslation.Create(t.EntityType,
|
|
56
|
+
NavigationTranslation.Create(t.EntityType, app.Id, t.LanguageCode, t.Label, t.Description));
|
|
57
57
|
}
|
|
58
58
|
await ((DbContext)context).SaveChangesAsync(ct);
|
|
59
59
|
}
|
|
@@ -72,18 +72,24 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
|
|
|
72
72
|
// --- Module translations (IDEMPOTENT — check before inserting) ---
|
|
73
73
|
// CRITICAL: nav_Translations has unique index IX_nav_Translations_EntityType_EntityId_LanguageCode
|
|
74
74
|
// Always guard with AnyAsync to prevent duplicate key errors on re-runs.
|
|
75
|
+
// CRITICAL: Use modEntity.Id (actual DB ID), NOT seed-time GUID from SeedData class.
|
|
75
76
|
// if (!await context.NavigationTranslations.AnyAsync(
|
|
76
|
-
// t => t.EntityId ==
|
|
77
|
+
// t => t.EntityId == mod1Entity.Id
|
|
77
78
|
// && t.EntityType == NavigationEntityType.Module, ct))
|
|
78
|
-
// { foreach (var t in {Module}NavigationSeedData.GetTranslationEntries())
|
|
79
|
+
// { foreach (var t in {Module}NavigationSeedData.GetTranslationEntries())
|
|
80
|
+
// { context.NavigationTranslations.Add(
|
|
81
|
+
// NavigationTranslation.Create(t.EntityType, mod1Entity.Id, t.LanguageCode, t.Label, t.Description)); } }
|
|
79
82
|
await ((DbContext)context).SaveChangesAsync(ct);
|
|
80
83
|
|
|
81
84
|
// --- Sections (idempotent per-section) ---
|
|
82
85
|
// Check each section before inserting: AnyAsync(s => s.Code == secEntry.Code && s.ModuleId == ...)
|
|
83
86
|
|
|
84
87
|
// --- Section translations (IDEMPOTENT — same guard pattern as module translations) ---
|
|
88
|
+
// CRITICAL: secEntry.Id is a seed-time GUID ≠ actual DB ID.
|
|
89
|
+
// Resolve actual section from DB: var actualSection = await context.NavigationSections
|
|
90
|
+
// .FirstAsync(s => s.Code == secEntry.Code && s.ModuleId == modEntity.Id, ct);
|
|
85
91
|
// if (!await context.NavigationTranslations.AnyAsync(
|
|
86
|
-
// t => t.EntityId ==
|
|
92
|
+
// t => t.EntityId == actualSection.Id && t.EntityType == NavigationEntityType.Section, ct))
|
|
87
93
|
|
|
88
94
|
// --- Resources (idempotent — use ACTUAL section IDs from DB) ---
|
|
89
95
|
// CRITICAL: NavigationSection.Create() generates its own random ID.
|
|
@@ -95,10 +101,13 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
|
|
|
95
101
|
|
|
96
102
|
public async Task SeedRolesAsync(ICoreDbContext context, CancellationToken ct)
|
|
97
103
|
{
|
|
98
|
-
//
|
|
99
|
-
var
|
|
104
|
+
// Resolve application from DB by Code — NOT seed-time GUID
|
|
105
|
+
var app = await context.NavigationApplications
|
|
106
|
+
.FirstAsync(a => a.Code == "{app_code}", ct);
|
|
107
|
+
|
|
108
|
+
// Check idempotence — scoped by actual DB ApplicationId
|
|
100
109
|
var exists = await context.Roles
|
|
101
|
-
.AnyAsync(r => r.ApplicationId ==
|
|
110
|
+
.AnyAsync(r => r.ApplicationId == app.Id, ct);
|
|
102
111
|
if (exists) return;
|
|
103
112
|
|
|
104
113
|
// Create application-scoped roles (Admin, Manager, Contributor, Viewer)
|
|
@@ -106,11 +115,13 @@ public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
|
|
|
106
115
|
foreach (var entry in ApplicationRolesSeedData.GetRoleEntries())
|
|
107
116
|
{
|
|
108
117
|
var role = Role.Create(
|
|
109
|
-
entry.
|
|
110
|
-
entry.
|
|
111
|
-
|
|
112
|
-
entry.
|
|
113
|
-
entry.IsSystem
|
|
118
|
+
name: entry.Name,
|
|
119
|
+
shortName: entry.Code,
|
|
120
|
+
category: RoleCategory.Application,
|
|
121
|
+
description: entry.Description,
|
|
122
|
+
isSystem: entry.IsSystem,
|
|
123
|
+
applicationId: app.Id,
|
|
124
|
+
code: entry.Code);
|
|
114
125
|
context.Roles.Add(role);
|
|
115
126
|
}
|
|
116
127
|
await ((DbContext)context).SaveChangesAsync(ct);
|
|
@@ -167,11 +178,12 @@ services.AddScoped<IClientSeedDataProvider, {AppPascalName}SeedDataProvider>();
|
|
|
167
178
|
|
|
168
179
|
## Critical Rules
|
|
169
180
|
|
|
170
|
-
1. **Factory methods mandatory**: `NavigationModule.Create(...)`, `Role.Create(...)`, `Permission.CreateForModule(...)`, `RolePermission.Create(...)` — NEVER `new Entity()`
|
|
181
|
+
1. **Factory methods mandatory**: `NavigationApplication.Create(zone, ...)`, `NavigationModule.Create(...)`, `Role.Create(name, shortName, category, ...)`, `Permission.CreateForModule(...)`, `RolePermission.Create(...)` — NEVER `new Entity()`
|
|
171
182
|
2. **Idempotence**: Each Seed method checks existence before inserting. **Translations require explicit `AnyAsync` guard** — unique index `IX_nav_Translations_EntityType_EntityId_LanguageCode` causes crashes on duplicate inserts
|
|
172
|
-
3. **Translation
|
|
173
|
-
4. **
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
183
|
+
3. **Translation EntityId**: ALWAYS use actual DB ID (e.g. `app.Id`, `mod1Entity.Id`, `actualSection.Id`), NEVER seed-time GUID from SeedData classes
|
|
184
|
+
4. **Translation guard pattern**: `if (!await context.NavigationTranslations.AnyAsync(t => t.EntityId == {actualDbId} && t.EntityType == ..., ct))` — MANDATORY before every translation batch insert
|
|
185
|
+
5. **SaveChangesAsync per group**: Navigation → save → Roles → save → Permissions → save → RolePermissions → save
|
|
186
|
+
6. **Execution order**: `SeedRolesAsync()` MUST be called BEFORE `SeedRolePermissionsAsync()` (roles must exist before mapping)
|
|
187
|
+
7. **Random GUIDs**: ALWAYS use `Guid.NewGuid()` — resolve FKs by Code lookup, not fixed IDs
|
|
188
|
+
8. **Resolve FK by Code**: Parent entities (applications, modules, roles) are found by `Code` from DB, not by seed-time GUID
|
|
189
|
+
9. **Order property**: Use `100` as default. If multiple providers exist, they run in Order sequence
|