@solidstarters/solid-core 1.2.184 → 1.2.186

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.
Files changed (68) hide show
  1. package/dist/config/iam.config.d.ts +2 -0
  2. package/dist/config/iam.config.d.ts.map +1 -1
  3. package/dist/config/iam.config.js +1 -0
  4. package/dist/config/iam.config.js.map +1 -1
  5. package/dist/dtos/selection-dynamic-query.dto.d.ts +1 -0
  6. package/dist/dtos/selection-dynamic-query.dto.d.ts.map +1 -1
  7. package/dist/dtos/selection-dynamic-query.dto.js +6 -1
  8. package/dist/dtos/selection-dynamic-query.dto.js.map +1 -1
  9. package/dist/helpers/module-metadata-helper.service.d.ts.map +1 -1
  10. package/dist/helpers/module-metadata-helper.service.js +8 -3
  11. package/dist/helpers/module-metadata-helper.service.js.map +1 -1
  12. package/dist/interfaces.d.ts +25 -1
  13. package/dist/interfaces.d.ts.map +1 -1
  14. package/dist/interfaces.js.map +1 -1
  15. package/dist/jobs/database/trigger-mcp-client-subscriber-database.service.d.ts.map +1 -1
  16. package/dist/jobs/database/trigger-mcp-client-subscriber-database.service.js +3 -2
  17. package/dist/jobs/database/trigger-mcp-client-subscriber-database.service.js.map +1 -1
  18. package/dist/seeders/seed-data/solid-core-metadata.json +48 -48
  19. package/dist/services/computed-fields/entity/uuid-externalid-entity-computed-field-provider.service.d.ts +10 -0
  20. package/dist/services/computed-fields/entity/uuid-externalid-entity-computed-field-provider.service.d.ts.map +1 -0
  21. package/dist/services/computed-fields/entity/uuid-externalid-entity-computed-field-provider.service.js +35 -0
  22. package/dist/services/computed-fields/entity/uuid-externalid-entity-computed-field-provider.service.js.map +1 -0
  23. package/dist/services/field-metadata.service.d.ts.map +1 -1
  24. package/dist/services/field-metadata.service.js +10 -0
  25. package/dist/services/field-metadata.service.js.map +1 -1
  26. package/dist/services/genai/mcp-handlers/solid-add-controller-handler-method-mcp-handler.service.d.ts +16 -0
  27. package/dist/services/genai/mcp-handlers/solid-add-controller-handler-method-mcp-handler.service.d.ts.map +1 -0
  28. package/dist/services/genai/mcp-handlers/solid-add-controller-handler-method-mcp-handler.service.js +73 -0
  29. package/dist/services/genai/mcp-handlers/solid-add-controller-handler-method-mcp-handler.service.js.map +1 -0
  30. package/dist/services/genai/mcp-handlers/solid-add-custom-service-method-mcp-handler.service.d.ts +16 -0
  31. package/dist/services/genai/mcp-handlers/solid-add-custom-service-method-mcp-handler.service.d.ts.map +1 -0
  32. package/dist/services/genai/mcp-handlers/solid-add-custom-service-method-mcp-handler.service.js +74 -0
  33. package/dist/services/genai/mcp-handlers/solid-add-custom-service-method-mcp-handler.service.js.map +1 -0
  34. package/dist/services/genai/mcp-handlers/solid-add-header-button-or-row-button-to-list-view-mcp-handler.service.d.ts +16 -0
  35. package/dist/services/genai/mcp-handlers/solid-add-header-button-or-row-button-to-list-view-mcp-handler.service.d.ts.map +1 -0
  36. package/dist/services/genai/mcp-handlers/solid-add-header-button-or-row-button-to-list-view-mcp-handler.service.js +151 -0
  37. package/dist/services/genai/mcp-handlers/solid-add-header-button-or-row-button-to-list-view-mcp-handler.service.js.map +1 -0
  38. package/dist/services/model-metadata.service.d.ts.map +1 -1
  39. package/dist/services/model-metadata.service.js +14 -12
  40. package/dist/services/model-metadata.service.js.map +1 -1
  41. package/dist/services/setting.service.d.ts.map +1 -1
  42. package/dist/services/setting.service.js +4 -2
  43. package/dist/services/setting.service.js.map +1 -1
  44. package/dist/services/solid-ts-morph.service.d.ts +15 -0
  45. package/dist/services/solid-ts-morph.service.d.ts.map +1 -1
  46. package/dist/services/solid-ts-morph.service.js +68 -0
  47. package/dist/services/solid-ts-morph.service.js.map +1 -1
  48. package/dist/solid-core.module.d.ts.map +1 -1
  49. package/dist/solid-core.module.js +8 -0
  50. package/dist/solid-core.module.js.map +1 -1
  51. package/dist/tsconfig.tsbuildinfo +1 -1
  52. package/package.json +1 -1
  53. package/src/config/iam.config.ts +2 -1
  54. package/src/dtos/selection-dynamic-query.dto.ts +4 -0
  55. package/src/helpers/module-metadata-helper.service.ts +10 -3
  56. package/src/interfaces.ts +28 -1
  57. package/src/jobs/database/trigger-mcp-client-subscriber-database.service.ts +4 -3
  58. package/src/seeders/seed-data/solid-core-metadata.json +48 -48
  59. package/src/services/computed-fields/entity/uuid-externalid-entity-computed-field-provider.service.ts +34 -0
  60. package/src/services/field-metadata.service.ts +10 -0
  61. package/src/services/genai/mcp-handlers/solid-add-controller-handler-method-mcp-handler.service.ts +67 -0
  62. package/src/services/genai/mcp-handlers/solid-add-custom-service-method-mcp-handler.service.ts +69 -0
  63. package/src/services/genai/mcp-handlers/solid-add-header-button-or-row-button-to-list-view-mcp-handler.service.ts +141 -0
  64. package/src/services/model-metadata.service.ts +25 -23
  65. package/src/services/setting.service.ts +4 -2
  66. package/src/services/solid-ts-morph.service.ts +112 -0
  67. package/src/solid-core.module.ts +10 -1
  68. package/src/controllers/user.controller.ts.bkp +0 -78
@@ -0,0 +1,67 @@
1
+ import { Injectable, Logger } from "@nestjs/common";
2
+ import { AiInteraction } from "src/entities/ai-interaction.entity";
3
+ import { IMcpToolResponseHandler, McpComputedProviderResponse, PlanStep } from "../../../interfaces";
4
+ import { SolidTsMorphService } from "src/services/solid-ts-morph.service";
5
+
6
+ @Injectable()
7
+ export class SolidAddControllerHandlerMcpHandler implements IMcpToolResponseHandler {
8
+ private readonly logger = new Logger(SolidAddControllerHandlerMcpHandler.name);
9
+
10
+ constructor(private readonly tsMorph: SolidTsMorphService) { }
11
+
12
+ async apply(aiInteraction: AiInteraction) {
13
+ const raw = this.safeParse(aiInteraction.message);
14
+ const payload: McpComputedProviderResponse | undefined = (raw?.data?.plan ? raw.data : raw) as McpComputedProviderResponse;
15
+
16
+ if (!payload || !Array.isArray(payload.plan)) {
17
+ throw new Error("SolidAddControllerHandlerMethodMcpHandler: invalid MCP response; missing plan[]");
18
+ }
19
+
20
+ // Batch all plan steps in a single txn so nodemon restarts only once.
21
+ this.tsMorph.begin();
22
+ try {
23
+ for (const step of payload.plan as PlanStep[]) {
24
+ switch (step.type) {
25
+ case "createNewFile": {
26
+ const overwrite = step.overwrite ?? false;
27
+ this.tsMorph.createNewFile(step.path, step.content, overwrite);
28
+ break;
29
+ }
30
+ case "addMethodToExistingClass": {
31
+ this.tsMorph.addMethodToExistingClass(
32
+ step.path,
33
+ step.className,
34
+ step.methodName,
35
+ step.content,
36
+ );
37
+ break;
38
+ }
39
+ default:
40
+ throw new Error(`Unsupported plan step type: ${(step as any).type}`);
41
+ }
42
+ }
43
+
44
+ const result = await this.tsMorph.commit();
45
+
46
+ return {
47
+ seedingRequired: true,
48
+ serverRebooting: true,
49
+ appliedSteps: payload.plan.length,
50
+ wroteFiles: result.wrote,
51
+ };
52
+ } catch (err) {
53
+ this.logger.error(`Apply failed; rolling back. ${String(err)}`);
54
+ this.tsMorph.rollback();
55
+ throw err;
56
+ }
57
+ }
58
+
59
+ private safeParse(str: string): any {
60
+ try {
61
+ return JSON.parse(str);
62
+ } catch {
63
+ const unescaped = str.replace(/\\'/g, "'");
64
+ return JSON.parse(unescaped);
65
+ }
66
+ }
67
+ }
@@ -0,0 +1,69 @@
1
+ import { Injectable, Logger } from "@nestjs/common";
2
+ import { AiInteraction } from "src/entities/ai-interaction.entity";
3
+ import { IMcpToolResponseHandler, McpComputedProviderResponse, PlanStep } from "../../../interfaces";
4
+ import { SolidTsMorphService } from "src/services/solid-ts-morph.service";
5
+
6
+ const RESTART_TOUCH_FILE = process.env.MCP_RESTART_TOUCH_FILE || "tmp/restart.touch";
7
+
8
+ @Injectable()
9
+ export class SolidAddCustomServiceMethodMcpHandler implements IMcpToolResponseHandler {
10
+ private readonly logger = new Logger(SolidAddCustomServiceMethodMcpHandler.name);
11
+
12
+ constructor(private readonly tsMorph: SolidTsMorphService) { }
13
+
14
+ async apply(aiInteraction: AiInteraction) {
15
+ const raw = this.safeParse(aiInteraction.message);
16
+ const payload: McpComputedProviderResponse | undefined = (raw?.data?.plan ? raw.data : raw) as McpComputedProviderResponse;
17
+
18
+ if (!payload || !Array.isArray(payload.plan)) {
19
+ throw new Error("SolidAddCustomServiceMethodMcpHandler: invalid MCP response; missing plan[]");
20
+ }
21
+
22
+ // Batch all plan steps in a single txn so nodemon restarts only once.
23
+ this.tsMorph.begin();
24
+ try {
25
+ for (const step of payload.plan as PlanStep[]) {
26
+ switch (step.type) {
27
+ case "createNewFile": {
28
+ const overwrite = step.overwrite ?? false;
29
+ this.tsMorph.createNewFile(step.path, step.content, overwrite);
30
+ break;
31
+ }
32
+ case "addMethodToExistingClass": {
33
+ this.tsMorph.addMethodToExistingClass(
34
+ step.path,
35
+ step.className,
36
+ step.methodName,
37
+ step.content,
38
+ );
39
+ break;
40
+ }
41
+ default:
42
+ throw new Error(`Unsupported plan step type: ${(step as any).type}`);
43
+ }
44
+ }
45
+
46
+ const result = await this.tsMorph.commit();
47
+
48
+ return {
49
+ seedingRequired: false,
50
+ serverRebooting: true,
51
+ appliedSteps: payload.plan.length,
52
+ wroteFiles: result.wrote,
53
+ };
54
+ } catch (err) {
55
+ this.logger.error(`Apply failed; rolling back. ${String(err)}`);
56
+ this.tsMorph.rollback();
57
+ throw err;
58
+ }
59
+ }
60
+
61
+ private safeParse(str: string): any {
62
+ try {
63
+ return JSON.parse(str);
64
+ } catch {
65
+ const unescaped = str.replace(/\\'/g, "'");
66
+ return JSON.parse(unescaped);
67
+ }
68
+ }
69
+ }
@@ -0,0 +1,141 @@
1
+ import { Injectable, Logger } from "@nestjs/common";
2
+ import { AiInteraction } from "src/entities/ai-interaction.entity";
3
+ import { IMcpToolResponseHandler, McpComputedProviderResponse, PlanStep } from "../../../interfaces";
4
+ import { SolidTsMorphService } from "src/services/solid-ts-morph.service";
5
+ import { ModuleMetadataHelperService } from "src/helpers/module-metadata-helper.service";
6
+ import * as fs from "fs/promises";
7
+
8
+ const RESTART_TOUCH_FILE = process.env.MCP_RESTART_TOUCH_FILE || "tmp/restart.touch";
9
+
10
+ @Injectable()
11
+ export class SolidAddHeaderButtonOrRowButtonToListViewMcpHandler implements IMcpToolResponseHandler {
12
+ private readonly logger = new Logger(SolidAddHeaderButtonOrRowButtonToListViewMcpHandler.name);
13
+
14
+ constructor(
15
+ private readonly tsMorph: SolidTsMorphService,
16
+ private readonly moduleMetadataHelperService: ModuleMetadataHelperService,
17
+
18
+ ) { }
19
+
20
+ async apply(aiInteraction: AiInteraction) {
21
+ const raw = this.safeParse(aiInteraction.message);
22
+ const payload: McpComputedProviderResponse | undefined = (raw?.data?.plan ? raw.data : raw) as McpComputedProviderResponse;
23
+
24
+ if (!payload || !Array.isArray(payload.plan)) {
25
+ throw new Error("SolidAddHeaderButtonOrRowButtonToListViewMcpHandler: invalid MCP response; missing plan[]");
26
+ }
27
+
28
+ // Batch all plan steps in a single txn so nodemon restarts only once.
29
+ this.tsMorph.begin();
30
+ try {
31
+ for (const step of payload.plan as PlanStep[]) {
32
+ switch (step.type) {
33
+ case "createNewFile": {
34
+ const overwrite = step.overwrite ?? false;
35
+ this.tsMorph.createNewFile(step.path, step.content, overwrite);
36
+ break;
37
+ }
38
+ case "registerSolidXExtensionComponent": {
39
+ this.tsMorph.registerSolidUiExtension(
40
+ step.path,
41
+ step.content,
42
+ );
43
+ this.tsMorph.addImport(
44
+ step.path,
45
+ step.importExtensionComponent,
46
+ );
47
+ break;
48
+ }
49
+ case "addListViewButton": {
50
+
51
+ const filePath =
52
+ await this.moduleMetadataHelperService.getModuleMetadataFilePath(
53
+ step.moduleName
54
+ );
55
+ try {
56
+ await fs.access(filePath);
57
+ } catch {
58
+ this.logger.error(`Metadata file not found: ${filePath}`);
59
+ return;
60
+ }
61
+ const metaData =
62
+ await this.moduleMetadataHelperService.getModuleMetadataConfiguration(
63
+ filePath
64
+ );
65
+ // Remove, update or insert logic
66
+
67
+
68
+ // Find the existing view of type "list" for the given module & model
69
+ const existingViewIndex = metaData.views.findIndex(
70
+ (view: any) =>
71
+ view.type === "list" &&
72
+ view.moduleUserKey === step.moduleName &&
73
+ view.modelUserKey === step.modelName
74
+ );
75
+
76
+ if (existingViewIndex !== -1) {
77
+ const view = metaData.views[existingViewIndex];
78
+
79
+ // Ensure layout & attrs exist
80
+ view.layout = view.layout || {};
81
+ view.layout.attrs = view.layout.attrs || {};
82
+
83
+ // Initialize rowButtons or headerButtons if not present
84
+ if (!Array.isArray(view.layout.attrs[step.buttonType])) {
85
+ view.layout.attrs[step.buttonType] = [];
86
+ }
87
+
88
+ let buttonContent = step.content;
89
+
90
+ // Parse only if it’s a string
91
+ if (typeof buttonContent === "string") {
92
+ try {
93
+ buttonContent = JSON.parse(buttonContent);
94
+ } catch (err) {
95
+ this.logger.error("❌ Failed to parse step.content JSON:", err);
96
+ return;
97
+ }
98
+ }
99
+
100
+
101
+ // Push the new button content
102
+ view.layout.attrs[step.buttonType].push(buttonContent);
103
+
104
+ console.log(`✅ Added ${step.buttonType} to view: ${view.name}`);
105
+ } else {
106
+ console.warn(`⚠️ No matching list view found for module=${step.moduleName} and model=${step.modelName}`);
107
+ }
108
+
109
+ const updatedContent = JSON.stringify(metaData, null, 2);
110
+ await fs.writeFile(filePath, updatedContent);
111
+ this.logger.log(`Updated list view in ${filePath}`);
112
+
113
+ break;
114
+ }
115
+ default:
116
+ throw new Error(`Unsupported plan step type: ${(step as any).type}`);
117
+ }
118
+ }
119
+
120
+ const result = await this.tsMorph.commit();
121
+
122
+ return {
123
+ seedingRequired: true,
124
+ serverRebooting: false,
125
+ };
126
+ } catch (err) {
127
+ this.logger.error(`Apply failed; rolling back. ${String(err)}`);
128
+ this.tsMorph.rollback();
129
+ throw err;
130
+ }
131
+ }
132
+
133
+ private safeParse(str: string): any {
134
+ try {
135
+ return JSON.parse(str);
136
+ } catch {
137
+ const unescaped = str.replace(/\\'/g, "'");
138
+ return JSON.parse(unescaped);
139
+ }
140
+ }
141
+ }
@@ -657,28 +657,30 @@ export class ModelMetadataService {
657
657
  // <moduleName>-metadata.json | Remove references to this model in the model metadata, menu, action & view sections. | Automatic
658
658
  const filePath = await this.moduleMetadataHelperService.getModuleMetadataFilePath(modelEntity.module.name);
659
659
  const metaData = await this.moduleMetadataHelperService.getModuleMetadataConfiguration(filePath);
660
- const existingModelIndex = metaData.moduleMetadata.models.findIndex(
661
- (existingModel: any) => existingModel.singularName === modelEntity.singularName
662
- );
660
+ if (metaData) {
661
+ const existingModelIndex = metaData.moduleMetadata.models.findIndex(
662
+ (existingModel: any) => existingModel.singularName === modelEntity.singularName
663
+ );
663
664
 
664
- // Remove the model to be deleted from the metadata
665
- if (existingModelIndex !== -1) {
666
- metaData.moduleMetadata.models.splice(existingModelIndex, 1);
667
- }
665
+ // Remove the model to be deleted from the metadata
666
+ if (existingModelIndex !== -1) {
667
+ metaData.moduleMetadata.models.splice(existingModelIndex, 1);
668
+ }
668
669
 
669
- // Remove references to this model in the menu, action & view sections.
670
- metaData.moduleMetadata.menus = metaData.moduleMetadata.menus.filter(
671
- (menu: any) => menu.modelUserKey !== modelEntity.singularName
672
- );
673
- metaData.moduleMetadata.actions = metaData.moduleMetadata.actions.filter(
674
- (action: any) => action.modelUserKey !== modelEntity.singularName
675
- );
676
- metaData.moduleMetadata.views = metaData.moduleMetadata.views.filter(
677
- (view: any) => view.modelUserKey !== modelEntity.singularName
678
- );
670
+ // Remove references to this model in the menu, action & view sections.
671
+ metaData.moduleMetadata.menus = metaData.moduleMetadata.menus.filter(
672
+ (menu: any) => menu.modelUserKey !== modelEntity.singularName
673
+ );
674
+ metaData.moduleMetadata.actions = metaData.moduleMetadata.actions.filter(
675
+ (action: any) => action.modelUserKey !== modelEntity.singularName
676
+ );
677
+ metaData.moduleMetadata.views = metaData.moduleMetadata.views.filter(
678
+ (view: any) => view.modelUserKey !== modelEntity.singularName
679
+ );
679
680
 
680
- const updatedContent = JSON.stringify(metaData, null, 2);
681
- await fs.writeFile(filePath, updatedContent);
681
+ const updatedContent = JSON.stringify(metaData, null, 2);
682
+ await fs.writeFile(filePath, updatedContent);
683
+ }
682
684
 
683
685
  // <moduleName>.module.ts | Remove all references and imports of the above files. | Manual (X)
684
686
  // const moduleFilePath = path.resolve(modulePath, `${dasherize(modelEntity.module.name)}.module.ts`);
@@ -901,12 +903,12 @@ export class ModelMetadataService {
901
903
  children: [
902
904
  {
903
905
  type: "column",
904
- attrs: { name: "group-1", label: "", className: "col-6" },
906
+ attrs: { name: "group-1", label: "", className: "col-12 sm:col-12 md:col-6 lg:col-6" },
905
907
  children: column1Fields
906
908
  },
907
909
  {
908
910
  type: "column",
909
- attrs: { name: "group-2", label: "", className: "col-6" },
911
+ attrs: { name: "group-2", label: "", className: "col-12 sm:col-12 md:col-6 lg:col-6" },
910
912
  children: column2Fields
911
913
  }
912
914
  ]
@@ -1014,12 +1016,12 @@ export class ModelMetadataService {
1014
1016
  children: [
1015
1017
  {
1016
1018
  type: "column",
1017
- attrs: { name: "group-1", label: "", className: "col-6" },
1019
+ attrs: { name: "group-1", label: "", className: "col-12 sm:col-12 md:col-6 lg:col-6" },
1018
1020
  children: firstHalf
1019
1021
  },
1020
1022
  {
1021
1023
  type: "column",
1022
- attrs: { name: "group-2", label: "", className: "col-6" },
1024
+ attrs: { name: "group-2", label: "", className: "col-12 sm:col-12 md:col-6 lg:col-6" },
1023
1025
  children: secondHalf
1024
1026
  }
1025
1027
  ]
@@ -76,7 +76,8 @@ export class SettingService extends CRUDService<Setting> {
76
76
  solidXGenAiCodeBuilderConfig: JSON.stringify({
77
77
  defaultProvider: "",
78
78
  availableProviders: []
79
- })
79
+ }),
80
+ showNameFieldsForRegistration:this.iamConfiguration.showNameFieldsForRegistration
80
81
  };
81
82
 
82
83
  const existingSettings = await this.repo.find();
@@ -187,7 +188,8 @@ export class SettingService extends CRUDService<Setting> {
187
188
  defaultProvider: "",
188
189
  availableProviders: []
189
190
  }),
190
- iamAutoGeneratedPassword:this.iamConfiguration.iamAutoGeneratedPassword
191
+ iamAutoGeneratedPassword:this.iamConfiguration.iamAutoGeneratedPassword,
192
+ showNameFieldsForRegistration:this.iamConfiguration.showNameFieldsForRegistration
191
193
  };
192
194
  }
193
195
 
@@ -3,6 +3,7 @@ import { Injectable, Logger } from "@nestjs/common";
3
3
  import { join, dirname, normalize, isAbsolute, basename } from "node:path";
4
4
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
5
5
  import { Project, Node, ObjectLiteralExpression, ArrayLiteralExpression, QuoteKind, IndentationText } from "ts-morph";
6
+ import { MethodDeclarationStructure, StructureKind } from "ts-morph";
6
7
 
7
8
  type Bucket = "providers" | "exports";
8
9
 
@@ -260,4 +261,115 @@ export class SolidTsMorphService {
260
261
  this.logger.log(`Staged provider registration in: ${this.rel(abs)} (${registerIn.join(", ")})`);
261
262
  return { staged: true };
262
263
  }
264
+
265
+
266
+ addMethodToExistingClass(
267
+ filePath: string,
268
+ className: string,
269
+ methodName: string,
270
+ methodCode: string,
271
+ ): { staged: boolean; overwritten: boolean; skipped: boolean } {
272
+ const abs = this.resolveRepoPath(filePath);
273
+ if (!existsSync(abs))
274
+ throw new Error(`addMethodToExistingClass: File not found at ${filePath}`);
275
+
276
+ const existing = this.project.getSourceFile(abs);
277
+ const sourceFile = existing
278
+ ? existing
279
+ : this.project.createSourceFile(abs, readFileSync(abs, "utf8"), { overwrite: true });
280
+
281
+ const targetClass = sourceFile.getClass(className);
282
+ if (!targetClass)
283
+ throw new Error(`addMethodToExistingClass: Class ${className} not found in ${filePath}`);
284
+
285
+ const existingMethod = targetClass.getMethod(methodName);
286
+ if (existingMethod) {
287
+
288
+ this.logger.log(`Skipped adding method '${methodName}' (already exists)`);
289
+ return { staged: false, overwritten: false, skipped: true };
290
+ }
291
+ // Add the LLM-generated method directly
292
+ targetClass.addMember(methodCode);
293
+
294
+
295
+ this.dirtySourceFiles.add(abs);
296
+ this.logger.log(`Staged method '${methodName}' in class ${className} at ${this.rel(abs)}`);
297
+ return { staged: true, overwritten: !!existingMethod, skipped: false };
298
+ }
299
+
300
+ registerSolidUiExtension(
301
+ filePath: string,
302
+ lineToAdd: string
303
+ ): { staged: boolean; overwritten: boolean; skipped: boolean } {
304
+ const abs = this.resolveRepoPath(filePath);
305
+ if (!existsSync(abs))
306
+ throw new Error(`registerSolidUiExtension: File not found at ${filePath}`);
307
+
308
+ const fileContent = readFileSync(abs, "utf8");
309
+
310
+ // Check if the line already exists (avoid duplicates)
311
+ if (fileContent.includes(lineToAdd.trim())) {
312
+ this.logger.log(`Skipped adding line (already exists): ${lineToAdd}`);
313
+ return { staged: false, overwritten: false, skipped: true };
314
+ }
315
+
316
+ // Append the new line at the end, ensuring newline
317
+ const newContent = fileContent.trimEnd() + "\n" + lineToAdd.trim() + "\n";
318
+
319
+ // Write updated content back
320
+ writeFileSync(abs, newContent, "utf8");
321
+
322
+ this.dirtySourceFiles.add(abs);
323
+ this.logger.log(`Staged new line in ${this.rel(abs)}: ${lineToAdd}`);
324
+ return { staged: true, overwritten: false, skipped: false };
325
+ }
326
+
327
+ addImport(
328
+ filePath: string,
329
+ importLine: string
330
+ ): { staged: boolean; overwritten: boolean; skipped: boolean } {
331
+ const abs = this.resolveRepoPath(filePath);
332
+ if (!existsSync(abs))
333
+ throw new Error(`addImport: File not found at ${filePath}`);
334
+
335
+ let fileContent = readFileSync(abs, "utf8");
336
+
337
+ // If import already exists — skip
338
+ if (fileContent.includes(importLine.trim())) {
339
+ this.logger.log(`Skipped adding import (already exists): ${importLine}`);
340
+ return { staged: false, overwritten: false, skipped: true };
341
+ }
342
+
343
+ // Find last import statement (so we can insert after all imports)
344
+ const importRegex = /^import .+ from .+;$/gm;
345
+ let lastImportMatch: RegExpExecArray | null;
346
+ let lastImportIndex = -1;
347
+
348
+ while ((lastImportMatch = importRegex.exec(fileContent)) !== null) {
349
+ lastImportIndex = lastImportMatch.index + lastImportMatch[0].length;
350
+ }
351
+
352
+ // Insert after last import (or at top if none exist)
353
+ let newContent: string;
354
+ if (lastImportIndex !== -1) {
355
+ newContent =
356
+ fileContent.slice(0, lastImportIndex) +
357
+ "\n" +
358
+ importLine.trim() +
359
+ "\n" +
360
+ fileContent.slice(lastImportIndex);
361
+ } else {
362
+ // No imports found — insert at top
363
+ newContent = importLine.trim() + "\n\n" + fileContent;
364
+ }
365
+
366
+ writeFileSync(abs, newContent, "utf8");
367
+
368
+ this.dirtySourceFiles.add(abs);
369
+ this.logger.log(`Staged import in ${this.rel(abs)}: ${importLine}`);
370
+ return { staged: true, overwritten: false, skipped: false };
371
+ }
372
+
373
+
374
+
263
375
  }
@@ -102,6 +102,7 @@ import { UserSeederService } from './seeders/user-seeder.service';
102
102
  import { AuthenticationService } from './services/authentication.service';
103
103
  import { BcryptService } from './services/bcrypt.service';
104
104
  import { UuidExternalIdComputedFieldProvider } from './services/computed-fields/uuid-external-id-computed-field-provider.service';
105
+ import { UuidExternalIdEntityComputedFieldProvider } from './services/computed-fields/entity/uuid-externalid-entity-computed-field-provider.service';
105
106
  import { EmailTemplateService } from './services/email-template.service';
106
107
  import { FileService } from './services/file.service';
107
108
  import { HashingService } from './services/hashing.service';
@@ -296,6 +297,9 @@ import { SolidCreateComputedProviderMcpHandler } from './services/genai/mcp-hand
296
297
  import { SolidTsMorphService } from './services/solid-ts-morph.service';
297
298
  import { SolidAddVariableToDashboardMcpHandler } from './services/genai/mcp-handlers/solid-add-variable-to-dashboard-mcp-handler.service';
298
299
  import { SolidAddQuestionToDashboardMcpHandler } from './services/genai/mcp-handlers/solid-add-question-to-dashboard-mcp-handler.service';
300
+ import { SolidAddCustomServiceMethodMcpHandler } from './services/genai/mcp-handlers/solid-add-custom-service-method-mcp-handler.service';
301
+ import { SolidAddHeaderButtonOrRowButtonToListViewMcpHandler } from './services/genai/mcp-handlers/solid-add-header-button-or-row-button-to-list-view-mcp-handler.service';
302
+ import { SolidAddControllerHandlerMcpHandler } from './services/genai/mcp-handlers/solid-add-controller-handler-method-mcp-handler.service';
299
303
 
300
304
 
301
305
  @Global()
@@ -538,6 +542,7 @@ import { SolidAddQuestionToDashboardMcpHandler } from './services/genai/mcp-hand
538
542
  TinyUrlService,
539
543
  PdfService,
540
544
  UuidExternalIdComputedFieldProvider,
545
+ UuidExternalIdEntityComputedFieldProvider,
541
546
  ListOfModelsSelectionProvider,
542
547
  ListOfScheduledJobsSelectionProvider,
543
548
  LocaleListSelectionProvider,
@@ -632,7 +637,7 @@ import { SolidAddQuestionToDashboardMcpHandler } from './services/genai/mcp-hand
632
637
  SolidCreateModelWithFieldsMcpHandler,
633
638
  SolidAddFieldsToModelMcpHandler,
634
639
  SolidUpdateLayoutMcpHandler,
635
-
640
+
636
641
  SolidCreateDashboardWithWidgetsMcpHandler,
637
642
  SolidCreateDashboardQuestionMcpHandler,
638
643
  SolidCreateDashboardQuestionSqlDatasetConfigMcpHandler,
@@ -640,6 +645,10 @@ import { SolidAddQuestionToDashboardMcpHandler } from './services/genai/mcp-hand
640
645
  SolidCreateComputedProviderMcpHandler,
641
646
  SolidAddVariableToDashboardMcpHandler,
642
647
  SolidAddQuestionToDashboardMcpHandler,
648
+
649
+ SolidAddCustomServiceMethodMcpHandler,
650
+ SolidAddHeaderButtonOrRowButtonToListViewMcpHandler,
651
+ SolidAddControllerHandlerMcpHandler,
643
652
  SolidTsMorphService,
644
653
 
645
654
  ViewMetadataRepository,
@@ -1,78 +0,0 @@
1
- import { Body, Controller, Delete, Get, Param, ParseIntPipe, Post, Query } from '@nestjs/common';
2
- import { ApiBearerAuth, ApiForbiddenResponse, ApiQuery, ApiTags } from '@nestjs/swagger';
3
- import { ActiveUserData } from 'src/interfaces/active-user-data.interface';
4
- import { PaginationQueryDto } from 'src/dtos/pagination-query.dto';
5
- import { ActiveUser } from '../decorators/active-user.decorator';
6
- import { UserService } from '../services/user.service';
7
- import { MutateUserRolesDto } from '../dtos/mutate-user-roles.dto';
8
- import { MutateUserRolesBulkDto } from '../dtos/mutate-user-roles-list.dto';
9
- import { BasicFilterDto } from 'src/dtos/basic-filters.dto';
10
-
11
- @Controller('users')
12
- @ApiTags("Iam")
13
- export class UserController {
14
- constructor(
15
- private readonly userService: UserService
16
- ) { }
17
-
18
- // /api/users
19
- @ApiBearerAuth("jwt")
20
- @Get()
21
- @ApiBearerAuth("jwt")
22
- @ApiQuery({ name: 'limit', required: false, type: Number })
23
- @ApiQuery({ name: 'offset', required: false, type: Number })
24
- @ApiQuery({ name: 'fields', required: false, type: Array })
25
- @ApiQuery({ name: 'sort', required: false, type: Array })
26
- @ApiQuery({ name: 'groupBy', required: false, type: Array })
27
- @ApiQuery({ name: 'populate', required: false, type: Array })
28
- @ApiQuery({ name: 'populateMedia', required: false, type: Array })
29
- @ApiQuery({ name: 'filters', required: false, type: Array })
30
- async findMany(
31
- @Query() basicFilterDto: BasicFilterDto,
32
- @ActiveUser() activeUser: ActiveUserData,
33
- ) {
34
- return this.userService.findMany(basicFilterDto);
35
- }
36
-
37
- // /api/users/:id
38
- @ApiBearerAuth("jwt")
39
- @ApiForbiddenResponse({ description: 'Forbidden.' })
40
- @Get(':id')
41
- findOne(@Param('id', ParseIntPipe) id: number) {
42
- return this.userService.findOne(id);
43
- }
44
-
45
- @ApiBearerAuth("jwt")
46
- @ApiForbiddenResponse({ description: 'Forbidden.' })
47
- @Post('roles')
48
- addRoleToUser(@Body() mutateUserRoles: MutateUserRolesDto) {
49
- return this.userService.addRoleToUser(mutateUserRoles.username, mutateUserRoles.roleName);
50
- }
51
-
52
- @ApiBearerAuth("jwt")
53
- @ApiForbiddenResponse({ description: 'Forbidden.' })
54
- @Post('roles/bulk')
55
- addRolesToUser(@Body() mutateUserRolesBulk: MutateUserRolesBulkDto) {
56
- return this.userService.addRolesToUser(mutateUserRolesBulk.username, mutateUserRolesBulk.roleNames);
57
- }
58
-
59
- @ApiBearerAuth("jwt")
60
- @ApiForbiddenResponse({ description: 'Forbidden.' })
61
- @Delete('roles')
62
- removeRoleFromUser(userEmail: string, roleName: string) {
63
- return this.userService.removeRoleFromUser(userEmail, roleName);
64
- }
65
-
66
- @ApiBearerAuth("jwt")
67
- @Delete('/bulk')
68
- async deleteMany(@Body() ids: number[]) {
69
- return this.userService.deleteMany(ids);
70
- }
71
-
72
-
73
- @ApiBearerAuth("jwt")
74
- @Delete(':id')
75
- remove(@Param('id') id: number) {
76
- return this.userService.remove(id);
77
- }
78
- }