@atlashub/smartstack-cli 3.30.0 → 3.32.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/installation.html +7 -2
- package/README.md +7 -1
- package/dist/index.js +33 -37
- package/dist/index.js.map +1 -1
- package/dist/mcp-entry.mjs +547 -97
- package/dist/mcp-entry.mjs.map +1 -1
- package/package.json +1 -1
- package/scripts/health-check.sh +2 -1
- package/templates/mcp-scaffolding/controller.cs.hbs +10 -7
- package/templates/mcp-scaffolding/entity-extension.cs.hbs +132 -124
- package/templates/mcp-scaffolding/frontend/api-client.ts.hbs +4 -4
- package/templates/mcp-scaffolding/tests/controller.test.cs.hbs +38 -15
- package/templates/mcp-scaffolding/tests/service.test.cs.hbs +20 -8
- package/templates/skills/apex/SKILL.md +7 -9
- package/templates/skills/apex/_shared.md +9 -2
- package/templates/skills/apex/references/code-generation.md +412 -0
- package/templates/skills/apex/references/post-checks.md +377 -37
- package/templates/skills/apex/references/smartstack-api.md +229 -5
- package/templates/skills/apex/references/smartstack-frontend.md +368 -11
- package/templates/skills/apex/references/smartstack-layers.md +54 -7
- package/templates/skills/apex/steps/step-00-init.md +1 -2
- package/templates/skills/apex/steps/step-01-analyze.md +45 -2
- package/templates/skills/apex/steps/step-02-plan.md +23 -2
- package/templates/skills/apex/steps/step-03-execute.md +195 -5
- package/templates/skills/apex/steps/step-04-examine.md +18 -5
- package/templates/skills/apex/steps/step-05-deep-review.md +9 -11
- package/templates/skills/apex/steps/step-06-resolve.md +5 -9
- package/templates/skills/apex/steps/step-07-tests.md +66 -1
- package/templates/skills/apex/steps/step-08-run-tests.md +12 -3
- package/templates/skills/application/references/provider-template.md +62 -39
- package/templates/skills/application/templates-backend.md +3 -3
- package/templates/skills/application/templates-frontend.md +12 -12
- package/templates/skills/application/templates-seed.md +14 -4
- package/templates/skills/business-analyse/SKILL.md +10 -7
- package/templates/skills/business-analyse/questionnaire/04-data.md +8 -0
- package/templates/skills/business-analyse/references/agent-module-prompt.md +84 -5
- package/templates/skills/business-analyse/references/agent-pooling-best-practices.md +83 -19
- package/templates/skills/business-analyse/references/consolidation-structural-checks.md +6 -2
- package/templates/skills/business-analyse/references/team-orchestration.md +470 -113
- package/templates/skills/business-analyse/references/validation-checklist.md +5 -4
- package/templates/skills/business-analyse/schemas/sections/analysis-schema.json +44 -0
- package/templates/skills/business-analyse/steps/step-03a2-analysis.md +72 -1
- package/templates/skills/business-analyse/steps/step-03c-compile.md +93 -7
- package/templates/skills/business-analyse/steps/step-03d-validate.md +34 -2
- package/templates/skills/business-analyse/steps/step-04b-analyze.md +40 -0
- package/templates/skills/controller/references/controller-code-templates.md +2 -2
- package/templates/skills/controller/templates.md +12 -12
- package/templates/skills/feature-full/steps/step-01-implementation.md +4 -4
- package/templates/skills/ralph-loop/references/category-rules.md +44 -2
- package/templates/skills/ralph-loop/references/compact-loop.md +37 -0
- package/templates/skills/ralph-loop/references/core-seed-data.md +51 -20
- package/templates/skills/review-code/references/owasp-api-top10.md +1 -1
package/dist/mcp-entry.mjs
CHANGED
|
@@ -25763,7 +25763,31 @@ var init_zod = __esm({
|
|
|
25763
25763
|
});
|
|
25764
25764
|
|
|
25765
25765
|
// src/mcp/types/index.ts
|
|
25766
|
-
|
|
25766
|
+
function resolveTenantMode(options) {
|
|
25767
|
+
if (options?.tenantMode) return options.tenantMode;
|
|
25768
|
+
if (options?.isSystemEntity === true) return "none";
|
|
25769
|
+
return "strict";
|
|
25770
|
+
}
|
|
25771
|
+
function tenantModeToTemplateFlags(mode) {
|
|
25772
|
+
return {
|
|
25773
|
+
// Backward compat — existing templates use {{#unless isSystemEntity}}
|
|
25774
|
+
isSystemEntity: mode === "none",
|
|
25775
|
+
// Mode-specific flags
|
|
25776
|
+
isStrict: mode === "strict",
|
|
25777
|
+
isOptional: mode === "optional",
|
|
25778
|
+
isScoped: mode === "scoped",
|
|
25779
|
+
isNone: mode === "none",
|
|
25780
|
+
// Derived flags for cleaner templates
|
|
25781
|
+
hasTenantId: mode !== "none",
|
|
25782
|
+
// strict, optional, scoped all have TenantId
|
|
25783
|
+
isNullableTenant: mode === "optional" || mode === "scoped",
|
|
25784
|
+
// Guid? vs Guid
|
|
25785
|
+
hasScope: mode === "scoped",
|
|
25786
|
+
// only scoped has EntityScope
|
|
25787
|
+
tenantMode: mode
|
|
25788
|
+
};
|
|
25789
|
+
}
|
|
25790
|
+
var ProjectConfigSchema, SmartStackConfigSchema, ConventionsConfigSchema, EfCoreContextSchema, EfCoreConfigSchema, ScaffoldingConfigSchema, ConfigSchema, TenantModeSchema, ValidateConventionsInputSchema, CheckMigrationsInputSchema, EntityPropertySchema, ScaffoldExtensionInputSchema, ApiDocsInputSchema, SuggestMigrationInputSchema, GeneratePermissionsInputSchema, TestTypeSchema, TestTargetSchema, ScaffoldTestsInputSchema, AnalyzeTestCoverageInputSchema, ValidateTestConventionsInputSchema, SuggestTestScenariosInputSchema, SecurityCheckSchema, ValidateSecurityInputSchema, QualityMetricSchema, AnalyzeCodeQualityInputSchema, ScaffoldApiClientInputSchema, ScaffoldRoutesInputSchema, ValidateFrontendRoutesInputSchema, SlotDefinitionSchema, ScaffoldFrontendExtensionInputSchema, AnalyzeExtensionPointsInputSchema, AnalyzeHierarchyPatternsInputSchema, ReviewCodeCheckSchema, ReviewCodeInputSchema;
|
|
25767
25791
|
var init_types3 = __esm({
|
|
25768
25792
|
"src/mcp/types/index.ts"() {
|
|
25769
25793
|
"use strict";
|
|
@@ -25854,6 +25878,7 @@ var init_types3 = __esm({
|
|
|
25854
25878
|
// Resolved DbContext to use (from projectConfig or default)
|
|
25855
25879
|
defaultDbContext: external_exports.enum(["core", "extensions"]).default("core")
|
|
25856
25880
|
});
|
|
25881
|
+
TenantModeSchema = external_exports.enum(["strict", "optional", "scoped", "none"]).default("strict");
|
|
25857
25882
|
ValidateConventionsInputSchema = external_exports.object({
|
|
25858
25883
|
path: external_exports.string().optional().describe("Project path to validate (default: SmartStack.app path)"),
|
|
25859
25884
|
checks: external_exports.array(external_exports.enum(["tables", "migrations", "services", "namespaces", "entities", "tenants", "controllers", "layouts", "tabs", "hierarchies", "protected-actions", "permissions", "frontend-routes", "feature-json", "all"])).default(["all"]).describe("Types of checks to perform")
|
|
@@ -25877,7 +25902,8 @@ var init_types3 = __esm({
|
|
|
25877
25902
|
baseEntity: external_exports.string().optional().describe("Base entity to extend (for entity type)"),
|
|
25878
25903
|
methods: external_exports.array(external_exports.string()).optional().describe("Methods to generate (for service type)"),
|
|
25879
25904
|
outputPath: external_exports.string().optional().describe("Custom output path"),
|
|
25880
|
-
isSystemEntity: external_exports.boolean().optional().describe("If true, creates a system entity without TenantId"),
|
|
25905
|
+
isSystemEntity: external_exports.boolean().optional().describe("[DEPRECATED: use tenantMode] If true, creates a system entity without TenantId"),
|
|
25906
|
+
tenantMode: TenantModeSchema.optional().describe("Tenant isolation mode: strict (default), optional (cross-tenant), scoped (with EntityScope), none (no tenant)"),
|
|
25881
25907
|
tablePrefix: external_exports.string().optional().describe('Domain prefix for table name (e.g., "auth_", "nav_", "cfg_")'),
|
|
25882
25908
|
schema: external_exports.enum(["core", "extensions"]).optional().describe("Database schema (default: core)"),
|
|
25883
25909
|
dryRun: external_exports.boolean().optional().describe("If true, preview generated code without writing files"),
|
|
@@ -25928,7 +25954,8 @@ var init_types3 = __esm({
|
|
|
25928
25954
|
includeAuthorization: external_exports.boolean().default(false).describe("Include authorization tests"),
|
|
25929
25955
|
includePerformance: external_exports.boolean().default(false).describe("Include performance tests"),
|
|
25930
25956
|
entityProperties: external_exports.array(EntityPropertySchema).optional().describe("Entity properties for test generation"),
|
|
25931
|
-
isSystemEntity: external_exports.boolean().default(false).describe("If true, entity has no TenantId"),
|
|
25957
|
+
isSystemEntity: external_exports.boolean().default(false).describe("[DEPRECATED: use tenantMode] If true, entity has no TenantId"),
|
|
25958
|
+
tenantMode: TenantModeSchema.optional().describe("Tenant isolation mode: strict (default), optional (cross-tenant), scoped (with EntityScope), none (no tenant)"),
|
|
25932
25959
|
dryRun: external_exports.boolean().default(false).describe("Preview without writing files")
|
|
25933
25960
|
}).optional()
|
|
25934
25961
|
});
|
|
@@ -26668,7 +26695,7 @@ import path8 from "path";
|
|
|
26668
26695
|
async function handleValidateConventions(args, config2) {
|
|
26669
26696
|
const input = ValidateConventionsInputSchema.parse(args);
|
|
26670
26697
|
const projectPath = input.path || config2.smartstack.projectPath;
|
|
26671
|
-
const checks = input.checks.includes("all") ? ["tables", "migrations", "services", "namespaces", "entities", "tenants", "controllers", "layouts", "tabs", "hierarchies", "protected-actions", "permissions", "frontend-routes", "feature-json"] : input.checks;
|
|
26698
|
+
const checks = input.checks.includes("all") ? ["tables", "migrations", "services", "namespaces", "entities", "tenants", "controllers", "layouts", "tabs", "hierarchies", "protected-actions", "permissions", "frontend-routes", "feature-json", "code-patterns"] : input.checks;
|
|
26672
26699
|
logger.info("Validating conventions", { projectPath, checks });
|
|
26673
26700
|
const result = {
|
|
26674
26701
|
valid: true,
|
|
@@ -26719,6 +26746,9 @@ async function handleValidateConventions(args, config2) {
|
|
|
26719
26746
|
if (checks.includes("feature-json")) {
|
|
26720
26747
|
await validateFeatureJson(structure, config2, result);
|
|
26721
26748
|
}
|
|
26749
|
+
if (checks.includes("code-patterns")) {
|
|
26750
|
+
await validateCodePatterns(structure, config2, result);
|
|
26751
|
+
}
|
|
26722
26752
|
result.valid = result.errors.length === 0;
|
|
26723
26753
|
result.summary = generateSummary(result, checks);
|
|
26724
26754
|
return formatResult(result);
|
|
@@ -26901,8 +26931,6 @@ async function validateNamespaces(structure, config2, result) {
|
|
|
26901
26931
|
if (namespaceMatch) {
|
|
26902
26932
|
const namespace = namespaceMatch[1];
|
|
26903
26933
|
if (!namespace.startsWith(layer.expected)) {
|
|
26904
|
-
const lastDot = namespace.lastIndexOf(".");
|
|
26905
|
-
const namespaceSuffix = lastDot > 0 ? namespace.substring(lastDot + 1) : "";
|
|
26906
26934
|
const isValidLayerPattern = ["Domain", "Application", "Infrastructure", "Api"].some((l) => namespace.includes(`.${l}`) || namespace.endsWith(`.${l}`));
|
|
26907
26935
|
const severity = isValidLayerPattern ? "warning" : "error";
|
|
26908
26936
|
result.errors.push({
|
|
@@ -26940,16 +26968,19 @@ async function validateEntities(structure, _config, result) {
|
|
|
26940
26968
|
const hasBaseEntity = inheritance.includes("BaseEntity");
|
|
26941
26969
|
const hasSystemEntity = inheritance.includes("SystemEntity");
|
|
26942
26970
|
const hasITenantEntity = inheritance.includes("ITenantEntity");
|
|
26971
|
+
const hasIOptionalTenantEntity = inheritance.includes("IOptionalTenantEntity");
|
|
26972
|
+
const hasIScopedTenantEntity = inheritance.includes("IScopedTenantEntity");
|
|
26973
|
+
const hasAnyTenantInterface = hasITenantEntity || hasIOptionalTenantEntity || hasIScopedTenantEntity;
|
|
26943
26974
|
if (!hasBaseEntity && !hasSystemEntity) {
|
|
26944
26975
|
continue;
|
|
26945
26976
|
}
|
|
26946
|
-
if (hasBaseEntity && !hasSystemEntity && !
|
|
26977
|
+
if (hasBaseEntity && !hasSystemEntity && !hasAnyTenantInterface) {
|
|
26947
26978
|
result.warnings.push({
|
|
26948
26979
|
type: "warning",
|
|
26949
26980
|
category: "entities",
|
|
26950
|
-
message: `Entity "${entityName}" inherits BaseEntity but doesn't implement
|
|
26981
|
+
message: `Entity "${entityName}" inherits BaseEntity but doesn't implement any tenant interface`,
|
|
26951
26982
|
file: path8.relative(structure.root, file),
|
|
26952
|
-
suggestion: "Add ITenantEntity
|
|
26983
|
+
suggestion: "Add ITenantEntity (strict), IOptionalTenantEntity (cross-tenant), or IScopedTenantEntity (with scope) for multi-tenant support"
|
|
26953
26984
|
});
|
|
26954
26985
|
}
|
|
26955
26986
|
if (!content.includes(`private ${entityName}()`)) {
|
|
@@ -26982,7 +27013,9 @@ async function validateTenantAwareness(structure, _config, result) {
|
|
|
26982
27013
|
return;
|
|
26983
27014
|
}
|
|
26984
27015
|
const entityFiles = await findFiles("**/*.cs", { cwd: structure.domain });
|
|
26985
|
-
let
|
|
27016
|
+
let strictCount = 0;
|
|
27017
|
+
let optionalCount = 0;
|
|
27018
|
+
let scopedCount = 0;
|
|
26986
27019
|
let systemEntityCount = 0;
|
|
26987
27020
|
let ambiguousCount = 0;
|
|
26988
27021
|
for (const file of entityFiles) {
|
|
@@ -26991,15 +27024,19 @@ async function validateTenantAwareness(structure, _config, result) {
|
|
|
26991
27024
|
if (!classMatch) continue;
|
|
26992
27025
|
const entityName = classMatch[1];
|
|
26993
27026
|
const inheritance = classMatch[2]?.trim() || "";
|
|
26994
|
-
if (!inheritance.includes("Entity") && !inheritance.includes("ITenant")) {
|
|
27027
|
+
if (!inheritance.includes("Entity") && !inheritance.includes("ITenant") && !inheritance.includes("IOptional") && !inheritance.includes("IScoped")) {
|
|
26995
27028
|
continue;
|
|
26996
27029
|
}
|
|
26997
|
-
const
|
|
27030
|
+
const hasIScopedTenantEntity = inheritance.includes("IScopedTenantEntity");
|
|
27031
|
+
const hasIOptionalTenantEntity = inheritance.includes("IOptionalTenantEntity") && !hasIScopedTenantEntity;
|
|
27032
|
+
const hasITenantEntity = inheritance.includes("ITenantEntity") && !hasIOptionalTenantEntity && !hasIScopedTenantEntity;
|
|
26998
27033
|
const hasSystemEntity = inheritance.includes("SystemEntity");
|
|
27034
|
+
const hasAnyTenantInterface = hasITenantEntity || hasIOptionalTenantEntity || hasIScopedTenantEntity;
|
|
26999
27035
|
const hasTenantId = content.includes("TenantId");
|
|
27000
27036
|
const hasTenantIdProperty = content.includes("public Guid TenantId") || content.includes("public required Guid TenantId");
|
|
27037
|
+
const hasNullableTenantIdProperty = content.includes("public Guid? TenantId");
|
|
27001
27038
|
if (hasITenantEntity) {
|
|
27002
|
-
|
|
27039
|
+
strictCount++;
|
|
27003
27040
|
if (!hasTenantIdProperty) {
|
|
27004
27041
|
result.errors.push({
|
|
27005
27042
|
type: "error",
|
|
@@ -27020,6 +27057,39 @@ async function validateTenantAwareness(structure, _config, result) {
|
|
|
27020
27057
|
});
|
|
27021
27058
|
}
|
|
27022
27059
|
}
|
|
27060
|
+
if (hasIOptionalTenantEntity) {
|
|
27061
|
+
optionalCount++;
|
|
27062
|
+
if (!hasNullableTenantIdProperty) {
|
|
27063
|
+
result.errors.push({
|
|
27064
|
+
type: "error",
|
|
27065
|
+
category: "tenants",
|
|
27066
|
+
message: `Entity "${entityName}" implements IOptionalTenantEntity but TenantId is not Guid?`,
|
|
27067
|
+
file: path8.relative(structure.root, file),
|
|
27068
|
+
suggestion: "TenantId must be nullable for IOptionalTenantEntity: public Guid? TenantId { get; private set; }"
|
|
27069
|
+
});
|
|
27070
|
+
}
|
|
27071
|
+
}
|
|
27072
|
+
if (hasIScopedTenantEntity) {
|
|
27073
|
+
scopedCount++;
|
|
27074
|
+
if (!hasNullableTenantIdProperty) {
|
|
27075
|
+
result.errors.push({
|
|
27076
|
+
type: "error",
|
|
27077
|
+
category: "tenants",
|
|
27078
|
+
message: `Entity "${entityName}" implements IScopedTenantEntity but TenantId is not Guid?`,
|
|
27079
|
+
file: path8.relative(structure.root, file),
|
|
27080
|
+
suggestion: "TenantId must be nullable for IScopedTenantEntity: public Guid? TenantId { get; private set; }"
|
|
27081
|
+
});
|
|
27082
|
+
}
|
|
27083
|
+
if (!content.includes("EntityScope") && !content.includes("Scope")) {
|
|
27084
|
+
result.errors.push({
|
|
27085
|
+
type: "error",
|
|
27086
|
+
category: "tenants",
|
|
27087
|
+
message: `Entity "${entityName}" implements IScopedTenantEntity but is missing Scope property`,
|
|
27088
|
+
file: path8.relative(structure.root, file),
|
|
27089
|
+
suggestion: "Add: public EntityScope Scope { get; private set; }"
|
|
27090
|
+
});
|
|
27091
|
+
}
|
|
27092
|
+
}
|
|
27023
27093
|
if (hasSystemEntity) {
|
|
27024
27094
|
systemEntityCount++;
|
|
27025
27095
|
if (hasTenantId) {
|
|
@@ -27032,22 +27102,23 @@ async function validateTenantAwareness(structure, _config, result) {
|
|
|
27032
27102
|
});
|
|
27033
27103
|
}
|
|
27034
27104
|
}
|
|
27035
|
-
if (!
|
|
27105
|
+
if (!hasAnyTenantInterface && !hasSystemEntity && hasTenantId) {
|
|
27036
27106
|
ambiguousCount++;
|
|
27037
27107
|
result.warnings.push({
|
|
27038
27108
|
type: "warning",
|
|
27039
27109
|
category: "tenants",
|
|
27040
|
-
message: `Entity "${entityName}" has TenantId but doesn't implement
|
|
27110
|
+
message: `Entity "${entityName}" has TenantId but doesn't implement any tenant interface`,
|
|
27041
27111
|
file: path8.relative(structure.root, file),
|
|
27042
|
-
suggestion: "Add ITenantEntity
|
|
27112
|
+
suggestion: "Add ITenantEntity (strict), IOptionalTenantEntity (cross-tenant), or IScopedTenantEntity (scoped)"
|
|
27043
27113
|
});
|
|
27044
27114
|
}
|
|
27045
27115
|
}
|
|
27046
|
-
|
|
27116
|
+
const totalTenant = strictCount + optionalCount + scopedCount;
|
|
27117
|
+
if (totalTenant + systemEntityCount > 0) {
|
|
27047
27118
|
result.warnings.push({
|
|
27048
27119
|
type: "warning",
|
|
27049
27120
|
category: "tenants",
|
|
27050
|
-
message: `Tenant summary: ${
|
|
27121
|
+
message: `Tenant summary: ${strictCount} strict, ${optionalCount} optional, ${scopedCount} scoped, ${systemEntityCount} system, ${ambiguousCount} ambiguous`
|
|
27051
27122
|
});
|
|
27052
27123
|
}
|
|
27053
27124
|
}
|
|
@@ -27437,7 +27508,7 @@ async function validateHierarchies(structure, _config, result) {
|
|
|
27437
27508
|
const hasSystemEntity = inheritance.includes("SystemEntity");
|
|
27438
27509
|
if (!hasBaseEntity && !hasSystemEntity) continue;
|
|
27439
27510
|
entityNames.add(entityName);
|
|
27440
|
-
const hasITenantEntity = inheritance.includes("ITenantEntity");
|
|
27511
|
+
const hasITenantEntity = inheritance.includes("ITenantEntity") || inheritance.includes("IOptionalTenantEntity") || inheritance.includes("IScopedTenantEntity");
|
|
27441
27512
|
const selfRefMatch = content.match(
|
|
27442
27513
|
new RegExp(
|
|
27443
27514
|
`public\\s+(?:Guid\\??|${entityName}\\??)\\s+(Parent(?:Id)?|Parent${entityName}(?:Id)?)\\s*\\{`,
|
|
@@ -27520,7 +27591,7 @@ async function validateHierarchies(structure, _config, result) {
|
|
|
27520
27591
|
category: "hierarchies",
|
|
27521
27592
|
message: `Entity "${h.name}" has tenant-aware parent "${h.parentEntity}" but is not tenant-aware itself`,
|
|
27522
27593
|
file: h.file,
|
|
27523
|
-
suggestion: `Add
|
|
27594
|
+
suggestion: `Add a tenant interface (ITenantEntity, IOptionalTenantEntity, or IScopedTenantEntity) to ${h.name} for consistent tenant isolation.`
|
|
27524
27595
|
});
|
|
27525
27596
|
}
|
|
27526
27597
|
}
|
|
@@ -27991,6 +28062,60 @@ async function validateFeatureJson(structure, _config, result) {
|
|
|
27991
28062
|
message: `Feature.json summary: ${featureFiles.length} file(s) validated`
|
|
27992
28063
|
});
|
|
27993
28064
|
}
|
|
28065
|
+
async function validateCodePatterns(structure, _config, result) {
|
|
28066
|
+
const projectPath = structure.root || "";
|
|
28067
|
+
if (!projectPath) return;
|
|
28068
|
+
const validatorFiles = await findFiles("**/*Validator.cs", { cwd: path8.join(projectPath, "src") });
|
|
28069
|
+
for (const file of validatorFiles) {
|
|
28070
|
+
const fullPath = path8.join(projectPath, "src", file);
|
|
28071
|
+
try {
|
|
28072
|
+
const content = await readText(fullPath);
|
|
28073
|
+
if (content.includes("^[a-z0-9_]+$") && !content.includes("^[a-z0-9_-]+$")) {
|
|
28074
|
+
result.errors.push({
|
|
28075
|
+
type: "error",
|
|
28076
|
+
category: "code-patterns",
|
|
28077
|
+
message: `Validator uses old Code regex without hyphen support`,
|
|
28078
|
+
file,
|
|
28079
|
+
suggestion: "Update regex to ^[a-z0-9_-]+$ to support auto-generated codes with hyphens (e.g., acme-emp-00001)"
|
|
28080
|
+
});
|
|
28081
|
+
}
|
|
28082
|
+
} catch {
|
|
28083
|
+
}
|
|
28084
|
+
}
|
|
28085
|
+
const serviceFiles = (await findFiles("**/*Service.cs", { cwd: path8.join(projectPath, "src") })).filter((f) => !path8.basename(f).startsWith("I"));
|
|
28086
|
+
for (const file of serviceFiles) {
|
|
28087
|
+
const fullPath = path8.join(projectPath, "src", file);
|
|
28088
|
+
try {
|
|
28089
|
+
const content = await readText(fullPath);
|
|
28090
|
+
if (content.includes("ICodeGenerator")) {
|
|
28091
|
+
const entityName = path8.basename(file).replace("Service.cs", "");
|
|
28092
|
+
const dtoFiles = await findFiles(`**/Create${entityName}Dto.cs`, { cwd: path8.join(projectPath, "src") });
|
|
28093
|
+
for (const dtoFile of dtoFiles) {
|
|
28094
|
+
const dtoFullPath = path8.join(projectPath, "src", dtoFile);
|
|
28095
|
+
try {
|
|
28096
|
+
const dtoContent = await readText(dtoFullPath);
|
|
28097
|
+
if (dtoContent.includes("public string Code")) {
|
|
28098
|
+
result.warnings.push({
|
|
28099
|
+
type: "warning",
|
|
28100
|
+
category: "code-patterns",
|
|
28101
|
+
message: `Create${entityName}Dto has Code property but service uses ICodeGenerator (code is auto-generated)`,
|
|
28102
|
+
file: dtoFile,
|
|
28103
|
+
suggestion: `Remove Code from Create${entityName}Dto \u2014 it is auto-generated by ICodeGenerator<${entityName}>`
|
|
28104
|
+
});
|
|
28105
|
+
}
|
|
28106
|
+
} catch {
|
|
28107
|
+
}
|
|
28108
|
+
}
|
|
28109
|
+
}
|
|
28110
|
+
} catch {
|
|
28111
|
+
}
|
|
28112
|
+
}
|
|
28113
|
+
result.warnings.push({
|
|
28114
|
+
type: "warning",
|
|
28115
|
+
category: "code-patterns",
|
|
28116
|
+
message: `Code patterns check completed: ${validatorFiles.length} validators, ${serviceFiles.length} services scanned`
|
|
28117
|
+
});
|
|
28118
|
+
}
|
|
27994
28119
|
function generateSummary(result, checks) {
|
|
27995
28120
|
const parts = [];
|
|
27996
28121
|
parts.push(`Checks performed: ${checks.join(", ")}`);
|
|
@@ -28059,7 +28184,7 @@ var init_validate_conventions = __esm({
|
|
|
28059
28184
|
type: "array",
|
|
28060
28185
|
items: {
|
|
28061
28186
|
type: "string",
|
|
28062
|
-
enum: ["tables", "migrations", "services", "namespaces", "entities", "tenants", "controllers", "layouts", "tabs", "hierarchies", "protected-actions", "permissions", "frontend-routes", "feature-json", "all"]
|
|
28187
|
+
enum: ["tables", "migrations", "services", "namespaces", "entities", "tenants", "controllers", "layouts", "tabs", "hierarchies", "protected-actions", "permissions", "frontend-routes", "feature-json", "code-patterns", "all"]
|
|
28063
28188
|
},
|
|
28064
28189
|
description: "Types of checks to perform",
|
|
28065
28190
|
default: ["all"]
|
|
@@ -34474,6 +34599,8 @@ async function scaffoldFeature(name, options, structure, config2, result, dryRun
|
|
|
34474
34599
|
}
|
|
34475
34600
|
async function scaffoldService(name, options, structure, config2, result, dryRun = false) {
|
|
34476
34601
|
const hierarchy = resolveHierarchy(options?.navRoute);
|
|
34602
|
+
const tenantMode = resolveTenantMode(options);
|
|
34603
|
+
const tmFlags = tenantModeToTemplateFlags(tenantMode);
|
|
34477
34604
|
const interfaceNamespace = options?.namespace || `${config2.conventions.namespaces.application}.Common.Interfaces`;
|
|
34478
34605
|
const implNamespace = hierarchy.infraPath ? `${config2.conventions.namespaces.infrastructure}.Services.${hierarchy.infraPath.replace(/[\\/]/g, ".")}` : `${config2.conventions.namespaces.infrastructure}.Services`;
|
|
34479
34606
|
const methods = options?.methods || ["GetByIdAsync", "GetAllAsync", "CreateAsync", "UpdateAsync", "DeleteAsync"];
|
|
@@ -34504,32 +34631,46 @@ using System.Linq;
|
|
|
34504
34631
|
using Microsoft.EntityFrameworkCore;
|
|
34505
34632
|
using Microsoft.Extensions.Logging;
|
|
34506
34633
|
using SmartStack.Application.Common.Interfaces.Identity;
|
|
34634
|
+
{{#unless isNone}}
|
|
34507
34635
|
using SmartStack.Application.Common.Interfaces.Tenants;
|
|
34636
|
+
{{/unless}}
|
|
34508
34637
|
using SmartStack.Application.Common.Interfaces.Persistence;
|
|
34509
34638
|
|
|
34510
34639
|
namespace {{implNamespace}};
|
|
34511
34640
|
|
|
34512
34641
|
/// <summary>
|
|
34513
34642
|
/// Service implementation for {{name}} operations.
|
|
34643
|
+
{{#if isStrict}}
|
|
34514
34644
|
/// IMPORTANT: All queries MUST filter by tenant ID for multi-tenant isolation.
|
|
34645
|
+
{{else if isOptional}}
|
|
34646
|
+
/// Cross-tenant entity: EF global query filter includes shared (TenantId=null) + current tenant data.
|
|
34647
|
+
{{else if isScoped}}
|
|
34648
|
+
/// Scoped entity: EF global query filter includes shared + current tenant data. Scope controls visibility.
|
|
34649
|
+
{{/if}}
|
|
34515
34650
|
/// IMPORTANT: GetAllAsync MUST support search parameter for frontend EntityLookup component.
|
|
34516
34651
|
/// </summary>
|
|
34517
34652
|
public class {{name}}Service : I{{name}}Service
|
|
34518
34653
|
{
|
|
34519
34654
|
private readonly IExtensionsDbContext _db;
|
|
34520
34655
|
private readonly ICurrentUserService _currentUser;
|
|
34656
|
+
{{#unless isNone}}
|
|
34521
34657
|
private readonly ICurrentTenantService _currentTenant;
|
|
34658
|
+
{{/unless}}
|
|
34522
34659
|
private readonly ILogger<{{name}}Service> _logger;
|
|
34523
34660
|
|
|
34524
34661
|
public {{name}}Service(
|
|
34525
34662
|
IExtensionsDbContext db,
|
|
34526
34663
|
ICurrentUserService currentUser,
|
|
34664
|
+
{{#unless isNone}}
|
|
34527
34665
|
ICurrentTenantService currentTenant,
|
|
34666
|
+
{{/unless}}
|
|
34528
34667
|
ILogger<{{name}}Service> logger)
|
|
34529
34668
|
{
|
|
34530
34669
|
_db = db;
|
|
34531
34670
|
_currentUser = currentUser;
|
|
34671
|
+
{{#unless isNone}}
|
|
34532
34672
|
_currentTenant = currentTenant;
|
|
34673
|
+
{{/unless}}
|
|
34533
34674
|
_logger = logger;
|
|
34534
34675
|
}
|
|
34535
34676
|
|
|
@@ -34537,14 +34678,28 @@ public class {{name}}Service : I{{name}}Service
|
|
|
34537
34678
|
/// <inheritdoc />
|
|
34538
34679
|
public async Task<object> {{this}}(CancellationToken cancellationToken = default)
|
|
34539
34680
|
{
|
|
34681
|
+
{{#if ../isStrict}}
|
|
34540
34682
|
var tenantId = _currentTenant.TenantId
|
|
34541
|
-
?? throw new
|
|
34683
|
+
?? throw new TenantContextRequiredException();
|
|
34542
34684
|
_logger.LogInformation("Executing {{this}} for tenant {TenantId}", tenantId);
|
|
34543
34685
|
// TODO: Implement {{this}} \u2014 ALL queries must filter by tenantId
|
|
34686
|
+
{{else if ../isOptional}}
|
|
34687
|
+
// Cross-tenant: tenantId is nullable. null = shared data, Guid = tenant-specific.
|
|
34688
|
+
// EF global filter automatically includes shared + current tenant data in queries.
|
|
34689
|
+
var tenantId = _currentTenant.TenantId; // nullable \u2014 null means creating shared data
|
|
34690
|
+
_logger.LogInformation("Executing {{this}} (tenantId: {TenantId})", tenantId?.ToString() ?? "shared");
|
|
34691
|
+
// TODO: Implement {{this}}
|
|
34692
|
+
{{else if ../isScoped}}
|
|
34693
|
+
// Scoped: tenantId is nullable. Scope controls visibility (Tenant/Shared/Platform).
|
|
34694
|
+
var tenantId = _currentTenant.TenantId;
|
|
34695
|
+
_logger.LogInformation("Executing {{this}} (tenantId: {TenantId})", tenantId?.ToString() ?? "shared");
|
|
34696
|
+
// TODO: Implement {{this}}
|
|
34697
|
+
{{else}}
|
|
34698
|
+
_logger.LogInformation("Executing {{this}}");
|
|
34699
|
+
// TODO: Implement {{this}}
|
|
34700
|
+
{{/if}}
|
|
34544
34701
|
// IMPORTANT: GetAllAsync MUST accept (string? search, int page, int pageSize) parameters
|
|
34545
|
-
// to enable EntityLookup search on the frontend.
|
|
34546
|
-
// if (!string.IsNullOrWhiteSpace(search))
|
|
34547
|
-
// query = query.Where(x => x.Name.Contains(search) || x.Code.Contains(search));
|
|
34702
|
+
// to enable EntityLookup search on the frontend.
|
|
34548
34703
|
await Task.CompletedTask;
|
|
34549
34704
|
throw new NotImplementedException();
|
|
34550
34705
|
}
|
|
@@ -34555,7 +34710,7 @@ public class {{name}}Service : I{{name}}Service
|
|
|
34555
34710
|
const diTemplate = `// Add to DependencyInjection.cs or ServiceCollectionExtensions.cs:
|
|
34556
34711
|
services.AddScoped<I{{name}}Service, {{name}}Service>();
|
|
34557
34712
|
`;
|
|
34558
|
-
const context = { interfaceNamespace, implNamespace, name, methods };
|
|
34713
|
+
const context = { interfaceNamespace, implNamespace, name, methods, ...tmFlags };
|
|
34559
34714
|
const interfaceContent = import_handlebars.default.compile(interfaceTemplate)(context);
|
|
34560
34715
|
const implementationContent = import_handlebars.default.compile(implementationTemplate)(context);
|
|
34561
34716
|
const diContent = import_handlebars.default.compile(diTemplate)(context);
|
|
@@ -34583,7 +34738,9 @@ async function scaffoldEntity(name, options, structure, config2, result, dryRun
|
|
|
34583
34738
|
const hierarchy = resolveHierarchy(options?.navRoute);
|
|
34584
34739
|
const namespace = options?.namespace || (hierarchy.domainPath ? `${config2.conventions.namespaces.domain}.${hierarchy.domainPath.replace(/[\\/]/g, ".")}` : config2.conventions.namespaces.domain);
|
|
34585
34740
|
const baseEntity = options?.baseEntity;
|
|
34586
|
-
const
|
|
34741
|
+
const tenantMode = resolveTenantMode(options);
|
|
34742
|
+
const tmFlags = tenantModeToTemplateFlags(tenantMode);
|
|
34743
|
+
const isSystemEntity = tmFlags.isSystemEntity;
|
|
34587
34744
|
const tablePrefix = options?.tablePrefix || "ref_";
|
|
34588
34745
|
const schema = options?.schema || config2.conventions.schemas.platform;
|
|
34589
34746
|
const entityTemplate = `using System;
|
|
@@ -34593,23 +34750,49 @@ namespace {{namespace}};
|
|
|
34593
34750
|
|
|
34594
34751
|
/// <summary>
|
|
34595
34752
|
/// {{name}} entity{{#if baseEntity}} extending {{baseEntity}}{{/if}}
|
|
34596
|
-
{{#
|
|
34597
|
-
/// Tenant-scoped: data is isolated per tenant
|
|
34753
|
+
{{#if isStrict}}
|
|
34754
|
+
/// Tenant-scoped: data is isolated per tenant (ITenantEntity)
|
|
34755
|
+
{{else if isOptional}}
|
|
34756
|
+
/// Cross-tenant: data can be shared across tenants or tenant-specific (IOptionalTenantEntity)
|
|
34757
|
+
{{else if isScoped}}
|
|
34758
|
+
/// Scoped: data visibility controlled by EntityScope (IScopedTenantEntity)
|
|
34598
34759
|
{{else}}
|
|
34599
34760
|
/// Platform-level entity: no tenant isolation
|
|
34600
|
-
{{/
|
|
34761
|
+
{{/if}}
|
|
34601
34762
|
/// </summary>
|
|
34602
|
-
|
|
34763
|
+
{{#if isStrict}}
|
|
34764
|
+
public class {{name}} : BaseEntity, ITenantEntity, IAuditableEntity
|
|
34765
|
+
{{else if isOptional}}
|
|
34766
|
+
public class {{name}} : BaseEntity, IOptionalTenantEntity, IAuditableEntity
|
|
34767
|
+
{{else if isScoped}}
|
|
34768
|
+
public class {{name}} : BaseEntity, IScopedTenantEntity, IAuditableEntity
|
|
34769
|
+
{{else}}
|
|
34770
|
+
public class {{name}} : BaseEntity, IAuditableEntity
|
|
34771
|
+
{{/if}}
|
|
34603
34772
|
{
|
|
34604
|
-
{{#
|
|
34773
|
+
{{#if hasTenantId}}
|
|
34605
34774
|
// === MULTI-TENANT ===
|
|
34606
34775
|
|
|
34776
|
+
{{#if isNullableTenant}}
|
|
34777
|
+
/// <summary>
|
|
34778
|
+
/// Tenant identifier \u2014 null means shared across all tenants
|
|
34779
|
+
/// </summary>
|
|
34780
|
+
public Guid? TenantId { get; private set; }
|
|
34781
|
+
{{else}}
|
|
34607
34782
|
/// <summary>
|
|
34608
34783
|
/// Tenant identifier for multi-tenant isolation (required)
|
|
34609
34784
|
/// </summary>
|
|
34610
34785
|
public Guid TenantId { get; private set; }
|
|
34786
|
+
{{/if}}
|
|
34611
34787
|
|
|
34612
|
-
{{/
|
|
34788
|
+
{{/if}}
|
|
34789
|
+
{{#if hasScope}}
|
|
34790
|
+
/// <summary>
|
|
34791
|
+
/// Visibility scope: Tenant (tenant-specific), Shared (all tenants), Platform (admin only)
|
|
34792
|
+
/// </summary>
|
|
34793
|
+
public EntityScope Scope { get; private set; }
|
|
34794
|
+
|
|
34795
|
+
{{/if}}
|
|
34613
34796
|
// === AUDIT ===
|
|
34614
34797
|
|
|
34615
34798
|
/// <summary>User who created this entity</summary>
|
|
@@ -34643,7 +34826,7 @@ public class {{name}} : BaseEntity{{#unless isSystemEntity}}, ITenantEntity, IAu
|
|
|
34643
34826
|
/// <summary>
|
|
34644
34827
|
/// Factory method to create a new {{name}}
|
|
34645
34828
|
/// </summary>
|
|
34646
|
-
{{#
|
|
34829
|
+
{{#if isStrict}}
|
|
34647
34830
|
/// <param name="tenantId">Required tenant identifier</param>
|
|
34648
34831
|
public static {{name}} Create(
|
|
34649
34832
|
Guid tenantId)
|
|
@@ -34658,6 +34841,37 @@ public class {{name}} : BaseEntity{{#unless isSystemEntity}}, ITenantEntity, IAu
|
|
|
34658
34841
|
CreatedAt = DateTime.UtcNow
|
|
34659
34842
|
};
|
|
34660
34843
|
}
|
|
34844
|
+
{{else if isOptional}}
|
|
34845
|
+
/// <param name="tenantId">Tenant identifier \u2014 null for shared (cross-tenant) data</param>
|
|
34846
|
+
public static {{name}} Create(
|
|
34847
|
+
Guid? tenantId = null)
|
|
34848
|
+
{
|
|
34849
|
+
return new {{name}}
|
|
34850
|
+
{
|
|
34851
|
+
Id = Guid.NewGuid(),
|
|
34852
|
+
TenantId = tenantId,
|
|
34853
|
+
CreatedAt = DateTime.UtcNow
|
|
34854
|
+
};
|
|
34855
|
+
}
|
|
34856
|
+
{{else if isScoped}}
|
|
34857
|
+
/// <param name="tenantId">Tenant identifier \u2014 null for shared/platform data</param>
|
|
34858
|
+
/// <param name="scope">Visibility scope (default: Tenant)</param>
|
|
34859
|
+
public static {{name}} Create(
|
|
34860
|
+
Guid? tenantId = null,
|
|
34861
|
+
EntityScope scope = EntityScope.Tenant)
|
|
34862
|
+
{
|
|
34863
|
+
// Validate scope-tenantId consistency
|
|
34864
|
+
if (scope == EntityScope.Tenant && tenantId == null)
|
|
34865
|
+
throw new ArgumentException("TenantId is required when scope is Tenant", nameof(tenantId));
|
|
34866
|
+
|
|
34867
|
+
return new {{name}}
|
|
34868
|
+
{
|
|
34869
|
+
Id = Guid.NewGuid(),
|
|
34870
|
+
TenantId = tenantId,
|
|
34871
|
+
Scope = scope,
|
|
34872
|
+
CreatedAt = DateTime.UtcNow
|
|
34873
|
+
};
|
|
34874
|
+
}
|
|
34661
34875
|
{{else}}
|
|
34662
34876
|
public static {{name}} Create()
|
|
34663
34877
|
{
|
|
@@ -34667,7 +34881,7 @@ public class {{name}} : BaseEntity{{#unless isSystemEntity}}, ITenantEntity, IAu
|
|
|
34667
34881
|
CreatedAt = DateTime.UtcNow
|
|
34668
34882
|
};
|
|
34669
34883
|
}
|
|
34670
|
-
{{/
|
|
34884
|
+
{{/if}}
|
|
34671
34885
|
|
|
34672
34886
|
/// <summary>
|
|
34673
34887
|
/// Update the entity
|
|
@@ -34695,13 +34909,32 @@ public class {{name}}Configuration : IEntityTypeConfiguration<{{name}}>
|
|
|
34695
34909
|
|
|
34696
34910
|
builder.HasKey(e => e.Id);
|
|
34697
34911
|
|
|
34698
|
-
{{#
|
|
34699
|
-
// Multi-tenant
|
|
34912
|
+
{{#if isStrict}}
|
|
34913
|
+
// Multi-tenant (strict: TenantId required)
|
|
34700
34914
|
builder.Property(e => e.TenantId).IsRequired();
|
|
34701
34915
|
builder.HasIndex(e => e.TenantId)
|
|
34702
34916
|
.HasDatabaseName("IX_{{tablePrefix}}{{name}}s_TenantId");
|
|
34703
34917
|
|
|
34704
|
-
{{
|
|
34918
|
+
{{else if isOptional}}
|
|
34919
|
+
// Multi-tenant (optional: TenantId nullable \u2014 null = shared across tenants)
|
|
34920
|
+
builder.Property(e => e.TenantId).IsRequired(false);
|
|
34921
|
+
builder.HasIndex(e => e.TenantId)
|
|
34922
|
+
.HasDatabaseName("IX_{{tablePrefix}}{{name}}s_TenantId");
|
|
34923
|
+
|
|
34924
|
+
{{else if isScoped}}
|
|
34925
|
+
// Multi-tenant (scoped: TenantId nullable + EntityScope)
|
|
34926
|
+
builder.Property(e => e.TenantId).IsRequired(false);
|
|
34927
|
+
builder.HasIndex(e => e.TenantId)
|
|
34928
|
+
.HasDatabaseName("IX_{{tablePrefix}}{{name}}s_TenantId");
|
|
34929
|
+
|
|
34930
|
+
builder.Property(e => e.Scope)
|
|
34931
|
+
.IsRequired()
|
|
34932
|
+
.HasConversion<string>()
|
|
34933
|
+
.HasMaxLength(20);
|
|
34934
|
+
builder.HasIndex(e => new { e.TenantId, e.Scope })
|
|
34935
|
+
.HasDatabaseName("IX_{{tablePrefix}}{{name}}s_TenantId_Scope");
|
|
34936
|
+
|
|
34937
|
+
{{/if}}
|
|
34705
34938
|
// Audit fields (from IAuditableEntity)
|
|
34706
34939
|
builder.Property(e => e.CreatedBy).HasMaxLength(256);
|
|
34707
34940
|
builder.Property(e => e.UpdatedBy).HasMaxLength(256);
|
|
@@ -34725,7 +34958,7 @@ public class {{name}}Configuration : IEntityTypeConfiguration<{{name}}>
|
|
|
34725
34958
|
namespace,
|
|
34726
34959
|
name,
|
|
34727
34960
|
baseEntity,
|
|
34728
|
-
|
|
34961
|
+
...tmFlags,
|
|
34729
34962
|
tablePrefix,
|
|
34730
34963
|
schema,
|
|
34731
34964
|
infrastructureNamespace: config2.conventions.namespaces.infrastructure,
|
|
@@ -34765,7 +34998,9 @@ public class {{name}}Configuration : IEntityTypeConfiguration<{{name}}>
|
|
|
34765
34998
|
result.instructions.push("");
|
|
34766
34999
|
result.instructions.push("BaseEntity fields (inherited):");
|
|
34767
35000
|
result.instructions.push(`- Id (Guid), CreatedAt (DateTime), UpdatedAt (DateTime?)`);
|
|
34768
|
-
result.instructions.push(
|
|
35001
|
+
if (tenantMode === "strict") result.instructions.push("- TenantId (Guid) \u2014 from ITenantEntity");
|
|
35002
|
+
else if (tenantMode === "optional") result.instructions.push("- TenantId (Guid?) \u2014 from IOptionalTenantEntity (null = shared)");
|
|
35003
|
+
else if (tenantMode === "scoped") result.instructions.push("- TenantId (Guid?) + Scope (EntityScope) \u2014 from IScopedTenantEntity");
|
|
34769
35004
|
result.instructions.push("- CreatedBy, UpdatedBy (string?) \u2014 from IAuditableEntity");
|
|
34770
35005
|
result.instructions.push("- Add your own business properties (Code, Name, etc.) as needed");
|
|
34771
35006
|
if (options?.withSeedData) {
|
|
@@ -34794,7 +35029,8 @@ async function scaffoldSeedData(name, options, structure, config2, result, dryRu
|
|
|
34794
35029
|
"loc_": "Localization"
|
|
34795
35030
|
};
|
|
34796
35031
|
const domain = options?.seedDataDomain || domainMap[tablePrefix] || "Reference";
|
|
34797
|
-
const
|
|
35032
|
+
const tenantMode = resolveTenantMode(options);
|
|
35033
|
+
const tmFlags = tenantModeToTemplateFlags(tenantMode);
|
|
34798
35034
|
const seedDataTemplate = `using SmartStack.Domain.{{domainNamespace}};
|
|
34799
35035
|
|
|
34800
35036
|
namespace SmartStack.Infrastructure.Persistence.Seeding.Data.{{domain}};
|
|
@@ -34836,9 +35072,14 @@ public static class {{name}}SeedData
|
|
|
34836
35072
|
new
|
|
34837
35073
|
{
|
|
34838
35074
|
Id = ExampleId,
|
|
34839
|
-
{{#
|
|
34840
|
-
TenantId =
|
|
34841
|
-
{{
|
|
35075
|
+
{{#if isStrict}}
|
|
35076
|
+
TenantId = SeedConstants.DefaultTenantId, // Seed data tenant-specific
|
|
35077
|
+
{{else if isOptional}}
|
|
35078
|
+
TenantId = (Guid?)null, // Shared cross-tenant seed data
|
|
35079
|
+
{{else if isScoped}}
|
|
35080
|
+
TenantId = (Guid?)null, // Shared seed data
|
|
35081
|
+
Scope = EntityScope.Shared,
|
|
35082
|
+
{{/if}}
|
|
34842
35083
|
// TODO: Ajouter les proprietes specifiques
|
|
34843
35084
|
CreatedAt = seedDate
|
|
34844
35085
|
}
|
|
@@ -34855,7 +35096,7 @@ public static class {{name}}SeedData
|
|
|
34855
35096
|
name,
|
|
34856
35097
|
domain,
|
|
34857
35098
|
domainNamespace: domain,
|
|
34858
|
-
|
|
35099
|
+
...tmFlags,
|
|
34859
35100
|
exampleGuid
|
|
34860
35101
|
};
|
|
34861
35102
|
const seedDataContent = import_handlebars.default.compile(seedDataTemplate)(context);
|
|
@@ -35380,7 +35621,9 @@ export function use{{name}}(options: Use{{name}}Options = {}) {
|
|
|
35380
35621
|
result.instructions.push("```");
|
|
35381
35622
|
}
|
|
35382
35623
|
async function scaffoldTest(name, options, structure, config2, result, dryRun = false) {
|
|
35383
|
-
const
|
|
35624
|
+
const tenantMode = resolveTenantMode(options);
|
|
35625
|
+
const tmFlags = tenantModeToTemplateFlags(tenantMode);
|
|
35626
|
+
const isSystemEntity = tmFlags.isSystemEntity;
|
|
35384
35627
|
const serviceTestTemplate2 = `using System;
|
|
35385
35628
|
using System.Threading;
|
|
35386
35629
|
using System.Threading.Tasks;
|
|
@@ -35486,7 +35729,7 @@ public class {{name}}ServiceTests
|
|
|
35486
35729
|
`;
|
|
35487
35730
|
const context = {
|
|
35488
35731
|
name,
|
|
35489
|
-
|
|
35732
|
+
...tmFlags
|
|
35490
35733
|
};
|
|
35491
35734
|
const testContent = import_handlebars.default.compile(serviceTestTemplate2)(context);
|
|
35492
35735
|
const testsPath = structure.application ? path10.join(path10.dirname(structure.application), `${path10.basename(structure.application)}.Tests`, "Services") : path10.join(config2.smartstack.projectPath, "Application.Tests", "Services");
|
|
@@ -35507,7 +35750,10 @@ public class {{name}}ServiceTests
|
|
|
35507
35750
|
async function scaffoldDtos(name, options, structure, config2, result, dryRun = false) {
|
|
35508
35751
|
const hierarchy = resolveHierarchy(options?.navRoute);
|
|
35509
35752
|
const namespace = options?.namespace || (hierarchy.domainPath ? `${config2.conventions.namespaces.application}.${hierarchy.domainPath.replace(/[\\/]/g, ".")}.DTOs` : `${config2.conventions.namespaces.application}.DTOs`);
|
|
35510
|
-
const
|
|
35753
|
+
const tenantMode = resolveTenantMode(options);
|
|
35754
|
+
const tmFlags = tenantModeToTemplateFlags(tenantMode);
|
|
35755
|
+
const codePattern = options?.codePattern;
|
|
35756
|
+
const isAutoCode = codePattern && codePattern.strategy && codePattern.strategy !== "manual";
|
|
35511
35757
|
const properties = options?.entityProperties || [
|
|
35512
35758
|
{ name: "Name", type: "string", required: true, maxLength: 200 },
|
|
35513
35759
|
{ name: "Description", type: "string?", required: false, maxLength: 500 }
|
|
@@ -35524,11 +35770,22 @@ public record {{name}}ResponseDto
|
|
|
35524
35770
|
/// <summary>Unique identifier</summary>
|
|
35525
35771
|
public Guid Id { get; init; }
|
|
35526
35772
|
|
|
35527
|
-
{{#
|
|
35773
|
+
{{#if isStrict}}
|
|
35528
35774
|
/// <summary>Tenant identifier</summary>
|
|
35529
35775
|
public Guid TenantId { get; init; }
|
|
35530
35776
|
|
|
35531
|
-
{{
|
|
35777
|
+
{{else if isOptional}}
|
|
35778
|
+
/// <summary>Tenant identifier \u2014 null for shared (cross-tenant) data</summary>
|
|
35779
|
+
public Guid? TenantId { get; init; }
|
|
35780
|
+
|
|
35781
|
+
{{else if isScoped}}
|
|
35782
|
+
/// <summary>Tenant identifier \u2014 null for shared/platform data</summary>
|
|
35783
|
+
public Guid? TenantId { get; init; }
|
|
35784
|
+
|
|
35785
|
+
/// <summary>Visibility scope</summary>
|
|
35786
|
+
public string Scope { get; init; } = "Tenant";
|
|
35787
|
+
|
|
35788
|
+
{{/if}}
|
|
35532
35789
|
/// <summary>Unique code</summary>
|
|
35533
35790
|
public string Code { get; init; } = string.Empty;
|
|
35534
35791
|
|
|
@@ -35547,7 +35804,37 @@ public record {{name}}ResponseDto
|
|
|
35547
35804
|
public string? CreatedBy { get; init; }
|
|
35548
35805
|
}
|
|
35549
35806
|
`;
|
|
35550
|
-
const createDtoTemplate = `using System;
|
|
35807
|
+
const createDtoTemplate = isAutoCode ? `using System;
|
|
35808
|
+
using System.ComponentModel.DataAnnotations;
|
|
35809
|
+
|
|
35810
|
+
namespace {{namespace}};
|
|
35811
|
+
|
|
35812
|
+
/// <summary>
|
|
35813
|
+
/// DTO for creating a new {{name}}.
|
|
35814
|
+
/// Code is auto-generated by ICodeGenerator<{{name}}> \u2014 not user-provided.
|
|
35815
|
+
/// </summary>
|
|
35816
|
+
public record Create{{name}}Dto
|
|
35817
|
+
{
|
|
35818
|
+
{{#each properties}}
|
|
35819
|
+
{{#if required}}
|
|
35820
|
+
/// <summary>{{name}} (required)</summary>
|
|
35821
|
+
[Required]
|
|
35822
|
+
{{#if maxLength}}
|
|
35823
|
+
[MaxLength({{maxLength}})]
|
|
35824
|
+
{{/if}}
|
|
35825
|
+
public {{type}} {{name}} { get; init; }{{#if (eq type "string")}} = string.Empty;{{/if}}
|
|
35826
|
+
|
|
35827
|
+
{{else}}
|
|
35828
|
+
/// <summary>{{name}} (optional)</summary>
|
|
35829
|
+
{{#if maxLength}}
|
|
35830
|
+
[MaxLength({{maxLength}})]
|
|
35831
|
+
{{/if}}
|
|
35832
|
+
public {{type}} {{name}} { get; init; }
|
|
35833
|
+
|
|
35834
|
+
{{/if}}
|
|
35835
|
+
{{/each}}
|
|
35836
|
+
}
|
|
35837
|
+
` : `using System;
|
|
35551
35838
|
using System.ComponentModel.DataAnnotations;
|
|
35552
35839
|
|
|
35553
35840
|
namespace {{namespace}};
|
|
@@ -35616,7 +35903,7 @@ public record Update{{name}}Dto
|
|
|
35616
35903
|
const context = {
|
|
35617
35904
|
namespace,
|
|
35618
35905
|
name,
|
|
35619
|
-
|
|
35906
|
+
...tmFlags,
|
|
35620
35907
|
properties
|
|
35621
35908
|
};
|
|
35622
35909
|
const responseContent = import_handlebars.default.compile(responseDtoTemplate)(context);
|
|
@@ -35640,14 +35927,49 @@ public record Update{{name}}Dto
|
|
|
35640
35927
|
result.instructions.push(`- ${name}ResponseDto: For API responses`);
|
|
35641
35928
|
result.instructions.push(`- Create${name}Dto: For POST requests`);
|
|
35642
35929
|
result.instructions.push(`- Update${name}Dto: For PUT requests`);
|
|
35930
|
+
if (isAutoCode) {
|
|
35931
|
+
const cp2 = codePattern;
|
|
35932
|
+
const strategy = cp2.strategy || "sequential";
|
|
35933
|
+
const prefix = cp2.prefix || name.toLowerCase().substring(0, 3);
|
|
35934
|
+
const digits = cp2.digits || Math.max(4, Math.ceil(Math.log10((cp2.estimatedVolume || 1e3) * 10)));
|
|
35935
|
+
const separator = cp2.separator || "-";
|
|
35936
|
+
const includeTenant = cp2.includeTenantSlug !== false;
|
|
35937
|
+
const example = includeTenant ? `{tenant}${separator}${prefix}${separator}${"0".repeat(digits - 1)}1` : `${prefix}${separator}${"0".repeat(digits - 1)}1`;
|
|
35938
|
+
result.instructions.push("");
|
|
35939
|
+
result.instructions.push(`## Code Auto-Generation (strategy: ${strategy})`);
|
|
35940
|
+
result.instructions.push(`Code is auto-generated for ${name} \u2014 removed from Create${name}Dto.`);
|
|
35941
|
+
result.instructions.push(`Example: ${example}`);
|
|
35942
|
+
result.instructions.push("");
|
|
35943
|
+
result.instructions.push("Required implementation (see references/code-generation.md):");
|
|
35944
|
+
result.instructions.push(`1. Register ICodeGenerator<${name}> in DependencyInjection.cs:`);
|
|
35945
|
+
result.instructions.push(` services.AddScoped<ICodeGenerator<${name}>>(sp =>`);
|
|
35946
|
+
result.instructions.push(` new CodeGenerator<${name}>(`);
|
|
35947
|
+
result.instructions.push(` sp.GetRequiredService<IExtensionsDbContext>(),`);
|
|
35948
|
+
result.instructions.push(` sp.GetRequiredService<ICurrentTenantService>(),`);
|
|
35949
|
+
result.instructions.push(` new CodePatternConfig(CodeStrategy.${strategy.split("-").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("")}, "${prefix}", ${includeTenant}, "${separator}", ${cp2.estimatedVolume || 1e3}, ${digits}),`);
|
|
35950
|
+
result.instructions.push(` sp.GetRequiredService<ILogger<CodeGenerator<${name}>>>()));`);
|
|
35951
|
+
result.instructions.push("");
|
|
35952
|
+
result.instructions.push(`2. Inject ICodeGenerator<${name}> in ${name}Service constructor`);
|
|
35953
|
+
result.instructions.push(`3. Call _codeGenerator.NextCodeAsync(ct) in CreateAsync BEFORE entity creation`);
|
|
35954
|
+
result.instructions.push(`4. Code field is NOT in Create${name}Dto \u2014 auto-generated, not user-provided`);
|
|
35955
|
+
}
|
|
35643
35956
|
}
|
|
35644
35957
|
async function scaffoldValidator(name, options, structure, config2, result, dryRun = false) {
|
|
35645
35958
|
const hierarchy = resolveHierarchy(options?.navRoute);
|
|
35646
35959
|
const namespace = options?.namespace || (hierarchy.domainPath ? `${config2.conventions.namespaces.application}.${hierarchy.domainPath.replace(/[\\/]/g, ".")}.Validators` : `${config2.conventions.namespaces.application}.Validators`);
|
|
35960
|
+
const codePatternValidator = options?.codePattern;
|
|
35961
|
+
const isAutoCodeValidator = codePatternValidator && codePatternValidator.strategy && codePatternValidator.strategy !== "manual";
|
|
35647
35962
|
const properties = options?.entityProperties || [
|
|
35648
35963
|
{ name: "Name", type: "string", required: true, maxLength: 200 },
|
|
35649
35964
|
{ name: "Description", type: "string?", required: false, maxLength: 500 }
|
|
35650
35965
|
];
|
|
35966
|
+
const codeValidationRules = isAutoCodeValidator ? ` // Code is auto-generated by ICodeGenerator<{{name}}> \u2014 no validation needed in CreateDto
|
|
35967
|
+
` : ` RuleFor(x => x.Code)
|
|
35968
|
+
.NotEmpty().WithMessage("Code is required")
|
|
35969
|
+
.MaximumLength(100).WithMessage("Code must not exceed 100 characters")
|
|
35970
|
+
.Matches("^[a-z0-9_-]+$").WithMessage("Code must be lowercase alphanumeric with underscores and hyphens");
|
|
35971
|
+
|
|
35972
|
+
`;
|
|
35651
35973
|
const createValidatorTemplate = `using FluentValidation;
|
|
35652
35974
|
using ${config2.conventions.namespaces.application}.DTOs;
|
|
35653
35975
|
|
|
@@ -35660,11 +35982,7 @@ public class Create{{name}}DtoValidator : AbstractValidator<Create{{name}}Dto>
|
|
|
35660
35982
|
{
|
|
35661
35983
|
public Create{{name}}DtoValidator()
|
|
35662
35984
|
{
|
|
35663
|
-
|
|
35664
|
-
.NotEmpty().WithMessage("Code is required")
|
|
35665
|
-
.MaximumLength(100).WithMessage("Code must not exceed 100 characters")
|
|
35666
|
-
.Matches("^[a-z0-9_]+$").WithMessage("Code must be lowercase alphanumeric with underscores");
|
|
35667
|
-
|
|
35985
|
+
${codeValidationRules}
|
|
35668
35986
|
{{#each properties}}
|
|
35669
35987
|
{{#if required}}
|
|
35670
35988
|
RuleFor(x => x.{{name}})
|
|
@@ -35738,7 +36056,8 @@ public class Update{{name}}DtoValidator : AbstractValidator<Update{{name}}Dto>
|
|
|
35738
36056
|
}
|
|
35739
36057
|
async function scaffoldRepository(name, options, structure, config2, result, dryRun = false) {
|
|
35740
36058
|
const hierarchy = resolveHierarchy(options?.navRoute);
|
|
35741
|
-
const
|
|
36059
|
+
const tenantMode = resolveTenantMode(options);
|
|
36060
|
+
const tmFlags = tenantModeToTemplateFlags(tenantMode);
|
|
35742
36061
|
const schema = options?.schema || config2.conventions.schemas.platform;
|
|
35743
36062
|
const dbContextName = schema === "extensions" ? "ExtensionsDbContext" : "CoreDbContext";
|
|
35744
36063
|
const interfaceTemplate = `using System;
|
|
@@ -35757,14 +36076,43 @@ public interface I{{name}}Repository
|
|
|
35757
36076
|
/// <summary>Get entity by ID</summary>
|
|
35758
36077
|
Task<{{name}}?> GetByIdAsync(Guid id, CancellationToken ct = default);
|
|
35759
36078
|
|
|
36079
|
+
{{#if isStrict}}
|
|
36080
|
+
/// <summary>Get entity by code (tenant-scoped)</summary>
|
|
36081
|
+
Task<{{name}}?> GetByCodeAsync(Guid tenantId, string code, CancellationToken ct = default);
|
|
36082
|
+
|
|
36083
|
+
/// <summary>Get all entities for tenant</summary>
|
|
36084
|
+
Task<IReadOnlyList<{{name}}>> GetAllAsync(Guid tenantId, CancellationToken ct = default);
|
|
36085
|
+
|
|
36086
|
+
/// <summary>Check if code exists in tenant</summary>
|
|
36087
|
+
Task<bool> ExistsAsync(Guid tenantId, string code, CancellationToken ct = default);
|
|
36088
|
+
{{else if isOptional}}
|
|
36089
|
+
/// <summary>Get entity by code (tenant-scoped or shared)</summary>
|
|
36090
|
+
Task<{{name}}?> GetByCodeAsync(Guid? tenantId, string code, CancellationToken ct = default);
|
|
36091
|
+
|
|
36092
|
+
/// <summary>Get all entities (includes shared + current tenant via EF global filter)</summary>
|
|
36093
|
+
Task<IReadOnlyList<{{name}}>> GetAllAsync(CancellationToken ct = default);
|
|
36094
|
+
|
|
36095
|
+
/// <summary>Check if code exists (tenant-scoped or shared)</summary>
|
|
36096
|
+
Task<bool> ExistsAsync(Guid? tenantId, string code, CancellationToken ct = default);
|
|
36097
|
+
{{else if isScoped}}
|
|
36098
|
+
/// <summary>Get entity by code (scoped)</summary>
|
|
36099
|
+
Task<{{name}}?> GetByCodeAsync(Guid? tenantId, string code, CancellationToken ct = default);
|
|
36100
|
+
|
|
36101
|
+
/// <summary>Get all entities (includes shared + current tenant via EF global filter)</summary>
|
|
36102
|
+
Task<IReadOnlyList<{{name}}>> GetAllAsync(CancellationToken ct = default);
|
|
36103
|
+
|
|
36104
|
+
/// <summary>Check if code exists (scoped)</summary>
|
|
36105
|
+
Task<bool> ExistsAsync(Guid? tenantId, string code, CancellationToken ct = default);
|
|
36106
|
+
{{else}}
|
|
35760
36107
|
/// <summary>Get entity by code</summary>
|
|
35761
|
-
Task<{{name}}?> GetByCodeAsync(
|
|
36108
|
+
Task<{{name}}?> GetByCodeAsync(string code, CancellationToken ct = default);
|
|
35762
36109
|
|
|
35763
|
-
/// <summary>Get all entities
|
|
35764
|
-
Task<IReadOnlyList<{{name}}>> GetAllAsync(
|
|
36110
|
+
/// <summary>Get all entities</summary>
|
|
36111
|
+
Task<IReadOnlyList<{{name}}>> GetAllAsync(CancellationToken ct = default);
|
|
35765
36112
|
|
|
35766
|
-
/// <summary>Check if code exists
|
|
35767
|
-
Task<bool> ExistsAsync(
|
|
36113
|
+
/// <summary>Check if code exists</summary>
|
|
36114
|
+
Task<bool> ExistsAsync(string code, CancellationToken ct = default);
|
|
36115
|
+
{{/if}}
|
|
35768
36116
|
|
|
35769
36117
|
/// <summary>Add new entity</summary>
|
|
35770
36118
|
Task<{{name}}> AddAsync({{name}} entity, CancellationToken ct = default);
|
|
@@ -35806,29 +36154,108 @@ public class {{name}}Repository : I{{name}}Repository
|
|
|
35806
36154
|
.FirstOrDefaultAsync(e => e.Id == id, ct);
|
|
35807
36155
|
}
|
|
35808
36156
|
|
|
36157
|
+
{{#if isStrict}}
|
|
35809
36158
|
/// <inheritdoc />
|
|
35810
|
-
public async Task<{{name}}?> GetByCodeAsync(
|
|
36159
|
+
public async Task<{{name}}?> GetByCodeAsync(Guid tenantId, string code, CancellationToken ct = default)
|
|
35811
36160
|
{
|
|
35812
36161
|
return await _context.{{name}}s
|
|
35813
|
-
.FirstOrDefaultAsync(e =>
|
|
36162
|
+
.FirstOrDefaultAsync(e => e.TenantId == tenantId && e.Code == code.ToLowerInvariant(), ct);
|
|
35814
36163
|
}
|
|
35815
36164
|
|
|
35816
36165
|
/// <inheritdoc />
|
|
35817
|
-
public async Task<IReadOnlyList<{{name}}>> GetAllAsync(
|
|
36166
|
+
public async Task<IReadOnlyList<{{name}}>> GetAllAsync(Guid tenantId, CancellationToken ct = default)
|
|
35818
36167
|
{
|
|
35819
36168
|
return await _context.{{name}}s
|
|
35820
|
-
|
|
36169
|
+
.Where(e => e.TenantId == tenantId)
|
|
35821
36170
|
.OrderBy(e => e.Code)
|
|
35822
36171
|
.ToListAsync(ct);
|
|
35823
36172
|
}
|
|
35824
36173
|
|
|
35825
36174
|
/// <inheritdoc />
|
|
35826
|
-
public async Task<bool> ExistsAsync(
|
|
36175
|
+
public async Task<bool> ExistsAsync(Guid tenantId, string code, CancellationToken ct = default)
|
|
35827
36176
|
{
|
|
35828
36177
|
return await _context.{{name}}s
|
|
35829
|
-
.AnyAsync(e =>
|
|
36178
|
+
.AnyAsync(e => e.TenantId == tenantId && e.Code == code.ToLowerInvariant(), ct);
|
|
36179
|
+
}
|
|
36180
|
+
{{else if isOptional}}
|
|
36181
|
+
/// <inheritdoc />
|
|
36182
|
+
public async Task<{{name}}?> GetByCodeAsync(Guid? tenantId, string code, CancellationToken ct = default)
|
|
36183
|
+
{
|
|
36184
|
+
// For optional tenant: match exact tenantId (null = shared, Guid = tenant-specific)
|
|
36185
|
+
return await _context.{{name}}s
|
|
36186
|
+
.FirstOrDefaultAsync(e =>
|
|
36187
|
+
(tenantId == null ? e.TenantId == null : e.TenantId == tenantId)
|
|
36188
|
+
&& e.Code == code.ToLowerInvariant(), ct);
|
|
35830
36189
|
}
|
|
35831
36190
|
|
|
36191
|
+
/// <inheritdoc />
|
|
36192
|
+
public async Task<IReadOnlyList<{{name}}>> GetAllAsync(CancellationToken ct = default)
|
|
36193
|
+
{
|
|
36194
|
+
// EF global query filter automatically includes shared (null) + current tenant
|
|
36195
|
+
return await _context.{{name}}s
|
|
36196
|
+
.OrderBy(e => e.Code)
|
|
36197
|
+
.ToListAsync(ct);
|
|
36198
|
+
}
|
|
36199
|
+
|
|
36200
|
+
/// <inheritdoc />
|
|
36201
|
+
public async Task<bool> ExistsAsync(Guid? tenantId, string code, CancellationToken ct = default)
|
|
36202
|
+
{
|
|
36203
|
+
return await _context.{{name}}s
|
|
36204
|
+
.AnyAsync(e =>
|
|
36205
|
+
(tenantId == null ? e.TenantId == null : e.TenantId == tenantId)
|
|
36206
|
+
&& e.Code == code.ToLowerInvariant(), ct);
|
|
36207
|
+
}
|
|
36208
|
+
{{else if isScoped}}
|
|
36209
|
+
/// <inheritdoc />
|
|
36210
|
+
public async Task<{{name}}?> GetByCodeAsync(Guid? tenantId, string code, CancellationToken ct = default)
|
|
36211
|
+
{
|
|
36212
|
+
return await _context.{{name}}s
|
|
36213
|
+
.FirstOrDefaultAsync(e =>
|
|
36214
|
+
(tenantId == null ? e.TenantId == null : e.TenantId == tenantId)
|
|
36215
|
+
&& e.Code == code.ToLowerInvariant(), ct);
|
|
36216
|
+
}
|
|
36217
|
+
|
|
36218
|
+
/// <inheritdoc />
|
|
36219
|
+
public async Task<IReadOnlyList<{{name}}>> GetAllAsync(CancellationToken ct = default)
|
|
36220
|
+
{
|
|
36221
|
+
// EF global query filter automatically includes shared (null) + current tenant
|
|
36222
|
+
return await _context.{{name}}s
|
|
36223
|
+
.OrderBy(e => e.Code)
|
|
36224
|
+
.ToListAsync(ct);
|
|
36225
|
+
}
|
|
36226
|
+
|
|
36227
|
+
/// <inheritdoc />
|
|
36228
|
+
public async Task<bool> ExistsAsync(Guid? tenantId, string code, CancellationToken ct = default)
|
|
36229
|
+
{
|
|
36230
|
+
return await _context.{{name}}s
|
|
36231
|
+
.AnyAsync(e =>
|
|
36232
|
+
(tenantId == null ? e.TenantId == null : e.TenantId == tenantId)
|
|
36233
|
+
&& e.Code == code.ToLowerInvariant(), ct);
|
|
36234
|
+
}
|
|
36235
|
+
{{else}}
|
|
36236
|
+
/// <inheritdoc />
|
|
36237
|
+
public async Task<{{name}}?> GetByCodeAsync(string code, CancellationToken ct = default)
|
|
36238
|
+
{
|
|
36239
|
+
return await _context.{{name}}s
|
|
36240
|
+
.FirstOrDefaultAsync(e => e.Code == code.ToLowerInvariant(), ct);
|
|
36241
|
+
}
|
|
36242
|
+
|
|
36243
|
+
/// <inheritdoc />
|
|
36244
|
+
public async Task<IReadOnlyList<{{name}}>> GetAllAsync(CancellationToken ct = default)
|
|
36245
|
+
{
|
|
36246
|
+
return await _context.{{name}}s
|
|
36247
|
+
.OrderBy(e => e.Code)
|
|
36248
|
+
.ToListAsync(ct);
|
|
36249
|
+
}
|
|
36250
|
+
|
|
36251
|
+
/// <inheritdoc />
|
|
36252
|
+
public async Task<bool> ExistsAsync(string code, CancellationToken ct = default)
|
|
36253
|
+
{
|
|
36254
|
+
return await _context.{{name}}s
|
|
36255
|
+
.AnyAsync(e => e.Code == code.ToLowerInvariant(), ct);
|
|
36256
|
+
}
|
|
36257
|
+
{{/if}}
|
|
36258
|
+
|
|
35832
36259
|
/// <inheritdoc />
|
|
35833
36260
|
public async Task<{{name}}> AddAsync({{name}} entity, CancellationToken ct = default)
|
|
35834
36261
|
{
|
|
@@ -35854,7 +36281,7 @@ public class {{name}}Repository : I{{name}}Repository
|
|
|
35854
36281
|
`;
|
|
35855
36282
|
const context = {
|
|
35856
36283
|
name,
|
|
35857
|
-
|
|
36284
|
+
...tmFlags,
|
|
35858
36285
|
dbContextName
|
|
35859
36286
|
};
|
|
35860
36287
|
const interfaceContent = import_handlebars.default.compile(interfaceTemplate)(context);
|
|
@@ -35981,7 +36408,12 @@ var init_scaffold_extension = __esm({
|
|
|
35981
36408
|
},
|
|
35982
36409
|
isSystemEntity: {
|
|
35983
36410
|
type: "boolean",
|
|
35984
|
-
description: "If true, creates a system entity without TenantId
|
|
36411
|
+
description: "[DEPRECATED: use tenantMode] If true, creates a system entity without TenantId"
|
|
36412
|
+
},
|
|
36413
|
+
tenantMode: {
|
|
36414
|
+
type: "string",
|
|
36415
|
+
enum: ["strict", "optional", "scoped", "none"],
|
|
36416
|
+
description: "Tenant isolation mode: strict (ITenantEntity, default), optional (IOptionalTenantEntity, cross-tenant), scoped (IScopedTenantEntity with EntityScope), none (no tenant)"
|
|
35985
36417
|
},
|
|
35986
36418
|
tablePrefix: {
|
|
35987
36419
|
type: "string",
|
|
@@ -36061,6 +36493,22 @@ var init_scaffold_extension = __esm({
|
|
|
36061
36493
|
type: "string",
|
|
36062
36494
|
enum: ["ancestors", "descendants", "both"],
|
|
36063
36495
|
description: "Direction for hierarchy traversal function (default: both)"
|
|
36496
|
+
},
|
|
36497
|
+
codePattern: {
|
|
36498
|
+
type: "object",
|
|
36499
|
+
description: 'Code auto-generation pattern for this entity. When strategy != "manual", Code is auto-generated and removed from CreateDto.',
|
|
36500
|
+
properties: {
|
|
36501
|
+
strategy: {
|
|
36502
|
+
type: "string",
|
|
36503
|
+
enum: ["sequential", "timestamp-daily", "timestamp-minute", "year-sequential", "uuid-short", "manual"],
|
|
36504
|
+
description: "Code generation strategy"
|
|
36505
|
+
},
|
|
36506
|
+
prefix: { type: "string", description: "Entity prefix (2-6 lowercase letters, e.g., emp, inv)" },
|
|
36507
|
+
includeTenantSlug: { type: "boolean", description: "Include tenant slug in code (default: true)" },
|
|
36508
|
+
separator: { type: "string", enum: ["-", "_"], description: "Segment separator (default: -)" },
|
|
36509
|
+
estimatedVolume: { type: "number", description: "Estimated records per tenant (used for digit calculation via x10 rule)" },
|
|
36510
|
+
digits: { type: "number", description: "Digit count for sequential part (default: auto-calculated from volume)" }
|
|
36511
|
+
}
|
|
36064
36512
|
}
|
|
36065
36513
|
}
|
|
36066
36514
|
}
|
|
@@ -52883,6 +53331,8 @@ async function handleScaffoldTests(args, config2) {
|
|
|
52883
53331
|
files: [],
|
|
52884
53332
|
instructions: []
|
|
52885
53333
|
};
|
|
53334
|
+
const tenantMode = resolveTenantMode(input.options);
|
|
53335
|
+
const tmFlags = tenantModeToTemplateFlags(tenantMode);
|
|
52886
53336
|
const options = {
|
|
52887
53337
|
includeEdgeCases: input.options?.includeEdgeCases ?? true,
|
|
52888
53338
|
includeTenantIsolation: input.options?.includeTenantIsolation ?? true,
|
|
@@ -52890,7 +53340,8 @@ async function handleScaffoldTests(args, config2) {
|
|
|
52890
53340
|
includeAudit: input.options?.includeAudit ?? true,
|
|
52891
53341
|
includeValidation: input.options?.includeValidation ?? true,
|
|
52892
53342
|
includeAuthorization: input.options?.includeAuthorization ?? false,
|
|
52893
|
-
isSystemEntity:
|
|
53343
|
+
isSystemEntity: tmFlags.isSystemEntity,
|
|
53344
|
+
...tmFlags
|
|
52894
53345
|
};
|
|
52895
53346
|
const testTypes = input.testTypes || ["unit"];
|
|
52896
53347
|
try {
|
|
@@ -53249,7 +53700,12 @@ var init_scaffold_tests = __esm({
|
|
|
53249
53700
|
isSystemEntity: {
|
|
53250
53701
|
type: "boolean",
|
|
53251
53702
|
default: false,
|
|
53252
|
-
description: "If true, entity has no TenantId"
|
|
53703
|
+
description: "[DEPRECATED: use tenantMode] If true, entity has no TenantId"
|
|
53704
|
+
},
|
|
53705
|
+
tenantMode: {
|
|
53706
|
+
type: "string",
|
|
53707
|
+
enum: ["strict", "optional", "scoped", "none"],
|
|
53708
|
+
description: "Tenant isolation mode: strict (default), optional (cross-tenant), scoped (with EntityScope), none (no tenant)"
|
|
53253
53709
|
},
|
|
53254
53710
|
dryRun: {
|
|
53255
53711
|
type: "boolean",
|
|
@@ -56595,9 +57051,8 @@ import type {
|
|
|
56595
57051
|
${name},
|
|
56596
57052
|
${name}CreateRequest,
|
|
56597
57053
|
${name}UpdateRequest,
|
|
56598
|
-
|
|
56599
|
-
|
|
56600
|
-
PaginatedResponse
|
|
57054
|
+
PaginationParams,
|
|
57055
|
+
PaginatedResult
|
|
56601
57056
|
} from '../types/${nameLower}';
|
|
56602
57057
|
|
|
56603
57058
|
const ROUTE = getRoute('${navRoute}');
|
|
@@ -56606,8 +57061,8 @@ export const ${nameLower}Api = {
|
|
|
56606
57061
|
${methods.includes("getAll") ? ` /**
|
|
56607
57062
|
* Get all ${name}s with pagination
|
|
56608
57063
|
*/
|
|
56609
|
-
async getAll(params?:
|
|
56610
|
-
const response = await apiClient.get<${name}
|
|
57064
|
+
async getAll(params?: PaginationParams): Promise<PaginatedResult<${name}>> {
|
|
57065
|
+
const response = await apiClient.get<PaginatedResult<${name}>>(ROUTE.api, { params });
|
|
56611
57066
|
return response.data;
|
|
56612
57067
|
},
|
|
56613
57068
|
` : ""}
|
|
@@ -56645,8 +57100,8 @@ ${methods.includes("delete") ? ` /**
|
|
|
56645
57100
|
${methods.includes("search") ? ` /**
|
|
56646
57101
|
* Search ${name}s
|
|
56647
57102
|
*/
|
|
56648
|
-
async search(query: string, params?:
|
|
56649
|
-
const response = await apiClient.get<${name}
|
|
57103
|
+
async search(query: string, params?: PaginationParams): Promise<PaginatedResult<${name}>> {
|
|
57104
|
+
const response = await apiClient.get<PaginatedResult<${name}>>(\`\${ROUTE.api}/search\`, {
|
|
56650
57105
|
params: { q: query, ...params }
|
|
56651
57106
|
});
|
|
56652
57107
|
return response.data;
|
|
@@ -56707,28 +57162,23 @@ export interface ${name}UpdateRequest {
|
|
|
56707
57162
|
isActive?: boolean;
|
|
56708
57163
|
}
|
|
56709
57164
|
|
|
56710
|
-
export interface
|
|
56711
|
-
items: ${name}[];
|
|
56712
|
-
totalCount: number;
|
|
56713
|
-
pageSize: number;
|
|
56714
|
-
currentPage: number;
|
|
56715
|
-
totalPages: number;
|
|
56716
|
-
}
|
|
56717
|
-
|
|
56718
|
-
export interface PaginatedRequest {
|
|
57165
|
+
export interface PaginationParams {
|
|
56719
57166
|
page?: number;
|
|
56720
57167
|
pageSize?: number;
|
|
56721
57168
|
sortBy?: string;
|
|
56722
57169
|
sortDirection?: 'asc' | 'desc';
|
|
57170
|
+
search?: string;
|
|
56723
57171
|
filter?: string;
|
|
56724
57172
|
}
|
|
56725
57173
|
|
|
56726
|
-
export interface
|
|
57174
|
+
export interface PaginatedResult<T> {
|
|
56727
57175
|
items: T[];
|
|
56728
57176
|
totalCount: number;
|
|
56729
57177
|
pageSize: number;
|
|
56730
|
-
|
|
57178
|
+
page: number;
|
|
56731
57179
|
totalPages: number;
|
|
57180
|
+
hasPreviousPage: boolean;
|
|
57181
|
+
hasNextPage: boolean;
|
|
56732
57182
|
}
|
|
56733
57183
|
`;
|
|
56734
57184
|
}
|
|
@@ -56741,7 +57191,7 @@ function generateHook(name, nameLower, methods) {
|
|
|
56741
57191
|
|
|
56742
57192
|
import { useQuery, useMutation, useQueryClient, UseQueryOptions, UseMutationOptions } from '@tanstack/react-query';
|
|
56743
57193
|
import { ${nameLower}Api } from '../services/api/${nameLower}';
|
|
56744
|
-
import type { ${name}, ${name}CreateRequest, ${name}UpdateRequest,
|
|
57194
|
+
import type { ${name}, ${name}CreateRequest, ${name}UpdateRequest, PaginationParams, PaginatedResult } from '../types/${nameLower}';
|
|
56745
57195
|
|
|
56746
57196
|
const QUERY_KEY = '${nameLower}s';
|
|
56747
57197
|
|
|
@@ -56749,8 +57199,8 @@ ${methods.includes("getAll") ? `/**
|
|
|
56749
57199
|
* Hook to fetch paginated ${name} list
|
|
56750
57200
|
*/
|
|
56751
57201
|
export function use${name}List(
|
|
56752
|
-
params?:
|
|
56753
|
-
options?: Omit<UseQueryOptions<
|
|
57202
|
+
params?: PaginationParams,
|
|
57203
|
+
options?: Omit<UseQueryOptions<PaginatedResult<${name}>>, 'queryKey' | 'queryFn'>
|
|
56754
57204
|
) {
|
|
56755
57205
|
return useQuery({
|
|
56756
57206
|
queryKey: [QUERY_KEY, 'list', params],
|
|
@@ -56831,8 +57281,8 @@ ${methods.includes("search") ? `/**
|
|
|
56831
57281
|
*/
|
|
56832
57282
|
export function use${name}Search(
|
|
56833
57283
|
query: string,
|
|
56834
|
-
params?:
|
|
56835
|
-
options?: Omit<UseQueryOptions<
|
|
57284
|
+
params?: PaginationParams,
|
|
57285
|
+
options?: Omit<UseQueryOptions<PaginatedResult<${name}>>, 'queryKey' | 'queryFn'>
|
|
56836
57286
|
) {
|
|
56837
57287
|
return useQuery({
|
|
56838
57288
|
queryKey: [QUERY_KEY, 'search', query, params],
|
|
@@ -57023,7 +57473,7 @@ async function scaffoldRoutes(input, config2) {
|
|
|
57023
57473
|
result.instructions.push("import { PageLoader } from '@/components/ui/PageLoader';");
|
|
57024
57474
|
result.instructions.push("");
|
|
57025
57475
|
const importedComponents = /* @__PURE__ */ new Set();
|
|
57026
|
-
for (const [
|
|
57476
|
+
for (const [_context, applications] of Object.entries(routeTree)) {
|
|
57027
57477
|
for (const [, modules] of Object.entries(applications)) {
|
|
57028
57478
|
for (const route of modules) {
|
|
57029
57479
|
const pageEntry = pageFiles.get(route.navRoute);
|