@atlashub/smartstack-cli 3.39.0 → 3.41.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/.documentation/apex.html +644 -644
- package/.documentation/css/styles.css +2320 -2320
- package/.documentation/init.html +1377 -1377
- package/.documentation/js/app.js +780 -780
- package/.documentation/prd-json-v2.0.0.md +396 -396
- package/.documentation/testing-ba-e2e.md +462 -462
- package/config/default-config.json +95 -95
- package/config/mcp-defaults.json +62 -62
- package/config/settings.json +53 -53
- package/config/settings.local.example.json +16 -16
- package/dist/index.js +6 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp-entry.mjs +6 -4
- package/dist/mcp-entry.mjs.map +1 -1
- package/package.json +115 -115
- package/scripts/extract-api-endpoints.ts +325 -325
- package/scripts/extract-business-rules.ts +440 -440
- package/scripts/generate-doc-with-mock-ui.ts +804 -804
- package/scripts/health-check.sh +168 -168
- package/scripts/postinstall.js +18 -18
- package/templates/agents/action.md +37 -37
- package/templates/agents/ba-reader.md +378 -378
- package/templates/agents/ba-writer.md +861 -861
- package/templates/agents/code-reviewer.md +163 -163
- package/templates/agents/db-reader.md +149 -149
- package/templates/agents/docs-context-reader.md +143 -143
- package/templates/agents/docs-sync-checker.md +122 -122
- package/templates/agents/efcore/conflicts.md +95 -84
- package/templates/agents/efcore/db-deploy.md +85 -74
- package/templates/agents/efcore/db-reset.md +96 -85
- package/templates/agents/efcore/db-seed.md +72 -61
- package/templates/agents/efcore/db-status.md +97 -86
- package/templates/agents/efcore/migration.md +197 -186
- package/templates/agents/efcore/rebase-snapshot.md +119 -108
- package/templates/agents/efcore/scan.md +103 -92
- package/templates/agents/efcore/squash.md +172 -161
- package/templates/agents/explore-codebase.md +66 -66
- package/templates/agents/explore-docs.md +98 -98
- package/templates/agents/fix-grammar.md +50 -50
- package/templates/agents/gitflow/abort.md +45 -45
- package/templates/agents/gitflow/cleanup.md +96 -96
- package/templates/agents/gitflow/commit.md +236 -236
- package/templates/agents/gitflow/exec.md +48 -48
- package/templates/agents/gitflow/finish.md +146 -146
- package/templates/agents/gitflow/init-clone.md +199 -199
- package/templates/agents/gitflow/init-detect.md +137 -137
- package/templates/agents/gitflow/init-validate.md +225 -225
- package/templates/agents/gitflow/init.md +340 -340
- package/templates/agents/gitflow/merge.md +145 -145
- package/templates/agents/gitflow/plan.md +42 -42
- package/templates/agents/gitflow/pr.md +191 -191
- package/templates/agents/gitflow/review.md +49 -49
- package/templates/agents/gitflow/start.md +147 -147
- package/templates/agents/gitflow/status.md +95 -95
- package/templates/agents/mcp-healthcheck.md +163 -163
- package/templates/agents/snipper.md +37 -37
- package/templates/agents/websearch.md +46 -46
- package/templates/hooks/appsettings-guard.sh +76 -76
- package/templates/hooks/docs-drift-check.md +96 -96
- package/templates/hooks/ef-migration-check.md +139 -139
- package/templates/hooks/hooks.json +58 -58
- package/templates/hooks/mcp-check.md +64 -64
- package/templates/hooks/ralph-mcp-logger.sh +46 -46
- package/templates/hooks/ralph-session-end.sh +69 -69
- package/templates/hooks/stop-hook.sh +177 -177
- package/templates/hooks/wsl-dotnet-cleanup.sh +24 -24
- package/templates/mcp-scaffolding/component.tsx.hbs +318 -318
- package/templates/mcp-scaffolding/controller.cs.hbs +192 -192
- package/templates/mcp-scaffolding/entity-extension.cs.hbs +239 -239
- package/templates/mcp-scaffolding/frontend/api-client.ts.hbs +116 -116
- package/templates/mcp-scaffolding/frontend/nav-routes.ts.hbs +133 -133
- package/templates/mcp-scaffolding/frontend/routes.tsx.hbs +126 -126
- package/templates/mcp-scaffolding/migrations/seed-roles.cs.hbs +261 -261
- package/templates/mcp-scaffolding/service-extension.cs.hbs +53 -53
- package/templates/mcp-scaffolding/tests/controller.test.cs.hbs +436 -436
- package/templates/mcp-scaffolding/tests/entity.test.cs.hbs +239 -239
- package/templates/mcp-scaffolding/tests/repository.test.cs.hbs +441 -441
- package/templates/mcp-scaffolding/tests/security.test.cs.hbs +442 -442
- package/templates/mcp-scaffolding/tests/service.test.cs.hbs +402 -402
- package/templates/mcp-scaffolding/tests/validator.test.cs.hbs +428 -428
- package/templates/project/DependencyInjection.Application.cs.template +25 -25
- package/templates/project/DependencyInjection.Infrastructure.cs.template +61 -61
- package/templates/project/DesignTimeExtensionsDbContextFactory.cs.template +70 -70
- package/templates/project/ExampleEntity.cs.template +116 -116
- package/templates/project/ExampleEntityConfiguration.cs.template +64 -64
- package/templates/project/ExampleService.cs.template +146 -146
- package/templates/project/ExtensionsDbContext.cs.template +41 -41
- package/templates/project/IExtensionsDbContext.cs.template +22 -22
- package/templates/project/Program.cs.template +47 -47
- package/templates/project/README.md +79 -79
- package/templates/project/api.ts.template +12 -12
- package/templates/project/appsettings.json.template +170 -170
- package/templates/project/claude-settings.json.template +5 -5
- package/templates/project/test-frontend/msw/handlers.ts +58 -58
- package/templates/project/test-frontend/msw/server.ts +25 -25
- package/templates/project/test-frontend/setup.ts +16 -16
- package/templates/project/test-frontend/test-utils.tsx +59 -59
- package/templates/project/test-frontend/vitest.config.ts +31 -31
- package/templates/ralph/README.md +93 -93
- package/templates/ralph/ralph.config.yaml +113 -113
- package/templates/scripts/setup-ralph-loop.sh +173 -173
- package/templates/skills/_resources/config-safety.md +61 -61
- package/templates/skills/_resources/context-digest-template.md +53 -53
- package/templates/skills/_resources/doc-context-cache.md +60 -60
- package/templates/skills/_resources/docs-manifest-schema.md +155 -155
- package/templates/skills/_resources/formatting-guide.md +124 -124
- package/templates/skills/_resources/mcp-validate-documentation-spec.md +181 -181
- package/templates/skills/_shared.md +228 -228
- package/templates/skills/admin/SKILL.md +48 -48
- package/templates/skills/ai-prompt/SKILL.md +107 -107
- package/templates/skills/ai-prompt/steps/step-00-init.md +47 -47
- package/templates/skills/ai-prompt/steps/step-01-implementation.md +122 -122
- package/templates/skills/apex/SKILL.md +168 -168
- package/templates/skills/apex/_shared.md +141 -141
- package/templates/skills/apex/references/agent-teams-protocol.md +164 -164
- package/templates/skills/apex/references/analysis-methods.md +141 -141
- package/templates/skills/apex/references/challenge-questions.md +145 -145
- package/templates/skills/apex/references/code-generation.md +412 -412
- package/templates/skills/apex/references/core-seed-data.md +1437 -1437
- package/templates/skills/apex/references/error-classification.md +144 -144
- package/templates/skills/apex/references/examine-build-validation.md +82 -82
- package/templates/skills/apex/references/execution-frontend-gates.md +177 -177
- package/templates/skills/apex/references/execution-frontend-patterns.md +105 -105
- package/templates/skills/apex/references/execution-layer1-rules.md +96 -96
- package/templates/skills/apex/references/initialization-challenge-flow.md +110 -110
- package/templates/skills/apex/references/planning-layer-mapping.md +151 -151
- package/templates/skills/apex/references/post-checks.md +1584 -1584
- package/templates/skills/apex/references/smartstack-api.md +1053 -1053
- package/templates/skills/apex/references/smartstack-frontend.md +1571 -1571
- package/templates/skills/apex/references/smartstack-layers.md +402 -402
- package/templates/skills/apex/steps/step-00-init.md +307 -307
- package/templates/skills/apex/steps/step-01-analyze.md +165 -165
- package/templates/skills/apex/steps/step-02-plan.md +144 -144
- package/templates/skills/apex/steps/step-03-execute.md +328 -328
- package/templates/skills/apex/steps/step-04-examine.md +263 -263
- package/templates/skills/apex/steps/step-05-deep-review.md +129 -129
- package/templates/skills/apex/steps/step-06-resolve.md +101 -101
- package/templates/skills/apex/steps/step-07-tests.md +238 -238
- package/templates/skills/apex/steps/step-08-run-tests.md +125 -125
- package/templates/skills/application/SKILL.md +4 -4
- package/templates/skills/application/references/application-roles-template.md +227 -227
- package/templates/skills/application/references/backend-controller-hierarchy.md +58 -58
- package/templates/skills/application/references/backend-entity-seeding.md +72 -72
- package/templates/skills/application/references/backend-seeding-and-dto-output.md +83 -83
- package/templates/skills/application/references/backend-table-prefix-mapping.md +79 -79
- package/templates/skills/application/references/backend-verification.md +88 -88
- package/templates/skills/application/references/frontend-i18n-and-output.md +67 -67
- package/templates/skills/application/references/frontend-route-naming.md +117 -117
- package/templates/skills/application/references/frontend-route-wiring-app-tsx.md +107 -107
- package/templates/skills/application/references/frontend-verification.md +156 -156
- package/templates/skills/application/references/migration-checklist-troubleshooting.md +1 -1
- package/templates/skills/application/references/provider-template.md +177 -177
- package/templates/skills/application/references/roles-client-project-handling.md +55 -55
- package/templates/skills/application/references/roles-fallback-procedure.md +149 -149
- package/templates/skills/application/references/test-coverage-requirements.md +213 -213
- package/templates/skills/application/references/test-frontend.md +73 -73
- package/templates/skills/application/references/test-prerequisites.md +72 -72
- package/templates/skills/application/steps/step-05-frontend.md +176 -176
- package/templates/skills/application/steps/step-06-migration.md +193 -193
- package/templates/skills/application/steps/step-07-tests.md +356 -356
- package/templates/skills/application/steps/step-08-documentation.md +137 -137
- package/templates/skills/application/templates-backend.md +463 -463
- package/templates/skills/application/templates-frontend.md +685 -685
- package/templates/skills/application/templates-i18n.md +520 -520
- package/templates/skills/application/templates-seed.md +1096 -1096
- package/templates/skills/business-analyse/SKILL.md +327 -327
- package/templates/skills/business-analyse/_architecture.md +123 -123
- package/templates/skills/business-analyse/_elicitation.md +206 -206
- package/templates/skills/business-analyse/_module-loop.md +115 -115
- package/templates/skills/business-analyse/_shared.md +383 -383
- package/templates/skills/business-analyse/_suggestions.md +34 -34
- package/templates/skills/business-analyse/html/ba-interactive.html +4477 -4477
- package/templates/skills/business-analyse/html/build-html.js +77 -77
- package/templates/skills/business-analyse/html/src/scripts/01-data-init.js +150 -150
- package/templates/skills/business-analyse/html/src/scripts/02-navigation.js +227 -227
- package/templates/skills/business-analyse/html/src/scripts/03-render-cadrage.js +199 -199
- package/templates/skills/business-analyse/html/src/scripts/04-render-modules.js +205 -205
- package/templates/skills/business-analyse/html/src/scripts/05-render-specs.js +647 -647
- package/templates/skills/business-analyse/html/src/scripts/06-render-consolidation.js +195 -195
- package/templates/skills/business-analyse/html/src/scripts/07-render-handoff.js +92 -92
- package/templates/skills/business-analyse/html/src/scripts/08-editing.js +135 -135
- package/templates/skills/business-analyse/html/src/scripts/09-export.js +168 -168
- package/templates/skills/business-analyse/html/src/scripts/10-comments.js +171 -171
- package/templates/skills/business-analyse/html/src/scripts/11-review-panel.js +166 -166
- package/templates/skills/business-analyse/html/src/styles/01-variables.css +38 -38
- package/templates/skills/business-analyse/html/src/styles/02-layout.css +101 -101
- package/templates/skills/business-analyse/html/src/styles/03-navigation.css +120 -120
- package/templates/skills/business-analyse/html/src/styles/04-cards.css +196 -196
- package/templates/skills/business-analyse/html/src/styles/05-modules.css +454 -454
- package/templates/skills/business-analyse/html/src/styles/06-wireframes.css +272 -272
- package/templates/skills/business-analyse/html/src/styles/07-comments.css +184 -184
- package/templates/skills/business-analyse/html/src/styles/08-review-panel.css +241 -241
- package/templates/skills/business-analyse/html/src/template.html +516 -516
- package/templates/skills/business-analyse/patterns/suggestion-catalog.md +546 -546
- package/templates/skills/business-analyse/questionnaire/00-application.md +160 -160
- package/templates/skills/business-analyse/questionnaire/00b-project.md +85 -85
- package/templates/skills/business-analyse/questionnaire/01-context.md +185 -185
- package/templates/skills/business-analyse/questionnaire/02-stakeholders.md +189 -189
- package/templates/skills/business-analyse/questionnaire/03-scope.md +164 -164
- package/templates/skills/business-analyse/questionnaire/04-data.md +88 -88
- package/templates/skills/business-analyse/questionnaire/05-integrations.md +58 -58
- package/templates/skills/business-analyse/questionnaire/06-security.md +68 -68
- package/templates/skills/business-analyse/questionnaire/07-ui.md +76 -76
- package/templates/skills/business-analyse/questionnaire/08-performance.md +42 -42
- package/templates/skills/business-analyse/questionnaire/09-constraints.md +45 -45
- package/templates/skills/business-analyse/questionnaire/10-documentation.md +43 -43
- package/templates/skills/business-analyse/questionnaire/11-data-lifecycle.md +59 -59
- package/templates/skills/business-analyse/questionnaire/12-migration.md +58 -58
- package/templates/skills/business-analyse/questionnaire/13-cross-module.md +69 -69
- package/templates/skills/business-analyse/questionnaire/14-risk-assumptions.md +135 -135
- package/templates/skills/business-analyse/questionnaire/15-success-metrics.md +136 -136
- package/templates/skills/business-analyse/questionnaire.md +337 -337
- package/templates/skills/business-analyse/react/application-viewer.md +242 -242
- package/templates/skills/business-analyse/react/components.md +551 -551
- package/templates/skills/business-analyse/react/i18n-template.md +306 -306
- package/templates/skills/business-analyse/references/acceptance-criteria.md +169 -169
- package/templates/skills/business-analyse/references/agent-module-prompt.md +362 -362
- package/templates/skills/business-analyse/references/agent-pooling-best-practices.md +557 -557
- package/templates/skills/business-analyse/references/analysis-semantic-checks.md +190 -190
- package/templates/skills/business-analyse/references/cache-warming-strategy.md +566 -566
- package/templates/skills/business-analyse/references/cadrage-challenge-patterns.md +41 -41
- package/templates/skills/business-analyse/references/cadrage-coverage-matrix.md +74 -74
- package/templates/skills/business-analyse/references/cadrage-pre-analysis.md +115 -115
- package/templates/skills/business-analyse/references/cadrage-shared-modules.md +68 -69
- package/templates/skills/business-analyse/references/cadrage-structure-cards.md +85 -85
- package/templates/skills/business-analyse/references/compilation-structure-cards.md +297 -297
- package/templates/skills/business-analyse/references/consolidation-structural-checks.md +107 -107
- package/templates/skills/business-analyse/references/deploy-data-build.md +180 -180
- package/templates/skills/business-analyse/references/deploy-modes.md +118 -118
- package/templates/skills/business-analyse/references/detection-strategies.md +424 -424
- package/templates/skills/business-analyse/references/entity-architecture-decision.md +218 -218
- package/templates/skills/business-analyse/references/handoff-file-templates.md +120 -120
- package/templates/skills/business-analyse/references/handoff-mappings.md +81 -81
- package/templates/skills/business-analyse/references/handoff-seeddata-generation.md +312 -312
- package/templates/skills/business-analyse/references/html-data-mapping.md +299 -299
- package/templates/skills/business-analyse/references/init-schema-deployment.md +65 -65
- package/templates/skills/business-analyse/references/naming-conventions.md +243 -243
- package/templates/skills/business-analyse/references/prd-generation.md +258 -258
- package/templates/skills/business-analyse/references/review-data-mapping.md +363 -363
- package/templates/skills/business-analyse/references/robustness-checks.md +542 -542
- package/templates/skills/business-analyse/references/spec-auto-inference.md +111 -111
- package/templates/skills/business-analyse/references/team-orchestration.md +1022 -1022
- package/templates/skills/business-analyse/references/ui-dashboard-spec.md +85 -85
- package/templates/skills/business-analyse/references/ui-resource-cards.md +259 -259
- package/templates/skills/business-analyse/references/validate-incremental-html.md +121 -121
- package/templates/skills/business-analyse/references/validation-checklist.md +347 -347
- package/templates/skills/business-analyse/references/wireframe-svg-style-guide.md +335 -335
- package/templates/skills/business-analyse/schemas/application-schema.json +453 -453
- package/templates/skills/business-analyse/schemas/feature-schema.json +53 -53
- package/templates/skills/business-analyse/schemas/project-schema.json +485 -485
- package/templates/skills/business-analyse/schemas/sections/analysis-schema.json +201 -201
- package/templates/skills/business-analyse/schemas/sections/discovery-schema.json +82 -82
- package/templates/skills/business-analyse/schemas/sections/handoff-schema.json +80 -80
- package/templates/skills/business-analyse/schemas/sections/metadata-schema.json +70 -70
- package/templates/skills/business-analyse/schemas/sections/specification-schema.json +547 -547
- package/templates/skills/business-analyse/schemas/sections/validation-schema.json +93 -93
- package/templates/skills/business-analyse/schemas/shared/common-defs.json +226 -226
- package/templates/skills/business-analyse/steps/step-00-init.md +575 -576
- package/templates/skills/business-analyse/steps/step-01-cadrage.md +767 -767
- package/templates/skills/business-analyse/steps/step-01b-applications.md +419 -419
- package/templates/skills/business-analyse/steps/step-02-decomposition.md +387 -387
- package/templates/skills/business-analyse/steps/step-03a-data.md +16 -16
- package/templates/skills/business-analyse/steps/step-03a1-setup.md +506 -506
- package/templates/skills/business-analyse/steps/step-03a2-analysis.md +252 -252
- package/templates/skills/business-analyse/steps/step-03b-ui.md +425 -425
- package/templates/skills/business-analyse/steps/step-03c-compile.md +611 -611
- package/templates/skills/business-analyse/steps/step-03d-validate.md +783 -783
- package/templates/skills/business-analyse/steps/step-04-consolidation.md +17 -17
- package/templates/skills/business-analyse/steps/step-04a-collect.md +415 -415
- package/templates/skills/business-analyse/steps/step-04b-analyze.md +163 -163
- package/templates/skills/business-analyse/steps/step-04c-decide.md +186 -186
- package/templates/skills/business-analyse/steps/step-05a-handoff.md +840 -840
- package/templates/skills/business-analyse/steps/step-05b-deploy.md +522 -522
- package/templates/skills/business-analyse/steps/step-05c-ralph-readiness.md +703 -703
- package/templates/skills/business-analyse/steps/step-06-review.md +278 -278
- package/templates/skills/business-analyse/templates/tpl-frd.md +168 -168
- package/templates/skills/business-analyse/templates/tpl-handoff.md +186 -186
- package/templates/skills/business-analyse/templates/tpl-launch-displays.md +59 -59
- package/templates/skills/business-analyse/templates/tpl-progress.md +172 -172
- package/templates/skills/business-analyse/templates-frd.md +476 -476
- package/templates/skills/business-analyse/templates-react.md +574 -574
- package/templates/skills/cc-agent/SKILL.md +129 -129
- package/templates/skills/cc-agent/references/agent-behavior-patterns.md +95 -95
- package/templates/skills/cc-agent/references/agent-frontmatter.md +213 -213
- package/templates/skills/cc-agent/references/permission-modes.md +102 -102
- package/templates/skills/cc-agent/references/tools-reference.md +144 -144
- package/templates/skills/cc-agent/steps/step-00-init.md +134 -134
- package/templates/skills/cc-agent/steps/step-01-design.md +186 -186
- package/templates/skills/cc-agent/steps/step-02-generate.md +131 -131
- package/templates/skills/cc-agent/steps/step-03-validate.md +130 -130
- package/templates/skills/cc-agent/templates/agent-categorized.md +67 -67
- package/templates/skills/cc-agent/templates/agent-standalone.md +56 -56
- package/templates/skills/cc-agent/templates/agent-with-skills.md +94 -94
- package/templates/skills/cc-audit/SKILL.md +108 -108
- package/templates/skills/cc-audit/references/agent-checklist.md +91 -91
- package/templates/skills/cc-audit/references/hook-checklist.md +110 -110
- package/templates/skills/cc-audit/references/skill-checklist.md +70 -70
- package/templates/skills/cc-audit/steps/step-00-init.md +98 -98
- package/templates/skills/cc-audit/steps/step-01-scan.md +142 -142
- package/templates/skills/cc-audit/steps/step-02-analyze.md +158 -158
- package/templates/skills/cc-audit/steps/step-03-report.md +142 -142
- package/templates/skills/cc-skill/SKILL.md +134 -134
- package/templates/skills/cc-skill/references/best-practices.md +167 -167
- package/templates/skills/cc-skill/references/frontmatter-reference.md +182 -182
- package/templates/skills/cc-skill/references/skill-patterns.md +199 -199
- package/templates/skills/cc-skill/steps/step-00-init.md +119 -119
- package/templates/skills/cc-skill/steps/step-01-design.md +199 -199
- package/templates/skills/cc-skill/steps/step-02-generate.md +145 -145
- package/templates/skills/cc-skill/steps/step-03-steps.md +151 -151
- package/templates/skills/cc-skill/steps/step-04-validate.md +124 -124
- package/templates/skills/cc-skill/templates/skill-forked.md +85 -85
- package/templates/skills/cc-skill/templates/skill-progressive.md +102 -102
- package/templates/skills/cc-skill/templates/skill-simple.md +75 -75
- package/templates/skills/cc-skill/templates/step-template.md +82 -82
- package/templates/skills/check-version/SKILL.md +196 -196
- package/templates/skills/controller/SKILL.md +162 -162
- package/templates/skills/controller/postman-templates.md +614 -614
- package/templates/skills/controller/references/controller-code-templates.md +159 -159
- package/templates/skills/controller/references/mcp-scaffold-workflow.md +209 -209
- package/templates/skills/controller/references/permission-sync-templates.md +149 -149
- package/templates/skills/controller/steps/step-00-init.md +193 -191
- package/templates/skills/controller/steps/step-01-analyze.md +146 -146
- package/templates/skills/controller/steps/step-02-plan.md +176 -176
- package/templates/skills/controller/steps/step-03-generate.md +189 -189
- package/templates/skills/controller/steps/step-04-perms.md +80 -80
- package/templates/skills/controller/steps/step-05-validate.md +107 -107
- package/templates/skills/controller/templates.md +1555 -1555
- package/templates/skills/debug/SKILL.md +70 -70
- package/templates/skills/debug/references/team-protocol.md +232 -232
- package/templates/skills/debug/steps/step-00-init.md +57 -57
- package/templates/skills/debug/steps/step-01-analyze.md +219 -219
- package/templates/skills/debug/steps/step-02-resolve.md +85 -85
- package/templates/skills/documentation/SKILL.md +132 -132
- package/templates/skills/documentation/data-schema.md +227 -227
- package/templates/skills/documentation/steps/step-00-init.md +70 -70
- package/templates/skills/documentation/steps/step-01-scan.md +113 -113
- package/templates/skills/documentation/steps/step-02-generate.md +231 -231
- package/templates/skills/documentation/steps/step-03-validate.md +251 -238
- package/templates/skills/documentation/templates.md +662 -663
- package/templates/skills/efcore/SKILL.md +168 -167
- package/templates/skills/efcore/references/both-contexts.md +32 -32
- package/templates/skills/efcore/references/database-operations.md +67 -67
- package/templates/skills/efcore/references/destructive-operations.md +38 -38
- package/templates/skills/efcore/references/reset-operations.md +81 -81
- package/templates/skills/efcore/references/seed-methods.md +86 -86
- package/templates/skills/efcore/references/shared-init-functions.md +250 -250
- package/templates/skills/efcore/references/sql-objects-injection.md +61 -61
- package/templates/skills/efcore/references/troubleshooting.md +81 -81
- package/templates/skills/efcore/references/zero-downtime-patterns.md +227 -227
- package/templates/skills/efcore/steps/db/step-deploy.md +217 -217
- package/templates/skills/efcore/steps/db/step-reset.md +186 -186
- package/templates/skills/efcore/steps/db/step-seed.md +166 -166
- package/templates/skills/efcore/steps/db/step-status.md +173 -173
- package/templates/skills/efcore/steps/migration/step-00-init.md +102 -102
- package/templates/skills/efcore/steps/migration/step-01-check.md +164 -164
- package/templates/skills/efcore/steps/migration/step-02-create.md +160 -160
- package/templates/skills/efcore/steps/migration/step-03-validate.md +168 -168
- package/templates/skills/efcore/steps/rebase-snapshot/step-00-init.md +173 -173
- package/templates/skills/efcore/steps/rebase-snapshot/step-01-backup.md +100 -100
- package/templates/skills/efcore/steps/rebase-snapshot/step-02-fetch.md +115 -115
- package/templates/skills/efcore/steps/rebase-snapshot/step-03-create.md +112 -112
- package/templates/skills/efcore/steps/rebase-snapshot/step-04-validate.md +157 -157
- package/templates/skills/efcore/steps/shared/step-00-init.md +131 -131
- package/templates/skills/efcore/steps/squash/step-00-init.md +141 -141
- package/templates/skills/efcore/steps/squash/step-01-backup.md +120 -120
- package/templates/skills/efcore/steps/squash/step-02-fetch.md +168 -168
- package/templates/skills/efcore/steps/squash/step-03-create.md +184 -184
- package/templates/skills/efcore/steps/squash/step-04-validate.md +174 -174
- package/templates/skills/explore/SKILL.md +98 -98
- package/templates/skills/feature-full/SKILL.md +111 -111
- package/templates/skills/feature-full/steps/step-00-init.md +57 -57
- package/templates/skills/feature-full/steps/step-01-implementation.md +120 -120
- package/templates/skills/gitflow/SKILL.md +377 -377
- package/templates/skills/gitflow/_shared.md +620 -620
- package/templates/skills/gitflow/phases/abort.md +189 -189
- package/templates/skills/gitflow/phases/cleanup.md +234 -234
- package/templates/skills/gitflow/phases/status.md +192 -192
- package/templates/skills/gitflow/references/commit-message-generation.md +58 -58
- package/templates/skills/gitflow/references/commit-migration-validation.md +49 -49
- package/templates/skills/gitflow/references/finish-cleanup.md +55 -55
- package/templates/skills/gitflow/references/finish-version-bumping.md +45 -45
- package/templates/skills/gitflow/references/init-config-template.md +135 -135
- package/templates/skills/gitflow/references/init-environment-detection.md +41 -41
- package/templates/skills/gitflow/references/init-name-normalization.md +103 -103
- package/templates/skills/gitflow/references/init-questions.md +185 -185
- package/templates/skills/gitflow/references/init-structure-creation.md +75 -75
- package/templates/skills/gitflow/references/init-version-detection.md +21 -21
- package/templates/skills/gitflow/references/init-workspace-detection.md +43 -43
- package/templates/skills/gitflow/references/merge-ci-status.md +36 -36
- package/templates/skills/gitflow/references/merge-execution.md +62 -62
- package/templates/skills/gitflow/references/merge-pr-context.md +76 -76
- package/templates/skills/gitflow/references/plan-template.md +69 -69
- package/templates/skills/gitflow/references/pr-build-checks.md +60 -60
- package/templates/skills/gitflow/references/pr-generation.md +58 -58
- package/templates/skills/gitflow/references/start-branch-normalization.md +28 -28
- package/templates/skills/gitflow/references/start-efcore-preflight.md +70 -70
- package/templates/skills/gitflow/references/start-local-config.md +113 -113
- package/templates/skills/gitflow/references/start-worktree-creation.md +50 -50
- package/templates/skills/gitflow/references/sync-push-verify.md +44 -44
- package/templates/skills/gitflow/references/sync-rebase-conflicts.md +38 -38
- package/templates/skills/gitflow/steps/step-commit.md +199 -199
- package/templates/skills/gitflow/steps/step-finish.md +147 -147
- package/templates/skills/gitflow/steps/step-init.md +190 -190
- package/templates/skills/gitflow/steps/step-merge.md +85 -85
- package/templates/skills/gitflow/steps/step-plan.md +151 -151
- package/templates/skills/gitflow/steps/step-pr.md +199 -199
- package/templates/skills/gitflow/steps/step-start.md +195 -195
- package/templates/skills/gitflow/steps/step-sync.md +161 -161
- package/templates/skills/gitflow/templates/config.json +72 -72
- package/templates/skills/mcp/SKILL.md +62 -62
- package/templates/skills/mcp/steps/step-01-healthcheck.md +108 -108
- package/templates/skills/mcp/steps/step-02-tools.md +73 -73
- package/templates/skills/notification/SKILL.md +173 -173
- package/templates/skills/quick-search/SKILL.md +99 -99
- package/templates/skills/ralph-loop/SKILL.md +234 -234
- package/templates/skills/ralph-loop/references/category-completeness.md +185 -185
- package/templates/skills/ralph-loop/references/category-rules.md +96 -96
- package/templates/skills/ralph-loop/references/compact-loop.md +300 -300
- package/templates/skills/ralph-loop/references/init-resume-recovery.md +127 -127
- package/templates/skills/ralph-loop/references/module-transition.md +151 -151
- package/templates/skills/ralph-loop/references/multi-module-queue.md +171 -171
- package/templates/skills/ralph-loop/references/parallel-execution.md +246 -246
- package/templates/skills/ralph-loop/references/section-splitting.md +439 -439
- package/templates/skills/ralph-loop/references/task-transform-legacy.md +256 -256
- package/templates/skills/ralph-loop/references/team-orchestration.md +547 -547
- package/templates/skills/ralph-loop/steps/step-00-init.md +150 -150
- package/templates/skills/ralph-loop/steps/step-01-task.md +174 -174
- package/templates/skills/ralph-loop/steps/step-02-execute.md +177 -177
- package/templates/skills/ralph-loop/steps/step-03-commit.md +92 -92
- package/templates/skills/ralph-loop/steps/step-04-check.md +207 -207
- package/templates/skills/ralph-loop/steps/step-05-report.md +175 -175
- package/templates/skills/refactor/SKILL.md +56 -56
- package/templates/skills/refactor/steps/step-01-discover.md +60 -60
- package/templates/skills/refactor/steps/step-02-execute.md +67 -67
- package/templates/skills/review-code/SKILL.md +95 -94
- package/templates/skills/review-code/references/clean-code-principles.md +292 -292
- package/templates/skills/review-code/references/code-quality-metrics.md +174 -174
- package/templates/skills/review-code/references/feedback-patterns.md +149 -149
- package/templates/skills/review-code/references/owasp-api-top10.md +243 -243
- package/templates/skills/review-code/references/security-checklist.md +212 -212
- package/templates/skills/review-code/steps/step-01-smartstack.md +96 -96
- package/templates/skills/review-code/steps/step-02-detailed-review.md +80 -80
- package/templates/skills/review-code/steps/step-03-react.md +44 -44
- package/templates/skills/ui-components/SKILL.md +137 -137
- package/templates/skills/ui-components/accessibility.md +170 -170
- package/templates/skills/ui-components/patterns/dashboard-chart.md +327 -327
- package/templates/skills/ui-components/patterns/data-table.md +39 -39
- package/templates/skills/ui-components/patterns/entity-card.md +77 -77
- package/templates/skills/ui-components/patterns/grid-layout.md +91 -91
- package/templates/skills/ui-components/patterns/kanban.md +43 -43
- package/templates/skills/ui-components/responsive-guidelines.md +278 -278
- package/templates/skills/ui-components/style-guide.md +113 -113
- package/templates/skills/utils/SKILL.md +44 -44
- package/templates/skills/utils/subcommands/test-web-config.md +152 -152
- package/templates/skills/utils/subcommands/test-web.md +123 -123
- package/templates/skills/validate/SKILL.md +181 -181
- package/templates/skills/validate-feature/SKILL.md +101 -101
- package/templates/skills/validate-feature/references/api-smoke-tests.md +140 -140
- package/templates/skills/validate-feature/references/db-validation-checks.md +180 -180
- package/templates/skills/validate-feature/steps/step-00-dependencies.md +121 -121
- package/templates/skills/validate-feature/steps/step-01-compile.md +39 -39
- package/templates/skills/validate-feature/steps/step-02-unit-tests.md +45 -45
- package/templates/skills/validate-feature/steps/step-03-integration-tests.md +53 -53
- package/templates/skills/validate-feature/steps/step-04-api-smoke.md +94 -94
- package/templates/skills/validate-feature/steps/step-05-db-validation.md +149 -149
- package/templates/skills/workflow/SKILL.md +127 -127
- package/templates/skills/workflow/steps/step-00-init.md +57 -57
- package/templates/skills/workflow/steps/step-01-implementation.md +84 -84
- package/templates/test-web/api-health.json +38 -38
- package/templates/test-web/minimal.json +19 -19
- package/templates/test-web/npm-package.json +46 -46
- package/templates/test-web/seo-check.json +54 -54
|
@@ -1,1437 +1,1437 @@
|
|
|
1
|
-
# Core Seed Data - Execution Reference
|
|
2
|
-
|
|
3
|
-
> **Loaded by:** apex step-03-execute (delegate mode + seedData tasks) and step-04-examine
|
|
4
|
-
> **Condition:** Seed data generation — infrastructure or seedData category tasks
|
|
5
|
-
> **Applies to:** Client projects only (seeding_strategy = "provider", ExtensionsDbContext)
|
|
6
|
-
>
|
|
7
|
-
> **Source of truth:** `/application` skill `templates-seed.md` (lines 608-916)
|
|
8
|
-
> **Moved from:** `ralph-loop/references/core-seed-data.md` (delegation refactoring)
|
|
9
|
-
|
|
10
|
-
---
|
|
11
|
-
|
|
12
|
-
## 1. Parameter Extraction
|
|
13
|
-
|
|
14
|
-
Extract navigation hierarchy from the task's `_seedDataMeta` (populated by guardrail 1e):
|
|
15
|
-
|
|
16
|
-
```javascript
|
|
17
|
-
const task = currentTask;
|
|
18
|
-
const meta = task._seedDataMeta || task._providerMeta || {};
|
|
19
|
-
const coreSeedData = meta.coreSeedData || {};
|
|
20
|
-
|
|
21
|
-
// Navigation hierarchy
|
|
22
|
-
const navModules = coreSeedData.navigationModules || coreSeedData.navigation || [];
|
|
23
|
-
const navSections = coreSeedData.navigationSections || [];
|
|
24
|
-
const navResources = coreSeedData.navigationResources || [];
|
|
25
|
-
const permissions = coreSeedData.permissions || [];
|
|
26
|
-
const rolePerms = coreSeedData.rolePermissions || [];
|
|
27
|
-
|
|
28
|
-
// Derived context (from guardrail or PRD)
|
|
29
|
-
const navRoute = meta.navRoute; // e.g. "human-resources.projects"
|
|
30
|
-
const appCode = meta.appCode; // e.g. "human-resources"
|
|
31
|
-
const moduleCode = task.module; // e.g. "projects"
|
|
32
|
-
|
|
33
|
-
// If _seedDataMeta is absent, fallback to PRD source
|
|
34
|
-
if (!navRoute) {
|
|
35
|
-
const prd = readJSON('.ralph/prd.json');
|
|
36
|
-
const navRoute = `${prd.source?.application || prd.metadata?.module}.${task.module}`;
|
|
37
|
-
}
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
**State variables after extraction:**
|
|
41
|
-
|
|
42
|
-
| Variable | Example | Source |
|
|
43
|
-
|----------|---------|--------|
|
|
44
|
-
| `navRoute` | `human-resources.projects` | `_seedDataMeta.navRoute` |
|
|
45
|
-
| `appCode` | `human-resources` | `_seedDataMeta.appCode` |
|
|
46
|
-
| `moduleCode` | `projects` | `task.module` |
|
|
47
|
-
| `navModules[]` | `[{code, label, icon, route, translations}]` | `coreSeedData.navigationModules` |
|
|
48
|
-
| `navSections[]` | `[{code, label, icon, route, parentCode, permission, sort}]` | `coreSeedData.navigationSections` |
|
|
49
|
-
| `navResources[]` | `[{code, type, entity, parentCode, permission}]` | `coreSeedData.navigationResources` |
|
|
50
|
-
| `permissions[]` | `[{path, action, description}]` | `coreSeedData.permissions` |
|
|
51
|
-
| `rolePerms[]` | `[{role, permissions[]}]` | `coreSeedData.rolePermissions` |
|
|
52
|
-
|
|
53
|
-
---
|
|
54
|
-
|
|
55
|
-
## 1b. NavigationApplicationSeedData.cs (ONCE per application)
|
|
56
|
-
|
|
57
|
-
**File:** `Infrastructure/Persistence/Seeding/Data/NavigationApplicationSeedData.cs`
|
|
58
|
-
|
|
59
|
-
> **MANDATORY:** This file MUST be created BEFORE any module seed data.
|
|
60
|
-
> Without it, modules have no parent ApplicationId and ApplicationRolesSeedData has no GUID reference.
|
|
61
|
-
> This file is created **ONCE per application** (not per module).
|
|
62
|
-
|
|
63
|
-
### Data Source
|
|
64
|
-
|
|
65
|
-
From `seedDataCore.navigationApplications[0]` in feature.json (generated by BA step-05a):
|
|
66
|
-
|
|
67
|
-
| Placeholder | Source |
|
|
68
|
-
|-------------|--------|
|
|
69
|
-
| `{appCode}` | `navigationApplications[0].code` |
|
|
70
|
-
| `{appLabel_xx}` | `navigationApplications[0].labels.xx` (fr, en, it, de) |
|
|
71
|
-
| `{appDesc_xx}` | `navigationApplications[0].description.xx` |
|
|
72
|
-
| `{appIcon}` | `navigationApplications[0].icon` |
|
|
73
|
-
|
|
74
|
-
### GUID Generation Rule
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
```csharp
|
|
78
|
-
// Deterministic GUID for APPLICATION
|
|
79
|
-
public static readonly Guid ApplicationId =
|
|
80
|
-
GenerateDeterministicGuid("navigation-application-{appCode}");
|
|
81
|
-
// Example: GenerateDeterministicGuid("navigation-application-human-resources")
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
### Template
|
|
85
|
-
|
|
86
|
-
```csharp
|
|
87
|
-
using SmartStack.Domain.Navigation;
|
|
88
|
-
|
|
89
|
-
namespace {BaseNamespace}.Infrastructure.Persistence.Seeding.Data;
|
|
90
|
-
|
|
91
|
-
/// <summary>
|
|
92
|
-
/// Navigation seed data for {AppLabel_en} application.
|
|
93
|
-
/// Consumed by IClientSeedDataProvider at application startup.
|
|
94
|
-
/// Created ONCE per application — modules reference ApplicationId as parent.
|
|
95
|
-
/// </summary>
|
|
96
|
-
public static class NavigationApplicationSeedData
|
|
97
|
-
{
|
|
98
|
-
// Deterministic GUID for this application
|
|
99
|
-
public static readonly Guid ApplicationId =
|
|
100
|
-
GenerateDeterministicGuid("navigation-application-{appCode}");
|
|
101
|
-
|
|
102
|
-
/// <summary>
|
|
103
|
-
/// Returns navigation application entry for seeding into core.nav_Applications.
|
|
104
|
-
/// </summary>
|
|
105
|
-
public static NavigationApplicationSeedEntry GetApplicationEntry()
|
|
106
|
-
{
|
|
107
|
-
return new NavigationApplicationSeedEntry
|
|
108
|
-
{
|
|
109
|
-
Id = ApplicationId,
|
|
110
|
-
Code = "{appCode}",
|
|
111
|
-
Label = "{appLabel_en}",
|
|
112
|
-
Description = "{appDesc_en}",
|
|
113
|
-
Icon = "{appIcon}", // Lucide React icon name
|
|
114
|
-
IconType = IconType.Lucide,
|
|
115
|
-
Route = ToKebabCase("/{appCode}"),
|
|
116
|
-
DisplayOrder = 1,
|
|
117
|
-
IsActive = true
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/// <summary>
|
|
122
|
-
/// Returns 4-language translations for this application.
|
|
123
|
-
/// </summary>
|
|
124
|
-
public static IEnumerable<NavigationTranslationSeedEntry> GetTranslationEntries()
|
|
125
|
-
{
|
|
126
|
-
var appId = ApplicationId;
|
|
127
|
-
return new[]
|
|
128
|
-
{
|
|
129
|
-
new NavigationTranslationSeedEntry
|
|
130
|
-
{
|
|
131
|
-
EntityType = NavigationEntityType.Application,
|
|
132
|
-
EntityId = appId,
|
|
133
|
-
LanguageCode = "fr",
|
|
134
|
-
Label = "{appLabel_fr}",
|
|
135
|
-
Description = "{appDesc_fr}"
|
|
136
|
-
},
|
|
137
|
-
new NavigationTranslationSeedEntry
|
|
138
|
-
{
|
|
139
|
-
EntityType = NavigationEntityType.Application,
|
|
140
|
-
EntityId = appId,
|
|
141
|
-
LanguageCode = "en",
|
|
142
|
-
Label = "{appLabel_en}",
|
|
143
|
-
Description = "{appDesc_en}"
|
|
144
|
-
},
|
|
145
|
-
new NavigationTranslationSeedEntry
|
|
146
|
-
{
|
|
147
|
-
EntityType = NavigationEntityType.Application,
|
|
148
|
-
EntityId = appId,
|
|
149
|
-
LanguageCode = "it",
|
|
150
|
-
Label = "{appLabel_it}",
|
|
151
|
-
Description = "{appDesc_it}"
|
|
152
|
-
},
|
|
153
|
-
new NavigationTranslationSeedEntry
|
|
154
|
-
{
|
|
155
|
-
EntityType = NavigationEntityType.Application,
|
|
156
|
-
EntityId = appId,
|
|
157
|
-
LanguageCode = "de",
|
|
158
|
-
Label = "{appLabel_de}",
|
|
159
|
-
Description = "{appDesc_de}"
|
|
160
|
-
}
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
private static Guid GenerateDeterministicGuid(string seed)
|
|
165
|
-
{
|
|
166
|
-
using var sha256 = System.Security.Cryptography.SHA256.Create();
|
|
167
|
-
var hash = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(seed));
|
|
168
|
-
return new Guid(hash.Take(16).ToArray());
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/// <summary>
|
|
172
|
-
/// Converts PascalCase route segments to kebab-case for web URLs.
|
|
173
|
-
/// </summary>
|
|
174
|
-
private static string ToKebabCase(string route)
|
|
175
|
-
{
|
|
176
|
-
if (string.IsNullOrEmpty(route)) return route;
|
|
177
|
-
|
|
178
|
-
var segments = route.Split('/');
|
|
179
|
-
var kebabSegments = new List<string>();
|
|
180
|
-
|
|
181
|
-
foreach (var segment in segments)
|
|
182
|
-
{
|
|
183
|
-
if (string.IsNullOrEmpty(segment))
|
|
184
|
-
{
|
|
185
|
-
kebabSegments.Add(segment);
|
|
186
|
-
continue;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
var kebab = System.Text.RegularExpressions.Regex
|
|
190
|
-
.Replace(segment, "([a-z])([A-Z])", "$1-$2")
|
|
191
|
-
.ToLowerInvariant();
|
|
192
|
-
kebabSegments.Add(kebab);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
return string.Join("/", kebabSegments);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/// <summary>Seed entry DTO for navigation application.</summary>
|
|
200
|
-
public class NavigationApplicationSeedEntry
|
|
201
|
-
{
|
|
202
|
-
public Guid Id { get; init; }
|
|
203
|
-
public string Code { get; init; } = null!;
|
|
204
|
-
public string Label { get; init; } = null!;
|
|
205
|
-
public string? Description { get; init; }
|
|
206
|
-
public string? Icon { get; init; }
|
|
207
|
-
public IconType IconType { get; init; }
|
|
208
|
-
public string? Route { get; init; }
|
|
209
|
-
public int DisplayOrder { get; init; }
|
|
210
|
-
public bool IsActive { get; init; }
|
|
211
|
-
}
|
|
212
|
-
```
|
|
213
|
-
|
|
214
|
-
**Replace placeholders** with values from `seedDataCore.navigationApplications[0]`.
|
|
215
|
-
|
|
216
|
-
---
|
|
217
|
-
|
|
218
|
-
## 2. NavigationModuleSeedData.cs
|
|
219
|
-
|
|
220
|
-
**File:** `Infrastructure/Persistence/Seeding/Data/{ModulePascal}/NavigationModuleSeedData.cs`
|
|
221
|
-
|
|
222
|
-
### GUID Generation Rule
|
|
223
|
-
|
|
224
|
-
```csharp
|
|
225
|
-
// NEVER use Guid.NewGuid() — ALWAYS deterministic
|
|
226
|
-
private static Guid GenerateDeterministicGuid(string seed)
|
|
227
|
-
{
|
|
228
|
-
using var sha256 = System.Security.Cryptography.SHA256.Create();
|
|
229
|
-
var hash = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(seed));
|
|
230
|
-
return new Guid(hash.Take(16).ToArray());
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
// Usage: GUIDs are derived from the navigation path
|
|
234
|
-
public static readonly Guid ModuleId = GenerateDeterministicGuid("navigation-module-{navRoute}");
|
|
235
|
-
// Example: GenerateDeterministicGuid("navigation-module-human-resources.projects")
|
|
236
|
-
```
|
|
237
|
-
|
|
238
|
-
### Template
|
|
239
|
-
|
|
240
|
-
```csharp
|
|
241
|
-
using SmartStack.Domain.Navigation;
|
|
242
|
-
|
|
243
|
-
namespace {BaseNamespace}.Infrastructure.Persistence.Seeding.Data.{ModulePascal};
|
|
244
|
-
|
|
245
|
-
/// <summary>
|
|
246
|
-
/// Navigation seed data for {ModuleLabel} module.
|
|
247
|
-
/// Consumed by IClientSeedDataProvider at application startup.
|
|
248
|
-
/// </summary>
|
|
249
|
-
public static class {ModulePascal}NavigationSeedData
|
|
250
|
-
{
|
|
251
|
-
// Deterministic GUID for this module
|
|
252
|
-
public static readonly Guid {ModulePascal}ModuleId =
|
|
253
|
-
GenerateDeterministicGuid("navigation-module-{navRoute}");
|
|
254
|
-
|
|
255
|
-
/// <summary>
|
|
256
|
-
/// Returns navigation module entry for seeding into core.nav_Modules.
|
|
257
|
-
/// </summary>
|
|
258
|
-
public static NavigationModuleSeedEntry GetModuleEntry(Guid applicationId)
|
|
259
|
-
{
|
|
260
|
-
return new NavigationModuleSeedEntry
|
|
261
|
-
{
|
|
262
|
-
Id = {ModulePascal}ModuleId,
|
|
263
|
-
ApplicationId = applicationId,
|
|
264
|
-
Code = "{moduleCode}",
|
|
265
|
-
Label = "{label_en}",
|
|
266
|
-
Description = "{desc_en}",
|
|
267
|
-
Icon = "{icon}", // Lucide React icon name
|
|
268
|
-
IconType = IconType.Lucide,
|
|
269
|
-
Route = ToKebabCase($"/{appCode}/{moduleCode}"),
|
|
270
|
-
DisplayOrder = {displayOrder},
|
|
271
|
-
IsActive = true
|
|
272
|
-
};
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/// <summary>
|
|
276
|
-
/// Returns 4-language translations for this module.
|
|
277
|
-
/// </summary>
|
|
278
|
-
public static IEnumerable<NavigationTranslationSeedEntry> GetTranslationEntries()
|
|
279
|
-
{
|
|
280
|
-
var moduleId = {ModulePascal}ModuleId;
|
|
281
|
-
return new[]
|
|
282
|
-
{
|
|
283
|
-
new NavigationTranslationSeedEntry
|
|
284
|
-
{
|
|
285
|
-
EntityType = NavigationEntityType.Module,
|
|
286
|
-
EntityId = moduleId,
|
|
287
|
-
LanguageCode = "fr",
|
|
288
|
-
Label = "{label_fr}",
|
|
289
|
-
Description = "{desc_fr}"
|
|
290
|
-
},
|
|
291
|
-
new NavigationTranslationSeedEntry
|
|
292
|
-
{
|
|
293
|
-
EntityType = NavigationEntityType.Module,
|
|
294
|
-
EntityId = moduleId,
|
|
295
|
-
LanguageCode = "en",
|
|
296
|
-
Label = "{label_en}",
|
|
297
|
-
Description = "{desc_en}"
|
|
298
|
-
},
|
|
299
|
-
new NavigationTranslationSeedEntry
|
|
300
|
-
{
|
|
301
|
-
EntityType = NavigationEntityType.Module,
|
|
302
|
-
EntityId = moduleId,
|
|
303
|
-
LanguageCode = "it",
|
|
304
|
-
Label = "{label_it}",
|
|
305
|
-
Description = "{desc_it}"
|
|
306
|
-
},
|
|
307
|
-
new NavigationTranslationSeedEntry
|
|
308
|
-
{
|
|
309
|
-
EntityType = NavigationEntityType.Module,
|
|
310
|
-
EntityId = moduleId,
|
|
311
|
-
LanguageCode = "de",
|
|
312
|
-
Label = "{label_de}",
|
|
313
|
-
Description = "{desc_de}"
|
|
314
|
-
}
|
|
315
|
-
};
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
private static Guid GenerateDeterministicGuid(string seed)
|
|
319
|
-
{
|
|
320
|
-
using var sha256 = System.Security.Cryptography.SHA256.Create();
|
|
321
|
-
var hash = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(seed));
|
|
322
|
-
return new Guid(hash.Take(16).ToArray());
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
/// <summary>
|
|
326
|
-
/// Converts PascalCase route segments to kebab-case for web URLs.
|
|
327
|
-
/// Example: /HumanResources/TimeManagement → /human-resources/time-management
|
|
328
|
-
/// </summary>
|
|
329
|
-
private static string ToKebabCase(string route)
|
|
330
|
-
{
|
|
331
|
-
if (string.IsNullOrEmpty(route)) return route;
|
|
332
|
-
|
|
333
|
-
var segments = route.Split('/');
|
|
334
|
-
var kebabSegments = new List<string>();
|
|
335
|
-
|
|
336
|
-
foreach (var segment in segments)
|
|
337
|
-
{
|
|
338
|
-
if (string.IsNullOrEmpty(segment))
|
|
339
|
-
{
|
|
340
|
-
kebabSegments.Add(segment);
|
|
341
|
-
continue;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// Convert PascalCase to kebab-case: HumanResources → human-resources
|
|
345
|
-
var kebab = System.Text.RegularExpressions.Regex
|
|
346
|
-
.Replace(segment, "([a-z])([A-Z])", "$1-$2")
|
|
347
|
-
.ToLowerInvariant();
|
|
348
|
-
kebabSegments.Add(kebab);
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
return string.Join("/", kebabSegments);
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
/// <summary>Seed entry DTO for navigation module.</summary>
|
|
356
|
-
public class NavigationModuleSeedEntry
|
|
357
|
-
{
|
|
358
|
-
public Guid Id { get; init; }
|
|
359
|
-
public Guid ApplicationId { get; init; }
|
|
360
|
-
public string Code { get; init; } = null!;
|
|
361
|
-
public string Label { get; init; } = null!;
|
|
362
|
-
public string Description { get; init; } = null!;
|
|
363
|
-
public string Icon { get; init; } = null!;
|
|
364
|
-
public IconType IconType { get; init; }
|
|
365
|
-
public string Route { get; init; } = null!;
|
|
366
|
-
public int DisplayOrder { get; init; }
|
|
367
|
-
public bool IsActive { get; init; }
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
/// <summary>Seed entry DTO for navigation translation.</summary>
|
|
371
|
-
public class NavigationTranslationSeedEntry
|
|
372
|
-
{
|
|
373
|
-
public NavigationEntityType EntityType { get; init; }
|
|
374
|
-
public Guid EntityId { get; init; }
|
|
375
|
-
public string LanguageCode { get; init; } = null!;
|
|
376
|
-
public string Label { get; init; } = null!;
|
|
377
|
-
public string Description { get; init; } = null!;
|
|
378
|
-
}
|
|
379
|
-
```
|
|
380
|
-
|
|
381
|
-
**Replace placeholders** with values from `navModules[]` and PRD `project` metadata.
|
|
382
|
-
|
|
383
|
-
---
|
|
384
|
-
|
|
385
|
-
## 2b. Navigation Sections & Resources (in same NavigationSeedData.cs)
|
|
386
|
-
|
|
387
|
-
> **CONDITIONAL:** Only generate if `seedDataCore.navigationSections[]` exists and is non-empty in feature.json.
|
|
388
|
-
> Sections and resources are added as additional methods in the **same** `{ModulePascal}NavigationSeedData.cs` file.
|
|
389
|
-
|
|
390
|
-
### GUID Generation Rules
|
|
391
|
-
|
|
392
|
-
```csharp
|
|
393
|
-
// Section GUID: deterministic from navRoute + section code
|
|
394
|
-
public static readonly Guid {SectionPascal}SectionId =
|
|
395
|
-
GenerateDeterministicGuid("navigation-section-{navRoute}.{sectionCode}");
|
|
396
|
-
// Example: GenerateDeterministicGuid("navigation-section-human-resources.employees.list")
|
|
397
|
-
|
|
398
|
-
// Resource GUID: deterministic from navRoute + section code + resource code
|
|
399
|
-
public static readonly Guid {ResourcePascal}ResourceId =
|
|
400
|
-
GenerateDeterministicGuid("navigation-resource-{navRoute}.{sectionCode}.{resourceCode}");
|
|
401
|
-
// Example: GenerateDeterministicGuid("navigation-resource-human-resources.employees.list.employees-grid")
|
|
402
|
-
```
|
|
403
|
-
|
|
404
|
-
### Section Methods (add to {ModulePascal}NavigationSeedData.cs)
|
|
405
|
-
|
|
406
|
-
> **ROUTE SPECIAL CASES (list and detail):**
|
|
407
|
-
> The `list` and `detail` sections are view modes of the module, NOT functional sub-areas.
|
|
408
|
-
> - `list` section route = module route (e.g., `/human-resources/employees`) — NO `/list` suffix
|
|
409
|
-
> - `detail` section route = module route + `/:id` (e.g., `/human-resources/employees/:id`) — NOT `/detail/:id`
|
|
410
|
-
> - FORBIDDEN: `/{module}/list`, `/{module}/detail/:id`
|
|
411
|
-
> - Other sections (dashboard, approve, import) = module route + `/{section-kebab}` (normal)
|
|
412
|
-
|
|
413
|
-
```csharp
|
|
414
|
-
// --- Add AFTER GetTranslationEntries() in {ModulePascal}NavigationSeedData.cs ---
|
|
415
|
-
|
|
416
|
-
// Deterministic GUIDs for sections
|
|
417
|
-
public static readonly Guid {Section1Pascal}SectionId =
|
|
418
|
-
GenerateDeterministicGuid("navigation-section-{navRoute}.{section1Code}");
|
|
419
|
-
// Repeat for each section...
|
|
420
|
-
|
|
421
|
-
/// <summary>
|
|
422
|
-
/// Returns navigation section entries for seeding into core.nav_Sections.
|
|
423
|
-
/// </summary>
|
|
424
|
-
public static IEnumerable<NavigationSectionSeedEntry> GetSectionEntries(Guid moduleId)
|
|
425
|
-
{
|
|
426
|
-
return new[]
|
|
427
|
-
{
|
|
428
|
-
new NavigationSectionSeedEntry
|
|
429
|
-
{
|
|
430
|
-
Id = {Section1Pascal}SectionId,
|
|
431
|
-
ModuleId = moduleId,
|
|
432
|
-
Code = "{section1Code}",
|
|
433
|
-
Label = "{section1_label_en}",
|
|
434
|
-
Description = "{section1_desc_en}",
|
|
435
|
-
Icon = "{section1_icon}",
|
|
436
|
-
IconType = IconType.Lucide,
|
|
437
|
-
// ROUTE CONVENTION:
|
|
438
|
-
// - "list" section → same as module route (no extra segment)
|
|
439
|
-
// - "detail" section → module route + "/:id"
|
|
440
|
-
// - Other sections → module route + "/{section-kebab}"
|
|
441
|
-
// FORBIDDEN: "/employees/list", "/employees/detail/:id"
|
|
442
|
-
Route = "{section_route}", // From seedDataCore.navigationSections[].route
|
|
443
|
-
DisplayOrder = {section1_sort},
|
|
444
|
-
IsActive = true
|
|
445
|
-
}
|
|
446
|
-
// Repeat for each section...
|
|
447
|
-
};
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
/// <summary>
|
|
451
|
-
/// Returns 4-language translations for sections.
|
|
452
|
-
/// </summary>
|
|
453
|
-
public static IEnumerable<NavigationTranslationSeedEntry> GetSectionTranslationEntries()
|
|
454
|
-
{
|
|
455
|
-
var entries = new List<NavigationTranslationSeedEntry>();
|
|
456
|
-
|
|
457
|
-
// Section: {section1Code}
|
|
458
|
-
var sec1Id = {Section1Pascal}SectionId;
|
|
459
|
-
entries.AddRange(new[]
|
|
460
|
-
{
|
|
461
|
-
new NavigationTranslationSeedEntry
|
|
462
|
-
{
|
|
463
|
-
EntityType = NavigationEntityType.Section,
|
|
464
|
-
EntityId = sec1Id,
|
|
465
|
-
LanguageCode = "fr",
|
|
466
|
-
Label = "{section1_label_fr}",
|
|
467
|
-
Description = "{section1_desc_fr}"
|
|
468
|
-
},
|
|
469
|
-
new NavigationTranslationSeedEntry
|
|
470
|
-
{
|
|
471
|
-
EntityType = NavigationEntityType.Section,
|
|
472
|
-
EntityId = sec1Id,
|
|
473
|
-
LanguageCode = "en",
|
|
474
|
-
Label = "{section1_label_en}",
|
|
475
|
-
Description = "{section1_desc_en}"
|
|
476
|
-
},
|
|
477
|
-
new NavigationTranslationSeedEntry
|
|
478
|
-
{
|
|
479
|
-
EntityType = NavigationEntityType.Section,
|
|
480
|
-
EntityId = sec1Id,
|
|
481
|
-
LanguageCode = "it",
|
|
482
|
-
Label = "{section1_label_it}",
|
|
483
|
-
Description = "{section1_desc_it}"
|
|
484
|
-
},
|
|
485
|
-
new NavigationTranslationSeedEntry
|
|
486
|
-
{
|
|
487
|
-
EntityType = NavigationEntityType.Section,
|
|
488
|
-
EntityId = sec1Id,
|
|
489
|
-
LanguageCode = "de",
|
|
490
|
-
Label = "{section1_label_de}",
|
|
491
|
-
Description = "{section1_desc_de}"
|
|
492
|
-
}
|
|
493
|
-
});
|
|
494
|
-
// Repeat for each section...
|
|
495
|
-
|
|
496
|
-
return entries;
|
|
497
|
-
}
|
|
498
|
-
```
|
|
499
|
-
|
|
500
|
-
### Resource Methods (add to {ModulePascal}NavigationSeedData.cs)
|
|
501
|
-
|
|
502
|
-
> **CONDITIONAL:** Only generate if `seedDataCore.navigationResources[]` exists and is non-empty.
|
|
503
|
-
|
|
504
|
-
```csharp
|
|
505
|
-
// --- Add AFTER GetSectionTranslationEntries() ---
|
|
506
|
-
|
|
507
|
-
// Deterministic GUIDs for resources
|
|
508
|
-
public static readonly Guid {Resource1Pascal}ResourceId =
|
|
509
|
-
GenerateDeterministicGuid("navigation-resource-{navRoute}.{parentSectionCode}.{resource1Code}");
|
|
510
|
-
// Repeat for each resource...
|
|
511
|
-
|
|
512
|
-
/// <summary>
|
|
513
|
-
/// Returns navigation resource entries for a given section.
|
|
514
|
-
/// </summary>
|
|
515
|
-
public static IEnumerable<NavigationResourceSeedEntry> GetResourceEntries(Guid sectionId)
|
|
516
|
-
{
|
|
517
|
-
var entries = new List<NavigationResourceSeedEntry>();
|
|
518
|
-
|
|
519
|
-
// Resources for section: {section1Code}
|
|
520
|
-
if (sectionId == {Section1Pascal}SectionId)
|
|
521
|
-
{
|
|
522
|
-
entries.AddRange(new[]
|
|
523
|
-
{
|
|
524
|
-
// RESOURCE ROUTE CONVENTION:
|
|
525
|
-
// Resources inherit their parent section's resolved route as base:
|
|
526
|
-
// - Under "list" section → base = module route (no /list)
|
|
527
|
-
// - Under "detail" section → base = module route (no /detail, resource routes don't include /:id)
|
|
528
|
-
// - Under other sections → base = module route + /{section-kebab}
|
|
529
|
-
// Then append: /{resource-kebab}
|
|
530
|
-
//
|
|
531
|
-
// Example: resource "export" under section "dashboard":
|
|
532
|
-
// Route = /human-resources/employees/dashboard/export
|
|
533
|
-
// Example: resource "employees-grid" under section "list":
|
|
534
|
-
// Route = /human-resources/employees/employees-grid (NOT /employees/list/employees-grid)
|
|
535
|
-
new NavigationResourceSeedEntry
|
|
536
|
-
{
|
|
537
|
-
Id = {Resource1Pascal}ResourceId,
|
|
538
|
-
SectionId = sectionId,
|
|
539
|
-
Code = "{resource1Code}",
|
|
540
|
-
Label = "{resource1_label_en}",
|
|
541
|
-
EntityType = "{resource1_entity}",
|
|
542
|
-
// Use parent section's resolved route + /{resource-kebab}
|
|
543
|
-
// For "list"/"detail" sections, the section route = module route (no /list or /detail segment)
|
|
544
|
-
Route = "{resource_route}", // From seedDataCore: parent section route + /{resource-kebab}
|
|
545
|
-
DisplayOrder = 1
|
|
546
|
-
}
|
|
547
|
-
// Repeat for each resource in this section...
|
|
548
|
-
});
|
|
549
|
-
}
|
|
550
|
-
// Repeat if-block for each section with resources...
|
|
551
|
-
|
|
552
|
-
return entries;
|
|
553
|
-
}
|
|
554
|
-
```
|
|
555
|
-
|
|
556
|
-
### Additional DTO Classes (add to bottom of file)
|
|
557
|
-
|
|
558
|
-
```csharp
|
|
559
|
-
/// <summary>Seed entry DTO for navigation section.</summary>
|
|
560
|
-
public class NavigationSectionSeedEntry
|
|
561
|
-
{
|
|
562
|
-
public Guid Id { get; init; }
|
|
563
|
-
public Guid ModuleId { get; init; }
|
|
564
|
-
public string Code { get; init; } = null!;
|
|
565
|
-
public string Label { get; init; } = null!;
|
|
566
|
-
public string Description { get; init; } = null!;
|
|
567
|
-
public string Icon { get; init; } = null!;
|
|
568
|
-
public IconType IconType { get; init; }
|
|
569
|
-
public string Route { get; init; } = null!;
|
|
570
|
-
public int DisplayOrder { get; init; }
|
|
571
|
-
public bool IsActive { get; init; }
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
/// <summary>Seed entry DTO for navigation resource.</summary>
|
|
575
|
-
public class NavigationResourceSeedEntry
|
|
576
|
-
{
|
|
577
|
-
public Guid Id { get; init; }
|
|
578
|
-
public Guid SectionId { get; init; }
|
|
579
|
-
public string Code { get; init; } = null!;
|
|
580
|
-
public string Label { get; init; } = null!;
|
|
581
|
-
public string? EntityType { get; init; }
|
|
582
|
-
public string? Route { get; init; }
|
|
583
|
-
public int DisplayOrder { get; init; }
|
|
584
|
-
}
|
|
585
|
-
```
|
|
586
|
-
|
|
587
|
-
### Placeholder Values Source
|
|
588
|
-
|
|
589
|
-
| Placeholder | Source in feature.json |
|
|
590
|
-
|-------------|----------------------|
|
|
591
|
-
| `{sectionCode}` | `seedDataCore.navigationSections[].code` |
|
|
592
|
-
| `{section_label_xx}` | `specification.navigation.entries[]` where `level == "section"` → `labels.xx` |
|
|
593
|
-
| `{section_icon}` | `seedDataCore.navigationSections[].icon` |
|
|
594
|
-
| `{section_sort}` | `seedDataCore.navigationSections[].sort` |
|
|
595
|
-
| `{section_route}` | `seedDataCore.navigationSections[].route` — **SPECIAL CASES:** `list` → module route (no `/list`), `detail` → module route + `/:id` (no `/detail/:id`), others → module route + `/{section-kebab}` |
|
|
596
|
-
| `{resourceCode}` | `seedDataCore.navigationResources[].code` |
|
|
597
|
-
| `{resource_entity}` | `seedDataCore.navigationResources[].entity` |
|
|
598
|
-
| `{resource_route}` | Computed from parent section route + `/{resource-kebab}`. **SPECIAL CASES:** if parent section is `list` → module route + `/{resource-kebab}` (no `/list/`), if parent is `detail` → module route + `/{resource-kebab}` (no `/detail/`). |
|
|
599
|
-
| `{parentSectionCode}` | `seedDataCore.navigationResources[].parentCode` |
|
|
600
|
-
|
|
601
|
-
---
|
|
602
|
-
|
|
603
|
-
## 3. PermissionsSeedData.cs — MCP-First
|
|
604
|
-
|
|
605
|
-
### CRITICAL: PermissionAction Safety Rules
|
|
606
|
-
|
|
607
|
-
> **NEVER use `Enum.Parse<PermissionAction>("...")` — this causes runtime crashes if the string is invalid.**
|
|
608
|
-
> The error only manifests at application startup, not at compile time.
|
|
609
|
-
|
|
610
|
-
**Valid PermissionAction values (from SmartStack.Domain.Authorization):**
|
|
611
|
-
|
|
612
|
-
| Enum Value | Int | Description |
|
|
613
|
-
|------------|-----|-------------|
|
|
614
|
-
| `PermissionAction.Access` | 0 | Wildcard permissions only (IsWildcard = true) |
|
|
615
|
-
| `PermissionAction.Read` | 1 | GET/HEAD — View data |
|
|
616
|
-
| `PermissionAction.Create` | 2 | POST — Create new records |
|
|
617
|
-
| `PermissionAction.Update` | 3 | PUT/PATCH — Modify existing records |
|
|
618
|
-
| `PermissionAction.Delete` | 4 | DELETE — Remove records |
|
|
619
|
-
| `PermissionAction.Export` | 5 | Export data (CSV, Excel, etc.) |
|
|
620
|
-
| `PermissionAction.Import` | 6 | Import data |
|
|
621
|
-
| `PermissionAction.Approve` | 7 | Approve workflow items |
|
|
622
|
-
| `PermissionAction.Reject` | 8 | Reject workflow items |
|
|
623
|
-
| `PermissionAction.Assign` | 9 | Assign items to users |
|
|
624
|
-
| `PermissionAction.Execute` | 10 | Execute actions (sync, run, etc.) |
|
|
625
|
-
|
|
626
|
-
**Anti-patterns (FORBIDDEN):**
|
|
627
|
-
|
|
628
|
-
```csharp
|
|
629
|
-
// FORBIDDEN — Runtime crash if string is not a valid enum value
|
|
630
|
-
Enum.Parse<PermissionAction>("Validate"); // ArgumentException at startup
|
|
631
|
-
(PermissionAction)Enum.Parse(typeof(PermissionAction), "Validate"); // Same crash
|
|
632
|
-
|
|
633
|
-
// FORBIDDEN — String-based action in anonymous objects
|
|
634
|
-
new { Action = "read" }; // Not type-safe, silent mismatch possible
|
|
635
|
-
```
|
|
636
|
-
|
|
637
|
-
**Correct patterns (MANDATORY):**
|
|
638
|
-
|
|
639
|
-
```csharp
|
|
640
|
-
// ALWAYS use the typed enum directly — compile-time safe
|
|
641
|
-
Action = PermissionAction.Read // Compile-time checked
|
|
642
|
-
Action = PermissionAction.Create // Compile-time checked
|
|
643
|
-
Action = PermissionAction.Approve // Compile-time checked
|
|
644
|
-
|
|
645
|
-
// For custom actions beyond standard CRUD, pick from the enum:
|
|
646
|
-
// Export, Import, Approve, Reject, Assign, Execute
|
|
647
|
-
```
|
|
648
|
-
|
|
649
|
-
**MCP validation:** `validate_conventions` with `checks: ["permissions"]` will detect and flag these anti-patterns.
|
|
650
|
-
|
|
651
|
-
### Step A: Call MCP (PRIMARY)
|
|
652
|
-
|
|
653
|
-
```
|
|
654
|
-
Tool: mcp__smartstack__generate_permissions
|
|
655
|
-
Args:
|
|
656
|
-
navRoute: "{navRoute}"
|
|
657
|
-
includeStandardActions: true
|
|
658
|
-
includeWildcard: true
|
|
659
|
-
```
|
|
660
|
-
|
|
661
|
-
MCP returns:
|
|
662
|
-
- `Permissions.cs` nested class (Application layer constants)
|
|
663
|
-
- Permission seed entries with deterministic GUIDs
|
|
664
|
-
|
|
665
|
-
### Step B: Write Permissions.cs (Application layer)
|
|
666
|
-
|
|
667
|
-
> **CRITICAL — Permission paths use the SAME kebab-case as NavRoute codes.**
|
|
668
|
-
> `{navRoute}` is already kebab-case (e.g., `human-resources.employees`).
|
|
669
|
-
> NEVER strip hyphens or derive codes from C# class names.
|
|
670
|
-
> FORBIDDEN: `humanresources.employees.read` → CORRECT: `human-resources.employees.read`
|
|
671
|
-
> SmartStack.app reference: `support-client.my-tickets.read`
|
|
672
|
-
|
|
673
|
-
```csharp
|
|
674
|
-
// Add to Application/Common/Authorization/Permissions.cs
|
|
675
|
-
// IMPORTANT: {navRoute} uses kebab-case segments (e.g., "human-resources.employees")
|
|
676
|
-
// Do NOT derive permission codes from C# identifiers — use navRoute directly
|
|
677
|
-
public static class {AppPascal}
|
|
678
|
-
{
|
|
679
|
-
public static class {ModulePascal}
|
|
680
|
-
{
|
|
681
|
-
public const string View = "{navRoute}.read"; // e.g., "human-resources.employees.read"
|
|
682
|
-
public const string Create = "{navRoute}.create";
|
|
683
|
-
public const string Update = "{navRoute}.update";
|
|
684
|
-
public const string Delete = "{navRoute}.delete";
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
```
|
|
688
|
-
|
|
689
|
-
### Step C: Write PermissionsSeedData.cs (Infrastructure layer)
|
|
690
|
-
|
|
691
|
-
**File:** `Infrastructure/Persistence/Seeding/Data/{ModulePascal}/PermissionsSeedData.cs`
|
|
692
|
-
|
|
693
|
-
```csharp
|
|
694
|
-
namespace {BaseNamespace}.Infrastructure.Persistence.Seeding.Data.{ModulePascal};
|
|
695
|
-
|
|
696
|
-
/// <summary>
|
|
697
|
-
/// Permission seed data for {ModuleLabel} module.
|
|
698
|
-
/// Consumed by IClientSeedDataProvider at application startup.
|
|
699
|
-
/// </summary>
|
|
700
|
-
public static class {ModulePascal}PermissionsSeedData
|
|
701
|
-
{
|
|
702
|
-
// Deterministic GUIDs for permissions
|
|
703
|
-
public static readonly Guid WildcardPermId = GenerateGuid("{navRoute}.*");
|
|
704
|
-
public static readonly Guid ReadPermId = GenerateGuid("{navRoute}.read");
|
|
705
|
-
public static readonly Guid CreatePermId = GenerateGuid("{navRoute}.create");
|
|
706
|
-
public static readonly Guid UpdatePermId = GenerateGuid("{navRoute}.update");
|
|
707
|
-
public static readonly Guid DeletePermId = GenerateGuid("{navRoute}.delete");
|
|
708
|
-
|
|
709
|
-
public static IEnumerable<PermissionSeedEntry> GetPermissionEntries(Guid moduleId)
|
|
710
|
-
{
|
|
711
|
-
return new[]
|
|
712
|
-
{
|
|
713
|
-
new PermissionSeedEntry { Id = WildcardPermId, Path = "{navRoute}.*", Level = PermissionLevel.Module, Action = PermissionAction.Access, IsWildcard = true, ModuleId = moduleId, Description = "Full {moduleLabel} access" },
|
|
714
|
-
new PermissionSeedEntry { Id = ReadPermId, Path = "{navRoute}.read", Level = PermissionLevel.Module, Action = PermissionAction.Read, IsWildcard = false, ModuleId = moduleId, Description = "View {moduleLabel}" },
|
|
715
|
-
new PermissionSeedEntry { Id = CreatePermId, Path = "{navRoute}.create", Level = PermissionLevel.Module, Action = PermissionAction.Create, IsWildcard = false, ModuleId = moduleId, Description = "Create {moduleLabel}" },
|
|
716
|
-
new PermissionSeedEntry { Id = UpdatePermId, Path = "{navRoute}.update", Level = PermissionLevel.Module, Action = PermissionAction.Update, IsWildcard = false, ModuleId = moduleId, Description = "Update {moduleLabel}" },
|
|
717
|
-
new PermissionSeedEntry { Id = DeletePermId, Path = "{navRoute}.delete", Level = PermissionLevel.Module, Action = PermissionAction.Delete, IsWildcard = false, ModuleId = moduleId, Description = "Delete {moduleLabel}" }
|
|
718
|
-
};
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
private static Guid GenerateGuid(string path)
|
|
722
|
-
{
|
|
723
|
-
using var sha256 = System.Security.Cryptography.SHA256.Create();
|
|
724
|
-
var hash = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes($"permission-{path}"));
|
|
725
|
-
return new Guid(hash.Take(16).ToArray());
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
public class PermissionSeedEntry
|
|
730
|
-
{
|
|
731
|
-
public Guid Id { get; init; }
|
|
732
|
-
public string Path { get; init; } = null!;
|
|
733
|
-
public PermissionLevel Level { get; init; }
|
|
734
|
-
public PermissionAction Action { get; init; }
|
|
735
|
-
public bool IsWildcard { get; init; }
|
|
736
|
-
public Guid ModuleId { get; init; }
|
|
737
|
-
public string Description { get; init; } = null!;
|
|
738
|
-
}
|
|
739
|
-
```
|
|
740
|
-
|
|
741
|
-
### Step C2: Section-Level Permissions (CONDITIONAL: only if `navSections[]` defined)
|
|
742
|
-
|
|
743
|
-
> When `seedDataCore.navigationSections` exists and is non-empty in feature.json,
|
|
744
|
-
> add section-level permission GUIDs and entries to `PermissionsSeedData.cs`.
|
|
745
|
-
|
|
746
|
-
```csharp
|
|
747
|
-
// --- Add to {ModulePascal}PermissionsSeedData class AFTER module-level permissions ---
|
|
748
|
-
|
|
749
|
-
// Section-level permissions (for each section in navSections[])
|
|
750
|
-
public static readonly Guid {SectionPascal}WildcardPermId = GenerateGuid("{navRoute}.{sectionCode}.*");
|
|
751
|
-
public static readonly Guid {SectionPascal}ReadPermId = GenerateGuid("{navRoute}.{sectionCode}.read");
|
|
752
|
-
public static readonly Guid {SectionPascal}CreatePermId = GenerateGuid("{navRoute}.{sectionCode}.create");
|
|
753
|
-
public static readonly Guid {SectionPascal}UpdatePermId = GenerateGuid("{navRoute}.{sectionCode}.update");
|
|
754
|
-
public static readonly Guid {SectionPascal}DeletePermId = GenerateGuid("{navRoute}.{sectionCode}.delete");
|
|
755
|
-
// Repeat for each section...
|
|
756
|
-
|
|
757
|
-
// Add to GetPermissionEntries() — AFTER module-level entries:
|
|
758
|
-
// Section: {sectionCode}
|
|
759
|
-
new PermissionSeedEntry { Id = {SectionPascal}WildcardPermId, Path = "{navRoute}.{sectionCode}.*", Level = PermissionLevel.Section, Action = PermissionAction.Access, IsWildcard = true, ModuleId = moduleId, Description = "Full {sectionLabel} access" },
|
|
760
|
-
new PermissionSeedEntry { Id = {SectionPascal}ReadPermId, Path = "{navRoute}.{sectionCode}.read", Level = PermissionLevel.Section, Action = PermissionAction.Read, IsWildcard = false, ModuleId = moduleId, Description = "View {sectionLabel}" },
|
|
761
|
-
new PermissionSeedEntry { Id = {SectionPascal}CreatePermId, Path = "{navRoute}.{sectionCode}.create", Level = PermissionLevel.Section, Action = PermissionAction.Create, IsWildcard = false, ModuleId = moduleId, Description = "Create {sectionLabel}" },
|
|
762
|
-
new PermissionSeedEntry { Id = {SectionPascal}UpdatePermId, Path = "{navRoute}.{sectionCode}.update", Level = PermissionLevel.Section, Action = PermissionAction.Update, IsWildcard = false, ModuleId = moduleId, Description = "Update {sectionLabel}" },
|
|
763
|
-
new PermissionSeedEntry { Id = {SectionPascal}DeletePermId, Path = "{navRoute}.{sectionCode}.delete", Level = PermissionLevel.Section, Action = PermissionAction.Delete, IsWildcard = false, ModuleId = moduleId, Description = "Delete {sectionLabel}" },
|
|
764
|
-
// Repeat for each section...
|
|
765
|
-
```
|
|
766
|
-
|
|
767
|
-
Also add section-level constants to `Permissions.cs` (Application layer):
|
|
768
|
-
|
|
769
|
-
```csharp
|
|
770
|
-
public static class {ModulePascal}
|
|
771
|
-
{
|
|
772
|
-
// ... existing module-level permissions ...
|
|
773
|
-
|
|
774
|
-
// Section-level (for each section in navSections[])
|
|
775
|
-
public static class {SectionPascal}
|
|
776
|
-
{
|
|
777
|
-
public const string View = "{navRoute}.{sectionCode}.read";
|
|
778
|
-
public const string Create = "{navRoute}.{sectionCode}.create";
|
|
779
|
-
public const string Update = "{navRoute}.{sectionCode}.update";
|
|
780
|
-
public const string Delete = "{navRoute}.{sectionCode}.delete";
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
```
|
|
784
|
-
|
|
785
|
-
### Step D: MCP Fallback
|
|
786
|
-
|
|
787
|
-
If MCP `generate_permissions` fails, use the template above directly with values derived from the PRD `coreSeedData.permissions[]`.
|
|
788
|
-
|
|
789
|
-
---
|
|
790
|
-
|
|
791
|
-
## 4. ApplicationRolesSeedData.cs (Application-Level, Once per Application)
|
|
792
|
-
|
|
793
|
-
**File:** `Infrastructure/Persistence/Seeding/Data/ApplicationRolesSeedData.cs`
|
|
794
|
-
|
|
795
|
-
### Purpose
|
|
796
|
-
|
|
797
|
-
Creates the 4 standard application-scoped roles: Admin, Manager, Contributor, Viewer.
|
|
798
|
-
|
|
799
|
-
> **CRITICAL — SmartStack core MAY already provide system roles (admin, manager, contributor, viewer).**
|
|
800
|
-
> If system roles already exist in `auth_Roles`, do NOT create duplicates.
|
|
801
|
-
> `SeedRolesAsync()` MUST check existence by Code, not just by ApplicationId.
|
|
802
|
-
> **For RolePermission mappings:** ALWAYS look up roles by Code at runtime (in `SeedRolePermissionsAsync()`).
|
|
803
|
-
> **FORBIDDEN:** Using `GenerateRoleGuid()`, `DeterministicGuid("role:admin")`, or any hardcoded role GUID
|
|
804
|
-
> when creating RolePermission entries. The role GUIDs may differ from what's in the database.
|
|
805
|
-
|
|
806
|
-
**CRITICAL:** This file is created **ONCE per application** (not per module).
|
|
807
|
-
|
|
808
|
-
### GUID Generation Rule
|
|
809
|
-
|
|
810
|
-
> **WARNING:** These deterministic GUIDs are ONLY used for role creation (if roles don't already exist).
|
|
811
|
-
> They MUST NEVER be used for RolePermission mapping — always look up roles by Code at runtime.
|
|
812
|
-
|
|
813
|
-
```csharp
|
|
814
|
-
// Deterministic GUID from application ID + role type
|
|
815
|
-
// WARNING: Only for role creation. NEVER use these GUIDs for RolePermission mapping.
|
|
816
|
-
// RolePermissions MUST resolve roles by Code at runtime (see SeedRolePermissionsAsync).
|
|
817
|
-
private static Guid GenerateRoleGuid(string roleType)
|
|
818
|
-
{
|
|
819
|
-
using var sha256 = System.Security.Cryptography.SHA256.Create();
|
|
820
|
-
var hash = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes($"role-{ApplicationId}-{roleType}"));
|
|
821
|
-
return new Guid(hash.Take(16).ToArray());
|
|
822
|
-
}
|
|
823
|
-
// FORBIDDEN in RolePermission mapping:
|
|
824
|
-
// var roleId = GenerateRoleGuid("admin"); // WRONG — GUID may not match DB
|
|
825
|
-
// var roleId = DeterministicGuid("role:admin"); // WRONG — use Code lookup instead
|
|
826
|
-
```
|
|
827
|
-
|
|
828
|
-
### Template
|
|
829
|
-
|
|
830
|
-
```csharp
|
|
831
|
-
using SmartStack.Domain.Platform.Administration.Roles;
|
|
832
|
-
|
|
833
|
-
namespace {BaseNamespace}.Infrastructure.Persistence.Seeding.Data;
|
|
834
|
-
|
|
835
|
-
/// <summary>
|
|
836
|
-
/// Application-scoped role seed data for {AppLabel}.
|
|
837
|
-
/// Defines the 4 standard application roles: Admin, Manager, Contributor, Viewer.
|
|
838
|
-
/// Consumed by IClientSeedDataProvider at application startup.
|
|
839
|
-
/// </summary>
|
|
840
|
-
public static class ApplicationRolesSeedData
|
|
841
|
-
{
|
|
842
|
-
// Application ID from NavigationApplicationSeedData (deterministic GUID)
|
|
843
|
-
public static readonly Guid ApplicationId = NavigationApplicationSeedData.ApplicationId;
|
|
844
|
-
|
|
845
|
-
public static readonly Guid AdminRoleId = GenerateRoleGuid("admin");
|
|
846
|
-
public static readonly Guid ManagerRoleId = GenerateRoleGuid("manager");
|
|
847
|
-
public static readonly Guid ContributorRoleId = GenerateRoleGuid("contributor");
|
|
848
|
-
public static readonly Guid ViewerRoleId = GenerateRoleGuid("viewer");
|
|
849
|
-
|
|
850
|
-
public static IEnumerable<ApplicationRoleSeedEntry> GetRoleEntries()
|
|
851
|
-
{
|
|
852
|
-
yield return new ApplicationRoleSeedEntry
|
|
853
|
-
{
|
|
854
|
-
Id = AdminRoleId,
|
|
855
|
-
Code = "admin",
|
|
856
|
-
Name = "{AppLabel} Admin",
|
|
857
|
-
Description = "Full administrative access to {AppLabel}",
|
|
858
|
-
ApplicationId = ApplicationId,
|
|
859
|
-
IsSystem = false,
|
|
860
|
-
IsActive = true,
|
|
861
|
-
DisplayOrder = 1
|
|
862
|
-
};
|
|
863
|
-
|
|
864
|
-
yield return new ApplicationRoleSeedEntry
|
|
865
|
-
{
|
|
866
|
-
Id = ManagerRoleId,
|
|
867
|
-
Code = "manager",
|
|
868
|
-
Name = "{AppLabel} Manager",
|
|
869
|
-
Description = "Management access to {AppLabel} (Create, Read, Update)",
|
|
870
|
-
ApplicationId = ApplicationId,
|
|
871
|
-
IsSystem = false,
|
|
872
|
-
IsActive = true,
|
|
873
|
-
DisplayOrder = 2
|
|
874
|
-
};
|
|
875
|
-
|
|
876
|
-
yield return new ApplicationRoleSeedEntry
|
|
877
|
-
{
|
|
878
|
-
Id = ContributorRoleId,
|
|
879
|
-
Code = "contributor",
|
|
880
|
-
Name = "{AppLabel} Contributor",
|
|
881
|
-
Description = "Contributor access to {AppLabel} (Create, Read)",
|
|
882
|
-
ApplicationId = ApplicationId,
|
|
883
|
-
IsSystem = false,
|
|
884
|
-
IsActive = true,
|
|
885
|
-
DisplayOrder = 3
|
|
886
|
-
};
|
|
887
|
-
|
|
888
|
-
yield return new ApplicationRoleSeedEntry
|
|
889
|
-
{
|
|
890
|
-
Id = ViewerRoleId,
|
|
891
|
-
Code = "viewer",
|
|
892
|
-
Name = "{AppLabel} Viewer",
|
|
893
|
-
Description = "Read-only access to {AppLabel}",
|
|
894
|
-
ApplicationId = ApplicationId,
|
|
895
|
-
IsSystem = false,
|
|
896
|
-
IsActive = true,
|
|
897
|
-
DisplayOrder = 4
|
|
898
|
-
};
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
private static Guid GenerateRoleGuid(string roleType)
|
|
902
|
-
{
|
|
903
|
-
using var sha256 = System.Security.Cryptography.SHA256.Create();
|
|
904
|
-
var hash = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes($"role-{ApplicationId}-{roleType}"));
|
|
905
|
-
return new Guid(hash.Take(16).ToArray());
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
public class ApplicationRoleSeedEntry
|
|
910
|
-
{
|
|
911
|
-
public Guid Id { get; init; }
|
|
912
|
-
public string Code { get; init; } = null!;
|
|
913
|
-
public string Name { get; init; } = null!;
|
|
914
|
-
public string Description { get; init; } = null!;
|
|
915
|
-
public Guid ApplicationId { get; init; }
|
|
916
|
-
public bool IsSystem { get; init; }
|
|
917
|
-
public bool IsActive { get; init; }
|
|
918
|
-
public int DisplayOrder { get; init; }
|
|
919
|
-
}
|
|
920
|
-
```
|
|
921
|
-
|
|
922
|
-
**Replace placeholders** with values from PRD and navigation metadata.
|
|
923
|
-
|
|
924
|
-
---
|
|
925
|
-
|
|
926
|
-
## 5. {Module}RolesSeedData.cs (Per Module)
|
|
927
|
-
|
|
928
|
-
**File:** `Infrastructure/Persistence/Seeding/Data/{ModulePascal}/RolesSeedData.cs`
|
|
929
|
-
|
|
930
|
-
> **CRITICAL:** This file uses `RoleCode` (string), NOT role GUIDs.
|
|
931
|
-
> Roles are resolved by Code at runtime in `SeedRolePermissionsAsync()`.
|
|
932
|
-
> **FORBIDDEN:** `DeterministicGuid("role:admin")`, `GenerateRoleGuid("admin")`, or any hardcoded Guid for roles.
|
|
933
|
-
> SmartStack core pre-seeds system roles — their IDs are NOT deterministic from the client perspective.
|
|
934
|
-
|
|
935
|
-
### Context-Based Role Mapping
|
|
936
|
-
|
|
937
|
-
| Application | Admin | Manager | Contributor | Viewer |
|
|
938
|
-
|-------------|-------|---------|-------------|--------|
|
|
939
|
-
| Any | CRUD | CRU | CR | R |
|
|
940
|
-
|
|
941
|
-
### Template
|
|
942
|
-
|
|
943
|
-
```csharp
|
|
944
|
-
namespace {BaseNamespace}.Infrastructure.Persistence.Seeding.Data.{ModulePascal};
|
|
945
|
-
|
|
946
|
-
/// <summary>
|
|
947
|
-
/// Role-permission mapping seed data for {ModuleLabel} module.
|
|
948
|
-
/// Maps permissions to application-scoped roles (Admin, Manager, Contributor, Viewer).
|
|
949
|
-
/// Consumed by IClientSeedDataProvider at application startup.
|
|
950
|
-
/// </summary>
|
|
951
|
-
public static class {ModulePascal}RolesSeedData
|
|
952
|
-
{
|
|
953
|
-
/// <summary>
|
|
954
|
-
/// Returns role-permission mappings for this module.
|
|
955
|
-
/// Roles are resolved at runtime by Code (not hardcoded GUIDs).
|
|
956
|
-
/// </summary>
|
|
957
|
-
public static IEnumerable<RolePermissionSeedEntry> GetRolePermissionEntries()
|
|
958
|
-
{
|
|
959
|
-
// Admin: wildcard access
|
|
960
|
-
yield return new RolePermissionSeedEntry { RoleCode = "admin", PermissionPath = "{navRoute}.*" };
|
|
961
|
-
|
|
962
|
-
// Manager: CRU (read + create + update — no delete)
|
|
963
|
-
yield return new RolePermissionSeedEntry { RoleCode = "manager", PermissionPath = "{navRoute}.read" };
|
|
964
|
-
yield return new RolePermissionSeedEntry { RoleCode = "manager", PermissionPath = "{navRoute}.create" };
|
|
965
|
-
yield return new RolePermissionSeedEntry { RoleCode = "manager", PermissionPath = "{navRoute}.update" };
|
|
966
|
-
|
|
967
|
-
// Contributor: CR
|
|
968
|
-
yield return new RolePermissionSeedEntry { RoleCode = "contributor", PermissionPath = "{navRoute}.read" };
|
|
969
|
-
yield return new RolePermissionSeedEntry { RoleCode = "contributor", PermissionPath = "{navRoute}.create" };
|
|
970
|
-
|
|
971
|
-
// Viewer: R
|
|
972
|
-
yield return new RolePermissionSeedEntry { RoleCode = "viewer", PermissionPath = "{navRoute}.read" };
|
|
973
|
-
|
|
974
|
-
// --- Section-level role mappings (CONDITIONAL: for each section in navSections[]) ---
|
|
975
|
-
// Admin: wildcard per section
|
|
976
|
-
yield return new RolePermissionSeedEntry { RoleCode = "admin", PermissionPath = "{navRoute}.{sectionCode}.*" };
|
|
977
|
-
|
|
978
|
-
// Manager: CRU per section (read + create + update — no delete)
|
|
979
|
-
yield return new RolePermissionSeedEntry { RoleCode = "manager", PermissionPath = "{navRoute}.{sectionCode}.read" };
|
|
980
|
-
yield return new RolePermissionSeedEntry { RoleCode = "manager", PermissionPath = "{navRoute}.{sectionCode}.create" };
|
|
981
|
-
yield return new RolePermissionSeedEntry { RoleCode = "manager", PermissionPath = "{navRoute}.{sectionCode}.update" };
|
|
982
|
-
|
|
983
|
-
// Contributor: CR per section
|
|
984
|
-
yield return new RolePermissionSeedEntry { RoleCode = "contributor", PermissionPath = "{navRoute}.{sectionCode}.read" };
|
|
985
|
-
yield return new RolePermissionSeedEntry { RoleCode = "contributor", PermissionPath = "{navRoute}.{sectionCode}.create" };
|
|
986
|
-
|
|
987
|
-
// Viewer: R per section
|
|
988
|
-
yield return new RolePermissionSeedEntry { RoleCode = "viewer", PermissionPath = "{navRoute}.{sectionCode}.read" };
|
|
989
|
-
// Repeat block for each section in navSections[]...
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
public class RolePermissionSeedEntry
|
|
994
|
-
{
|
|
995
|
-
public string RoleCode { get; init; } = null!;
|
|
996
|
-
public string PermissionPath { get; init; } = null!;
|
|
997
|
-
}
|
|
998
|
-
```
|
|
999
|
-
|
|
1000
|
-
---
|
|
1001
|
-
|
|
1002
|
-
## 6. IClientSeedDataProvider Implementation
|
|
1003
|
-
|
|
1004
|
-
**File:** `Infrastructure/Persistence/Seeding/{AppPascalName}SeedDataProvider.cs`
|
|
1005
|
-
|
|
1006
|
-
### Critical Rules
|
|
1007
|
-
|
|
1008
|
-
| Rule | Description |
|
|
1009
|
-
|------|-------------|
|
|
1010
|
-
| Factory methods | `NavigationModule.Create(...)`, `Role.Create(...)`, `Permission.CreateForModule(...)`, `RolePermission.Create(...)` — NEVER `new Entity()` |
|
|
1011
|
-
| Idempotence | Each Seed method checks existence before inserting |
|
|
1012
|
-
| Execution order | Navigation → Roles → Permissions → RolePermissions (roles MUST exist before mapping) |
|
|
1013
|
-
| SaveChanges per group | Navigation -> save -> Roles -> save -> Permissions -> save -> RolePermissions -> save |
|
|
1014
|
-
| FK resolution by Code | Parent entities (modules, roles) found by `Code`, not hardcoded GUID |
|
|
1015
|
-
| DI registration | `services.AddScoped<IClientSeedDataProvider, {AppPascalName}SeedDataProvider>()` |
|
|
1016
|
-
|
|
1017
|
-
### Template
|
|
1018
|
-
|
|
1019
|
-
```csharp
|
|
1020
|
-
using Microsoft.EntityFrameworkCore;
|
|
1021
|
-
using SmartStack.Application.Common.Interfaces;
|
|
1022
|
-
using SmartStack.Application.Common.Interfaces.Seeding;
|
|
1023
|
-
using SmartStack.Domain.Navigation;
|
|
1024
|
-
using SmartStack.Domain.Platform.Administration.Roles;
|
|
1025
|
-
using {BaseNamespace}.Infrastructure.Persistence.Seeding.Data;
|
|
1026
|
-
using {BaseNamespace}.Infrastructure.Persistence.Seeding.Data.{Module1Pascal};
|
|
1027
|
-
// using {BaseNamespace}.Infrastructure.Persistence.Seeding.Data.{Module2Pascal}; // Add per module
|
|
1028
|
-
|
|
1029
|
-
namespace {BaseNamespace}.Infrastructure.Persistence.Seeding;
|
|
1030
|
-
|
|
1031
|
-
/// <summary>
|
|
1032
|
-
/// Seeds {AppLabel} navigation, roles, permissions, and role-permission data
|
|
1033
|
-
/// into the SmartStack Core schema at application startup.
|
|
1034
|
-
/// </summary>
|
|
1035
|
-
public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
|
|
1036
|
-
{
|
|
1037
|
-
public int Order => 100;
|
|
1038
|
-
|
|
1039
|
-
public async Task SeedNavigationAsync(ICoreDbContext context, CancellationToken ct)
|
|
1040
|
-
{
|
|
1041
|
-
// --- Application (from NavigationApplicationSeedData) ---
|
|
1042
|
-
// NOTE: Idempotence is at MODULE level (not application level).
|
|
1043
|
-
// If the application already exists, we load it and continue to seed any missing modules.
|
|
1044
|
-
// This allows adding Module 2+ to an existing application without re-running the full seed.
|
|
1045
|
-
var appEntry = NavigationApplicationSeedData.GetApplicationEntry();
|
|
1046
|
-
var existingApp = await context.NavigationApplications
|
|
1047
|
-
.FirstOrDefaultAsync(a => a.Code == appEntry.Code, ct);
|
|
1048
|
-
|
|
1049
|
-
NavigationApplication app;
|
|
1050
|
-
if (existingApp != null)
|
|
1051
|
-
{
|
|
1052
|
-
app = existingApp; // Application already seeded — reuse it for module seeding below
|
|
1053
|
-
}
|
|
1054
|
-
else
|
|
1055
|
-
{
|
|
1056
|
-
app = NavigationApplication.Create(
|
|
1057
|
-
appEntry.Code, appEntry.Label,
|
|
1058
|
-
appEntry.Description, appEntry.Icon, appEntry.IconType,
|
|
1059
|
-
appEntry.Route, appEntry.DisplayOrder);
|
|
1060
|
-
context.NavigationApplications.Add(app);
|
|
1061
|
-
await ((DbContext)context).SaveChangesAsync(ct);
|
|
1062
|
-
|
|
1063
|
-
// --- Application translations (4 languages, from NavigationApplicationSeedData) ---
|
|
1064
|
-
foreach (var t in NavigationApplicationSeedData.GetTranslationEntries())
|
|
1065
|
-
{
|
|
1066
|
-
context.NavigationTranslations.Add(
|
|
1067
|
-
NavigationTranslation.Create(t.EntityType, t.EntityId, t.LanguageCode, t.Label, t.Description));
|
|
1068
|
-
}
|
|
1069
|
-
await ((DbContext)context).SaveChangesAsync(ct);
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
// --- Modules (idempotent per-module — allows adding Module 2+ later) ---
|
|
1073
|
-
// Module: {Module1}
|
|
1074
|
-
var mod1Exists = await context.NavigationModules
|
|
1075
|
-
.AnyAsync(m => m.Code == "{module1Code}" && m.ApplicationId == app.Id, ct);
|
|
1076
|
-
if (!mod1Exists)
|
|
1077
|
-
{
|
|
1078
|
-
var mod1Entry = {Module1Pascal}NavigationSeedData.GetModuleEntry(app.Id);
|
|
1079
|
-
var mod1 = NavigationModule.Create(
|
|
1080
|
-
mod1Entry.ApplicationId, mod1Entry.Code, mod1Entry.Label,
|
|
1081
|
-
mod1Entry.Description, mod1Entry.Icon, mod1Entry.IconType,
|
|
1082
|
-
mod1Entry.Route, mod1Entry.DisplayOrder);
|
|
1083
|
-
context.NavigationModules.Add(mod1);
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
// Repeat for each module (each with its own idempotence check)...
|
|
1087
|
-
await ((DbContext)context).SaveChangesAsync(ct);
|
|
1088
|
-
|
|
1089
|
-
// Resolve module entities for section/resource seeding (works for both new AND existing modules)
|
|
1090
|
-
var mod1Entity = await context.NavigationModules
|
|
1091
|
-
.FirstAsync(m => m.Code == "{module1Code}" && m.ApplicationId == app.Id, ct);
|
|
1092
|
-
// Repeat for each module...
|
|
1093
|
-
|
|
1094
|
-
// --- Module translations (idempotent — unique index IX_nav_Translations_EntityType_EntityId_LanguageCode) ---
|
|
1095
|
-
// CRITICAL: Always check existence before inserting translations to avoid duplicate key errors
|
|
1096
|
-
// on re-runs, partial failures, or DB reset scenarios.
|
|
1097
|
-
if (!await context.NavigationTranslations.AnyAsync(
|
|
1098
|
-
t => t.EntityId == {Module1Pascal}NavigationSeedData.{Module1Pascal}ModuleId
|
|
1099
|
-
&& t.EntityType == NavigationEntityType.Module, ct))
|
|
1100
|
-
{
|
|
1101
|
-
foreach (var t in {Module1Pascal}NavigationSeedData.GetTranslationEntries())
|
|
1102
|
-
{
|
|
1103
|
-
context.NavigationTranslations.Add(
|
|
1104
|
-
NavigationTranslation.Create(t.EntityType, t.EntityId, t.LanguageCode, t.Label, t.Description));
|
|
1105
|
-
}
|
|
1106
|
-
}
|
|
1107
|
-
// Repeat for each module...
|
|
1108
|
-
await ((DbContext)context).SaveChangesAsync(ct);
|
|
1109
|
-
|
|
1110
|
-
// --- Sections (idempotent — check each section before inserting) ---
|
|
1111
|
-
// Module 1 sections
|
|
1112
|
-
foreach (var secEntry in {Module1Pascal}NavigationSeedData.GetSectionEntries(mod1Entity.Id))
|
|
1113
|
-
{
|
|
1114
|
-
var secExists = await context.NavigationSections
|
|
1115
|
-
.AnyAsync(s => s.Code == secEntry.Code && s.ModuleId == mod1Entity.Id, ct);
|
|
1116
|
-
if (!secExists)
|
|
1117
|
-
{
|
|
1118
|
-
var sec = NavigationSection.Create(
|
|
1119
|
-
secEntry.ModuleId, secEntry.Code, secEntry.Label,
|
|
1120
|
-
secEntry.Description, secEntry.Icon, secEntry.IconType,
|
|
1121
|
-
secEntry.Route, secEntry.DisplayOrder);
|
|
1122
|
-
context.NavigationSections.Add(sec);
|
|
1123
|
-
}
|
|
1124
|
-
}
|
|
1125
|
-
// Repeat for each module that has sections...
|
|
1126
|
-
await ((DbContext)context).SaveChangesAsync(ct);
|
|
1127
|
-
|
|
1128
|
-
// --- Section translations (idempotent — check before inserting) ---
|
|
1129
|
-
foreach (var secEntry in {Module1Pascal}NavigationSeedData.GetSectionEntries(mod1Entity.Id))
|
|
1130
|
-
{
|
|
1131
|
-
if (!await context.NavigationTranslations.AnyAsync(
|
|
1132
|
-
t => t.EntityId == secEntry.Id && t.EntityType == NavigationEntityType.Section, ct))
|
|
1133
|
-
{
|
|
1134
|
-
foreach (var t in {Module1Pascal}NavigationSeedData.GetSectionTranslationEntries()
|
|
1135
|
-
.Where(st => st.EntityId == secEntry.Id))
|
|
1136
|
-
{
|
|
1137
|
-
context.NavigationTranslations.Add(
|
|
1138
|
-
NavigationTranslation.Create(t.EntityType, t.EntityId, t.LanguageCode, t.Label, t.Description));
|
|
1139
|
-
}
|
|
1140
|
-
}
|
|
1141
|
-
}
|
|
1142
|
-
// Repeat for each module that has sections...
|
|
1143
|
-
await ((DbContext)context).SaveChangesAsync(ct);
|
|
1144
|
-
|
|
1145
|
-
// --- Resources (idempotent — use ACTUAL section IDs from DB, not deterministic seed IDs) ---
|
|
1146
|
-
// CRITICAL: NavigationSection.Create() generates its own ID in DB.
|
|
1147
|
-
// The deterministic GUID from GetSectionEntries() is NOT the actual SectionId.
|
|
1148
|
-
// We MUST query the real section by Code+ModuleId to get the actual DB ID,
|
|
1149
|
-
// otherwise FK_nav_Resources_nav_Sections_SectionId will fail.
|
|
1150
|
-
foreach (var secEntry in {Module1Pascal}NavigationSeedData.GetSectionEntries(mod1Entity.Id))
|
|
1151
|
-
{
|
|
1152
|
-
var actualSection = await context.NavigationSections
|
|
1153
|
-
.FirstAsync(s => s.Code == secEntry.Code && s.ModuleId == mod1Entity.Id, ct);
|
|
1154
|
-
|
|
1155
|
-
foreach (var resEntry in {Module1Pascal}NavigationSeedData.GetResourceEntries(secEntry.Id))
|
|
1156
|
-
{
|
|
1157
|
-
var resExists = await context.NavigationResources
|
|
1158
|
-
.AnyAsync(r => r.Code == resEntry.Code && r.SectionId == actualSection.Id, ct);
|
|
1159
|
-
if (!resExists)
|
|
1160
|
-
{
|
|
1161
|
-
var res = NavigationResource.Create(
|
|
1162
|
-
actualSection.Id, resEntry.Code, resEntry.Label,
|
|
1163
|
-
resEntry.EntityType, resEntry.Route, resEntry.DisplayOrder);
|
|
1164
|
-
context.NavigationResources.Add(res);
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
}
|
|
1168
|
-
// Repeat for each module that has resources...
|
|
1169
|
-
await ((DbContext)context).SaveChangesAsync(ct);
|
|
1170
|
-
}
|
|
1171
|
-
|
|
1172
|
-
public async Task SeedRolesAsync(ICoreDbContext context, CancellationToken ct)
|
|
1173
|
-
{
|
|
1174
|
-
// Check idempotence — verify by Code (roles may already exist from SmartStack core)
|
|
1175
|
-
var existingRoleCodes = await context.Roles
|
|
1176
|
-
.Where(r => r.Code == "admin" || r.Code == "manager" || r.Code == "contributor" || r.Code == "viewer")
|
|
1177
|
-
.Select(r => r.Code)
|
|
1178
|
-
.ToListAsync(ct);
|
|
1179
|
-
|
|
1180
|
-
// Only create roles that don't already exist (SmartStack core may pre-seed system roles)
|
|
1181
|
-
foreach (var entry in ApplicationRolesSeedData.GetRoleEntries())
|
|
1182
|
-
{
|
|
1183
|
-
if (existingRoleCodes.Contains(entry.Code)) continue; // Skip if already exists
|
|
1184
|
-
|
|
1185
|
-
var role = Role.Create(
|
|
1186
|
-
entry.Code,
|
|
1187
|
-
entry.Name,
|
|
1188
|
-
entry.Description,
|
|
1189
|
-
entry.ApplicationId,
|
|
1190
|
-
entry.IsSystem);
|
|
1191
|
-
context.Roles.Add(role);
|
|
1192
|
-
}
|
|
1193
|
-
await ((DbContext)context).SaveChangesAsync(ct);
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
|
-
public async Task SeedPermissionsAsync(ICoreDbContext context, CancellationToken ct)
|
|
1197
|
-
{
|
|
1198
|
-
var exists = await context.Permissions
|
|
1199
|
-
.AnyAsync(p => p.Path == "{appCode}.*", ct);
|
|
1200
|
-
if (exists) return;
|
|
1201
|
-
|
|
1202
|
-
// Application-level wildcard
|
|
1203
|
-
var appWildcard = Permission.CreateWildcard(
|
|
1204
|
-
"{appCode}.*", PermissionLevel.Application,
|
|
1205
|
-
"Full {appLabel_en} access");
|
|
1206
|
-
context.Permissions.Add(appWildcard);
|
|
1207
|
-
|
|
1208
|
-
// Module permissions
|
|
1209
|
-
var mod1 = await context.NavigationModules
|
|
1210
|
-
.FirstAsync(m => m.Code == "{module1Code}", ct);
|
|
1211
|
-
foreach (var entry in {Module1Pascal}PermissionsSeedData.GetPermissionEntries(mod1.Id))
|
|
1212
|
-
{
|
|
1213
|
-
var perm = entry.IsWildcard
|
|
1214
|
-
? Permission.CreateWildcard(entry.Path, entry.Level, entry.Description)
|
|
1215
|
-
: Permission.CreateForModule(entry.Path, entry.Level, entry.Action, false, entry.ModuleId, entry.Description);
|
|
1216
|
-
context.Permissions.Add(perm);
|
|
1217
|
-
}
|
|
1218
|
-
|
|
1219
|
-
// Repeat for each module...
|
|
1220
|
-
await ((DbContext)context).SaveChangesAsync(ct);
|
|
1221
|
-
}
|
|
1222
|
-
|
|
1223
|
-
public async Task SeedRolePermissionsAsync(ICoreDbContext context, CancellationToken ct)
|
|
1224
|
-
{
|
|
1225
|
-
var exists = await context.RolePermissions
|
|
1226
|
-
.AnyAsync(rp => rp.Permission!.Path.StartsWith("{appCode}."), ct);
|
|
1227
|
-
if (exists) return;
|
|
1228
|
-
|
|
1229
|
-
// CRITICAL: Resolve roles by Code from DB — NEVER use deterministic GUIDs.
|
|
1230
|
-
// Application-scoped roles (admin, manager, contributor, viewer) are created by
|
|
1231
|
-
// SeedRolesAsync() above. System roles use their own IDs that do NOT match
|
|
1232
|
-
// DeterministicGuid("role:admin").
|
|
1233
|
-
var roles = await context.Roles
|
|
1234
|
-
.Where(r => r.ApplicationId != null || r.IsSystem)
|
|
1235
|
-
.ToListAsync(ct);
|
|
1236
|
-
|
|
1237
|
-
// Resolve permissions
|
|
1238
|
-
var permissions = await context.Permissions
|
|
1239
|
-
.Where(p => p.Path.StartsWith("{appCode}."))
|
|
1240
|
-
.ToListAsync(ct);
|
|
1241
|
-
|
|
1242
|
-
// Apply role-permission mappings from all modules
|
|
1243
|
-
var allMappings = new List<RolePermissionSeedEntry>();
|
|
1244
|
-
allMappings.AddRange({Module1Pascal}RolesSeedData.GetRolePermissionEntries());
|
|
1245
|
-
// allMappings.AddRange({Module2Pascal}RolesSeedData.GetRolePermissionEntries()); // per module
|
|
1246
|
-
|
|
1247
|
-
foreach (var mapping in allMappings)
|
|
1248
|
-
{
|
|
1249
|
-
var role = roles.FirstOrDefault(r => r.Code == mapping.RoleCode);
|
|
1250
|
-
var perm = permissions.FirstOrDefault(p => p.Path == mapping.PermissionPath);
|
|
1251
|
-
|
|
1252
|
-
if (role == null)
|
|
1253
|
-
{
|
|
1254
|
-
// CRITICAL: Role not found — SeedRolesAsync() may not have run.
|
|
1255
|
-
// This causes silent permission failure → 401 on protected pages.
|
|
1256
|
-
Console.WriteLine($"[SEED WARNING] Role '{mapping.RoleCode}' not found. Role-permission mapping skipped for '{mapping.PermissionPath}'.");
|
|
1257
|
-
continue;
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
if (perm == null)
|
|
1261
|
-
{
|
|
1262
|
-
Console.WriteLine($"[SEED WARNING] Permission '{mapping.PermissionPath}' not found. Role-permission mapping skipped for role '{mapping.RoleCode}'.");
|
|
1263
|
-
continue;
|
|
1264
|
-
}
|
|
1265
|
-
|
|
1266
|
-
context.RolePermissions.Add(RolePermission.Create(role.Id, perm.Id, "system"));
|
|
1267
|
-
}
|
|
1268
|
-
|
|
1269
|
-
await ((DbContext)context).SaveChangesAsync(ct);
|
|
1270
|
-
}
|
|
1271
|
-
}
|
|
1272
|
-
```
|
|
1273
|
-
|
|
1274
|
-
### DI Registration
|
|
1275
|
-
|
|
1276
|
-
```csharp
|
|
1277
|
-
// In Infrastructure/DependencyInjection.cs — add:
|
|
1278
|
-
using SmartStack.Application.Common.Interfaces.Seeding;
|
|
1279
|
-
|
|
1280
|
-
// In the registration method:
|
|
1281
|
-
services.AddScoped<IClientSeedDataProvider, {AppPascalName}SeedDataProvider>();
|
|
1282
|
-
```
|
|
1283
|
-
|
|
1284
|
-
---
|
|
1285
|
-
|
|
1286
|
-
## 7. Multi-Module Handling
|
|
1287
|
-
|
|
1288
|
-
When processing multiple modules in the same ralph-loop run:
|
|
1289
|
-
|
|
1290
|
-
### Module 1 (first): Creates everything from scratch
|
|
1291
|
-
|
|
1292
|
-
0. `NavigationApplicationSeedData.cs` (**application-level**, once per app — FIRST FILE, provides ApplicationId)
|
|
1293
|
-
1. `ApplicationRolesSeedData.cs` (application-level, once per app — references NavigationApplicationSeedData.ApplicationId)
|
|
1294
|
-
2. `{Module1}NavigationSeedData.cs` (module + sections + resources + all translations)
|
|
1295
|
-
3. `{Module1}PermissionsSeedData.cs`
|
|
1296
|
-
4. `{Module1}RolesSeedData.cs`
|
|
1297
|
-
5. `{AppPascalName}SeedDataProvider.cs` (new, with 4 methods — including section/resource seeding)
|
|
1298
|
-
6. DI registration (new)
|
|
1299
|
-
|
|
1300
|
-
### Module 2+ (subsequent): Append to existing provider
|
|
1301
|
-
|
|
1302
|
-
0. `NavigationApplicationSeedData.cs` (already exists — skip)
|
|
1303
|
-
1. `ApplicationRolesSeedData.cs` (already exists — skip)
|
|
1304
|
-
2. `{Module2}NavigationSeedData.cs` (new file — include sections + resources if defined in feature.json)
|
|
1305
|
-
3. `{Module2}PermissionsSeedData.cs` (new file)
|
|
1306
|
-
4. `{Module2}RolesSeedData.cs` (new file)
|
|
1307
|
-
5. `{AppPascalName}SeedDataProvider.cs` (**modify** — add using, add entries in Navigation/Permissions/RolePermissions methods, **including section/resource seeding for the new module**)
|
|
1308
|
-
6. DI registration (already exists — skip)
|
|
1309
|
-
|
|
1310
|
-
**Detection:** Check if `{AppPascalName}SeedDataProvider.cs` exists. If yes, READ it and ADD the new module's entries to the appropriate methods (Navigation, Permissions, RolePermissions). Do NOT modify SeedRolesAsync() or the Application creation in SeedNavigationAsync().
|
|
1311
|
-
|
|
1312
|
-
### Section/Resource Conditionality
|
|
1313
|
-
|
|
1314
|
-
Sections and resources are **optional per module**. When processing a module's feature.json:
|
|
1315
|
-
|
|
1316
|
-
| Condition | Action |
|
|
1317
|
-
|-----------|--------|
|
|
1318
|
-
| `seedDataCore.navigationSections` absent or empty | Skip `GetSectionEntries()` / `GetSectionTranslationEntries()` / section seeding in provider |
|
|
1319
|
-
| `seedDataCore.navigationResources` absent or empty | Skip `GetResourceEntries()` / resource seeding in provider |
|
|
1320
|
-
| Both present | Generate all section + resource methods and provider code |
|
|
1321
|
-
|
|
1322
|
-
---
|
|
1323
|
-
|
|
1324
|
-
## 8. Verification Checklist (BLOCKING)
|
|
1325
|
-
|
|
1326
|
-
Before marking the task as completed, verify ALL:
|
|
1327
|
-
|
|
1328
|
-
**Application-Level (FIRST — before modules):**
|
|
1329
|
-
- [ ] `NavigationApplicationSeedData.cs` created (once per application, at `Infrastructure/Persistence/Seeding/Data/`)
|
|
1330
|
-
- [ ] Application GUID is deterministic (SHA256 of `"navigation-application-{appCode}"`)
|
|
1331
|
-
- [ ] GetApplicationEntry() takes no parameters (no contextId)
|
|
1332
|
-
- [ ] Application translations created (4 languages: fr, en, it, de, EntityType = Application)
|
|
1333
|
-
- [ ] `IClientSeedDataProvider.SeedNavigationAsync()` uses `NavigationApplicationSeedData` (NO hardcoded `{appLabel_en}` / `{appIcon}` placeholders)
|
|
1334
|
-
- [ ] `ApplicationRolesSeedData.ApplicationId` references `NavigationApplicationSeedData.ApplicationId` (NO `{ApplicationGuid}` placeholder)
|
|
1335
|
-
|
|
1336
|
-
**Module-Level:**
|
|
1337
|
-
- [ ] Deterministic GUIDs (NEVER `Guid.NewGuid()`) — SHA256 of path
|
|
1338
|
-
- [ ] 4 languages for each navigation entity (fr, en, it, de)
|
|
1339
|
-
- [ ] `ApplicationRolesSeedData.cs` created (once per application)
|
|
1340
|
-
- [ ] 4 application roles defined: Admin, Manager, Contributor, Viewer
|
|
1341
|
-
- [ ] Each role has a valid `Code` value ("admin", "manager", "contributor", "viewer")
|
|
1342
|
-
- [ ] `Permissions.cs` constants match seed data paths
|
|
1343
|
-
- [ ] MCP `generate_permissions` called (or fallback used)
|
|
1344
|
-
- [ ] Role-permission mappings assigned (Admin, Manager, Contributor, Viewer)
|
|
1345
|
-
- [ ] **RolePermission mappings use Code-based lookup** — NEVER `DeterministicGuid("role:admin")` or `GenerateRoleGuid()`
|
|
1346
|
-
- [ ] **SeedRolesAsync checks existence by Code** — SmartStack core may pre-seed system roles
|
|
1347
|
-
- [ ] `IClientSeedDataProvider` generated with 4 methods (Navigation, Roles, Permissions, RolePermissions)
|
|
1348
|
-
- [ ] Execution order: Navigation (application → modules → sections → resources) → Roles → Permissions → RolePermissions
|
|
1349
|
-
- [ ] Each Seed method is idempotent (checks existence before inserting)
|
|
1350
|
-
- [ ] Factory methods used throughout (NEVER `new Entity()`)
|
|
1351
|
-
- [ ] `SaveChangesAsync` called per group (Navigation → Roles → Permissions → RolePermissions)
|
|
1352
|
-
- [ ] DI registration added: `services.AddScoped<IClientSeedDataProvider, ...>()`
|
|
1353
|
-
- [ ] NavigationSections seeded (if `seedDataCore.navigationSections` present in feature.json)
|
|
1354
|
-
- [ ] NavigationResources seeded (if `seedDataCore.navigationResources` present in feature.json)
|
|
1355
|
-
- [ ] Section/Resource translations created (4 languages each, EntityType = Section/Resource)
|
|
1356
|
-
- [ ] `dotnet build` passes after generation
|
|
1357
|
-
- [ ] NO `Enum.Parse<PermissionAction>` usage anywhere in seeding code (use typed enum directly)
|
|
1358
|
-
- [ ] ALL PermissionAction values are from the valid enum: Access, Read, Create, Update, Delete, Export, Import, Approve, Reject, Assign, Execute
|
|
1359
|
-
|
|
1360
|
-
**Seed Data Integrity (BLOCKING — run AFTER `dotnet build`):**
|
|
1361
|
-
- [ ] Application startup test: `dotnet run --urls http://localhost:0 --environment Development` exits without seed data exceptions
|
|
1362
|
-
- [ ] Verify `nav_Applications` has entry for `{appCode}` (query or startup log)
|
|
1363
|
-
- [ ] Verify `nav_Modules` has entries for each module (count matches feature.json modules)
|
|
1364
|
-
- [ ] Verify `auth_Roles` has 4 application-scoped roles (admin, manager, contributor, viewer)
|
|
1365
|
-
- [ ] Verify `auth_Permissions` has entries for each module (wildcard + CRUD)
|
|
1366
|
-
|
|
1367
|
-
**If ANY check fails, the task status = 'failed'.**
|
|
1368
|
-
|
|
1369
|
-
---
|
|
1370
|
-
|
|
1371
|
-
## 9. Business Seed Data (DevDataSeeder) — TenantId Rules
|
|
1372
|
-
|
|
1373
|
-
> **Applies to:** Seed data for business entities (reference types, categories, statuses).
|
|
1374
|
-
> **NOT the same as** core seed data above (navigation, permissions, roles).
|
|
1375
|
-
|
|
1376
|
-
### Rules
|
|
1377
|
-
|
|
1378
|
-
| Rule | Description |
|
|
1379
|
-
|------|-------------|
|
|
1380
|
-
| TenantId MANDATORY | ALL business seed entities MUST set `TenantId` |
|
|
1381
|
-
| Deterministic TenantId | Use `SeedConstants.DefaultTenantId` (NEVER inline GUID) |
|
|
1382
|
-
| DevDataSeeder pattern | Implement `IDevDataSeeder` with `SeedAsync()` method |
|
|
1383
|
-
| Idempotency | Each seeder MUST check `AnyAsync()` before inserting |
|
|
1384
|
-
| Order | DevDataSeeder `Order >= 200` (after core seed data at Order 100) |
|
|
1385
|
-
|
|
1386
|
-
### Template (Reference Types)
|
|
1387
|
-
|
|
1388
|
-
```csharp
|
|
1389
|
-
public class {Module}DevDataSeeder : IDevDataSeeder
|
|
1390
|
-
{
|
|
1391
|
-
public int Order => 200; // After core seed data (Order 100)
|
|
1392
|
-
|
|
1393
|
-
public async Task SeedAsync(ExtensionsDbContext context, CancellationToken ct)
|
|
1394
|
-
{
|
|
1395
|
-
if (await context.Set<{EntityType}>().AnyAsync(ct)) return;
|
|
1396
|
-
|
|
1397
|
-
var items = new[]
|
|
1398
|
-
{
|
|
1399
|
-
new {EntityType}
|
|
1400
|
-
{
|
|
1401
|
-
Id = GenerateDeterministicGuid("{entity-type}-{code}"),
|
|
1402
|
-
Code = "{code}",
|
|
1403
|
-
Name = "{name}",
|
|
1404
|
-
TenantId = SeedConstants.DefaultTenantId, // MANDATORY
|
|
1405
|
-
IsActive = true
|
|
1406
|
-
}
|
|
1407
|
-
};
|
|
1408
|
-
|
|
1409
|
-
context.Set<{EntityType}>().AddRange(items);
|
|
1410
|
-
await context.SaveChangesAsync(ct);
|
|
1411
|
-
}
|
|
1412
|
-
|
|
1413
|
-
private static Guid GenerateDeterministicGuid(string seed)
|
|
1414
|
-
{
|
|
1415
|
-
using var sha256 = System.Security.Cryptography.SHA256.Create();
|
|
1416
|
-
var hash = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(seed));
|
|
1417
|
-
return new Guid(hash.Take(16).ToArray());
|
|
1418
|
-
}
|
|
1419
|
-
}
|
|
1420
|
-
```
|
|
1421
|
-
|
|
1422
|
-
### FORBIDDEN
|
|
1423
|
-
|
|
1424
|
-
- Seeding business entities WITHOUT `TenantId`
|
|
1425
|
-
- Using `Guid.NewGuid()` for TenantId
|
|
1426
|
-
- Omitting idempotency check (`AnyAsync`)
|
|
1427
|
-
- Hardcoding TenantId inline (use `SeedConstants.DefaultTenantId`)
|
|
1428
|
-
|
|
1429
|
-
### CROSS-SCHEMA FK WARNING
|
|
1430
|
-
|
|
1431
|
-
> `SeedConstants.DefaultTenantId` MUST reference a tenant that EXISTS in `core.tenant_Tenants`.
|
|
1432
|
-
> The SmartStack platform seeds a default tenant during `InitializeSmartStackAsync()`.
|
|
1433
|
-
> If you use a custom GUID, ensure it is created BEFORE `DevDataSeeder` runs (Order >= 200).
|
|
1434
|
-
>
|
|
1435
|
-
> **Pipeline validation:**
|
|
1436
|
-
> - ralph-loop POST-CHECK warns if GUID not found in project config
|
|
1437
|
-
> - validate-feature step-05 verifies FK exists in real database via SQL query
|
|
1
|
+
# Core Seed Data - Execution Reference
|
|
2
|
+
|
|
3
|
+
> **Loaded by:** apex step-03-execute (delegate mode + seedData tasks) and step-04-examine
|
|
4
|
+
> **Condition:** Seed data generation — infrastructure or seedData category tasks
|
|
5
|
+
> **Applies to:** Client projects only (seeding_strategy = "provider", ExtensionsDbContext)
|
|
6
|
+
>
|
|
7
|
+
> **Source of truth:** `/application` skill `templates-seed.md` (lines 608-916)
|
|
8
|
+
> **Moved from:** `ralph-loop/references/core-seed-data.md` (delegation refactoring)
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 1. Parameter Extraction
|
|
13
|
+
|
|
14
|
+
Extract navigation hierarchy from the task's `_seedDataMeta` (populated by guardrail 1e):
|
|
15
|
+
|
|
16
|
+
```javascript
|
|
17
|
+
const task = currentTask;
|
|
18
|
+
const meta = task._seedDataMeta || task._providerMeta || {};
|
|
19
|
+
const coreSeedData = meta.coreSeedData || {};
|
|
20
|
+
|
|
21
|
+
// Navigation hierarchy
|
|
22
|
+
const navModules = coreSeedData.navigationModules || coreSeedData.navigation || [];
|
|
23
|
+
const navSections = coreSeedData.navigationSections || [];
|
|
24
|
+
const navResources = coreSeedData.navigationResources || [];
|
|
25
|
+
const permissions = coreSeedData.permissions || [];
|
|
26
|
+
const rolePerms = coreSeedData.rolePermissions || [];
|
|
27
|
+
|
|
28
|
+
// Derived context (from guardrail or PRD)
|
|
29
|
+
const navRoute = meta.navRoute; // e.g. "human-resources.projects"
|
|
30
|
+
const appCode = meta.appCode; // e.g. "human-resources"
|
|
31
|
+
const moduleCode = task.module; // e.g. "projects"
|
|
32
|
+
|
|
33
|
+
// If _seedDataMeta is absent, fallback to PRD source
|
|
34
|
+
if (!navRoute) {
|
|
35
|
+
const prd = readJSON('.ralph/prd.json');
|
|
36
|
+
const navRoute = `${prd.source?.application || prd.metadata?.module}.${task.module}`;
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**State variables after extraction:**
|
|
41
|
+
|
|
42
|
+
| Variable | Example | Source |
|
|
43
|
+
|----------|---------|--------|
|
|
44
|
+
| `navRoute` | `human-resources.projects` | `_seedDataMeta.navRoute` |
|
|
45
|
+
| `appCode` | `human-resources` | `_seedDataMeta.appCode` |
|
|
46
|
+
| `moduleCode` | `projects` | `task.module` |
|
|
47
|
+
| `navModules[]` | `[{code, label, icon, route, translations}]` | `coreSeedData.navigationModules` |
|
|
48
|
+
| `navSections[]` | `[{code, label, icon, route, parentCode, permission, sort}]` | `coreSeedData.navigationSections` |
|
|
49
|
+
| `navResources[]` | `[{code, type, entity, parentCode, permission}]` | `coreSeedData.navigationResources` |
|
|
50
|
+
| `permissions[]` | `[{path, action, description}]` | `coreSeedData.permissions` |
|
|
51
|
+
| `rolePerms[]` | `[{role, permissions[]}]` | `coreSeedData.rolePermissions` |
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## 1b. NavigationApplicationSeedData.cs (ONCE per application)
|
|
56
|
+
|
|
57
|
+
**File:** `Infrastructure/Persistence/Seeding/Data/NavigationApplicationSeedData.cs`
|
|
58
|
+
|
|
59
|
+
> **MANDATORY:** This file MUST be created BEFORE any module seed data.
|
|
60
|
+
> Without it, modules have no parent ApplicationId and ApplicationRolesSeedData has no GUID reference.
|
|
61
|
+
> This file is created **ONCE per application** (not per module).
|
|
62
|
+
|
|
63
|
+
### Data Source
|
|
64
|
+
|
|
65
|
+
From `seedDataCore.navigationApplications[0]` in feature.json (generated by BA step-05a):
|
|
66
|
+
|
|
67
|
+
| Placeholder | Source |
|
|
68
|
+
|-------------|--------|
|
|
69
|
+
| `{appCode}` | `navigationApplications[0].code` |
|
|
70
|
+
| `{appLabel_xx}` | `navigationApplications[0].labels.xx` (fr, en, it, de) |
|
|
71
|
+
| `{appDesc_xx}` | `navigationApplications[0].description.xx` |
|
|
72
|
+
| `{appIcon}` | `navigationApplications[0].icon` |
|
|
73
|
+
|
|
74
|
+
### GUID Generation Rule
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
```csharp
|
|
78
|
+
// Deterministic GUID for APPLICATION
|
|
79
|
+
public static readonly Guid ApplicationId =
|
|
80
|
+
GenerateDeterministicGuid("navigation-application-{appCode}");
|
|
81
|
+
// Example: GenerateDeterministicGuid("navigation-application-human-resources")
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Template
|
|
85
|
+
|
|
86
|
+
```csharp
|
|
87
|
+
using SmartStack.Domain.Navigation;
|
|
88
|
+
|
|
89
|
+
namespace {BaseNamespace}.Infrastructure.Persistence.Seeding.Data;
|
|
90
|
+
|
|
91
|
+
/// <summary>
|
|
92
|
+
/// Navigation seed data for {AppLabel_en} application.
|
|
93
|
+
/// Consumed by IClientSeedDataProvider at application startup.
|
|
94
|
+
/// Created ONCE per application — modules reference ApplicationId as parent.
|
|
95
|
+
/// </summary>
|
|
96
|
+
public static class NavigationApplicationSeedData
|
|
97
|
+
{
|
|
98
|
+
// Deterministic GUID for this application
|
|
99
|
+
public static readonly Guid ApplicationId =
|
|
100
|
+
GenerateDeterministicGuid("navigation-application-{appCode}");
|
|
101
|
+
|
|
102
|
+
/// <summary>
|
|
103
|
+
/// Returns navigation application entry for seeding into core.nav_Applications.
|
|
104
|
+
/// </summary>
|
|
105
|
+
public static NavigationApplicationSeedEntry GetApplicationEntry()
|
|
106
|
+
{
|
|
107
|
+
return new NavigationApplicationSeedEntry
|
|
108
|
+
{
|
|
109
|
+
Id = ApplicationId,
|
|
110
|
+
Code = "{appCode}",
|
|
111
|
+
Label = "{appLabel_en}",
|
|
112
|
+
Description = "{appDesc_en}",
|
|
113
|
+
Icon = "{appIcon}", // Lucide React icon name
|
|
114
|
+
IconType = IconType.Lucide,
|
|
115
|
+
Route = ToKebabCase("/{appCode}"),
|
|
116
|
+
DisplayOrder = 1,
|
|
117
|
+
IsActive = true
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// <summary>
|
|
122
|
+
/// Returns 4-language translations for this application.
|
|
123
|
+
/// </summary>
|
|
124
|
+
public static IEnumerable<NavigationTranslationSeedEntry> GetTranslationEntries()
|
|
125
|
+
{
|
|
126
|
+
var appId = ApplicationId;
|
|
127
|
+
return new[]
|
|
128
|
+
{
|
|
129
|
+
new NavigationTranslationSeedEntry
|
|
130
|
+
{
|
|
131
|
+
EntityType = NavigationEntityType.Application,
|
|
132
|
+
EntityId = appId,
|
|
133
|
+
LanguageCode = "fr",
|
|
134
|
+
Label = "{appLabel_fr}",
|
|
135
|
+
Description = "{appDesc_fr}"
|
|
136
|
+
},
|
|
137
|
+
new NavigationTranslationSeedEntry
|
|
138
|
+
{
|
|
139
|
+
EntityType = NavigationEntityType.Application,
|
|
140
|
+
EntityId = appId,
|
|
141
|
+
LanguageCode = "en",
|
|
142
|
+
Label = "{appLabel_en}",
|
|
143
|
+
Description = "{appDesc_en}"
|
|
144
|
+
},
|
|
145
|
+
new NavigationTranslationSeedEntry
|
|
146
|
+
{
|
|
147
|
+
EntityType = NavigationEntityType.Application,
|
|
148
|
+
EntityId = appId,
|
|
149
|
+
LanguageCode = "it",
|
|
150
|
+
Label = "{appLabel_it}",
|
|
151
|
+
Description = "{appDesc_it}"
|
|
152
|
+
},
|
|
153
|
+
new NavigationTranslationSeedEntry
|
|
154
|
+
{
|
|
155
|
+
EntityType = NavigationEntityType.Application,
|
|
156
|
+
EntityId = appId,
|
|
157
|
+
LanguageCode = "de",
|
|
158
|
+
Label = "{appLabel_de}",
|
|
159
|
+
Description = "{appDesc_de}"
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private static Guid GenerateDeterministicGuid(string seed)
|
|
165
|
+
{
|
|
166
|
+
using var sha256 = System.Security.Cryptography.SHA256.Create();
|
|
167
|
+
var hash = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(seed));
|
|
168
|
+
return new Guid(hash.Take(16).ToArray());
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/// <summary>
|
|
172
|
+
/// Converts PascalCase route segments to kebab-case for web URLs.
|
|
173
|
+
/// </summary>
|
|
174
|
+
private static string ToKebabCase(string route)
|
|
175
|
+
{
|
|
176
|
+
if (string.IsNullOrEmpty(route)) return route;
|
|
177
|
+
|
|
178
|
+
var segments = route.Split('/');
|
|
179
|
+
var kebabSegments = new List<string>();
|
|
180
|
+
|
|
181
|
+
foreach (var segment in segments)
|
|
182
|
+
{
|
|
183
|
+
if (string.IsNullOrEmpty(segment))
|
|
184
|
+
{
|
|
185
|
+
kebabSegments.Add(segment);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
var kebab = System.Text.RegularExpressions.Regex
|
|
190
|
+
.Replace(segment, "([a-z])([A-Z])", "$1-$2")
|
|
191
|
+
.ToLowerInvariant();
|
|
192
|
+
kebabSegments.Add(kebab);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return string.Join("/", kebabSegments);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/// <summary>Seed entry DTO for navigation application.</summary>
|
|
200
|
+
public class NavigationApplicationSeedEntry
|
|
201
|
+
{
|
|
202
|
+
public Guid Id { get; init; }
|
|
203
|
+
public string Code { get; init; } = null!;
|
|
204
|
+
public string Label { get; init; } = null!;
|
|
205
|
+
public string? Description { get; init; }
|
|
206
|
+
public string? Icon { get; init; }
|
|
207
|
+
public IconType IconType { get; init; }
|
|
208
|
+
public string? Route { get; init; }
|
|
209
|
+
public int DisplayOrder { get; init; }
|
|
210
|
+
public bool IsActive { get; init; }
|
|
211
|
+
}
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
**Replace placeholders** with values from `seedDataCore.navigationApplications[0]`.
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## 2. NavigationModuleSeedData.cs
|
|
219
|
+
|
|
220
|
+
**File:** `Infrastructure/Persistence/Seeding/Data/{ModulePascal}/NavigationModuleSeedData.cs`
|
|
221
|
+
|
|
222
|
+
### GUID Generation Rule
|
|
223
|
+
|
|
224
|
+
```csharp
|
|
225
|
+
// NEVER use Guid.NewGuid() — ALWAYS deterministic
|
|
226
|
+
private static Guid GenerateDeterministicGuid(string seed)
|
|
227
|
+
{
|
|
228
|
+
using var sha256 = System.Security.Cryptography.SHA256.Create();
|
|
229
|
+
var hash = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(seed));
|
|
230
|
+
return new Guid(hash.Take(16).ToArray());
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Usage: GUIDs are derived from the navigation path
|
|
234
|
+
public static readonly Guid ModuleId = GenerateDeterministicGuid("navigation-module-{navRoute}");
|
|
235
|
+
// Example: GenerateDeterministicGuid("navigation-module-human-resources.projects")
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Template
|
|
239
|
+
|
|
240
|
+
```csharp
|
|
241
|
+
using SmartStack.Domain.Navigation;
|
|
242
|
+
|
|
243
|
+
namespace {BaseNamespace}.Infrastructure.Persistence.Seeding.Data.{ModulePascal};
|
|
244
|
+
|
|
245
|
+
/// <summary>
|
|
246
|
+
/// Navigation seed data for {ModuleLabel} module.
|
|
247
|
+
/// Consumed by IClientSeedDataProvider at application startup.
|
|
248
|
+
/// </summary>
|
|
249
|
+
public static class {ModulePascal}NavigationSeedData
|
|
250
|
+
{
|
|
251
|
+
// Deterministic GUID for this module
|
|
252
|
+
public static readonly Guid {ModulePascal}ModuleId =
|
|
253
|
+
GenerateDeterministicGuid("navigation-module-{navRoute}");
|
|
254
|
+
|
|
255
|
+
/// <summary>
|
|
256
|
+
/// Returns navigation module entry for seeding into core.nav_Modules.
|
|
257
|
+
/// </summary>
|
|
258
|
+
public static NavigationModuleSeedEntry GetModuleEntry(Guid applicationId)
|
|
259
|
+
{
|
|
260
|
+
return new NavigationModuleSeedEntry
|
|
261
|
+
{
|
|
262
|
+
Id = {ModulePascal}ModuleId,
|
|
263
|
+
ApplicationId = applicationId,
|
|
264
|
+
Code = "{moduleCode}",
|
|
265
|
+
Label = "{label_en}",
|
|
266
|
+
Description = "{desc_en}",
|
|
267
|
+
Icon = "{icon}", // Lucide React icon name
|
|
268
|
+
IconType = IconType.Lucide,
|
|
269
|
+
Route = ToKebabCase($"/{appCode}/{moduleCode}"),
|
|
270
|
+
DisplayOrder = {displayOrder},
|
|
271
|
+
IsActive = true
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/// <summary>
|
|
276
|
+
/// Returns 4-language translations for this module.
|
|
277
|
+
/// </summary>
|
|
278
|
+
public static IEnumerable<NavigationTranslationSeedEntry> GetTranslationEntries()
|
|
279
|
+
{
|
|
280
|
+
var moduleId = {ModulePascal}ModuleId;
|
|
281
|
+
return new[]
|
|
282
|
+
{
|
|
283
|
+
new NavigationTranslationSeedEntry
|
|
284
|
+
{
|
|
285
|
+
EntityType = NavigationEntityType.Module,
|
|
286
|
+
EntityId = moduleId,
|
|
287
|
+
LanguageCode = "fr",
|
|
288
|
+
Label = "{label_fr}",
|
|
289
|
+
Description = "{desc_fr}"
|
|
290
|
+
},
|
|
291
|
+
new NavigationTranslationSeedEntry
|
|
292
|
+
{
|
|
293
|
+
EntityType = NavigationEntityType.Module,
|
|
294
|
+
EntityId = moduleId,
|
|
295
|
+
LanguageCode = "en",
|
|
296
|
+
Label = "{label_en}",
|
|
297
|
+
Description = "{desc_en}"
|
|
298
|
+
},
|
|
299
|
+
new NavigationTranslationSeedEntry
|
|
300
|
+
{
|
|
301
|
+
EntityType = NavigationEntityType.Module,
|
|
302
|
+
EntityId = moduleId,
|
|
303
|
+
LanguageCode = "it",
|
|
304
|
+
Label = "{label_it}",
|
|
305
|
+
Description = "{desc_it}"
|
|
306
|
+
},
|
|
307
|
+
new NavigationTranslationSeedEntry
|
|
308
|
+
{
|
|
309
|
+
EntityType = NavigationEntityType.Module,
|
|
310
|
+
EntityId = moduleId,
|
|
311
|
+
LanguageCode = "de",
|
|
312
|
+
Label = "{label_de}",
|
|
313
|
+
Description = "{desc_de}"
|
|
314
|
+
}
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
private static Guid GenerateDeterministicGuid(string seed)
|
|
319
|
+
{
|
|
320
|
+
using var sha256 = System.Security.Cryptography.SHA256.Create();
|
|
321
|
+
var hash = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(seed));
|
|
322
|
+
return new Guid(hash.Take(16).ToArray());
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/// <summary>
|
|
326
|
+
/// Converts PascalCase route segments to kebab-case for web URLs.
|
|
327
|
+
/// Example: /HumanResources/TimeManagement → /human-resources/time-management
|
|
328
|
+
/// </summary>
|
|
329
|
+
private static string ToKebabCase(string route)
|
|
330
|
+
{
|
|
331
|
+
if (string.IsNullOrEmpty(route)) return route;
|
|
332
|
+
|
|
333
|
+
var segments = route.Split('/');
|
|
334
|
+
var kebabSegments = new List<string>();
|
|
335
|
+
|
|
336
|
+
foreach (var segment in segments)
|
|
337
|
+
{
|
|
338
|
+
if (string.IsNullOrEmpty(segment))
|
|
339
|
+
{
|
|
340
|
+
kebabSegments.Add(segment);
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Convert PascalCase to kebab-case: HumanResources → human-resources
|
|
345
|
+
var kebab = System.Text.RegularExpressions.Regex
|
|
346
|
+
.Replace(segment, "([a-z])([A-Z])", "$1-$2")
|
|
347
|
+
.ToLowerInvariant();
|
|
348
|
+
kebabSegments.Add(kebab);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return string.Join("/", kebabSegments);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/// <summary>Seed entry DTO for navigation module.</summary>
|
|
356
|
+
public class NavigationModuleSeedEntry
|
|
357
|
+
{
|
|
358
|
+
public Guid Id { get; init; }
|
|
359
|
+
public Guid ApplicationId { get; init; }
|
|
360
|
+
public string Code { get; init; } = null!;
|
|
361
|
+
public string Label { get; init; } = null!;
|
|
362
|
+
public string Description { get; init; } = null!;
|
|
363
|
+
public string Icon { get; init; } = null!;
|
|
364
|
+
public IconType IconType { get; init; }
|
|
365
|
+
public string Route { get; init; } = null!;
|
|
366
|
+
public int DisplayOrder { get; init; }
|
|
367
|
+
public bool IsActive { get; init; }
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/// <summary>Seed entry DTO for navigation translation.</summary>
|
|
371
|
+
public class NavigationTranslationSeedEntry
|
|
372
|
+
{
|
|
373
|
+
public NavigationEntityType EntityType { get; init; }
|
|
374
|
+
public Guid EntityId { get; init; }
|
|
375
|
+
public string LanguageCode { get; init; } = null!;
|
|
376
|
+
public string Label { get; init; } = null!;
|
|
377
|
+
public string Description { get; init; } = null!;
|
|
378
|
+
}
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
**Replace placeholders** with values from `navModules[]` and PRD `project` metadata.
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
## 2b. Navigation Sections & Resources (in same NavigationSeedData.cs)
|
|
386
|
+
|
|
387
|
+
> **CONDITIONAL:** Only generate if `seedDataCore.navigationSections[]` exists and is non-empty in feature.json.
|
|
388
|
+
> Sections and resources are added as additional methods in the **same** `{ModulePascal}NavigationSeedData.cs` file.
|
|
389
|
+
|
|
390
|
+
### GUID Generation Rules
|
|
391
|
+
|
|
392
|
+
```csharp
|
|
393
|
+
// Section GUID: deterministic from navRoute + section code
|
|
394
|
+
public static readonly Guid {SectionPascal}SectionId =
|
|
395
|
+
GenerateDeterministicGuid("navigation-section-{navRoute}.{sectionCode}");
|
|
396
|
+
// Example: GenerateDeterministicGuid("navigation-section-human-resources.employees.list")
|
|
397
|
+
|
|
398
|
+
// Resource GUID: deterministic from navRoute + section code + resource code
|
|
399
|
+
public static readonly Guid {ResourcePascal}ResourceId =
|
|
400
|
+
GenerateDeterministicGuid("navigation-resource-{navRoute}.{sectionCode}.{resourceCode}");
|
|
401
|
+
// Example: GenerateDeterministicGuid("navigation-resource-human-resources.employees.list.employees-grid")
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### Section Methods (add to {ModulePascal}NavigationSeedData.cs)
|
|
405
|
+
|
|
406
|
+
> **ROUTE SPECIAL CASES (list and detail):**
|
|
407
|
+
> The `list` and `detail` sections are view modes of the module, NOT functional sub-areas.
|
|
408
|
+
> - `list` section route = module route (e.g., `/human-resources/employees`) — NO `/list` suffix
|
|
409
|
+
> - `detail` section route = module route + `/:id` (e.g., `/human-resources/employees/:id`) — NOT `/detail/:id`
|
|
410
|
+
> - FORBIDDEN: `/{module}/list`, `/{module}/detail/:id`
|
|
411
|
+
> - Other sections (dashboard, approve, import) = module route + `/{section-kebab}` (normal)
|
|
412
|
+
|
|
413
|
+
```csharp
|
|
414
|
+
// --- Add AFTER GetTranslationEntries() in {ModulePascal}NavigationSeedData.cs ---
|
|
415
|
+
|
|
416
|
+
// Deterministic GUIDs for sections
|
|
417
|
+
public static readonly Guid {Section1Pascal}SectionId =
|
|
418
|
+
GenerateDeterministicGuid("navigation-section-{navRoute}.{section1Code}");
|
|
419
|
+
// Repeat for each section...
|
|
420
|
+
|
|
421
|
+
/// <summary>
|
|
422
|
+
/// Returns navigation section entries for seeding into core.nav_Sections.
|
|
423
|
+
/// </summary>
|
|
424
|
+
public static IEnumerable<NavigationSectionSeedEntry> GetSectionEntries(Guid moduleId)
|
|
425
|
+
{
|
|
426
|
+
return new[]
|
|
427
|
+
{
|
|
428
|
+
new NavigationSectionSeedEntry
|
|
429
|
+
{
|
|
430
|
+
Id = {Section1Pascal}SectionId,
|
|
431
|
+
ModuleId = moduleId,
|
|
432
|
+
Code = "{section1Code}",
|
|
433
|
+
Label = "{section1_label_en}",
|
|
434
|
+
Description = "{section1_desc_en}",
|
|
435
|
+
Icon = "{section1_icon}",
|
|
436
|
+
IconType = IconType.Lucide,
|
|
437
|
+
// ROUTE CONVENTION:
|
|
438
|
+
// - "list" section → same as module route (no extra segment)
|
|
439
|
+
// - "detail" section → module route + "/:id"
|
|
440
|
+
// - Other sections → module route + "/{section-kebab}"
|
|
441
|
+
// FORBIDDEN: "/employees/list", "/employees/detail/:id"
|
|
442
|
+
Route = "{section_route}", // From seedDataCore.navigationSections[].route
|
|
443
|
+
DisplayOrder = {section1_sort},
|
|
444
|
+
IsActive = true
|
|
445
|
+
}
|
|
446
|
+
// Repeat for each section...
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/// <summary>
|
|
451
|
+
/// Returns 4-language translations for sections.
|
|
452
|
+
/// </summary>
|
|
453
|
+
public static IEnumerable<NavigationTranslationSeedEntry> GetSectionTranslationEntries()
|
|
454
|
+
{
|
|
455
|
+
var entries = new List<NavigationTranslationSeedEntry>();
|
|
456
|
+
|
|
457
|
+
// Section: {section1Code}
|
|
458
|
+
var sec1Id = {Section1Pascal}SectionId;
|
|
459
|
+
entries.AddRange(new[]
|
|
460
|
+
{
|
|
461
|
+
new NavigationTranslationSeedEntry
|
|
462
|
+
{
|
|
463
|
+
EntityType = NavigationEntityType.Section,
|
|
464
|
+
EntityId = sec1Id,
|
|
465
|
+
LanguageCode = "fr",
|
|
466
|
+
Label = "{section1_label_fr}",
|
|
467
|
+
Description = "{section1_desc_fr}"
|
|
468
|
+
},
|
|
469
|
+
new NavigationTranslationSeedEntry
|
|
470
|
+
{
|
|
471
|
+
EntityType = NavigationEntityType.Section,
|
|
472
|
+
EntityId = sec1Id,
|
|
473
|
+
LanguageCode = "en",
|
|
474
|
+
Label = "{section1_label_en}",
|
|
475
|
+
Description = "{section1_desc_en}"
|
|
476
|
+
},
|
|
477
|
+
new NavigationTranslationSeedEntry
|
|
478
|
+
{
|
|
479
|
+
EntityType = NavigationEntityType.Section,
|
|
480
|
+
EntityId = sec1Id,
|
|
481
|
+
LanguageCode = "it",
|
|
482
|
+
Label = "{section1_label_it}",
|
|
483
|
+
Description = "{section1_desc_it}"
|
|
484
|
+
},
|
|
485
|
+
new NavigationTranslationSeedEntry
|
|
486
|
+
{
|
|
487
|
+
EntityType = NavigationEntityType.Section,
|
|
488
|
+
EntityId = sec1Id,
|
|
489
|
+
LanguageCode = "de",
|
|
490
|
+
Label = "{section1_label_de}",
|
|
491
|
+
Description = "{section1_desc_de}"
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
// Repeat for each section...
|
|
495
|
+
|
|
496
|
+
return entries;
|
|
497
|
+
}
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
### Resource Methods (add to {ModulePascal}NavigationSeedData.cs)
|
|
501
|
+
|
|
502
|
+
> **CONDITIONAL:** Only generate if `seedDataCore.navigationResources[]` exists and is non-empty.
|
|
503
|
+
|
|
504
|
+
```csharp
|
|
505
|
+
// --- Add AFTER GetSectionTranslationEntries() ---
|
|
506
|
+
|
|
507
|
+
// Deterministic GUIDs for resources
|
|
508
|
+
public static readonly Guid {Resource1Pascal}ResourceId =
|
|
509
|
+
GenerateDeterministicGuid("navigation-resource-{navRoute}.{parentSectionCode}.{resource1Code}");
|
|
510
|
+
// Repeat for each resource...
|
|
511
|
+
|
|
512
|
+
/// <summary>
|
|
513
|
+
/// Returns navigation resource entries for a given section.
|
|
514
|
+
/// </summary>
|
|
515
|
+
public static IEnumerable<NavigationResourceSeedEntry> GetResourceEntries(Guid sectionId)
|
|
516
|
+
{
|
|
517
|
+
var entries = new List<NavigationResourceSeedEntry>();
|
|
518
|
+
|
|
519
|
+
// Resources for section: {section1Code}
|
|
520
|
+
if (sectionId == {Section1Pascal}SectionId)
|
|
521
|
+
{
|
|
522
|
+
entries.AddRange(new[]
|
|
523
|
+
{
|
|
524
|
+
// RESOURCE ROUTE CONVENTION:
|
|
525
|
+
// Resources inherit their parent section's resolved route as base:
|
|
526
|
+
// - Under "list" section → base = module route (no /list)
|
|
527
|
+
// - Under "detail" section → base = module route (no /detail, resource routes don't include /:id)
|
|
528
|
+
// - Under other sections → base = module route + /{section-kebab}
|
|
529
|
+
// Then append: /{resource-kebab}
|
|
530
|
+
//
|
|
531
|
+
// Example: resource "export" under section "dashboard":
|
|
532
|
+
// Route = /human-resources/employees/dashboard/export
|
|
533
|
+
// Example: resource "employees-grid" under section "list":
|
|
534
|
+
// Route = /human-resources/employees/employees-grid (NOT /employees/list/employees-grid)
|
|
535
|
+
new NavigationResourceSeedEntry
|
|
536
|
+
{
|
|
537
|
+
Id = {Resource1Pascal}ResourceId,
|
|
538
|
+
SectionId = sectionId,
|
|
539
|
+
Code = "{resource1Code}",
|
|
540
|
+
Label = "{resource1_label_en}",
|
|
541
|
+
EntityType = "{resource1_entity}",
|
|
542
|
+
// Use parent section's resolved route + /{resource-kebab}
|
|
543
|
+
// For "list"/"detail" sections, the section route = module route (no /list or /detail segment)
|
|
544
|
+
Route = "{resource_route}", // From seedDataCore: parent section route + /{resource-kebab}
|
|
545
|
+
DisplayOrder = 1
|
|
546
|
+
}
|
|
547
|
+
// Repeat for each resource in this section...
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
// Repeat if-block for each section with resources...
|
|
551
|
+
|
|
552
|
+
return entries;
|
|
553
|
+
}
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
### Additional DTO Classes (add to bottom of file)
|
|
557
|
+
|
|
558
|
+
```csharp
|
|
559
|
+
/// <summary>Seed entry DTO for navigation section.</summary>
|
|
560
|
+
public class NavigationSectionSeedEntry
|
|
561
|
+
{
|
|
562
|
+
public Guid Id { get; init; }
|
|
563
|
+
public Guid ModuleId { get; init; }
|
|
564
|
+
public string Code { get; init; } = null!;
|
|
565
|
+
public string Label { get; init; } = null!;
|
|
566
|
+
public string Description { get; init; } = null!;
|
|
567
|
+
public string Icon { get; init; } = null!;
|
|
568
|
+
public IconType IconType { get; init; }
|
|
569
|
+
public string Route { get; init; } = null!;
|
|
570
|
+
public int DisplayOrder { get; init; }
|
|
571
|
+
public bool IsActive { get; init; }
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/// <summary>Seed entry DTO for navigation resource.</summary>
|
|
575
|
+
public class NavigationResourceSeedEntry
|
|
576
|
+
{
|
|
577
|
+
public Guid Id { get; init; }
|
|
578
|
+
public Guid SectionId { get; init; }
|
|
579
|
+
public string Code { get; init; } = null!;
|
|
580
|
+
public string Label { get; init; } = null!;
|
|
581
|
+
public string? EntityType { get; init; }
|
|
582
|
+
public string? Route { get; init; }
|
|
583
|
+
public int DisplayOrder { get; init; }
|
|
584
|
+
}
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
### Placeholder Values Source
|
|
588
|
+
|
|
589
|
+
| Placeholder | Source in feature.json |
|
|
590
|
+
|-------------|----------------------|
|
|
591
|
+
| `{sectionCode}` | `seedDataCore.navigationSections[].code` |
|
|
592
|
+
| `{section_label_xx}` | `specification.navigation.entries[]` where `level == "section"` → `labels.xx` |
|
|
593
|
+
| `{section_icon}` | `seedDataCore.navigationSections[].icon` |
|
|
594
|
+
| `{section_sort}` | `seedDataCore.navigationSections[].sort` |
|
|
595
|
+
| `{section_route}` | `seedDataCore.navigationSections[].route` — **SPECIAL CASES:** `list` → module route (no `/list`), `detail` → module route + `/:id` (no `/detail/:id`), others → module route + `/{section-kebab}` |
|
|
596
|
+
| `{resourceCode}` | `seedDataCore.navigationResources[].code` |
|
|
597
|
+
| `{resource_entity}` | `seedDataCore.navigationResources[].entity` |
|
|
598
|
+
| `{resource_route}` | Computed from parent section route + `/{resource-kebab}`. **SPECIAL CASES:** if parent section is `list` → module route + `/{resource-kebab}` (no `/list/`), if parent is `detail` → module route + `/{resource-kebab}` (no `/detail/`). |
|
|
599
|
+
| `{parentSectionCode}` | `seedDataCore.navigationResources[].parentCode` |
|
|
600
|
+
|
|
601
|
+
---
|
|
602
|
+
|
|
603
|
+
## 3. PermissionsSeedData.cs — MCP-First
|
|
604
|
+
|
|
605
|
+
### CRITICAL: PermissionAction Safety Rules
|
|
606
|
+
|
|
607
|
+
> **NEVER use `Enum.Parse<PermissionAction>("...")` — this causes runtime crashes if the string is invalid.**
|
|
608
|
+
> The error only manifests at application startup, not at compile time.
|
|
609
|
+
|
|
610
|
+
**Valid PermissionAction values (from SmartStack.Domain.Authorization):**
|
|
611
|
+
|
|
612
|
+
| Enum Value | Int | Description |
|
|
613
|
+
|------------|-----|-------------|
|
|
614
|
+
| `PermissionAction.Access` | 0 | Wildcard permissions only (IsWildcard = true) |
|
|
615
|
+
| `PermissionAction.Read` | 1 | GET/HEAD — View data |
|
|
616
|
+
| `PermissionAction.Create` | 2 | POST — Create new records |
|
|
617
|
+
| `PermissionAction.Update` | 3 | PUT/PATCH — Modify existing records |
|
|
618
|
+
| `PermissionAction.Delete` | 4 | DELETE — Remove records |
|
|
619
|
+
| `PermissionAction.Export` | 5 | Export data (CSV, Excel, etc.) |
|
|
620
|
+
| `PermissionAction.Import` | 6 | Import data |
|
|
621
|
+
| `PermissionAction.Approve` | 7 | Approve workflow items |
|
|
622
|
+
| `PermissionAction.Reject` | 8 | Reject workflow items |
|
|
623
|
+
| `PermissionAction.Assign` | 9 | Assign items to users |
|
|
624
|
+
| `PermissionAction.Execute` | 10 | Execute actions (sync, run, etc.) |
|
|
625
|
+
|
|
626
|
+
**Anti-patterns (FORBIDDEN):**
|
|
627
|
+
|
|
628
|
+
```csharp
|
|
629
|
+
// FORBIDDEN — Runtime crash if string is not a valid enum value
|
|
630
|
+
Enum.Parse<PermissionAction>("Validate"); // ArgumentException at startup
|
|
631
|
+
(PermissionAction)Enum.Parse(typeof(PermissionAction), "Validate"); // Same crash
|
|
632
|
+
|
|
633
|
+
// FORBIDDEN — String-based action in anonymous objects
|
|
634
|
+
new { Action = "read" }; // Not type-safe, silent mismatch possible
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
**Correct patterns (MANDATORY):**
|
|
638
|
+
|
|
639
|
+
```csharp
|
|
640
|
+
// ALWAYS use the typed enum directly — compile-time safe
|
|
641
|
+
Action = PermissionAction.Read // Compile-time checked
|
|
642
|
+
Action = PermissionAction.Create // Compile-time checked
|
|
643
|
+
Action = PermissionAction.Approve // Compile-time checked
|
|
644
|
+
|
|
645
|
+
// For custom actions beyond standard CRUD, pick from the enum:
|
|
646
|
+
// Export, Import, Approve, Reject, Assign, Execute
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
**MCP validation:** `validate_conventions` with `checks: ["permissions"]` will detect and flag these anti-patterns.
|
|
650
|
+
|
|
651
|
+
### Step A: Call MCP (PRIMARY)
|
|
652
|
+
|
|
653
|
+
```
|
|
654
|
+
Tool: mcp__smartstack__generate_permissions
|
|
655
|
+
Args:
|
|
656
|
+
navRoute: "{navRoute}"
|
|
657
|
+
includeStandardActions: true
|
|
658
|
+
includeWildcard: true
|
|
659
|
+
```
|
|
660
|
+
|
|
661
|
+
MCP returns:
|
|
662
|
+
- `Permissions.cs` nested class (Application layer constants)
|
|
663
|
+
- Permission seed entries with deterministic GUIDs
|
|
664
|
+
|
|
665
|
+
### Step B: Write Permissions.cs (Application layer)
|
|
666
|
+
|
|
667
|
+
> **CRITICAL — Permission paths use the SAME kebab-case as NavRoute codes.**
|
|
668
|
+
> `{navRoute}` is already kebab-case (e.g., `human-resources.employees`).
|
|
669
|
+
> NEVER strip hyphens or derive codes from C# class names.
|
|
670
|
+
> FORBIDDEN: `humanresources.employees.read` → CORRECT: `human-resources.employees.read`
|
|
671
|
+
> SmartStack.app reference: `support-client.my-tickets.read`
|
|
672
|
+
|
|
673
|
+
```csharp
|
|
674
|
+
// Add to Application/Common/Authorization/Permissions.cs
|
|
675
|
+
// IMPORTANT: {navRoute} uses kebab-case segments (e.g., "human-resources.employees")
|
|
676
|
+
// Do NOT derive permission codes from C# identifiers — use navRoute directly
|
|
677
|
+
public static class {AppPascal}
|
|
678
|
+
{
|
|
679
|
+
public static class {ModulePascal}
|
|
680
|
+
{
|
|
681
|
+
public const string View = "{navRoute}.read"; // e.g., "human-resources.employees.read"
|
|
682
|
+
public const string Create = "{navRoute}.create";
|
|
683
|
+
public const string Update = "{navRoute}.update";
|
|
684
|
+
public const string Delete = "{navRoute}.delete";
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
### Step C: Write PermissionsSeedData.cs (Infrastructure layer)
|
|
690
|
+
|
|
691
|
+
**File:** `Infrastructure/Persistence/Seeding/Data/{ModulePascal}/PermissionsSeedData.cs`
|
|
692
|
+
|
|
693
|
+
```csharp
|
|
694
|
+
namespace {BaseNamespace}.Infrastructure.Persistence.Seeding.Data.{ModulePascal};
|
|
695
|
+
|
|
696
|
+
/// <summary>
|
|
697
|
+
/// Permission seed data for {ModuleLabel} module.
|
|
698
|
+
/// Consumed by IClientSeedDataProvider at application startup.
|
|
699
|
+
/// </summary>
|
|
700
|
+
public static class {ModulePascal}PermissionsSeedData
|
|
701
|
+
{
|
|
702
|
+
// Deterministic GUIDs for permissions
|
|
703
|
+
public static readonly Guid WildcardPermId = GenerateGuid("{navRoute}.*");
|
|
704
|
+
public static readonly Guid ReadPermId = GenerateGuid("{navRoute}.read");
|
|
705
|
+
public static readonly Guid CreatePermId = GenerateGuid("{navRoute}.create");
|
|
706
|
+
public static readonly Guid UpdatePermId = GenerateGuid("{navRoute}.update");
|
|
707
|
+
public static readonly Guid DeletePermId = GenerateGuid("{navRoute}.delete");
|
|
708
|
+
|
|
709
|
+
public static IEnumerable<PermissionSeedEntry> GetPermissionEntries(Guid moduleId)
|
|
710
|
+
{
|
|
711
|
+
return new[]
|
|
712
|
+
{
|
|
713
|
+
new PermissionSeedEntry { Id = WildcardPermId, Path = "{navRoute}.*", Level = PermissionLevel.Module, Action = PermissionAction.Access, IsWildcard = true, ModuleId = moduleId, Description = "Full {moduleLabel} access" },
|
|
714
|
+
new PermissionSeedEntry { Id = ReadPermId, Path = "{navRoute}.read", Level = PermissionLevel.Module, Action = PermissionAction.Read, IsWildcard = false, ModuleId = moduleId, Description = "View {moduleLabel}" },
|
|
715
|
+
new PermissionSeedEntry { Id = CreatePermId, Path = "{navRoute}.create", Level = PermissionLevel.Module, Action = PermissionAction.Create, IsWildcard = false, ModuleId = moduleId, Description = "Create {moduleLabel}" },
|
|
716
|
+
new PermissionSeedEntry { Id = UpdatePermId, Path = "{navRoute}.update", Level = PermissionLevel.Module, Action = PermissionAction.Update, IsWildcard = false, ModuleId = moduleId, Description = "Update {moduleLabel}" },
|
|
717
|
+
new PermissionSeedEntry { Id = DeletePermId, Path = "{navRoute}.delete", Level = PermissionLevel.Module, Action = PermissionAction.Delete, IsWildcard = false, ModuleId = moduleId, Description = "Delete {moduleLabel}" }
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
private static Guid GenerateGuid(string path)
|
|
722
|
+
{
|
|
723
|
+
using var sha256 = System.Security.Cryptography.SHA256.Create();
|
|
724
|
+
var hash = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes($"permission-{path}"));
|
|
725
|
+
return new Guid(hash.Take(16).ToArray());
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
public class PermissionSeedEntry
|
|
730
|
+
{
|
|
731
|
+
public Guid Id { get; init; }
|
|
732
|
+
public string Path { get; init; } = null!;
|
|
733
|
+
public PermissionLevel Level { get; init; }
|
|
734
|
+
public PermissionAction Action { get; init; }
|
|
735
|
+
public bool IsWildcard { get; init; }
|
|
736
|
+
public Guid ModuleId { get; init; }
|
|
737
|
+
public string Description { get; init; } = null!;
|
|
738
|
+
}
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
### Step C2: Section-Level Permissions (CONDITIONAL: only if `navSections[]` defined)
|
|
742
|
+
|
|
743
|
+
> When `seedDataCore.navigationSections` exists and is non-empty in feature.json,
|
|
744
|
+
> add section-level permission GUIDs and entries to `PermissionsSeedData.cs`.
|
|
745
|
+
|
|
746
|
+
```csharp
|
|
747
|
+
// --- Add to {ModulePascal}PermissionsSeedData class AFTER module-level permissions ---
|
|
748
|
+
|
|
749
|
+
// Section-level permissions (for each section in navSections[])
|
|
750
|
+
public static readonly Guid {SectionPascal}WildcardPermId = GenerateGuid("{navRoute}.{sectionCode}.*");
|
|
751
|
+
public static readonly Guid {SectionPascal}ReadPermId = GenerateGuid("{navRoute}.{sectionCode}.read");
|
|
752
|
+
public static readonly Guid {SectionPascal}CreatePermId = GenerateGuid("{navRoute}.{sectionCode}.create");
|
|
753
|
+
public static readonly Guid {SectionPascal}UpdatePermId = GenerateGuid("{navRoute}.{sectionCode}.update");
|
|
754
|
+
public static readonly Guid {SectionPascal}DeletePermId = GenerateGuid("{navRoute}.{sectionCode}.delete");
|
|
755
|
+
// Repeat for each section...
|
|
756
|
+
|
|
757
|
+
// Add to GetPermissionEntries() — AFTER module-level entries:
|
|
758
|
+
// Section: {sectionCode}
|
|
759
|
+
new PermissionSeedEntry { Id = {SectionPascal}WildcardPermId, Path = "{navRoute}.{sectionCode}.*", Level = PermissionLevel.Section, Action = PermissionAction.Access, IsWildcard = true, ModuleId = moduleId, Description = "Full {sectionLabel} access" },
|
|
760
|
+
new PermissionSeedEntry { Id = {SectionPascal}ReadPermId, Path = "{navRoute}.{sectionCode}.read", Level = PermissionLevel.Section, Action = PermissionAction.Read, IsWildcard = false, ModuleId = moduleId, Description = "View {sectionLabel}" },
|
|
761
|
+
new PermissionSeedEntry { Id = {SectionPascal}CreatePermId, Path = "{navRoute}.{sectionCode}.create", Level = PermissionLevel.Section, Action = PermissionAction.Create, IsWildcard = false, ModuleId = moduleId, Description = "Create {sectionLabel}" },
|
|
762
|
+
new PermissionSeedEntry { Id = {SectionPascal}UpdatePermId, Path = "{navRoute}.{sectionCode}.update", Level = PermissionLevel.Section, Action = PermissionAction.Update, IsWildcard = false, ModuleId = moduleId, Description = "Update {sectionLabel}" },
|
|
763
|
+
new PermissionSeedEntry { Id = {SectionPascal}DeletePermId, Path = "{navRoute}.{sectionCode}.delete", Level = PermissionLevel.Section, Action = PermissionAction.Delete, IsWildcard = false, ModuleId = moduleId, Description = "Delete {sectionLabel}" },
|
|
764
|
+
// Repeat for each section...
|
|
765
|
+
```
|
|
766
|
+
|
|
767
|
+
Also add section-level constants to `Permissions.cs` (Application layer):
|
|
768
|
+
|
|
769
|
+
```csharp
|
|
770
|
+
public static class {ModulePascal}
|
|
771
|
+
{
|
|
772
|
+
// ... existing module-level permissions ...
|
|
773
|
+
|
|
774
|
+
// Section-level (for each section in navSections[])
|
|
775
|
+
public static class {SectionPascal}
|
|
776
|
+
{
|
|
777
|
+
public const string View = "{navRoute}.{sectionCode}.read";
|
|
778
|
+
public const string Create = "{navRoute}.{sectionCode}.create";
|
|
779
|
+
public const string Update = "{navRoute}.{sectionCode}.update";
|
|
780
|
+
public const string Delete = "{navRoute}.{sectionCode}.delete";
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
```
|
|
784
|
+
|
|
785
|
+
### Step D: MCP Fallback
|
|
786
|
+
|
|
787
|
+
If MCP `generate_permissions` fails, use the template above directly with values derived from the PRD `coreSeedData.permissions[]`.
|
|
788
|
+
|
|
789
|
+
---
|
|
790
|
+
|
|
791
|
+
## 4. ApplicationRolesSeedData.cs (Application-Level, Once per Application)
|
|
792
|
+
|
|
793
|
+
**File:** `Infrastructure/Persistence/Seeding/Data/ApplicationRolesSeedData.cs`
|
|
794
|
+
|
|
795
|
+
### Purpose
|
|
796
|
+
|
|
797
|
+
Creates the 4 standard application-scoped roles: Admin, Manager, Contributor, Viewer.
|
|
798
|
+
|
|
799
|
+
> **CRITICAL — SmartStack core MAY already provide system roles (admin, manager, contributor, viewer).**
|
|
800
|
+
> If system roles already exist in `auth_Roles`, do NOT create duplicates.
|
|
801
|
+
> `SeedRolesAsync()` MUST check existence by Code, not just by ApplicationId.
|
|
802
|
+
> **For RolePermission mappings:** ALWAYS look up roles by Code at runtime (in `SeedRolePermissionsAsync()`).
|
|
803
|
+
> **FORBIDDEN:** Using `GenerateRoleGuid()`, `DeterministicGuid("role:admin")`, or any hardcoded role GUID
|
|
804
|
+
> when creating RolePermission entries. The role GUIDs may differ from what's in the database.
|
|
805
|
+
|
|
806
|
+
**CRITICAL:** This file is created **ONCE per application** (not per module).
|
|
807
|
+
|
|
808
|
+
### GUID Generation Rule
|
|
809
|
+
|
|
810
|
+
> **WARNING:** These deterministic GUIDs are ONLY used for role creation (if roles don't already exist).
|
|
811
|
+
> They MUST NEVER be used for RolePermission mapping — always look up roles by Code at runtime.
|
|
812
|
+
|
|
813
|
+
```csharp
|
|
814
|
+
// Deterministic GUID from application ID + role type
|
|
815
|
+
// WARNING: Only for role creation. NEVER use these GUIDs for RolePermission mapping.
|
|
816
|
+
// RolePermissions MUST resolve roles by Code at runtime (see SeedRolePermissionsAsync).
|
|
817
|
+
private static Guid GenerateRoleGuid(string roleType)
|
|
818
|
+
{
|
|
819
|
+
using var sha256 = System.Security.Cryptography.SHA256.Create();
|
|
820
|
+
var hash = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes($"role-{ApplicationId}-{roleType}"));
|
|
821
|
+
return new Guid(hash.Take(16).ToArray());
|
|
822
|
+
}
|
|
823
|
+
// FORBIDDEN in RolePermission mapping:
|
|
824
|
+
// var roleId = GenerateRoleGuid("admin"); // WRONG — GUID may not match DB
|
|
825
|
+
// var roleId = DeterministicGuid("role:admin"); // WRONG — use Code lookup instead
|
|
826
|
+
```
|
|
827
|
+
|
|
828
|
+
### Template
|
|
829
|
+
|
|
830
|
+
```csharp
|
|
831
|
+
using SmartStack.Domain.Platform.Administration.Roles;
|
|
832
|
+
|
|
833
|
+
namespace {BaseNamespace}.Infrastructure.Persistence.Seeding.Data;
|
|
834
|
+
|
|
835
|
+
/// <summary>
|
|
836
|
+
/// Application-scoped role seed data for {AppLabel}.
|
|
837
|
+
/// Defines the 4 standard application roles: Admin, Manager, Contributor, Viewer.
|
|
838
|
+
/// Consumed by IClientSeedDataProvider at application startup.
|
|
839
|
+
/// </summary>
|
|
840
|
+
public static class ApplicationRolesSeedData
|
|
841
|
+
{
|
|
842
|
+
// Application ID from NavigationApplicationSeedData (deterministic GUID)
|
|
843
|
+
public static readonly Guid ApplicationId = NavigationApplicationSeedData.ApplicationId;
|
|
844
|
+
|
|
845
|
+
public static readonly Guid AdminRoleId = GenerateRoleGuid("admin");
|
|
846
|
+
public static readonly Guid ManagerRoleId = GenerateRoleGuid("manager");
|
|
847
|
+
public static readonly Guid ContributorRoleId = GenerateRoleGuid("contributor");
|
|
848
|
+
public static readonly Guid ViewerRoleId = GenerateRoleGuid("viewer");
|
|
849
|
+
|
|
850
|
+
public static IEnumerable<ApplicationRoleSeedEntry> GetRoleEntries()
|
|
851
|
+
{
|
|
852
|
+
yield return new ApplicationRoleSeedEntry
|
|
853
|
+
{
|
|
854
|
+
Id = AdminRoleId,
|
|
855
|
+
Code = "admin",
|
|
856
|
+
Name = "{AppLabel} Admin",
|
|
857
|
+
Description = "Full administrative access to {AppLabel}",
|
|
858
|
+
ApplicationId = ApplicationId,
|
|
859
|
+
IsSystem = false,
|
|
860
|
+
IsActive = true,
|
|
861
|
+
DisplayOrder = 1
|
|
862
|
+
};
|
|
863
|
+
|
|
864
|
+
yield return new ApplicationRoleSeedEntry
|
|
865
|
+
{
|
|
866
|
+
Id = ManagerRoleId,
|
|
867
|
+
Code = "manager",
|
|
868
|
+
Name = "{AppLabel} Manager",
|
|
869
|
+
Description = "Management access to {AppLabel} (Create, Read, Update)",
|
|
870
|
+
ApplicationId = ApplicationId,
|
|
871
|
+
IsSystem = false,
|
|
872
|
+
IsActive = true,
|
|
873
|
+
DisplayOrder = 2
|
|
874
|
+
};
|
|
875
|
+
|
|
876
|
+
yield return new ApplicationRoleSeedEntry
|
|
877
|
+
{
|
|
878
|
+
Id = ContributorRoleId,
|
|
879
|
+
Code = "contributor",
|
|
880
|
+
Name = "{AppLabel} Contributor",
|
|
881
|
+
Description = "Contributor access to {AppLabel} (Create, Read)",
|
|
882
|
+
ApplicationId = ApplicationId,
|
|
883
|
+
IsSystem = false,
|
|
884
|
+
IsActive = true,
|
|
885
|
+
DisplayOrder = 3
|
|
886
|
+
};
|
|
887
|
+
|
|
888
|
+
yield return new ApplicationRoleSeedEntry
|
|
889
|
+
{
|
|
890
|
+
Id = ViewerRoleId,
|
|
891
|
+
Code = "viewer",
|
|
892
|
+
Name = "{AppLabel} Viewer",
|
|
893
|
+
Description = "Read-only access to {AppLabel}",
|
|
894
|
+
ApplicationId = ApplicationId,
|
|
895
|
+
IsSystem = false,
|
|
896
|
+
IsActive = true,
|
|
897
|
+
DisplayOrder = 4
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
private static Guid GenerateRoleGuid(string roleType)
|
|
902
|
+
{
|
|
903
|
+
using var sha256 = System.Security.Cryptography.SHA256.Create();
|
|
904
|
+
var hash = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes($"role-{ApplicationId}-{roleType}"));
|
|
905
|
+
return new Guid(hash.Take(16).ToArray());
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
public class ApplicationRoleSeedEntry
|
|
910
|
+
{
|
|
911
|
+
public Guid Id { get; init; }
|
|
912
|
+
public string Code { get; init; } = null!;
|
|
913
|
+
public string Name { get; init; } = null!;
|
|
914
|
+
public string Description { get; init; } = null!;
|
|
915
|
+
public Guid ApplicationId { get; init; }
|
|
916
|
+
public bool IsSystem { get; init; }
|
|
917
|
+
public bool IsActive { get; init; }
|
|
918
|
+
public int DisplayOrder { get; init; }
|
|
919
|
+
}
|
|
920
|
+
```
|
|
921
|
+
|
|
922
|
+
**Replace placeholders** with values from PRD and navigation metadata.
|
|
923
|
+
|
|
924
|
+
---
|
|
925
|
+
|
|
926
|
+
## 5. {Module}RolesSeedData.cs (Per Module)
|
|
927
|
+
|
|
928
|
+
**File:** `Infrastructure/Persistence/Seeding/Data/{ModulePascal}/RolesSeedData.cs`
|
|
929
|
+
|
|
930
|
+
> **CRITICAL:** This file uses `RoleCode` (string), NOT role GUIDs.
|
|
931
|
+
> Roles are resolved by Code at runtime in `SeedRolePermissionsAsync()`.
|
|
932
|
+
> **FORBIDDEN:** `DeterministicGuid("role:admin")`, `GenerateRoleGuid("admin")`, or any hardcoded Guid for roles.
|
|
933
|
+
> SmartStack core pre-seeds system roles — their IDs are NOT deterministic from the client perspective.
|
|
934
|
+
|
|
935
|
+
### Context-Based Role Mapping
|
|
936
|
+
|
|
937
|
+
| Application | Admin | Manager | Contributor | Viewer |
|
|
938
|
+
|-------------|-------|---------|-------------|--------|
|
|
939
|
+
| Any | CRUD | CRU | CR | R |
|
|
940
|
+
|
|
941
|
+
### Template
|
|
942
|
+
|
|
943
|
+
```csharp
|
|
944
|
+
namespace {BaseNamespace}.Infrastructure.Persistence.Seeding.Data.{ModulePascal};
|
|
945
|
+
|
|
946
|
+
/// <summary>
|
|
947
|
+
/// Role-permission mapping seed data for {ModuleLabel} module.
|
|
948
|
+
/// Maps permissions to application-scoped roles (Admin, Manager, Contributor, Viewer).
|
|
949
|
+
/// Consumed by IClientSeedDataProvider at application startup.
|
|
950
|
+
/// </summary>
|
|
951
|
+
public static class {ModulePascal}RolesSeedData
|
|
952
|
+
{
|
|
953
|
+
/// <summary>
|
|
954
|
+
/// Returns role-permission mappings for this module.
|
|
955
|
+
/// Roles are resolved at runtime by Code (not hardcoded GUIDs).
|
|
956
|
+
/// </summary>
|
|
957
|
+
public static IEnumerable<RolePermissionSeedEntry> GetRolePermissionEntries()
|
|
958
|
+
{
|
|
959
|
+
// Admin: wildcard access
|
|
960
|
+
yield return new RolePermissionSeedEntry { RoleCode = "admin", PermissionPath = "{navRoute}.*" };
|
|
961
|
+
|
|
962
|
+
// Manager: CRU (read + create + update — no delete)
|
|
963
|
+
yield return new RolePermissionSeedEntry { RoleCode = "manager", PermissionPath = "{navRoute}.read" };
|
|
964
|
+
yield return new RolePermissionSeedEntry { RoleCode = "manager", PermissionPath = "{navRoute}.create" };
|
|
965
|
+
yield return new RolePermissionSeedEntry { RoleCode = "manager", PermissionPath = "{navRoute}.update" };
|
|
966
|
+
|
|
967
|
+
// Contributor: CR
|
|
968
|
+
yield return new RolePermissionSeedEntry { RoleCode = "contributor", PermissionPath = "{navRoute}.read" };
|
|
969
|
+
yield return new RolePermissionSeedEntry { RoleCode = "contributor", PermissionPath = "{navRoute}.create" };
|
|
970
|
+
|
|
971
|
+
// Viewer: R
|
|
972
|
+
yield return new RolePermissionSeedEntry { RoleCode = "viewer", PermissionPath = "{navRoute}.read" };
|
|
973
|
+
|
|
974
|
+
// --- Section-level role mappings (CONDITIONAL: for each section in navSections[]) ---
|
|
975
|
+
// Admin: wildcard per section
|
|
976
|
+
yield return new RolePermissionSeedEntry { RoleCode = "admin", PermissionPath = "{navRoute}.{sectionCode}.*" };
|
|
977
|
+
|
|
978
|
+
// Manager: CRU per section (read + create + update — no delete)
|
|
979
|
+
yield return new RolePermissionSeedEntry { RoleCode = "manager", PermissionPath = "{navRoute}.{sectionCode}.read" };
|
|
980
|
+
yield return new RolePermissionSeedEntry { RoleCode = "manager", PermissionPath = "{navRoute}.{sectionCode}.create" };
|
|
981
|
+
yield return new RolePermissionSeedEntry { RoleCode = "manager", PermissionPath = "{navRoute}.{sectionCode}.update" };
|
|
982
|
+
|
|
983
|
+
// Contributor: CR per section
|
|
984
|
+
yield return new RolePermissionSeedEntry { RoleCode = "contributor", PermissionPath = "{navRoute}.{sectionCode}.read" };
|
|
985
|
+
yield return new RolePermissionSeedEntry { RoleCode = "contributor", PermissionPath = "{navRoute}.{sectionCode}.create" };
|
|
986
|
+
|
|
987
|
+
// Viewer: R per section
|
|
988
|
+
yield return new RolePermissionSeedEntry { RoleCode = "viewer", PermissionPath = "{navRoute}.{sectionCode}.read" };
|
|
989
|
+
// Repeat block for each section in navSections[]...
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
public class RolePermissionSeedEntry
|
|
994
|
+
{
|
|
995
|
+
public string RoleCode { get; init; } = null!;
|
|
996
|
+
public string PermissionPath { get; init; } = null!;
|
|
997
|
+
}
|
|
998
|
+
```
|
|
999
|
+
|
|
1000
|
+
---
|
|
1001
|
+
|
|
1002
|
+
## 6. IClientSeedDataProvider Implementation
|
|
1003
|
+
|
|
1004
|
+
**File:** `Infrastructure/Persistence/Seeding/{AppPascalName}SeedDataProvider.cs`
|
|
1005
|
+
|
|
1006
|
+
### Critical Rules
|
|
1007
|
+
|
|
1008
|
+
| Rule | Description |
|
|
1009
|
+
|------|-------------|
|
|
1010
|
+
| Factory methods | `NavigationModule.Create(...)`, `Role.Create(...)`, `Permission.CreateForModule(...)`, `RolePermission.Create(...)` — NEVER `new Entity()` |
|
|
1011
|
+
| Idempotence | Each Seed method checks existence before inserting |
|
|
1012
|
+
| Execution order | Navigation → Roles → Permissions → RolePermissions (roles MUST exist before mapping) |
|
|
1013
|
+
| SaveChanges per group | Navigation -> save -> Roles -> save -> Permissions -> save -> RolePermissions -> save |
|
|
1014
|
+
| FK resolution by Code | Parent entities (modules, roles) found by `Code`, not hardcoded GUID |
|
|
1015
|
+
| DI registration | `services.AddScoped<IClientSeedDataProvider, {AppPascalName}SeedDataProvider>()` |
|
|
1016
|
+
|
|
1017
|
+
### Template
|
|
1018
|
+
|
|
1019
|
+
```csharp
|
|
1020
|
+
using Microsoft.EntityFrameworkCore;
|
|
1021
|
+
using SmartStack.Application.Common.Interfaces;
|
|
1022
|
+
using SmartStack.Application.Common.Interfaces.Seeding;
|
|
1023
|
+
using SmartStack.Domain.Navigation;
|
|
1024
|
+
using SmartStack.Domain.Platform.Administration.Roles;
|
|
1025
|
+
using {BaseNamespace}.Infrastructure.Persistence.Seeding.Data;
|
|
1026
|
+
using {BaseNamespace}.Infrastructure.Persistence.Seeding.Data.{Module1Pascal};
|
|
1027
|
+
// using {BaseNamespace}.Infrastructure.Persistence.Seeding.Data.{Module2Pascal}; // Add per module
|
|
1028
|
+
|
|
1029
|
+
namespace {BaseNamespace}.Infrastructure.Persistence.Seeding;
|
|
1030
|
+
|
|
1031
|
+
/// <summary>
|
|
1032
|
+
/// Seeds {AppLabel} navigation, roles, permissions, and role-permission data
|
|
1033
|
+
/// into the SmartStack Core schema at application startup.
|
|
1034
|
+
/// </summary>
|
|
1035
|
+
public class {AppPascalName}SeedDataProvider : IClientSeedDataProvider
|
|
1036
|
+
{
|
|
1037
|
+
public int Order => 100;
|
|
1038
|
+
|
|
1039
|
+
public async Task SeedNavigationAsync(ICoreDbContext context, CancellationToken ct)
|
|
1040
|
+
{
|
|
1041
|
+
// --- Application (from NavigationApplicationSeedData) ---
|
|
1042
|
+
// NOTE: Idempotence is at MODULE level (not application level).
|
|
1043
|
+
// If the application already exists, we load it and continue to seed any missing modules.
|
|
1044
|
+
// This allows adding Module 2+ to an existing application without re-running the full seed.
|
|
1045
|
+
var appEntry = NavigationApplicationSeedData.GetApplicationEntry();
|
|
1046
|
+
var existingApp = await context.NavigationApplications
|
|
1047
|
+
.FirstOrDefaultAsync(a => a.Code == appEntry.Code, ct);
|
|
1048
|
+
|
|
1049
|
+
NavigationApplication app;
|
|
1050
|
+
if (existingApp != null)
|
|
1051
|
+
{
|
|
1052
|
+
app = existingApp; // Application already seeded — reuse it for module seeding below
|
|
1053
|
+
}
|
|
1054
|
+
else
|
|
1055
|
+
{
|
|
1056
|
+
app = NavigationApplication.Create(
|
|
1057
|
+
appEntry.Code, appEntry.Label,
|
|
1058
|
+
appEntry.Description, appEntry.Icon, appEntry.IconType,
|
|
1059
|
+
appEntry.Route, appEntry.DisplayOrder);
|
|
1060
|
+
context.NavigationApplications.Add(app);
|
|
1061
|
+
await ((DbContext)context).SaveChangesAsync(ct);
|
|
1062
|
+
|
|
1063
|
+
// --- Application translations (4 languages, from NavigationApplicationSeedData) ---
|
|
1064
|
+
foreach (var t in NavigationApplicationSeedData.GetTranslationEntries())
|
|
1065
|
+
{
|
|
1066
|
+
context.NavigationTranslations.Add(
|
|
1067
|
+
NavigationTranslation.Create(t.EntityType, t.EntityId, t.LanguageCode, t.Label, t.Description));
|
|
1068
|
+
}
|
|
1069
|
+
await ((DbContext)context).SaveChangesAsync(ct);
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// --- Modules (idempotent per-module — allows adding Module 2+ later) ---
|
|
1073
|
+
// Module: {Module1}
|
|
1074
|
+
var mod1Exists = await context.NavigationModules
|
|
1075
|
+
.AnyAsync(m => m.Code == "{module1Code}" && m.ApplicationId == app.Id, ct);
|
|
1076
|
+
if (!mod1Exists)
|
|
1077
|
+
{
|
|
1078
|
+
var mod1Entry = {Module1Pascal}NavigationSeedData.GetModuleEntry(app.Id);
|
|
1079
|
+
var mod1 = NavigationModule.Create(
|
|
1080
|
+
mod1Entry.ApplicationId, mod1Entry.Code, mod1Entry.Label,
|
|
1081
|
+
mod1Entry.Description, mod1Entry.Icon, mod1Entry.IconType,
|
|
1082
|
+
mod1Entry.Route, mod1Entry.DisplayOrder);
|
|
1083
|
+
context.NavigationModules.Add(mod1);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// Repeat for each module (each with its own idempotence check)...
|
|
1087
|
+
await ((DbContext)context).SaveChangesAsync(ct);
|
|
1088
|
+
|
|
1089
|
+
// Resolve module entities for section/resource seeding (works for both new AND existing modules)
|
|
1090
|
+
var mod1Entity = await context.NavigationModules
|
|
1091
|
+
.FirstAsync(m => m.Code == "{module1Code}" && m.ApplicationId == app.Id, ct);
|
|
1092
|
+
// Repeat for each module...
|
|
1093
|
+
|
|
1094
|
+
// --- Module translations (idempotent — unique index IX_nav_Translations_EntityType_EntityId_LanguageCode) ---
|
|
1095
|
+
// CRITICAL: Always check existence before inserting translations to avoid duplicate key errors
|
|
1096
|
+
// on re-runs, partial failures, or DB reset scenarios.
|
|
1097
|
+
if (!await context.NavigationTranslations.AnyAsync(
|
|
1098
|
+
t => t.EntityId == {Module1Pascal}NavigationSeedData.{Module1Pascal}ModuleId
|
|
1099
|
+
&& t.EntityType == NavigationEntityType.Module, ct))
|
|
1100
|
+
{
|
|
1101
|
+
foreach (var t in {Module1Pascal}NavigationSeedData.GetTranslationEntries())
|
|
1102
|
+
{
|
|
1103
|
+
context.NavigationTranslations.Add(
|
|
1104
|
+
NavigationTranslation.Create(t.EntityType, t.EntityId, t.LanguageCode, t.Label, t.Description));
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
// Repeat for each module...
|
|
1108
|
+
await ((DbContext)context).SaveChangesAsync(ct);
|
|
1109
|
+
|
|
1110
|
+
// --- Sections (idempotent — check each section before inserting) ---
|
|
1111
|
+
// Module 1 sections
|
|
1112
|
+
foreach (var secEntry in {Module1Pascal}NavigationSeedData.GetSectionEntries(mod1Entity.Id))
|
|
1113
|
+
{
|
|
1114
|
+
var secExists = await context.NavigationSections
|
|
1115
|
+
.AnyAsync(s => s.Code == secEntry.Code && s.ModuleId == mod1Entity.Id, ct);
|
|
1116
|
+
if (!secExists)
|
|
1117
|
+
{
|
|
1118
|
+
var sec = NavigationSection.Create(
|
|
1119
|
+
secEntry.ModuleId, secEntry.Code, secEntry.Label,
|
|
1120
|
+
secEntry.Description, secEntry.Icon, secEntry.IconType,
|
|
1121
|
+
secEntry.Route, secEntry.DisplayOrder);
|
|
1122
|
+
context.NavigationSections.Add(sec);
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
// Repeat for each module that has sections...
|
|
1126
|
+
await ((DbContext)context).SaveChangesAsync(ct);
|
|
1127
|
+
|
|
1128
|
+
// --- Section translations (idempotent — check before inserting) ---
|
|
1129
|
+
foreach (var secEntry in {Module1Pascal}NavigationSeedData.GetSectionEntries(mod1Entity.Id))
|
|
1130
|
+
{
|
|
1131
|
+
if (!await context.NavigationTranslations.AnyAsync(
|
|
1132
|
+
t => t.EntityId == secEntry.Id && t.EntityType == NavigationEntityType.Section, ct))
|
|
1133
|
+
{
|
|
1134
|
+
foreach (var t in {Module1Pascal}NavigationSeedData.GetSectionTranslationEntries()
|
|
1135
|
+
.Where(st => st.EntityId == secEntry.Id))
|
|
1136
|
+
{
|
|
1137
|
+
context.NavigationTranslations.Add(
|
|
1138
|
+
NavigationTranslation.Create(t.EntityType, t.EntityId, t.LanguageCode, t.Label, t.Description));
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
// Repeat for each module that has sections...
|
|
1143
|
+
await ((DbContext)context).SaveChangesAsync(ct);
|
|
1144
|
+
|
|
1145
|
+
// --- Resources (idempotent — use ACTUAL section IDs from DB, not deterministic seed IDs) ---
|
|
1146
|
+
// CRITICAL: NavigationSection.Create() generates its own ID in DB.
|
|
1147
|
+
// The deterministic GUID from GetSectionEntries() is NOT the actual SectionId.
|
|
1148
|
+
// We MUST query the real section by Code+ModuleId to get the actual DB ID,
|
|
1149
|
+
// otherwise FK_nav_Resources_nav_Sections_SectionId will fail.
|
|
1150
|
+
foreach (var secEntry in {Module1Pascal}NavigationSeedData.GetSectionEntries(mod1Entity.Id))
|
|
1151
|
+
{
|
|
1152
|
+
var actualSection = await context.NavigationSections
|
|
1153
|
+
.FirstAsync(s => s.Code == secEntry.Code && s.ModuleId == mod1Entity.Id, ct);
|
|
1154
|
+
|
|
1155
|
+
foreach (var resEntry in {Module1Pascal}NavigationSeedData.GetResourceEntries(secEntry.Id))
|
|
1156
|
+
{
|
|
1157
|
+
var resExists = await context.NavigationResources
|
|
1158
|
+
.AnyAsync(r => r.Code == resEntry.Code && r.SectionId == actualSection.Id, ct);
|
|
1159
|
+
if (!resExists)
|
|
1160
|
+
{
|
|
1161
|
+
var res = NavigationResource.Create(
|
|
1162
|
+
actualSection.Id, resEntry.Code, resEntry.Label,
|
|
1163
|
+
resEntry.EntityType, resEntry.Route, resEntry.DisplayOrder);
|
|
1164
|
+
context.NavigationResources.Add(res);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
// Repeat for each module that has resources...
|
|
1169
|
+
await ((DbContext)context).SaveChangesAsync(ct);
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
public async Task SeedRolesAsync(ICoreDbContext context, CancellationToken ct)
|
|
1173
|
+
{
|
|
1174
|
+
// Check idempotence — verify by Code (roles may already exist from SmartStack core)
|
|
1175
|
+
var existingRoleCodes = await context.Roles
|
|
1176
|
+
.Where(r => r.Code == "admin" || r.Code == "manager" || r.Code == "contributor" || r.Code == "viewer")
|
|
1177
|
+
.Select(r => r.Code)
|
|
1178
|
+
.ToListAsync(ct);
|
|
1179
|
+
|
|
1180
|
+
// Only create roles that don't already exist (SmartStack core may pre-seed system roles)
|
|
1181
|
+
foreach (var entry in ApplicationRolesSeedData.GetRoleEntries())
|
|
1182
|
+
{
|
|
1183
|
+
if (existingRoleCodes.Contains(entry.Code)) continue; // Skip if already exists
|
|
1184
|
+
|
|
1185
|
+
var role = Role.Create(
|
|
1186
|
+
entry.Code,
|
|
1187
|
+
entry.Name,
|
|
1188
|
+
entry.Description,
|
|
1189
|
+
entry.ApplicationId,
|
|
1190
|
+
entry.IsSystem);
|
|
1191
|
+
context.Roles.Add(role);
|
|
1192
|
+
}
|
|
1193
|
+
await ((DbContext)context).SaveChangesAsync(ct);
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
public async Task SeedPermissionsAsync(ICoreDbContext context, CancellationToken ct)
|
|
1197
|
+
{
|
|
1198
|
+
var exists = await context.Permissions
|
|
1199
|
+
.AnyAsync(p => p.Path == "{appCode}.*", ct);
|
|
1200
|
+
if (exists) return;
|
|
1201
|
+
|
|
1202
|
+
// Application-level wildcard
|
|
1203
|
+
var appWildcard = Permission.CreateWildcard(
|
|
1204
|
+
"{appCode}.*", PermissionLevel.Application,
|
|
1205
|
+
"Full {appLabel_en} access");
|
|
1206
|
+
context.Permissions.Add(appWildcard);
|
|
1207
|
+
|
|
1208
|
+
// Module permissions
|
|
1209
|
+
var mod1 = await context.NavigationModules
|
|
1210
|
+
.FirstAsync(m => m.Code == "{module1Code}", ct);
|
|
1211
|
+
foreach (var entry in {Module1Pascal}PermissionsSeedData.GetPermissionEntries(mod1.Id))
|
|
1212
|
+
{
|
|
1213
|
+
var perm = entry.IsWildcard
|
|
1214
|
+
? Permission.CreateWildcard(entry.Path, entry.Level, entry.Description)
|
|
1215
|
+
: Permission.CreateForModule(entry.Path, entry.Level, entry.Action, false, entry.ModuleId, entry.Description);
|
|
1216
|
+
context.Permissions.Add(perm);
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// Repeat for each module...
|
|
1220
|
+
await ((DbContext)context).SaveChangesAsync(ct);
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
public async Task SeedRolePermissionsAsync(ICoreDbContext context, CancellationToken ct)
|
|
1224
|
+
{
|
|
1225
|
+
var exists = await context.RolePermissions
|
|
1226
|
+
.AnyAsync(rp => rp.Permission!.Path.StartsWith("{appCode}."), ct);
|
|
1227
|
+
if (exists) return;
|
|
1228
|
+
|
|
1229
|
+
// CRITICAL: Resolve roles by Code from DB — NEVER use deterministic GUIDs.
|
|
1230
|
+
// Application-scoped roles (admin, manager, contributor, viewer) are created by
|
|
1231
|
+
// SeedRolesAsync() above. System roles use their own IDs that do NOT match
|
|
1232
|
+
// DeterministicGuid("role:admin").
|
|
1233
|
+
var roles = await context.Roles
|
|
1234
|
+
.Where(r => r.ApplicationId != null || r.IsSystem)
|
|
1235
|
+
.ToListAsync(ct);
|
|
1236
|
+
|
|
1237
|
+
// Resolve permissions
|
|
1238
|
+
var permissions = await context.Permissions
|
|
1239
|
+
.Where(p => p.Path.StartsWith("{appCode}."))
|
|
1240
|
+
.ToListAsync(ct);
|
|
1241
|
+
|
|
1242
|
+
// Apply role-permission mappings from all modules
|
|
1243
|
+
var allMappings = new List<RolePermissionSeedEntry>();
|
|
1244
|
+
allMappings.AddRange({Module1Pascal}RolesSeedData.GetRolePermissionEntries());
|
|
1245
|
+
// allMappings.AddRange({Module2Pascal}RolesSeedData.GetRolePermissionEntries()); // per module
|
|
1246
|
+
|
|
1247
|
+
foreach (var mapping in allMappings)
|
|
1248
|
+
{
|
|
1249
|
+
var role = roles.FirstOrDefault(r => r.Code == mapping.RoleCode);
|
|
1250
|
+
var perm = permissions.FirstOrDefault(p => p.Path == mapping.PermissionPath);
|
|
1251
|
+
|
|
1252
|
+
if (role == null)
|
|
1253
|
+
{
|
|
1254
|
+
// CRITICAL: Role not found — SeedRolesAsync() may not have run.
|
|
1255
|
+
// This causes silent permission failure → 401 on protected pages.
|
|
1256
|
+
Console.WriteLine($"[SEED WARNING] Role '{mapping.RoleCode}' not found. Role-permission mapping skipped for '{mapping.PermissionPath}'.");
|
|
1257
|
+
continue;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
if (perm == null)
|
|
1261
|
+
{
|
|
1262
|
+
Console.WriteLine($"[SEED WARNING] Permission '{mapping.PermissionPath}' not found. Role-permission mapping skipped for role '{mapping.RoleCode}'.");
|
|
1263
|
+
continue;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
context.RolePermissions.Add(RolePermission.Create(role.Id, perm.Id, "system"));
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
await ((DbContext)context).SaveChangesAsync(ct);
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
```
|
|
1273
|
+
|
|
1274
|
+
### DI Registration
|
|
1275
|
+
|
|
1276
|
+
```csharp
|
|
1277
|
+
// In Infrastructure/DependencyInjection.cs — add:
|
|
1278
|
+
using SmartStack.Application.Common.Interfaces.Seeding;
|
|
1279
|
+
|
|
1280
|
+
// In the registration method:
|
|
1281
|
+
services.AddScoped<IClientSeedDataProvider, {AppPascalName}SeedDataProvider>();
|
|
1282
|
+
```
|
|
1283
|
+
|
|
1284
|
+
---
|
|
1285
|
+
|
|
1286
|
+
## 7. Multi-Module Handling
|
|
1287
|
+
|
|
1288
|
+
When processing multiple modules in the same ralph-loop run:
|
|
1289
|
+
|
|
1290
|
+
### Module 1 (first): Creates everything from scratch
|
|
1291
|
+
|
|
1292
|
+
0. `NavigationApplicationSeedData.cs` (**application-level**, once per app — FIRST FILE, provides ApplicationId)
|
|
1293
|
+
1. `ApplicationRolesSeedData.cs` (application-level, once per app — references NavigationApplicationSeedData.ApplicationId)
|
|
1294
|
+
2. `{Module1}NavigationSeedData.cs` (module + sections + resources + all translations)
|
|
1295
|
+
3. `{Module1}PermissionsSeedData.cs`
|
|
1296
|
+
4. `{Module1}RolesSeedData.cs`
|
|
1297
|
+
5. `{AppPascalName}SeedDataProvider.cs` (new, with 4 methods — including section/resource seeding)
|
|
1298
|
+
6. DI registration (new)
|
|
1299
|
+
|
|
1300
|
+
### Module 2+ (subsequent): Append to existing provider
|
|
1301
|
+
|
|
1302
|
+
0. `NavigationApplicationSeedData.cs` (already exists — skip)
|
|
1303
|
+
1. `ApplicationRolesSeedData.cs` (already exists — skip)
|
|
1304
|
+
2. `{Module2}NavigationSeedData.cs` (new file — include sections + resources if defined in feature.json)
|
|
1305
|
+
3. `{Module2}PermissionsSeedData.cs` (new file)
|
|
1306
|
+
4. `{Module2}RolesSeedData.cs` (new file)
|
|
1307
|
+
5. `{AppPascalName}SeedDataProvider.cs` (**modify** — add using, add entries in Navigation/Permissions/RolePermissions methods, **including section/resource seeding for the new module**)
|
|
1308
|
+
6. DI registration (already exists — skip)
|
|
1309
|
+
|
|
1310
|
+
**Detection:** Check if `{AppPascalName}SeedDataProvider.cs` exists. If yes, READ it and ADD the new module's entries to the appropriate methods (Navigation, Permissions, RolePermissions). Do NOT modify SeedRolesAsync() or the Application creation in SeedNavigationAsync().
|
|
1311
|
+
|
|
1312
|
+
### Section/Resource Conditionality
|
|
1313
|
+
|
|
1314
|
+
Sections and resources are **optional per module**. When processing a module's feature.json:
|
|
1315
|
+
|
|
1316
|
+
| Condition | Action |
|
|
1317
|
+
|-----------|--------|
|
|
1318
|
+
| `seedDataCore.navigationSections` absent or empty | Skip `GetSectionEntries()` / `GetSectionTranslationEntries()` / section seeding in provider |
|
|
1319
|
+
| `seedDataCore.navigationResources` absent or empty | Skip `GetResourceEntries()` / resource seeding in provider |
|
|
1320
|
+
| Both present | Generate all section + resource methods and provider code |
|
|
1321
|
+
|
|
1322
|
+
---
|
|
1323
|
+
|
|
1324
|
+
## 8. Verification Checklist (BLOCKING)
|
|
1325
|
+
|
|
1326
|
+
Before marking the task as completed, verify ALL:
|
|
1327
|
+
|
|
1328
|
+
**Application-Level (FIRST — before modules):**
|
|
1329
|
+
- [ ] `NavigationApplicationSeedData.cs` created (once per application, at `Infrastructure/Persistence/Seeding/Data/`)
|
|
1330
|
+
- [ ] Application GUID is deterministic (SHA256 of `"navigation-application-{appCode}"`)
|
|
1331
|
+
- [ ] GetApplicationEntry() takes no parameters (no contextId)
|
|
1332
|
+
- [ ] Application translations created (4 languages: fr, en, it, de, EntityType = Application)
|
|
1333
|
+
- [ ] `IClientSeedDataProvider.SeedNavigationAsync()` uses `NavigationApplicationSeedData` (NO hardcoded `{appLabel_en}` / `{appIcon}` placeholders)
|
|
1334
|
+
- [ ] `ApplicationRolesSeedData.ApplicationId` references `NavigationApplicationSeedData.ApplicationId` (NO `{ApplicationGuid}` placeholder)
|
|
1335
|
+
|
|
1336
|
+
**Module-Level:**
|
|
1337
|
+
- [ ] Deterministic GUIDs (NEVER `Guid.NewGuid()`) — SHA256 of path
|
|
1338
|
+
- [ ] 4 languages for each navigation entity (fr, en, it, de)
|
|
1339
|
+
- [ ] `ApplicationRolesSeedData.cs` created (once per application)
|
|
1340
|
+
- [ ] 4 application roles defined: Admin, Manager, Contributor, Viewer
|
|
1341
|
+
- [ ] Each role has a valid `Code` value ("admin", "manager", "contributor", "viewer")
|
|
1342
|
+
- [ ] `Permissions.cs` constants match seed data paths
|
|
1343
|
+
- [ ] MCP `generate_permissions` called (or fallback used)
|
|
1344
|
+
- [ ] Role-permission mappings assigned (Admin, Manager, Contributor, Viewer)
|
|
1345
|
+
- [ ] **RolePermission mappings use Code-based lookup** — NEVER `DeterministicGuid("role:admin")` or `GenerateRoleGuid()`
|
|
1346
|
+
- [ ] **SeedRolesAsync checks existence by Code** — SmartStack core may pre-seed system roles
|
|
1347
|
+
- [ ] `IClientSeedDataProvider` generated with 4 methods (Navigation, Roles, Permissions, RolePermissions)
|
|
1348
|
+
- [ ] Execution order: Navigation (application → modules → sections → resources) → Roles → Permissions → RolePermissions
|
|
1349
|
+
- [ ] Each Seed method is idempotent (checks existence before inserting)
|
|
1350
|
+
- [ ] Factory methods used throughout (NEVER `new Entity()`)
|
|
1351
|
+
- [ ] `SaveChangesAsync` called per group (Navigation → Roles → Permissions → RolePermissions)
|
|
1352
|
+
- [ ] DI registration added: `services.AddScoped<IClientSeedDataProvider, ...>()`
|
|
1353
|
+
- [ ] NavigationSections seeded (if `seedDataCore.navigationSections` present in feature.json)
|
|
1354
|
+
- [ ] NavigationResources seeded (if `seedDataCore.navigationResources` present in feature.json)
|
|
1355
|
+
- [ ] Section/Resource translations created (4 languages each, EntityType = Section/Resource)
|
|
1356
|
+
- [ ] `dotnet build` passes after generation
|
|
1357
|
+
- [ ] NO `Enum.Parse<PermissionAction>` usage anywhere in seeding code (use typed enum directly)
|
|
1358
|
+
- [ ] ALL PermissionAction values are from the valid enum: Access, Read, Create, Update, Delete, Export, Import, Approve, Reject, Assign, Execute
|
|
1359
|
+
|
|
1360
|
+
**Seed Data Integrity (BLOCKING — run AFTER `dotnet build`):**
|
|
1361
|
+
- [ ] Application startup test: `dotnet run --urls http://localhost:0 --environment Development` exits without seed data exceptions
|
|
1362
|
+
- [ ] Verify `nav_Applications` has entry for `{appCode}` (query or startup log)
|
|
1363
|
+
- [ ] Verify `nav_Modules` has entries for each module (count matches feature.json modules)
|
|
1364
|
+
- [ ] Verify `auth_Roles` has 4 application-scoped roles (admin, manager, contributor, viewer)
|
|
1365
|
+
- [ ] Verify `auth_Permissions` has entries for each module (wildcard + CRUD)
|
|
1366
|
+
|
|
1367
|
+
**If ANY check fails, the task status = 'failed'.**
|
|
1368
|
+
|
|
1369
|
+
---
|
|
1370
|
+
|
|
1371
|
+
## 9. Business Seed Data (DevDataSeeder) — TenantId Rules
|
|
1372
|
+
|
|
1373
|
+
> **Applies to:** Seed data for business entities (reference types, categories, statuses).
|
|
1374
|
+
> **NOT the same as** core seed data above (navigation, permissions, roles).
|
|
1375
|
+
|
|
1376
|
+
### Rules
|
|
1377
|
+
|
|
1378
|
+
| Rule | Description |
|
|
1379
|
+
|------|-------------|
|
|
1380
|
+
| TenantId MANDATORY | ALL business seed entities MUST set `TenantId` |
|
|
1381
|
+
| Deterministic TenantId | Use `SeedConstants.DefaultTenantId` (NEVER inline GUID) |
|
|
1382
|
+
| DevDataSeeder pattern | Implement `IDevDataSeeder` with `SeedAsync()` method |
|
|
1383
|
+
| Idempotency | Each seeder MUST check `AnyAsync()` before inserting |
|
|
1384
|
+
| Order | DevDataSeeder `Order >= 200` (after core seed data at Order 100) |
|
|
1385
|
+
|
|
1386
|
+
### Template (Reference Types)
|
|
1387
|
+
|
|
1388
|
+
```csharp
|
|
1389
|
+
public class {Module}DevDataSeeder : IDevDataSeeder
|
|
1390
|
+
{
|
|
1391
|
+
public int Order => 200; // After core seed data (Order 100)
|
|
1392
|
+
|
|
1393
|
+
public async Task SeedAsync(ExtensionsDbContext context, CancellationToken ct)
|
|
1394
|
+
{
|
|
1395
|
+
if (await context.Set<{EntityType}>().AnyAsync(ct)) return;
|
|
1396
|
+
|
|
1397
|
+
var items = new[]
|
|
1398
|
+
{
|
|
1399
|
+
new {EntityType}
|
|
1400
|
+
{
|
|
1401
|
+
Id = GenerateDeterministicGuid("{entity-type}-{code}"),
|
|
1402
|
+
Code = "{code}",
|
|
1403
|
+
Name = "{name}",
|
|
1404
|
+
TenantId = SeedConstants.DefaultTenantId, // MANDATORY
|
|
1405
|
+
IsActive = true
|
|
1406
|
+
}
|
|
1407
|
+
};
|
|
1408
|
+
|
|
1409
|
+
context.Set<{EntityType}>().AddRange(items);
|
|
1410
|
+
await context.SaveChangesAsync(ct);
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
private static Guid GenerateDeterministicGuid(string seed)
|
|
1414
|
+
{
|
|
1415
|
+
using var sha256 = System.Security.Cryptography.SHA256.Create();
|
|
1416
|
+
var hash = sha256.ComputeHash(System.Text.Encoding.UTF8.GetBytes(seed));
|
|
1417
|
+
return new Guid(hash.Take(16).ToArray());
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
```
|
|
1421
|
+
|
|
1422
|
+
### FORBIDDEN
|
|
1423
|
+
|
|
1424
|
+
- Seeding business entities WITHOUT `TenantId`
|
|
1425
|
+
- Using `Guid.NewGuid()` for TenantId
|
|
1426
|
+
- Omitting idempotency check (`AnyAsync`)
|
|
1427
|
+
- Hardcoding TenantId inline (use `SeedConstants.DefaultTenantId`)
|
|
1428
|
+
|
|
1429
|
+
### CROSS-SCHEMA FK WARNING
|
|
1430
|
+
|
|
1431
|
+
> `SeedConstants.DefaultTenantId` MUST reference a tenant that EXISTS in `core.tenant_Tenants`.
|
|
1432
|
+
> The SmartStack platform seeds a default tenant during `InitializeSmartStackAsync()`.
|
|
1433
|
+
> If you use a custom GUID, ensure it is created BEFORE `DevDataSeeder` runs (Order >= 200).
|
|
1434
|
+
>
|
|
1435
|
+
> **Pipeline validation:**
|
|
1436
|
+
> - ralph-loop POST-CHECK warns if GUID not found in project config
|
|
1437
|
+
> - validate-feature step-05 verifies FK exists in real database via SQL query
|