@solidstarters/solid-core 1.2.185 → 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.
- package/dist/dtos/selection-dynamic-query.dto.d.ts +1 -0
- package/dist/dtos/selection-dynamic-query.dto.d.ts.map +1 -1
- package/dist/dtos/selection-dynamic-query.dto.js +6 -1
- package/dist/dtos/selection-dynamic-query.dto.js.map +1 -1
- package/dist/helpers/module-metadata-helper.service.d.ts.map +1 -1
- package/dist/helpers/module-metadata-helper.service.js +8 -3
- package/dist/helpers/module-metadata-helper.service.js.map +1 -1
- package/dist/interfaces.d.ts +17 -1
- package/dist/interfaces.d.ts.map +1 -1
- package/dist/interfaces.js.map +1 -1
- package/dist/services/field-metadata.service.d.ts.map +1 -1
- package/dist/services/field-metadata.service.js +10 -0
- package/dist/services/field-metadata.service.js.map +1 -1
- package/dist/services/genai/mcp-handlers/solid-add-controller-handler-method-mcp-handler.service.d.ts +16 -0
- package/dist/services/genai/mcp-handlers/solid-add-controller-handler-method-mcp-handler.service.d.ts.map +1 -0
- package/dist/services/genai/mcp-handlers/solid-add-controller-handler-method-mcp-handler.service.js +73 -0
- package/dist/services/genai/mcp-handlers/solid-add-controller-handler-method-mcp-handler.service.js.map +1 -0
- package/dist/services/genai/mcp-handlers/solid-add-header-button-or-row-button-to-list-view-mcp-handler.service.d.ts +16 -0
- package/dist/services/genai/mcp-handlers/solid-add-header-button-or-row-button-to-list-view-mcp-handler.service.d.ts.map +1 -0
- package/dist/services/genai/mcp-handlers/solid-add-header-button-or-row-button-to-list-view-mcp-handler.service.js +151 -0
- package/dist/services/genai/mcp-handlers/solid-add-header-button-or-row-button-to-list-view-mcp-handler.service.js.map +1 -0
- package/dist/services/model-metadata.service.d.ts.map +1 -1
- package/dist/services/model-metadata.service.js +10 -8
- package/dist/services/model-metadata.service.js.map +1 -1
- package/dist/services/solid-ts-morph.service.d.ts +10 -0
- package/dist/services/solid-ts-morph.service.d.ts.map +1 -1
- package/dist/services/solid-ts-morph.service.js +47 -0
- package/dist/services/solid-ts-morph.service.js.map +1 -1
- package/dist/solid-core.module.d.ts.map +1 -1
- package/dist/solid-core.module.js +4 -0
- package/dist/solid-core.module.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/dtos/selection-dynamic-query.dto.ts +4 -0
- package/src/helpers/module-metadata-helper.service.ts +10 -3
- package/src/interfaces.ts +17 -1
- package/src/services/field-metadata.service.ts +10 -0
- package/src/services/genai/mcp-handlers/solid-add-controller-handler-method-mcp-handler.service.ts +67 -0
- package/src/services/genai/mcp-handlers/solid-add-header-button-or-row-button-to-list-view-mcp-handler.service.ts +141 -0
- package/src/services/model-metadata.service.ts +21 -19
- package/src/services/solid-ts-morph.service.ts +75 -0
- package/src/solid-core.module.ts +4 -0
- package/src/controllers/user.controller.ts.bkp +0 -78
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solidstarters/solid-core",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.186",
|
|
4
4
|
"description": "This module is a NestJS module containing all the required core providers required by a Solid application",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -16,8 +16,15 @@ export class ModuleMetadataHelperService {
|
|
|
16
16
|
// }
|
|
17
17
|
|
|
18
18
|
async getModuleMetadataConfiguration(configFilePath: string): Promise<any> {
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
try {
|
|
20
|
+
const fileContent = await fs.readFile(configFilePath, 'utf8');
|
|
21
|
+
return JSON.parse(fileContent);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
this.logger.error(`module metadata configuration non existent at: ${configFilePath}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return null;
|
|
21
28
|
}
|
|
22
29
|
|
|
23
30
|
async getModulePath(moduleName: string): Promise<string> {
|
|
@@ -30,7 +37,7 @@ export class ModuleMetadataHelperService {
|
|
|
30
37
|
const filePath = path.join(folderPath, `${dashModuleName}-metadata.json`);
|
|
31
38
|
// Check if filePath exists
|
|
32
39
|
const fileExists = await this.fileService.fileExists(filePath);
|
|
33
|
-
this.logger.debug(`File exists: ${fileExists} at ${filePath}`);
|
|
40
|
+
// this.logger.debug(`File exists: ${fileExists} at ${filePath}`);
|
|
34
41
|
if (!fileExists) {
|
|
35
42
|
// If the module is solid-core, try the fallback path, in case the current root directory is the solid core project
|
|
36
43
|
if (dashModuleName === SOLID_CORE_MODULE_NAME) {
|
package/src/interfaces.ts
CHANGED
|
@@ -83,6 +83,9 @@ export interface McpResponse {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
export interface ISelectionProviderContext {
|
|
86
|
+
limit: number;
|
|
87
|
+
offset: number;
|
|
88
|
+
formValues: Record<string, any>;
|
|
86
89
|
// query: string;
|
|
87
90
|
}
|
|
88
91
|
|
|
@@ -278,7 +281,7 @@ export interface IErrorCodeProvider {
|
|
|
278
281
|
|
|
279
282
|
// MCP Tool Related
|
|
280
283
|
|
|
281
|
-
export type PlanStep = CreateNewFileStep | RegisterNestProviderStep | AddMethodToExistingClassStep;
|
|
284
|
+
export type PlanStep = CreateNewFileStep | RegisterNestProviderStep | AddMethodToExistingClassStep | RegisterSolidXExtensionComponentStep | AddListViewButtonStep;
|
|
282
285
|
|
|
283
286
|
export interface CreateNewFileStep {
|
|
284
287
|
type: "createNewFile";
|
|
@@ -307,7 +310,20 @@ export interface AddMethodToExistingClassStep {
|
|
|
307
310
|
rationale?: string; // optional, ignored by executor
|
|
308
311
|
}
|
|
309
312
|
|
|
313
|
+
export interface RegisterSolidXExtensionComponentStep {
|
|
314
|
+
type: "registerSolidXExtensionComponent";
|
|
315
|
+
path: string; // e.g. apps/api/src/address-master/services/address-master.service.ts
|
|
316
|
+
content?: string; // Code
|
|
317
|
+
importExtensionComponent?: string;
|
|
318
|
+
}
|
|
310
319
|
|
|
320
|
+
export interface AddListViewButtonStep {
|
|
321
|
+
type: "addListViewButton";
|
|
322
|
+
moduleName?: string;
|
|
323
|
+
modelName?: string;
|
|
324
|
+
buttonType?: string;
|
|
325
|
+
content?: string; // Code
|
|
326
|
+
}
|
|
311
327
|
|
|
312
328
|
export interface McpComputedProviderResponse {
|
|
313
329
|
plan: PlanStep[];
|
|
@@ -15,6 +15,7 @@ import { ModelMetadata } from '../entities/model-metadata.entity';
|
|
|
15
15
|
import { ISelectionProviderValues } from '../interfaces';
|
|
16
16
|
import { CrudHelperService } from './crud-helper.service';
|
|
17
17
|
import { ERROR_MESSAGES } from 'src/constants/error-messages';
|
|
18
|
+
import qs from 'qs';
|
|
18
19
|
|
|
19
20
|
|
|
20
21
|
@Injectable()
|
|
@@ -1046,6 +1047,11 @@ export class FieldMetadataService implements OnApplicationBootstrap {
|
|
|
1046
1047
|
const selectionDynamicProviderCtxt = JSON.parse(entity.selectionDynamicProviderCtxt ? entity.selectionDynamicProviderCtxt : '{}');
|
|
1047
1048
|
selectionDynamicProviderCtxt['limit'] = query.limit;
|
|
1048
1049
|
selectionDynamicProviderCtxt['offset'] = query.offset;
|
|
1050
|
+
const formValues = query.formValues || {};
|
|
1051
|
+
// Parse the form values if they are in string format using qs
|
|
1052
|
+
const parsedFormValues = typeof formValues === 'string' ? qs.parse(formValues, { allowDots: true }) : formValues;
|
|
1053
|
+
selectionDynamicProviderCtxt['formValues'] = parsedFormValues;
|
|
1054
|
+
|
|
1049
1055
|
|
|
1050
1056
|
// 3. get hold of the provider instance from the SolidRegistry
|
|
1051
1057
|
const selectionProviderInstance = this.solidRegistry.getSelectionProviderInstance(selectionDynamicProvider);
|
|
@@ -1073,6 +1079,10 @@ export class FieldMetadataService implements OnApplicationBootstrap {
|
|
|
1073
1079
|
const selectionDynamicProviderCtxt = JSON.parse(entity.selectionDynamicProviderCtxt ? entity.selectionDynamicProviderCtxt : '{}');
|
|
1074
1080
|
selectionDynamicProviderCtxt['limit'] = query.limit;
|
|
1075
1081
|
selectionDynamicProviderCtxt['offset'] = query.offset;
|
|
1082
|
+
const formValues = query.formValues || {};
|
|
1083
|
+
// Parse the form values if they are in string format using qs
|
|
1084
|
+
const parsedFormValues = typeof formValues === 'string' ? qs.parse(formValues, { allowDots: true }) : formValues;
|
|
1085
|
+
selectionDynamicProviderCtxt['formValues'] = parsedFormValues;
|
|
1076
1086
|
|
|
1077
1087
|
// 3. get hold of the provider instance from the SolidRegistry
|
|
1078
1088
|
const selectionProviderInstance = this.solidRegistry.getSelectionProviderInstance(selectionDynamicProvider);
|
package/src/services/genai/mcp-handlers/solid-add-controller-handler-method-mcp-handler.service.ts
ADDED
|
@@ -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,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
|
-
|
|
661
|
-
|
|
662
|
-
|
|
660
|
+
if (metaData) {
|
|
661
|
+
const existingModelIndex = metaData.moduleMetadata.models.findIndex(
|
|
662
|
+
(existingModel: any) => existingModel.singularName === modelEntity.singularName
|
|
663
|
+
);
|
|
663
664
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
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
|
-
|
|
681
|
-
|
|
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`);
|
|
@@ -297,4 +297,79 @@ export class SolidTsMorphService {
|
|
|
297
297
|
return { staged: true, overwritten: !!existingMethod, skipped: false };
|
|
298
298
|
}
|
|
299
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
|
+
|
|
300
375
|
}
|
package/src/solid-core.module.ts
CHANGED
|
@@ -298,6 +298,8 @@ import { SolidTsMorphService } from './services/solid-ts-morph.service';
|
|
|
298
298
|
import { SolidAddVariableToDashboardMcpHandler } from './services/genai/mcp-handlers/solid-add-variable-to-dashboard-mcp-handler.service';
|
|
299
299
|
import { SolidAddQuestionToDashboardMcpHandler } from './services/genai/mcp-handlers/solid-add-question-to-dashboard-mcp-handler.service';
|
|
300
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';
|
|
301
303
|
|
|
302
304
|
|
|
303
305
|
@Global()
|
|
@@ -645,6 +647,8 @@ import { SolidAddCustomServiceMethodMcpHandler } from './services/genai/mcp-hand
|
|
|
645
647
|
SolidAddQuestionToDashboardMcpHandler,
|
|
646
648
|
|
|
647
649
|
SolidAddCustomServiceMethodMcpHandler,
|
|
650
|
+
SolidAddHeaderButtonOrRowButtonToListViewMcpHandler,
|
|
651
|
+
SolidAddControllerHandlerMcpHandler,
|
|
648
652
|
SolidTsMorphService,
|
|
649
653
|
|
|
650
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
|
-
}
|