@atlashub/smartstack-mcp 1.3.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +2196 -119
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -81,6 +81,26 @@ import path2 from "path";
|
|
|
81
81
|
import fs from "fs-extra";
|
|
82
82
|
import path from "path";
|
|
83
83
|
import { glob } from "glob";
|
|
84
|
+
var FileSystemError = class extends Error {
|
|
85
|
+
constructor(message, operation, path14, cause) {
|
|
86
|
+
super(message);
|
|
87
|
+
this.operation = operation;
|
|
88
|
+
this.path = path14;
|
|
89
|
+
this.cause = cause;
|
|
90
|
+
this.name = "FileSystemError";
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
function validatePathSecurity(targetPath, baseDir) {
|
|
94
|
+
const normalizedTarget = path.resolve(targetPath);
|
|
95
|
+
const normalizedBase = path.resolve(baseDir);
|
|
96
|
+
if (!normalizedTarget.startsWith(normalizedBase + path.sep) && normalizedTarget !== normalizedBase) {
|
|
97
|
+
throw new FileSystemError(
|
|
98
|
+
`Path traversal detected: "${targetPath}" is outside allowed directory "${baseDir}"`,
|
|
99
|
+
"validatePathSecurity",
|
|
100
|
+
targetPath
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
84
104
|
async function fileExists(filePath) {
|
|
85
105
|
try {
|
|
86
106
|
const stat = await fs.stat(filePath);
|
|
@@ -101,15 +121,57 @@ async function ensureDirectory(dirPath) {
|
|
|
101
121
|
await fs.ensureDir(dirPath);
|
|
102
122
|
}
|
|
103
123
|
async function readJson(filePath) {
|
|
104
|
-
|
|
105
|
-
|
|
124
|
+
try {
|
|
125
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
126
|
+
try {
|
|
127
|
+
return JSON.parse(content);
|
|
128
|
+
} catch (parseError) {
|
|
129
|
+
throw new FileSystemError(
|
|
130
|
+
`Invalid JSON in file: ${filePath}`,
|
|
131
|
+
"readJson",
|
|
132
|
+
filePath,
|
|
133
|
+
parseError instanceof Error ? parseError : void 0
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
} catch (error) {
|
|
137
|
+
if (error instanceof FileSystemError) {
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
141
|
+
throw new FileSystemError(
|
|
142
|
+
`Failed to read JSON file: ${filePath} - ${err.message}`,
|
|
143
|
+
"readJson",
|
|
144
|
+
filePath,
|
|
145
|
+
err
|
|
146
|
+
);
|
|
147
|
+
}
|
|
106
148
|
}
|
|
107
149
|
async function readText(filePath) {
|
|
108
|
-
|
|
150
|
+
try {
|
|
151
|
+
return await fs.readFile(filePath, "utf-8");
|
|
152
|
+
} catch (error) {
|
|
153
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
154
|
+
throw new FileSystemError(
|
|
155
|
+
`Failed to read file: ${filePath} - ${err.message}`,
|
|
156
|
+
"readText",
|
|
157
|
+
filePath,
|
|
158
|
+
err
|
|
159
|
+
);
|
|
160
|
+
}
|
|
109
161
|
}
|
|
110
162
|
async function writeText(filePath, content) {
|
|
111
|
-
|
|
112
|
-
|
|
163
|
+
try {
|
|
164
|
+
await fs.ensureDir(path.dirname(filePath));
|
|
165
|
+
await fs.writeFile(filePath, content, "utf-8");
|
|
166
|
+
} catch (error) {
|
|
167
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
168
|
+
throw new FileSystemError(
|
|
169
|
+
`Failed to write file: ${filePath} - ${err.message}`,
|
|
170
|
+
"writeText",
|
|
171
|
+
filePath,
|
|
172
|
+
err
|
|
173
|
+
);
|
|
174
|
+
}
|
|
113
175
|
}
|
|
114
176
|
async function findFiles(pattern, options = {}) {
|
|
115
177
|
const { cwd = process.cwd(), ignore = [] } = options;
|
|
@@ -126,7 +188,8 @@ async function findFiles(pattern, options = {}) {
|
|
|
126
188
|
var defaultConfig = {
|
|
127
189
|
version: "1.0.0",
|
|
128
190
|
smartstack: {
|
|
129
|
-
|
|
191
|
+
// Will be resolved in getConfig() - never use a hard-coded path
|
|
192
|
+
projectPath: "",
|
|
130
193
|
apiUrl: process.env.SMARTSTACK_API_URL || "https://localhost:5001",
|
|
131
194
|
apiEnabled: process.env.SMARTSTACK_API_ENABLED !== "false"
|
|
132
195
|
},
|
|
@@ -146,7 +209,8 @@ var defaultConfig = {
|
|
|
146
209
|
"entra_",
|
|
147
210
|
"ref_",
|
|
148
211
|
"loc_",
|
|
149
|
-
"lic_"
|
|
212
|
+
"lic_",
|
|
213
|
+
"tenant_"
|
|
150
214
|
],
|
|
151
215
|
codePrefixes: {
|
|
152
216
|
core: "core_",
|
|
@@ -189,6 +253,17 @@ var defaultConfig = {
|
|
|
189
253
|
}
|
|
190
254
|
};
|
|
191
255
|
var cachedConfig = null;
|
|
256
|
+
function resolveProjectPath(configPath) {
|
|
257
|
+
if (process.env.SMARTSTACK_PROJECT_PATH) {
|
|
258
|
+
return process.env.SMARTSTACK_PROJECT_PATH;
|
|
259
|
+
}
|
|
260
|
+
if (configPath && configPath.trim()) {
|
|
261
|
+
return configPath;
|
|
262
|
+
}
|
|
263
|
+
const cwd = process.cwd();
|
|
264
|
+
logger.warn("No SMARTSTACK_PROJECT_PATH configured, using current directory", { cwd });
|
|
265
|
+
return cwd;
|
|
266
|
+
}
|
|
192
267
|
async function getConfig() {
|
|
193
268
|
if (cachedConfig) {
|
|
194
269
|
return cachedConfig;
|
|
@@ -201,18 +276,23 @@ async function getConfig() {
|
|
|
201
276
|
logger.info("Configuration loaded from file", { path: configPath });
|
|
202
277
|
} catch (error) {
|
|
203
278
|
logger.warn("Failed to load config file, using defaults", { error });
|
|
204
|
-
cachedConfig = defaultConfig;
|
|
279
|
+
cachedConfig = { ...defaultConfig };
|
|
205
280
|
}
|
|
206
281
|
} else {
|
|
207
282
|
logger.debug("No config file found, using defaults");
|
|
208
|
-
cachedConfig = defaultConfig;
|
|
209
|
-
}
|
|
210
|
-
if (process.env.SMARTSTACK_PROJECT_PATH) {
|
|
211
|
-
cachedConfig.smartstack.projectPath = process.env.SMARTSTACK_PROJECT_PATH;
|
|
283
|
+
cachedConfig = { ...defaultConfig };
|
|
212
284
|
}
|
|
285
|
+
cachedConfig.smartstack.projectPath = resolveProjectPath(
|
|
286
|
+
cachedConfig.smartstack.projectPath
|
|
287
|
+
);
|
|
213
288
|
if (process.env.SMARTSTACK_API_URL) {
|
|
214
289
|
cachedConfig.smartstack.apiUrl = process.env.SMARTSTACK_API_URL;
|
|
215
290
|
}
|
|
291
|
+
if (!cachedConfig.smartstack.projectPath) {
|
|
292
|
+
throw new Error(
|
|
293
|
+
"SmartStack project path not configured. Set SMARTSTACK_PROJECT_PATH environment variable or configure in config file."
|
|
294
|
+
);
|
|
295
|
+
}
|
|
216
296
|
return cachedConfig;
|
|
217
297
|
}
|
|
218
298
|
function mergeConfig(base, override) {
|
|
@@ -335,15 +415,21 @@ var ConfigSchema = z.object({
|
|
|
335
415
|
});
|
|
336
416
|
var ValidateConventionsInputSchema = z.object({
|
|
337
417
|
path: z.string().optional().describe("Project path to validate (default: SmartStack.app path)"),
|
|
338
|
-
checks: z.array(z.enum(["tables", "migrations", "services", "namespaces", "entities", "all"])).default(["all"]).describe("Types of checks to perform")
|
|
418
|
+
checks: z.array(z.enum(["tables", "migrations", "services", "namespaces", "entities", "tenants", "controllers", "all"])).default(["all"]).describe("Types of checks to perform")
|
|
339
419
|
});
|
|
340
420
|
var CheckMigrationsInputSchema = z.object({
|
|
341
421
|
projectPath: z.string().optional().describe("EF Core project path"),
|
|
342
422
|
branch: z.string().optional().describe("Git branch to check (default: current)"),
|
|
343
423
|
compareBranch: z.string().optional().describe("Branch to compare against")
|
|
344
424
|
});
|
|
425
|
+
var EntityPropertySchema = z.object({
|
|
426
|
+
name: z.string().describe("Property name (PascalCase)"),
|
|
427
|
+
type: z.string().describe("C# type (string, int, Guid, DateTime, etc.)"),
|
|
428
|
+
required: z.boolean().optional().describe("If true, property is required"),
|
|
429
|
+
maxLength: z.number().optional().describe("Max length for string properties")
|
|
430
|
+
});
|
|
345
431
|
var ScaffoldExtensionInputSchema = z.object({
|
|
346
|
-
type: z.enum(["service", "entity", "controller", "component"]).describe(
|
|
432
|
+
type: z.enum(["feature", "service", "entity", "controller", "component", "test", "dto", "validator", "repository"]).describe('Type of extension to scaffold. Use "feature" for full-stack generation.'),
|
|
347
433
|
name: z.string().describe('Name of the extension (e.g., "UserProfile", "Order")'),
|
|
348
434
|
options: z.object({
|
|
349
435
|
namespace: z.string().optional().describe("Custom namespace"),
|
|
@@ -352,7 +438,17 @@ var ScaffoldExtensionInputSchema = z.object({
|
|
|
352
438
|
outputPath: z.string().optional().describe("Custom output path"),
|
|
353
439
|
isSystemEntity: z.boolean().optional().describe("If true, creates a system entity without TenantId"),
|
|
354
440
|
tablePrefix: z.string().optional().describe('Domain prefix for table name (e.g., "auth_", "nav_", "cfg_")'),
|
|
355
|
-
schema: z.enum(["core", "extensions"]).optional().describe("Database schema (default: core)")
|
|
441
|
+
schema: z.enum(["core", "extensions"]).optional().describe("Database schema (default: core)"),
|
|
442
|
+
dryRun: z.boolean().optional().describe("If true, preview generated code without writing files"),
|
|
443
|
+
skipService: z.boolean().optional().describe("For feature type: skip service generation"),
|
|
444
|
+
skipController: z.boolean().optional().describe("For feature type: skip controller generation"),
|
|
445
|
+
skipComponent: z.boolean().optional().describe("For feature type: skip React component generation"),
|
|
446
|
+
clientExtension: z.boolean().optional().describe("If true, use extensions schema for client-specific code"),
|
|
447
|
+
withTests: z.boolean().optional().describe("For feature type: also generate unit tests"),
|
|
448
|
+
withDtos: z.boolean().optional().describe("For feature type: generate DTOs (Create, Update, Response)"),
|
|
449
|
+
withValidation: z.boolean().optional().describe("For feature type: generate FluentValidation validators"),
|
|
450
|
+
withRepository: z.boolean().optional().describe("For feature type: generate repository pattern"),
|
|
451
|
+
entityProperties: z.array(EntityPropertySchema).optional().describe("Entity properties for DTO/Validator generation")
|
|
356
452
|
}).optional()
|
|
357
453
|
});
|
|
358
454
|
var ApiDocsInputSchema = z.object({
|
|
@@ -369,10 +465,30 @@ import { exec } from "child_process";
|
|
|
369
465
|
import { promisify } from "util";
|
|
370
466
|
import path3 from "path";
|
|
371
467
|
var execAsync = promisify(exec);
|
|
468
|
+
var GitError = class extends Error {
|
|
469
|
+
constructor(message, command, cwd, cause) {
|
|
470
|
+
super(message);
|
|
471
|
+
this.command = command;
|
|
472
|
+
this.cwd = cwd;
|
|
473
|
+
this.cause = cause;
|
|
474
|
+
this.name = "GitError";
|
|
475
|
+
}
|
|
476
|
+
};
|
|
372
477
|
async function git(command, cwd) {
|
|
373
|
-
const options = cwd ? { cwd } : {};
|
|
374
|
-
|
|
375
|
-
|
|
478
|
+
const options = cwd ? { cwd, maxBuffer: 10 * 1024 * 1024 } : { maxBuffer: 10 * 1024 * 1024 };
|
|
479
|
+
try {
|
|
480
|
+
const { stdout } = await execAsync(`git ${command}`, options);
|
|
481
|
+
return stdout.trim();
|
|
482
|
+
} catch (error) {
|
|
483
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
484
|
+
const stderr = error?.stderr || "";
|
|
485
|
+
throw new GitError(
|
|
486
|
+
`Git command failed: git ${command}${stderr ? ` - ${stderr.trim()}` : ""}`,
|
|
487
|
+
command,
|
|
488
|
+
cwd,
|
|
489
|
+
err
|
|
490
|
+
);
|
|
491
|
+
}
|
|
376
492
|
}
|
|
377
493
|
async function isGitRepo(cwd) {
|
|
378
494
|
const gitDir = path3.join(cwd || process.cwd(), ".git");
|
|
@@ -547,7 +663,7 @@ async function findControllerFiles(apiPath) {
|
|
|
547
663
|
import path5 from "path";
|
|
548
664
|
var validateConventionsTool = {
|
|
549
665
|
name: "validate_conventions",
|
|
550
|
-
description: "Validate AtlasHub/SmartStack conventions: SQL schemas (core/extensions), domain table prefixes (auth_, nav_, ai_, etc.), migration naming ({context}_v{version}_{sequence}_*), service interfaces (I*Service), namespace structure",
|
|
666
|
+
description: "Validate AtlasHub/SmartStack conventions: SQL schemas (core/extensions), domain table prefixes (auth_, nav_, ai_, etc.), migration naming ({context}_v{version}_{sequence}_*), service interfaces (I*Service), namespace structure, controller routes (NavRoute)",
|
|
551
667
|
inputSchema: {
|
|
552
668
|
type: "object",
|
|
553
669
|
properties: {
|
|
@@ -559,7 +675,7 @@ var validateConventionsTool = {
|
|
|
559
675
|
type: "array",
|
|
560
676
|
items: {
|
|
561
677
|
type: "string",
|
|
562
|
-
enum: ["tables", "migrations", "services", "namespaces", "all"]
|
|
678
|
+
enum: ["tables", "migrations", "services", "namespaces", "entities", "tenants", "controllers", "all"]
|
|
563
679
|
},
|
|
564
680
|
description: "Types of checks to perform",
|
|
565
681
|
default: ["all"]
|
|
@@ -570,7 +686,7 @@ var validateConventionsTool = {
|
|
|
570
686
|
async function handleValidateConventions(args, config) {
|
|
571
687
|
const input = ValidateConventionsInputSchema.parse(args);
|
|
572
688
|
const projectPath = input.path || config.smartstack.projectPath;
|
|
573
|
-
const checks = input.checks.includes("all") ? ["tables", "migrations", "services", "namespaces"] : input.checks;
|
|
689
|
+
const checks = input.checks.includes("all") ? ["tables", "migrations", "services", "namespaces", "entities", "tenants", "controllers"] : input.checks;
|
|
574
690
|
logger.info("Validating conventions", { projectPath, checks });
|
|
575
691
|
const result = {
|
|
576
692
|
valid: true,
|
|
@@ -591,6 +707,15 @@ async function handleValidateConventions(args, config) {
|
|
|
591
707
|
if (checks.includes("namespaces")) {
|
|
592
708
|
await validateNamespaces(structure, config, result);
|
|
593
709
|
}
|
|
710
|
+
if (checks.includes("entities")) {
|
|
711
|
+
await validateEntities(structure, config, result);
|
|
712
|
+
}
|
|
713
|
+
if (checks.includes("tenants")) {
|
|
714
|
+
await validateTenantAwareness(structure, config, result);
|
|
715
|
+
}
|
|
716
|
+
if (checks.includes("controllers")) {
|
|
717
|
+
await validateControllerRoutes(structure, config, result);
|
|
718
|
+
}
|
|
594
719
|
result.valid = result.errors.length === 0;
|
|
595
720
|
result.summary = generateSummary(result, checks);
|
|
596
721
|
return formatResult(result);
|
|
@@ -785,6 +910,242 @@ async function validateNamespaces(structure, config, result) {
|
|
|
785
910
|
}
|
|
786
911
|
}
|
|
787
912
|
}
|
|
913
|
+
async function validateEntities(structure, _config, result) {
|
|
914
|
+
if (!structure.domain) {
|
|
915
|
+
result.warnings.push({
|
|
916
|
+
type: "warning",
|
|
917
|
+
category: "entities",
|
|
918
|
+
message: "Domain project not found, skipping entity validation"
|
|
919
|
+
});
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
const entityFiles = await findFiles("**/*.cs", { cwd: structure.domain });
|
|
923
|
+
for (const file of entityFiles) {
|
|
924
|
+
const content = await readText(file);
|
|
925
|
+
const fileName = path5.basename(file, ".cs");
|
|
926
|
+
if (fileName.endsWith("Dto") || fileName.endsWith("Command") || fileName.endsWith("Query") || fileName.endsWith("Handler") || fileName.endsWith("Validator") || fileName.endsWith("Exception") || fileName.startsWith("I")) {
|
|
927
|
+
continue;
|
|
928
|
+
}
|
|
929
|
+
const classMatch = content.match(/public\s+(?:class|record)\s+(\w+)(?:\s*:\s*([^{]+))?/);
|
|
930
|
+
if (!classMatch) continue;
|
|
931
|
+
const entityName = classMatch[1];
|
|
932
|
+
const inheritance = classMatch[2]?.trim() || "";
|
|
933
|
+
const hasBaseEntity = inheritance.includes("BaseEntity");
|
|
934
|
+
const hasSystemEntity = inheritance.includes("SystemEntity");
|
|
935
|
+
const hasITenantEntity = inheritance.includes("ITenantEntity");
|
|
936
|
+
if (!hasBaseEntity && !hasSystemEntity) {
|
|
937
|
+
continue;
|
|
938
|
+
}
|
|
939
|
+
if (hasBaseEntity && !hasSystemEntity && !hasITenantEntity) {
|
|
940
|
+
result.warnings.push({
|
|
941
|
+
type: "warning",
|
|
942
|
+
category: "entities",
|
|
943
|
+
message: `Entity "${entityName}" inherits BaseEntity but doesn't implement ITenantEntity`,
|
|
944
|
+
file: path5.relative(structure.root, file),
|
|
945
|
+
suggestion: "Add ITenantEntity interface for multi-tenant support, or use SystemEntity for platform-level entities"
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
if (!content.includes(`private ${entityName}()`)) {
|
|
949
|
+
result.warnings.push({
|
|
950
|
+
type: "warning",
|
|
951
|
+
category: "entities",
|
|
952
|
+
message: `Entity "${entityName}" is missing private constructor for EF Core`,
|
|
953
|
+
file: path5.relative(structure.root, file),
|
|
954
|
+
suggestion: `Add: private ${entityName}() { }`
|
|
955
|
+
});
|
|
956
|
+
}
|
|
957
|
+
if (!content.includes(`public static ${entityName} Create(`)) {
|
|
958
|
+
result.warnings.push({
|
|
959
|
+
type: "warning",
|
|
960
|
+
category: "entities",
|
|
961
|
+
message: `Entity "${entityName}" is missing factory method`,
|
|
962
|
+
file: path5.relative(structure.root, file),
|
|
963
|
+
suggestion: `Add factory method: public static ${entityName} Create(...)`
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
async function validateTenantAwareness(structure, _config, result) {
|
|
969
|
+
if (!structure.domain) {
|
|
970
|
+
result.warnings.push({
|
|
971
|
+
type: "warning",
|
|
972
|
+
category: "tenants",
|
|
973
|
+
message: "Domain project not found, skipping tenant validation"
|
|
974
|
+
});
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
const entityFiles = await findFiles("**/*.cs", { cwd: structure.domain });
|
|
978
|
+
let tenantAwareCount = 0;
|
|
979
|
+
let systemEntityCount = 0;
|
|
980
|
+
let ambiguousCount = 0;
|
|
981
|
+
for (const file of entityFiles) {
|
|
982
|
+
const content = await readText(file);
|
|
983
|
+
const classMatch = content.match(/public\s+(?:class|record)\s+(\w+)(?:\s*:\s*([^{]+))?/);
|
|
984
|
+
if (!classMatch) continue;
|
|
985
|
+
const entityName = classMatch[1];
|
|
986
|
+
const inheritance = classMatch[2]?.trim() || "";
|
|
987
|
+
if (!inheritance.includes("Entity") && !inheritance.includes("ITenant")) {
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
const hasITenantEntity = inheritance.includes("ITenantEntity");
|
|
991
|
+
const hasSystemEntity = inheritance.includes("SystemEntity");
|
|
992
|
+
const hasTenantId = content.includes("TenantId");
|
|
993
|
+
const hasTenantIdProperty = content.includes("public Guid TenantId") || content.includes("public required Guid TenantId");
|
|
994
|
+
if (hasITenantEntity) {
|
|
995
|
+
tenantAwareCount++;
|
|
996
|
+
if (!hasTenantIdProperty) {
|
|
997
|
+
result.errors.push({
|
|
998
|
+
type: "error",
|
|
999
|
+
category: "tenants",
|
|
1000
|
+
message: `Entity "${entityName}" implements ITenantEntity but is missing TenantId property`,
|
|
1001
|
+
file: path5.relative(structure.root, file),
|
|
1002
|
+
suggestion: "Add: public Guid TenantId { get; private set; }"
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
const createMethod = content.match(/public static \w+ Create\s*\(([^)]*)\)/);
|
|
1006
|
+
if (createMethod && !createMethod[1].includes("tenantId") && !createMethod[1].includes("TenantId")) {
|
|
1007
|
+
result.errors.push({
|
|
1008
|
+
type: "error",
|
|
1009
|
+
category: "tenants",
|
|
1010
|
+
message: `Entity "${entityName}" implements ITenantEntity but Create() doesn't require tenantId`,
|
|
1011
|
+
file: path5.relative(structure.root, file),
|
|
1012
|
+
suggestion: "Add tenantId as first parameter: Create(Guid tenantId, ...)"
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
if (hasSystemEntity) {
|
|
1017
|
+
systemEntityCount++;
|
|
1018
|
+
if (hasTenantId) {
|
|
1019
|
+
result.errors.push({
|
|
1020
|
+
type: "error",
|
|
1021
|
+
category: "tenants",
|
|
1022
|
+
message: `System entity "${entityName}" should not have TenantId`,
|
|
1023
|
+
file: path5.relative(structure.root, file),
|
|
1024
|
+
suggestion: "Remove TenantId from system entities"
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
if (!hasITenantEntity && !hasSystemEntity && hasTenantId) {
|
|
1029
|
+
ambiguousCount++;
|
|
1030
|
+
result.warnings.push({
|
|
1031
|
+
type: "warning",
|
|
1032
|
+
category: "tenants",
|
|
1033
|
+
message: `Entity "${entityName}" has TenantId but doesn't implement ITenantEntity`,
|
|
1034
|
+
file: path5.relative(structure.root, file),
|
|
1035
|
+
suggestion: "Add ITenantEntity interface for explicit tenant-awareness"
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
if (tenantAwareCount + systemEntityCount > 0) {
|
|
1040
|
+
result.warnings.push({
|
|
1041
|
+
type: "warning",
|
|
1042
|
+
category: "tenants",
|
|
1043
|
+
message: `Tenant summary: ${tenantAwareCount} tenant-aware, ${systemEntityCount} system, ${ambiguousCount} ambiguous`
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
async function validateControllerRoutes(structure, _config, result) {
|
|
1048
|
+
if (!structure.api) {
|
|
1049
|
+
result.warnings.push({
|
|
1050
|
+
type: "warning",
|
|
1051
|
+
category: "controllers",
|
|
1052
|
+
message: "API project not found, skipping controller route validation"
|
|
1053
|
+
});
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
const controllerFiles = await findFiles("**/Controllers/**/*Controller.cs", {
|
|
1057
|
+
cwd: structure.api
|
|
1058
|
+
});
|
|
1059
|
+
const systemControllers = [
|
|
1060
|
+
// Auth controllers
|
|
1061
|
+
"AuthController",
|
|
1062
|
+
"RegistrationController",
|
|
1063
|
+
"OnboardingController",
|
|
1064
|
+
"NavigationController",
|
|
1065
|
+
"LogsController",
|
|
1066
|
+
"EntraController",
|
|
1067
|
+
// User-level controllers (not in navigation hierarchy)
|
|
1068
|
+
"PreferencesController",
|
|
1069
|
+
"ApplicationTrackingController",
|
|
1070
|
+
// Support module controllers (using api/support/* routes)
|
|
1071
|
+
"NotificationsController",
|
|
1072
|
+
"SlaController",
|
|
1073
|
+
"TemplatesController",
|
|
1074
|
+
"TicketsController",
|
|
1075
|
+
// DashboardController exists in both User and Support - both use hardcoded routes
|
|
1076
|
+
"DashboardController",
|
|
1077
|
+
// Structure controllers (using api/structure/* routes - navigation paths don't exist yet)
|
|
1078
|
+
"CostCentersController",
|
|
1079
|
+
"OrganisationsController",
|
|
1080
|
+
"OrganizationalDomainsController",
|
|
1081
|
+
// Business controllers (using api/business/* routes - navigation paths don't exist yet)
|
|
1082
|
+
"MyTicketsController"
|
|
1083
|
+
];
|
|
1084
|
+
let navRouteCount = 0;
|
|
1085
|
+
let hardcodedRouteCount = 0;
|
|
1086
|
+
let systemControllerCount = 0;
|
|
1087
|
+
for (const file of controllerFiles) {
|
|
1088
|
+
const content = await readText(file);
|
|
1089
|
+
const fileName = path5.basename(file, ".cs");
|
|
1090
|
+
if (systemControllers.includes(fileName)) {
|
|
1091
|
+
systemControllerCount++;
|
|
1092
|
+
continue;
|
|
1093
|
+
}
|
|
1094
|
+
const hasNavRoute = content.includes("[NavRoute(");
|
|
1095
|
+
const hasHardcodedRoute = content.includes('[Route("api/[controller]")]') || content.includes('[Route("api/') || /\[Route\s*\(\s*"[^"]+"\s*\)\]/.test(content);
|
|
1096
|
+
if (hasNavRoute) {
|
|
1097
|
+
navRouteCount++;
|
|
1098
|
+
const navRouteMatch = content.match(/\[NavRoute\s*\(\s*"([^"]+)"(?:\s*,\s*Suffix\s*=\s*"([^"]+)")?\s*\)\]/);
|
|
1099
|
+
if (navRouteMatch) {
|
|
1100
|
+
const routePath = navRouteMatch[1];
|
|
1101
|
+
const parts = routePath.split(".");
|
|
1102
|
+
if (parts.length < 2) {
|
|
1103
|
+
result.warnings.push({
|
|
1104
|
+
type: "warning",
|
|
1105
|
+
category: "controllers",
|
|
1106
|
+
message: `Controller "${fileName}" has NavRoute with insufficient depth: "${routePath}"`,
|
|
1107
|
+
file: path5.relative(structure.root, file),
|
|
1108
|
+
suggestion: 'NavRoute should have at least 2 levels: "context.application" (e.g., "platform.administration")'
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
const hasUppercase = parts.some((part) => part !== part.toLowerCase());
|
|
1112
|
+
if (hasUppercase) {
|
|
1113
|
+
result.errors.push({
|
|
1114
|
+
type: "error",
|
|
1115
|
+
category: "controllers",
|
|
1116
|
+
message: `Controller "${fileName}" has NavRoute with uppercase characters: "${routePath}"`,
|
|
1117
|
+
file: path5.relative(structure.root, file),
|
|
1118
|
+
suggestion: 'NavRoute paths must be lowercase (e.g., "platform.administration.users")'
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
} else if (hasHardcodedRoute) {
|
|
1123
|
+
hardcodedRouteCount++;
|
|
1124
|
+
result.warnings.push({
|
|
1125
|
+
type: "warning",
|
|
1126
|
+
category: "controllers",
|
|
1127
|
+
message: `Controller "${fileName}" uses hardcoded Route instead of NavRoute`,
|
|
1128
|
+
file: path5.relative(structure.root, file),
|
|
1129
|
+
suggestion: 'Use [NavRoute("context.application.module")] for navigation-based routing'
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
const totalControllers = controllerFiles.length;
|
|
1134
|
+
const businessControllers = totalControllers - systemControllerCount;
|
|
1135
|
+
const navRoutePercentage = businessControllers > 0 ? Math.round(navRouteCount / businessControllers * 100) : 0;
|
|
1136
|
+
result.warnings.push({
|
|
1137
|
+
type: "warning",
|
|
1138
|
+
category: "controllers",
|
|
1139
|
+
message: `Route summary: ${navRouteCount}/${businessControllers} business controllers use NavRoute (${navRoutePercentage}%), ${systemControllerCount} system controllers excluded`
|
|
1140
|
+
});
|
|
1141
|
+
if (navRoutePercentage < 80 && businessControllers > 0) {
|
|
1142
|
+
result.warnings.push({
|
|
1143
|
+
type: "warning",
|
|
1144
|
+
category: "controllers",
|
|
1145
|
+
message: `NavRoute adoption is below 80% (${navRoutePercentage}%). Consider migrating remaining controllers.`
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
788
1149
|
function generateSummary(result, checks) {
|
|
789
1150
|
const parts = [];
|
|
790
1151
|
parts.push(`Checks performed: ${checks.join(", ")}`);
|
|
@@ -928,9 +1289,23 @@ async function parseMigrations(migrationsPath, rootPath) {
|
|
|
928
1289
|
return a.sequence.localeCompare(b.sequence);
|
|
929
1290
|
});
|
|
930
1291
|
}
|
|
1292
|
+
function parseSemver(version) {
|
|
1293
|
+
const match = version.match(/^(\d+)\.(\d+)\.(\d+)$/);
|
|
1294
|
+
if (!match) {
|
|
1295
|
+
return null;
|
|
1296
|
+
}
|
|
1297
|
+
return [
|
|
1298
|
+
parseInt(match[1], 10),
|
|
1299
|
+
parseInt(match[2], 10),
|
|
1300
|
+
parseInt(match[3], 10)
|
|
1301
|
+
];
|
|
1302
|
+
}
|
|
931
1303
|
function compareVersions(a, b) {
|
|
932
|
-
const partsA = a
|
|
933
|
-
const partsB = b
|
|
1304
|
+
const partsA = parseSemver(a);
|
|
1305
|
+
const partsB = parseSemver(b);
|
|
1306
|
+
if (!partsA && !partsB) return 0;
|
|
1307
|
+
if (!partsA) return 1;
|
|
1308
|
+
if (!partsB) return -1;
|
|
934
1309
|
for (let i = 0; i < 3; i++) {
|
|
935
1310
|
if (partsA[i] > partsB[i]) return 1;
|
|
936
1311
|
if (partsA[i] < partsB[i]) return -1;
|
|
@@ -955,6 +1330,14 @@ function checkNamingConventions(result, _config) {
|
|
|
955
1330
|
resolution: `Use format: {context}_v{version}_{sequence}_{Description} where version is semver (1.0.0, 1.2.0, etc.)`
|
|
956
1331
|
});
|
|
957
1332
|
}
|
|
1333
|
+
if (migration.version !== "0.0.0" && !parseSemver(migration.version)) {
|
|
1334
|
+
result.conflicts.push({
|
|
1335
|
+
type: "naming",
|
|
1336
|
+
description: `Migration "${migration.name}" has invalid version format: "${migration.version}"`,
|
|
1337
|
+
files: [migration.file],
|
|
1338
|
+
resolution: `Use semver format (major.minor.patch): 1.0.0, 1.2.0, 2.0.0, etc.`
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
958
1341
|
}
|
|
959
1342
|
}
|
|
960
1343
|
function checkChronologicalOrder(result) {
|
|
@@ -1118,14 +1501,14 @@ import Handlebars from "handlebars";
|
|
|
1118
1501
|
import path7 from "path";
|
|
1119
1502
|
var scaffoldExtensionTool = {
|
|
1120
1503
|
name: "scaffold_extension",
|
|
1121
|
-
description: "Generate code to extend SmartStack:
|
|
1504
|
+
description: "Generate code to extend SmartStack: feature (full-stack), entity, service, controller, component, dto, validator, repository, or test",
|
|
1122
1505
|
inputSchema: {
|
|
1123
1506
|
type: "object",
|
|
1124
1507
|
properties: {
|
|
1125
1508
|
type: {
|
|
1126
1509
|
type: "string",
|
|
1127
|
-
enum: ["service", "entity", "controller", "component"],
|
|
1128
|
-
description:
|
|
1510
|
+
enum: ["feature", "service", "entity", "controller", "component", "test", "dto", "validator", "repository"],
|
|
1511
|
+
description: 'Type of extension to scaffold. Use "feature" for full-stack generation.'
|
|
1129
1512
|
},
|
|
1130
1513
|
name: {
|
|
1131
1514
|
type: "string",
|
|
@@ -1163,6 +1546,63 @@ var scaffoldExtensionTool = {
|
|
|
1163
1546
|
type: "string",
|
|
1164
1547
|
enum: ["core", "extensions"],
|
|
1165
1548
|
description: "Database schema (default: core)"
|
|
1549
|
+
},
|
|
1550
|
+
dryRun: {
|
|
1551
|
+
type: "boolean",
|
|
1552
|
+
description: "If true, preview generated code without writing files"
|
|
1553
|
+
},
|
|
1554
|
+
skipService: {
|
|
1555
|
+
type: "boolean",
|
|
1556
|
+
description: "For feature type: skip service generation"
|
|
1557
|
+
},
|
|
1558
|
+
skipController: {
|
|
1559
|
+
type: "boolean",
|
|
1560
|
+
description: "For feature type: skip controller generation"
|
|
1561
|
+
},
|
|
1562
|
+
skipComponent: {
|
|
1563
|
+
type: "boolean",
|
|
1564
|
+
description: "For feature type: skip React component generation"
|
|
1565
|
+
},
|
|
1566
|
+
clientExtension: {
|
|
1567
|
+
type: "boolean",
|
|
1568
|
+
description: "If true, use extensions schema for client-specific code"
|
|
1569
|
+
},
|
|
1570
|
+
withTests: {
|
|
1571
|
+
type: "boolean",
|
|
1572
|
+
description: "For feature type: also generate unit tests"
|
|
1573
|
+
},
|
|
1574
|
+
withDtos: {
|
|
1575
|
+
type: "boolean",
|
|
1576
|
+
description: "For feature type: generate DTOs (Create, Update, Response)"
|
|
1577
|
+
},
|
|
1578
|
+
withValidation: {
|
|
1579
|
+
type: "boolean",
|
|
1580
|
+
description: "For feature type: generate FluentValidation validators"
|
|
1581
|
+
},
|
|
1582
|
+
withRepository: {
|
|
1583
|
+
type: "boolean",
|
|
1584
|
+
description: "For feature type: generate repository pattern"
|
|
1585
|
+
},
|
|
1586
|
+
entityProperties: {
|
|
1587
|
+
type: "array",
|
|
1588
|
+
items: {
|
|
1589
|
+
type: "object",
|
|
1590
|
+
properties: {
|
|
1591
|
+
name: { type: "string" },
|
|
1592
|
+
type: { type: "string" },
|
|
1593
|
+
required: { type: "boolean" },
|
|
1594
|
+
maxLength: { type: "number" }
|
|
1595
|
+
}
|
|
1596
|
+
},
|
|
1597
|
+
description: "Entity properties for DTO/Validator generation"
|
|
1598
|
+
},
|
|
1599
|
+
navRoute: {
|
|
1600
|
+
type: "string",
|
|
1601
|
+
description: 'Navigation route path for controller (e.g., "platform.administration.users"). Required for controllers.'
|
|
1602
|
+
},
|
|
1603
|
+
navRouteSuffix: {
|
|
1604
|
+
type: "string",
|
|
1605
|
+
description: 'Optional suffix for NavRoute (e.g., "dashboard" for sub-resources)'
|
|
1166
1606
|
}
|
|
1167
1607
|
}
|
|
1168
1608
|
}
|
|
@@ -1181,35 +1621,149 @@ Handlebars.registerHelper("kebabCase", (str) => {
|
|
|
1181
1621
|
});
|
|
1182
1622
|
async function handleScaffoldExtension(args, config) {
|
|
1183
1623
|
const input = ScaffoldExtensionInputSchema.parse(args);
|
|
1184
|
-
|
|
1624
|
+
const dryRun = input.options?.dryRun || false;
|
|
1625
|
+
logger.info("Scaffolding extension", { type: input.type, name: input.name, dryRun });
|
|
1185
1626
|
const structure = await findSmartStackStructure(config.smartstack.projectPath);
|
|
1186
1627
|
const result = {
|
|
1187
1628
|
success: true,
|
|
1188
1629
|
files: [],
|
|
1189
1630
|
instructions: []
|
|
1190
1631
|
};
|
|
1632
|
+
if (input.options?.clientExtension) {
|
|
1633
|
+
input.options.schema = "extensions";
|
|
1634
|
+
input.options.tablePrefix = input.options.tablePrefix || "ext_";
|
|
1635
|
+
}
|
|
1191
1636
|
try {
|
|
1192
1637
|
switch (input.type) {
|
|
1638
|
+
case "feature":
|
|
1639
|
+
await scaffoldFeature(input.name, input.options, structure, config, result, dryRun);
|
|
1640
|
+
break;
|
|
1193
1641
|
case "service":
|
|
1194
|
-
await scaffoldService(input.name, input.options, structure, config, result);
|
|
1642
|
+
await scaffoldService(input.name, input.options, structure, config, result, dryRun);
|
|
1195
1643
|
break;
|
|
1196
1644
|
case "entity":
|
|
1197
|
-
await scaffoldEntity(input.name, input.options, structure, config, result);
|
|
1645
|
+
await scaffoldEntity(input.name, input.options, structure, config, result, dryRun);
|
|
1198
1646
|
break;
|
|
1199
1647
|
case "controller":
|
|
1200
|
-
await scaffoldController(input.name, input.options, structure, config, result);
|
|
1648
|
+
await scaffoldController(input.name, input.options, structure, config, result, dryRun);
|
|
1201
1649
|
break;
|
|
1202
1650
|
case "component":
|
|
1203
|
-
await scaffoldComponent(input.name, input.options, structure, config, result);
|
|
1651
|
+
await scaffoldComponent(input.name, input.options, structure, config, result, dryRun);
|
|
1652
|
+
break;
|
|
1653
|
+
case "test":
|
|
1654
|
+
await scaffoldTest(input.name, input.options, structure, config, result, dryRun);
|
|
1655
|
+
break;
|
|
1656
|
+
case "dto":
|
|
1657
|
+
await scaffoldDtos(input.name, input.options, structure, config, result, dryRun);
|
|
1658
|
+
break;
|
|
1659
|
+
case "validator":
|
|
1660
|
+
await scaffoldValidator(input.name, input.options, structure, config, result, dryRun);
|
|
1661
|
+
break;
|
|
1662
|
+
case "repository":
|
|
1663
|
+
await scaffoldRepository(input.name, input.options, structure, config, result, dryRun);
|
|
1204
1664
|
break;
|
|
1205
1665
|
}
|
|
1206
1666
|
} catch (error) {
|
|
1207
1667
|
result.success = false;
|
|
1208
1668
|
result.instructions.push(`Error: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
1209
1669
|
}
|
|
1210
|
-
return formatResult3(result, input.type, input.name);
|
|
1670
|
+
return formatResult3(result, input.type, input.name, dryRun);
|
|
1671
|
+
}
|
|
1672
|
+
async function scaffoldFeature(name, options, structure, config, result, dryRun = false) {
|
|
1673
|
+
const skipService = options?.skipService || false;
|
|
1674
|
+
const skipController = options?.skipController || false;
|
|
1675
|
+
const skipComponent = options?.skipComponent || false;
|
|
1676
|
+
const isClientExtension = options?.clientExtension || false;
|
|
1677
|
+
const withTests = options?.withTests || false;
|
|
1678
|
+
const withDtos = options?.withDtos || false;
|
|
1679
|
+
const withValidation = options?.withValidation || false;
|
|
1680
|
+
const withRepository = options?.withRepository || false;
|
|
1681
|
+
let stepNumber = 1;
|
|
1682
|
+
result.instructions.push(`# Full-Stack Feature: ${name}`);
|
|
1683
|
+
result.instructions.push("");
|
|
1684
|
+
if (isClientExtension) {
|
|
1685
|
+
result.instructions.push("> **Client Extension**: Using `extensions` schema and `ext_` prefix");
|
|
1686
|
+
result.instructions.push("");
|
|
1687
|
+
}
|
|
1688
|
+
result.instructions.push(`## ${stepNumber}. Domain Entity`);
|
|
1689
|
+
await scaffoldEntity(name, options, structure, config, result, dryRun);
|
|
1690
|
+
result.instructions.push("");
|
|
1691
|
+
stepNumber++;
|
|
1692
|
+
if (withDtos) {
|
|
1693
|
+
result.instructions.push(`## ${stepNumber}. DTOs (Data Transfer Objects)`);
|
|
1694
|
+
await scaffoldDtos(name, options, structure, config, result, dryRun);
|
|
1695
|
+
result.instructions.push("");
|
|
1696
|
+
stepNumber++;
|
|
1697
|
+
}
|
|
1698
|
+
if (withValidation) {
|
|
1699
|
+
result.instructions.push(`## ${stepNumber}. FluentValidation Validators`);
|
|
1700
|
+
await scaffoldValidator(name, options, structure, config, result, dryRun);
|
|
1701
|
+
result.instructions.push("");
|
|
1702
|
+
stepNumber++;
|
|
1703
|
+
}
|
|
1704
|
+
if (withRepository) {
|
|
1705
|
+
result.instructions.push(`## ${stepNumber}. Repository Pattern`);
|
|
1706
|
+
await scaffoldRepository(name, options, structure, config, result, dryRun);
|
|
1707
|
+
result.instructions.push("");
|
|
1708
|
+
stepNumber++;
|
|
1709
|
+
}
|
|
1710
|
+
if (!skipService) {
|
|
1711
|
+
result.instructions.push(`## ${stepNumber}. Application Service`);
|
|
1712
|
+
const serviceMethods = [
|
|
1713
|
+
`GetByIdAsync`,
|
|
1714
|
+
`GetAllAsync`,
|
|
1715
|
+
`CreateAsync`,
|
|
1716
|
+
`UpdateAsync`,
|
|
1717
|
+
`DeleteAsync`
|
|
1718
|
+
];
|
|
1719
|
+
await scaffoldService(name, { ...options, methods: serviceMethods }, structure, config, result, dryRun);
|
|
1720
|
+
result.instructions.push("");
|
|
1721
|
+
stepNumber++;
|
|
1722
|
+
}
|
|
1723
|
+
if (!skipController) {
|
|
1724
|
+
result.instructions.push(`## ${stepNumber}. API Controller`);
|
|
1725
|
+
await scaffoldController(name, options, structure, config, result, dryRun);
|
|
1726
|
+
result.instructions.push("");
|
|
1727
|
+
stepNumber++;
|
|
1728
|
+
}
|
|
1729
|
+
if (!skipComponent) {
|
|
1730
|
+
result.instructions.push(`## ${stepNumber}. React Component`);
|
|
1731
|
+
await scaffoldComponent(name, options, structure, config, result, dryRun);
|
|
1732
|
+
result.instructions.push("");
|
|
1733
|
+
stepNumber++;
|
|
1734
|
+
}
|
|
1735
|
+
if (withTests && !skipService) {
|
|
1736
|
+
result.instructions.push(`## ${stepNumber}. Unit Tests`);
|
|
1737
|
+
await scaffoldTest(name, options, structure, config, result, dryRun);
|
|
1738
|
+
result.instructions.push("");
|
|
1739
|
+
}
|
|
1740
|
+
const generated = ["Entity"];
|
|
1741
|
+
if (withDtos) generated.push("DTOs");
|
|
1742
|
+
if (withValidation) generated.push("Validators");
|
|
1743
|
+
if (withRepository) generated.push("Repository");
|
|
1744
|
+
if (!skipService) generated.push("Service");
|
|
1745
|
+
if (!skipController) generated.push("Controller");
|
|
1746
|
+
if (!skipComponent) generated.push("Component");
|
|
1747
|
+
if (withTests && !skipService) generated.push("Tests");
|
|
1748
|
+
result.instructions.push("---");
|
|
1749
|
+
result.instructions.push(`## Summary: Generated ${generated.join(" + ")}`);
|
|
1750
|
+
result.instructions.push("");
|
|
1751
|
+
result.instructions.push("### Next Steps:");
|
|
1752
|
+
result.instructions.push(`1. Add DbSet to ApplicationDbContext: \`public DbSet<${name}> ${name}s => Set<${name}>();\``);
|
|
1753
|
+
if (withRepository) {
|
|
1754
|
+
result.instructions.push(`2. Register repository: \`services.AddScoped<I${name}Repository, ${name}Repository>();\``);
|
|
1755
|
+
}
|
|
1756
|
+
result.instructions.push(`${withRepository ? "3" : "2"}. Register service: \`services.AddScoped<I${name}Service, ${name}Service>();\``);
|
|
1757
|
+
if (withValidation) {
|
|
1758
|
+
result.instructions.push(`${withRepository ? "4" : "3"}. Register validators: \`services.AddValidatorsFromAssemblyContaining<Create${name}DtoValidator>();\``);
|
|
1759
|
+
}
|
|
1760
|
+
result.instructions.push(`${withRepository ? withValidation ? "5" : "4" : withValidation ? "4" : "3"}. Create migration: \`dotnet ef migrations add ${options?.tablePrefix || "ref_"}vX.X.X_XXX_Add${name}\``);
|
|
1761
|
+
result.instructions.push(`${withRepository ? withValidation ? "6" : "5" : withValidation ? "5" : "4"}. Run migration: \`dotnet ef database update\``);
|
|
1762
|
+
if (!skipComponent) {
|
|
1763
|
+
result.instructions.push(`Import component: \`import { ${name} } from './components/${name}';\``);
|
|
1764
|
+
}
|
|
1211
1765
|
}
|
|
1212
|
-
async function scaffoldService(name, options, structure, config, result) {
|
|
1766
|
+
async function scaffoldService(name, options, structure, config, result, dryRun = false) {
|
|
1213
1767
|
const namespace = options?.namespace || `${config.conventions.namespaces.application}.Services`;
|
|
1214
1768
|
const methods = options?.methods || ["GetByIdAsync", "GetAllAsync", "CreateAsync", "UpdateAsync", "DeleteAsync"];
|
|
1215
1769
|
const interfaceTemplate = `using System.Threading;
|
|
@@ -1271,19 +1825,24 @@ services.AddScoped<I{{name}}Service, {{name}}Service>();
|
|
|
1271
1825
|
const interfaceContent = Handlebars.compile(interfaceTemplate)(context);
|
|
1272
1826
|
const implementationContent = Handlebars.compile(implementationTemplate)(context);
|
|
1273
1827
|
const diContent = Handlebars.compile(diTemplate)(context);
|
|
1274
|
-
const
|
|
1828
|
+
const projectRoot = config.smartstack.projectPath;
|
|
1829
|
+
const basePath = structure.application || projectRoot;
|
|
1275
1830
|
const servicesPath = path7.join(basePath, "Services");
|
|
1276
|
-
await ensureDirectory(servicesPath);
|
|
1277
1831
|
const interfacePath = path7.join(servicesPath, `I${name}Service.cs`);
|
|
1278
1832
|
const implementationPath = path7.join(servicesPath, `${name}Service.cs`);
|
|
1279
|
-
|
|
1833
|
+
validatePathSecurity(interfacePath, projectRoot);
|
|
1834
|
+
validatePathSecurity(implementationPath, projectRoot);
|
|
1835
|
+
if (!dryRun) {
|
|
1836
|
+
await ensureDirectory(servicesPath);
|
|
1837
|
+
await writeText(interfacePath, interfaceContent);
|
|
1838
|
+
await writeText(implementationPath, implementationContent);
|
|
1839
|
+
}
|
|
1280
1840
|
result.files.push({ path: interfacePath, content: interfaceContent, type: "created" });
|
|
1281
|
-
await writeText(implementationPath, implementationContent);
|
|
1282
1841
|
result.files.push({ path: implementationPath, content: implementationContent, type: "created" });
|
|
1283
1842
|
result.instructions.push("Register service in DI container:");
|
|
1284
1843
|
result.instructions.push(diContent);
|
|
1285
1844
|
}
|
|
1286
|
-
async function scaffoldEntity(name, options, structure, config, result) {
|
|
1845
|
+
async function scaffoldEntity(name, options, structure, config, result, dryRun = false) {
|
|
1287
1846
|
const namespace = options?.namespace || config.conventions.namespaces.domain;
|
|
1288
1847
|
const baseEntity = options?.baseEntity;
|
|
1289
1848
|
const isSystemEntity = options?.isSystemEntity || false;
|
|
@@ -1297,14 +1856,30 @@ namespace {{namespace}};
|
|
|
1297
1856
|
|
|
1298
1857
|
/// <summary>
|
|
1299
1858
|
/// {{name}} entity{{#if baseEntity}} extending {{baseEntity}}{{/if}}
|
|
1859
|
+
{{#unless isSystemEntity}}
|
|
1860
|
+
/// Tenant-scoped: data is isolated per tenant
|
|
1861
|
+
{{else}}
|
|
1862
|
+
/// System entity: platform-level, no tenant isolation
|
|
1863
|
+
{{/unless}}
|
|
1300
1864
|
/// </summary>
|
|
1301
1865
|
{{#if isSystemEntity}}
|
|
1302
1866
|
public class {{name}} : SystemEntity
|
|
1303
1867
|
{{else}}
|
|
1304
|
-
public class {{name}} : BaseEntity
|
|
1868
|
+
public class {{name}} : BaseEntity, ITenantEntity
|
|
1305
1869
|
{{/if}}
|
|
1306
1870
|
{
|
|
1871
|
+
{{#unless isSystemEntity}}
|
|
1872
|
+
// === MULTI-TENANT ===
|
|
1873
|
+
|
|
1874
|
+
/// <summary>
|
|
1875
|
+
/// Tenant identifier for multi-tenant isolation (required)
|
|
1876
|
+
/// </summary>
|
|
1877
|
+
public Guid TenantId { get; private set; }
|
|
1878
|
+
|
|
1879
|
+
{{/unless}}
|
|
1307
1880
|
{{#if baseEntity}}
|
|
1881
|
+
// === RELATIONSHIPS ===
|
|
1882
|
+
|
|
1308
1883
|
/// <summary>
|
|
1309
1884
|
/// Foreign key to {{baseEntity}}
|
|
1310
1885
|
/// </summary>
|
|
@@ -1341,11 +1916,17 @@ public class {{name}} : BaseEntity
|
|
|
1341
1916
|
};
|
|
1342
1917
|
}
|
|
1343
1918
|
{{else}}
|
|
1919
|
+
/// <param name="tenantId">Required tenant identifier</param>
|
|
1920
|
+
/// <param name="code">Unique code within tenant (will be lowercased)</param>
|
|
1921
|
+
/// <param name="createdBy">User who created this entity</param>
|
|
1344
1922
|
public static {{name}} Create(
|
|
1345
1923
|
Guid tenantId,
|
|
1346
1924
|
string code,
|
|
1347
1925
|
string? createdBy = null)
|
|
1348
1926
|
{
|
|
1927
|
+
if (tenantId == Guid.Empty)
|
|
1928
|
+
throw new ArgumentException("TenantId is required", nameof(tenantId));
|
|
1929
|
+
|
|
1349
1930
|
return new {{name}}
|
|
1350
1931
|
{
|
|
1351
1932
|
Id = Guid.NewGuid(),
|
|
@@ -1395,6 +1976,14 @@ using {{domainNamespace}};
|
|
|
1395
1976
|
|
|
1396
1977
|
namespace {{infrastructureNamespace}}.Persistence.Configurations;
|
|
1397
1978
|
|
|
1979
|
+
/// <summary>
|
|
1980
|
+
/// EF Core configuration for {{name}}
|
|
1981
|
+
{{#unless isSystemEntity}}
|
|
1982
|
+
/// Tenant-aware: includes tenant isolation query filter
|
|
1983
|
+
{{else}}
|
|
1984
|
+
/// System entity: no tenant isolation
|
|
1985
|
+
{{/unless}}
|
|
1986
|
+
/// </summary>
|
|
1398
1987
|
public class {{name}}Configuration : IEntityTypeConfiguration<{{name}}>
|
|
1399
1988
|
{
|
|
1400
1989
|
public void Configure(EntityTypeBuilder<{{name}}> builder)
|
|
@@ -1406,9 +1995,17 @@ public class {{name}}Configuration : IEntityTypeConfiguration<{{name}}>
|
|
|
1406
1995
|
builder.HasKey(e => e.Id);
|
|
1407
1996
|
|
|
1408
1997
|
{{#unless isSystemEntity}}
|
|
1409
|
-
//
|
|
1998
|
+
// ============================================
|
|
1999
|
+
// MULTI-TENANT CONFIGURATION
|
|
2000
|
+
// ============================================
|
|
2001
|
+
|
|
2002
|
+
// TenantId is required for tenant isolation
|
|
1410
2003
|
builder.Property(e => e.TenantId).IsRequired();
|
|
1411
|
-
builder.HasIndex(e => e.TenantId)
|
|
2004
|
+
builder.HasIndex(e => e.TenantId)
|
|
2005
|
+
.HasDatabaseName("IX_{{tablePrefix}}{{name}}s_TenantId");
|
|
2006
|
+
|
|
2007
|
+
// Tenant relationship (configured in Tenant configuration)
|
|
2008
|
+
// builder.HasOne<Tenant>().WithMany().HasForeignKey(e => e.TenantId);
|
|
1412
2009
|
|
|
1413
2010
|
// Code: lowercase, unique per tenant (filtered for soft delete)
|
|
1414
2011
|
builder.Property(e => e.Code).HasMaxLength(100).IsRequired();
|
|
@@ -1417,7 +2014,7 @@ public class {{name}}Configuration : IEntityTypeConfiguration<{{name}}>
|
|
|
1417
2014
|
.HasFilter("[IsDeleted] = 0")
|
|
1418
2015
|
.HasDatabaseName("IX_{{tablePrefix}}{{name}}s_Tenant_Code_Unique");
|
|
1419
2016
|
{{else}}
|
|
1420
|
-
// Code: lowercase, unique (filtered for soft delete)
|
|
2017
|
+
// Code: lowercase, unique globally (filtered for soft delete)
|
|
1421
2018
|
builder.Property(e => e.Code).HasMaxLength(100).IsRequired();
|
|
1422
2019
|
builder.HasIndex(e => e.Code)
|
|
1423
2020
|
.IsUnique()
|
|
@@ -1434,6 +2031,10 @@ public class {{name}}Configuration : IEntityTypeConfiguration<{{name}}>
|
|
|
1434
2031
|
builder.Property(e => e.DeletedBy).HasMaxLength(256);
|
|
1435
2032
|
|
|
1436
2033
|
{{#if baseEntity}}
|
|
2034
|
+
// ============================================
|
|
2035
|
+
// RELATIONSHIPS
|
|
2036
|
+
// ============================================
|
|
2037
|
+
|
|
1437
2038
|
// Relationship to {{baseEntity}} (1:1)
|
|
1438
2039
|
builder.HasOne(e => e.{{baseEntity}})
|
|
1439
2040
|
.WithOne()
|
|
@@ -1445,7 +2046,17 @@ public class {{name}}Configuration : IEntityTypeConfiguration<{{name}}>
|
|
|
1445
2046
|
.IsUnique();
|
|
1446
2047
|
{{/if}}
|
|
1447
2048
|
|
|
1448
|
-
//
|
|
2049
|
+
// ============================================
|
|
2050
|
+
// QUERY FILTERS
|
|
2051
|
+
// ============================================
|
|
2052
|
+
// Note: Global query filters are applied in DbContext.OnModelCreating
|
|
2053
|
+
// - Soft delete: .HasQueryFilter(e => !e.IsDeleted)
|
|
2054
|
+
{{#unless isSystemEntity}}
|
|
2055
|
+
// - Tenant isolation: .HasQueryFilter(e => e.TenantId == _tenantId)
|
|
2056
|
+
// Combined filter applied via ITenantEntity interface check
|
|
2057
|
+
{{/unless}}
|
|
2058
|
+
|
|
2059
|
+
// TODO: Add additional business-specific configuration
|
|
1449
2060
|
}
|
|
1450
2061
|
}
|
|
1451
2062
|
`;
|
|
@@ -1461,15 +2072,20 @@ public class {{name}}Configuration : IEntityTypeConfiguration<{{name}}>
|
|
|
1461
2072
|
};
|
|
1462
2073
|
const entityContent = Handlebars.compile(entityTemplate)(context);
|
|
1463
2074
|
const configContent = Handlebars.compile(configTemplate)(context);
|
|
1464
|
-
const
|
|
1465
|
-
const
|
|
1466
|
-
|
|
1467
|
-
await ensureDirectory(path7.join(infraPath, "Persistence", "Configurations"));
|
|
2075
|
+
const projectRoot = config.smartstack.projectPath;
|
|
2076
|
+
const domainPath = structure.domain || path7.join(projectRoot, "Domain");
|
|
2077
|
+
const infraPath = structure.infrastructure || path7.join(projectRoot, "Infrastructure");
|
|
1468
2078
|
const entityFilePath = path7.join(domainPath, `${name}.cs`);
|
|
1469
2079
|
const configFilePath = path7.join(infraPath, "Persistence", "Configurations", `${name}Configuration.cs`);
|
|
1470
|
-
|
|
2080
|
+
validatePathSecurity(entityFilePath, projectRoot);
|
|
2081
|
+
validatePathSecurity(configFilePath, projectRoot);
|
|
2082
|
+
if (!dryRun) {
|
|
2083
|
+
await ensureDirectory(domainPath);
|
|
2084
|
+
await ensureDirectory(path7.join(infraPath, "Persistence", "Configurations"));
|
|
2085
|
+
await writeText(entityFilePath, entityContent);
|
|
2086
|
+
await writeText(configFilePath, configContent);
|
|
2087
|
+
}
|
|
1471
2088
|
result.files.push({ path: entityFilePath, content: entityContent, type: "created" });
|
|
1472
|
-
await writeText(configFilePath, configContent);
|
|
1473
2089
|
result.files.push({ path: configFilePath, content: configContent, type: "created" });
|
|
1474
2090
|
result.instructions.push(`Add DbSet to ApplicationDbContext:`);
|
|
1475
2091
|
result.instructions.push(`public DbSet<${name}> ${name}s => Set<${name}>();`);
|
|
@@ -1483,19 +2099,23 @@ public class {{name}}Configuration : IEntityTypeConfiguration<{{name}}>
|
|
|
1483
2099
|
result.instructions.push("- IsDeleted, DeletedAt, DeletedBy (soft delete)");
|
|
1484
2100
|
result.instructions.push("- RowVersion (concurrency)");
|
|
1485
2101
|
}
|
|
1486
|
-
async function scaffoldController(name, options, structure, config, result) {
|
|
2102
|
+
async function scaffoldController(name, options, structure, config, result, dryRun = false) {
|
|
1487
2103
|
const namespace = options?.namespace || `${config.conventions.namespaces.api}.Controllers`;
|
|
2104
|
+
const navRoute = options?.navRoute;
|
|
2105
|
+
const navRouteSuffix = options?.navRouteSuffix;
|
|
2106
|
+
const routeAttribute = navRoute ? navRouteSuffix ? `[NavRoute("${navRoute}", Suffix = "${navRouteSuffix}")]` : `[NavRoute("${navRoute}")]` : `[Route("api/[controller]")]`;
|
|
2107
|
+
const navRouteUsing = navRoute ? "using SmartStack.Api.Core.Routing;\n" : "";
|
|
1488
2108
|
const controllerTemplate = `using Microsoft.AspNetCore.Authorization;
|
|
1489
2109
|
using Microsoft.AspNetCore.Mvc;
|
|
1490
2110
|
using Microsoft.Extensions.Logging;
|
|
1491
|
-
|
|
2111
|
+
${navRouteUsing}
|
|
1492
2112
|
namespace {{namespace}};
|
|
1493
2113
|
|
|
1494
2114
|
/// <summary>
|
|
1495
2115
|
/// API controller for {{name}} operations
|
|
1496
2116
|
/// </summary>
|
|
1497
2117
|
[ApiController]
|
|
1498
|
-
|
|
2118
|
+
{{routeAttribute}}
|
|
1499
2119
|
[Authorize]
|
|
1500
2120
|
public class {{name}}Controller : ControllerBase
|
|
1501
2121
|
{
|
|
@@ -1570,23 +2190,42 @@ public record Update{{name}}Request();
|
|
|
1570
2190
|
const context = {
|
|
1571
2191
|
namespace,
|
|
1572
2192
|
name,
|
|
1573
|
-
nameLower: name.charAt(0).toLowerCase() + name.slice(1)
|
|
2193
|
+
nameLower: name.charAt(0).toLowerCase() + name.slice(1),
|
|
2194
|
+
routeAttribute
|
|
1574
2195
|
};
|
|
1575
2196
|
const controllerContent = Handlebars.compile(controllerTemplate)(context);
|
|
1576
|
-
const
|
|
2197
|
+
const projectRoot = config.smartstack.projectPath;
|
|
2198
|
+
const apiPath = structure.api || path7.join(projectRoot, "Api");
|
|
1577
2199
|
const controllersPath = path7.join(apiPath, "Controllers");
|
|
1578
|
-
await ensureDirectory(controllersPath);
|
|
1579
2200
|
const controllerFilePath = path7.join(controllersPath, `${name}Controller.cs`);
|
|
1580
|
-
|
|
2201
|
+
validatePathSecurity(controllerFilePath, projectRoot);
|
|
2202
|
+
if (!dryRun) {
|
|
2203
|
+
await ensureDirectory(controllersPath);
|
|
2204
|
+
await writeText(controllerFilePath, controllerContent);
|
|
2205
|
+
}
|
|
1581
2206
|
result.files.push({ path: controllerFilePath, content: controllerContent, type: "created" });
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
}
|
|
1589
|
-
|
|
2207
|
+
if (navRoute) {
|
|
2208
|
+
result.instructions.push("Controller created with NavRoute (Navigation-based routing).");
|
|
2209
|
+
result.instructions.push(`NavRoute: ${navRoute}${navRouteSuffix ? ` (Suffix: ${navRouteSuffix})` : ""}`);
|
|
2210
|
+
result.instructions.push("");
|
|
2211
|
+
result.instructions.push("The actual API route will be resolved from Navigation entities at startup.");
|
|
2212
|
+
result.instructions.push("Ensure the navigation path exists in the database:");
|
|
2213
|
+
result.instructions.push(` Context > Application > Module > Section matching "${navRoute}"`);
|
|
2214
|
+
} else {
|
|
2215
|
+
result.instructions.push("Controller created with traditional routing.");
|
|
2216
|
+
result.instructions.push("");
|
|
2217
|
+
result.instructions.push("\u26A0\uFE0F Consider using NavRoute for navigation-based routing:");
|
|
2218
|
+
result.instructions.push(` [NavRoute("context.application.module")]`);
|
|
2219
|
+
result.instructions.push("");
|
|
2220
|
+
result.instructions.push("API endpoints (with traditional routing):");
|
|
2221
|
+
result.instructions.push(` GET /api/${name.toLowerCase()}`);
|
|
2222
|
+
result.instructions.push(` GET /api/${name.toLowerCase()}/{id}`);
|
|
2223
|
+
result.instructions.push(` POST /api/${name.toLowerCase()}`);
|
|
2224
|
+
result.instructions.push(` PUT /api/${name.toLowerCase()}/{id}`);
|
|
2225
|
+
result.instructions.push(` DELETE /api/${name.toLowerCase()}/{id}`);
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
async function scaffoldComponent(name, options, structure, config, result, dryRun = false) {
|
|
1590
2229
|
const componentTemplate = `import React, { useState, useEffect } from 'react';
|
|
1591
2230
|
|
|
1592
2231
|
interface {{name}}Props {
|
|
@@ -1737,51 +2376,553 @@ export function use{{name}}(options: Use{{name}}Options = {}) {
|
|
|
1737
2376
|
};
|
|
1738
2377
|
const componentContent = Handlebars.compile(componentTemplate)(context);
|
|
1739
2378
|
const hookContent = Handlebars.compile(hookTemplate)(context);
|
|
1740
|
-
const
|
|
2379
|
+
const projectRoot = config.smartstack.projectPath;
|
|
2380
|
+
const webPath = structure.web || path7.join(projectRoot, "web", "smartstack-web");
|
|
1741
2381
|
const componentsPath = options?.outputPath || path7.join(webPath, "src", "components");
|
|
1742
2382
|
const hooksPath = path7.join(webPath, "src", "hooks");
|
|
1743
|
-
await ensureDirectory(componentsPath);
|
|
1744
|
-
await ensureDirectory(hooksPath);
|
|
1745
2383
|
const componentFilePath = path7.join(componentsPath, `${name}.tsx`);
|
|
1746
2384
|
const hookFilePath = path7.join(hooksPath, `use${name}.ts`);
|
|
1747
|
-
|
|
2385
|
+
validatePathSecurity(componentFilePath, projectRoot);
|
|
2386
|
+
validatePathSecurity(hookFilePath, projectRoot);
|
|
2387
|
+
if (!dryRun) {
|
|
2388
|
+
await ensureDirectory(componentsPath);
|
|
2389
|
+
await ensureDirectory(hooksPath);
|
|
2390
|
+
await writeText(componentFilePath, componentContent);
|
|
2391
|
+
await writeText(hookFilePath, hookContent);
|
|
2392
|
+
}
|
|
1748
2393
|
result.files.push({ path: componentFilePath, content: componentContent, type: "created" });
|
|
1749
|
-
await writeText(hookFilePath, hookContent);
|
|
1750
2394
|
result.files.push({ path: hookFilePath, content: hookContent, type: "created" });
|
|
1751
2395
|
result.instructions.push("Import and use the component:");
|
|
1752
2396
|
result.instructions.push(`import { ${name} } from './components/${name}';`);
|
|
1753
2397
|
result.instructions.push(`import { use${name} } from './hooks/use${name}';`);
|
|
1754
2398
|
}
|
|
1755
|
-
function
|
|
1756
|
-
const
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
2399
|
+
async function scaffoldTest(name, options, structure, config, result, dryRun = false) {
|
|
2400
|
+
const isSystemEntity = options?.isSystemEntity || false;
|
|
2401
|
+
const serviceTestTemplate = `using System;
|
|
2402
|
+
using System.Threading;
|
|
2403
|
+
using System.Threading.Tasks;
|
|
2404
|
+
using Microsoft.Extensions.Logging;
|
|
2405
|
+
using Moq;
|
|
2406
|
+
using Xunit;
|
|
2407
|
+
using FluentAssertions;
|
|
2408
|
+
using ${config.conventions.namespaces.application}.Services;
|
|
2409
|
+
|
|
2410
|
+
namespace ${config.conventions.namespaces.application}.Tests.Services;
|
|
2411
|
+
|
|
2412
|
+
/// <summary>
|
|
2413
|
+
/// Unit tests for {{name}}Service
|
|
2414
|
+
/// </summary>
|
|
2415
|
+
public class {{name}}ServiceTests
|
|
2416
|
+
{
|
|
2417
|
+
private readonly Mock<ILogger<{{name}}Service>> _loggerMock;
|
|
2418
|
+
private readonly {{name}}Service _sut;
|
|
2419
|
+
|
|
2420
|
+
public {{name}}ServiceTests()
|
|
2421
|
+
{
|
|
2422
|
+
_loggerMock = new Mock<ILogger<{{name}}Service>>();
|
|
2423
|
+
_sut = new {{name}}Service(_loggerMock.Object);
|
|
1774
2424
|
}
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
2425
|
+
|
|
2426
|
+
[Fact]
|
|
2427
|
+
public async Task GetByIdAsync_ShouldReturnEntity_WhenExists()
|
|
2428
|
+
{
|
|
2429
|
+
// Arrange
|
|
2430
|
+
var id = Guid.NewGuid();
|
|
2431
|
+
{{#unless isSystemEntity}}
|
|
2432
|
+
var tenantId = Guid.NewGuid();
|
|
2433
|
+
{{/unless}}
|
|
2434
|
+
|
|
2435
|
+
// Act
|
|
2436
|
+
var result = await _sut.GetByIdAsync(CancellationToken.None);
|
|
2437
|
+
|
|
2438
|
+
// Assert
|
|
2439
|
+
// TODO: Implement actual assertion
|
|
2440
|
+
result.Should().NotBeNull();
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
[Fact]
|
|
2444
|
+
public async Task GetAllAsync_ShouldReturnList()
|
|
2445
|
+
{
|
|
2446
|
+
// Arrange
|
|
2447
|
+
{{#unless isSystemEntity}}
|
|
2448
|
+
var tenantId = Guid.NewGuid();
|
|
2449
|
+
{{/unless}}
|
|
2450
|
+
|
|
2451
|
+
// Act
|
|
2452
|
+
var result = await _sut.GetAllAsync(CancellationToken.None);
|
|
2453
|
+
|
|
2454
|
+
// Assert
|
|
2455
|
+
// TODO: Implement actual assertion
|
|
2456
|
+
result.Should().NotBeNull();
|
|
2457
|
+
}
|
|
2458
|
+
|
|
2459
|
+
[Fact]
|
|
2460
|
+
public async Task CreateAsync_ShouldCreateEntity()
|
|
2461
|
+
{
|
|
2462
|
+
// Arrange
|
|
2463
|
+
{{#unless isSystemEntity}}
|
|
2464
|
+
var tenantId = Guid.NewGuid();
|
|
2465
|
+
{{/unless}}
|
|
2466
|
+
|
|
2467
|
+
// Act
|
|
2468
|
+
var result = await _sut.CreateAsync(CancellationToken.None);
|
|
2469
|
+
|
|
2470
|
+
// Assert
|
|
2471
|
+
// TODO: Implement actual assertion
|
|
2472
|
+
result.Should().NotBeNull();
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2475
|
+
[Fact]
|
|
2476
|
+
public async Task UpdateAsync_ShouldUpdateEntity_WhenExists()
|
|
2477
|
+
{
|
|
2478
|
+
// Arrange
|
|
2479
|
+
var id = Guid.NewGuid();
|
|
2480
|
+
|
|
2481
|
+
// Act
|
|
2482
|
+
var result = await _sut.UpdateAsync(CancellationToken.None);
|
|
2483
|
+
|
|
2484
|
+
// Assert
|
|
2485
|
+
// TODO: Implement actual assertion
|
|
2486
|
+
result.Should().NotBeNull();
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2489
|
+
[Fact]
|
|
2490
|
+
public async Task DeleteAsync_ShouldSoftDelete_WhenExists()
|
|
2491
|
+
{
|
|
2492
|
+
// Arrange
|
|
2493
|
+
var id = Guid.NewGuid();
|
|
2494
|
+
|
|
2495
|
+
// Act
|
|
2496
|
+
var result = await _sut.DeleteAsync(CancellationToken.None);
|
|
2497
|
+
|
|
2498
|
+
// Assert
|
|
2499
|
+
// TODO: Implement actual assertion
|
|
2500
|
+
result.Should().NotBeNull();
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
`;
|
|
2504
|
+
const context = {
|
|
2505
|
+
name,
|
|
2506
|
+
isSystemEntity
|
|
2507
|
+
};
|
|
2508
|
+
const testContent = Handlebars.compile(serviceTestTemplate)(context);
|
|
2509
|
+
const testsPath = structure.application ? path7.join(path7.dirname(structure.application), `${path7.basename(structure.application)}.Tests`, "Services") : path7.join(config.smartstack.projectPath, "Application.Tests", "Services");
|
|
2510
|
+
const testFilePath = path7.join(testsPath, `${name}ServiceTests.cs`);
|
|
2511
|
+
if (!dryRun) {
|
|
2512
|
+
await ensureDirectory(testsPath);
|
|
2513
|
+
await writeText(testFilePath, testContent);
|
|
2514
|
+
}
|
|
2515
|
+
result.files.push({ path: testFilePath, content: testContent, type: "created" });
|
|
2516
|
+
result.instructions.push("Run tests:");
|
|
2517
|
+
result.instructions.push(`dotnet test --filter "FullyQualifiedName~${name}ServiceTests"`);
|
|
2518
|
+
result.instructions.push("");
|
|
2519
|
+
result.instructions.push("Required packages:");
|
|
2520
|
+
result.instructions.push("- xunit");
|
|
2521
|
+
result.instructions.push("- Moq");
|
|
2522
|
+
result.instructions.push("- FluentAssertions");
|
|
2523
|
+
}
|
|
2524
|
+
async function scaffoldDtos(name, options, structure, config, result, dryRun = false) {
|
|
2525
|
+
const namespace = options?.namespace || `${config.conventions.namespaces.application}.DTOs`;
|
|
2526
|
+
const isSystemEntity = options?.isSystemEntity || false;
|
|
2527
|
+
const properties = options?.entityProperties || [
|
|
2528
|
+
{ name: "Name", type: "string", required: true, maxLength: 200 },
|
|
2529
|
+
{ name: "Description", type: "string?", required: false, maxLength: 500 }
|
|
2530
|
+
];
|
|
2531
|
+
const responseDtoTemplate = `using System;
|
|
2532
|
+
|
|
2533
|
+
namespace {{namespace}};
|
|
2534
|
+
|
|
2535
|
+
/// <summary>
|
|
2536
|
+
/// Response DTO for {{name}}
|
|
2537
|
+
/// </summary>
|
|
2538
|
+
public record {{name}}ResponseDto
|
|
2539
|
+
{
|
|
2540
|
+
/// <summary>Unique identifier</summary>
|
|
2541
|
+
public Guid Id { get; init; }
|
|
2542
|
+
|
|
2543
|
+
{{#unless isSystemEntity}}
|
|
2544
|
+
/// <summary>Tenant identifier</summary>
|
|
2545
|
+
public Guid TenantId { get; init; }
|
|
2546
|
+
|
|
2547
|
+
{{/unless}}
|
|
2548
|
+
/// <summary>Unique code</summary>
|
|
2549
|
+
public string Code { get; init; } = string.Empty;
|
|
2550
|
+
|
|
2551
|
+
{{#each properties}}
|
|
2552
|
+
/// <summary>{{name}}</summary>
|
|
2553
|
+
public {{type}} {{name}} { get; init; }{{#if (eq type "string")}} = string.Empty;{{/if}}
|
|
2554
|
+
|
|
2555
|
+
{{/each}}
|
|
2556
|
+
/// <summary>Creation timestamp</summary>
|
|
2557
|
+
public DateTime CreatedAt { get; init; }
|
|
2558
|
+
|
|
2559
|
+
/// <summary>Last update timestamp</summary>
|
|
2560
|
+
public DateTime? UpdatedAt { get; init; }
|
|
2561
|
+
|
|
2562
|
+
/// <summary>Created by user</summary>
|
|
2563
|
+
public string? CreatedBy { get; init; }
|
|
2564
|
+
}
|
|
2565
|
+
`;
|
|
2566
|
+
const createDtoTemplate = `using System;
|
|
2567
|
+
using System.ComponentModel.DataAnnotations;
|
|
2568
|
+
|
|
2569
|
+
namespace {{namespace}};
|
|
2570
|
+
|
|
2571
|
+
/// <summary>
|
|
2572
|
+
/// DTO for creating a new {{name}}
|
|
2573
|
+
/// </summary>
|
|
2574
|
+
public record Create{{name}}Dto
|
|
2575
|
+
{
|
|
2576
|
+
/// <summary>Unique code (will be lowercased)</summary>
|
|
2577
|
+
[Required]
|
|
2578
|
+
[MaxLength(100)]
|
|
2579
|
+
public string Code { get; init; } = string.Empty;
|
|
2580
|
+
|
|
2581
|
+
{{#each properties}}
|
|
2582
|
+
{{#if required}}
|
|
2583
|
+
/// <summary>{{name}} (required)</summary>
|
|
2584
|
+
[Required]
|
|
2585
|
+
{{#if maxLength}}
|
|
2586
|
+
[MaxLength({{maxLength}})]
|
|
2587
|
+
{{/if}}
|
|
2588
|
+
public {{type}} {{name}} { get; init; }{{#if (eq type "string")}} = string.Empty;{{/if}}
|
|
2589
|
+
|
|
2590
|
+
{{else}}
|
|
2591
|
+
/// <summary>{{name}} (optional)</summary>
|
|
2592
|
+
{{#if maxLength}}
|
|
2593
|
+
[MaxLength({{maxLength}})]
|
|
2594
|
+
{{/if}}
|
|
2595
|
+
public {{type}} {{name}} { get; init; }
|
|
2596
|
+
|
|
2597
|
+
{{/if}}
|
|
2598
|
+
{{/each}}
|
|
2599
|
+
}
|
|
2600
|
+
`;
|
|
2601
|
+
const updateDtoTemplate = `using System;
|
|
2602
|
+
using System.ComponentModel.DataAnnotations;
|
|
2603
|
+
|
|
2604
|
+
namespace {{namespace}};
|
|
2605
|
+
|
|
2606
|
+
/// <summary>
|
|
2607
|
+
/// DTO for updating an existing {{name}}
|
|
2608
|
+
/// </summary>
|
|
2609
|
+
public record Update{{name}}Dto
|
|
2610
|
+
{
|
|
2611
|
+
{{#each properties}}
|
|
2612
|
+
{{#if required}}
|
|
2613
|
+
/// <summary>{{name}} (required)</summary>
|
|
2614
|
+
[Required]
|
|
2615
|
+
{{#if maxLength}}
|
|
2616
|
+
[MaxLength({{maxLength}})]
|
|
2617
|
+
{{/if}}
|
|
2618
|
+
public {{type}} {{name}} { get; init; }{{#if (eq type "string")}} = string.Empty;{{/if}}
|
|
2619
|
+
|
|
2620
|
+
{{else}}
|
|
2621
|
+
/// <summary>{{name}} (optional)</summary>
|
|
2622
|
+
{{#if maxLength}}
|
|
2623
|
+
[MaxLength({{maxLength}})]
|
|
2624
|
+
{{/if}}
|
|
2625
|
+
public {{type}} {{name}} { get; init; }
|
|
2626
|
+
|
|
2627
|
+
{{/if}}
|
|
2628
|
+
{{/each}}
|
|
2629
|
+
}
|
|
2630
|
+
`;
|
|
2631
|
+
Handlebars.registerHelper("eq", (a, b) => a === b);
|
|
2632
|
+
const context = {
|
|
2633
|
+
namespace,
|
|
2634
|
+
name,
|
|
2635
|
+
isSystemEntity,
|
|
2636
|
+
properties
|
|
2637
|
+
};
|
|
2638
|
+
const responseContent = Handlebars.compile(responseDtoTemplate)(context);
|
|
2639
|
+
const createContent = Handlebars.compile(createDtoTemplate)(context);
|
|
2640
|
+
const updateContent = Handlebars.compile(updateDtoTemplate)(context);
|
|
2641
|
+
const basePath = structure.application || config.smartstack.projectPath;
|
|
2642
|
+
const dtosPath = path7.join(basePath, "DTOs", name);
|
|
2643
|
+
const responseFilePath = path7.join(dtosPath, `${name}ResponseDto.cs`);
|
|
2644
|
+
const createFilePath = path7.join(dtosPath, `Create${name}Dto.cs`);
|
|
2645
|
+
const updateFilePath = path7.join(dtosPath, `Update${name}Dto.cs`);
|
|
2646
|
+
if (!dryRun) {
|
|
2647
|
+
await ensureDirectory(dtosPath);
|
|
2648
|
+
await writeText(responseFilePath, responseContent);
|
|
2649
|
+
await writeText(createFilePath, createContent);
|
|
2650
|
+
await writeText(updateFilePath, updateContent);
|
|
2651
|
+
}
|
|
2652
|
+
result.files.push({ path: responseFilePath, content: responseContent, type: "created" });
|
|
2653
|
+
result.files.push({ path: createFilePath, content: createContent, type: "created" });
|
|
2654
|
+
result.files.push({ path: updateFilePath, content: updateContent, type: "created" });
|
|
2655
|
+
result.instructions.push("DTOs generated:");
|
|
2656
|
+
result.instructions.push(`- ${name}ResponseDto: For API responses`);
|
|
2657
|
+
result.instructions.push(`- Create${name}Dto: For POST requests`);
|
|
2658
|
+
result.instructions.push(`- Update${name}Dto: For PUT requests`);
|
|
2659
|
+
}
|
|
2660
|
+
async function scaffoldValidator(name, options, structure, config, result, dryRun = false) {
|
|
2661
|
+
const namespace = options?.namespace || `${config.conventions.namespaces.application}.Validators`;
|
|
2662
|
+
const properties = options?.entityProperties || [
|
|
2663
|
+
{ name: "Name", type: "string", required: true, maxLength: 200 },
|
|
2664
|
+
{ name: "Description", type: "string?", required: false, maxLength: 500 }
|
|
2665
|
+
];
|
|
2666
|
+
const createValidatorTemplate = `using FluentValidation;
|
|
2667
|
+
using ${config.conventions.namespaces.application}.DTOs;
|
|
2668
|
+
|
|
2669
|
+
namespace {{namespace}};
|
|
2670
|
+
|
|
2671
|
+
/// <summary>
|
|
2672
|
+
/// Validator for Create{{name}}Dto
|
|
2673
|
+
/// </summary>
|
|
2674
|
+
public class Create{{name}}DtoValidator : AbstractValidator<Create{{name}}Dto>
|
|
2675
|
+
{
|
|
2676
|
+
public Create{{name}}DtoValidator()
|
|
2677
|
+
{
|
|
2678
|
+
RuleFor(x => x.Code)
|
|
2679
|
+
.NotEmpty().WithMessage("Code is required")
|
|
2680
|
+
.MaximumLength(100).WithMessage("Code must not exceed 100 characters")
|
|
2681
|
+
.Matches("^[a-z0-9_]+$").WithMessage("Code must be lowercase alphanumeric with underscores");
|
|
2682
|
+
|
|
2683
|
+
{{#each properties}}
|
|
2684
|
+
{{#if required}}
|
|
2685
|
+
RuleFor(x => x.{{name}})
|
|
2686
|
+
.NotEmpty().WithMessage("{{name}} is required"){{#if maxLength}}
|
|
2687
|
+
.MaximumLength({{maxLength}}).WithMessage("{{name}} must not exceed {{maxLength}} characters"){{/if}};
|
|
2688
|
+
|
|
2689
|
+
{{else}}
|
|
2690
|
+
{{#if maxLength}}
|
|
2691
|
+
RuleFor(x => x.{{name}})
|
|
2692
|
+
.MaximumLength({{maxLength}}).WithMessage("{{name}} must not exceed {{maxLength}} characters")
|
|
2693
|
+
.When(x => !string.IsNullOrEmpty(x.{{name}}));
|
|
2694
|
+
|
|
2695
|
+
{{/if}}
|
|
2696
|
+
{{/if}}
|
|
2697
|
+
{{/each}}
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
`;
|
|
2701
|
+
const updateValidatorTemplate = `using FluentValidation;
|
|
2702
|
+
using ${config.conventions.namespaces.application}.DTOs;
|
|
2703
|
+
|
|
2704
|
+
namespace {{namespace}};
|
|
2705
|
+
|
|
2706
|
+
/// <summary>
|
|
2707
|
+
/// Validator for Update{{name}}Dto
|
|
2708
|
+
/// </summary>
|
|
2709
|
+
public class Update{{name}}DtoValidator : AbstractValidator<Update{{name}}Dto>
|
|
2710
|
+
{
|
|
2711
|
+
public Update{{name}}DtoValidator()
|
|
2712
|
+
{
|
|
2713
|
+
{{#each properties}}
|
|
2714
|
+
{{#if required}}
|
|
2715
|
+
RuleFor(x => x.{{name}})
|
|
2716
|
+
.NotEmpty().WithMessage("{{name}} is required"){{#if maxLength}}
|
|
2717
|
+
.MaximumLength({{maxLength}}).WithMessage("{{name}} must not exceed {{maxLength}} characters"){{/if}};
|
|
2718
|
+
|
|
2719
|
+
{{else}}
|
|
2720
|
+
{{#if maxLength}}
|
|
2721
|
+
RuleFor(x => x.{{name}})
|
|
2722
|
+
.MaximumLength({{maxLength}}).WithMessage("{{name}} must not exceed {{maxLength}} characters")
|
|
2723
|
+
.When(x => !string.IsNullOrEmpty(x.{{name}}));
|
|
2724
|
+
|
|
2725
|
+
{{/if}}
|
|
2726
|
+
{{/if}}
|
|
2727
|
+
{{/each}}
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
`;
|
|
2731
|
+
const context = {
|
|
2732
|
+
namespace,
|
|
2733
|
+
name,
|
|
2734
|
+
properties
|
|
2735
|
+
};
|
|
2736
|
+
const createValidatorContent = Handlebars.compile(createValidatorTemplate)(context);
|
|
2737
|
+
const updateValidatorContent = Handlebars.compile(updateValidatorTemplate)(context);
|
|
2738
|
+
const basePath = structure.application || config.smartstack.projectPath;
|
|
2739
|
+
const validatorsPath = path7.join(basePath, "Validators");
|
|
2740
|
+
const createValidatorFilePath = path7.join(validatorsPath, `Create${name}DtoValidator.cs`);
|
|
2741
|
+
const updateValidatorFilePath = path7.join(validatorsPath, `Update${name}DtoValidator.cs`);
|
|
2742
|
+
if (!dryRun) {
|
|
2743
|
+
await ensureDirectory(validatorsPath);
|
|
2744
|
+
await writeText(createValidatorFilePath, createValidatorContent);
|
|
2745
|
+
await writeText(updateValidatorFilePath, updateValidatorContent);
|
|
2746
|
+
}
|
|
2747
|
+
result.files.push({ path: createValidatorFilePath, content: createValidatorContent, type: "created" });
|
|
2748
|
+
result.files.push({ path: updateValidatorFilePath, content: updateValidatorContent, type: "created" });
|
|
2749
|
+
result.instructions.push("Register validators in DI:");
|
|
2750
|
+
result.instructions.push(`services.AddValidatorsFromAssemblyContaining<Create${name}DtoValidator>();`);
|
|
2751
|
+
result.instructions.push("");
|
|
2752
|
+
result.instructions.push("Required package: FluentValidation.DependencyInjectionExtensions");
|
|
2753
|
+
}
|
|
2754
|
+
async function scaffoldRepository(name, options, structure, config, result, dryRun = false) {
|
|
2755
|
+
const isSystemEntity = options?.isSystemEntity || false;
|
|
2756
|
+
const interfaceTemplate = `using System;
|
|
2757
|
+
using System.Collections.Generic;
|
|
2758
|
+
using System.Threading;
|
|
2759
|
+
using System.Threading.Tasks;
|
|
2760
|
+
using ${config.conventions.namespaces.domain};
|
|
2761
|
+
|
|
2762
|
+
namespace ${config.conventions.namespaces.application}.Repositories;
|
|
2763
|
+
|
|
2764
|
+
/// <summary>
|
|
2765
|
+
/// Repository interface for {{name}} entity
|
|
2766
|
+
/// </summary>
|
|
2767
|
+
public interface I{{name}}Repository
|
|
2768
|
+
{
|
|
2769
|
+
/// <summary>Get entity by ID</summary>
|
|
2770
|
+
Task<{{name}}?> GetByIdAsync(Guid id, CancellationToken ct = default);
|
|
2771
|
+
|
|
2772
|
+
/// <summary>Get entity by code</summary>
|
|
2773
|
+
Task<{{name}}?> GetByCodeAsync({{#unless isSystemEntity}}Guid tenantId, {{/unless}}string code, CancellationToken ct = default);
|
|
2774
|
+
|
|
2775
|
+
/// <summary>Get all entities{{#unless isSystemEntity}} for tenant{{/unless}}</summary>
|
|
2776
|
+
Task<IReadOnlyList<{{name}}>> GetAllAsync({{#unless isSystemEntity}}Guid tenantId, {{/unless}}CancellationToken ct = default);
|
|
2777
|
+
|
|
2778
|
+
/// <summary>Check if code exists{{#unless isSystemEntity}} in tenant{{/unless}}</summary>
|
|
2779
|
+
Task<bool> ExistsAsync({{#unless isSystemEntity}}Guid tenantId, {{/unless}}string code, CancellationToken ct = default);
|
|
2780
|
+
|
|
2781
|
+
/// <summary>Add new entity</summary>
|
|
2782
|
+
Task<{{name}}> AddAsync({{name}} entity, CancellationToken ct = default);
|
|
2783
|
+
|
|
2784
|
+
/// <summary>Update entity</summary>
|
|
2785
|
+
Task UpdateAsync({{name}} entity, CancellationToken ct = default);
|
|
2786
|
+
|
|
2787
|
+
/// <summary>Soft delete entity</summary>
|
|
2788
|
+
Task DeleteAsync({{name}} entity, CancellationToken ct = default);
|
|
2789
|
+
}
|
|
2790
|
+
`;
|
|
2791
|
+
const implementationTemplate = `using System;
|
|
2792
|
+
using System.Collections.Generic;
|
|
2793
|
+
using System.Linq;
|
|
2794
|
+
using System.Threading;
|
|
2795
|
+
using System.Threading.Tasks;
|
|
2796
|
+
using Microsoft.EntityFrameworkCore;
|
|
2797
|
+
using ${config.conventions.namespaces.domain};
|
|
2798
|
+
using ${config.conventions.namespaces.infrastructure}.Persistence;
|
|
2799
|
+
|
|
2800
|
+
namespace ${config.conventions.namespaces.infrastructure}.Repositories;
|
|
2801
|
+
|
|
2802
|
+
/// <summary>
|
|
2803
|
+
/// Repository implementation for {{name}} entity
|
|
2804
|
+
/// </summary>
|
|
2805
|
+
public class {{name}}Repository : I{{name}}Repository
|
|
2806
|
+
{
|
|
2807
|
+
private readonly ApplicationDbContext _context;
|
|
2808
|
+
|
|
2809
|
+
public {{name}}Repository(ApplicationDbContext context)
|
|
2810
|
+
{
|
|
2811
|
+
_context = context;
|
|
2812
|
+
}
|
|
2813
|
+
|
|
2814
|
+
/// <inheritdoc />
|
|
2815
|
+
public async Task<{{name}}?> GetByIdAsync(Guid id, CancellationToken ct = default)
|
|
2816
|
+
{
|
|
2817
|
+
return await _context.{{name}}s
|
|
2818
|
+
.FirstOrDefaultAsync(e => e.Id == id, ct);
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2821
|
+
/// <inheritdoc />
|
|
2822
|
+
public async Task<{{name}}?> GetByCodeAsync({{#unless isSystemEntity}}Guid tenantId, {{/unless}}string code, CancellationToken ct = default)
|
|
2823
|
+
{
|
|
2824
|
+
return await _context.{{name}}s
|
|
2825
|
+
.FirstOrDefaultAsync(e => {{#unless isSystemEntity}}e.TenantId == tenantId && {{/unless}}e.Code == code.ToLowerInvariant(), ct);
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2828
|
+
/// <inheritdoc />
|
|
2829
|
+
public async Task<IReadOnlyList<{{name}}>> GetAllAsync({{#unless isSystemEntity}}Guid tenantId, {{/unless}}CancellationToken ct = default)
|
|
2830
|
+
{
|
|
2831
|
+
return await _context.{{name}}s
|
|
2832
|
+
{{#unless isSystemEntity}}.Where(e => e.TenantId == tenantId){{/unless}}
|
|
2833
|
+
.OrderBy(e => e.Code)
|
|
2834
|
+
.ToListAsync(ct);
|
|
2835
|
+
}
|
|
2836
|
+
|
|
2837
|
+
/// <inheritdoc />
|
|
2838
|
+
public async Task<bool> ExistsAsync({{#unless isSystemEntity}}Guid tenantId, {{/unless}}string code, CancellationToken ct = default)
|
|
2839
|
+
{
|
|
2840
|
+
return await _context.{{name}}s
|
|
2841
|
+
.AnyAsync(e => {{#unless isSystemEntity}}e.TenantId == tenantId && {{/unless}}e.Code == code.ToLowerInvariant(), ct);
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
/// <inheritdoc />
|
|
2845
|
+
public async Task<{{name}}> AddAsync({{name}} entity, CancellationToken ct = default)
|
|
2846
|
+
{
|
|
2847
|
+
await _context.{{name}}s.AddAsync(entity, ct);
|
|
2848
|
+
await _context.SaveChangesAsync(ct);
|
|
2849
|
+
return entity;
|
|
2850
|
+
}
|
|
2851
|
+
|
|
2852
|
+
/// <inheritdoc />
|
|
2853
|
+
public async Task UpdateAsync({{name}} entity, CancellationToken ct = default)
|
|
2854
|
+
{
|
|
2855
|
+
_context.{{name}}s.Update(entity);
|
|
2856
|
+
await _context.SaveChangesAsync(ct);
|
|
2857
|
+
}
|
|
2858
|
+
|
|
2859
|
+
/// <inheritdoc />
|
|
2860
|
+
public async Task DeleteAsync({{name}} entity, CancellationToken ct = default)
|
|
2861
|
+
{
|
|
2862
|
+
entity.SoftDelete();
|
|
2863
|
+
await UpdateAsync(entity, ct);
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
`;
|
|
2867
|
+
const context = {
|
|
2868
|
+
name,
|
|
2869
|
+
isSystemEntity
|
|
2870
|
+
};
|
|
2871
|
+
const interfaceContent = Handlebars.compile(interfaceTemplate)(context);
|
|
2872
|
+
const implementationContent = Handlebars.compile(implementationTemplate)(context);
|
|
2873
|
+
const appPath = structure.application || config.smartstack.projectPath;
|
|
2874
|
+
const infraPath = structure.infrastructure || path7.join(config.smartstack.projectPath, "Infrastructure");
|
|
2875
|
+
const interfaceFilePath = path7.join(appPath, "Repositories", `I${name}Repository.cs`);
|
|
2876
|
+
const implementationFilePath = path7.join(infraPath, "Repositories", `${name}Repository.cs`);
|
|
2877
|
+
if (!dryRun) {
|
|
2878
|
+
await ensureDirectory(path7.join(appPath, "Repositories"));
|
|
2879
|
+
await ensureDirectory(path7.join(infraPath, "Repositories"));
|
|
2880
|
+
await writeText(interfaceFilePath, interfaceContent);
|
|
2881
|
+
await writeText(implementationFilePath, implementationContent);
|
|
2882
|
+
}
|
|
2883
|
+
result.files.push({ path: interfaceFilePath, content: interfaceContent, type: "created" });
|
|
2884
|
+
result.files.push({ path: implementationFilePath, content: implementationContent, type: "created" });
|
|
2885
|
+
result.instructions.push("Register repository in DI:");
|
|
2886
|
+
result.instructions.push(`services.AddScoped<I${name}Repository, ${name}Repository>();`);
|
|
2887
|
+
}
|
|
2888
|
+
function formatResult3(result, type, name, dryRun = false) {
|
|
2889
|
+
const lines = [];
|
|
2890
|
+
if (dryRun) {
|
|
2891
|
+
lines.push(`# \u{1F50D} DRY-RUN: Scaffold ${type}: ${name}`);
|
|
2892
|
+
lines.push("");
|
|
2893
|
+
lines.push("> **Preview mode**: No files were written. Review the generated code below.");
|
|
2894
|
+
lines.push("> To create files, run without `dryRun: true`");
|
|
2895
|
+
lines.push("");
|
|
2896
|
+
} else {
|
|
2897
|
+
lines.push(`# Scaffold ${type}: ${name}`);
|
|
2898
|
+
lines.push("");
|
|
2899
|
+
}
|
|
2900
|
+
if (result.success) {
|
|
2901
|
+
lines.push(dryRun ? "## \u{1F4C4} Files to Generate" : "## \u2705 Files Generated");
|
|
2902
|
+
lines.push("");
|
|
2903
|
+
for (const file of result.files) {
|
|
2904
|
+
lines.push(`### ${file.type === "created" ? "\u{1F4C4}" : "\u270F\uFE0F"} ${path7.basename(file.path)}`);
|
|
2905
|
+
lines.push(`**Path**: \`${file.path}\``);
|
|
2906
|
+
lines.push("");
|
|
2907
|
+
lines.push("```" + (file.path.endsWith(".cs") ? "csharp" : "typescript"));
|
|
2908
|
+
const contentLines = file.content.split("\n").slice(0, 50);
|
|
2909
|
+
lines.push(contentLines.join("\n"));
|
|
2910
|
+
if (file.content.split("\n").length > 50) {
|
|
2911
|
+
lines.push("// ... (truncated)");
|
|
2912
|
+
}
|
|
2913
|
+
lines.push("```");
|
|
2914
|
+
lines.push("");
|
|
2915
|
+
}
|
|
2916
|
+
if (result.instructions.length > 0) {
|
|
2917
|
+
lines.push("## \u{1F4CB} Next Steps");
|
|
2918
|
+
lines.push("");
|
|
2919
|
+
for (const instruction of result.instructions) {
|
|
2920
|
+
if (instruction.startsWith("services.") || instruction.startsWith("public DbSet")) {
|
|
2921
|
+
lines.push("```csharp");
|
|
2922
|
+
lines.push(instruction);
|
|
2923
|
+
lines.push("```");
|
|
2924
|
+
} else if (instruction.startsWith("dotnet ")) {
|
|
2925
|
+
lines.push("```bash");
|
|
1785
2926
|
lines.push(instruction);
|
|
1786
2927
|
lines.push("```");
|
|
1787
2928
|
} else if (instruction.startsWith("import ")) {
|
|
@@ -1891,7 +3032,7 @@ async function fetchFromSwagger(apiUrl) {
|
|
|
1891
3032
|
required: p.required || false,
|
|
1892
3033
|
description: p.description
|
|
1893
3034
|
})),
|
|
1894
|
-
requestBody: operation.requestBody ?
|
|
3035
|
+
requestBody: operation.requestBody ? { type: "object", required: true } : void 0,
|
|
1895
3036
|
responses: Object.entries(operation.responses || {}).map(([status, resp]) => ({
|
|
1896
3037
|
status: parseInt(status, 10),
|
|
1897
3038
|
description: resp.description
|
|
@@ -1942,9 +3083,14 @@ async function parseControllers(structure) {
|
|
|
1942
3083
|
});
|
|
1943
3084
|
}
|
|
1944
3085
|
}
|
|
3086
|
+
let requestBody;
|
|
1945
3087
|
if (afterAttribute.includes("[FromBody]")) {
|
|
1946
3088
|
const bodyMatch = afterAttribute.match(/\[FromBody\]\s*(\w+)\s+(\w+)/);
|
|
1947
3089
|
if (bodyMatch) {
|
|
3090
|
+
requestBody = {
|
|
3091
|
+
type: bodyMatch[1],
|
|
3092
|
+
required: true
|
|
3093
|
+
};
|
|
1948
3094
|
parameters.push({
|
|
1949
3095
|
name: bodyMatch[2],
|
|
1950
3096
|
in: "body",
|
|
@@ -1953,6 +3099,14 @@ async function parseControllers(structure) {
|
|
|
1953
3099
|
});
|
|
1954
3100
|
}
|
|
1955
3101
|
}
|
|
3102
|
+
let summary;
|
|
3103
|
+
const methodStartIndex = content.lastIndexOf("/// <summary>", match.index);
|
|
3104
|
+
if (methodStartIndex !== -1 && match.index - methodStartIndex < 500) {
|
|
3105
|
+
const summaryMatch = content.substring(methodStartIndex, match.index).match(/\/\/\/\s*<summary>\s*\n?\s*\/\/\/\s*(.+?)\s*\n?\s*\/\/\/\s*<\/summary>/s);
|
|
3106
|
+
if (summaryMatch) {
|
|
3107
|
+
summary = summaryMatch[1].replace(/\/\/\/\s*/g, "").trim();
|
|
3108
|
+
}
|
|
3109
|
+
}
|
|
1956
3110
|
const queryMatches = afterAttribute.matchAll(/\[FromQuery\]\s*(\w+)\s+(\w+)/g);
|
|
1957
3111
|
for (const qm of queryMatches) {
|
|
1958
3112
|
parameters.push({
|
|
@@ -1962,20 +3116,88 @@ async function parseControllers(structure) {
|
|
|
1962
3116
|
required: false
|
|
1963
3117
|
});
|
|
1964
3118
|
}
|
|
3119
|
+
const example = {};
|
|
3120
|
+
if (requestBody) {
|
|
3121
|
+
example.request = generateExampleRequest(requestBody.type);
|
|
3122
|
+
}
|
|
3123
|
+
if (method === "GET" || method === "POST") {
|
|
3124
|
+
example.response = generateExampleResponse(controllerName, method);
|
|
3125
|
+
}
|
|
1965
3126
|
endpoints.push({
|
|
1966
3127
|
method,
|
|
1967
3128
|
path: fullPath.replace(/\/+/g, "/"),
|
|
1968
3129
|
controller: controllerName,
|
|
1969
3130
|
action: methodMatch ? methodMatch[2] : "Unknown",
|
|
3131
|
+
summary,
|
|
1970
3132
|
parameters,
|
|
1971
|
-
|
|
1972
|
-
|
|
3133
|
+
requestBody,
|
|
3134
|
+
responses: generateResponses(method),
|
|
3135
|
+
authorize: hasAuthorize,
|
|
3136
|
+
example
|
|
1973
3137
|
});
|
|
1974
3138
|
}
|
|
1975
3139
|
}
|
|
1976
3140
|
}
|
|
1977
3141
|
return endpoints;
|
|
1978
3142
|
}
|
|
3143
|
+
function generateResponses(method) {
|
|
3144
|
+
switch (method) {
|
|
3145
|
+
case "GET":
|
|
3146
|
+
return [
|
|
3147
|
+
{ status: 200, description: "Success" },
|
|
3148
|
+
{ status: 404, description: "Not Found" }
|
|
3149
|
+
];
|
|
3150
|
+
case "POST":
|
|
3151
|
+
return [
|
|
3152
|
+
{ status: 201, description: "Created" },
|
|
3153
|
+
{ status: 400, description: "Bad Request" },
|
|
3154
|
+
{ status: 422, description: "Validation Error" }
|
|
3155
|
+
];
|
|
3156
|
+
case "PUT":
|
|
3157
|
+
case "PATCH":
|
|
3158
|
+
return [
|
|
3159
|
+
{ status: 204, description: "No Content" },
|
|
3160
|
+
{ status: 400, description: "Bad Request" },
|
|
3161
|
+
{ status: 404, description: "Not Found" }
|
|
3162
|
+
];
|
|
3163
|
+
case "DELETE":
|
|
3164
|
+
return [
|
|
3165
|
+
{ status: 204, description: "No Content" },
|
|
3166
|
+
{ status: 404, description: "Not Found" }
|
|
3167
|
+
];
|
|
3168
|
+
default:
|
|
3169
|
+
return [{ status: 200, description: "Success" }];
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
function generateExampleRequest(typeName) {
|
|
3173
|
+
const name = typeName.replace("Request", "").replace("Dto", "").replace("Create", "").replace("Update", "");
|
|
3174
|
+
const example = {
|
|
3175
|
+
code: `${name.toLowerCase()}_001`
|
|
3176
|
+
};
|
|
3177
|
+
if (typeName.includes("Create")) {
|
|
3178
|
+
example.name = `New ${name}`;
|
|
3179
|
+
example.description = `Description for ${name}`;
|
|
3180
|
+
} else if (typeName.includes("Update")) {
|
|
3181
|
+
example.name = `Updated ${name}`;
|
|
3182
|
+
example.description = `Updated description for ${name}`;
|
|
3183
|
+
}
|
|
3184
|
+
return JSON.stringify(example, null, 2);
|
|
3185
|
+
}
|
|
3186
|
+
function generateExampleResponse(controllerName, method) {
|
|
3187
|
+
const example = {
|
|
3188
|
+
id: "3fa85f64-5717-4562-b3fc-2c963f66afa6",
|
|
3189
|
+
tenantId: "6fa85f64-5717-4562-b3fc-2c963f66afa6",
|
|
3190
|
+
code: `${controllerName.toLowerCase()}_001`,
|
|
3191
|
+
name: `Example ${controllerName}`,
|
|
3192
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3193
|
+
updatedAt: null,
|
|
3194
|
+
createdBy: "user@example.com"
|
|
3195
|
+
};
|
|
3196
|
+
if (method === "GET") {
|
|
3197
|
+
return JSON.stringify(example, null, 2);
|
|
3198
|
+
}
|
|
3199
|
+
return JSON.stringify(example, null, 2);
|
|
3200
|
+
}
|
|
1979
3201
|
function formatAsMarkdown(endpoints) {
|
|
1980
3202
|
const lines = [];
|
|
1981
3203
|
lines.push("# SmartStack API Documentation");
|
|
@@ -2009,6 +3231,18 @@ function formatAsMarkdown(endpoints) {
|
|
|
2009
3231
|
}
|
|
2010
3232
|
lines.push("");
|
|
2011
3233
|
}
|
|
3234
|
+
if (endpoint.requestBody) {
|
|
3235
|
+
lines.push("**Request Body:**");
|
|
3236
|
+
lines.push("");
|
|
3237
|
+
lines.push(`Type: \`${endpoint.requestBody.type}\``);
|
|
3238
|
+
lines.push("");
|
|
3239
|
+
if (endpoint.example?.request) {
|
|
3240
|
+
lines.push("```json");
|
|
3241
|
+
lines.push(endpoint.example.request);
|
|
3242
|
+
lines.push("```");
|
|
3243
|
+
lines.push("");
|
|
3244
|
+
}
|
|
3245
|
+
}
|
|
2012
3246
|
if (endpoint.responses.length > 0) {
|
|
2013
3247
|
lines.push("**Responses:**");
|
|
2014
3248
|
lines.push("");
|
|
@@ -2017,11 +3251,27 @@ function formatAsMarkdown(endpoints) {
|
|
|
2017
3251
|
}
|
|
2018
3252
|
lines.push("");
|
|
2019
3253
|
}
|
|
3254
|
+
if (endpoint.example?.response && (endpoint.method === "GET" || endpoint.method === "POST")) {
|
|
3255
|
+
lines.push("**Example Response:**");
|
|
3256
|
+
lines.push("");
|
|
3257
|
+
lines.push("```json");
|
|
3258
|
+
lines.push(endpoint.example.response);
|
|
3259
|
+
lines.push("```");
|
|
3260
|
+
lines.push("");
|
|
3261
|
+
}
|
|
2020
3262
|
}
|
|
2021
3263
|
}
|
|
2022
3264
|
if (endpoints.length === 0) {
|
|
2023
3265
|
lines.push("No endpoints found matching the filter criteria.");
|
|
2024
3266
|
}
|
|
3267
|
+
lines.push("---");
|
|
3268
|
+
lines.push("");
|
|
3269
|
+
lines.push("## Summary");
|
|
3270
|
+
lines.push("");
|
|
3271
|
+
lines.push(`- **Total Endpoints**: ${endpoints.length}`);
|
|
3272
|
+
lines.push(`- **Controllers**: ${new Set(endpoints.map((e) => e.controller)).size}`);
|
|
3273
|
+
lines.push(`- **Authenticated**: ${endpoints.filter((e) => e.authorize).length}`);
|
|
3274
|
+
lines.push(`- **Public**: ${endpoints.filter((e) => !e.authorize).length}`);
|
|
2025
3275
|
return lines.join("\n");
|
|
2026
3276
|
}
|
|
2027
3277
|
function formatAsOpenApi(endpoints) {
|
|
@@ -2056,7 +3306,144 @@ function formatAsOpenApi(endpoints) {
|
|
|
2056
3306
|
security: endpoint.authorize ? [{ bearerAuth: [] }] : void 0
|
|
2057
3307
|
};
|
|
2058
3308
|
}
|
|
2059
|
-
return JSON.stringify(spec, null, 2);
|
|
3309
|
+
return JSON.stringify(spec, null, 2);
|
|
3310
|
+
}
|
|
3311
|
+
|
|
3312
|
+
// src/tools/suggest-migration.ts
|
|
3313
|
+
import { z as z2 } from "zod";
|
|
3314
|
+
import path9 from "path";
|
|
3315
|
+
var suggestMigrationTool = {
|
|
3316
|
+
name: "suggest_migration",
|
|
3317
|
+
description: "Suggest a migration name following SmartStack conventions ({context}_v{version}_{sequence}_{Description})",
|
|
3318
|
+
inputSchema: {
|
|
3319
|
+
type: "object",
|
|
3320
|
+
properties: {
|
|
3321
|
+
description: {
|
|
3322
|
+
type: "string",
|
|
3323
|
+
description: 'Description of what the migration does (e.g., "Add User Profiles", "Create Orders Table")'
|
|
3324
|
+
},
|
|
3325
|
+
context: {
|
|
3326
|
+
type: "string",
|
|
3327
|
+
enum: ["core", "extensions"],
|
|
3328
|
+
description: "DbContext name (default: core)"
|
|
3329
|
+
},
|
|
3330
|
+
version: {
|
|
3331
|
+
type: "string",
|
|
3332
|
+
description: 'Semver version (e.g., "1.0.0", "1.2.0"). If not provided, uses latest from existing migrations.'
|
|
3333
|
+
}
|
|
3334
|
+
},
|
|
3335
|
+
required: ["description"]
|
|
3336
|
+
}
|
|
3337
|
+
};
|
|
3338
|
+
var SuggestMigrationInputSchema = z2.object({
|
|
3339
|
+
description: z2.string().describe("Description of what the migration does"),
|
|
3340
|
+
context: z2.enum(["core", "extensions"]).optional().describe("DbContext name (default: core)"),
|
|
3341
|
+
version: z2.string().optional().describe('Semver version (e.g., "1.0.0")')
|
|
3342
|
+
});
|
|
3343
|
+
async function handleSuggestMigration(args, config) {
|
|
3344
|
+
const input = SuggestMigrationInputSchema.parse(args);
|
|
3345
|
+
const context = input.context || "core";
|
|
3346
|
+
logger.info("Suggesting migration name", { description: input.description, context });
|
|
3347
|
+
const structure = await findSmartStackStructure(config.smartstack.projectPath);
|
|
3348
|
+
const existingMigrations = await findExistingMigrations(structure, config, context);
|
|
3349
|
+
let version = input.version;
|
|
3350
|
+
let sequence = 1;
|
|
3351
|
+
if (existingMigrations.length > 0) {
|
|
3352
|
+
const latestMigration = existingMigrations[existingMigrations.length - 1];
|
|
3353
|
+
if (!version) {
|
|
3354
|
+
version = latestMigration.version;
|
|
3355
|
+
}
|
|
3356
|
+
const migrationsInVersion = existingMigrations.filter((m) => m.version === version);
|
|
3357
|
+
if (migrationsInVersion.length > 0) {
|
|
3358
|
+
const maxSequence = Math.max(...migrationsInVersion.map((m) => m.sequence));
|
|
3359
|
+
sequence = maxSequence + 1;
|
|
3360
|
+
}
|
|
3361
|
+
} else {
|
|
3362
|
+
version = version || "1.0.0";
|
|
3363
|
+
}
|
|
3364
|
+
const pascalDescription = toPascalCase(input.description);
|
|
3365
|
+
const sequenceStr = sequence.toString().padStart(3, "0");
|
|
3366
|
+
const migrationName = `${context}_v${version}_${sequenceStr}_${pascalDescription}`;
|
|
3367
|
+
const command = `dotnet ef migrations add ${migrationName} --context ApplicationDbContext`;
|
|
3368
|
+
const lines = [];
|
|
3369
|
+
lines.push("# Migration Name Suggestion");
|
|
3370
|
+
lines.push("");
|
|
3371
|
+
lines.push("## Suggested Name");
|
|
3372
|
+
lines.push("```");
|
|
3373
|
+
lines.push(migrationName);
|
|
3374
|
+
lines.push("```");
|
|
3375
|
+
lines.push("");
|
|
3376
|
+
lines.push("## Command");
|
|
3377
|
+
lines.push("```bash");
|
|
3378
|
+
lines.push(command);
|
|
3379
|
+
lines.push("```");
|
|
3380
|
+
lines.push("");
|
|
3381
|
+
lines.push("## Convention Details");
|
|
3382
|
+
lines.push("");
|
|
3383
|
+
lines.push(`| Part | Value | Description |`);
|
|
3384
|
+
lines.push(`|------|-------|-------------|`);
|
|
3385
|
+
lines.push(`| Context | \`${context}\` | DbContext name |`);
|
|
3386
|
+
lines.push(`| Version | \`v${version}\` | Semver version |`);
|
|
3387
|
+
lines.push(`| Sequence | \`${sequenceStr}\` | Order in version |`);
|
|
3388
|
+
lines.push(`| Description | \`${pascalDescription}\` | Migration description |`);
|
|
3389
|
+
lines.push("");
|
|
3390
|
+
if (existingMigrations.length > 0) {
|
|
3391
|
+
lines.push("## Existing Migrations (last 5)");
|
|
3392
|
+
lines.push("");
|
|
3393
|
+
const recent = existingMigrations.slice(-5);
|
|
3394
|
+
for (const m of recent) {
|
|
3395
|
+
lines.push(`- \`${m.name}\``);
|
|
3396
|
+
}
|
|
3397
|
+
}
|
|
3398
|
+
return lines.join("\n");
|
|
3399
|
+
}
|
|
3400
|
+
async function findExistingMigrations(structure, config, context) {
|
|
3401
|
+
const migrations = [];
|
|
3402
|
+
const infraPath = structure.infrastructure || path9.join(config.smartstack.projectPath, "Infrastructure");
|
|
3403
|
+
const migrationsPath = path9.join(infraPath, "Migrations");
|
|
3404
|
+
try {
|
|
3405
|
+
const migrationFiles = await findFiles("*.cs", { cwd: migrationsPath });
|
|
3406
|
+
const migrationPattern = /^(\w+)_v(\d+\.\d+\.\d+)_(\d+)_(\w+)\.cs$/;
|
|
3407
|
+
for (const file of migrationFiles) {
|
|
3408
|
+
const fileName = path9.basename(file);
|
|
3409
|
+
if (fileName.includes(".Designer.") || fileName.includes("ModelSnapshot")) {
|
|
3410
|
+
continue;
|
|
3411
|
+
}
|
|
3412
|
+
const match = fileName.match(migrationPattern);
|
|
3413
|
+
if (match) {
|
|
3414
|
+
const [, ctx, ver, seq, desc] = match;
|
|
3415
|
+
if (ctx === context || !context) {
|
|
3416
|
+
migrations.push({
|
|
3417
|
+
name: fileName.replace(".cs", ""),
|
|
3418
|
+
context: ctx,
|
|
3419
|
+
version: ver,
|
|
3420
|
+
sequence: parseInt(seq, 10),
|
|
3421
|
+
description: desc
|
|
3422
|
+
});
|
|
3423
|
+
}
|
|
3424
|
+
}
|
|
3425
|
+
}
|
|
3426
|
+
migrations.sort((a, b) => {
|
|
3427
|
+
const verCompare = compareVersions2(a.version, b.version);
|
|
3428
|
+
if (verCompare !== 0) return verCompare;
|
|
3429
|
+
return a.sequence - b.sequence;
|
|
3430
|
+
});
|
|
3431
|
+
} catch {
|
|
3432
|
+
logger.debug("No migrations folder found");
|
|
3433
|
+
}
|
|
3434
|
+
return migrations;
|
|
3435
|
+
}
|
|
3436
|
+
function toPascalCase(str) {
|
|
3437
|
+
return str.replace(/[^a-zA-Z0-9\s]/g, "").split(/\s+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("");
|
|
3438
|
+
}
|
|
3439
|
+
function compareVersions2(a, b) {
|
|
3440
|
+
const aParts = a.split(".").map(Number);
|
|
3441
|
+
const bParts = b.split(".").map(Number);
|
|
3442
|
+
for (let i = 0; i < 3; i++) {
|
|
3443
|
+
if (aParts[i] > bParts[i]) return 1;
|
|
3444
|
+
if (aParts[i] < bParts[i]) return -1;
|
|
3445
|
+
}
|
|
3446
|
+
return 0;
|
|
2060
3447
|
}
|
|
2061
3448
|
|
|
2062
3449
|
// src/resources/conventions.ts
|
|
@@ -2105,6 +3492,7 @@ Tables are organized by domain using prefixes:
|
|
|
2105
3492
|
| \`ref_\` | References | ref_Companies, ref_Departments |
|
|
2106
3493
|
| \`loc_\` | Localization | loc_Languages, loc_Translations |
|
|
2107
3494
|
| \`lic_\` | Licensing | lic_Licenses |
|
|
3495
|
+
| \`tenant_\` | Multi-Tenancy | tenant_Tenants, tenant_TenantUsers, tenant_TenantUserRoles |
|
|
2108
3496
|
|
|
2109
3497
|
### Navigation Code Prefixes
|
|
2110
3498
|
|
|
@@ -2571,6 +3959,286 @@ public class MyEntityConfiguration : IEntityTypeConfiguration<MyEntity>
|
|
|
2571
3959
|
|
|
2572
3960
|
---
|
|
2573
3961
|
|
|
3962
|
+
## 8. Multi-Tenant Architecture
|
|
3963
|
+
|
|
3964
|
+
### Overview
|
|
3965
|
+
|
|
3966
|
+
SmartStack implements a comprehensive multi-tenant architecture where:
|
|
3967
|
+
- **Platform scope**: Users with roles via \`UserRole\` (no tenant) see ALL tenants
|
|
3968
|
+
- **Tenant scope**: Users with roles via \`TenantUserRole\` see ONLY their tenant
|
|
3969
|
+
|
|
3970
|
+
### Tenant Model
|
|
3971
|
+
|
|
3972
|
+
\`\`\`
|
|
3973
|
+
License (1) \u2500\u2500\u2500\u2500\u2500\u2500\u25BA (N) Tenant
|
|
3974
|
+
\u2502
|
|
3975
|
+
\u251C\u2500\u2500\u25BA (1:1) Organisation (if B2B)
|
|
3976
|
+
\u2502
|
|
3977
|
+
\u2514\u2500\u2500\u25BA (N) TenantUser \u2500\u2500\u25BA (N) TenantUserRole
|
|
3978
|
+
\u2502
|
|
3979
|
+
\u2514\u2500\u2500\u25BA User (global)
|
|
3980
|
+
\`\`\`
|
|
3981
|
+
|
|
3982
|
+
### Core Entities
|
|
3983
|
+
|
|
3984
|
+
#### Tenant
|
|
3985
|
+
|
|
3986
|
+
| Field | Type | Description |
|
|
3987
|
+
|-------|------|-------------|
|
|
3988
|
+
| \`Id\` | Guid | Primary key |
|
|
3989
|
+
| \`LicenseId\` | Guid (FK) | Link to License (features & limits) |
|
|
3990
|
+
| \`Slug\` | string? | Optional vanity URL (unique, lowercase, alphanum + hyphen) |
|
|
3991
|
+
| \`Name\` | string | Display name |
|
|
3992
|
+
| \`Type\` | enum | B2B (with Organisation) or B2C |
|
|
3993
|
+
| \`Status\` | enum | Active, Suspended, Deleted |
|
|
3994
|
+
| \`CreatedBy\` | Guid (FK) | User who created the tenant |
|
|
3995
|
+
| \`CreatedAt\` | DateTime | Creation timestamp |
|
|
3996
|
+
|
|
3997
|
+
\`\`\`csharp
|
|
3998
|
+
public class Tenant : SystemEntity
|
|
3999
|
+
{
|
|
4000
|
+
public Guid LicenseId { get; private set; }
|
|
4001
|
+
public License License { get; private set; }
|
|
4002
|
+
|
|
4003
|
+
public string? Slug { get; private set; } // Vanity URL (optional)
|
|
4004
|
+
public string Name { get; private set; }
|
|
4005
|
+
public TenantType Type { get; private set; }
|
|
4006
|
+
public TenantStatus Status { get; private set; }
|
|
4007
|
+
|
|
4008
|
+
public Guid CreatedBy { get; private set; }
|
|
4009
|
+
public User Creator { get; private set; }
|
|
4010
|
+
|
|
4011
|
+
// Navigation
|
|
4012
|
+
public ICollection<TenantUser> TenantUsers { get; private set; }
|
|
4013
|
+
public Organisation? Organisation { get; private set; } // If B2B
|
|
4014
|
+
}
|
|
4015
|
+
|
|
4016
|
+
public enum TenantType { B2B = 1, B2C = 2 }
|
|
4017
|
+
public enum TenantStatus { Active = 1, Suspended = 2, Deleted = 3 }
|
|
4018
|
+
\`\`\`
|
|
4019
|
+
|
|
4020
|
+
#### TenantUser
|
|
4021
|
+
|
|
4022
|
+
Links a User to a Tenant with membership metadata.
|
|
4023
|
+
|
|
4024
|
+
| Field | Type | Description |
|
|
4025
|
+
|-------|------|-------------|
|
|
4026
|
+
| \`Id\` | Guid | Primary key |
|
|
4027
|
+
| \`TenantId\` | Guid (FK) | **NOT NULL** - Tenant reference |
|
|
4028
|
+
| \`UserId\` | Guid (FK) | User reference |
|
|
4029
|
+
| \`IsDefault\` | bool | Default tenant for this user (unique per user) |
|
|
4030
|
+
| \`Status\` | enum | Pending, Active, Revoked |
|
|
4031
|
+
| \`JoinedAt\` | DateTime | When user joined tenant |
|
|
4032
|
+
|
|
4033
|
+
\`\`\`csharp
|
|
4034
|
+
public class TenantUser
|
|
4035
|
+
{
|
|
4036
|
+
public Guid Id { get; private set; }
|
|
4037
|
+
public Guid TenantId { get; private set; } // NOT NULL
|
|
4038
|
+
public Tenant Tenant { get; private set; }
|
|
4039
|
+
|
|
4040
|
+
public Guid UserId { get; private set; }
|
|
4041
|
+
public User User { get; private set; }
|
|
4042
|
+
|
|
4043
|
+
public bool IsDefault { get; private set; }
|
|
4044
|
+
public TenantUserStatus Status { get; private set; }
|
|
4045
|
+
public DateTime JoinedAt { get; private set; }
|
|
4046
|
+
|
|
4047
|
+
// Roles within this tenant
|
|
4048
|
+
public ICollection<TenantUserRole> Roles { get; private set; }
|
|
4049
|
+
}
|
|
4050
|
+
|
|
4051
|
+
public enum TenantUserStatus { Pending = 1, Active = 2, Revoked = 3 }
|
|
4052
|
+
\`\`\`
|
|
4053
|
+
|
|
4054
|
+
#### TenantUserRole
|
|
4055
|
+
|
|
4056
|
+
Assigns roles to a user within a specific tenant.
|
|
4057
|
+
|
|
4058
|
+
| Field | Type | Description |
|
|
4059
|
+
|-------|------|-------------|
|
|
4060
|
+
| \`TenantUserId\` | Guid (FK) | TenantUser reference |
|
|
4061
|
+
| \`RoleId\` | Guid (FK) | Role reference |
|
|
4062
|
+
| \`AssignedAt\` | DateTime | When role was assigned |
|
|
4063
|
+
| \`AssignedBy\` | string? | Who assigned the role |
|
|
4064
|
+
|
|
4065
|
+
\`\`\`csharp
|
|
4066
|
+
public class TenantUserRole
|
|
4067
|
+
{
|
|
4068
|
+
public Guid TenantUserId { get; private set; }
|
|
4069
|
+
public TenantUser TenantUser { get; private set; }
|
|
4070
|
+
|
|
4071
|
+
public Guid RoleId { get; private set; }
|
|
4072
|
+
public Role Role { get; private set; }
|
|
4073
|
+
|
|
4074
|
+
public DateTime AssignedAt { get; private set; }
|
|
4075
|
+
public string? AssignedBy { get; private set; }
|
|
4076
|
+
}
|
|
4077
|
+
\`\`\`
|
|
4078
|
+
|
|
4079
|
+
#### TenantInvitation
|
|
4080
|
+
|
|
4081
|
+
Manages user invitations to a tenant.
|
|
4082
|
+
|
|
4083
|
+
| Field | Type | Description |
|
|
4084
|
+
|-------|------|-------------|
|
|
4085
|
+
| \`Id\` | Guid | Primary key |
|
|
4086
|
+
| \`TenantId\` | Guid (FK) | Target tenant |
|
|
4087
|
+
| \`Email\` | string | Invited email address |
|
|
4088
|
+
| \`RoleId\` | Guid (FK) | Role to assign on acceptance |
|
|
4089
|
+
| \`Token\` | string | Unique invitation token |
|
|
4090
|
+
| \`ExpiresAt\` | DateTime | Token expiration |
|
|
4091
|
+
| \`Status\` | enum | Pending, Accepted, Expired, Revoked |
|
|
4092
|
+
| \`InvitedBy\` | Guid (FK) | User who sent invitation |
|
|
4093
|
+
| \`InvitedAt\` | DateTime | Invitation timestamp |
|
|
4094
|
+
|
|
4095
|
+
\`\`\`csharp
|
|
4096
|
+
public class TenantInvitation
|
|
4097
|
+
{
|
|
4098
|
+
public Guid Id { get; private set; }
|
|
4099
|
+
public Guid TenantId { get; private set; }
|
|
4100
|
+
public Tenant Tenant { get; private set; }
|
|
4101
|
+
|
|
4102
|
+
public string Email { get; private set; }
|
|
4103
|
+
public Guid RoleId { get; private set; }
|
|
4104
|
+
public Role Role { get; private set; }
|
|
4105
|
+
|
|
4106
|
+
public string Token { get; private set; }
|
|
4107
|
+
public DateTime ExpiresAt { get; private set; }
|
|
4108
|
+
public TenantInvitationStatus Status { get; private set; }
|
|
4109
|
+
|
|
4110
|
+
public Guid InvitedBy { get; private set; }
|
|
4111
|
+
public User Inviter { get; private set; }
|
|
4112
|
+
public DateTime InvitedAt { get; private set; }
|
|
4113
|
+
}
|
|
4114
|
+
|
|
4115
|
+
public enum TenantInvitationStatus { Pending = 1, Accepted = 2, Expired = 3, Revoked = 4 }
|
|
4116
|
+
\`\`\`
|
|
4117
|
+
|
|
4118
|
+
### Access Control Matrix
|
|
4119
|
+
|
|
4120
|
+
| Role Source | Scope | Description |
|
|
4121
|
+
|-------------|-------|-------------|
|
|
4122
|
+
| \`UserRole\` (existing) | Platform | Roles WITHOUT tenant \u2192 sees ALL tenants |
|
|
4123
|
+
| \`TenantUserRole\` (new) | Tenant | Roles WITH tenant \u2192 sees ONLY that tenant |
|
|
4124
|
+
|
|
4125
|
+
**Examples:**
|
|
4126
|
+
|
|
4127
|
+
\`\`\`
|
|
4128
|
+
UserRole: UserId=1, RoleId=PlatformAdmin \u2192 User 1 sees ALL tenants
|
|
4129
|
+
UserRole: UserId=2, RoleId=PlatformReader \u2192 User 2 reads ALL tenants
|
|
4130
|
+
|
|
4131
|
+
TenantUserRole: TenantUserId=3, RoleId=Admin \u2192 User 3 is Admin of Tenant A only
|
|
4132
|
+
TenantUserRole: TenantUserId=4, RoleId=User \u2192 User 4 is User of Tenant A only
|
|
4133
|
+
\`\`\`
|
|
4134
|
+
|
|
4135
|
+
### Organisation (B2B Extension)
|
|
4136
|
+
|
|
4137
|
+
Organisation is a 1:1 extension of Tenant for B2B scenarios:
|
|
4138
|
+
|
|
4139
|
+
\`\`\`csharp
|
|
4140
|
+
public class Organisation : BaseEntity
|
|
4141
|
+
{
|
|
4142
|
+
public Guid TenantId { get; private set; } // FK to Tenant
|
|
4143
|
+
public Tenant Tenant { get; private set; }
|
|
4144
|
+
|
|
4145
|
+
// Organisation-specific fields
|
|
4146
|
+
public string LegalName { get; private set; }
|
|
4147
|
+
public string? VatNumber { get; private set; }
|
|
4148
|
+
public string? Address { get; private set; }
|
|
4149
|
+
// ... other B2B fields
|
|
4150
|
+
}
|
|
4151
|
+
\`\`\`
|
|
4152
|
+
|
|
4153
|
+
### License Integration
|
|
4154
|
+
|
|
4155
|
+
Features and limits are managed via License:
|
|
4156
|
+
|
|
4157
|
+
\`\`\`csharp
|
|
4158
|
+
// License.LimitsJson example
|
|
4159
|
+
{
|
|
4160
|
+
"max_users": 100,
|
|
4161
|
+
"max_tenants": 5,
|
|
4162
|
+
"max_storage_gb": 50
|
|
4163
|
+
}
|
|
4164
|
+
|
|
4165
|
+
// Check in service
|
|
4166
|
+
public async Task<bool> CanCreateTenantAsync(Guid licenseId)
|
|
4167
|
+
{
|
|
4168
|
+
var license = await _licenseService.GetAsync(licenseId);
|
|
4169
|
+
var limits = JsonSerializer.Deserialize<LicenseLimits>(license.LimitsJson);
|
|
4170
|
+
var currentTenants = await _tenantRepo.CountByLicenseAsync(licenseId);
|
|
4171
|
+
return currentTenants < limits.MaxTenants;
|
|
4172
|
+
}
|
|
4173
|
+
\`\`\`
|
|
4174
|
+
|
|
4175
|
+
### URL Routing
|
|
4176
|
+
|
|
4177
|
+
Tenant identification in URLs:
|
|
4178
|
+
|
|
4179
|
+
| Pattern | Example | Use Case |
|
|
4180
|
+
|---------|---------|----------|
|
|
4181
|
+
| GUID (default) | \`/{tenant-guid}/api/...\` | Always works, secure |
|
|
4182
|
+
| Slug (optional) | \`/{slug}/api/...\` | Vanity URL for premium clients |
|
|
4183
|
+
|
|
4184
|
+
\`\`\`csharp
|
|
4185
|
+
// Middleware resolution priority
|
|
4186
|
+
1. Extract from URL path (/{tenant-identifier}/...)
|
|
4187
|
+
2. Check if GUID \u2192 resolve directly
|
|
4188
|
+
3. Check if Slug \u2192 resolve by slug
|
|
4189
|
+
4. Fallback to user's default tenant (IsDefault=true)
|
|
4190
|
+
\`\`\`
|
|
4191
|
+
|
|
4192
|
+
### Audit Trail
|
|
4193
|
+
|
|
4194
|
+
Critical tables have history tracking:
|
|
4195
|
+
|
|
4196
|
+
- \`TenantUserHistory\` - tracks membership changes
|
|
4197
|
+
- \`TenantUserRoleHistory\` - tracks role assignment changes
|
|
4198
|
+
|
|
4199
|
+
\`\`\`csharp
|
|
4200
|
+
public class TenantUserHistory : IHistoryEntity<Guid>
|
|
4201
|
+
{
|
|
4202
|
+
public Guid Id { get; set; }
|
|
4203
|
+
public Guid SourceId { get; set; } // TenantUser.Id
|
|
4204
|
+
public ChangeType ChangeType { get; set; }
|
|
4205
|
+
public string? OldValues { get; set; } // JSON
|
|
4206
|
+
public string? NewValues { get; set; } // JSON
|
|
4207
|
+
public DateTime ChangedAt { get; set; }
|
|
4208
|
+
public string? ChangedBy { get; set; }
|
|
4209
|
+
}
|
|
4210
|
+
\`\`\`
|
|
4211
|
+
|
|
4212
|
+
### Database Constraints
|
|
4213
|
+
|
|
4214
|
+
\`\`\`sql
|
|
4215
|
+
-- Tenant slug unique (if set)
|
|
4216
|
+
CREATE UNIQUE INDEX IX_Tenant_Slug ON tenant_Tenants(Slug) WHERE Slug IS NOT NULL;
|
|
4217
|
+
|
|
4218
|
+
-- Only one default tenant per user
|
|
4219
|
+
CREATE UNIQUE INDEX IX_TenantUser_IsDefault
|
|
4220
|
+
ON tenant_TenantUsers(UserId) WHERE IsDefault = 1;
|
|
4221
|
+
|
|
4222
|
+
-- Composite key for TenantUserRole
|
|
4223
|
+
ALTER TABLE tenant_TenantUserRoles
|
|
4224
|
+
ADD CONSTRAINT PK_TenantUserRole PRIMARY KEY (TenantUserId, RoleId);
|
|
4225
|
+
\`\`\`
|
|
4226
|
+
|
|
4227
|
+
### Table Prefix
|
|
4228
|
+
|
|
4229
|
+
All tenant-related tables use the \`tenant_\` prefix:
|
|
4230
|
+
|
|
4231
|
+
| Table | Prefix |
|
|
4232
|
+
|-------|--------|
|
|
4233
|
+
| Tenants | \`tenant_Tenants\` |
|
|
4234
|
+
| TenantUsers | \`tenant_TenantUsers\` |
|
|
4235
|
+
| TenantUserRoles | \`tenant_TenantUserRoles\` |
|
|
4236
|
+
| TenantInvitations | \`tenant_TenantInvitations\` |
|
|
4237
|
+
| TenantUserHistory | \`tenant_TenantUserHistory\` |
|
|
4238
|
+
| TenantUserRoleHistory | \`tenant_TenantUserRoleHistory\` |
|
|
4239
|
+
|
|
4240
|
+
---
|
|
4241
|
+
|
|
2574
4242
|
## Quick Reference
|
|
2575
4243
|
|
|
2576
4244
|
| Category | Convention | Example |
|
|
@@ -2586,11 +4254,185 @@ public class MyEntityConfiguration : IEntityTypeConfiguration<MyEntity>
|
|
|
2586
4254
|
| Domain namespace | \`${namespaces.domain}\` | - |
|
|
2587
4255
|
| API namespace | \`${namespaces.api}\` | - |
|
|
2588
4256
|
| Required entity fields | Id, TenantId, Code, Audit, SoftDelete, RowVersion | See Entity Conventions |
|
|
4257
|
+
| Tenant prefix | \`tenant_\` | \`tenant_Tenants\`, \`tenant_TenantUsers\` |
|
|
4258
|
+
| Platform roles | \`UserRole\` | No TenantId \u2192 sees ALL tenants |
|
|
4259
|
+
| Tenant roles | \`TenantUserRole\` | With TenantId \u2192 sees ONLY that tenant |
|
|
4260
|
+
| License:Tenant | 1:N | One License can have multiple Tenants |
|
|
4261
|
+
| Dry-run scaffold | \`dryRun: true\` | Preview generated code without writing |
|
|
4262
|
+
| Tenant-aware entity | \`ITenantEntity\` | Default for scaffold_extension |
|
|
4263
|
+
| System entity | \`isSystemEntity: true\` | No TenantId, platform-level |
|
|
4264
|
+
| Full-stack scaffold | \`type: "feature"\` | Entity + DTOs + Validators + Repository + Service + Controller + Component |
|
|
4265
|
+
| Client extension | \`clientExtension: true\` | Auto-sets schema=extensions + prefix=ext_ |
|
|
4266
|
+
| Unit tests | \`type: "test"\` | xUnit tests with Moq & FluentAssertions |
|
|
4267
|
+
| DTOs generation | \`withDtos: true\` | Create, Update, Response DTOs |
|
|
4268
|
+
| Validation | \`withValidation: true\` | FluentValidation validators |
|
|
4269
|
+
| Repository | \`withRepository: true\` | Interface + Implementation |
|
|
4270
|
+
| Migration naming | \`suggest_migration\` | {context}_v{version}_{seq}_{Desc} |
|
|
4271
|
+
|
|
4272
|
+
---
|
|
4273
|
+
|
|
4274
|
+
## 9. MCP Tools & Resources
|
|
4275
|
+
|
|
4276
|
+
### Available Tools
|
|
4277
|
+
|
|
4278
|
+
| Tool | Description |
|
|
4279
|
+
|------|-------------|
|
|
4280
|
+
| \`scaffold_extension\` | Generate code: feature (full-stack), entity, service, controller, component, dto, validator, repository, or test |
|
|
4281
|
+
| \`validate_conventions\` | Validate SmartStack conventions compliance |
|
|
4282
|
+
| \`check_migrations\` | Analyze EF Core migrations for conflicts |
|
|
4283
|
+
| \`api_docs\` | Get API documentation from Swagger/Controllers with examples |
|
|
4284
|
+
| \`suggest_migration\` | Suggest migration name following conventions |
|
|
4285
|
+
|
|
4286
|
+
### Available Resources
|
|
4287
|
+
|
|
4288
|
+
| URI | Description |
|
|
4289
|
+
|-----|-------------|
|
|
4290
|
+
| \`smartstack://conventions\` | This documentation |
|
|
4291
|
+
| \`smartstack://project\` | Project structure information |
|
|
4292
|
+
| \`smartstack://entities\` | Quick reference of all domain entities |
|
|
4293
|
+
| \`smartstack://entities/{filter}\` | Filtered entity list by name/prefix |
|
|
4294
|
+
| \`smartstack://schema\` | Database schema information |
|
|
4295
|
+
| \`smartstack://api\` | API endpoints documentation |
|
|
4296
|
+
|
|
4297
|
+
### Scaffold Extension
|
|
4298
|
+
|
|
4299
|
+
Generate tenant-aware code by default:
|
|
4300
|
+
|
|
4301
|
+
**Type Options:**
|
|
4302
|
+
|
|
4303
|
+
| Type | Description | Generated Files |
|
|
4304
|
+
|------|-------------|-----------------|
|
|
4305
|
+
| \`feature\` | **Full-stack generation** | Entity + DTOs + Validators + Repository + Service + Controller + Component |
|
|
4306
|
+
| \`entity\` | Domain entity only | Entity class + EF Configuration |
|
|
4307
|
+
| \`dto\` | DTOs only | Create, Update, Response DTOs |
|
|
4308
|
+
| \`validator\` | Validators only | FluentValidation validators |
|
|
4309
|
+
| \`repository\` | Repository only | Interface + Implementation |
|
|
4310
|
+
| \`service\` | Service layer only | Interface + Implementation |
|
|
4311
|
+
| \`controller\` | API controller only | REST Controller |
|
|
4312
|
+
| \`component\` | React component only | Component + Hook + Types |
|
|
4313
|
+
| \`test\` | Unit tests only | xUnit tests with Moq |
|
|
4314
|
+
|
|
4315
|
+
#### Full-Stack Feature (Recommended)
|
|
4316
|
+
|
|
4317
|
+
Generate all layers in one command:
|
|
4318
|
+
|
|
4319
|
+
\`\`\`json
|
|
4320
|
+
// Complete feature: Entity \u2192 Service \u2192 Controller \u2192 Component
|
|
4321
|
+
{
|
|
4322
|
+
"type": "feature",
|
|
4323
|
+
"name": "Product",
|
|
4324
|
+
"options": {
|
|
4325
|
+
"tablePrefix": "ref_",
|
|
4326
|
+
"dryRun": true, // Preview first
|
|
4327
|
+
"withTests": true // Include unit tests
|
|
4328
|
+
}
|
|
4329
|
+
}
|
|
4330
|
+
|
|
4331
|
+
// Client extension (uses 'extensions' schema + 'ext_' prefix)
|
|
4332
|
+
{
|
|
4333
|
+
"type": "feature",
|
|
4334
|
+
"name": "CustomReport",
|
|
4335
|
+
"options": {
|
|
4336
|
+
"clientExtension": true, // Auto-sets schema + prefix
|
|
4337
|
+
"skipController": true // Skip API, React-only
|
|
4338
|
+
}
|
|
4339
|
+
}
|
|
4340
|
+
\`\`\`
|
|
4341
|
+
|
|
4342
|
+
#### Individual Types
|
|
4343
|
+
|
|
4344
|
+
\`\`\`json
|
|
4345
|
+
// Entity (default: tenant-aware with ITenantEntity)
|
|
4346
|
+
{
|
|
4347
|
+
"type": "entity",
|
|
4348
|
+
"name": "Product",
|
|
4349
|
+
"options": {
|
|
4350
|
+
"tablePrefix": "ref_",
|
|
4351
|
+
"dryRun": true // Preview without writing files
|
|
4352
|
+
}
|
|
4353
|
+
}
|
|
4354
|
+
|
|
4355
|
+
// System entity (no TenantId)
|
|
4356
|
+
{
|
|
4357
|
+
"type": "entity",
|
|
4358
|
+
"name": "GlobalConfig",
|
|
4359
|
+
"options": {
|
|
4360
|
+
"isSystemEntity": true,
|
|
4361
|
+
"tablePrefix": "cfg_"
|
|
4362
|
+
}
|
|
4363
|
+
}
|
|
4364
|
+
|
|
4365
|
+
// Unit tests for a service
|
|
4366
|
+
{
|
|
4367
|
+
"type": "test",
|
|
4368
|
+
"name": "Product"
|
|
4369
|
+
}
|
|
4370
|
+
\`\`\`
|
|
4371
|
+
|
|
4372
|
+
**Common Options:**
|
|
4373
|
+
|
|
4374
|
+
| Option | Type | Default | Description |
|
|
4375
|
+
|--------|------|---------|-------------|
|
|
4376
|
+
| \`dryRun\` | boolean | false | Preview generated code without writing files |
|
|
4377
|
+
| \`isSystemEntity\` | boolean | false | Create system entity (no TenantId) |
|
|
4378
|
+
| \`tablePrefix\` | string | "ref_" | Domain prefix for table name |
|
|
4379
|
+
| \`schema\` | string | "core" | Database schema ("core" or "extensions") |
|
|
4380
|
+
| \`namespace\` | string | auto | Custom namespace |
|
|
4381
|
+
| \`baseEntity\` | string | - | Base entity to extend |
|
|
4382
|
+
|
|
4383
|
+
**Feature-Specific Options:**
|
|
4384
|
+
|
|
4385
|
+
| Option | Type | Default | Description |
|
|
4386
|
+
|--------|------|---------|-------------|
|
|
4387
|
+
| \`clientExtension\` | boolean | false | Use 'extensions' schema + 'ext_' prefix |
|
|
4388
|
+
| \`skipService\` | boolean | false | Skip service layer generation |
|
|
4389
|
+
| \`skipController\` | boolean | false | Skip API controller generation |
|
|
4390
|
+
| \`skipComponent\` | boolean | false | Skip React component generation |
|
|
4391
|
+
| \`withTests\` | boolean | false | Generate unit tests (xUnit + Moq) |
|
|
4392
|
+
| \`withDtos\` | boolean | false | Generate DTOs (Create, Update, Response) |
|
|
4393
|
+
| \`withValidation\` | boolean | false | Generate FluentValidation validators |
|
|
4394
|
+
| \`withRepository\` | boolean | false | Generate repository pattern |
|
|
4395
|
+
| \`entityProperties\` | array | - | Define entity properties for DTOs |
|
|
4396
|
+
|
|
4397
|
+
### Suggest Migration
|
|
4398
|
+
|
|
4399
|
+
Generate migration names following conventions:
|
|
4400
|
+
|
|
4401
|
+
\`\`\`json
|
|
4402
|
+
{
|
|
4403
|
+
"description": "Add User Profiles",
|
|
4404
|
+
"context": "core", // optional: core or extensions
|
|
4405
|
+
"version": "1.2.0" // optional: auto-detected from existing
|
|
4406
|
+
}
|
|
4407
|
+
\`\`\`
|
|
4408
|
+
|
|
4409
|
+
Output: \`core_v1.2.0_001_AddUserProfiles\`
|
|
4410
|
+
|
|
4411
|
+
### Validate Conventions
|
|
4412
|
+
|
|
4413
|
+
Run specific or all checks:
|
|
4414
|
+
|
|
4415
|
+
\`\`\`json
|
|
4416
|
+
{
|
|
4417
|
+
"checks": ["tables", "migrations", "services", "namespaces", "entities", "tenants"]
|
|
4418
|
+
}
|
|
4419
|
+
\`\`\`
|
|
4420
|
+
|
|
4421
|
+
**Check Types:**
|
|
4422
|
+
|
|
4423
|
+
| Check | Validates |
|
|
4424
|
+
|-------|-----------|
|
|
4425
|
+
| \`tables\` | Schema specification, domain prefixes |
|
|
4426
|
+
| \`migrations\` | Naming convention, version ordering |
|
|
4427
|
+
| \`services\` | Interface implementation pattern |
|
|
4428
|
+
| \`namespaces\` | Layer namespace structure |
|
|
4429
|
+
| \`entities\` | BaseEntity/SystemEntity inheritance, factory methods |
|
|
4430
|
+
| \`tenants\` | ITenantEntity implementation, TenantId consistency |
|
|
2589
4431
|
`;
|
|
2590
4432
|
}
|
|
2591
4433
|
|
|
2592
4434
|
// src/resources/project-info.ts
|
|
2593
|
-
import
|
|
4435
|
+
import path10 from "path";
|
|
2594
4436
|
var projectInfoResourceTemplate = {
|
|
2595
4437
|
uri: "smartstack://project",
|
|
2596
4438
|
name: "SmartStack Project Info",
|
|
@@ -2627,16 +4469,16 @@ async function getProjectInfoResource(config) {
|
|
|
2627
4469
|
lines.push("```");
|
|
2628
4470
|
lines.push(`${projectInfo.name}/`);
|
|
2629
4471
|
if (structure.domain) {
|
|
2630
|
-
lines.push(`\u251C\u2500\u2500 ${
|
|
4472
|
+
lines.push(`\u251C\u2500\u2500 ${path10.basename(structure.domain)}/ # Domain layer (entities)`);
|
|
2631
4473
|
}
|
|
2632
4474
|
if (structure.application) {
|
|
2633
|
-
lines.push(`\u251C\u2500\u2500 ${
|
|
4475
|
+
lines.push(`\u251C\u2500\u2500 ${path10.basename(structure.application)}/ # Application layer (services)`);
|
|
2634
4476
|
}
|
|
2635
4477
|
if (structure.infrastructure) {
|
|
2636
|
-
lines.push(`\u251C\u2500\u2500 ${
|
|
4478
|
+
lines.push(`\u251C\u2500\u2500 ${path10.basename(structure.infrastructure)}/ # Infrastructure (EF Core)`);
|
|
2637
4479
|
}
|
|
2638
4480
|
if (structure.api) {
|
|
2639
|
-
lines.push(`\u251C\u2500\u2500 ${
|
|
4481
|
+
lines.push(`\u251C\u2500\u2500 ${path10.basename(structure.api)}/ # API layer (controllers)`);
|
|
2640
4482
|
}
|
|
2641
4483
|
if (structure.web) {
|
|
2642
4484
|
lines.push(`\u2514\u2500\u2500 web/smartstack-web/ # React frontend`);
|
|
@@ -2649,8 +4491,8 @@ async function getProjectInfoResource(config) {
|
|
|
2649
4491
|
lines.push("| Project | Path |");
|
|
2650
4492
|
lines.push("|---------|------|");
|
|
2651
4493
|
for (const csproj of projectInfo.csprojFiles) {
|
|
2652
|
-
const name =
|
|
2653
|
-
const relativePath =
|
|
4494
|
+
const name = path10.basename(csproj, ".csproj");
|
|
4495
|
+
const relativePath = path10.relative(projectPath, csproj);
|
|
2654
4496
|
lines.push(`| ${name} | \`${relativePath}\` |`);
|
|
2655
4497
|
}
|
|
2656
4498
|
lines.push("");
|
|
@@ -2660,10 +4502,10 @@ async function getProjectInfoResource(config) {
|
|
|
2660
4502
|
cwd: structure.migrations,
|
|
2661
4503
|
ignore: ["*.Designer.cs"]
|
|
2662
4504
|
});
|
|
2663
|
-
const migrations = migrationFiles.map((f) =>
|
|
4505
|
+
const migrations = migrationFiles.map((f) => path10.basename(f)).filter((f) => !f.includes("ModelSnapshot") && !f.includes(".Designer.")).sort();
|
|
2664
4506
|
lines.push("## EF Core Migrations");
|
|
2665
4507
|
lines.push("");
|
|
2666
|
-
lines.push(`**Location**: \`${
|
|
4508
|
+
lines.push(`**Location**: \`${path10.relative(projectPath, structure.migrations)}\``);
|
|
2667
4509
|
lines.push(`**Total Migrations**: ${migrations.length}`);
|
|
2668
4510
|
lines.push("");
|
|
2669
4511
|
if (migrations.length > 0) {
|
|
@@ -2698,11 +4540,11 @@ async function getProjectInfoResource(config) {
|
|
|
2698
4540
|
lines.push("dotnet build");
|
|
2699
4541
|
lines.push("");
|
|
2700
4542
|
lines.push("# Run API");
|
|
2701
|
-
lines.push(`cd ${structure.api ?
|
|
4543
|
+
lines.push(`cd ${structure.api ? path10.relative(projectPath, structure.api) : "SmartStack.Api"}`);
|
|
2702
4544
|
lines.push("dotnet run");
|
|
2703
4545
|
lines.push("");
|
|
2704
4546
|
lines.push("# Run frontend");
|
|
2705
|
-
lines.push(`cd ${structure.web ?
|
|
4547
|
+
lines.push(`cd ${structure.web ? path10.relative(projectPath, structure.web) : "web/smartstack-web"}`);
|
|
2706
4548
|
lines.push("npm run dev");
|
|
2707
4549
|
lines.push("");
|
|
2708
4550
|
lines.push("# Create migration");
|
|
@@ -2725,7 +4567,7 @@ async function getProjectInfoResource(config) {
|
|
|
2725
4567
|
}
|
|
2726
4568
|
|
|
2727
4569
|
// src/resources/api-endpoints.ts
|
|
2728
|
-
import
|
|
4570
|
+
import path11 from "path";
|
|
2729
4571
|
var apiEndpointsResourceTemplate = {
|
|
2730
4572
|
uri: "smartstack://api/",
|
|
2731
4573
|
name: "SmartStack API Endpoints",
|
|
@@ -2750,7 +4592,7 @@ async function getApiEndpointsResource(config, endpointFilter) {
|
|
|
2750
4592
|
}
|
|
2751
4593
|
async function parseController(filePath, _rootPath) {
|
|
2752
4594
|
const content = await readText(filePath);
|
|
2753
|
-
const fileName =
|
|
4595
|
+
const fileName = path11.basename(filePath, ".cs");
|
|
2754
4596
|
const controllerName = fileName.replace("Controller", "");
|
|
2755
4597
|
const endpoints = [];
|
|
2756
4598
|
const routeMatch = content.match(/\[Route\s*\(\s*"([^"]+)"\s*\)\]/);
|
|
@@ -2897,7 +4739,7 @@ function getMethodEmoji(method) {
|
|
|
2897
4739
|
}
|
|
2898
4740
|
|
|
2899
4741
|
// src/resources/db-schema.ts
|
|
2900
|
-
import
|
|
4742
|
+
import path12 from "path";
|
|
2901
4743
|
var dbSchemaResourceTemplate = {
|
|
2902
4744
|
uri: "smartstack://schema/",
|
|
2903
4745
|
name: "SmartStack Database Schema",
|
|
@@ -2987,7 +4829,7 @@ async function parseEntity(filePath, rootPath, _config) {
|
|
|
2987
4829
|
tableName,
|
|
2988
4830
|
properties,
|
|
2989
4831
|
relationships,
|
|
2990
|
-
file:
|
|
4832
|
+
file: path12.relative(rootPath, filePath)
|
|
2991
4833
|
};
|
|
2992
4834
|
}
|
|
2993
4835
|
async function enrichFromConfigurations(entities, infrastructurePath, _config) {
|
|
@@ -3132,6 +4974,231 @@ function formatSchema(entities, filter, _config) {
|
|
|
3132
4974
|
return lines.join("\n");
|
|
3133
4975
|
}
|
|
3134
4976
|
|
|
4977
|
+
// src/resources/entities.ts
|
|
4978
|
+
import path13 from "path";
|
|
4979
|
+
var entitiesResourceTemplate = {
|
|
4980
|
+
uri: "smartstack://entities/",
|
|
4981
|
+
name: "SmartStack Entities",
|
|
4982
|
+
description: "Quick reference list of all domain entities with tenant-awareness and table prefixes. Use smartstack://entities/{name} for details.",
|
|
4983
|
+
mimeType: "text/markdown"
|
|
4984
|
+
};
|
|
4985
|
+
async function getEntitiesResource(config, entityFilter) {
|
|
4986
|
+
const structure = await findSmartStackStructure(config.smartstack.projectPath);
|
|
4987
|
+
if (!structure.domain) {
|
|
4988
|
+
return "# SmartStack Entities\n\nNo domain project found.";
|
|
4989
|
+
}
|
|
4990
|
+
const entities = [];
|
|
4991
|
+
const entityFiles = await findEntityFiles(structure.domain);
|
|
4992
|
+
for (const file of entityFiles) {
|
|
4993
|
+
const entity = await parseEntitySummary(file, structure.root, config);
|
|
4994
|
+
if (entity) {
|
|
4995
|
+
entities.push(entity);
|
|
4996
|
+
}
|
|
4997
|
+
}
|
|
4998
|
+
entities.sort((a, b) => {
|
|
4999
|
+
if (a.tablePrefix !== b.tablePrefix) {
|
|
5000
|
+
return a.tablePrefix.localeCompare(b.tablePrefix);
|
|
5001
|
+
}
|
|
5002
|
+
return a.name.localeCompare(b.name);
|
|
5003
|
+
});
|
|
5004
|
+
const filteredEntities = entityFilter ? entities.filter(
|
|
5005
|
+
(e) => e.name.toLowerCase().includes(entityFilter.toLowerCase()) || e.tableName.toLowerCase().includes(entityFilter.toLowerCase()) || e.tablePrefix.toLowerCase().includes(entityFilter.toLowerCase())
|
|
5006
|
+
) : entities;
|
|
5007
|
+
return formatEntities(filteredEntities, entityFilter, config);
|
|
5008
|
+
}
|
|
5009
|
+
async function parseEntitySummary(filePath, rootPath, config) {
|
|
5010
|
+
const content = await readText(filePath);
|
|
5011
|
+
const classMatch = content.match(/public\s+(?:class|record)\s+(\w+)(?:\s*:\s*([^{]+))?/);
|
|
5012
|
+
if (!classMatch) return null;
|
|
5013
|
+
const entityName = classMatch[1];
|
|
5014
|
+
const inheritance = classMatch[2]?.trim() || "";
|
|
5015
|
+
if (entityName.endsWith("Dto") || entityName.endsWith("Command") || entityName.endsWith("Query") || entityName.endsWith("Handler") || entityName.endsWith("Configuration") || entityName.endsWith("Service") || entityName.endsWith("Validator") || entityName.endsWith("Exception")) {
|
|
5016
|
+
return null;
|
|
5017
|
+
}
|
|
5018
|
+
const baseClassMatch = inheritance.match(/^(\w+)/);
|
|
5019
|
+
const baseClass = baseClassMatch ? baseClassMatch[1] : "Unknown";
|
|
5020
|
+
const isSystemEntity = baseClass === "SystemEntity" || inheritance.includes("ISystemEntity") || content.includes("// System entity") || !content.includes("TenantId");
|
|
5021
|
+
const isTenantAware = content.includes("TenantId") && (content.includes("public Guid TenantId") || content.includes("public required Guid TenantId"));
|
|
5022
|
+
const hasCode = content.includes("public string Code") || content.includes("public required string Code") || content.includes("public string? Code");
|
|
5023
|
+
const hasSoftDelete = content.includes("IsDeleted") || inheritance.includes("ISoftDeletable") || baseClass === "BaseEntity";
|
|
5024
|
+
const hasRowVersion = content.includes("RowVersion") || inheritance.includes("IHasRowVersion") || baseClass === "BaseEntity";
|
|
5025
|
+
const { tableName, tablePrefix, schema } = inferTableInfo(entityName, config);
|
|
5026
|
+
return {
|
|
5027
|
+
name: entityName,
|
|
5028
|
+
tableName,
|
|
5029
|
+
tablePrefix,
|
|
5030
|
+
schema,
|
|
5031
|
+
isTenantAware,
|
|
5032
|
+
isSystemEntity,
|
|
5033
|
+
baseClass,
|
|
5034
|
+
hasCode,
|
|
5035
|
+
hasSoftDelete,
|
|
5036
|
+
hasRowVersion,
|
|
5037
|
+
file: filePath,
|
|
5038
|
+
relativePath: path13.relative(rootPath, filePath)
|
|
5039
|
+
};
|
|
5040
|
+
}
|
|
5041
|
+
function inferTableInfo(entityName, config) {
|
|
5042
|
+
const prefixMapping = {
|
|
5043
|
+
// Authentication & Authorization
|
|
5044
|
+
"User": "auth_",
|
|
5045
|
+
"Role": "auth_",
|
|
5046
|
+
"Permission": "auth_",
|
|
5047
|
+
"UserRole": "auth_",
|
|
5048
|
+
"RolePermission": "auth_",
|
|
5049
|
+
"RefreshToken": "auth_",
|
|
5050
|
+
// Navigation
|
|
5051
|
+
"NavigationItem": "nav_",
|
|
5052
|
+
"NavigationMenu": "nav_",
|
|
5053
|
+
"Page": "nav_",
|
|
5054
|
+
// AI
|
|
5055
|
+
"Prompt": "ai_",
|
|
5056
|
+
"Completion": "ai_",
|
|
5057
|
+
"Model": "ai_",
|
|
5058
|
+
"Conversation": "ai_",
|
|
5059
|
+
// Configuration
|
|
5060
|
+
"Setting": "cfg_",
|
|
5061
|
+
"Configuration": "cfg_",
|
|
5062
|
+
// Workflow
|
|
5063
|
+
"Workflow": "wkf_",
|
|
5064
|
+
"WorkflowStep": "wkf_",
|
|
5065
|
+
"WorkflowInstance": "wkf_",
|
|
5066
|
+
// Support
|
|
5067
|
+
"Ticket": "support_",
|
|
5068
|
+
"TicketComment": "support_",
|
|
5069
|
+
// Entra (Azure AD)
|
|
5070
|
+
"EntraUser": "entra_",
|
|
5071
|
+
"EntraGroup": "entra_",
|
|
5072
|
+
// Licensing
|
|
5073
|
+
"License": "lic_",
|
|
5074
|
+
"Subscription": "lic_",
|
|
5075
|
+
// Tenant
|
|
5076
|
+
"Tenant": "tenant_",
|
|
5077
|
+
"TenantUser": "tenant_",
|
|
5078
|
+
"TenantUserRole": "tenant_",
|
|
5079
|
+
"TenantInvitation": "tenant_",
|
|
5080
|
+
"Organisation": "tenant_",
|
|
5081
|
+
// Localization
|
|
5082
|
+
"Translation": "loc_",
|
|
5083
|
+
"Language": "loc_"
|
|
5084
|
+
};
|
|
5085
|
+
let tablePrefix = "ref_";
|
|
5086
|
+
for (const [pattern, prefix] of Object.entries(prefixMapping)) {
|
|
5087
|
+
if (entityName === pattern || entityName.startsWith(pattern)) {
|
|
5088
|
+
tablePrefix = prefix;
|
|
5089
|
+
break;
|
|
5090
|
+
}
|
|
5091
|
+
}
|
|
5092
|
+
const validPrefixes = config.conventions.tablePrefixes;
|
|
5093
|
+
if (!validPrefixes.includes(tablePrefix)) {
|
|
5094
|
+
tablePrefix = "ref_";
|
|
5095
|
+
}
|
|
5096
|
+
const tableName = `${tablePrefix}${pluralize(entityName)}`;
|
|
5097
|
+
const schema = config.conventions.schemas.platform;
|
|
5098
|
+
return { tableName, tablePrefix, schema };
|
|
5099
|
+
}
|
|
5100
|
+
function pluralize(name) {
|
|
5101
|
+
if (name.endsWith("y") && !/[aeiou]y$/i.test(name)) {
|
|
5102
|
+
return name.slice(0, -1) + "ies";
|
|
5103
|
+
}
|
|
5104
|
+
if (name.endsWith("s") || name.endsWith("x") || name.endsWith("ch") || name.endsWith("sh")) {
|
|
5105
|
+
return name + "es";
|
|
5106
|
+
}
|
|
5107
|
+
return name + "s";
|
|
5108
|
+
}
|
|
5109
|
+
function formatEntities(entities, filter, config) {
|
|
5110
|
+
const lines = [];
|
|
5111
|
+
lines.push("# SmartStack Entities");
|
|
5112
|
+
lines.push("");
|
|
5113
|
+
if (filter) {
|
|
5114
|
+
lines.push(`> Filtered by: \`${filter}\``);
|
|
5115
|
+
lines.push("");
|
|
5116
|
+
}
|
|
5117
|
+
if (entities.length === 0) {
|
|
5118
|
+
lines.push("No entities found matching the criteria.");
|
|
5119
|
+
return lines.join("\n");
|
|
5120
|
+
}
|
|
5121
|
+
const tenantAwareCount = entities.filter((e) => e.isTenantAware).length;
|
|
5122
|
+
const systemCount = entities.filter((e) => e.isSystemEntity).length;
|
|
5123
|
+
const prefixes = [...new Set(entities.map((e) => e.tablePrefix))];
|
|
5124
|
+
lines.push("## Summary");
|
|
5125
|
+
lines.push("");
|
|
5126
|
+
lines.push(`- **Total Entities**: ${entities.length}`);
|
|
5127
|
+
lines.push(`- **Tenant-Aware**: ${tenantAwareCount}`);
|
|
5128
|
+
lines.push(`- **System Entities**: ${systemCount}`);
|
|
5129
|
+
lines.push(`- **Table Prefixes Used**: ${prefixes.join(", ")}`);
|
|
5130
|
+
lines.push("");
|
|
5131
|
+
lines.push("## Quick Reference");
|
|
5132
|
+
lines.push("");
|
|
5133
|
+
lines.push("| Entity | Table | Tenant | Code | SoftDel | Base |");
|
|
5134
|
+
lines.push("|--------|-------|--------|------|---------|------|");
|
|
5135
|
+
for (const entity of entities) {
|
|
5136
|
+
const tenantIcon = entity.isTenantAware ? "\u2705" : entity.isSystemEntity ? "\u{1F512}" : "\u274C";
|
|
5137
|
+
const codeIcon = entity.hasCode ? "\u2705" : "\u274C";
|
|
5138
|
+
const softDelIcon = entity.hasSoftDelete ? "\u2705" : "\u274C";
|
|
5139
|
+
lines.push(
|
|
5140
|
+
`| ${entity.name} | \`${entity.tableName}\` | ${tenantIcon} | ${codeIcon} | ${softDelIcon} | ${entity.baseClass} |`
|
|
5141
|
+
);
|
|
5142
|
+
}
|
|
5143
|
+
lines.push("");
|
|
5144
|
+
lines.push("## By Domain");
|
|
5145
|
+
lines.push("");
|
|
5146
|
+
const byPrefix = /* @__PURE__ */ new Map();
|
|
5147
|
+
for (const entity of entities) {
|
|
5148
|
+
const list = byPrefix.get(entity.tablePrefix) || [];
|
|
5149
|
+
list.push(entity);
|
|
5150
|
+
byPrefix.set(entity.tablePrefix, list);
|
|
5151
|
+
}
|
|
5152
|
+
const prefixNames = {
|
|
5153
|
+
"auth_": "Authentication & Authorization",
|
|
5154
|
+
"nav_": "Navigation",
|
|
5155
|
+
"ai_": "AI & Prompts",
|
|
5156
|
+
"cfg_": "Configuration",
|
|
5157
|
+
"wkf_": "Workflows",
|
|
5158
|
+
"support_": "Support",
|
|
5159
|
+
"entra_": "Microsoft Entra",
|
|
5160
|
+
"lic_": "Licensing",
|
|
5161
|
+
"tenant_": "Multi-Tenant",
|
|
5162
|
+
"loc_": "Localization",
|
|
5163
|
+
"ref_": "Reference Data",
|
|
5164
|
+
"usr_": "User Data"
|
|
5165
|
+
};
|
|
5166
|
+
for (const [prefix, prefixEntities] of byPrefix) {
|
|
5167
|
+
const domainName = prefixNames[prefix] || prefix;
|
|
5168
|
+
lines.push(`### ${domainName} (\`${prefix}\`)`);
|
|
5169
|
+
lines.push("");
|
|
5170
|
+
for (const entity of prefixEntities) {
|
|
5171
|
+
const badges = [];
|
|
5172
|
+
if (entity.isTenantAware) badges.push("tenant-aware");
|
|
5173
|
+
if (entity.isSystemEntity) badges.push("system");
|
|
5174
|
+
if (entity.hasCode) badges.push("has-code");
|
|
5175
|
+
lines.push(`- **${entity.name}** \u2192 \`${entity.tableName}\``);
|
|
5176
|
+
if (badges.length > 0) {
|
|
5177
|
+
lines.push(` - ${badges.map((b) => `\`${b}\``).join(" ")}`);
|
|
5178
|
+
}
|
|
5179
|
+
lines.push(` - File: \`${entity.relativePath}\``);
|
|
5180
|
+
}
|
|
5181
|
+
lines.push("");
|
|
5182
|
+
}
|
|
5183
|
+
lines.push("## Conventions");
|
|
5184
|
+
lines.push("");
|
|
5185
|
+
lines.push("| Property | Convention |");
|
|
5186
|
+
lines.push("|----------|------------|");
|
|
5187
|
+
lines.push(`| Schema | \`${config.conventions.schemas.platform}\` |`);
|
|
5188
|
+
lines.push(`| Valid Prefixes | ${config.conventions.tablePrefixes.map((p) => `\`${p}\``).join(", ")} |`);
|
|
5189
|
+
lines.push("| Tenant-aware | Must have `TenantId` (GUID, NOT NULL) |");
|
|
5190
|
+
lines.push("| System entity | No `TenantId`, used for platform-level data |");
|
|
5191
|
+
lines.push("| Code field | Lowercase, unique per tenant (or globally for system) |");
|
|
5192
|
+
lines.push("");
|
|
5193
|
+
lines.push("## Legend");
|
|
5194
|
+
lines.push("");
|
|
5195
|
+
lines.push("- \u2705 = Feature present");
|
|
5196
|
+
lines.push("- \u274C = Feature absent");
|
|
5197
|
+
lines.push("- \u{1F512} = System entity (no tenant scope)");
|
|
5198
|
+
lines.push("");
|
|
5199
|
+
return lines.join("\n");
|
|
5200
|
+
}
|
|
5201
|
+
|
|
3135
5202
|
// src/server.ts
|
|
3136
5203
|
async function createServer() {
|
|
3137
5204
|
const config = await getConfig();
|
|
@@ -3154,7 +5221,8 @@ async function createServer() {
|
|
|
3154
5221
|
validateConventionsTool,
|
|
3155
5222
|
checkMigrationsTool,
|
|
3156
5223
|
scaffoldExtensionTool,
|
|
3157
|
-
apiDocsTool
|
|
5224
|
+
apiDocsTool,
|
|
5225
|
+
suggestMigrationTool
|
|
3158
5226
|
]
|
|
3159
5227
|
};
|
|
3160
5228
|
});
|
|
@@ -3177,6 +5245,9 @@ async function createServer() {
|
|
|
3177
5245
|
case "api_docs":
|
|
3178
5246
|
result = await handleApiDocs(args, config);
|
|
3179
5247
|
break;
|
|
5248
|
+
case "suggest_migration":
|
|
5249
|
+
result = await handleSuggestMigration(args, config);
|
|
5250
|
+
break;
|
|
3180
5251
|
default:
|
|
3181
5252
|
throw new Error(`Unknown tool: ${name}`);
|
|
3182
5253
|
}
|
|
@@ -3210,7 +5281,8 @@ async function createServer() {
|
|
|
3210
5281
|
conventionsResourceTemplate,
|
|
3211
5282
|
projectInfoResourceTemplate,
|
|
3212
5283
|
apiEndpointsResourceTemplate,
|
|
3213
|
-
dbSchemaResourceTemplate
|
|
5284
|
+
dbSchemaResourceTemplate,
|
|
5285
|
+
entitiesResourceTemplate
|
|
3214
5286
|
]
|
|
3215
5287
|
};
|
|
3216
5288
|
});
|
|
@@ -3230,6 +5302,11 @@ async function createServer() {
|
|
|
3230
5302
|
} else if (uri.startsWith("smartstack://schema/")) {
|
|
3231
5303
|
const table = uri.replace("smartstack://schema/", "");
|
|
3232
5304
|
content = await getDbSchemaResource(config, table);
|
|
5305
|
+
} else if (uri.startsWith("smartstack://entities/")) {
|
|
5306
|
+
const entityFilter = uri.replace("smartstack://entities/", "");
|
|
5307
|
+
content = await getEntitiesResource(config, entityFilter || void 0);
|
|
5308
|
+
} else if (uri === "smartstack://entities") {
|
|
5309
|
+
content = await getEntitiesResource(config);
|
|
3233
5310
|
} else {
|
|
3234
5311
|
throw new Error(`Unknown resource: ${uri}`);
|
|
3235
5312
|
}
|