From 8a608a09ddfefb184b629fa06ccbea4eaf84479d Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Thu, 14 May 2026 10:25:31 +0530 Subject: [PATCH 001/136] 0.1.10-beta.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index a2a0e669..564dae38 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@solidxai/core", - "version": "0.1.9", + "version": "0.1.10-beta.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@solidxai/core", - "version": "0.1.9", + "version": "0.1.10-beta.0", "license": "ISC", "dependencies": { "@aws-sdk/client-s3": "^3.637.0", diff --git a/package.json b/package.json index e61ecb1e..1eeb5bc2 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@solidxai/core", - "version": "0.1.9", + "version": "0.1.10-beta.0", "description": "This module is a NestJS module containing all the required core providers required by a Solid application", "main": "dist/index.js", "types": "dist/index.d.ts", From 040dfc6c83d7ba0c1dc158f83a6a354142915475 Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Thu, 14 May 2026 19:30:40 +0530 Subject: [PATCH 002/136] formatting changes --- src/controllers/user.controller.ts | 32 +++++++++++++++--------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/controllers/user.controller.ts b/src/controllers/user.controller.ts index 412b0a6e..96f5fa56 100755 --- a/src/controllers/user.controller.ts +++ b/src/controllers/user.controller.ts @@ -20,37 +20,37 @@ export class UserController { @ApiBearerAuth("jwt") @Post() @UseInterceptors(AnyFilesInterceptor()) - create(@Body() createDto: CreateUserDto, @UploadedFiles() files: Array,@SolidRequestContextDecorator() solidRequestContext:SolidRequestContextDto) { - return this.service.create(createDto, files,solidRequestContext); + create(@Body() createDto: CreateUserDto, @UploadedFiles() files: Array, @SolidRequestContextDecorator() solidRequestContext: SolidRequestContextDto) { + return this.service.create(createDto, files, solidRequestContext); } @ApiBearerAuth("jwt") @Post('/bulk') @UseInterceptors(AnyFilesInterceptor()) - insertMany(@Body() createDtos: CreateUserDto[], @UploadedFiles() filesArray: Express.Multer.File[][] = [],@SolidRequestContextDecorator() solidRequestContext:SolidRequestContextDto) { - return this.service.insertMany(createDtos, filesArray,solidRequestContext); + insertMany(@Body() createDtos: CreateUserDto[], @UploadedFiles() filesArray: Express.Multer.File[][] = [], @SolidRequestContextDecorator() solidRequestContext: SolidRequestContextDto) { + return this.service.insertMany(createDtos, filesArray, solidRequestContext); } @ApiBearerAuth("jwt") @Put(':id') @UseInterceptors(AnyFilesInterceptor()) - update(@Param('id') id: number, @Body() updateDto: UpdateUserDto, @UploadedFiles() files: Array ,@SolidRequestContextDecorator() solidRequestContext:SolidRequestContextDto) { - return this.service.update(id, updateDto, files,false,solidRequestContext); + update(@Param('id') id: number, @Body() updateDto: UpdateUserDto, @UploadedFiles() files: Array, @SolidRequestContextDecorator() solidRequestContext: SolidRequestContextDto) { + return this.service.update(id, updateDto, files, false, solidRequestContext); } @ApiBearerAuth("jwt") @Patch(':id/update-user-and-roles') - updateUser(@Param('id') id: number,@Body() updateDto: any, @UploadedFiles() files: Array, @SolidRequestContextDecorator() solidRequestContext:SolidRequestContextDto) { - return this.service.updateUser(id, updateDto, files,solidRequestContext); + updateUser(@Param('id') id: number, @Body() updateDto: any, @UploadedFiles() files: Array, @SolidRequestContextDecorator() solidRequestContext: SolidRequestContextDto) { + return this.service.updateUser(id, updateDto, files, solidRequestContext); } @ApiBearerAuth("jwt") @Patch(':id') @UseInterceptors(AnyFilesInterceptor()) - partialUpdate(@Param('id') id: number, @Body() updateDto: UpdateUserDto, @UploadedFiles() files: Array,@SolidRequestContextDecorator() solidRequestContext:SolidRequestContextDto) { + partialUpdate(@Param('id') id: number, @Body() updateDto: UpdateUserDto, @UploadedFiles() files: Array, @SolidRequestContextDecorator() solidRequestContext: SolidRequestContextDto) { return this.service.update(id, updateDto, files, true, solidRequestContext); } @@ -67,8 +67,8 @@ export class UserController { @ApiQuery({ name: 'populateMedia', required: false, type: Array }) @ApiQuery({ name: 'filters', required: false, type: Array }) @Get() - async findMany(@Query() query: any, @SolidRequestContextDecorator() solidRequestContext:SolidRequestContextDto) { - return this.service.find(query,solidRequestContext); + async findMany(@Query() query: any, @SolidRequestContextDecorator() solidRequestContext: SolidRequestContextDto) { + return this.service.find(query, solidRequestContext); } @ApiBearerAuth("jwt") @@ -81,18 +81,18 @@ export class UserController { @ApiBearerAuth("jwt") @Get(':id') - async findOne(@Param('id') id: string, @Query() query: any,@SolidRequestContextDecorator() solidRequestContext:SolidRequestContextDto) { - return this.service.findOne(+id, query,solidRequestContext); + async findOne(@Param('id') id: string, @Query() query: any, @SolidRequestContextDecorator() solidRequestContext: SolidRequestContextDto) { + return this.service.findOne(+id, query, solidRequestContext); } @Delete('/bulk') - async deleteMany(@Body() ids: number[],@SolidRequestContextDecorator() solidRequestContext:SolidRequestContextDto) { - return this.service.deleteMany(ids,solidRequestContext); + async deleteMany(@Body() ids: number[], @SolidRequestContextDecorator() solidRequestContext: SolidRequestContextDto) { + return this.service.deleteMany(ids, solidRequestContext); } @ApiBearerAuth("jwt") @Delete(':id') - async delete(@Param('id') id: number,@SolidRequestContextDecorator() solidRequestContext:SolidRequestContextDto) { + async delete(@Param('id') id: number, @SolidRequestContextDecorator() solidRequestContext: SolidRequestContextDto) { return this.service.delete(id, solidRequestContext); } From 0bd705ab60b9f576514a45fdf5209b715c42a3a6 Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Thu, 14 May 2026 19:31:03 +0530 Subject: [PATCH 003/136] bug fix in rabbitmq subscriber optimisation in all subscribers to default to role both if not specified --- src/services/queues/database-subscriber.service.ts | 12 ++++++------ src/services/queues/rabbitmq-subscriber.service.ts | 10 +++++----- src/services/queues/redis-subscriber.service.ts | 7 +++++-- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/services/queues/database-subscriber.service.ts b/src/services/queues/database-subscriber.service.ts index 31c89340..80ebeee6 100644 --- a/src/services/queues/database-subscriber.service.ts +++ b/src/services/queues/database-subscriber.service.ts @@ -16,9 +16,9 @@ export abstract class DatabaseSubscriber implements OnModuleInit, QueueSubscr protected readonly mqMessageQueueService: MqMessageQueueService, protected readonly poller: PollerService, ) { - this.serviceRole = process.env.QUEUES_SERVICE_ROLE; - if (!this.serviceRole) { - this.logger.debug('Queue service Role is not defined in the environment variables'); + this.serviceRole = process.env.QUEUES_SERVICE_ROLE || 'both'; + if (!process.env.QUEUES_SERVICE_ROLE) { + this.logger.debug('QUEUES_SERVICE_ROLE is not defined. Defaulting DatabaseSubscriber service role to "both".'); } // this.logger.debug(`DatabaseSubscriber instance created with options: ${JSON.stringify(this.options())}`); } @@ -60,7 +60,7 @@ export abstract class DatabaseSubscriber implements OnModuleInit, QueueSubscr await this.processMessage(message); } - catch (error) { + catch (error: any) { this.logger.error(`Error processing message: ${error.message}`); // if an error occurs then if retryCount is set we start retrying. @@ -152,7 +152,7 @@ export abstract class DatabaseSubscriber implements OnModuleInit, QueueSubscr private async retryMessage(message: QueueMessage) { try { await this.processMessage(message); - } catch (error) { + } catch (error: any) { if (message.currentRetry < message.retryCount) { await this.updateStatusInDatabase('retrying', message); @@ -203,7 +203,7 @@ export abstract class DatabaseSubscriber implements OnModuleInit, QueueSubscr this.logger.debug(`Message status updated to ${stage} for messageId: ${mqMessage.id}`); } } - catch (error) { + catch (error: any) { this.logger.error(error.message, error.stack); } } diff --git a/src/services/queues/rabbitmq-subscriber.service.ts b/src/services/queues/rabbitmq-subscriber.service.ts index 69998cbf..7e6c6eda 100755 --- a/src/services/queues/rabbitmq-subscriber.service.ts +++ b/src/services/queues/rabbitmq-subscriber.service.ts @@ -31,12 +31,12 @@ export abstract class RabbitMqSubscriber implements OnModuleInit, QueueSubscr constructor(protected readonly mqMessageService: MqMessageService, protected readonly mqMessageQueueService: MqMessageQueueService) { this.url = process.env.QUEUES_RABBIT_MQ_URL; - this.serviceRole = process.env.QUEUES_SERVICE_ROLE; + this.serviceRole = process.env.QUEUES_SERVICE_ROLE || 'both'; if (!this.url) { this.logger.debug('RabbitMqPublisher url is not defined in the environment variables'); } - if (!this.serviceRole) { - this.logger.debug('Queue service Role is not defined in the environment variables'); + if (!process.env.QUEUES_SERVICE_ROLE) { + this.logger.debug('QUEUES_SERVICE_ROLE is not defined. Defaulting RabbitMqSubscriber service role to "both".'); } // this.logger.debug(`RabbitMqSubscriber instance created with options: ${JSON.stringify(this.options())} and url: ${this.url}`); } @@ -85,7 +85,7 @@ export abstract class RabbitMqSubscriber implements OnModuleInit, QueueSubscr async onModuleInit(): Promise { // Not using SettingService here as that will necessitate all implementors of RabbitMqSubscriber to also inject SettingService which is not ideal. // Instead we directly read the environment variables here. - const defaultBroker = process.env.QUEUES_DEFAULT_BROKER || 'rabbitmq'; + const defaultBroker = process.env.QUEUES_DEFAULT_BROKER || 'database'; const solidCliRunning = process.env.SOLID_CLI_RUNNING || "false"; const queueNameRegex = (process.env.QUEUES_QUEUE_NAME_REGEX_TO_ENABLE || '').trim(); const roleAllowed = ['both', 'subscriber'].includes(this.serviceRole); @@ -407,7 +407,7 @@ export abstract class RabbitMqSubscriber implements OnModuleInit, QueueSubscr await this.mqMessageService.repo.update(mqMessage.id, updatedFields); } } - catch (error) { + catch (error: any) { this.logger.error(error.message, error.stack); } diff --git a/src/services/queues/redis-subscriber.service.ts b/src/services/queues/redis-subscriber.service.ts index d707959e..8c8a0e74 100644 --- a/src/services/queues/redis-subscriber.service.ts +++ b/src/services/queues/redis-subscriber.service.ts @@ -17,7 +17,10 @@ export abstract class RedisSubscriber implements OnModuleInit, OnModuleDestro protected readonly mqMessageService: MqMessageService, protected readonly mqMessageQueueService: MqMessageQueueService, ) { - this.serviceRole = process.env.QUEUES_SERVICE_ROLE; + this.serviceRole = process.env.QUEUES_SERVICE_ROLE || 'both'; + if (!process.env.QUEUES_SERVICE_ROLE) { + this.logger.debug('QUEUES_SERVICE_ROLE is not defined. Defaulting RedisSubscriber service role to "both".'); + } if (!process.env.QUEUES_REDIS_URL) { this.logger.debug('RedisSubscriber: QUEUES_REDIS_URL is not defined in the environment variables'); } @@ -201,7 +204,7 @@ export abstract class RedisSubscriber implements OnModuleInit, OnModuleDestro if (stage === 'failed') updatedFields['error'] = error; await this.mqMessageService.repo.update(mqMessage.id, updatedFields); } - } catch (err) { + } catch (err: any) { this.logger.error(err.message, err.stack); } } From b90b1356a710419c3ed8a0823fcab7c7198fbf18 Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Thu, 14 May 2026 19:31:16 +0530 Subject: [PATCH 004/136] ts errors fixed --- src/services/crud.service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/crud.service.ts b/src/services/crud.service.ts index c0727890..f76fb0a5 100755 --- a/src/services/crud.service.ts +++ b/src/services/crud.service.ts @@ -920,7 +920,7 @@ export class CRUDService { // Add two generic value i.e ); return { message: SUCCESS_MESSAGES.RECORD_RECOVERED, data: softDeletedRows }; - } catch (error) { + } catch (error: any) { if (error instanceof QueryFailedError) { if ((error as any).code === '23505') { throw new Error(ERROR_MESSAGES.CONFLICTING_RECORD_ON_UNARCHIVE); @@ -966,7 +966,7 @@ export class CRUDService { // Add two generic value i.e ); return { message: SUCCESS_MESSAGES.SELECTED_RECORDS_RECOVERED, recoveredIds: ids }; - } catch (error) { + } catch (error: any) { if (error instanceof QueryFailedError) { if ((error as any).code === "23505") { throw new Error(ERROR_MESSAGES.CONFLICTING_RECORD_ON_UNARCHIVE); From 007104f7593a48597b390a923e6d711a8a43ba2e Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Thu, 14 May 2026 19:31:34 +0530 Subject: [PATCH 005/136] added import multer this fixes un-necessary ts errors in vscode --- src/solid-core.module.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/solid-core.module.ts b/src/solid-core.module.ts index 8ba290ae..e7e48abf 100755 --- a/src/solid-core.module.ts +++ b/src/solid-core.module.ts @@ -1,3 +1,4 @@ +import 'multer'; import { Global, MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; import * as express from 'express'; import { ConfigModule, ConfigService } from '@nestjs/config'; From efe898beb9bbbb8042f3980ba2a9edb2f8c19b83 Mon Sep 17 00:00:00 2001 From: Gaurav Dafale Date: Fri, 15 May 2026 10:56:21 +0530 Subject: [PATCH 006/136] fix: username --- src/services/user.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/user.service.ts b/src/services/user.service.ts index d13e96d3..b527ef68 100755 --- a/src/services/user.service.ts +++ b/src/services/user.service.ts @@ -285,7 +285,7 @@ export class UserService extends CRUDService { } async resolveUserOnOauthFacebook(oauthUserDto: OauthUserDto): Promise { - const normalizedEmail = oauthUserDto.email?.trim().toLowerCase() || null; + const normalizedEmail = oauthUserDto.email?.trim().toLowerCase(); let user: User | null = null; if (oauthUserDto.providerId) { From 20fb959355a24253be0140f3fe5cb9830fcc07fd Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Fri, 15 May 2026 14:54:09 +0530 Subject: [PATCH 007/136] changes to post the testing results to a webhook api with a test payload --- src/commands/run-tests.command.ts | 62 ++++++++---- src/testing/reporter/webhook-reporter.ts | 116 +++++++++++++++++++++++ 2 files changed, 161 insertions(+), 17 deletions(-) create mode 100644 src/testing/reporter/webhook-reporter.ts diff --git a/src/commands/run-tests.command.ts b/src/commands/run-tests.command.ts index e2f12356..0849777b 100644 --- a/src/commands/run-tests.command.ts +++ b/src/commands/run-tests.command.ts @@ -3,6 +3,7 @@ import { SubCommand, CommandRunner, Option } from 'nest-commander'; import * as path from 'path'; import { ModuleMetadataHelperService } from 'src/helpers/module-metadata-helper.service'; import { ConsoleReporter } from 'src/testing/reporter/console-reporter'; +import { WebhookReporter } from 'src/testing/reporter/webhook-reporter'; import { runFromMetadata } from 'src/testing/runner/run-from-metadata'; import type { TestingMetadata } from 'src/testing/contracts/testing-metadata.types'; import { SpecRegistry } from 'src/testing/core/spec-registry'; @@ -21,6 +22,7 @@ interface TestRunCommandOptions { retries?: number; listSpecs?: boolean; printApiLogs?: boolean; + runName?: string; } @SubCommand({ @@ -89,23 +91,40 @@ export class TestRunCommand extends CommandRunner { const headless = options?.headless ?? true; const printApiLogs = options?.printApiLogs ?? false; - await runFromMetadata({ - metadata: metadata as TestingMetadata, - scenarioIds, - includeTags, - skipScenarioIds, - reporter: new ConsoleReporter(), - api: apiBaseUrl ? { baseUrl: apiBaseUrl } : undefined, - ui: { baseUrl: uiBaseUrl, headless }, - defaults: { - timeoutMs: options?.timeoutMs, - retries: options?.retries, - }, - options: { printApiLogs }, - specs: specEntries.length - ? (registry) => loadSpecRegistrations(specEntries, metadataPath, registry) - : undefined, - }); + const webhookUrl = process.env.SOLIDCTL_WEBHOOK_URL; + const runName = options?.runName ?? `run-${Date.now()}`; + const reporter = webhookUrl + ? new WebhookReporter(webhookUrl, runName) + : new ConsoleReporter(); + + try { + await runFromMetadata({ + metadata: metadata as TestingMetadata, + scenarioIds, + includeTags, + skipScenarioIds, + reporter, + api: apiBaseUrl ? { baseUrl: apiBaseUrl } : undefined, + ui: { baseUrl: uiBaseUrl, headless }, + defaults: { + timeoutMs: options?.timeoutMs, + retries: options?.retries, + }, + options: { printApiLogs }, + specs: specEntries.length + ? (registry) => loadSpecRegistrations(specEntries, metadataPath, registry) + : undefined, + }); + } catch (err: any) { + this.logger.error('Run tests command failed'); + console.error('Run tests command failed'); + process.exitCode = 1; + } finally { + if (reporter instanceof WebhookReporter) { + await reporter.flush(process.exitCode ?? 0); + } + } + return; } catch (err: any) { this.logger.error('Run tests command failed'); console.error('Run tests command failed'); @@ -223,6 +242,15 @@ export class TestRunCommand extends CommandRunner { parseRetries(val: string): number { return Number(val); } + + @Option({ + flags: '--run-name [name]', + description: 'Logical name for this test run (used by webhook reporter).', + required: false, + }) + parseRunName(val: string): string { + return val; + } } function splitCsv(value?: string): string[] | undefined { diff --git a/src/testing/reporter/webhook-reporter.ts b/src/testing/reporter/webhook-reporter.ts new file mode 100644 index 00000000..1ceed3b5 --- /dev/null +++ b/src/testing/reporter/webhook-reporter.ts @@ -0,0 +1,116 @@ +import { ConsoleReporter } from "./console-reporter"; +import type { OpStep, ScenarioSpec } from "../contracts/testing-metadata.types"; +import type { StepPhase } from "../contracts/runtime-context.types"; + +interface TestStepResult { + phase: string; + operation: string; + name?: string; + ok: boolean; + durationMs: number; + error?: string; +} + +interface TestScenarioResult { + id: string; + name?: string; + ok: boolean; + durationMs: number; + error?: string; + steps: TestStepResult[]; +} + +export interface TestRunPayload { + runName: string; + startedAt: string; + completedAt: string; + durationMs: number; + exitCode: number; + total: number; + passed: number; + failed: number; + scenarios: TestScenarioResult[]; +} + +function formatError(err: unknown): string { + if (!err) return ""; + if (err instanceof Error) return err.stack || err.message; + return String(err); +} + +export class WebhookReporter extends ConsoleReporter { + private readonly startedAt = new Date(); + private readonly accumulated: TestScenarioResult[] = []; + private currentSteps: TestStepResult[] = []; + + constructor( + private readonly webhookUrl: string, + private readonly runName: string, + ) { + super(); + } + + override onStepEnd(args: { + scenarioId: string; + phase: StepPhase; + step: OpStep; + ok: boolean; + error?: unknown; + durationMs: number; + }): void { + super.onStepEnd(args); + this.currentSteps.push({ + phase: String(args.phase), + operation: args.step.op, + name: args.step.name, + ok: args.ok, + durationMs: args.durationMs, + error: args.error ? formatError(args.error) : undefined, + }); + } + + override onScenarioEnd( + scenario: ScenarioSpec, + result: { ok: boolean; error?: unknown; durationMs: number }, + ): void { + super.onScenarioEnd(scenario, result); + this.accumulated.push({ + id: scenario.id, + name: scenario.name, + ok: result.ok, + durationMs: result.durationMs, + error: result.error ? formatError(result.error) : undefined, + steps: [...this.currentSteps], + }); + this.currentSteps = []; + } + + async flush(exitCode: number): Promise { + const completedAt = new Date(); + const payload: TestRunPayload = { + runName: this.runName, + startedAt: this.startedAt.toISOString(), + completedAt: completedAt.toISOString(), + durationMs: completedAt.getTime() - this.startedAt.getTime(), + exitCode, + total: this.accumulated.length, + passed: this.accumulated.filter((s) => s.ok).length, + failed: this.accumulated.filter((s) => !s.ok).length, + scenarios: this.accumulated, + }; + + try { + const response = await fetch(this.webhookUrl, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + signal: AbortSignal.timeout(10_000), + }); + if (!response.ok) { + console.warn(`[WebhookReporter] Webhook returned ${response.status}`); + } + } catch (err) { + console.warn(`[WebhookReporter] Failed to deliver test results: ${err}`); + } + } +} From 2d2c26ea6d1d0928e5158923b5ee549559a53e61 Mon Sep 17 00:00:00 2001 From: Jenendar Date: Fri, 15 May 2026 16:18:47 +0530 Subject: [PATCH 008/136] bug fix for shortext field mapping --- src/services/field-metadata.service.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/services/field-metadata.service.ts b/src/services/field-metadata.service.ts index 6a71dfab..ca5b2ba2 100755 --- a/src/services/field-metadata.service.ts +++ b/src/services/field-metadata.service.ts @@ -745,6 +745,8 @@ export class FieldMetadataService implements OnApplicationBootstrap { "type", "ormType", "isSystem", + "regexPattern", + "regexPatternNotMatchingErrorMsg", "defaultValue", "min", "max", From a46a23349793066719aa190eb6b5f60b3e20506b Mon Sep 17 00:00:00 2001 From: Jenendar Date: Fri, 15 May 2026 16:39:20 +0530 Subject: [PATCH 009/136] removed min and max from long text mapping --- src/services/field-metadata.service.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/services/field-metadata.service.ts b/src/services/field-metadata.service.ts index ca5b2ba2..abc8f619 100755 --- a/src/services/field-metadata.service.ts +++ b/src/services/field-metadata.service.ts @@ -774,8 +774,6 @@ export class FieldMetadataService implements OnApplicationBootstrap { "regexPattern", "regexPatternNotMatchingErrorMsg", "defaultValue", - "min", - "max", "required", "unique", "index", From 109ac915f043b4ec9a64a7fdfaccd4b25d8e0f07 Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Fri, 15 May 2026 16:59:58 +0530 Subject: [PATCH 010/136] 0.1.10-beta.1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 564dae38..f504126f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.0", + "version": "0.1.10-beta.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@solidxai/core", - "version": "0.1.10-beta.0", + "version": "0.1.10-beta.1", "license": "ISC", "dependencies": { "@aws-sdk/client-s3": "^3.637.0", diff --git a/package.json b/package.json index 1eeb5bc2..10c7f228 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.0", + "version": "0.1.10-beta.1", "description": "This module is a NestJS module containing all the required core providers required by a Solid application", "main": "dist/index.js", "types": "dist/index.d.ts", From c64ac47830882b6c7b85bfb9bee5c5ecd064c610 Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Fri, 15 May 2026 17:01:11 +0530 Subject: [PATCH 011/136] type fixes --- src/commands/run-tests.command.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/run-tests.command.ts b/src/commands/run-tests.command.ts index 0849777b..c1f5b270 100644 --- a/src/commands/run-tests.command.ts +++ b/src/commands/run-tests.command.ts @@ -121,7 +121,7 @@ export class TestRunCommand extends CommandRunner { process.exitCode = 1; } finally { if (reporter instanceof WebhookReporter) { - await reporter.flush(process.exitCode ?? 0); + await reporter.flush(Number(process.exitCode ?? 0)); } } return; From 26ceb890adc13714a372d3deace3e4559d6cff2d Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Fri, 15 May 2026 17:01:18 +0530 Subject: [PATCH 012/136] 0.1.10-beta.2 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index f504126f..68706431 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.1", + "version": "0.1.10-beta.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@solidxai/core", - "version": "0.1.10-beta.1", + "version": "0.1.10-beta.2", "license": "ISC", "dependencies": { "@aws-sdk/client-s3": "^3.637.0", diff --git a/package.json b/package.json index 10c7f228..0fb27294 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.1", + "version": "0.1.10-beta.2", "description": "This module is a NestJS module containing all the required core providers required by a Solid application", "main": "dist/index.js", "types": "dist/index.d.ts", From 26d74d1e9d708ac57ae29f4248e67892b190a87c Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Fri, 15 May 2026 17:09:19 +0530 Subject: [PATCH 013/136] license changes --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 68706431..889eb155 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,7 +7,7 @@ "": { "name": "@solidxai/core", "version": "0.1.10-beta.2", - "license": "ISC", + "license": "BUSL-1.1", "dependencies": { "@aws-sdk/client-s3": "^3.637.0", "@aws-sdk/client-textract": "^3.873.0", From f7d3614405321a3b49a274276bcbc4b1b83fa557 Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Fri, 15 May 2026 17:18:59 +0530 Subject: [PATCH 014/136] changes to add a websocket adaptor to the bootstrap server routine --- package-lock.json | 80 +++++++++++++++++++++++++++++++++ package.json | 4 ++ src/helpers/bootstrap.helper.ts | 3 ++ 3 files changed, 87 insertions(+) diff --git a/package-lock.json b/package-lock.json index 889eb155..730bf3ee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -78,10 +78,12 @@ "@nestjs/mongoose": "^10.0.10", "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.0.0", + "@nestjs/platform-ws": "^10.0.0", "@nestjs/serve-static": "^4.0.2", "@nestjs/swagger": "^7.2.0", "@nestjs/testing": "^10.0.0", "@nestjs/typeorm": "^10.0.1", + "@nestjs/websockets": "^10.0.0", "@solidxai/code-builder": "^0.0.2", "@types/express": "^4.17.17", "@types/hapi__joi": "^17.1.12", @@ -135,9 +137,11 @@ "@nestjs/mongoose": "^10.0.10", "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.0.0", + "@nestjs/platform-ws": "^10.0.0", "@nestjs/serve-static": "^4.0.2", "@nestjs/swagger": "^7.2.0", "@nestjs/typeorm": "^10.0.1", + "@nestjs/websockets": "^10.0.0", "nest-commander": "^3.12.5", "nest-winston": "^1.9.7", "nestjs-cls": "^5.4.3", @@ -3879,6 +3883,48 @@ "@nestjs/core": "^10.0.0" } }, + "node_modules/@nestjs/platform-ws": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/@nestjs/platform-ws/-/platform-ws-10.4.22.tgz", + "integrity": "sha512-ZBL66p8axCyvQw6lP6R5uMAamVGfDb0/LtbdxDjMjbWb5/wi070P0MWrjzTudEA3ThsDMNOsfawZlsFUkSfCzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "2.8.1", + "ws": "8.18.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nest" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/websockets": "^10.0.0", + "rxjs": "^7.1.0" + } + }, + "node_modules/@nestjs/platform-ws/node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/@nestjs/schedule": { "version": "6.1.3", "resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-6.1.3.tgz", @@ -4194,6 +4240,30 @@ "typeorm": "^0.3.0" } }, + "node_modules/@nestjs/websockets": { + "version": "10.4.22", + "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.4.22.tgz", + "integrity": "sha512-OLd4i0Faq7vgdtB5vVUrJ54hWEtcXy9poJ6n7kbbh/5ms+KffUl+wwGsbe7uSXLrkoyI8xXU6fZPkFArI+XiRg==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "iterare": "1.2.1", + "object-hash": "3.0.0", + "tslib": "2.8.1" + }, + "peerDependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0", + "@nestjs/platform-socket.io": "^10.0.0", + "reflect-metadata": "^0.1.12 || ^0.2.0", + "rxjs": "^7.1.0" + }, + "peerDependenciesMeta": { + "@nestjs/platform-socket.io": { + "optional": true + } + } + }, "node_modules/@nicolo-ribaudo/chokidar-2": { "version": "2.1.8-no-fsevents.3", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz", @@ -13697,6 +13767,16 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", diff --git a/package.json b/package.json index 0fb27294..5bb88631 100755 --- a/package.json +++ b/package.json @@ -98,9 +98,11 @@ "@nestjs/mongoose": "^10.0.10", "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.0.0", + "@nestjs/platform-ws": "^10.0.0", "@nestjs/serve-static": "^4.0.2", "@nestjs/swagger": "^7.2.0", "@nestjs/typeorm": "^10.0.1", + "@nestjs/websockets": "^10.0.0", "nest-commander": "^3.12.5", "nest-winston": "^1.9.7", "nestjs-cls": "^5.4.3", @@ -125,10 +127,12 @@ "@nestjs/mongoose": "^10.0.10", "@nestjs/passport": "^10.0.3", "@nestjs/platform-express": "^10.0.0", + "@nestjs/platform-ws": "^10.0.0", "@nestjs/serve-static": "^4.0.2", "@nestjs/swagger": "^7.2.0", "@nestjs/testing": "^10.0.0", "@nestjs/typeorm": "^10.0.1", + "@nestjs/websockets": "^10.0.0", "@solidxai/code-builder": "^0.0.2", "@types/express": "^4.17.17", "@types/hapi__joi": "^17.1.12", diff --git a/src/helpers/bootstrap.helper.ts b/src/helpers/bootstrap.helper.ts index bca20eb2..f8f687ec 100644 --- a/src/helpers/bootstrap.helper.ts +++ b/src/helpers/bootstrap.helper.ts @@ -1,5 +1,6 @@ import { ValidationPipe } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; +import { WsAdapter } from '@nestjs/platform-ws'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { NextFunction, Request, Response } from 'express'; import helmet from 'helmet'; @@ -178,6 +179,8 @@ export async function bootstrapSolidApp( const types = require('pg').types; types.setTypeParser(types.builtins.INT8, (val: string) => parseInt(val)); + app.useWebSocketAdapter(new WsAdapter(app)); + await app.listen(port); } From 8576c6c0517a92472c8d9f6a529f4af790eee2a8 Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Fri, 15 May 2026 17:24:05 +0530 Subject: [PATCH 015/136] 0.1.10-beta.3 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 730bf3ee..eaa6c5d7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.2", + "version": "0.1.10-beta.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@solidxai/core", - "version": "0.1.10-beta.2", + "version": "0.1.10-beta.3", "license": "BUSL-1.1", "dependencies": { "@aws-sdk/client-s3": "^3.637.0", diff --git a/package.json b/package.json index 5bb88631..9c370620 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.2", + "version": "0.1.10-beta.3", "description": "This module is a NestJS module containing all the required core providers required by a Solid application", "main": "dist/index.js", "types": "dist/index.d.ts", From f0a50771bcc36f300d8ca5d7869374517a58852b Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Sat, 16 May 2026 07:22:40 +0530 Subject: [PATCH 016/136] queue related bug fixes --- src/services/queues/database-publisher.service.ts | 8 ++++---- src/services/queues/rabbitmq-publisher.service.ts | 8 ++++---- src/services/queues/redis-publisher.service.ts | 9 ++++++--- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/services/queues/database-publisher.service.ts b/src/services/queues/database-publisher.service.ts index 2fa441e7..3dd24c86 100644 --- a/src/services/queues/database-publisher.service.ts +++ b/src/services/queues/database-publisher.service.ts @@ -15,9 +15,9 @@ export abstract class DatabasePublisher implements QueuePublisher { protected readonly mqMessageService: MqMessageService, protected readonly mqMessageQueueService: MqMessageQueueService, ) { - this.serviceRole = process.env.QUEUES_SERVICE_ROLE; - if (!this.serviceRole) { - this.logger.debug('Queue service Role is not defined in the environment variables'); + this.serviceRole = process.env.QUEUES_SERVICE_ROLE || 'both'; + if (!process.env.QUEUES_SERVICE_ROLE) { + this.logger.debug('QUEUES_SERVICE_ROLE is not defined. Defaulting DatabasePublisher service role to "both".'); } // this.logger.debug(`DatabasePublisher instance created with options: ${JSON.stringify(this.options())}`); } @@ -74,7 +74,7 @@ export abstract class DatabasePublisher implements QueuePublisher { mqMessageQueueId: mqMessageQueue.id, }); } - catch (error) { + catch (error: any) { this.logger.error(error.message, error.stack); } diff --git a/src/services/queues/rabbitmq-publisher.service.ts b/src/services/queues/rabbitmq-publisher.service.ts index af98c245..64d397b0 100755 --- a/src/services/queues/rabbitmq-publisher.service.ts +++ b/src/services/queues/rabbitmq-publisher.service.ts @@ -22,12 +22,12 @@ export abstract class RabbitMqPublisher implements OnModuleDestroy, QueuePubl protected readonly mqMessageQueueService: MqMessageQueueService, ) { this.url = process.env.QUEUES_RABBIT_MQ_URL; - this.serviceRole = process.env.QUEUES_SERVICE_ROLE; + this.serviceRole = process.env.QUEUES_SERVICE_ROLE || 'both'; if (!this.url) { this.logger.debug('RabbitMqPublisher url is not defined in the environment variables'); } - if (!this.serviceRole) { - this.logger.debug('Queue service Role is not defined in the environment variables'); + if (!process.env.QUEUES_SERVICE_ROLE) { + this.logger.debug('QUEUES_SERVICE_ROLE is not defined. Defaulting RabbitMqPublisher service role to "both".'); } // this.logger.debug(`RabbitMqPublisher instance created with options: ${JSON.stringify(this.options())} and url: ${this.url}`); } @@ -224,7 +224,7 @@ export abstract class RabbitMqPublisher implements OnModuleDestroy, QueuePubl mqMessageQueueId: mqMessageQueue.id, }); } - catch (error) { + catch (error: any) { this.logger.error(error.message, error.stack); } diff --git a/src/services/queues/redis-publisher.service.ts b/src/services/queues/redis-publisher.service.ts index 10dd7469..de869108 100644 --- a/src/services/queues/redis-publisher.service.ts +++ b/src/services/queues/redis-publisher.service.ts @@ -16,7 +16,10 @@ export abstract class RedisPublisher implements OnModuleDestroy, QueuePublish protected readonly mqMessageService: MqMessageService, protected readonly mqMessageQueueService: MqMessageQueueService, ) { - this.serviceRole = process.env.QUEUES_SERVICE_ROLE; + this.serviceRole = process.env.QUEUES_SERVICE_ROLE || 'both'; + if (!process.env.QUEUES_SERVICE_ROLE) { + this.logger.debug('QUEUES_SERVICE_ROLE is not defined. Defaulting RedisPublisher service role to "both".'); + } if (!process.env.QUEUES_REDIS_URL) { this.logger.debug('RedisPublisher: QUEUES_REDIS_URL is not defined in the environment variables'); } @@ -87,8 +90,8 @@ export abstract class RedisPublisher implements OnModuleDestroy, QueuePublish parentEntity: message.parentEntity, mqMessageQueueId: mqMessageQueue.id, }); - } catch (error) { + } catch (error: any) { this.logger.error(error.message, error.stack); } } -} \ No newline at end of file +} From f8639947d918ab5a2196ca8f04cab965b2185e43 Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Sat, 16 May 2026 07:23:49 +0530 Subject: [PATCH 017/136] bug fix in how alternative actions are rendered --- src/services/view-metadata.service.ts | 33 ++++++++++++++++++++------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/src/services/view-metadata.service.ts b/src/services/view-metadata.service.ts index ed171fcb..ad7bfc8b 100755 --- a/src/services/view-metadata.service.ts +++ b/src/services/view-metadata.service.ts @@ -134,6 +134,7 @@ export class ViewMetadataService extends CRUDService { let viewModes = []; const menuItemModelId = menuItem?.action?.model?.id; const menuItemModuleId = menuItem?.module?.id; + const collectionViewTypes = ['card', 'list', 'kanban', 'tree']; if (menuItemModelId && menuItemModuleId) { const actionQb = await this.actionMetadataService.repo.createSecurityRuleAwareQueryBuilder('action'); const actionsForViewModes = await actionQb @@ -142,16 +143,32 @@ export class ViewMetadataService extends CRUDService { .leftJoinAndSelect('action.view', 'view') .where('model.id = :modelId', { modelId: menuItemModelId }) .andWhere('module.id = :moduleId', { moduleId: menuItemModuleId }) - .andWhere('view.type IN (:...viewTypes)', { viewTypes: ['card', 'list', 'kanban', 'tree'] }) + .andWhere('view.type IN (:...viewTypes)', { viewTypes: collectionViewTypes }) .getMany(); - viewModes = actionsForViewModes.map(actionItem => ({ - type: actionItem.view?.type ?? '', - menuItemId: menuItem.id, - menuItemName: menuItem.displayName, - actionId: actionItem.id ?? '', - actionName: actionItem.displayName ?? '', - })); + const canonicalActionsByViewType = new Map(); + for (const actionItem of actionsForViewModes) { + const resolvedViewType = actionItem.view?.type; + if (!resolvedViewType || canonicalActionsByViewType.has(resolvedViewType)) { + continue; + } + canonicalActionsByViewType.set(resolvedViewType, actionItem); + } + + if (action?.view?.type && collectionViewTypes.includes(action.view.type) && action?.id) { + canonicalActionsByViewType.set(action.view.type, action); + } + + viewModes = collectionViewTypes + .map((resolvedViewType) => canonicalActionsByViewType.get(resolvedViewType)) + .filter(Boolean) + .map(actionItem => ({ + type: actionItem.view?.type ?? '', + menuItemId: menuItem.id, + menuItemName: menuItem.displayName, + actionId: actionItem.id ?? '', + actionName: actionItem.displayName ?? '', + })); } const viewId = action?.view?.id From 2f08fd877d0164bf1355d5eda81e81acf3a786a2 Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Sat, 16 May 2026 07:45:36 +0530 Subject: [PATCH 018/136] enum for passwordless modes --- .../settings/default-settings-provider.service.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/services/settings/default-settings-provider.service.ts b/src/services/settings/default-settings-provider.service.ts index cf8bcdd9..4608f77d 100644 --- a/src/services/settings/default-settings-provider.service.ts +++ b/src/services/settings/default-settings-provider.service.ts @@ -724,7 +724,11 @@ const getSolidCoreSettings = (isProd: boolean) => label: "Registration Validation Type", group: "authentication-settings", sortOrder: 30, - controlType: "shortText", + controlType: "selectionStatic", + options: [ + { label: "Email", value: "email" }, + { label: "Mobile", value: "mobile" }, + ], }, { moduleName: "solid-core", @@ -734,7 +738,12 @@ const getSolidCoreSettings = (isProd: boolean) => label: "Login Validation Type", group: "authentication-settings", sortOrder: 40, - controlType: "shortText", + controlType: "selectionStatic", + options: [ + { label: "Email", value: "email" }, + { label: "Mobile", value: "mobile" }, + { label: "Selectable", value: "selectable" }, + ], }, { moduleName: "solid-core", From fa556622fcb758448fef13818c6d6b98ba82deea Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Sat, 16 May 2026 11:08:29 +0530 Subject: [PATCH 019/136] normalised @ApiTags --- src/controllers/action-metadata.controller.ts | 2 +- src/controllers/facebook-authentication.controller.ts | 2 +- src/controllers/google-authentication.controller.ts | 2 +- src/controllers/menu-item-metadata.controller.ts | 2 +- src/controllers/microsoft-authentication.controller.ts | 2 +- src/controllers/mq-message-queue.controller.ts | 2 +- src/controllers/mq-message.controller.ts | 2 +- src/controllers/view-metadata.controller.ts | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/controllers/action-metadata.controller.ts b/src/controllers/action-metadata.controller.ts index 719c4c2a..b163420d 100755 --- a/src/controllers/action-metadata.controller.ts +++ b/src/controllers/action-metadata.controller.ts @@ -7,7 +7,7 @@ import { UpdateActionMetadataDto } from '../dtos/update-action-metadata.dto'; import { SolidRequestContextDto } from 'src/dtos/solid-request-context.dto'; import { SolidRequestContextDecorator } from 'src/decorators/solid-request-context.decorator'; -@ApiTags('App Builder') +@ApiTags('Solid Core') @Controller('action-metadata') //FIXME: Change this to the model plural name export class ActionMetadataController { constructor(private readonly service: ActionMetadataService) { } diff --git a/src/controllers/facebook-authentication.controller.ts b/src/controllers/facebook-authentication.controller.ts index 912616b1..a2acb886 100644 --- a/src/controllers/facebook-authentication.controller.ts +++ b/src/controllers/facebook-authentication.controller.ts @@ -24,7 +24,7 @@ import { UserService } from "../services/user.service"; import type { SolidCoreSetting } from "../services/settings/default-settings-provider.service"; @Auth(AuthType.None) -@ApiTags("Iam") +@ApiTags("Solid Core") @Controller("iam/facebook") export class FacebookAuthenticationController { constructor( diff --git a/src/controllers/google-authentication.controller.ts b/src/controllers/google-authentication.controller.ts index 5deee164..dd63d205 100755 --- a/src/controllers/google-authentication.controller.ts +++ b/src/controllers/google-authentication.controller.ts @@ -16,7 +16,7 @@ import type { SolidCoreSetting } from "src/services/settings/default-settings-pr @Auth(AuthType.None) @Controller('iam/google') -@ApiTags("Iam") +@ApiTags("Solid Core") // @UseGuards(ThrottlerGuard) // @SkipThrottle({ login: false, short: false, burst: true, sustained: true }) //Enable the login throttle only export class GoogleAuthenticationController { diff --git a/src/controllers/menu-item-metadata.controller.ts b/src/controllers/menu-item-metadata.controller.ts index d38a88aa..25fac2f6 100755 --- a/src/controllers/menu-item-metadata.controller.ts +++ b/src/controllers/menu-item-metadata.controller.ts @@ -9,7 +9,7 @@ import { ActiveUserData } from 'src/interfaces/active-user-data.interface'; import { SolidRequestContextDto } from 'src/dtos/solid-request-context.dto'; import { SolidRequestContextDecorator } from 'src/decorators/solid-request-context.decorator'; -@ApiTags('App Builder') +@ApiTags('Solid Core') @Controller('menu-item-metadata') //FIXME: Change this to the model plural name export class MenuItemMetadataController { constructor(private readonly service: MenuItemMetadataService) { } diff --git a/src/controllers/microsoft-authentication.controller.ts b/src/controllers/microsoft-authentication.controller.ts index 7e5149ff..f34aab27 100644 --- a/src/controllers/microsoft-authentication.controller.ts +++ b/src/controllers/microsoft-authentication.controller.ts @@ -24,7 +24,7 @@ import { UserService } from "../services/user.service"; import type { SolidCoreSetting } from "../services/settings/default-settings-provider.service"; @Auth(AuthType.None) -@ApiTags("Iam") +@ApiTags("Solid Core") @Controller("iam/microsoft") export class MicrosoftAuthenticationController { constructor( diff --git a/src/controllers/mq-message-queue.controller.ts b/src/controllers/mq-message-queue.controller.ts index e4f29f49..965866c2 100755 --- a/src/controllers/mq-message-queue.controller.ts +++ b/src/controllers/mq-message-queue.controller.ts @@ -7,7 +7,7 @@ import { MqMessageQueueService } from '../services/mq-message-queue.service'; import { SolidRequestContextDto } from 'src/dtos/solid-request-context.dto'; import { SolidRequestContextDecorator } from 'src/decorators/solid-request-context.decorator'; -@ApiTags('Queues') +@ApiTags('Solid Core') @Controller('mq-message-queue') //FIXME: Change this to the model plural name export class MqMessageQueueController { constructor(protected readonly service: MqMessageQueueService) { } diff --git a/src/controllers/mq-message.controller.ts b/src/controllers/mq-message.controller.ts index 15bbd3c7..89ec2919 100755 --- a/src/controllers/mq-message.controller.ts +++ b/src/controllers/mq-message.controller.ts @@ -7,7 +7,7 @@ import { AnyFilesInterceptor } from '@nestjs/platform-express'; import { SolidRequestContextDecorator } from 'src/decorators/solid-request-context.decorator'; import { SolidRequestContextDto } from 'src/dtos/solid-request-context.dto'; -@ApiTags('Queues') +@ApiTags('Solid Core') @Controller('mq-message') //FIXME: Change this to the model plural name export class MqMessageController { constructor(protected readonly service: MqMessageService) { } diff --git a/src/controllers/view-metadata.controller.ts b/src/controllers/view-metadata.controller.ts index 9dba068c..9eda28fb 100755 --- a/src/controllers/view-metadata.controller.ts +++ b/src/controllers/view-metadata.controller.ts @@ -9,7 +9,7 @@ import { SolidRequestContextDto } from 'src/dtos/solid-request-context.dto'; import { ActiveUser } from 'src/decorators/active-user.decorator'; import { ActiveUserData } from 'src/interfaces/active-user-data.interface'; -@ApiTags('App Builder') +@ApiTags('Solid Core') @Controller('view-metadata') //FIXME: Change this to the model plural name export class ViewMetadataController { constructor(private readonly service: ViewMetadataService) { } From 72393f2955eab22ed8f963397cb1b12ea2f1a2db Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Sat, 16 May 2026 11:09:53 +0530 Subject: [PATCH 020/136] added typing to catch blocks + authentication service bug fixes for private registration flow --- .../BigIntFieldCrudManager.ts | 2 +- ...-mcp-client-subscriber-database.service.ts | 2 +- src/seeders/module-metadata-seeder.service.ts | 4 +- src/services/authentication.service.ts | 44 +++++++------------ .../queues/rabbitmq-publisher.service.ts | 6 +-- .../queues/rabbitmq-subscriber.service.ts | 8 ++-- .../queues/redis-publisher.service.ts | 2 +- .../queues/redis-subscriber.service.ts | 2 +- .../scheduled-jobs/scheduler.service.ts | 2 +- src/services/solid-introspect.service.ts | 4 +- src/services/user-activity-history.service.ts | 2 +- 11 files changed, 33 insertions(+), 45 deletions(-) diff --git a/src/helpers/field-crud-managers/BigIntFieldCrudManager.ts b/src/helpers/field-crud-managers/BigIntFieldCrudManager.ts index 24eb1fed..77b78d8f 100755 --- a/src/helpers/field-crud-managers/BigIntFieldCrudManager.ts +++ b/src/helpers/field-crud-managers/BigIntFieldCrudManager.ts @@ -20,7 +20,7 @@ export class BigIntFieldCrudManager implements FieldCrudManager { if (typeof fieldValue === 'string' || typeof fieldValue === 'number') { fieldValue = BigInt(fieldValue); } - } catch (err) { + } catch (err: any) { return [{ field: this.options.fieldName, error: 'Invalid numeric value' }]; } } diff --git a/src/jobs/database/trigger-mcp-client-subscriber-database.service.ts b/src/jobs/database/trigger-mcp-client-subscriber-database.service.ts index f3d71ef0..0e54ef8a 100644 --- a/src/jobs/database/trigger-mcp-client-subscriber-database.service.ts +++ b/src/jobs/database/trigger-mcp-client-subscriber-database.service.ts @@ -60,7 +60,7 @@ export class TriggerMcpClientSubscriberDatabase extends DatabaseSubscriber( - signUpDto: SignUpDto, - entity: T, - repo: Repository, - ): Promise { + private async performSignUp(signUpDto: SignUpDto, entity: T, repo: Repository): Promise { try { - const onForcePasswordChange = - this.settingService.getConfigValue( - "forceChangePasswordOnFirstLogin", - ); - const activateUserOnRegistration = - this.settingService.getConfigValue( - "activateUserOnRegistration", - ); - const defaultRole = - this.settingService.getConfigValue("defaultRole"); + const onForcePasswordChange = this.settingService.getConfigValue("forceChangePasswordOnFirstLogin"); + const activateUserOnRegistration = this.settingService.getConfigValue("activateUserOnRegistration"); + const defaultRole = this.settingService.getConfigValue("defaultRole"); var { user, pwd, autoGeneratedPwd } = await this.populateForSignup( entity, @@ -220,17 +211,14 @@ export class AuthenticationService { } const savedUser = await repo.save(user); const userRoles = signUpDto.roles ?? []; - if ( - (signUpDto.roles?.length ?? 0) === 0 && - signUpDto.username !== "sa" && - defaultRole - ) { + if ((signUpDto.roles?.length ?? 0) === 0 && signUpDto.username !== "sa" && defaultRole) { userRoles.push(defaultRole); } await this.handlePostSignup(savedUser, userRoles, pwd, autoGeneratedPwd); return savedUser; - } catch (err) { + } + catch (err: any) { const pgUniqueViolationErrorCode = "23505"; if (err.code === pgUniqueViolationErrorCode) { throw new ConflictException( @@ -485,7 +473,7 @@ export class AuthenticationService { validationSource, ); await this.notifyUserOnOtpInitiateRegistration(user, validationSource); - } catch (err) { + } catch (err: any) { if (err.code === "23505") { throw new ConflictException(ERROR_MESSAGES.USER_ALREADY_EXISTS); } @@ -1306,7 +1294,7 @@ export class AuthenticationService { // Assuming all users do not have mobile as mandatory. if ( forgotPasswordSendVerificationTokenOn == - ForgotPasswordSendVerificationTokenOn.MOBILE && + ForgotPasswordSendVerificationTokenOn.MOBILE && user.mobile ) { const smsService = this.smsFactory.getSmsService(); @@ -1336,11 +1324,11 @@ export class AuthenticationService { confirmForgotPasswordDto.verificationToken, ); if (!user) - throw new UnauthorizedException(ERROR_MESSAGES.INVALID_CREDENTIALS); + throw new UnauthorizedException("Invalid verification token"); if (user.lastLoginProvider !== "local") throw new UnauthorizedException(ERROR_MESSAGES.INVALID_CREDENTIALS); if (!user.active) - throw new UnauthorizedException(ERROR_MESSAGES.INVALID_CREDENTIALS); + throw new UnauthorizedException("User is inactive"); // 1) Atomically consume the token (only one request can succeed) const { affected } = await m @@ -1432,7 +1420,7 @@ export class AuthenticationService { // Assuming all users do not have mobile as mandatory. if ( forgotPasswordSendVerificationTokenOn == - ForgotPasswordSendVerificationTokenOn.MOBILE && + ForgotPasswordSendVerificationTokenOn.MOBILE && user.mobile ) { const smsService = this.smsFactory.getSmsService(); @@ -1548,7 +1536,7 @@ export class AuthenticationService { accessToken: await this.generateAccessToken(user), refreshToken: currentRefreshToken, }; - } catch (err) { + } catch (err: any) { if (err instanceof InvalidatedRefreshTokenError) { // Take action: notify user that his refresh token might have been stolen? throw new UnauthorizedException(ERROR_MESSAGES.ACCESS_DENIED); @@ -1880,7 +1868,7 @@ export class AuthenticationService { await this.userActivityHistoryService.logEvent("logout", user); return { message: SUCCESS_MESSAGES.LOGOUT_SUCCESS }; - } catch (err) { + } catch (err: any) { throw err instanceof UnauthorizedException || err instanceof InternalServerErrorException ? err diff --git a/src/services/queues/rabbitmq-publisher.service.ts b/src/services/queues/rabbitmq-publisher.service.ts index 64d397b0..061a4be0 100755 --- a/src/services/queues/rabbitmq-publisher.service.ts +++ b/src/services/queues/rabbitmq-publisher.service.ts @@ -118,7 +118,7 @@ export abstract class RabbitMqPublisher implements OnModuleDestroy, QueuePubl if (this.channel) { try { await this.channel.close(); - } catch (err) { + } catch (err: any) { this.logger.warn( `RabbitMqPublisher error closing channel: ${(err as Error).message}`, ); @@ -130,7 +130,7 @@ export abstract class RabbitMqPublisher implements OnModuleDestroy, QueuePubl if (this.connection) { try { await this.connection.close(); - } catch (err) { + } catch (err: any) { this.logger.warn( `RabbitMqPublisher error closing connection: ${(err as Error).message}`, ); @@ -189,7 +189,7 @@ export abstract class RabbitMqPublisher implements OnModuleDestroy, QueuePubl // } // await channel.waitForConfirms(); // this.logger.debug('RabbitMqPublisher Message published successfully'); - } catch (err) { + } catch (err: any) { this.logger.error(`RabbitMqPublisher Message publish failed: ${JSON.stringify(err)}`); if (err instanceof Error) { this.logger.error(`RabbitMqPublisher Error stack: ${err.stack}`); diff --git a/src/services/queues/rabbitmq-subscriber.service.ts b/src/services/queues/rabbitmq-subscriber.service.ts index 7e6c6eda..9ffd249b 100755 --- a/src/services/queues/rabbitmq-subscriber.service.ts +++ b/src/services/queues/rabbitmq-subscriber.service.ts @@ -116,7 +116,7 @@ export abstract class RabbitMqSubscriber implements OnModuleInit, QueueSubscr const namespacedQueueName = buildNamespacedQueueName(queueName); try { await this.connectAndConsume(namespacedQueueName); - } catch (err) { + } catch (err: any) { this.logger.error(`Failed to connect to RabbitMQ for queue ${namespacedQueueName}: ${(err as Error).message}`, (err as Error).stack); this.triggerReconnect(namespacedQueueName, 'initial connection failure'); } @@ -142,7 +142,7 @@ export abstract class RabbitMqSubscriber implements OnModuleInit, QueueSubscr let connection: amqp.Connection; try { connection = await this.establishConnection(); - } catch (err) { + } catch (err: any) { this.logger.error(`Failed to connect to RabbitMQ for queue ${queueName}: ${(err as Error).message}`, (err as Error).stack); throw err; } @@ -277,7 +277,7 @@ export abstract class RabbitMqSubscriber implements OnModuleInit, QueueSubscr channel.sendToQueue(failedQueue, body, errorMessage ? { headers: { 'x-error': errorMessage } } : undefined); - } catch (err) { + } catch (err: any) { this.logger.error(`Failed to publish to failed queue ${failedQueue}: ${(err as Error).message}`); } } @@ -302,7 +302,7 @@ export abstract class RabbitMqSubscriber implements OnModuleInit, QueueSubscr this.reconnectAttempt = 0; this.logger.log(`RabbitMqSubscriber reconnected for queue ${queueName}`); return; - } catch (err) { + } catch (err: any) { this.reconnectAttempt += 1; const delay = this.backoff(); this.logger.warn(`RabbitMqSubscriber reconnect failed for queue ${queueName}; retrying in ${delay}ms`); diff --git a/src/services/queues/redis-publisher.service.ts b/src/services/queues/redis-publisher.service.ts index de869108..a8ea343a 100644 --- a/src/services/queues/redis-publisher.service.ts +++ b/src/services/queues/redis-publisher.service.ts @@ -68,7 +68,7 @@ export abstract class RedisPublisher implements OnModuleDestroy, QueuePublish const client = this.getClient(); await client.publish(namespacedQueueName, JSON.stringify(message)); this.logger.debug(`RedisPublisher published message ${message.messageId} to channel ${namespacedQueueName}`); - } catch (err) { + } catch (err: any) { this.logger.error(`RedisPublisher failed to publish message: ${(err as Error).message}`, (err as Error).stack); } diff --git a/src/services/queues/redis-subscriber.service.ts b/src/services/queues/redis-subscriber.service.ts index 8c8a0e74..b302af56 100644 --- a/src/services/queues/redis-subscriber.service.ts +++ b/src/services/queues/redis-subscriber.service.ts @@ -150,7 +150,7 @@ export abstract class RedisSubscriber implements OnModuleInit, OnModuleDestro try { await this.connectAndSubscribe(channel); this.logger.log(`RedisSubscriber reconnected for channel ${channel}`); - } catch (err) { + } catch (err: any) { this.triggerReconnect(channel, `reconnect failed: ${(err as Error).message}`); } }, delay); diff --git a/src/services/scheduled-jobs/scheduler.service.ts b/src/services/scheduled-jobs/scheduler.service.ts index 37deee16..85002527 100644 --- a/src/services/scheduled-jobs/scheduler.service.ts +++ b/src/services/scheduled-jobs/scheduler.service.ts @@ -101,7 +101,7 @@ export class SchedulerServiceImpl implements ISchedulerService { await this.scheduledJobRepo.save(job); this.logger.log(`[${now.getTime()}]: scheduler service finished running job: ${job.job}`); - } catch (err) { + } catch (err: any) { this.logger.error(`[${now.getTime()}]: scheduler service failed to run job ${job.job}`, err.stack); } finally { this.runningJobs.delete(jobKey); diff --git a/src/services/solid-introspect.service.ts b/src/services/solid-introspect.service.ts index 119f3e12..a58b8b20 100755 --- a/src/services/solid-introspect.service.ts +++ b/src/services/solid-introspect.service.ts @@ -197,7 +197,7 @@ export class SolidIntrospectService implements OnApplicationBootstrap { let ds: DataSource | undefined; try { ds = this.moduleRef.get(token, { strict: false }); - } catch (err) { + } catch (err: any) { this.logger.warn(`DataSource token for "${dsName ?? 'default'}" not found: ${err?.message ?? err}`); } if (!ds) { @@ -209,7 +209,7 @@ export class SolidIntrospectService implements OnApplicationBootstrap { if (!ds.isInitialized) { try { await ds.initialize(); // only if you need to initialize here; in many apps datasources are created earlier - } catch (err) { + } catch (err: any) { this.logger.error(`Failed to initialize DataSource "${dsName}": ${err}`); continue; } diff --git a/src/services/user-activity-history.service.ts b/src/services/user-activity-history.service.ts index 36ae9117..44dcc21c 100644 --- a/src/services/user-activity-history.service.ts +++ b/src/services/user-activity-history.service.ts @@ -38,7 +38,7 @@ export class UserActivityHistoryService extends CRUDService ipAddress: ip, userAgent, }); - } catch (err) { + } catch (err: any) { this._logger.warn(`Failed to log event "${event}" for user ${user?.id}: ${err}`); } } From 5faa3048131b5cafac7ccdc5f3772bbccd3d1d94 Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Sat, 16 May 2026 11:10:04 +0530 Subject: [PATCH 021/136] added typing to catch blocks + authentication service bug fixes for private registration flow --- src/testing/core/testing-engine.ts | 4 ++-- src/testing/reporter/webhook-reporter.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/testing/core/testing-engine.ts b/src/testing/core/testing-engine.ts index f1dc7f1a..a7e4a3cc 100644 --- a/src/testing/core/testing-engine.ts +++ b/src/testing/core/testing-engine.ts @@ -57,7 +57,7 @@ export class TestingEngine { } else { await execute(); } - } catch (err) { + } catch (err: any) { scenarioError = err; } finally { const durationMs = Date.now() - scenarioStart; @@ -108,7 +108,7 @@ export class TestingEngine { // console.log(`Step ${resolvedStep.name} attempting to saveAs ${resolvedStep.saveAs}`, JSON.stringify(result)); ctx.resources.set(resolvedStep.saveAs, result); } - } catch (err) { + } catch (err: any) { stepError = err; throw err; } finally { diff --git a/src/testing/reporter/webhook-reporter.ts b/src/testing/reporter/webhook-reporter.ts index 1ceed3b5..48b879b3 100644 --- a/src/testing/reporter/webhook-reporter.ts +++ b/src/testing/reporter/webhook-reporter.ts @@ -109,7 +109,7 @@ export class WebhookReporter extends ConsoleReporter { if (!response.ok) { console.warn(`[WebhookReporter] Webhook returned ${response.status}`); } - } catch (err) { + } catch (err: any) { console.warn(`[WebhookReporter] Failed to deliver test results: ${err}`); } } From cbe4b792a4edcb1e1f9ad28fd2c0fee03cf01cb5 Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Sat, 16 May 2026 12:34:18 +0530 Subject: [PATCH 022/136] changed model metadata service default form layout to be single column --- src/services/model-metadata.service.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/services/model-metadata.service.ts b/src/services/model-metadata.service.ts index fab35282..2bb9062a 100755 --- a/src/services/model-metadata.service.ts +++ b/src/services/model-metadata.service.ts @@ -826,15 +826,15 @@ export class ModelMetadataService { // Populate the View, Actions and Menus in the config file private populateVAMConfigInFileInternal(formViewLayoutFields: any[], model: ModelMetadata, listViewLayoutFields: { type: string; attrs: { name: string; }; }[], treeViewLayoutFields: { type: string; attrs: { name: string; }; }[], metaData: any) { const column1Fields = []; - const column2Fields = []; + // const column2Fields = []; // Distribute fields between two columns for (let i = 0; i < formViewLayoutFields.length; i++) { - if (i % 2 === 0) { - column1Fields.push(formViewLayoutFields[i]); - } else { - column2Fields.push(formViewLayoutFields[i]); - } + // if (i % 2 === 0) { + column1Fields.push(formViewLayoutFields[i]); + // } else { + // column2Fields.push(formViewLayoutFields[i]); + // } } const actionName = `${model.singularName}-list-action`; const treeViewActionName = `${model.singularName}-tree-action`; @@ -955,11 +955,11 @@ export class ModelMetadataService { attrs: { name: "group-1", label: "", className: "col-12 sm:col-12 md:col-6 lg:col-6" }, children: column1Fields }, - { - type: "column", - attrs: { name: "group-2", label: "", className: "col-12 sm:col-12 md:col-6 lg:col-6" }, - children: column2Fields - } + // { + // type: "column", + // attrs: { name: "group-2", label: "", className: "col-12 sm:col-12 md:col-6 lg:col-6" }, + // children: column2Fields + // } ] }, ] From 496d14d39c0e3886dd4350e84aa8a6d086dff64c Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Sat, 16 May 2026 14:22:17 +0530 Subject: [PATCH 023/136] auto addition of menus happens at the bottom now. list view layout changes for security access rules + enabled delete made welcome email & sms SystemAdminEditable --- src/seeders/seed-data/solid-core-metadata.json | 15 +++++++++++++-- src/services/model-metadata.service.ts | 3 ++- .../settings/default-settings-provider.service.ts | 12 ++++++++++-- 3 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/seeders/seed-data/solid-core-metadata.json b/src/seeders/seed-data/solid-core-metadata.json index dd9fb29b..cb03bcec 100755 --- a/src/seeders/seed-data/solid-core-metadata.json +++ b/src/seeders/seed-data/solid-core-metadata.json @@ -11552,7 +11552,7 @@ "enableGlobalSearch": true, "create": true, "edit": true, - "delete": false + "delete": true }, "children": [ { @@ -11570,7 +11570,18 @@ "name": "name", "label": "Name", "sortable": true, - "filterable": true + "filterable": true, + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "description", + "label": "Description", + "sortable": true, + "filterable": true, + "isSearchable": true } } ] diff --git a/src/services/model-metadata.service.ts b/src/services/model-metadata.service.ts index 2bb9062a..cc8cb82f 100755 --- a/src/services/model-metadata.service.ts +++ b/src/services/model-metadata.service.ts @@ -842,6 +842,7 @@ export class ModelMetadataService { const treeViewName = `${model.singularName}-tree-view`; const formViewName = `${model.singularName}-form-view`; const menuName = `${model.singularName}-menu-item`; + const nextMenuSequenceNumber = (metaData.menus?.length ?? 0) + 1; const action = { displayName: `${model.displayName} List Action`, @@ -874,7 +875,7 @@ export class ModelMetadataService { const menu = { displayName: `${model.displayName}`, name: menuName, - sequenceNumber: 1, + sequenceNumber: nextMenuSequenceNumber, actionUserKey: actionName, moduleUserKey: `${model.module.name}`, parentMenuItemUserKey: "", diff --git a/src/services/settings/default-settings-provider.service.ts b/src/services/settings/default-settings-provider.service.ts index 4608f77d..5a1ac4b1 100644 --- a/src/services/settings/default-settings-provider.service.ts +++ b/src/services/settings/default-settings-provider.service.ts @@ -908,7 +908,11 @@ const getSolidCoreSettings = (isProd: boolean) => ( process.env.IAM_SEND_WELCOME_EMAIL_ON_SIGNUP ?? "false" ).toLowerCase() === "true", - level: SettingLevel.SystemEnv, + level: SettingLevel.SystemAdminEditable, + label: "Send Welcome Email On Signup", + group: "authentication-settings", + sortOrder: 180, + controlType: "boolean", }, { moduleName: "solid-core", @@ -917,7 +921,11 @@ const getSolidCoreSettings = (isProd: boolean) => ( process.env.IAM_SEND_WELCOME_SMS_ON_SIGNUP ?? "false" ).toLowerCase() === "true", - level: SettingLevel.SystemEnv, + level: SettingLevel.SystemAdminEditable, + label: "Send Welcome SMS On Signup", + group: "authentication-settings", + sortOrder: 190, + controlType: "boolean", }, { moduleName: "solid-core", From e3351d47cef0f03e34b446ba5b57a85e7bc4acfd Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Sat, 16 May 2026 22:46:42 +0530 Subject: [PATCH 024/136] ts changes error: any --- .../SelectionDynamicFieldCrudManager.ts | 2 +- src/helpers/module-metadata-helper.service.ts | 2 +- src/repository/security-rule.repository.ts | 2 +- src/seeders/module-metadata-seeder.service.ts | 4 ++-- .../permission-metadata-seeder.service.ts | 2 +- src/services/authentication.service.ts | 6 +++--- src/services/chatter-message.service.ts | 2 +- src/services/crud.service.ts | 2 +- src/services/csv.service.ts | 2 +- src/services/dashboard.service.ts | 2 +- .../database/database-bootstrap.service.ts | 2 +- src/services/excel.service.ts | 2 +- src/services/export-transaction.service.ts | 4 ++-- src/services/field-metadata.service.ts | 2 +- src/services/fixtures.service.ts | 4 ++-- src/services/import-transaction.service.ts | 4 ++-- src/services/list-of-values.service.ts | 2 +- src/services/model-metadata.service.ts | 18 +++++++++--------- src/services/module-metadata.service.ts | 14 +++++++------- .../queues/database-subscriber.service.ts | 2 +- .../queues/rabbitmq-subscriber.service.ts | 8 ++++---- .../queues/redis-subscriber.service.ts | 6 +++--- src/services/role-metadata.service.ts | 2 +- .../scheduled-jobs/scheduler.service.ts | 8 ++++---- src/services/sms/TwilioSMSService.ts | 4 ++-- .../computed-entity-field.subscriber.ts | 2 +- src/subscribers/security-rule.subscriber.ts | 16 ++++++++-------- src/subscribers/view-metadata.subscriber.ts | 2 +- 28 files changed, 64 insertions(+), 64 deletions(-) diff --git a/src/helpers/field-crud-managers/SelectionDynamicFieldCrudManager.ts b/src/helpers/field-crud-managers/SelectionDynamicFieldCrudManager.ts index 91090b19..44bd8a68 100755 --- a/src/helpers/field-crud-managers/SelectionDynamicFieldCrudManager.ts +++ b/src/helpers/field-crud-managers/SelectionDynamicFieldCrudManager.ts @@ -119,7 +119,7 @@ export class SelectionDynamicFieldCrudManager implements FieldCrudManager { } return false; } - catch (error) { + catch (error: any) { // Use the values method as a fallback, if the value method is not implemented const values = await providerInstance.values('', ctxt); const isValid = values.some(v => v.value === fieldValue); diff --git a/src/helpers/module-metadata-helper.service.ts b/src/helpers/module-metadata-helper.service.ts index 46822cab..dffdad22 100644 --- a/src/helpers/module-metadata-helper.service.ts +++ b/src/helpers/module-metadata-helper.service.ts @@ -23,7 +23,7 @@ export class ModuleMetadataHelperService { const fileContent = await fs.readFile(configFilePath, 'utf8'); return JSON.parse(fileContent); } - catch (error) { + catch (error: any) { this.logger.error(`module metadata configuration non existent at: ${configFilePath}`); } diff --git a/src/repository/security-rule.repository.ts b/src/repository/security-rule.repository.ts index e6ae3dfd..6fe45711 100644 --- a/src/repository/security-rule.repository.ts +++ b/src/repository/security-rule.repository.ts @@ -56,7 +56,7 @@ export class SecurityRuleRepository extends SolidBaseRepository { evaluatedRules.push(evaluatedRule); - } catch (error) { + } catch (error: any) { this.logger.error(`Error parsing security rule: ${rule.securityRuleConfig}`, error); this.logger.error(error.stack); throw error; diff --git a/src/seeders/module-metadata-seeder.service.ts b/src/seeders/module-metadata-seeder.service.ts index 92b764ea..d5700452 100755 --- a/src/seeders/module-metadata-seeder.service.ts +++ b/src/seeders/module-metadata-seeder.service.ts @@ -238,7 +238,7 @@ export class ModuleMetadataSeederService { //FIXME: Handle displaying the created users credentials in a better way. // this.logger.log(`Newly created username is: ${usersDetail?.length > 0 ? usersDetail[0]?.username : ''} and password is ${usersDetail?.length > 0 ? usersDetail[0]?.password : ''}`); - } catch (error) { + } catch (error: any) { this.logSeedFailureForCli(error, { moduleName: currentModule, step: currentStep, @@ -463,7 +463,7 @@ export class ModuleMetadataSeederService { await this.createPermissionIfNotExists(permissionName); } - } catch (error) { + } catch (error: any) { this.logger.error(error); } } diff --git a/src/seeders/permission-metadata-seeder.service.ts b/src/seeders/permission-metadata-seeder.service.ts index 2c7529ee..137e6e3f 100755 --- a/src/seeders/permission-metadata-seeder.service.ts +++ b/src/seeders/permission-metadata-seeder.service.ts @@ -52,7 +52,7 @@ export class PermissionMetadataSeederService { } } - } catch (error) { + } catch (error: any) { this.logger.error(error); } } diff --git a/src/services/authentication.service.ts b/src/services/authentication.service.ts index 44f2f9da..4c98bd02 100755 --- a/src/services/authentication.service.ts +++ b/src/services/authentication.service.ts @@ -1587,7 +1587,7 @@ export class AuthenticationService { } else { throw new UnauthorizedException(ERROR_MESSAGES.INVALID_USER_PROFILE); } - } catch (error) { + } catch (error: any) { throw new UnauthorizedException( ERROR_MESSAGES.GOOGLE_OAUTH_PROFILE_FETCH_FAILED, ); @@ -1656,7 +1656,7 @@ export class AuthenticationService { } else { throw new UnauthorizedException(ERROR_MESSAGES.INVALID_USER_PROFILE); } - } catch (error) { + } catch (error: any) { if (error instanceof UnauthorizedException) { throw error; } @@ -1721,7 +1721,7 @@ export class AuthenticationService { } else { throw new UnauthorizedException(ERROR_MESSAGES.INVALID_USER_PROFILE); } - } catch (error) { + } catch (error: any) { throw new UnauthorizedException("Microsoft OAuth profile fetch failed"); } } diff --git a/src/services/chatter-message.service.ts b/src/services/chatter-message.service.ts index 9ba47cd6..487e2998 100644 --- a/src/services/chatter-message.service.ts +++ b/src/services/chatter-message.service.ts @@ -421,7 +421,7 @@ export class ChatterMessageService extends CRUDService { if (value.id) { return value.id.toString(); } - } catch (error) { + } catch (error: any) { console.error('Error fetching related model metadata:', error); return value.id ? value.id.toString() : ''; } diff --git a/src/services/crud.service.ts b/src/services/crud.service.ts index f76fb0a5..d5af26ab 100755 --- a/src/services/crud.service.ts +++ b/src/services/crud.service.ts @@ -129,7 +129,7 @@ export class CRUDService { // Add two generic value i.e await this.saveMedia(model, files, savedEntity); } return savedEntity; - } catch (error) { + } catch (error: any) { if (error instanceof QueryFailedError && error.message.includes('duplicate key value violates unique constraint')) { throw new BadRequestException(ERROR_MESSAGES.DUPLICATE_ENTRY); } diff --git a/src/services/csv.service.ts b/src/services/csv.service.ts index f8795500..930567e5 100644 --- a/src/services/csv.service.ts +++ b/src/services/csv.service.ts @@ -68,7 +68,7 @@ export class CsvService { } csvStream.end(); // ✅ Ensure CSV stream is finalized - } catch (error) { + } catch (error: any) { this.logger.error(`❌ Error writing CSV: ${error.message}`); passThrough.destroy(error); // ✅ Properly destroy stream on error throw error; diff --git a/src/services/dashboard.service.ts b/src/services/dashboard.service.ts index 63a3cd49..0d880401 100644 --- a/src/services/dashboard.service.ts +++ b/src/services/dashboard.service.ts @@ -117,7 +117,7 @@ export class DashboardService extends CRUDService { const filePath = await this.moduleMetadataHelperService.getModuleMetadataFilePath(moduleName); try { await fs.access(filePath); - } catch (error) { + } catch (error: any) { throw new Error(`Configuration file not found for module: ${moduleName}`); } const metaData = await this.moduleMetadataHelperService.getModuleMetadataConfiguration(filePath); diff --git a/src/services/database/database-bootstrap.service.ts b/src/services/database/database-bootstrap.service.ts index 6b5dd3dc..0b55d225 100644 --- a/src/services/database/database-bootstrap.service.ts +++ b/src/services/database/database-bootstrap.service.ts @@ -80,7 +80,7 @@ export class DatabaseBootstrapService implements OnModuleInit { await this.dataSource.query(sql); this.logger.debug(`[${this.dataSource.name}] Applied ${fileName}`); - } catch (error) { + } catch (error: any) { // DO NOT THROW — continue with next file this.logger.error( `[${this.dataSource.name}] Failed ${fileName}`, diff --git a/src/services/excel.service.ts b/src/services/excel.service.ts index 5b66e011..f357418e 100644 --- a/src/services/excel.service.ts +++ b/src/services/excel.service.ts @@ -93,7 +93,7 @@ export class ExcelService { workbook.commit(); // passThrough.end(); // ✅ Properly close the stream - } catch (error) { + } catch (error: any) { this.logger.error(`❌ Error writing Excel: ${error.message}`); passThrough.destroy(error); // Destroy stream throw error; diff --git a/src/services/export-transaction.service.ts b/src/services/export-transaction.service.ts index 64a6834d..2cf498eb 100644 --- a/src/services/export-transaction.service.ts +++ b/src/services/export-transaction.service.ts @@ -90,7 +90,7 @@ export class ExportTransactionService extends CRUDService { const fileName = this.getFileName(templateName, uuid, templateFormat); const mimeType = this.getMimeType(templateFormat); return { exportStream, fileName, mimeType, exportTransaction }; - } catch (error) { + } catch (error: any) { this.updateExportTransaction(id, ExportStatus.FAILED, error.message); throw error; } @@ -114,7 +114,7 @@ export class ExportTransactionService extends CRUDService { // Store the file using the appropriate storage provider await this.storeExportStream(exportStream, exportTransaction, this.getFileName(templateName, uuid, templateFormat)); this.updateExportTransaction(id, ExportStatus.COMPLETED); - } catch (error) { + } catch (error: any) { this.updateExportTransaction(id, ExportStatus.FAILED, error.message); throw error; diff --git a/src/services/field-metadata.service.ts b/src/services/field-metadata.service.ts index abc8f619..668b13c8 100755 --- a/src/services/field-metadata.service.ts +++ b/src/services/field-metadata.service.ts @@ -1256,7 +1256,7 @@ export class FieldMetadataService implements OnApplicationBootstrap { // Write the updated object back to the file const updatedContent = JSON.stringify(metaData, null, 2); await fs.writeFile(filePath, updatedContent); - } catch (error) { + } catch (error: any) { this.logger.error('File creation failed:', error); throw new Error(ERROR_MESSAGES.FILE_WRITE_FAILED); // Trigger rollback } diff --git a/src/services/fixtures.service.ts b/src/services/fixtures.service.ts index 39ae7023..38a98b93 100644 --- a/src/services/fixtures.service.ts +++ b/src/services/fixtures.service.ts @@ -35,7 +35,7 @@ export class FixturesService { // Create the model instance in the database const createdInstance = await modelServiceInstance.create(modelFixture.data); this.logger.log(`Successfully created fixture for model: ${modelFixture.singularName} with ID: ${createdInstance.id}`); - } catch (error) { + } catch (error: any) { this.logger.error(`Error creating fixture for model: ${modelFixture.singularName} - ${error.message}`); } } @@ -59,7 +59,7 @@ export class FixturesService { const deleteCriteria = modelFixture.data; // This should be adjusted based on actual criteria await modelServiceInstance.delete(deleteCriteria); this.logger.log(`Successfully deleted fixture for model: ${modelFixture.singularName}`); - } catch (error) { + } catch (error: any) { this.logger.error(`Error deleting fixture for model: ${modelFixture.singularName} - ${error.message}`); } } diff --git a/src/services/import-transaction.service.ts b/src/services/import-transaction.service.ts index dc2b341b..e110b79b 100644 --- a/src/services/import-transaction.service.ts +++ b/src/services/import-transaction.service.ts @@ -25,7 +25,7 @@ import { SolidIntrospectService } from './solid-introspect.service'; import { ModelMetadataHelperService } from 'src/helpers/model-metadata-helper.service'; import { getUserExcludedFields } from 'src/helpers/user-helper'; import { ActiveUserData } from 'src/interfaces/active-user-data.interface'; -import {upperFirst, camelCase} from 'lodash'; +import { upperFirst, camelCase } from 'lodash'; import { classify } from '../helpers/string.helper'; interface ImportTemplateFileInfo { @@ -557,7 +557,7 @@ export class ImportTransactionService extends CRUDService { const createdRecord = await this.insertRecord(record, JSON.parse(importTransaction.mapping) as ImportMapping[], importTransaction.modelMetadata, modelService); ids.push(createdRecord.id); // Add the ID of the created record to the ids array } - catch (error) { + catch (error: any) { this.logger.debug(`Error inserting record: ${JSON.stringify(record)}. Error: ${error.message}`); // Get the Import transaction error log repo const errorLog = await this.createErrorLogEntry(importTransaction, record, error); diff --git a/src/services/list-of-values.service.ts b/src/services/list-of-values.service.ts index 44c56972..b21eb9f1 100644 --- a/src/services/list-of-values.service.ts +++ b/src/services/list-of-values.service.ts @@ -140,7 +140,7 @@ export class ListOfValuesService extends CRUDService { const filePath = await this.moduleMetadataHelperService.getModuleMetadataFilePath(moduleName); try { await fs.access(filePath); - } catch (error) { + } catch (error: any) { throw new Error(`Configuration file not found for module: ${moduleName}`); } const metaData = await this.moduleMetadataHelperService.getModuleMetadataConfiguration(filePath); diff --git a/src/services/model-metadata.service.ts b/src/services/model-metadata.service.ts index cc8cb82f..5d458377 100755 --- a/src/services/model-metadata.service.ts +++ b/src/services/model-metadata.service.ts @@ -154,7 +154,7 @@ export class ModelMetadataService { return model }); - } catch (error) { + } catch (error: any) { // console.error('Transaction failed:', error); this.logger.error('Transaction failed:', error); throw error; @@ -189,7 +189,7 @@ export class ModelMetadataService { // return model }); - } catch (error) { + } catch (error: any) { // console.error('Transaction failed:', error); this.logger.error('Transaction failed:', error); throw error; @@ -309,7 +309,7 @@ export class ModelMetadataService { const updatedContent = JSON.stringify(metaData, null, 2); await fs.writeFile(filePath, updatedContent); - } catch (error) { + } catch (error: any) { // console.error('File creation failed:', error); this.logger.error('File creation failed:', error); throw new Error(ERROR_MESSAGES.FILE_WRITE_FAILED); // Trigger rollback @@ -490,7 +490,7 @@ export class ModelMetadataService { const updatedContent = JSON.stringify(metaData, null, 2); await fs.writeFile(filePath, updatedContent); - } catch (error) { + } catch (error: any) { // console.error('File creation failed:', error); this.logger.error('File creation failed:', error); throw new Error(ERROR_MESSAGES.FILE_WRITE_FAILED); // Trigger rollback @@ -524,7 +524,7 @@ export class ModelMetadataService { await this.cleanupOnDelete(entity.id); const r = await this.modelMetadataRepo.remove(entity); return r; - } catch (error) { + } catch (error: any) { } } @@ -621,7 +621,7 @@ export class ModelMetadataService { try { await fs.unlink(fileToDelete); this.logger.log(`Deleted file: ${fileToDelete}`); - } catch (error) { + } catch (error: any) { this.logger.error(`Error deleting file: ${fileToDelete}`, error); } } @@ -725,7 +725,7 @@ export class ModelMetadataService { ); this.solidTsMorphService.removeModuleMembers(moduleFilePath, removedIdentifiers); await this.solidTsMorphService.commit(); - } catch (error) { + } catch (error: any) { this.solidTsMorphService.rollback(); this.logger.error(`Failed to clean up module file for model '${modelEntity.singularName}':`, error); } @@ -789,7 +789,7 @@ export class ModelMetadataService { await this.populateVAMConfigInDb(model); await this.populateVAMConfigInFile(model); }); - } catch (error) { + } catch (error: any) { this.logger.error('generateVAMConfig Transaction failed:', error); throw error; } @@ -816,7 +816,7 @@ export class ModelMetadataService { const updatedContent = JSON.stringify(metaData, null, 2); await fs.writeFile(filePath, updatedContent); - } catch (error) { + } catch (error: any) { // console.error('File creation failed:', error); this.logger.error('File updation failed for View, action, menus config:', error); throw new Error('File updation failed for View, action, menus config'); // Trigger rollback diff --git a/src/services/module-metadata.service.ts b/src/services/module-metadata.service.ts index 1b61a048..0e5a8c16 100755 --- a/src/services/module-metadata.service.ts +++ b/src/services/module-metadata.service.ts @@ -123,7 +123,7 @@ export class ModuleMetadataService { await this.createInFile(module); return module }); - } catch (error) { + } catch (error: any) { // console.error('Transaction failed:', error); this.logger.error('Transaction failed:', error); throw error; @@ -181,7 +181,7 @@ export class ModuleMetadataService { actionUserKey: `${module?.name}-home-action`, moduleUserKey: module?.name, parentMenuItemUserKey: "", - iconName : "home" + iconName: "home" } ], views: [], @@ -204,7 +204,7 @@ export class ModuleMetadataService { // Write the JSON to the file await fs.writeFile(filePath, metadataJson); - } catch (error) { + } catch (error: any) { // console.error('File creation failed:', error); this.logger.error('File creation failed:', error); throw new Error(ERROR_MESSAGES.FILE_WRITE_FAILED); // Trigger rollback @@ -219,7 +219,7 @@ export class ModuleMetadataService { await this.updateInFile(module); return module }); - } catch (error) { + } catch (error: any) { // console.error('Transaction failed:', error); this.logger.error('Transaction failed:', error); throw error; @@ -255,7 +255,7 @@ export class ModuleMetadataService { try { metaData = await this.moduleMetadataHelperService.getModuleMetadataConfiguration(filePath); - } catch (error) { + } catch (error: any) { metaData = { moduleMetadata: { name: null, @@ -290,7 +290,7 @@ export class ModuleMetadataService { const updatedContent = JSON.stringify(metaData, null, 2); await fs.writeFile(filePath, updatedContent); - } catch (error) { + } catch (error: any) { // console.error('File creation failed:', error); this.logger.error('File creation failed:', error); throw new Error(ERROR_MESSAGES.FILE_WRITE_FAILED); // Trigger rollback @@ -365,7 +365,7 @@ export class ModuleMetadataService { await fs.rm(modulePath, { recursive: true, force: true }); await fs.rm(moduleMetadataPAth, { recursive: true, force: true }); this.logger.log(`Deleted file: ${moduleMetadataPAth}`); - } catch (error) { + } catch (error: any) { this.logger.error(`Error deleting file: ${moduleMetadataPAth}`, error); throw new Error(ERROR_MESSAGES.FILE_DELETE_FAILED); // Trigger rollback } diff --git a/src/services/queues/database-subscriber.service.ts b/src/services/queues/database-subscriber.service.ts index 80ebeee6..8f07b5fe 100644 --- a/src/services/queues/database-subscriber.service.ts +++ b/src/services/queues/database-subscriber.service.ts @@ -108,7 +108,7 @@ export abstract class DatabaseSubscriber implements OnModuleInit, QueueSubscr this.logger.log(`DatabaseSubscriber for queue ${queueName} is disabled because it does not match QUEUES_QUEUE_NAME_REGEX_TO_ENABLE=${queueNameRegex}`); return; } - } catch (error) { + } catch (error: any) { this.logger.error(`Invalid QUEUES_QUEUE_NAME_REGEX_TO_ENABLE regex "${queueNameRegex}". Subscriber for queue ${queueName} will not start.`); return; } diff --git a/src/services/queues/rabbitmq-subscriber.service.ts b/src/services/queues/rabbitmq-subscriber.service.ts index 9ffd249b..776d8d64 100755 --- a/src/services/queues/rabbitmq-subscriber.service.ts +++ b/src/services/queues/rabbitmq-subscriber.service.ts @@ -107,7 +107,7 @@ export abstract class RabbitMqSubscriber implements OnModuleInit, QueueSubscr this.logger.log(`RabbitMqSubscriber for queue ${queueName} is disabled because it does not match QUEUES_QUEUE_NAME_REGEX_TO_ENABLE=${queueNameRegex}`); return; } - } catch (error) { + } catch (error: any) { this.logger.error(`Invalid QUEUES_QUEUE_NAME_REGEX_TO_ENABLE regex "${queueNameRegex}". Subscriber for queue ${queueName} will not start.`); return; } @@ -210,7 +210,7 @@ export abstract class RabbitMqSubscriber implements OnModuleInit, QueueSubscr const messageContentString = rawMessage.content.toString(); message = JSON.parse(messageContentString) as QueueMessage; this.logger.debug(`rabbitmq subscriber received message with id: ${message.messageId} for queue ${queueName}`); - } catch (error) { + } catch (error: any) { this.logger.error(`Invalid JSON message on queue ${queueName}: ${(error as Error).message}`); await this.publishToFailedQueue(queueName, rawMessage.content, channel, error); channel.ack(rawMessage); @@ -223,7 +223,7 @@ export abstract class RabbitMqSubscriber implements OnModuleInit, QueueSubscr try { await this.processMessage(message, rawMessage, channel, queueName); - } catch (error) { + } catch (error: any) { await this.handleProcessingError(message, rawMessage, channel, error, queueName); } }, @@ -485,7 +485,7 @@ export abstract class RabbitMqSubscriber implements OnModuleInit, QueueSubscr // - If timeoutPromise rejects first, we fail fast with timeout error. // This ensures we mark DB status via normal error handling before broker ack-timeout. return await Promise.race([subscribePromise, timeoutPromise]); - } catch (error) { + } catch (error: any) { const errorMessage = (error as Error)?.message || String(error); this.logger.error( `Subscriber execution failed for queue ${queueName} and messageId ${messageId}: ${errorMessage}`, diff --git a/src/services/queues/redis-subscriber.service.ts b/src/services/queues/redis-subscriber.service.ts index b302af56..7f77bd23 100644 --- a/src/services/queues/redis-subscriber.service.ts +++ b/src/services/queues/redis-subscriber.service.ts @@ -53,7 +53,7 @@ export abstract class RedisSubscriber implements OnModuleInit, OnModuleDestro ); return; } - } catch (error) { + } catch (error: any) { this.logger.error( `Invalid QUEUES_QUEUE_NAME_REGEX_TO_ENABLE regex "${queueNameRegex}". Subscriber for queue ${queueName} will not start.`, ); @@ -95,7 +95,7 @@ export abstract class RedisSubscriber implements OnModuleInit, OnModuleDestro let message: QueueMessage = null; try { message = JSON.parse(rawMessage) as QueueMessage; - } catch (error) { + } catch (error: any) { this.logger.error(`RedisSubscriber invalid JSON on channel ${channel}: ${(error as Error).message}`); return; } @@ -106,7 +106,7 @@ export abstract class RedisSubscriber implements OnModuleInit, OnModuleDestro try { await this.processMessage(message); - } catch (error) { + } catch (error: any) { await this.handleProcessingError(message, error, channel); } }); diff --git a/src/services/role-metadata.service.ts b/src/services/role-metadata.service.ts index d778d0d9..2842587e 100755 --- a/src/services/role-metadata.service.ts +++ b/src/services/role-metadata.service.ts @@ -91,7 +91,7 @@ export class RoleMetadataService extends CRUDService { } */ } - } catch (error) { + } catch (error: any) { this.logger.error(error); } } diff --git a/src/services/scheduled-jobs/scheduler.service.ts b/src/services/scheduled-jobs/scheduler.service.ts index 85002527..1681cca0 100644 --- a/src/services/scheduled-jobs/scheduler.service.ts +++ b/src/services/scheduled-jobs/scheduler.service.ts @@ -36,7 +36,7 @@ export class SchedulerServiceImpl implements ISchedulerService { if (jobsRegexToEnable && jobsRegexToEnable !== "all") { try { jobsRegex = new RegExp(jobsRegexToEnable); - } catch (error) { + } catch (error: any) { this.logger.error(`Invalid SOLID_SCHEDULER_JOBS_REGEX_TO_ENABLE regex "${jobsRegexToEnable}". Scheduler loop will skip this run.`); return; } @@ -153,7 +153,7 @@ export class SchedulerServiceImpl implements ISchedulerService { try { const parsed = JSON.parse(dayOfWeek); return Array.isArray(parsed) ? parsed : []; - } catch (error) { + } catch (error: any) { this.logger.warn(`Invalid dayOfWeek JSON '${dayOfWeek}'`, error as any); return []; } @@ -217,10 +217,10 @@ export class SchedulerServiceImpl implements ISchedulerService { if (runAfterNext.getTime() - nextRun.getTime() < 60000) { throw new Error('Cron expression interval must be at least 1 minute'); } - + this.logger.log(`Custom cron '${job.cronExpression}' next run: ${nextRun}`); return nextRun; - } catch (error) { + } catch (error: any) { this.logger.error(`Invalid cron expression for job ${job.scheduleName}: ${job.cronExpression}. Reason: ${(error as Error).message}`); // Fallback to daily if cron parsing fails return new Date(base.getTime() + 24 * 60 * 60 * 1000); diff --git a/src/services/sms/TwilioSMSService.ts b/src/services/sms/TwilioSMSService.ts index 8d9700ee..5a8703b5 100644 --- a/src/services/sms/TwilioSMSService.ts +++ b/src/services/sms/TwilioSMSService.ts @@ -69,7 +69,7 @@ export class TwilioSMSService implements ISMS { try { const bodyTemplate = Handlebars.compile(smsTemplate.body); body = bodyTemplate(templateParams); - } catch (error) { + } catch (error: any) { throw new Error('Unable to compile sms template body'); } // Finally send the email. @@ -110,7 +110,7 @@ export class TwilioSMSService implements ISMS { } return r; - } catch (error) { + } catch (error: any) { throw new Error(error); } } diff --git a/src/subscribers/computed-entity-field.subscriber.ts b/src/subscribers/computed-entity-field.subscriber.ts index 431c97c3..c4f52028 100644 --- a/src/subscribers/computed-entity-field.subscriber.ts +++ b/src/subscribers/computed-entity-field.subscriber.ts @@ -142,7 +142,7 @@ export class ComputedEntityFieldSubscriber implements EntitySubscriberInterface const providerInstance = provider.instance as IEntityPreComputeFieldProvider; // IEntityComputedFieldProvider const computedValue = await providerInstance.preComputeValue(entity, computedFieldMetadata); //FIXME There should some way to check/assert if the provider actually has a postComputeAndSaveValue return computedValue; //TODO: This line here is just for backward compatibility, once the pre compute interface is change to return void, we will get rid of it. - } catch (error) { + } catch (error: any) { throw new InternalServerErrorException(`Error evaluating computed field ${computedFieldMetadata.fieldName} for model ${computedFieldMetadata.modelName} for triggered entity ${entity.constructor.name}: ${error.message}`); } } diff --git a/src/subscribers/security-rule.subscriber.ts b/src/subscribers/security-rule.subscriber.ts index 88d094e8..a4c3d293 100644 --- a/src/subscribers/security-rule.subscriber.ts +++ b/src/subscribers/security-rule.subscriber.ts @@ -27,19 +27,19 @@ export class SecurityRuleSubscriber implements EntitySubscriberInterface) { await this.saveSecurityRules(event); } - + async afterUpdate(event: UpdateEvent) { await this.saveSecurityRules(event); } - async saveSecurityRules(event: UpdateEvent| InsertEvent) { + async saveSecurityRules(event: UpdateEvent | InsertEvent) { const securityRule = event.entity as SecurityRule; const modelMetadata = event.entity.modelMetadata; if (!modelMetadata) { this.logger.error(`Model metadata not found for security rule with id ${event.entity.id}`); return; } - + const modelMetadataRepo = this.dataSource.getRepository(ModelMetadata); const populatedModelMetadata = await modelMetadataRepo.findOne({ where: { @@ -58,7 +58,7 @@ export class SecurityRuleSubscriber implements EntitySubscriberInterface ruleFromFile.name === securityRule.name); - const {id, roleId, modelMetadataId, ...requiredDto} = await this.securityRuleRepo.toDto(securityRule) - metaData.securityRules[securityRuleIndex] = {...requiredDto, securityRuleConfig: JSON.parse(securityRule.securityRuleConfig)} + const { id, roleId, modelMetadataId, ...requiredDto } = await this.securityRuleRepo.toDto(securityRule) + metaData.securityRules[securityRuleIndex] = { ...requiredDto, securityRuleConfig: JSON.parse(securityRule.securityRuleConfig) } } else { const securityRules = [] - const {id, roleId, modelMetadataId, ...requiredDto} = await this.securityRuleRepo.toDto(securityRule) - securityRules.push({...requiredDto, securityRuleConfig: JSON.parse(securityRule.securityRuleConfig)}) + const { id, roleId, modelMetadataId, ...requiredDto } = await this.securityRuleRepo.toDto(securityRule) + securityRules.push({ ...requiredDto, securityRuleConfig: JSON.parse(securityRule.securityRuleConfig) }) metaData.securityRules = securityRules } // Write the updated object back to the file diff --git a/src/subscribers/view-metadata.subscriber.ts b/src/subscribers/view-metadata.subscriber.ts index e86117bd..fa6de800 100644 --- a/src/subscribers/view-metadata.subscriber.ts +++ b/src/subscribers/view-metadata.subscriber.ts @@ -43,7 +43,7 @@ export class ViewMetadataSubsciber implements EntitySubscriberInterface Date: Sat, 16 May 2026 22:46:47 +0530 Subject: [PATCH 025/136] 0.1.10-alpha.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index eaa6c5d7..f2b9bedc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.3", + "version": "0.1.10-alpha.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@solidxai/core", - "version": "0.1.10-beta.3", + "version": "0.1.10-alpha.0", "license": "BUSL-1.1", "dependencies": { "@aws-sdk/client-s3": "^3.637.0", diff --git a/package.json b/package.json index 9c370620..5c252898 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.3", + "version": "0.1.10-alpha.0", "description": "This module is a NestJS module containing all the required core providers required by a Solid application", "main": "dist/index.js", "types": "dist/index.d.ts", From ff907ae801a26dcf85aee52727428d0c2677665a Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Mon, 18 May 2026 12:15:05 +0530 Subject: [PATCH 026/136] removed un-necessary index on newvalue display --- src/entities/chatter-message-details.entity.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/entities/chatter-message-details.entity.ts b/src/entities/chatter-message-details.entity.ts index 4e8927be..5104ce36 100644 --- a/src/entities/chatter-message-details.entity.ts +++ b/src/entities/chatter-message-details.entity.ts @@ -18,7 +18,6 @@ export class ChatterMessageDetails extends CommonEntity { @Column({ type: "varchar", nullable: true }) oldValueDisplay: string; - @Index() @Column({ type: "varchar", nullable: true }) newValueDisplay: string; @@ -30,4 +29,4 @@ export class ChatterMessageDetails extends CommonEntity { @Column({ type: "varchar", nullable: true }) fieldType: string; -} \ No newline at end of file +} From 513c95398a898851fdf76146b8034aedf87b2e85 Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Mon, 18 May 2026 12:15:13 +0530 Subject: [PATCH 027/136] 0.1.10-alpha.1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index f2b9bedc..23b99ad2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@solidxai/core", - "version": "0.1.10-alpha.0", + "version": "0.1.10-alpha.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@solidxai/core", - "version": "0.1.10-alpha.0", + "version": "0.1.10-alpha.1", "license": "BUSL-1.1", "dependencies": { "@aws-sdk/client-s3": "^3.637.0", diff --git a/package.json b/package.json index 5c252898..8c06f30d 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@solidxai/core", - "version": "0.1.10-alpha.0", + "version": "0.1.10-alpha.1", "description": "This module is a NestJS module containing all the required core providers required by a Solid application", "main": "dist/index.js", "types": "dist/index.d.ts", From a919d45476318ca28f75aef51299b06c7ea43e19 Mon Sep 17 00:00:00 2001 From: Chetan Date: Mon, 18 May 2026 12:57:09 +0530 Subject: [PATCH 028/136] failed login attempts field added to the json and update dto and exposed in entity --- src/dtos/update-user.dto.ts | 4 ++++ src/entities/user.entity.ts | 2 +- .../seed-data/solid-core-metadata.json | 21 +++++++++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/dtos/update-user.dto.ts b/src/dtos/update-user.dto.ts index 04dcc0d7..ed976074 100755 --- a/src/dtos/update-user.dto.ts +++ b/src/dtos/update-user.dto.ts @@ -161,4 +161,8 @@ export class UpdateUserDto { @IsOptional() @ApiProperty() userViewMetadataCommand: string; + @IsOptional() + @IsInt() + @ApiProperty() + failedLoginAttempts: number; } \ No newline at end of file diff --git a/src/entities/user.entity.ts b/src/entities/user.entity.ts index c362f223..67478f1d 100755 --- a/src/entities/user.entity.ts +++ b/src/entities/user.entity.ts @@ -177,7 +177,7 @@ export class User extends CommonEntity { @Column({ nullable: true }) rehashedAt: Date; - // dont send to client + @Expose() @Column({ type: "int", default: 0 }) failedLoginAttempts: number = 0; diff --git a/src/seeders/seed-data/solid-core-metadata.json b/src/seeders/seed-data/solid-core-metadata.json index cb03bcec..56474438 100755 --- a/src/seeders/seed-data/solid-core-metadata.json +++ b/src/seeders/seed-data/solid-core-metadata.json @@ -1761,6 +1761,19 @@ "encrypt": false, "isSystem": true }, + { + "name": "failedLoginAttempts", + "displayName": "Failed Login Attempts", + "type": "int", + "ormType": "integer", + "defaultValue": "0", + "required": false, + "unique": false, + "index": false, + "private": false, + "encrypt": false, + "isSystem": true + }, { "name": "profilePicture", "displayName": "Profile Picture", @@ -9841,6 +9854,14 @@ "name": "active", "isSearchable": true } + }, + { + "type": "field", + "attrs": { + "label" : "Blocked / Unblocked", + "name": "failedLoginAttempts", + "viewWidget": "SolidUserBlockedStatusListWidget" + } } ] } From a4cf809815ddb5458406f39f7db7cebaf5d6039d Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Mon, 18 May 2026 20:24:22 +0530 Subject: [PATCH 029/136] 0.1.10-beta.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 23b99ad2..d832a87f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@solidxai/core", - "version": "0.1.10-alpha.1", + "version": "0.1.10-beta.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@solidxai/core", - "version": "0.1.10-alpha.1", + "version": "0.1.10-beta.0", "license": "BUSL-1.1", "dependencies": { "@aws-sdk/client-s3": "^3.637.0", diff --git a/package.json b/package.json index 8c06f30d..43db59bf 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@solidxai/core", - "version": "0.1.10-alpha.1", + "version": "0.1.10-beta.0", "description": "This module is a NestJS module containing all the required core providers required by a Solid application", "main": "dist/index.js", "types": "dist/index.d.ts", From 1b394acf30c2520f8f8f395fc56091ffea3e1501 Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Mon, 18 May 2026 20:31:17 +0530 Subject: [PATCH 030/136] moved back to beta3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 43db59bf..9c370620 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.0", + "version": "0.1.10-beta.3", "description": "This module is a NestJS module containing all the required core providers required by a Solid application", "main": "dist/index.js", "types": "dist/index.d.ts", From ead6077265271ffb4bbffc9bdd1ae67af7bc635a Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Mon, 18 May 2026 22:17:47 +0530 Subject: [PATCH 031/136] bug fix in chatter message user resolution after we moved to queues. --- ...atter-queue-subscriber-database.service.ts | 6 ++- .../chatter-queue-subscriber.service.ts | 6 ++- .../chatter-queue-subscriber-redis.service.ts | 13 ++++-- src/services/chatter-message.service.ts | 45 +++++++------------ 4 files changed, 34 insertions(+), 36 deletions(-) diff --git a/src/jobs/database/chatter-queue-subscriber-database.service.ts b/src/jobs/database/chatter-queue-subscriber-database.service.ts index 1bfea5ea..391fc4ac 100644 --- a/src/jobs/database/chatter-queue-subscriber-database.service.ts +++ b/src/jobs/database/chatter-queue-subscriber-database.service.ts @@ -35,7 +35,7 @@ export class ChatterQueueSubscriberDatabase extends DatabaseSubscriber ({ propertyName: n })), + false, + p.userId, ); break; case 'delete': - await this.chatterMessageService.postAuditMessageOnDelete(p.modelName, p.before); + await this.chatterMessageService.postAuditMessageOnDelete(p.modelName, p.before, false, p.userId); break; } } diff --git a/src/jobs/rabbitmq/chatter-queue-subscriber.service.ts b/src/jobs/rabbitmq/chatter-queue-subscriber.service.ts index 8b1dc46c..3c14c202 100644 --- a/src/jobs/rabbitmq/chatter-queue-subscriber.service.ts +++ b/src/jobs/rabbitmq/chatter-queue-subscriber.service.ts @@ -33,7 +33,7 @@ export class ChatterQueueSubscriberRabbitmq extends RabbitMqSubscriber ({ propertyName: n })), + false, + p.userId, ); break; case 'delete': - await this.chatterMessageService.postAuditMessageOnDelete(p.modelName, p.before); + await this.chatterMessageService.postAuditMessageOnDelete(p.modelName, p.before, false, p.userId); break; } } diff --git a/src/jobs/redis/chatter-queue-subscriber-redis.service.ts b/src/jobs/redis/chatter-queue-subscriber-redis.service.ts index f1748880..15fa0856 100644 --- a/src/jobs/redis/chatter-queue-subscriber-redis.service.ts +++ b/src/jobs/redis/chatter-queue-subscriber-redis.service.ts @@ -33,13 +33,20 @@ export class ChatterQueueSubscriberRedis extends RedisSubscriber { switch (p.eventType) { case 'insert': - await this.chatterMessageService.postAuditMessageOnInsert(p.after, { name: p.modelName } as any); + await this.chatterMessageService.postAuditMessageOnInsert(p.after, p.modelName, false, p.userId); break; case 'update': - await this.chatterMessageService.postAuditMessageOnUpdate(p.after, { name: p.modelName } as any, p.before, (p.updatedColumnNames || []).map(n => ({ propertyName: n }))); + await this.chatterMessageService.postAuditMessageOnUpdate( + p.after, + p.modelName, + p.before, + (p.updatedColumnNames || []).map(n => ({ propertyName: n })), + false, + p.userId, + ); break; case 'delete': - await this.chatterMessageService.postAuditMessageOnDelete(p.before, { name: p.modelName } as any, p.before); + await this.chatterMessageService.postAuditMessageOnDelete(p.modelName, p.before, false, p.userId); break; } } diff --git a/src/services/chatter-message.service.ts b/src/services/chatter-message.service.ts index 487e2998..fb933d51 100644 --- a/src/services/chatter-message.service.ts +++ b/src/services/chatter-message.service.ts @@ -47,6 +47,16 @@ export class ChatterMessageService extends CRUDService { super(entityManager, repo, 'chatterMessage', 'solid-core', moduleRef); } + private resolveMessageUser(userId?: number | null) { + if (userId) { + return { id: userId } as any; + } + + const activeUser = this.requestContextService.getActiveUser(); + const activeUserId = activeUser?.sub ?? null; + return activeUserId ? ({ id: activeUserId } as any) : null; + } + async markCompleted(id: number) { const activeUser = this.requestContextService.getActiveUser(); if (!activeUser) { @@ -114,7 +124,7 @@ export class ChatterMessageService extends CRUDService { return savedMessage; } - async postAuditMessageOnInsert(entity: any, modelName: string, messageQueue: boolean = false) { + async postAuditMessageOnInsert(entity: any, modelName: string, messageQueue: boolean = false, userId?: number | null) { if (!entity) { return; } @@ -139,8 +149,6 @@ export class ChatterMessageService extends CRUDService { !(field.type === 'relation' && field.relationType === 'one-to-many') ); - const activeUser = this.requestContextService.getActiveUser(); - const chatterMessage = new ChatterMessage(); chatterMessage.messageType = CHATTER_MESSAGE_TYPE.AUDIT; chatterMessage.messageSubType = CHATTER_MESSAGE_SUBTYPE.AUDIT_INSERT; @@ -150,13 +158,7 @@ export class ChatterMessageService extends CRUDService { chatterMessage.modelDisplayName = model?.displayName; chatterMessage.modelUserKey = entity[model?.userKeyField?.name]; chatterMessage.messageBody = `New ${model?.displayName} created`; - - if (activeUser) { - const userId = activeUser?.sub; - chatterMessage.user = { id: userId } as any; - } else { - chatterMessage.user = null; - } + chatterMessage.user = this.resolveMessageUser(userId); const savedMessage = await this.repo.save(chatterMessage); @@ -177,7 +179,7 @@ export class ChatterMessageService extends CRUDService { } } - async postAuditMessageOnUpdate(entity: any, modelName: string, databaseEntity: any, updatedColumns: any[] = [], messageQueue: boolean = false) { + async postAuditMessageOnUpdate(entity: any, modelName: string, databaseEntity: any, updatedColumns: any[] = [], messageQueue: boolean = false, userId?: number | null) { if (!databaseEntity || !entity) { return; } @@ -259,8 +261,6 @@ export class ChatterMessageService extends CRUDService { return; } - const activeUser = this.requestContextService.getActiveUser(); - const chatterMessage = new ChatterMessage(); chatterMessage.messageType = CHATTER_MESSAGE_TYPE.AUDIT; chatterMessage.messageSubType = CHATTER_MESSAGE_SUBTYPE.AUDIT_UPDATE; @@ -270,13 +270,7 @@ export class ChatterMessageService extends CRUDService { chatterMessage.modelDisplayName = model.displayName; chatterMessage.modelUserKey = entity[model?.userKeyField?.name]; chatterMessage.messageBody = `${model?.displayName} updated`; - - if (activeUser) { - const userId = activeUser?.sub; - chatterMessage.user = { id: userId } as any; - } else { - chatterMessage.user = null; - } + chatterMessage.user = this.resolveMessageUser(userId); const savedMessage = await this.repo.save(chatterMessage); @@ -294,7 +288,7 @@ export class ChatterMessageService extends CRUDService { } } - async postAuditMessageOnDelete(modelName: string, databaseEntity: any, messageQueue: boolean = false) { + async postAuditMessageOnDelete(modelName: string, databaseEntity: any, messageQueue: boolean = false, userId?: number | null) { const model = await this.modelMetadataRepo.findOne({ where: { singularName: lowerFirst(modelName) @@ -335,14 +329,7 @@ export class ChatterMessageService extends CRUDService { chatterMessage.modelUserKey = databaseEntity[model?.userKeyField?.name]; chatterMessage.messageBody = `${model?.displayName} deleted`; - const activeUser = this.requestContextService.getActiveUser(); - - if (activeUser) { - const userId = activeUser?.sub; - chatterMessage.user = { id: userId } as any; - } else { - chatterMessage.user = null; - } + chatterMessage.user = this.resolveMessageUser(userId); const savedMessage = await this.repo.save(chatterMessage); From 9895e6a8ad056e7dcc7bbfa14dcf4ff90f8ba976 Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Mon, 18 May 2026 22:55:03 +0530 Subject: [PATCH 032/136] chatter message user resolution chagnes --- src/services/chatter-message.service.ts | 35 ++++++++++++++----------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/src/services/chatter-message.service.ts b/src/services/chatter-message.service.ts index fb933d51..6a8f3b1c 100644 --- a/src/services/chatter-message.service.ts +++ b/src/services/chatter-message.service.ts @@ -47,14 +47,24 @@ export class ChatterMessageService extends CRUDService { super(entityManager, repo, 'chatterMessage', 'solid-core', moduleRef); } - private resolveMessageUser(userId?: number | null) { + private resolveMessageUserId(userId?: number | null): number | null { if (userId) { - return { id: userId } as any; + return userId; } - const activeUser = this.requestContextService.getActiveUser(); - const activeUserId = activeUser?.sub ?? null; - return activeUserId ? ({ id: activeUserId } as any) : null; + return this.requestContextService.getActiveUser()?.sub ?? null; + } + + private resolveMessageUser(userId?: number | null) { + const resolvedUserId = this.resolveMessageUserId(userId); + return resolvedUserId ? ({ id: resolvedUserId } as any) : null; + } + + private stampMessageAuditFields(chatterMessage: ChatterMessage, userId?: number | null) { + const resolvedUserId = this.resolveMessageUserId(userId); + chatterMessage.user = resolvedUserId ? ({ id: resolvedUserId } as any) : null; + chatterMessage.createdBy = resolvedUserId; + chatterMessage.updatedBy = resolvedUserId; } async markCompleted(id: number) { @@ -88,14 +98,7 @@ export class ChatterMessageService extends CRUDService { }); chatterMessage.modelDisplayName = model?.displayName ?? null; - const activeUser = this.requestContextService.getActiveUser(); - - if (activeUser) { - const userId = activeUser?.sub; - chatterMessage.user = { id: userId } as any; - } else { - chatterMessage.user = null; - } + this.stampMessageAuditFields(chatterMessage); const savedMessage = await this.repo.save(chatterMessage); @@ -158,7 +161,7 @@ export class ChatterMessageService extends CRUDService { chatterMessage.modelDisplayName = model?.displayName; chatterMessage.modelUserKey = entity[model?.userKeyField?.name]; chatterMessage.messageBody = `New ${model?.displayName} created`; - chatterMessage.user = this.resolveMessageUser(userId); + this.stampMessageAuditFields(chatterMessage, userId); const savedMessage = await this.repo.save(chatterMessage); @@ -270,7 +273,7 @@ export class ChatterMessageService extends CRUDService { chatterMessage.modelDisplayName = model.displayName; chatterMessage.modelUserKey = entity[model?.userKeyField?.name]; chatterMessage.messageBody = `${model?.displayName} updated`; - chatterMessage.user = this.resolveMessageUser(userId); + this.stampMessageAuditFields(chatterMessage, userId); const savedMessage = await this.repo.save(chatterMessage); @@ -329,7 +332,7 @@ export class ChatterMessageService extends CRUDService { chatterMessage.modelUserKey = databaseEntity[model?.userKeyField?.name]; chatterMessage.messageBody = `${model?.displayName} deleted`; - chatterMessage.user = this.resolveMessageUser(userId); + this.stampMessageAuditFields(chatterMessage, userId); const savedMessage = await this.repo.save(chatterMessage); From 8d45ac24a38116f73f0adb23b5be5a9fd740d985 Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Tue, 19 May 2026 11:51:45 +0530 Subject: [PATCH 033/136] 0.1.10-beta.4 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index d832a87f..cf89db9f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.0", + "version": "0.1.10-beta.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@solidxai/core", - "version": "0.1.10-beta.0", + "version": "0.1.10-beta.4", "license": "BUSL-1.1", "dependencies": { "@aws-sdk/client-s3": "^3.637.0", diff --git a/package.json b/package.json index 9c370620..ded94eeb 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.3", + "version": "0.1.10-beta.4", "description": "This module is a NestJS module containing all the required core providers required by a Solid application", "main": "dist/index.js", "types": "dist/index.d.ts", From 4a5f40ebca5e924c468ef727bc33efcb0f194cab Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Tue, 19 May 2026 13:57:54 +0530 Subject: [PATCH 034/136] 0.1.10-beta.5 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index cf89db9f..e1528238 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.4", + "version": "0.1.10-beta.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@solidxai/core", - "version": "0.1.10-beta.4", + "version": "0.1.10-beta.5", "license": "BUSL-1.1", "dependencies": { "@aws-sdk/client-s3": "^3.637.0", diff --git a/package.json b/package.json index ded94eeb..69630544 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.4", + "version": "0.1.10-beta.5", "description": "This module is a NestJS module containing all the required core providers required by a Solid application", "main": "dist/index.js", "types": "dist/index.d.ts", From f473e6fe9f2e2843a41922d35f0706da18982685 Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Tue, 19 May 2026 14:02:25 +0530 Subject: [PATCH 035/136] 0.1.10-beta.6 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index e1528238..696509a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.5", + "version": "0.1.10-beta.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@solidxai/core", - "version": "0.1.10-beta.5", + "version": "0.1.10-beta.6", "license": "BUSL-1.1", "dependencies": { "@aws-sdk/client-s3": "^3.637.0", diff --git a/package.json b/package.json index 69630544..ae569bcd 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.5", + "version": "0.1.10-beta.6", "description": "This module is a NestJS module containing all the required core providers required by a Solid application", "main": "dist/index.js", "types": "dist/index.d.ts", From a10e42d591d724bc048b622c21947165d59e4a27 Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Tue, 19 May 2026 14:27:21 +0530 Subject: [PATCH 036/136] changes to the test runner --- src/seeders/module-test-data.service.ts | 122 ++++++++++++++++++++--- src/testing/reporter/console-reporter.ts | 27 +++++ src/testing/reporter/reporter.types.ts | 7 ++ src/testing/runner/run-from-metadata.ts | 20 +++- 4 files changed, 160 insertions(+), 16 deletions(-) diff --git a/src/seeders/module-test-data.service.ts b/src/seeders/module-test-data.service.ts index 28bfa9a4..5e3f1571 100644 --- a/src/seeders/module-test-data.service.ts +++ b/src/seeders/module-test-data.service.ts @@ -24,6 +24,7 @@ import { TestingRoleSpec, TestingUserSpec } from 'src/testing/contracts/testing- @Injectable() export class ModuleTestDataService { private readonly logger = new Logger(ModuleTestDataService.name); + private static readonly TEARDOWN_RETRY_ATTEMPTS = 5; constructor( private readonly moduleRef: ModuleRef, @@ -665,31 +666,122 @@ export class ModuleTestDataService { private async dropTestDatabaseObjects(databases: Record): Promise { const entries = Object.entries(databases); for (const [dsName, dbName] of entries) { - const dataSource = this.resolveDataSourceByName(dsName); - if (!dataSource.isInitialized) { - await dataSource.initialize(); - } + await this.dropTestDatabaseObjectsWithRetry(dsName, dbName); + } + } - console.log(`Dropping test database/schema "${dbName}" on datasource "${dsName}"...`); + private async dropTestDatabaseObjectsWithRetry(dsName: string, dbName: string): Promise { + let lastError: unknown; + + for (let attempt = 1; attempt <= ModuleTestDataService.TEARDOWN_RETRY_ATTEMPTS; attempt += 1) { + console.log(`Attempting to tear down "${dbName}" on datasource "${dsName}" (${attempt}/${ModuleTestDataService.TEARDOWN_RETRY_ATTEMPTS})...`); - const queryRunner = dataSource.createQueryRunner(); try { - const type = dataSource.options.type; - if (type === 'postgres') { - await queryRunner.query(`DROP DATABASE IF EXISTS "${dbName}"`); - } else if (type === 'mssql') { - await this.dropMssqlSchema(queryRunner, dbName); - } else if (type === 'mysql' || type === 'mariadb') { - await queryRunner.query(`DROP DATABASE IF EXISTS \`${dbName}\``); - } else { - throw new Error(`Unsupported database type for test data deletion: ${type}`); + await this.dropSingleTestDatabaseObject(dsName, dbName); + return; + } catch (error) { + lastError = error; + if (attempt >= ModuleTestDataService.TEARDOWN_RETRY_ATTEMPTS) { + throw error; } + + await this.sleep(this.teardownRetryDelayMs(attempt)); + } + } + + throw lastError instanceof Error ? lastError : new Error(String(lastError)); + } + + private teardownRetryDelayMs(attempt: number): number { + const baseMs = 500; + const incrementMs = 350; + const jitterMs = Math.floor(Math.random() * 250); + return baseMs + ((attempt - 1) * incrementMs) + jitterMs; + } + + private async sleep(ms: number): Promise { + await new Promise((resolve) => setTimeout(resolve, ms)); + } + + private async dropSingleTestDatabaseObject(dsName: string, dbName: string): Promise { + const dataSource = this.resolveDataSourceByName(dsName); + + if (dataSource.options.type === 'postgres') { + await this.dropPostgresDatabase(dataSource, dbName); + return; + } + + if (!dataSource.isInitialized) { + await dataSource.initialize(); + } + + const queryRunner = dataSource.createQueryRunner(); + try { + const type = dataSource.options.type; + if (type === 'mssql') { + await this.dropMssqlSchema(queryRunner, dbName); + } else if (type === 'mysql' || type === 'mariadb') { + await queryRunner.query(`DROP DATABASE IF EXISTS \`${dbName}\``); + } else { + throw new Error(`Unsupported database type for test data deletion: ${type}`); + } + } finally { + await queryRunner.release(); + } + } + + private async dropPostgresDatabase(dataSource: DataSource, dbName: string): Promise { + if (dataSource.isInitialized) { + await dataSource.destroy(); + } + + const adminDataSource = new DataSource({ + ...(dataSource.options as any), + database: this.resolvePostgresMaintenanceDatabase(dataSource), + name: `${String(dataSource.name ?? 'default')}_teardown_admin_${Date.now()}`, + synchronize: false, + migrationsRun: false, + entities: [], + subscribers: [], + migrations: [], + }); + + try { + await adminDataSource.initialize(); + const queryRunner = adminDataSource.createQueryRunner(); + try { + await queryRunner.query( + `SELECT pg_terminate_backend(pid) + FROM pg_stat_activity + WHERE datname = $1 + AND pid <> pg_backend_pid()`, + [dbName], + ); + await queryRunner.query(`DROP DATABASE IF EXISTS "${dbName}"`); } finally { await queryRunner.release(); } + } finally { + if (adminDataSource.isInitialized) { + await adminDataSource.destroy(); + } } } + private resolvePostgresMaintenanceDatabase(dataSource: DataSource): string { + const configured = process.env.POSTGRES_MAINTENANCE_DATABASE?.trim(); + if (configured) { + return configured; + } + + const currentDb = String((dataSource.options as any)?.database ?? '').trim(); + if (currentDb && currentDb !== 'postgres') { + return 'postgres'; + } + + return 'template1'; + } + private async dropMssqlSchema(queryRunner: ReturnType, schemaName: string): Promise { const foreignKeys: Array<{ fk_name: string; table_name: string }> = await queryRunner.query( `SELECT fk.name AS fk_name, t.name AS table_name diff --git a/src/testing/reporter/console-reporter.ts b/src/testing/reporter/console-reporter.ts index d4b0ae12..4de22e86 100644 --- a/src/testing/reporter/console-reporter.ts +++ b/src/testing/reporter/console-reporter.ts @@ -146,6 +146,10 @@ function formatStepLabel(step: OpStep): string { } export class ConsoleReporter implements Reporter { + private totalScenarios = 0; + private passedScenarios = 0; + private failedScenarios = 0; + onScenarioStart(scenario: { id: string; name?: string }): void { const label = scenario.name ? `${scenario.id} (${scenario.name})` : scenario.id; console.log(`\n▶ Scenario: ${label}`); @@ -157,6 +161,12 @@ export class ConsoleReporter implements Reporter { ): void { const label = scenario.name ? `${scenario.id} (${scenario.name})` : scenario.id; const status = result.ok ? "✔" : "✖"; + this.totalScenarios += 1; + if (result.ok) { + this.passedScenarios += 1; + } else { + this.failedScenarios += 1; + } console.log(`${status} Scenario: ${label} (${result.durationMs}ms)`); } @@ -226,4 +236,21 @@ export class ConsoleReporter implements Reporter { if (!dataText.length) return; console.log(indentLines(dataText, `${STEP_INDENT}${INDENT}${INDENT}`)); } + + onRunEnd(args: { + ok: boolean; + total: number; + passed: number; + failed: number; + durationMs: number; + }): void { + const durationSeconds = (args.durationMs / 1000).toFixed(2); + const finalStatus = args.ok ? "PASSED" : "FAILED"; + + console.log("\n════════ Test Run Summary ════════"); + console.log(`Result: Test run ${finalStatus}`); + console.log(`Cases: total=${args.total}, passed=${args.passed}, failed=${args.failed}`); + console.log(`Duration: ${durationSeconds}s`); + console.log("══════════════════════════════════"); + } } diff --git a/src/testing/reporter/reporter.types.ts b/src/testing/reporter/reporter.types.ts index afbf9840..34bd4481 100644 --- a/src/testing/reporter/reporter.types.ts +++ b/src/testing/reporter/reporter.types.ts @@ -33,4 +33,11 @@ export interface Reporter { contentType: string; data: Buffer | string; }): void; + onRunEnd?(args: { + ok: boolean; + total: number; + passed: number; + failed: number; + durationMs: number; + }): void; } diff --git a/src/testing/runner/run-from-metadata.ts b/src/testing/runner/run-from-metadata.ts index f271047d..72f1003f 100644 --- a/src/testing/runner/run-from-metadata.ts +++ b/src/testing/runner/run-from-metadata.ts @@ -44,6 +44,7 @@ export type RunnerOptions = { }; export async function runFromMetadata(opts: RunnerOptions): Promise { + const startedAt = Date.now(); const registry = new StepRegistry(); registerApiSteps(registry); registerUiSteps(registry); @@ -71,15 +72,32 @@ export async function runFromMetadata(opts: RunnerOptions): Promise { const ui = new PlaywrightAdapter(opts.ui); const ctxBase = { resources, reporter, api, ui, specRegistry, testData, options: opts.options }; const uiStarted = { value: false }; + let passed = 0; + let failed = 0; + let runError: unknown; try { for (const scenario of scenarios) { if (scenarioNeedsUi(scenario)) { await ensureUiStarted(ctxBase, uiStarted); } - await engine.runScenario(scenario, ctxBase); + try { + await engine.runScenario(scenario, ctxBase); + passed += 1; + } catch (error) { + failed += 1; + runError = error; + throw error; + } } } finally { + reporter.onRunEnd?.({ + ok: !runError, + total: scenarios.length, + passed, + failed, + durationMs: Date.now() - startedAt, + }); if (uiStarted.value) { await ui.stop(); } From a56366125a288e504f8bc6d8d549f2b32f9786cb Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Tue, 19 May 2026 14:36:15 +0530 Subject: [PATCH 037/136] 0.1.10-beta.7 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 696509a8..793a4d45 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.6", + "version": "0.1.10-beta.7", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@solidxai/core", - "version": "0.1.10-beta.6", + "version": "0.1.10-beta.7", "license": "BUSL-1.1", "dependencies": { "@aws-sdk/client-s3": "^3.637.0", diff --git a/package.json b/package.json index ae569bcd..70c560ec 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.6", + "version": "0.1.10-beta.7", "description": "This module is a NestJS module containing all the required core providers required by a Solid application", "main": "dist/index.js", "types": "dist/index.d.ts", From 21e17f5ebdecfbe81a870893268559900e31d90f Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Tue, 19 May 2026 14:56:05 +0530 Subject: [PATCH 038/136] 0.1.10-beta.8 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 793a4d45..2e0b392a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.7", + "version": "0.1.10-beta.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@solidxai/core", - "version": "0.1.10-beta.7", + "version": "0.1.10-beta.8", "license": "BUSL-1.1", "dependencies": { "@aws-sdk/client-s3": "^3.637.0", diff --git a/package.json b/package.json index 70c560ec..d68fa29e 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.7", + "version": "0.1.10-beta.8", "description": "This module is a NestJS module containing all the required core providers required by a Solid application", "main": "dist/index.js", "types": "dist/index.d.ts", From 314630c9a0f4537044e2243cc78060d8407fa953 Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Tue, 19 May 2026 15:08:48 +0530 Subject: [PATCH 039/136] 0.1.10-beta.9 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2e0b392a..08b48c05 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.8", + "version": "0.1.10-beta.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@solidxai/core", - "version": "0.1.10-beta.8", + "version": "0.1.10-beta.9", "license": "BUSL-1.1", "dependencies": { "@aws-sdk/client-s3": "^3.637.0", diff --git a/package.json b/package.json index d68fa29e..ebd2f4ca 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.8", + "version": "0.1.10-beta.9", "description": "This module is a NestJS module containing all the required core providers required by a Solid application", "main": "dist/index.js", "types": "dist/index.d.ts", From cc22ad9e3f78e0128ce90665e6cf99440f2434b4 Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Tue, 19 May 2026 16:08:29 +0530 Subject: [PATCH 040/136] 0.1.10-beta.9 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2e0b392a..08b48c05 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.8", + "version": "0.1.10-beta.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@solidxai/core", - "version": "0.1.10-beta.8", + "version": "0.1.10-beta.9", "license": "BUSL-1.1", "dependencies": { "@aws-sdk/client-s3": "^3.637.0", diff --git a/package.json b/package.json index d68fa29e..ebd2f4ca 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.8", + "version": "0.1.10-beta.9", "description": "This module is a NestJS module containing all the required core providers required by a Solid application", "main": "dist/index.js", "types": "dist/index.d.ts", From 2450682d1a59c08318765dacd381d1eab8e15efa Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Tue, 19 May 2026 16:13:42 +0530 Subject: [PATCH 041/136] 0.1.10-beta.10 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 08b48c05..6e324fb9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.9", + "version": "0.1.10-beta.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@solidxai/core", - "version": "0.1.10-beta.9", + "version": "0.1.10-beta.10", "license": "BUSL-1.1", "dependencies": { "@aws-sdk/client-s3": "^3.637.0", diff --git a/package.json b/package.json index ebd2f4ca..c5e2f6bb 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.9", + "version": "0.1.10-beta.10", "description": "This module is a NestJS module containing all the required core providers required by a Solid application", "main": "dist/index.js", "types": "dist/index.d.ts", From 378b9de69a7b2b8ee846ca48e5642091f52bc65b Mon Sep 17 00:00:00 2001 From: Jenendar Date: Fri, 22 May 2026 11:34:01 +0530 Subject: [PATCH 042/136] playwrite added as dependency instead --- package.json | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index c5e2f6bb..03fdd672 100755 --- a/package.json +++ b/package.json @@ -79,12 +79,11 @@ "ts-morph": "^27.0.2", "twilio": "^5.8.0", "uuid": "^9.0.1", - "xlsx": "^0.18.5" + "xlsx": "^0.18.5", + "playwright": ">=1.0.0" }, "peerDependenciesMeta": { - "playwright": { - "optional": true - } + }, "peerDependencies": { "@nestjs/axios": "^3.0.2", @@ -106,7 +105,6 @@ "nest-commander": "^3.12.5", "nest-winston": "^1.9.7", "nestjs-cls": "^5.4.3", - "playwright": ">=1.0.0", "reflect-metadata": "^0.2.2", "typeorm": "^0.3.20", "typeorm-naming-strategies": "^4.1.0", From e8860a4e9bad7e274c12e8f62df3ed1faec96bc8 Mon Sep 17 00:00:00 2001 From: Rajesh Chityal Date: Fri, 22 May 2026 12:51:16 +0530 Subject: [PATCH 043/136] MCP table solid changes --- src/controllers/mcp-audit-log.controller.ts | 70 ++++ src/dtos/create-mcp-audit-log.dto.ts | 84 +++++ src/dtos/update-mcp-audit-log.dto.ts | 83 +++++ src/entities/mcp-audit-log.entity.ts | 55 +++ src/index.ts | 4 + src/repository/mcp-audit-log.repository.ts | 17 + .../seed-data/solid-core-metadata.json | 346 ++++++++++++++++++ src/services/mcp-audit-log.service.ts | 19 + src/solid-core.module.ts | 8 + 9 files changed, 686 insertions(+) create mode 100644 src/controllers/mcp-audit-log.controller.ts create mode 100644 src/dtos/create-mcp-audit-log.dto.ts create mode 100644 src/dtos/update-mcp-audit-log.dto.ts create mode 100644 src/entities/mcp-audit-log.entity.ts create mode 100644 src/repository/mcp-audit-log.repository.ts create mode 100644 src/services/mcp-audit-log.service.ts diff --git a/src/controllers/mcp-audit-log.controller.ts b/src/controllers/mcp-audit-log.controller.ts new file mode 100644 index 00000000..6f7ff6a0 --- /dev/null +++ b/src/controllers/mcp-audit-log.controller.ts @@ -0,0 +1,70 @@ +import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query, UploadedFiles, UseInterceptors } from '@nestjs/common'; +import { AnyFilesInterceptor } from '@nestjs/platform-express'; +import { ApiBearerAuth, ApiQuery, ApiTags } from '@nestjs/swagger'; +import { CreateMcpAuditLogDto } from 'src/dtos/create-mcp-audit-log.dto'; +import { UpdateMcpAuditLogDto } from 'src/dtos/update-mcp-audit-log.dto'; +import { McpAuditLogService } from 'src/services/mcp-audit-log.service'; + +@ApiTags('Solid Core') +@Controller('mcp-audit-log') +export class McpAuditLogController { + constructor(private readonly service: McpAuditLogService) {} + + @ApiBearerAuth('jwt') + @Post() + @UseInterceptors(AnyFilesInterceptor()) + create(@Body() createDto: CreateMcpAuditLogDto, @UploadedFiles() files: Array) { + return this.service.create(createDto, files); + } + + @ApiBearerAuth('jwt') + @Post('/bulk') + @UseInterceptors(AnyFilesInterceptor()) + insertMany(@Body() createDtos: CreateMcpAuditLogDto[], @UploadedFiles() filesArray: Express.Multer.File[][] = []) { + return this.service.insertMany(createDtos, filesArray); + } + + @ApiBearerAuth('jwt') + @Put(':id') + @UseInterceptors(AnyFilesInterceptor()) + update(@Param('id') id: number, @Body() updateDto: UpdateMcpAuditLogDto, @UploadedFiles() files: Array) { + return this.service.update(id, updateDto, files); + } + + @ApiBearerAuth('jwt') + @Patch(':id') + @UseInterceptors(AnyFilesInterceptor()) + partialUpdate(@Param('id') id: number, @Body() updateDto: UpdateMcpAuditLogDto, @UploadedFiles() files: Array) { + return this.service.update(id, updateDto, files, true); + } + + @ApiBearerAuth('jwt') + @ApiQuery({ name: 'limit', required: false, type: Number }) + @ApiQuery({ name: 'offset', required: false, type: Number }) + @ApiQuery({ name: 'fields', required: false, type: Array }) + @ApiQuery({ name: 'sort', required: false, type: Array }) + @ApiQuery({ name: 'populate', required: false, type: Array }) + @ApiQuery({ name: 'filters', required: false, type: Array }) + @Get() + findMany(@Query() query: any) { + return this.service.find(query); + } + + @ApiBearerAuth('jwt') + @Get(':id') + findOne(@Param('id') id: string, @Query() query: any) { + return this.service.findOne(+id, query); + } + + @ApiBearerAuth('jwt') + @Delete('/bulk') + deleteMany(@Body() ids: number[]) { + return this.service.deleteMany(ids); + } + + @ApiBearerAuth('jwt') + @Delete(':id') + delete(@Param('id') id: number) { + return this.service.delete(id); + } +} diff --git a/src/dtos/create-mcp-audit-log.dto.ts b/src/dtos/create-mcp-audit-log.dto.ts new file mode 100644 index 00000000..b48da21b --- /dev/null +++ b/src/dtos/create-mcp-audit-log.dto.ts @@ -0,0 +1,84 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsDate, IsInt, IsNotEmpty, IsNumber, IsOptional, IsString } from 'class-validator'; + +export class CreateMcpAuditLogDto { + @IsOptional() + @IsInt() + @ApiProperty() + userId: number; + + @IsOptional() + @IsInt() + @ApiProperty() + apiKeyId: number; + + @IsOptional() + @IsString() + @ApiProperty() + username: string; + + @IsNotEmpty() + @IsString() + @ApiProperty() + transport: string; + + @IsOptional() + @IsString() + @ApiProperty() + mcpSessionId: string; + + @IsOptional() + @IsString() + @ApiProperty() + clientAddr: string; + + @IsNotEmpty() + @IsString() + @ApiProperty() + method: string; + + @IsOptional() + @IsString() + @ApiProperty() + requestId: string; + + @IsOptional() + @IsString() + @ApiProperty() + toolName: string; + + @IsOptional() + @IsString() + @ApiProperty() + requestParams: string; + + @IsNotEmpty() + @IsString() + @ApiProperty() + status: string; + + @IsOptional() + @IsString() + @ApiProperty() + responseResult: string; + + @IsOptional() + @IsInt() + @ApiProperty() + errorCode: number; + + @IsOptional() + @IsString() + @ApiProperty() + errorMessage: string; + + @IsOptional() + @IsNumber() + @ApiProperty() + durationMs: number; + + @IsOptional() + @IsDate() + @ApiProperty() + createdAt: Date; +} diff --git a/src/dtos/update-mcp-audit-log.dto.ts b/src/dtos/update-mcp-audit-log.dto.ts new file mode 100644 index 00000000..6186e043 --- /dev/null +++ b/src/dtos/update-mcp-audit-log.dto.ts @@ -0,0 +1,83 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsInt, IsNumber, IsOptional, IsString } from 'class-validator'; + +export class UpdateMcpAuditLogDto { + @IsOptional() + @IsInt() + id: number; + + @IsOptional() + @IsInt() + @ApiProperty() + userId: number; + + @IsOptional() + @IsInt() + @ApiProperty() + apiKeyId: number; + + @IsOptional() + @IsString() + @ApiProperty() + username: string; + + @IsOptional() + @IsString() + @ApiProperty() + transport: string; + + @IsOptional() + @IsString() + @ApiProperty() + mcpSessionId: string; + + @IsOptional() + @IsString() + @ApiProperty() + clientAddr: string; + + @IsOptional() + @IsString() + @ApiProperty() + method: string; + + @IsOptional() + @IsString() + @ApiProperty() + requestId: string; + + @IsOptional() + @IsString() + @ApiProperty() + toolName: string; + + @IsOptional() + @IsString() + @ApiProperty() + requestParams: string; + + @IsOptional() + @IsString() + @ApiProperty() + status: string; + + @IsOptional() + @IsString() + @ApiProperty() + responseResult: string; + + @IsOptional() + @IsInt() + @ApiProperty() + errorCode: number; + + @IsOptional() + @IsString() + @ApiProperty() + errorMessage: string; + + @IsOptional() + @IsNumber() + @ApiProperty() + durationMs: number; +} diff --git a/src/entities/mcp-audit-log.entity.ts b/src/entities/mcp-audit-log.entity.ts new file mode 100644 index 00000000..20003eae --- /dev/null +++ b/src/entities/mcp-audit-log.entity.ts @@ -0,0 +1,55 @@ +import { Column, Entity, Index } from 'typeorm'; +import { CommonEntity } from 'src/entities/common.entity'; +import { getColumnType } from 'src/helpers/typeorm-db-helper'; + +@Entity({ name: 'ss_mcp_audit_log' }) +export class McpAuditLog extends CommonEntity { + @Index() + @Column({ nullable: true }) + userId: number; + + @Column({ nullable: true }) + apiKeyId: number; + + @Column({ nullable: true, length: 128 }) + username: string; + + @Column({ length: 32 }) + transport: string; + + @Index() + @Column({ nullable: true, length: 64 }) + mcpSessionId: string; + + @Column({ nullable: true, length: 64 }) + clientAddr: string; + + @Index() + @Column({ length: 64 }) + method: string; + + @Column({ nullable: true, length: 64 }) + requestId: string; + + @Index() + @Column({ nullable: true, length: 128 }) + toolName: string; + + @Column({ nullable: true, ...getColumnType('longText') }) + requestParams: string; + + @Column({ length: 16 }) + status: string; + + @Column({ nullable: true, ...getColumnType('longText') }) + responseResult: string; + + @Column({ nullable: true }) + errorCode: number; + + @Column({ nullable: true, ...getColumnType('longText') }) + errorMessage: string; + + @Column({ nullable: true, ...getColumnType('decimal') }) + durationMs: number; +} diff --git a/src/index.ts b/src/index.ts index 9a63c27d..df8e1839 100755 --- a/src/index.ts +++ b/src/index.ts @@ -49,6 +49,7 @@ export * from './dtos/create-mq-message-queue.dto' export * from './dtos/create-mq-message.dto' export * from './dtos/create-agent-session.dto' export * from './dtos/create-agent-event.dto' +export * from './dtos/create-mcp-audit-log.dto' export * from './dtos/create-scheduled-job.dto' export * from './dtos/create-permission-metadata.dto' export * from './dtos/create-role-metadata.dto' @@ -89,6 +90,7 @@ export * from './dtos/update-mq-message-queue.dto' export * from './dtos/update-mq-message.dto' export * from './dtos/update-agent-session.dto' export * from './dtos/update-agent-event.dto' +export * from './dtos/update-mcp-audit-log.dto' export * from './dtos/update-scheduled-job.dto' export * from './dtos/update-permission-metadata.dto' export * from './dtos/update-role-metadata.dto' @@ -122,6 +124,7 @@ export * from './entities/mq-message-queue.entity' export * from './entities/mq-message.entity' export * from './entities/agent-session.entity' export * from './entities/agent-event.entity' +export * from './entities/mcp-audit-log.entity' export * from './entities/scheduled-job.entity' export * from './entities/permission-metadata.entity' export * from './entities/role-metadata.entity' @@ -297,6 +300,7 @@ export * from './services/mq-message-queue.service' export * from './services/mq-message.service' export * from './services/agent-session.service' export * from './services/agent-event.service' +export * from './services/mcp-audit-log.service' export * from './services/scheduled-job.service' export * from './services/pdf.service' export * from './services/permission-metadata.service' diff --git a/src/repository/mcp-audit-log.repository.ts b/src/repository/mcp-audit-log.repository.ts new file mode 100644 index 00000000..e106587f --- /dev/null +++ b/src/repository/mcp-audit-log.repository.ts @@ -0,0 +1,17 @@ +import { Injectable } from '@nestjs/common'; +import { McpAuditLog } from 'src/entities/mcp-audit-log.entity'; +import { RequestContextService } from 'src/services/request-context.service'; +import { DataSource } from 'typeorm'; +import { SecurityRuleRepository } from './security-rule.repository'; +import { SolidBaseRepository } from './solid-base.repository'; + +@Injectable() +export class McpAuditLogRepository extends SolidBaseRepository { + constructor( + readonly dataSource: DataSource, + readonly requestContextService: RequestContextService, + readonly securityRuleRepository: SecurityRuleRepository, + ) { + super(McpAuditLog, dataSource, requestContextService, securityRuleRepository); + } +} diff --git a/src/seeders/seed-data/solid-core-metadata.json b/src/seeders/seed-data/solid-core-metadata.json index 56474438..72fccb8a 100755 --- a/src/seeders/seed-data/solid-core-metadata.json +++ b/src/seeders/seed-data/solid-core-metadata.json @@ -5932,6 +5932,214 @@ "isSystem": true } ] + }, + { + "singularName": "mcpAuditLog", + "pluralName": "mcpAuditLogs", + "displayName": "MCP Audit Log", + "description": "Audit trail of MCP requests handled by the server", + "dataSource": "default", + "dataSourceType": "postgres", + "tableName": "ss_mcp_audit_log", + "isChild": false, + "isLegacyTable": true, + "isLegacyTableWithId": true, + "enableAuditTracking": false, + "enableSoftDelete": false, + "draftPublishWorkflow": false, + "internationalisation": false, + "isSystem": true, + "userKeyFieldUserKey": "id", + "fields": [ + { + "name": "userId", + "displayName": "User ID", + "type": "int", + "columnName": "user_id", + "required": false, + "unique": false, + "index": true, + "private": false, + "encrypt": false, + "isSystem": true + }, + { + "name": "apiKeyId", + "displayName": "API Key ID", + "type": "int", + "columnName": "api_key_id", + "required": false, + "unique": false, + "index": false, + "private": false, + "encrypt": false, + "isSystem": true + }, + { + "name": "username", + "displayName": "Username", + "type": "shortText", + "columnName": "username", + "length": 128, + "required": false, + "unique": false, + "index": false, + "private": false, + "encrypt": false, + "isSystem": true + }, + { + "name": "transport", + "displayName": "Transport", + "type": "shortText", + "columnName": "transport", + "length": 32, + "required": true, + "unique": false, + "index": false, + "private": false, + "encrypt": false, + "isSystem": true + }, + { + "name": "mcpSessionId", + "displayName": "MCP Session ID", + "type": "shortText", + "columnName": "mcp_session_id", + "length": 64, + "required": false, + "unique": false, + "index": true, + "private": false, + "encrypt": false, + "isSystem": true + }, + { + "name": "clientAddr", + "displayName": "Client Address", + "type": "shortText", + "columnName": "client_addr", + "length": 64, + "required": false, + "unique": false, + "index": false, + "private": false, + "encrypt": false, + "isSystem": true + }, + { + "name": "method", + "displayName": "Method", + "type": "shortText", + "columnName": "method", + "length": 64, + "required": true, + "unique": false, + "index": true, + "private": false, + "encrypt": false, + "isSystem": true + }, + { + "name": "requestId", + "displayName": "Request ID", + "type": "shortText", + "columnName": "request_id", + "length": 64, + "required": false, + "unique": false, + "index": false, + "private": false, + "encrypt": false, + "isSystem": true + }, + { + "name": "toolName", + "displayName": "Tool Name", + "type": "shortText", + "columnName": "tool_name", + "length": 128, + "required": false, + "unique": false, + "index": true, + "private": false, + "encrypt": false, + "isSystem": true + }, + { + "name": "requestParams", + "displayName": "Request Params", + "type": "longText", + "columnName": "request_params", + "required": false, + "unique": false, + "index": false, + "private": false, + "encrypt": false, + "isSystem": true + }, + { + "name": "status", + "displayName": "Status", + "type": "shortText", + "columnName": "status", + "length": 16, + "required": true, + "unique": false, + "index": false, + "private": false, + "encrypt": false, + "isSystem": true + }, + { + "name": "responseResult", + "displayName": "Response Result", + "type": "longText", + "columnName": "response_result", + "required": false, + "unique": false, + "index": false, + "private": false, + "encrypt": false, + "isSystem": true + }, + { + "name": "errorCode", + "displayName": "Error Code", + "type": "int", + "columnName": "error_code", + "required": false, + "unique": false, + "index": false, + "private": false, + "encrypt": false, + "isSystem": true + }, + { + "name": "errorMessage", + "displayName": "Error Message", + "type": "longText", + "columnName": "error_message", + "required": false, + "unique": false, + "index": false, + "private": false, + "encrypt": false, + "isSystem": true + }, + { + "name": "durationMs", + "displayName": "Duration (ms)", + "type": "decimal", + "columnName": "duration_ms", + "required": false, + "unique": false, + "index": false, + "private": false, + "encrypt": false, + "isSystem": true + } + ] } ] }, @@ -5998,6 +6206,8 @@ "AgentSessionController.findOne", "AgentEventController.findMany", "AgentEventController.findOne", + "McpAuditLogController.findMany", + "McpAuditLogController.findOne", "mcp:invoke", "agent:invoke" ] @@ -6607,6 +6817,32 @@ "viewUserKey": "agentEvent-form-view", "moduleUserKey": "solid-core", "modelUserKey": "agentEvent" + }, + { + "displayName": "MCP Audit Log List Action", + "name": "mcpAuditLog-list-action", + "type": "solid", + "domain": "", + "context": "", + "customComponent": "", + "customIsModal": true, + "serverEndpoint": "", + "viewUserKey": "mcpAuditLog-list-view", + "moduleUserKey": "solid-core", + "modelUserKey": "mcpAuditLog" + }, + { + "displayName": "MCP Audit Log Form Action", + "name": "mcpAuditLog-form-action", + "type": "solid", + "domain": "", + "context": "", + "customComponent": "", + "customIsModal": true, + "serverEndpoint": "", + "viewUserKey": "mcpAuditLog-form-view", + "moduleUserKey": "solid-core", + "modelUserKey": "mcpAuditLog" } ], "menus": [ @@ -6963,6 +7199,14 @@ "actionUserKey": "agentEvent-list-action", "moduleUserKey": "solid-core", "parentMenuItemUserKey": "agent-menu-item" + }, + { + "displayName": "MCP Audit Log", + "name": "mcpAuditLog-menu-item", + "sequenceNumber": 3, + "actionUserKey": "mcpAuditLog-list-action", + "moduleUserKey": "solid-core", + "parentMenuItemUserKey": "agent-menu-item" } ], "views": [ @@ -13894,6 +14138,108 @@ } ] } + }, + { + "name": "mcpAuditLog-list-view", + "displayName": "MCP Audit Logs", + "type": "list", + "context": "{}", + "moduleUserKey": "solid-core", + "modelUserKey": "mcpAuditLog", + "layout": { + "type": "list", + "attrs": { + "pagination": true, + "pageSizeOptions": [10, 25, 50], + "enableGlobalSearch": true, + "create": true, + "edit": true, + "delete": false + }, + "children": [ + { "type": "field", "attrs": { "name": "id" } }, + { "type": "field", "attrs": { "name": "createdAt" } }, + { "type": "field", "attrs": { "name": "method", "isSearchable": true } }, + { "type": "field", "attrs": { "name": "toolName", "isSearchable": true } }, + { "type": "field", "attrs": { "name": "status", "isSearchable": true } }, + { "type": "field", "attrs": { "name": "username", "isSearchable": true } }, + { "type": "field", "attrs": { "name": "userId" } }, + { "type": "field", "attrs": { "name": "transport", "isSearchable": true } }, + { "type": "field", "attrs": { "name": "mcpSessionId", "isSearchable": true } }, + { "type": "field", "attrs": { "name": "clientAddr", "isSearchable": true } }, + { "type": "field", "attrs": { "name": "durationMs" } }, + { "type": "field", "attrs": { "name": "errorCode" } } + ] + } + }, + { + "name": "mcpAuditLog-form-view", + "displayName": "MCP Audit Log", + "type": "form", + "context": "{}", + "moduleUserKey": "solid-core", + "modelUserKey": "mcpAuditLog", + "layout": { + "type": "form", + "attrs": { + "name": "form-1", + "label": "MCP Audit Log", + "className": "grid" + }, + "children": [ + { + "type": "sheet", + "attrs": { "name": "sheet-1" }, + "children": [ + { + "type": "row", + "attrs": { "name": "row-1" }, + "children": [ + { + "type": "column", + "attrs": { "name": "col-1", "label": "", "className": "col-12 sm:col-12 md:col-6 lg:col-6" }, + "children": [ + { "type": "field", "attrs": { "name": "method" } }, + { "type": "field", "attrs": { "name": "toolName" } }, + { "type": "field", "attrs": { "name": "status" } }, + { "type": "field", "attrs": { "name": "transport" } }, + { "type": "field", "attrs": { "name": "mcpSessionId" } }, + { "type": "field", "attrs": { "name": "requestId" } } + ] + }, + { + "type": "column", + "attrs": { "name": "col-2", "label": "", "className": "col-12 sm:col-12 md:col-6 lg:col-6" }, + "children": [ + { "type": "field", "attrs": { "name": "userId" } }, + { "type": "field", "attrs": { "name": "apiKeyId" } }, + { "type": "field", "attrs": { "name": "username" } }, + { "type": "field", "attrs": { "name": "clientAddr" } }, + { "type": "field", "attrs": { "name": "durationMs" } }, + { "type": "field", "attrs": { "name": "errorCode" } } + ] + } + ] + }, + { + "type": "row", + "attrs": { "name": "row-2" }, + "children": [ + { + "type": "column", + "attrs": { "name": "col-payload", "label": "", "className": "col-12" }, + "children": [ + { "type": "field", "attrs": { "name": "requestParams", "viewWidget": "SolidJsonFormViewWidget" } }, + { "type": "field", "attrs": { "name": "responseResult", "viewWidget": "SolidJsonFormViewWidget" } }, + { "type": "field", "attrs": { "name": "errorMessage" } } + ] + } + ] + } + ] + } + ] + } } ], "emailTemplates": [ diff --git a/src/services/mcp-audit-log.service.ts b/src/services/mcp-audit-log.service.ts new file mode 100644 index 00000000..ca7651b0 --- /dev/null +++ b/src/services/mcp-audit-log.service.ts @@ -0,0 +1,19 @@ +import { Injectable } from '@nestjs/common'; +import { ModuleRef } from '@nestjs/core'; +import { InjectEntityManager } from '@nestjs/typeorm'; +import { McpAuditLog } from 'src/entities/mcp-audit-log.entity'; +import { McpAuditLogRepository } from 'src/repository/mcp-audit-log.repository'; +import { EntityManager } from 'typeorm'; +import { CRUDService } from './crud.service'; + +@Injectable() +export class McpAuditLogService extends CRUDService { + constructor( + @InjectEntityManager() + readonly entityManager: EntityManager, + readonly repo: McpAuditLogRepository, + readonly moduleRef: ModuleRef, + ) { + super(entityManager, repo, 'mcpAuditLog', 'solid-core', moduleRef); + } +} diff --git a/src/solid-core.module.ts b/src/solid-core.module.ts index e7e48abf..0baf971b 100755 --- a/src/solid-core.module.ts +++ b/src/solid-core.module.ts @@ -191,6 +191,7 @@ import { SavedFiltersController } from './controllers/saved-filters.controller'; import { ScheduledJobController } from './controllers/scheduled-job.controller'; import { AgentSessionController } from './controllers/agent-session.controller'; import { AgentEventController } from './controllers/agent-event.controller'; +import { McpAuditLogController } from './controllers/mcp-audit-log.controller'; import { SecurityRuleController } from './controllers/security-rule.controller'; import { SettingController } from './controllers/setting.controller'; import { InfoController } from './controllers/info.controller'; @@ -217,6 +218,7 @@ import { SavedFilters } from './entities/saved-filters.entity'; import { ScheduledJob } from './entities/scheduled-job.entity'; import { AgentSession } from './entities/agent-session.entity'; import { AgentEvent } from './entities/agent-event.entity'; +import { McpAuditLog } from './entities/mcp-audit-log.entity'; import { SecurityRule } from './entities/security-rule.entity'; import { Setting } from './entities/setting.entity'; import { UserActivityHistory } from './entities/user-activity-history.entity'; @@ -295,6 +297,7 @@ import { SavedFiltersRepository } from './repository/saved-filters.repository'; import { ScheduledJobRepository } from './repository/scheduled-job.repository'; import { AgentSessionRepository } from './repository/agent-session.repository'; import { AgentEventRepository } from './repository/agent-event.repository'; +import { McpAuditLogRepository } from './repository/mcp-audit-log.repository'; import { SecurityRuleRepository } from './repository/security-rule.repository'; import { SettingRepository } from './repository/setting.repository'; import { SmsTemplateRepository } from './repository/sms-template.repository'; @@ -344,6 +347,7 @@ import { SavedFiltersService } from './services/saved-filters.service'; import { ScheduledJobService } from './services/scheduled-job.service'; import { AgentSessionService } from './services/agent-session.service'; import { AgentEventService } from './services/agent-event.service'; +import { McpAuditLogService } from './services/mcp-audit-log.service'; import { SchedulerServiceImpl } from './services/scheduled-jobs/scheduler.service'; import { SecurityRuleService } from './services/security-rule.service'; import { ListOfDashboardQuestionProvidersSelectionProvider } from './services/selection-providers/list-of-dashboard-question-providers-selection-provider.service'; @@ -427,6 +431,7 @@ import { Entity } from 'typeorm'; ScheduledJob, AgentSession, AgentEvent, + McpAuditLog, SecurityRule, Setting, SmsTemplate, @@ -505,6 +510,7 @@ import { Entity } from 'typeorm'; ScheduledJobController, AgentSessionController, AgentEventController, + McpAuditLogController, SecurityRuleController, ServiceController, SettingController, @@ -774,8 +780,10 @@ import { Entity } from 'typeorm'; ScheduledJobRepository, AgentSessionRepository, AgentEventRepository, + McpAuditLogRepository, AgentSessionService, AgentEventService, + McpAuditLogService, ScheduledJobSubscriber, AlphaNumExternalIdComputationProvider, ListOfValuesSubscriber, From 659266abd8aa4111ffa03716469bd15ce67e3cbc Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Sat, 23 May 2026 12:02:16 +0530 Subject: [PATCH 044/136] got rid of fieldNamesForRefresh and fieldIdsForRefresh dead chain since it is not used now, due to fields being read in the code builder from the metadata json directly --- src/commands/refresh-model.command.ts | 33 +------------------------- src/interfaces.ts | 2 -- src/services/model-metadata.service.ts | 15 ++---------- 3 files changed, 3 insertions(+), 47 deletions(-) diff --git a/src/commands/refresh-model.command.ts b/src/commands/refresh-model.command.ts index 9c5e2661..8a87a08c 100755 --- a/src/commands/refresh-model.command.ts +++ b/src/commands/refresh-model.command.ts @@ -1,14 +1,11 @@ -import { BadRequestException, forwardRef, Inject, Logger } from '@nestjs/common'; +import { forwardRef, Inject, Logger } from '@nestjs/common'; import { Command, CommandRunner, Option } from 'nest-commander'; import { ModelMetadataService } from 'src/services/model-metadata.service'; import { CommandError } from './helper'; -import { In } from 'typeorm'; interface CommandOptions { name?: string; id?: number; - fieldIds?: number[]; - fieldNames?: string[]; dryRun?: boolean; } @@ -36,8 +33,6 @@ export class RefreshModelCommand extends CommandRunner { modelId: options.id, modelUserKey: options.name, dryRun: options.dryRun, - fieldIdsForRefresh: options.fieldIds, - fieldNamesForRefresh: options.fieldNames, }; await this.modelMetadataService.handleGenerateCode(codeGenerationOptions); } @@ -70,32 +65,6 @@ export class RefreshModelCommand extends CommandRunner { return (val === 'false') ? false : true; } - // Accept field IDs as an argument - @Option({ - flags: '-fids, --fieldIds [Array of field IDs]', - description: 'Json array of Field IDs from the ss_field_metadata table', - }) - parseFieldIds(val: string): number[] { - //Check if the value is a json array - if (!val.startsWith('[') || !val.endsWith(']')) { - throw new BadRequestException('Field IDs should be a json array'); - } - return JSON.parse(val).map((id: string) => parseInt(id)); - } - - // Accept field Names as an argument - @Option({ - flags: '-fnames, --fieldNames [Array of field Names]', - description: 'Json array of Field Names from the ss_field_metadata table', - }) - parseFieldNames(val: string): string[] { - //Check if the value is a json array - if (!val.startsWith('[') || !val.endsWith(']')) { - throw new BadRequestException('Field Names should be a json array'); - } - return JSON.parse(val).map((name: string) => name.toString()); - } - // Validate the options passed private validate(options: CommandOptions): CommandError[] { if (!options.id && !options.name) { diff --git a/src/interfaces.ts b/src/interfaces.ts index c9ca03db..399ab9f5 100755 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -121,8 +121,6 @@ export interface CodeGenerationOptions { fieldIdsForRemoval?: number[]; fieldNamesForRemoval?: string[]; dryRun?: boolean; - fieldIdsForRefresh?: number[]; - fieldNamesForRefresh?: string[]; } export interface TriggerMcpClientOptions { diff --git a/src/services/model-metadata.service.ts b/src/services/model-metadata.service.ts index 5d458377..d37e8313 100755 --- a/src/services/model-metadata.service.ts +++ b/src/services/model-metadata.service.ts @@ -1275,24 +1275,13 @@ export class ModelMetadataService { }; const model = options.modelId ? await this.findOne(options.modelId, query) : await this.findOneByUserKey(options.modelUserKey, query.populate); - let fieldsForRefresh = model.fields.filter((field) => !field.isMarkedForRemoval); - - // If a list of field ids or field names is passed for refresh, use these fields only - if (options.fieldIdsForRefresh && options.fieldIdsForRefresh.length > 0) { - fieldsForRefresh = fieldsForRefresh.filter((field) => options.fieldIdsForRefresh.includes(+field.id)); - } else if (options.fieldNamesForRefresh && options.fieldNamesForRefresh.length > 0) { - fieldsForRefresh = fieldsForRefresh.filter((field) => options.fieldNamesForRefresh.includes(field.name)); - } - // const fieldsForRefresh = model.fields.filter((field) => !field.isMarkedForRemoval); - //Execute the schematic command to refresh the model - const refreshOuput = await this.executeRefreshModelCommand(model, fieldsForRefresh, options.dryRun); + const refreshOuput = await this.executeRefreshModelCommand(model, options.dryRun); return `${refreshOuput}`; } - private async executeRefreshModelCommand(model: ModelMetadata, fieldsForRefresh: FieldMetadata[], dryRun: boolean = false): Promise { - // const fieldsForRefresh = model.fields.filter((field) => !field.isMarkedForRemoval); + private async executeRefreshModelCommand(model: ModelMetadata, dryRun: boolean = false): Promise { const output = await this.schematicService.executeSchematicCommand( REFRESH_MODEL_COMMAND, { From 6d5c476d926ecc0e4871d482630cfbdddf9d6ff0 Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Sat, 23 May 2026 12:58:44 +0530 Subject: [PATCH 045/136] changes to route genertion commands thru solidctl. The refresh-module & refresh-model commands are what are eventually called and end up calling the services only, keeping calling consistent and ensuring everything is route thru solidctl generate --- src/controllers/model-metadata.controller.ts | 2 +- src/controllers/module-metadata.controller.ts | 2 +- src/helpers/command.service.ts | 2 ++ src/services/model-metadata.service.ts | 11 +++++++++++ src/services/module-metadata.service.ts | 11 +++++++++++ 5 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/controllers/model-metadata.controller.ts b/src/controllers/model-metadata.controller.ts index c124eed2..8ccde7e2 100755 --- a/src/controllers/model-metadata.controller.ts +++ b/src/controllers/model-metadata.controller.ts @@ -92,7 +92,7 @@ export class ModelMetadataController { @ApiBearerAuth("jwt") @Post(':id/generate-code') generateCode(@Param('id', ParseIntPipe) id: number) { - return this.modelMetadataService.handleGenerateCode({ modelId: id }); + return this.modelMetadataService.generateCodeViaCtl(id); } @ApiBearerAuth("jwt") diff --git a/src/controllers/module-metadata.controller.ts b/src/controllers/module-metadata.controller.ts index 1f3c2b40..e5d3120d 100755 --- a/src/controllers/module-metadata.controller.ts +++ b/src/controllers/module-metadata.controller.ts @@ -62,7 +62,7 @@ export class ModuleMetadataController { @ApiBearerAuth("jwt") @Post(':id/generate-code') generateCode(@Param('id', ParseIntPipe) id: number) { - return this.moduleMetadataService.generateCode({ moduleId: id }); + return this.moduleMetadataService.generateCodeViaCtl(id); } diff --git a/src/helpers/command.service.ts b/src/helpers/command.service.ts index df332ca8..7bc3afcb 100755 --- a/src/helpers/command.service.ts +++ b/src/helpers/command.service.ts @@ -4,6 +4,7 @@ import { spawn } from 'child_process'; export type CommandWithArgs = { command: string; args: string[]; + cwd?: string; }; @Injectable() @@ -43,6 +44,7 @@ export class CommandService { const child = spawn(command, spawnArgs, { shell: isWindows, // Use shell on Windows to handle .cmd files stdio: ['pipe', 'pipe', 'pipe'], + cwd: commandWithArgs.cwd, }); let stdout = ''; diff --git a/src/services/model-metadata.service.ts b/src/services/model-metadata.service.ts index d37e8313..61568b3a 100755 --- a/src/services/model-metadata.service.ts +++ b/src/services/model-metadata.service.ts @@ -27,6 +27,7 @@ import { REMOVE_FIELDS_COMMAND, SchematicService } from '../helpers/schematic.service'; +import { CommandService } from '../helpers/command.service'; import { CodeGenerationOptions } from '../interfaces'; import { CrudHelperService } from './crud-helper.service'; import { FieldMetadataService } from './field-metadata.service'; @@ -49,6 +50,7 @@ export class ModelMetadataService { private readonly modelMetadataRepo: ModelMetadataRepository, private readonly fieldMetadataRepo: FieldMetadataRepository, private readonly schematicService: SchematicService, + private readonly commandService: CommandService, @InjectDataSource() private readonly dataSource: DataSource, private readonly crudHelperService: CrudHelperService, @@ -735,6 +737,15 @@ export class ModelMetadataService { } + @DisallowInProduction() + async generateCodeViaCtl(modelId: number): Promise { + return this.commandService.executeCommandWithArgs({ + command: 'npx', + args: ['@solixai/solidctl@latest', 'generate', 'model', `--id=${modelId}`], + cwd: path.join(process.cwd(), '..'), + }); + } + @DisallowInProduction() async handleGenerateCode(options: CodeGenerationOptions): Promise { const affectedModelIds = [], refreshModelCodeOutputLines = [], removeFieldCodeOutputLines = []; diff --git a/src/services/module-metadata.service.ts b/src/services/module-metadata.service.ts index 0e5a8c16..1c2bba4c 100755 --- a/src/services/module-metadata.service.ts +++ b/src/services/module-metadata.service.ts @@ -22,6 +22,7 @@ import { ADD_MODULE_COMMAND, SchematicService, } from '../helpers/schematic.service'; +import { CommandService } from '../helpers/command.service'; import { SolidRegistry } from '../helpers/solid-registry'; import { CodeGenerationOptions, ModuleMetadataConfiguration } from '../interfaces'; import { CrudHelperService } from './crud-helper.service'; @@ -40,6 +41,7 @@ export class ModuleMetadataService { private readonly moduleMetadataRepo: ModuleMetadataRepository, private readonly crudHelperService: CrudHelperService, private readonly schematicService: SchematicService, + private readonly commandService: CommandService, private readonly fileService: DiskFileService, private readonly settingService: SettingService, @@ -403,6 +405,15 @@ export class ModuleMetadataService { return true } + @DisallowInProduction() + async generateCodeViaCtl(moduleId: number): Promise { + return this.commandService.executeCommandWithArgs({ + command: 'npx', + args: ['@solixai/solidctl@latest', 'generate', 'module', `--id=${moduleId}`], + cwd: path.join(process.cwd(), '..'), + }); + } + @DisallowInProduction() async generateCode(options: CodeGenerationOptions): Promise { if (!options.moduleId && !options.moduleUserKey) { From cdb78796b0ff91a32693ea946443d8b01afb2179 Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Sat, 23 May 2026 13:01:08 +0530 Subject: [PATCH 046/136] 0.1.10-beta.11 --- package-lock.json | 4 ++-- package.json | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6e324fb9..bee2cb6f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.10", + "version": "0.1.10-beta.11", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@solidxai/core", - "version": "0.1.10-beta.10", + "version": "0.1.10-beta.11", "license": "BUSL-1.1", "dependencies": { "@aws-sdk/client-s3": "^3.637.0", diff --git a/package.json b/package.json index 03fdd672..bc7d086b 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.10", + "version": "0.1.10-beta.11", "description": "This module is a NestJS module containing all the required core providers required by a Solid application", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -82,9 +82,7 @@ "xlsx": "^0.18.5", "playwright": ">=1.0.0" }, - "peerDependenciesMeta": { - - }, + "peerDependenciesMeta": {}, "peerDependencies": { "@nestjs/axios": "^3.0.2", "@nestjs/cache-manager": "^2.2.2", From a6a97ec33bec38b9e7f926f3180b34fb0f2bba91 Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Sat, 23 May 2026 13:25:40 +0530 Subject: [PATCH 047/136] removed id command options and retained the singular name option only for consistency purposes --- src/commands/refresh-model.command.ts | 24 +++++------------------- src/commands/refresh-module.command.ts | 19 +++---------------- src/services/model-metadata.service.ts | 3 ++- src/services/module-metadata.service.ts | 3 ++- 4 files changed, 12 insertions(+), 37 deletions(-) diff --git a/src/commands/refresh-model.command.ts b/src/commands/refresh-model.command.ts index 8a87a08c..983d6d97 100755 --- a/src/commands/refresh-model.command.ts +++ b/src/commands/refresh-model.command.ts @@ -4,8 +4,7 @@ import { ModelMetadataService } from 'src/services/model-metadata.service'; import { CommandError } from './helper'; interface CommandOptions { - name?: string; - id?: number; + name: string; dryRun?: boolean; } @@ -30,32 +29,20 @@ export class RefreshModelCommand extends CommandRunner { } const codeGenerationOptions = { - modelId: options.id, modelUserKey: options.name, dryRun: options.dryRun, }; await this.modelMetadataService.handleGenerateCode(codeGenerationOptions); } - // Accept the model ID as an argument @Option({ - flags: '-i, --id [model ID]', - description: 'Model ID from the ss_model_metadata table', - }) - parseId(val: string): number { - return +val; - } - - // Accept the module name as an argument - @Option({ - flags: '-n, --name [model name]', - description: 'Model Name from the ss_model_metadata table', + flags: '-n, --name ', + description: 'Model name (singularName) from the ss_model_metadata table', }) parseName(val: string): string { return val; } - // Accept dry run as an argument @Option({ flags: '-d, --dryRun [dry run]', description: 'Dry run the command', @@ -65,10 +52,9 @@ export class RefreshModelCommand extends CommandRunner { return (val === 'false') ? false : true; } - // Validate the options passed private validate(options: CommandOptions): CommandError[] { - if (!options.id && !options.name) { - return [new CommandError('Model ID or Model Name is required')]; + if (!options.name) { + return [new CommandError('Model Name is required')]; } return []; } diff --git a/src/commands/refresh-module.command.ts b/src/commands/refresh-module.command.ts index 1583c66d..91390b44 100755 --- a/src/commands/refresh-module.command.ts +++ b/src/commands/refresh-module.command.ts @@ -4,7 +4,6 @@ import { ModuleMetadataService } from '../services/module-metadata.service'; import { CommandError } from './helper'; interface CommandOptions { - id: number; name: string; dryRun: boolean; } @@ -28,7 +27,6 @@ export class RefreshModuleCommand extends CommandRunner { return; } const codeGenerationOptions = { - moduleId: options.id, moduleUserKey: options.name, dryRun: options.dryRun, }; @@ -36,16 +34,7 @@ export class RefreshModuleCommand extends CommandRunner { } @Option({ - flags: '-i, --id [module ID]', - description: 'Module ID from the ss_module_metadata table', - }) - parseId(val: string): number { - return +val; - } - - // Accept the module name as an argument - @Option({ - flags: '-n, --name [module name]', + flags: '-n, --name ', description: 'Module Name from the ss_module_metadata table', }) parseName(val: string): string { @@ -61,11 +50,9 @@ export class RefreshModuleCommand extends CommandRunner { return (val === 'false') ? false : true; } - - // Validate the options passed validate(options: CommandOptions): CommandError[] { - if (!options.id && !options.name) { - return [new CommandError('Module ID or Module Name is required')]; + if (!options.name) { + return [new CommandError('Module Name is required')]; } return []; } diff --git a/src/services/model-metadata.service.ts b/src/services/model-metadata.service.ts index 61568b3a..b0fb0b1d 100755 --- a/src/services/model-metadata.service.ts +++ b/src/services/model-metadata.service.ts @@ -739,9 +739,10 @@ export class ModelMetadataService { @DisallowInProduction() async generateCodeViaCtl(modelId: number): Promise { + const model = await this.findOne(modelId); return this.commandService.executeCommandWithArgs({ command: 'npx', - args: ['@solixai/solidctl@latest', 'generate', 'model', `--id=${modelId}`], + args: ['@solixai/solidctl@latest', 'generate', 'model', `--name=${model.singularName}`], cwd: path.join(process.cwd(), '..'), }); } diff --git a/src/services/module-metadata.service.ts b/src/services/module-metadata.service.ts index 1c2bba4c..2884afb9 100755 --- a/src/services/module-metadata.service.ts +++ b/src/services/module-metadata.service.ts @@ -407,9 +407,10 @@ export class ModuleMetadataService { @DisallowInProduction() async generateCodeViaCtl(moduleId: number): Promise { + const module = await this.findOne(moduleId); return this.commandService.executeCommandWithArgs({ command: 'npx', - args: ['@solixai/solidctl@latest', 'generate', 'module', `--id=${moduleId}`], + args: ['@solixai/solidctl@latest', 'generate', 'module', `--name=${module.name}`], cwd: path.join(process.cwd(), '..'), }); } From f0c6d2ff0ffc29dd63ab36925eed40e7a6261a04 Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Sat, 23 May 2026 13:31:49 +0530 Subject: [PATCH 048/136] 0.1.10-beta.12 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index bee2cb6f..22d1327f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.11", + "version": "0.1.10-beta.12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@solidxai/core", - "version": "0.1.10-beta.11", + "version": "0.1.10-beta.12", "license": "BUSL-1.1", "dependencies": { "@aws-sdk/client-s3": "^3.637.0", diff --git a/package.json b/package.json index bc7d086b..0fdf4a1b 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.11", + "version": "0.1.10-beta.12", "description": "This module is a NestJS module containing all the required core providers required by a Solid application", "main": "dist/index.js", "types": "dist/index.d.ts", From 6a70db55be131bcd5f9c6c83ffa0dc09dc3e4123 Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Mon, 25 May 2026 13:16:45 +0530 Subject: [PATCH 049/136] changes to avoid too many initialization logs. controlled thru the verboseBootstrap option to the boostrap helper --- src/helpers/bootstrap.helper.ts | 19 +++++++++++++------ src/winston.logger.ts | 12 +++++------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/helpers/bootstrap.helper.ts b/src/helpers/bootstrap.helper.ts index f8f687ec..9b63cd16 100644 --- a/src/helpers/bootstrap.helper.ts +++ b/src/helpers/bootstrap.helper.ts @@ -9,7 +9,7 @@ import { existsSync } from 'fs'; import { resolve } from 'path'; import { WINSTON_MODULE_NEST_PROVIDER, WinstonModule } from 'nest-winston'; import { CommandFactory } from 'nest-commander'; -import { WinstonLoggerConfig } from '../winston.logger'; +import { createWinstonLoggerConfig } from '../winston.logger'; import { WrapResponseInterceptor } from '../interceptors/wrap-response.interceptor'; import { buildDefaultCorsOptions } from './cors.helper'; import { buildDefaultSecurityHeaderOptions, buildPermissionsPolicyHeader, PermissionsPolicyConfig } from './security.helper'; @@ -50,6 +50,8 @@ export interface SolidBootstrapOptions { swagger?: SolidSwaggerOptions | false; /** Permissions-Policy header overrides (merged with defaults). */ permissionsPolicyOverrides?: Partial; + /** Show full NestJS init logs during bootstrap (route mapping, module deps, pollers). Defaults to false. */ + verboseBootstrap?: boolean; } /** @@ -70,11 +72,12 @@ export async function bootstrapSolidApp( ): Promise { registerGlobalProcessHandlers(); - const { globalPrefix = 'api', swagger = {}, permissionsPolicyOverrides = {} } = options; + const { globalPrefix = 'api', swagger = {}, permissionsPolicyOverrides = {}, verboseBootstrap = false } = options; + const startTime = Date.now(); const appModule = await appModuleFactory(); const app = await NestFactory.create(appModule, { - logger: WinstonModule.createLogger(WinstonLoggerConfig), + logger: WinstonModule.createLogger({ ...createWinstonLoggerConfig(), level: verboseBootstrap ? 'debug' : 'error' }), }); const apiEnabled = parseBooleanEnv('API_ENABLED', true); @@ -115,9 +118,6 @@ export async function bootstrapSolidApp( next(); }); - // Winston logger - app.useLogger(app.get(WINSTON_MODULE_NEST_PROVIDER)); - const port = process.env.PORT || 3000; if (globalPrefix) { @@ -182,6 +182,13 @@ export async function bootstrapSolidApp( app.useWebSocketAdapter(new WsAdapter(app)); await app.listen(port); + + // Wire up Winston as the runtime logger only AFTER listen — this suppresses all + // framework init noise (route mapping, module deps, onModuleInit) during boot. + app.useLogger(app.get(WINSTON_MODULE_NEST_PROVIDER)); + + const elapsed = ((Date.now() - startTime) / 1000).toFixed(2); + app.get(WINSTON_MODULE_NEST_PROVIDER).log(`Server started on port ${port} in ${elapsed}s`, 'Bootstrap'); } // ---- CLI bootstrap ---- diff --git a/src/winston.logger.ts b/src/winston.logger.ts index f9adc378..53c4fb55 100644 --- a/src/winston.logger.ts +++ b/src/winston.logger.ts @@ -6,8 +6,8 @@ import { WINSTON_MODULE_PROVIDER } from 'nest-winston'; import * as winston from 'winston'; import { Environment } from './decorators/disallow-in-production.decorator'; -export const WinstonLoggerConfig = { - level: process.env.LOG_LEVEL || (process.env.ENV === Environment.Production ? 'info' : 'debug'), +export const createWinstonLoggerConfig = () => ({ + level: process.env.LOG_LEVEL || (process.env.ENV === Environment.Production ? 'warn' : 'info'), format: winston.format.combine( winston.format.timestamp(), winston.format.errors({ stack: true }), @@ -21,8 +21,6 @@ export const WinstonLoggerConfig = { transports: [ new winston.transports.Console({ format: winston.format.combine( - // winston.format.colorize(), - // winston.format.timestamp(), winston.format.printf(({ level, message, timestamp }) => { return `[${timestamp}] ${level.toUpperCase()}: ${message}`; }), @@ -30,15 +28,15 @@ export const WinstonLoggerConfig = { }), new winston.transports.File({ filename: 'logs/application.log', - // format: winston.format.json(), }), new winston.transports.File({ filename: 'logs/error.log', level: 'error', - // format: winston.format.json(), }), ], -}; +}); + +export const WinstonLoggerConfig = createWinstonLoggerConfig(); export class WinstonTypeORMLogger implements TypeORMLogger { constructor(@Inject(WINSTON_MODULE_PROVIDER) private readonly logger: Logger) { } From 58de1823a0e732cbdef8762bfe7c475403471261 Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Mon, 25 May 2026 13:23:40 +0530 Subject: [PATCH 050/136] 0.1.10-beta.13 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 22d1327f..33b28939 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.12", + "version": "0.1.10-beta.13", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@solidxai/core", - "version": "0.1.10-beta.12", + "version": "0.1.10-beta.13", "license": "BUSL-1.1", "dependencies": { "@aws-sdk/client-s3": "^3.637.0", diff --git a/package.json b/package.json index 0fdf4a1b..acabdf65 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.12", + "version": "0.1.10-beta.13", "description": "This module is a NestJS module containing all the required core providers required by a Solid application", "main": "dist/index.js", "types": "dist/index.d.ts", From 8cd5284fdc4d2d08468a9df270645bf4d7c77b53 Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Mon, 25 May 2026 13:24:30 +0530 Subject: [PATCH 051/136] 0.1.10-beta.14 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 33b28939..cdaec0d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.13", + "version": "0.1.10-beta.14", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@solidxai/core", - "version": "0.1.10-beta.13", + "version": "0.1.10-beta.14", "license": "BUSL-1.1", "dependencies": { "@aws-sdk/client-s3": "^3.637.0", diff --git a/package.json b/package.json index acabdf65..7e28387f 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.13", + "version": "0.1.10-beta.14", "description": "This module is a NestJS module containing all the required core providers required by a Solid application", "main": "dist/index.js", "types": "dist/index.d.ts", From 07c5a3d91899703fc6cbc4d2e005d78c8aebf491 Mon Sep 17 00:00:00 2001 From: Rajesh Chityal Date: Mon, 25 May 2026 15:50:31 +0530 Subject: [PATCH 052/136] Agent table clean up --- .../seed-data/solid-core-metadata.json | 240 ++++++++++++------ 1 file changed, 159 insertions(+), 81 deletions(-) diff --git a/src/seeders/seed-data/solid-core-metadata.json b/src/seeders/seed-data/solid-core-metadata.json index 72fccb8a..d10642c0 100755 --- a/src/seeders/seed-data/solid-core-metadata.json +++ b/src/seeders/seed-data/solid-core-metadata.json @@ -13999,34 +13999,16 @@ "children": [ { "type": "column", - "attrs": { "name": "col-1", "label": "", "className": "col-12 sm:col-12 md:col-6 lg:col-6" }, + "attrs": { "name": "col-1", "label": "", "className": "col-12" }, "children": [ { "type": "field", "attrs": { "name": "sessionId" } }, { "type": "field", "attrs": { "name": "modelName" } }, { "type": "field", "attrs": { "name": "status" } }, - { "type": "field", "attrs": { "name": "projectRoot" } } - ] - }, - { - "type": "column", - "attrs": { "name": "col-2", "label": "", "className": "col-12 sm:col-12 md:col-6 lg:col-6" }, - "children": [ + { "type": "field", "attrs": { "name": "projectRoot" } }, { "type": "field", "attrs": { "name": "totalSteps" } }, { "type": "field", "attrs": { "name": "totalCost" } }, { "type": "field", "attrs": { "name": "totalInputTokens" } }, - { "type": "field", "attrs": { "name": "totalOutputTokens" } } - ] - } - ] - }, - { - "type": "row", - "attrs": { "name": "row-2" }, - "children": [ - { - "type": "column", - "attrs": { "name": "col-summary", "label": "", "className": "col-12" }, - "children": [ + { "type": "field", "attrs": { "name": "totalOutputTokens" } }, { "type": "field", "attrs": { "name": "summary" } } ] } @@ -14090,46 +14072,94 @@ "attrs": { "name": "sheet-1" }, "children": [ { - "type": "row", - "attrs": { "name": "row-1" }, + "type": "notebook", + "attrs": { "name": "notebook-1" }, "children": [ { - "type": "column", - "attrs": { "name": "col-1", "label": "", "className": "col-12 sm:col-12 md:col-6 lg:col-6" }, + "type": "page", + "attrs": { "name": "page-general", "label": "General" }, "children": [ - { "type": "field", "attrs": { "name": "sessionId" } }, - { "type": "field", "attrs": { "name": "eventType" } }, - { "type": "field", "attrs": { "name": "turnNumber" } }, - { "type": "field", "attrs": { "name": "stepNumber" } }, - { "type": "field", "attrs": { "name": "toolName" } }, - { "type": "field", "attrs": { "name": "modelUsed" } } + { + "type": "row", + "attrs": { "name": "page-general-row-1" }, + "children": [ + { + "type": "column", + "attrs": { "name": "page-general-col-1", "label": "", "className": "col-12" }, + "children": [ + { "type": "field", "attrs": { "name": "sessionId" } }, + { "type": "field", "attrs": { "name": "eventType" } }, + { "type": "field", "attrs": { "name": "turnNumber" } }, + { "type": "field", "attrs": { "name": "stepNumber" } }, + { "type": "field", "attrs": { "name": "toolName" } }, + { "type": "field", "attrs": { "name": "modelUsed" } }, + { "type": "field", "attrs": { "name": "durationMs" } }, + { "type": "field", "attrs": { "name": "cost" } }, + { "type": "field", "attrs": { "name": "inputTokens" } }, + { "type": "field", "attrs": { "name": "outputTokens" } }, + { "type": "field", "attrs": { "name": "toolReturncode" } }, + { "type": "field", "attrs": { "name": "content" } } + ] + } + ] + } ] }, { - "type": "column", - "attrs": { "name": "col-2", "label": "", "className": "col-12 sm:col-12 md:col-6 lg:col-6" }, + "type": "page", + "attrs": { "name": "page-tool-arguments", "label": "Tool Arguments" }, "children": [ - { "type": "field", "attrs": { "name": "durationMs" } }, - { "type": "field", "attrs": { "name": "cost" } }, - { "type": "field", "attrs": { "name": "inputTokens" } }, - { "type": "field", "attrs": { "name": "outputTokens" } }, - { "type": "field", "attrs": { "name": "toolReturncode" } } + { + "type": "row", + "attrs": { "name": "page-tool-arguments-row-1" }, + "children": [ + { + "type": "column", + "attrs": { "name": "page-tool-arguments-col-1", "label": "", "className": "col-12" }, + "children": [ + { "type": "field", "attrs": { "name": "toolArguments", "viewWidget": "SolidJsonFormViewWidget" } } + ] + } + ] + } ] - } - ] - }, - { - "type": "row", - "attrs": { "name": "row-2" }, - "children": [ + }, { - "type": "column", - "attrs": { "name": "col-content", "label": "", "className": "col-12" }, + "type": "page", + "attrs": { "name": "page-tool-output", "label": "Tool Output" }, "children": [ - { "type": "field", "attrs": { "name": "content" } }, - { "type": "field", "attrs": { "name": "toolArguments", "viewWidget": "SolidJsonFormViewWidget" } }, - { "type": "field", "attrs": { "name": "toolOutput", "viewWidget": "SolidJsonFormViewWidget" } }, - { "type": "field", "attrs": { "name": "eventData", "viewWidget": "SolidJsonFormViewWidget" } } + { + "type": "row", + "attrs": { "name": "page-tool-output-row-1" }, + "children": [ + { + "type": "column", + "attrs": { "name": "page-tool-output-col-1", "label": "", "className": "col-12" }, + "children": [ + { "type": "field", "attrs": { "name": "toolOutput", "viewWidget": "SolidJsonFormViewWidget" } } + ] + } + ] + } + ] + }, + { + "type": "page", + "attrs": { "name": "page-event-data", "label": "Event Data" }, + "children": [ + { + "type": "row", + "attrs": { "name": "page-event-data-row-1" }, + "children": [ + { + "type": "column", + "attrs": { "name": "page-event-data-col-1", "label": "", "className": "col-12" }, + "children": [ + { "type": "field", "attrs": { "name": "eventData", "viewWidget": "SolidJsonFormViewWidget" } } + ] + } + ] + } ] } ] @@ -14192,46 +14222,94 @@ "attrs": { "name": "sheet-1" }, "children": [ { - "type": "row", - "attrs": { "name": "row-1" }, + "type": "notebook", + "attrs": { "name": "notebook-1" }, "children": [ { - "type": "column", - "attrs": { "name": "col-1", "label": "", "className": "col-12 sm:col-12 md:col-6 lg:col-6" }, + "type": "page", + "attrs": { "name": "page-general", "label": "General" }, "children": [ - { "type": "field", "attrs": { "name": "method" } }, - { "type": "field", "attrs": { "name": "toolName" } }, - { "type": "field", "attrs": { "name": "status" } }, - { "type": "field", "attrs": { "name": "transport" } }, - { "type": "field", "attrs": { "name": "mcpSessionId" } }, - { "type": "field", "attrs": { "name": "requestId" } } + { + "type": "row", + "attrs": { "name": "page-general-row-1" }, + "children": [ + { + "type": "column", + "attrs": { "name": "page-general-col-1", "label": "", "className": "col-12" }, + "children": [ + { "type": "field", "attrs": { "name": "method" } }, + { "type": "field", "attrs": { "name": "toolName" } }, + { "type": "field", "attrs": { "name": "status" } }, + { "type": "field", "attrs": { "name": "transport" } }, + { "type": "field", "attrs": { "name": "mcpSessionId" } }, + { "type": "field", "attrs": { "name": "requestId" } }, + { "type": "field", "attrs": { "name": "userId" } }, + { "type": "field", "attrs": { "name": "apiKeyId" } }, + { "type": "field", "attrs": { "name": "username" } }, + { "type": "field", "attrs": { "name": "clientAddr" } }, + { "type": "field", "attrs": { "name": "durationMs" } }, + { "type": "field", "attrs": { "name": "errorCode" } } + ] + } + ] + } ] }, { - "type": "column", - "attrs": { "name": "col-2", "label": "", "className": "col-12 sm:col-12 md:col-6 lg:col-6" }, + "type": "page", + "attrs": { "name": "page-request-params", "label": "Request Params" }, "children": [ - { "type": "field", "attrs": { "name": "userId" } }, - { "type": "field", "attrs": { "name": "apiKeyId" } }, - { "type": "field", "attrs": { "name": "username" } }, - { "type": "field", "attrs": { "name": "clientAddr" } }, - { "type": "field", "attrs": { "name": "durationMs" } }, - { "type": "field", "attrs": { "name": "errorCode" } } + { + "type": "row", + "attrs": { "name": "page-request-params-row-1" }, + "children": [ + { + "type": "column", + "attrs": { "name": "page-request-params-col-1", "label": "", "className": "col-12" }, + "children": [ + { "type": "field", "attrs": { "name": "requestParams", "viewWidget": "SolidJsonFormViewWidget" } } + ] + } + ] + } ] - } - ] - }, - { - "type": "row", - "attrs": { "name": "row-2" }, - "children": [ + }, { - "type": "column", - "attrs": { "name": "col-payload", "label": "", "className": "col-12" }, + "type": "page", + "attrs": { "name": "page-response-result", "label": "Response Result" }, + "children": [ + { + "type": "row", + "attrs": { "name": "page-response-result-row-1" }, + "children": [ + { + "type": "column", + "attrs": { "name": "page-response-result-col-1", "label": "", "className": "col-12" }, + "children": [ + { "type": "field", "attrs": { "name": "responseResult", "viewWidget": "SolidJsonFormViewWidget" } } + ] + } + ] + } + ] + }, + { + "type": "page", + "attrs": { "name": "page-error-message", "label": "Error Message" }, "children": [ - { "type": "field", "attrs": { "name": "requestParams", "viewWidget": "SolidJsonFormViewWidget" } }, - { "type": "field", "attrs": { "name": "responseResult", "viewWidget": "SolidJsonFormViewWidget" } }, - { "type": "field", "attrs": { "name": "errorMessage" } } + { + "type": "row", + "attrs": { "name": "page-error-message-row-1" }, + "children": [ + { + "type": "column", + "attrs": { "name": "page-error-message-col-1", "label": "", "className": "col-12" }, + "children": [ + { "type": "field", "attrs": { "name": "errorMessage" } } + ] + } + ] + } ] } ] From 866db1e0329a81d903ca4d2822ebaef89812441a Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Mon, 25 May 2026 18:32:03 +0530 Subject: [PATCH 053/136] changes to colour the server startup line --- package-lock.json | 12 +----------- src/helpers/bootstrap.helper.ts | 2 +- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/package-lock.json b/package-lock.json index cdaec0d9..79b38225 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,6 +49,7 @@ "passport-local": "^1.0.0", "passport-microsoft": "^2.1.0", "pg": "^8.11.3", + "playwright": ">=1.0.0", "pluralize": "^8.0.0", "puppeteer": "^23.2.0", "qs": "^6.12.0", @@ -145,16 +146,10 @@ "nest-commander": "^3.12.5", "nest-winston": "^1.9.7", "nestjs-cls": "^5.4.3", - "playwright": ">=1.0.0", "reflect-metadata": "^0.2.2", "typeorm": "^0.3.20", "typeorm-naming-strategies": "^4.1.0", "winston": "^3.17.0" - }, - "peerDependenciesMeta": { - "playwright": { - "optional": true - } } }, "node_modules/@angular-devkit/core": { @@ -14569,8 +14564,6 @@ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", "license": "Apache-2.0", - "optional": true, - "peer": true, "dependencies": { "playwright-core": "1.59.1" }, @@ -14589,8 +14582,6 @@ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", "license": "Apache-2.0", - "optional": true, - "peer": true, "bin": { "playwright-core": "cli.js" }, @@ -14608,7 +14599,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } diff --git a/src/helpers/bootstrap.helper.ts b/src/helpers/bootstrap.helper.ts index 9b63cd16..a540779e 100644 --- a/src/helpers/bootstrap.helper.ts +++ b/src/helpers/bootstrap.helper.ts @@ -188,7 +188,7 @@ export async function bootstrapSolidApp( app.useLogger(app.get(WINSTON_MODULE_NEST_PROVIDER)); const elapsed = ((Date.now() - startTime) / 1000).toFixed(2); - app.get(WINSTON_MODULE_NEST_PROVIDER).log(`Server started on port ${port} in ${elapsed}s`, 'Bootstrap'); + app.get(WINSTON_MODULE_NEST_PROVIDER).log(`\x1b[32mServer started on port ${port} in ${elapsed}s\x1b[0m`, 'Bootstrap'); } // ---- CLI bootstrap ---- From f140c78aaca78754e70eef7c568ca1af58d238c7 Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Mon, 25 May 2026 18:32:19 +0530 Subject: [PATCH 054/136] 0.1.10-beta.15 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 79b38225..287a6438 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.14", + "version": "0.1.10-beta.15", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@solidxai/core", - "version": "0.1.10-beta.14", + "version": "0.1.10-beta.15", "license": "BUSL-1.1", "dependencies": { "@aws-sdk/client-s3": "^3.637.0", diff --git a/package.json b/package.json index 7e28387f..a2aaacc0 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.14", + "version": "0.1.10-beta.15", "description": "This module is a NestJS module containing all the required core providers required by a Solid application", "main": "dist/index.js", "types": "dist/index.d.ts", From 2e3883296592196be6c156e65b31dd0a208db2a0 Mon Sep 17 00:00:00 2001 From: sundaram Date: Wed, 27 May 2026 15:22:12 +0530 Subject: [PATCH 055/136] feat(chatter): add secure custom-note edit API with attachment add/remove support --- src/controllers/chatter-message.controller.ts | 12 ++ src/dtos/update-chatter-note-message.dto.ts | 14 ++ src/index.ts | 1 + src/services/chatter-message.service.ts | 136 +++++++++++++++++- 4 files changed, 161 insertions(+), 2 deletions(-) create mode 100644 src/dtos/update-chatter-note-message.dto.ts diff --git a/src/controllers/chatter-message.controller.ts b/src/controllers/chatter-message.controller.ts index 48bfe2aa..361b1feb 100644 --- a/src/controllers/chatter-message.controller.ts +++ b/src/controllers/chatter-message.controller.ts @@ -5,6 +5,7 @@ import { ChatterMessageService } from '../services/chatter-message.service'; import { CreateChatterMessageDto } from '../dtos/create-chatter-message.dto'; import { UpdateChatterMessageDto } from '../dtos/update-chatter-message.dto'; import { PostChatterMessageDto } from '../dtos/post-chatter-message.dto'; +import { UpdateChatterNoteMessageDto } from '../dtos/update-chatter-note-message.dto'; import { SolidRequestContextDecorator } from 'src/decorators/solid-request-context.decorator'; import { SolidRequestContextDto } from 'src/dtos/solid-request-context.dto'; import { Public } from 'src/decorators/public.decorator'; @@ -127,4 +128,15 @@ export class ChatterMessageController { async markCompleted(@Param('id') id: string) { return this.service.markCompleted(+id); } + + @ApiBearerAuth("jwt") + @Patch(':id/note') + @UseInterceptors(AnyFilesInterceptor()) + async updateCustomNoteMessage( + @Param('id') id: string, + @Body() updateDto: UpdateChatterNoteMessageDto, + @UploadedFiles() files: Array, + ) { + return this.service.updateCustomNoteMessage(+id, updateDto, files); + } } diff --git a/src/dtos/update-chatter-note-message.dto.ts b/src/dtos/update-chatter-note-message.dto.ts new file mode 100644 index 00000000..0086adeb --- /dev/null +++ b/src/dtos/update-chatter-note-message.dto.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; + +export class UpdateChatterNoteMessageDto { + @IsString() + @IsOptional() + @ApiProperty() + messageBody: string; + + @IsString() + @IsOptional() + @ApiProperty({ required: false, description: 'Comma-separated media IDs to remove from this note.' }) + removeAttachmentIds?: string; +} diff --git a/src/index.ts b/src/index.ts index 9a63c27d..a3f3b42b 100755 --- a/src/index.ts +++ b/src/index.ts @@ -29,6 +29,7 @@ export * from './decorators/settings-provider.decorator' export * from './decorators/extension-user-creation-provider.decorator' export * from './dtos/post-chatter-message.dto' +export * from './dtos/update-chatter-note-message.dto' export * from './dtos/security-rule-config.dto' export * from './dtos/basic-filters.dto' export * from './dtos/solid-request-context.dto' diff --git a/src/services/chatter-message.service.ts b/src/services/chatter-message.service.ts index 6a8f3b1c..0be2c379 100644 --- a/src/services/chatter-message.service.ts +++ b/src/services/chatter-message.service.ts @@ -1,18 +1,21 @@ import { LocalDateTimeTransformer, serializeDate } from 'src/transformers/typeorm/local-date-time-transformer'; -import { BadRequestException, forwardRef, Inject, Injectable, NotFoundException } from '@nestjs/common'; +import { BadRequestException, ForbiddenException, forwardRef, Inject, Injectable, NotFoundException } from '@nestjs/common'; import { ModuleRef } from "@nestjs/core"; import { InjectEntityManager } from '@nestjs/typeorm'; -import { Brackets, EntityManager, EntityMetadata } from 'typeorm'; +import { Brackets, EntityManager, EntityMetadata, In } from 'typeorm'; +import * as path from 'path'; import { classify } from '@angular-devkit/core/src/utils/strings'; import { CHATTER_MESSAGE_STATUS, CHATTER_MESSAGE_SUBTYPE, CHATTER_MESSAGE_TYPE } from 'src/constants/chatter-message.constants'; import { ERROR_MESSAGES } from 'src/constants/error-messages'; import { PostChatterMessageDto } from 'src/dtos/post-chatter-message.dto'; +import { UpdateChatterNoteMessageDto } from 'src/dtos/update-chatter-note-message.dto'; import { ModelMetadataHelperService } from 'src/helpers/model-metadata-helper.service'; import { lowerFirst } from 'src/helpers/string.helper'; import { ChatterMessageDetailsRepository } from 'src/repository/chatter-message-details.repository'; import { ChatterMessageRepository } from 'src/repository/chatter-message.repository'; import { FieldMetadataRepository } from 'src/repository/field-metadata.repository'; +import { MediaRepository } from 'src/repository/media.repository'; import { ModelMetadataRepository } from 'src/repository/model-metadata.repository'; import { CRUDService } from 'src/services/crud.service'; import { MediaStorageProviderType } from '../dtos/create-media-storage-provider-metadata.dto'; @@ -21,6 +24,9 @@ import { ChatterMessage } from '../entities/chatter-message.entity'; import { getMediaStorageProvider } from './mediaStorageProviders'; import { RequestContextService } from './request-context.service'; import { Logger } from '@nestjs/common'; +import { DiskFileService, S3FileService } from './file'; +import { DEFAULT_MEDIA_FILE_STORAGE_DIR } from "src/services/settings/default-settings-provider.service"; +import type { SolidCoreSetting } from "src/services/settings/default-settings-provider.service"; @Injectable() export class ChatterMessageService extends CRUDService { @@ -33,6 +39,7 @@ export class ChatterMessageService extends CRUDService { readonly repo: ChatterMessageRepository, // @InjectRepository(ChatterMessageDetailsRepository, 'default') readonly chatterMessageDetailsRepo: ChatterMessageDetailsRepository, + readonly mediaRepository: MediaRepository, // @InjectRepository(FieldMetadata, 'default') // readonly fieldMetadataRepo: Repository, readonly fieldMetadataRepo: FieldMetadataRepository, @@ -43,6 +50,8 @@ export class ChatterMessageService extends CRUDService { private readonly modelMetadataRepo: ModelMetadataRepository, readonly requestContextService: RequestContextService, private readonly modelMetadataHelperService: ModelMetadataHelperService, + private readonly diskFileService: DiskFileService, + private readonly s3FileService: S3FileService, ) { super(entityManager, repo, 'chatterMessage', 'solid-core', moduleRef); } @@ -67,6 +76,30 @@ export class ChatterMessageService extends CRUDService { chatterMessage.updatedBy = resolvedUserId; } + private isEditableCustomNoteMessage(message: ChatterMessage): boolean { + if (message.messageType !== CHATTER_MESSAGE_TYPE.CUSTOM) { + return false; + } + return [CHATTER_MESSAGE_SUBTYPE.CUSTOM, CHATTER_MESSAGE_SUBTYPE.NOTE].includes(message.messageSubType as any); + } + + private parseAttachmentIds(value?: string): number[] { + if (!value || typeof value !== 'string') return []; + return value + .split(',') + .map(v => Number(v.trim())) + .filter(v => Number.isInteger(v) && v > 0); + } + + private getFullFilePathForDisk(relativeUri: string): string { + const base = this.settingService.getConfigValue("fileStorageDir") + || DEFAULT_MEDIA_FILE_STORAGE_DIR; + if (path.isAbsolute(relativeUri) || relativeUri.startsWith(`${base}/`)) { + return relativeUri; + } + return `${base}/${relativeUri}`; + } + async markCompleted(id: number) { const activeUser = this.requestContextService.getActiveUser(); if (!activeUser) { @@ -82,6 +115,105 @@ export class ChatterMessageService extends CRUDService { return this.repo.save(message); } + async updateCustomNoteMessage(id: number, updateDto: UpdateChatterNoteMessageDto, files: Express.Multer.File[] = []) { + const activeUser = this.requestContextService.getActiveUser(); + if (!activeUser) { + throw new ForbiddenException(ERROR_MESSAGES.FORBIDDEN); + } + + const message = await this.repo.findOne({ where: { id }, relations: { user: true } }); + if (!message) { + throw new NotFoundException(`Entity [solid-core.chatterMessage] with id ${id} not found`); + } + + if (!this.isEditableCustomNoteMessage(message)) { + throw new BadRequestException('Only custom note messages can be edited.'); + } + + if (!message.user?.id || message.user.id !== activeUser.sub) { + throw new ForbiddenException('You can only edit your own custom note messages.'); + } + + const removeAttachmentIds = this.parseAttachmentIds(updateDto?.removeAttachmentIds); + const hasMessageBody = typeof updateDto?.messageBody === 'string'; + const trimmedMessageBody = (updateDto?.messageBody ?? '').trim(); + const hasNewFiles = Array.isArray(files) && files.length > 0; + + if (!hasMessageBody && removeAttachmentIds.length === 0 && !hasNewFiles) { + throw new BadRequestException('No note changes submitted.'); + } + + if (hasMessageBody && trimmedMessageBody.length === 0) { + throw new BadRequestException('Message body cannot be empty.'); + } + + if (hasMessageBody) { + message.messageBody = trimmedMessageBody; + } + message.updatedBy = activeUser.sub; + // Ensure updatedAt changes even for attachment-only edits. + message.updatedAt = new Date(); + const savedMessage = await this.repo.save(message); + + if (removeAttachmentIds.length > 0 || hasNewFiles) { + const model = await this.modelMetadataService.findOneBySingularName('chatterMessage', { + fields: { + model: true, + mediaStorageProvider: true, + }, + module: true, + }); + + const mediaFields = model.fields.filter(field => field.type === 'mediaSingle' || field.type === 'mediaMultiple'); + const attachmentFieldIds = mediaFields.map(field => field.id); + + if (removeAttachmentIds.length > 0 && attachmentFieldIds.length > 0) { + const mediaToRemove = await this.mediaRepository.find({ + where: { + id: In(removeAttachmentIds), + entityId: savedMessage.id, + modelMetadata: { id: model.id }, + fieldMetadata: { id: In(attachmentFieldIds) }, + }, + relations: { + mediaStorageProviderMetadata: true, + fieldMetadata: true, + }, + }); + + for (const media of mediaToRemove) { + const storageType = media.mediaStorageProviderMetadata?.type as MediaStorageProviderType; + if (storageType === MediaStorageProviderType.AwsS3) { + const bucketName = media.mediaStorageProviderMetadata?.bucketName; + const region = media.mediaStorageProviderMetadata?.region; + if (bucketName && media.relativeUri) { + await this.s3FileService.delete(`${bucketName}:${media.relativeUri}`, { region }); + } + } else if (media.relativeUri) { + await this.diskFileService.delete(this.getFullFilePathForDisk(media.relativeUri)); + } + } + + if (mediaToRemove.length > 0) { + await this.mediaRepository.remove(mediaToRemove); + } + } + + for (const mediaField of mediaFields) { + const storageProviderMetadata = mediaField.mediaStorageProvider; + const storageProviderType = storageProviderMetadata.type as MediaStorageProviderType; + const storageProvider = await getMediaStorageProvider(this.moduleRef, storageProviderType); + + const media = files.filter(multerFile => multerFile.fieldname === mediaField.name); + if (media.length > 0) { + await storageProvider.store(media, savedMessage, mediaField); + } + } + } + + return savedMessage; + } + async postMessage(postDto: PostChatterMessageDto, files: Express.Multer.File[] = []) { const chatterMessage = new ChatterMessage(); chatterMessage.messageType = CHATTER_MESSAGE_TYPE.CUSTOM; From 80fa1805c810748b7786570f8e616fc42fb1b8ee Mon Sep 17 00:00:00 2001 From: sundaram Date: Wed, 27 May 2026 15:29:59 +0530 Subject: [PATCH 056/136] feat(chatter): add updateCustomNoteMessage action to ChatterMessageController --- src/seeders/seed-data/solid-core-metadata.json | 1 + 1 file changed, 1 insertion(+) diff --git a/src/seeders/seed-data/solid-core-metadata.json b/src/seeders/seed-data/solid-core-metadata.json index cb03bcec..41d27c97 100755 --- a/src/seeders/seed-data/solid-core-metadata.json +++ b/src/seeders/seed-data/solid-core-metadata.json @@ -5970,6 +5970,7 @@ "ChatterMessageController.postMessage", "ChatterMessageController.findMany", "ChatterMessageController.markCompleted", + "ChatterMessageController.updateCustomNoteMessage", "ImportTransactionController.getImportTemplate", "ImportTransactionController.getImportInstructions", "ImportTransactionController.getImportMappingInfo", From b4c5e0cd33c52bdc5c5eb3efd0ef624316b3f5cf Mon Sep 17 00:00:00 2001 From: sundaram Date: Wed, 27 May 2026 16:35:25 +0530 Subject: [PATCH 057/136] feat(media): add deleteByMediaRecord method to MediaStorageProvider implementations --- src/interfaces.ts | 2 +- src/services/chatter-message.service.ts | 26 ++----------------- .../file-s3-storage-provider.ts | 12 +++++++++ .../file-storage-provider.ts | 9 ++++++- 4 files changed, 23 insertions(+), 26 deletions(-) diff --git a/src/interfaces.ts b/src/interfaces.ts index c9ca03db..7c36cc3a 100755 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -42,6 +42,7 @@ export interface ValidationError { export interface MediaStorageProvider { store(files: Express.Multer.File[], entity: T, mediaFieldMetadata: FieldMetadata): Promise; delete(entity: T, mediaFieldMetadata: FieldMetadata): Promise; + deleteByMediaRecord(media: Media): Promise; retrieve(entity: T, mediaFieldMetadata: FieldMetadata): Promise; storeStreams(streamPairs: [Readable, string][], entity: T, mediaFieldMetadata: FieldMetadata): Promise; // delete(file: string): Promise; @@ -450,4 +451,3 @@ export interface AuditQueuePayload { updatedColumnNames?: string[]; userId?: number | null; } - diff --git a/src/services/chatter-message.service.ts b/src/services/chatter-message.service.ts index 0be2c379..790c0184 100644 --- a/src/services/chatter-message.service.ts +++ b/src/services/chatter-message.service.ts @@ -3,7 +3,6 @@ import { BadRequestException, ForbiddenException, forwardRef, Inject, Injectable import { ModuleRef } from "@nestjs/core"; import { InjectEntityManager } from '@nestjs/typeorm'; import { Brackets, EntityManager, EntityMetadata, In } from 'typeorm'; -import * as path from 'path'; import { classify } from '@angular-devkit/core/src/utils/strings'; import { CHATTER_MESSAGE_STATUS, CHATTER_MESSAGE_SUBTYPE, CHATTER_MESSAGE_TYPE } from 'src/constants/chatter-message.constants'; @@ -24,9 +23,6 @@ import { ChatterMessage } from '../entities/chatter-message.entity'; import { getMediaStorageProvider } from './mediaStorageProviders'; import { RequestContextService } from './request-context.service'; import { Logger } from '@nestjs/common'; -import { DiskFileService, S3FileService } from './file'; -import { DEFAULT_MEDIA_FILE_STORAGE_DIR } from "src/services/settings/default-settings-provider.service"; -import type { SolidCoreSetting } from "src/services/settings/default-settings-provider.service"; @Injectable() export class ChatterMessageService extends CRUDService { @@ -50,8 +46,6 @@ export class ChatterMessageService extends CRUDService { private readonly modelMetadataRepo: ModelMetadataRepository, readonly requestContextService: RequestContextService, private readonly modelMetadataHelperService: ModelMetadataHelperService, - private readonly diskFileService: DiskFileService, - private readonly s3FileService: S3FileService, ) { super(entityManager, repo, 'chatterMessage', 'solid-core', moduleRef); } @@ -91,15 +85,6 @@ export class ChatterMessageService extends CRUDService { .filter(v => Number.isInteger(v) && v > 0); } - private getFullFilePathForDisk(relativeUri: string): string { - const base = this.settingService.getConfigValue("fileStorageDir") - || DEFAULT_MEDIA_FILE_STORAGE_DIR; - if (path.isAbsolute(relativeUri) || relativeUri.startsWith(`${base}/`)) { - return relativeUri; - } - return `${base}/${relativeUri}`; - } - async markCompleted(id: number) { const activeUser = this.requestContextService.getActiveUser(); if (!activeUser) { @@ -183,15 +168,8 @@ export class ChatterMessageService extends CRUDService { for (const media of mediaToRemove) { const storageType = media.mediaStorageProviderMetadata?.type as MediaStorageProviderType; - if (storageType === MediaStorageProviderType.AwsS3) { - const bucketName = media.mediaStorageProviderMetadata?.bucketName; - const region = media.mediaStorageProviderMetadata?.region; - if (bucketName && media.relativeUri) { - await this.s3FileService.delete(`${bucketName}:${media.relativeUri}`, { region }); - } - } else if (media.relativeUri) { - await this.diskFileService.delete(this.getFullFilePathForDisk(media.relativeUri)); - } + const storageProvider = await getMediaStorageProvider(this.moduleRef, storageType); + await storageProvider.deleteByMediaRecord(media); } if (mediaToRemove.length > 0) { diff --git a/src/services/mediaStorageProviders/file-s3-storage-provider.ts b/src/services/mediaStorageProviders/file-s3-storage-provider.ts index 8b2257ce..4e1da188 100755 --- a/src/services/mediaStorageProviders/file-s3-storage-provider.ts +++ b/src/services/mediaStorageProviders/file-s3-storage-provider.ts @@ -120,6 +120,18 @@ export class FileS3StorageProvider implements MediaStorageProvider { } } + async deleteByMediaRecord(media: Media): Promise { + const storageProvider = media?.mediaStorageProviderMetadata; + if (!storageProvider) { + throw new Error(`mediaStorageProviderMetadata is not populated for media id ${media?.id ?? 'unknown'}`); + } + if (!storageProvider?.bucketName || !media?.relativeUri) { + return; + } + const region = this.getEffectiveRegion(storageProvider.region); + await this.s3FileService.delete(`${storageProvider.bucketName}:${media.relativeUri}`, { region }); + } + /** * Get the effective region to use for S3 operations. * Uses the provider-specific region if configured, otherwise falls back to env variable. diff --git a/src/services/mediaStorageProviders/file-storage-provider.ts b/src/services/mediaStorageProviders/file-storage-provider.ts index dd91d9ec..8e5be961 100755 --- a/src/services/mediaStorageProviders/file-storage-provider.ts +++ b/src/services/mediaStorageProviders/file-storage-provider.ts @@ -110,6 +110,13 @@ export class FileStorageProvider implements MediaStorageProvider { // }); } + async deleteByMediaRecord(media: Media): Promise { + if (!media?.relativeUri) { + return; + } + await this.fileService.delete(this.getFullFilePath(media.relativeUri)); + } + private getFullFilePath(fileName: string): string { const base = this.settingService.getConfigValue("fileStorageDir") || DEFAULT_MEDIA_FILE_STORAGE_DIR; @@ -122,4 +129,4 @@ export class FileStorageProvider implements MediaStorageProvider { private getFileName(file: Express.Multer.File): string { return `${file.filename}-${file.originalname}`; } -} \ No newline at end of file +} From 2e99b99a972a09e88b1db0806cf165f298397e08 Mon Sep 17 00:00:00 2001 From: Gaurav Dafale Date: Thu, 28 May 2026 10:25:00 +0530 Subject: [PATCH 058/136] feat: add gupshup whatsapp provider --- src/controllers/gupshup-webhook.controller.ts | 91 +++++ src/factories/whatsapp.factory.ts | 65 ++-- src/listeners/user-registration.listener.ts | 55 ++- src/services/authentication.service.ts | 327 +++++++++++++++--- .../whatsapp/GupshupOtpWhatsappService.ts | 174 ++++++++++ 5 files changed, 628 insertions(+), 84 deletions(-) create mode 100644 src/controllers/gupshup-webhook.controller.ts create mode 100644 src/services/whatsapp/GupshupOtpWhatsappService.ts diff --git a/src/controllers/gupshup-webhook.controller.ts b/src/controllers/gupshup-webhook.controller.ts new file mode 100644 index 00000000..ba7ab5d1 --- /dev/null +++ b/src/controllers/gupshup-webhook.controller.ts @@ -0,0 +1,91 @@ +import { + Body, + Controller, + HttpCode, + HttpStatus, + Logger, + Post, + Req, +} from "@nestjs/common"; +import { ApiTags } from "@nestjs/swagger"; +import { Request } from "express"; +import { Auth } from "src/decorators/auth.decorator"; +import { Public } from "src/decorators/public.decorator"; +import { AuthType } from "src/enums/auth-type.enum"; + +@Auth(AuthType.None) +@Controller("webhook/whatsapp/gupshup") +@ApiTags("Solid Core") +export class GupshupWebhookController { + private readonly logger = new Logger(GupshupWebhookController.name); + + @Public() + @Post() + @HttpCode(HttpStatus.OK) + async receiveWebhook(@Req() req: Request, @Body() body: unknown) { + const userAgent = req.headers["user-agent"] ?? null; + this.logger.log( + `Received Gupshup WhatsApp webhook${userAgent ? ` from ${userAgent}` : ""}`, + ); + this.logger.debug(`Gupshup webhook payload: ${JSON.stringify(body)}`); + + const statusInfo = this.extractStatusInfo(body); + if (statusInfo) { + this.logger.log( + `Gupshup delivery update: status=${statusInfo.status ?? "unknown"}, messageId=${statusInfo.messageId ?? "n/a"}, destination=${statusInfo.destination ?? "n/a"}, reason=${statusInfo.reason ?? "n/a"}`, + ); + } + + return { + success: true, + message: "Webhook received", + }; + } + + private extractStatusInfo(body: unknown): { + status?: string; + messageId?: string; + destination?: string; + reason?: string; + } | null { + if (!body || typeof body !== "object") { + return null; + } + + const payload = body as Record; + + const status = + this.asString(payload.status) || + this.asString(payload.messageStatus) || + this.asString((payload.payload as Record)?.status) || + this.asString((payload.payload as Record)?.type); + + const messageId = + this.asString(payload.messageId) || + this.asString(payload.id) || + this.asString((payload.payload as Record)?.id) || + this.asString((payload.payload as Record)?.messageId); + + const destination = + this.asString(payload.destination) || + this.asString(payload.phone) || + this.asString((payload.payload as Record)?.destination) || + this.asString((payload.payload as Record)?.phone); + + const reason = + this.asString(payload.reason) || + this.asString(payload.error) || + this.asString((payload.payload as Record)?.reason) || + this.asString((payload.payload as Record)?.error); + + if (!status && !messageId && !destination && !reason) { + return null; + } + + return { status, messageId, destination, reason }; + } + + private asString(value: unknown): string | undefined { + return typeof value === "string" && value.trim() ? value.trim() : undefined; + } +} diff --git a/src/factories/whatsapp.factory.ts b/src/factories/whatsapp.factory.ts index fc1b4949..9938c17c 100644 --- a/src/factories/whatsapp.factory.ts +++ b/src/factories/whatsapp.factory.ts @@ -1,42 +1,43 @@ -import { Inject, Injectable, Logger } from "@nestjs/common"; -import { ConfigType } from "@nestjs/config"; +import { Injectable, Logger } from "@nestjs/common"; import { ModuleRef } from "@nestjs/core"; import { SolidRegistry } from "src/helpers/solid-registry"; import { IWhatsAppTransport } from "src/interfaces"; import { SettingService } from "src/services/setting.service"; import type { SolidCoreSetting } from "src/services/settings/default-settings-provider.service"; -function norm(s?: string) { - return s?.trim().toLowerCase(); -} - -// This factory will be use to return a mail service instance, using the configured environment variables @Injectable() export class WhatsAppFactory { - private readonly logger = new Logger(this.constructor.name); - constructor( - private readonly moduleRef: ModuleRef, // Use the module ref to dynamically resolve the mail service - private readonly solidRegistry: SolidRegistry, - private readonly settingService: SettingService, - ) { } - - getWhatsappService(name: string = null): IWhatsAppTransport { - // This is the default provider - const whatsappServiceName = name || this.settingService.getConfigValue("whatsappProvider"); - if (!whatsappServiceName) { - throw new Error("Unable to resolve whatsapp provider") - } - const whatsappProviders = this.solidRegistry.getWhatsappProviders(); - - // Return the instance which matches the whatsappServiceName - if (!whatsappProviders.length) { - // throw new Error("No mail providers are registered."); - this.logger.error("No whatsapp providers are registered."); - } - - const whatsappServiceProvider = whatsappProviders.find(provider => provider.name === whatsappServiceName); - - return whatsappServiceProvider.instance as IWhatsAppTransport; + private readonly logger = new Logger(WhatsAppFactory.name); + + constructor( + private readonly moduleRef: ModuleRef, + private readonly solidRegistry: SolidRegistry, + private readonly settingService: SettingService, + ) {} + + getWhatsappService(name?: string): IWhatsAppTransport { + const providerKey = + name || + this.settingService.getConfigValue("whatsappProvider"); + + if (!providerKey) { + throw new Error("Unable to resolve whatsapp provider"); + } + + const whatsappProviders = this.solidRegistry.getWhatsappProviders(); + + if (!whatsappProviders.length) { + throw new Error("No whatsapp providers are registered."); + } + + const whatsappServiceProvider = whatsappProviders.find((provider) => + provider.name?.toLowerCase().includes(providerKey.toLowerCase()), + ); + + if (!whatsappServiceProvider) { + throw new Error(`WhatsApp provider '${providerKey}' not found`); } -} \ No newline at end of file + return whatsappServiceProvider.instance as IWhatsAppTransport; + } +} diff --git a/src/listeners/user-registration.listener.ts b/src/listeners/user-registration.listener.ts index 91703e9f..9915aa83 100755 --- a/src/listeners/user-registration.listener.ts +++ b/src/listeners/user-registration.listener.ts @@ -1,14 +1,57 @@ - import { User } from "../entities/user.entity"; import { OnEvent } from "@nestjs/event-emitter"; import { Injectable, Logger } from "@nestjs/common"; import { EventDetails, EventType } from "../interfaces"; +import { WhatsAppFactory } from "src/factories/whatsapp.factory"; @Injectable() export class UserRegistrationListener { - private logger = new Logger(UserRegistrationListener.name); - @OnEvent(EventType.USER_REGISTERED) - handleUserRegistration(event: EventDetails) { - this.logger.log(`User registered with details: ${JSON.stringify(event.payload)}`); + private readonly logger = new Logger(UserRegistrationListener.name); + + constructor(private readonly whatsAppFactory: WhatsAppFactory) {} + + @OnEvent(EventType.USER_REGISTERED) + async handleUserRegistration(event: EventDetails) { + this.logger.log(`User registered with details: ${JSON.stringify(event.payload)}`); + + const notifyTo = process.env.WHATSAPP_EVENT_NOTIFY_TO; + if (!notifyTo) { + this.logger.debug("WHATSAPP_EVENT_NOTIFY_TO not set. Skipping registration WhatsApp notification."); + return; + } + + try { + const whatsappService = this.whatsAppFactory.getWhatsappService(); + const username = event.payload?.username || "User"; + const userId = event.payload?.id || "N/A"; + + await whatsappService.sendWhatsAppMessage( + notifyTo, + "registration_event", + { + payload: { + channel: "whatsapp", + source: process.env.COMMON_GUPSHUP_WHATSAPP_SOURCE, + destination: notifyTo, + "src.name": process.env.COMMON_GUPSHUP_APP_NAME || "solidx", + message: { + type: "text", + text: `New user registered: ${username} (id: ${userId})`, + }, + }, + }, + ); + + this.logger.log(`Sent registration WhatsApp notification to ${notifyTo}`); + } catch (error: any) { + const status = error?.response?.status; + const responseData = error?.response?.data; + const errorMessage = error?.message; + const stack = error?.stack; + + this.logger.error( + `Failed to send registration WhatsApp notification to ${notifyTo}. status=${status ?? "unknown"}, response=${typeof responseData === "object" ? JSON.stringify(responseData) : responseData}, message=${errorMessage}, stack=${stack}`, + ); } -} \ No newline at end of file + } +} diff --git a/src/services/authentication.service.ts b/src/services/authentication.service.ts index 4c98bd02..4d46daa2 100755 --- a/src/services/authentication.service.ts +++ b/src/services/authentication.service.ts @@ -50,6 +50,7 @@ import { SettingService } from "./setting.service"; import { UserActivityHistoryService } from "./user-activity-history.service"; import { UserService } from "./user.service"; import { SmsFactory } from "src/factories/sms.factory"; +import { WhatsAppFactory } from "src/factories/whatsapp.factory"; import { SolidRegistry } from "src/helpers/solid-registry"; enum LoginProvider { @@ -79,6 +80,7 @@ export class AuthenticationService { private readonly mailServiceFactory: MailFactory, // private readonly smsService: Msg91OTPService, private readonly smsFactory: SmsFactory, + private readonly whatsAppFactory: WhatsAppFactory, private readonly eventEmitter: EventEmitter2, private readonly settingService: SettingService, private readonly roleMetadataService: RoleMetadataService, @@ -193,11 +195,22 @@ export class AuthenticationService { return this.performSignUp(signUpDto, new User(), this.userRepository); } - private async performSignUp(signUpDto: SignUpDto, entity: T, repo: Repository): Promise { + private async performSignUp( + signUpDto: SignUpDto, + entity: T, + repo: Repository, + ): Promise { try { - const onForcePasswordChange = this.settingService.getConfigValue("forceChangePasswordOnFirstLogin"); - const activateUserOnRegistration = this.settingService.getConfigValue("activateUserOnRegistration"); - const defaultRole = this.settingService.getConfigValue("defaultRole"); + const onForcePasswordChange = + this.settingService.getConfigValue( + "forceChangePasswordOnFirstLogin", + ); + const activateUserOnRegistration = + this.settingService.getConfigValue( + "activateUserOnRegistration", + ); + const defaultRole = + this.settingService.getConfigValue("defaultRole"); var { user, pwd, autoGeneratedPwd } = await this.populateForSignup( entity, @@ -211,14 +224,19 @@ export class AuthenticationService { } const savedUser = await repo.save(user); const userRoles = signUpDto.roles ?? []; - if ((signUpDto.roles?.length ?? 0) === 0 && signUpDto.username !== "sa" && defaultRole) { + if ( + (signUpDto.roles?.length ?? 0) === 0 && + signUpDto.username !== "sa" && + defaultRole + ) { userRoles.push(defaultRole); } await this.handlePostSignup(savedUser, userRoles, pwd, autoGeneratedPwd); + await this.handlePasswordlessSignupOtp(savedUser, signUpDto, autoGeneratedPwd, repo); + this.triggerRegistrationEvent(savedUser); return savedUser; - } - catch (err: any) { + } catch (err: any) { const pgUniqueViolationErrorCode = "23505"; if (err.code === pgUniqueViolationErrorCode) { throw new ConflictException( @@ -231,6 +249,42 @@ export class AuthenticationService { } } + private async handlePasswordlessSignupOtp( + user: T, + signUpDto: SignUpDto, + autoGeneratedPwd: string, + repo: Repository, + ): Promise { + const isPasswordProvided = !!signUpDto.password; + const isAutoGeneratedPassword = !!autoGeneratedPwd; + if (isPasswordProvided || isAutoGeneratedPassword) { + return; + } + + if (!user.mobile) { + this.logger.warn( + `Skipping passwordless signup OTP WhatsApp notification for user ${user.username}: mobile is missing.`, + ); + return; + } + + const isPasswordlessRegistrationEnabled = + await this.isPasswordlessRegistrationEnabled(); + if (!isPasswordlessRegistrationEnabled) { + return; + } + + await this.assignRegistrationOtp( + PasswordlessRegistrationValidateWhatSources.MOBILE, + user, + ); + await repo.save(user); + await this.notifyUserOnOtpInitiateRegistration( + user, + PasswordlessRegistrationValidateWhatSources.MOBILE, + ); + } + /** @deprecated Use IExtensionUserCreationProvider instead. Kept for backward compatibility. */ async signupForExtensionUser( signUpDto: SignUpDto, @@ -602,7 +656,7 @@ export class AuthenticationService { const companyLogo = await this.getCompanyLogo(); if ( registrationValidationSource === - PasswordlessLoginValidateWhatSources.EMAIL + PasswordlessRegistrationValidateWhatSources.EMAIL ) { const mailService = this.mailServiceFactory.getMailService(); mailService.sendEmailUsingTemplate( @@ -632,25 +686,141 @@ export class AuthenticationService { } if ( registrationValidationSource === - PasswordlessLoginValidateWhatSources.MOBILE + PasswordlessRegistrationValidateWhatSources.MOBILE ) { - const smsService = this.smsFactory.getSmsService(); - smsService.sendSMSUsingTemplate( + const templateParams = { + solidAppName: + this.settingService.getConfigValue("appTitle"), + otp: user.mobileVerificationTokenOnRegistration, + mobileVerificationTokenOnRegistration: + user.mobileVerificationTokenOnRegistration, + firstName: user.username, + fullName: user.fullName ? user.fullName : user.username, + companyLogoUrl: companyLogo, + }; + + const whatsappDestination = this.normalizeWhatsAppDestination( user.mobile, - "otp-on-register", - { - solidAppName: - this.settingService.getConfigValue("appTitle"), - otp: user.mobileVerificationTokenOnRegistration, - mobileVerificationTokenOnRegistration: - user.mobileVerificationTokenOnRegistration, - firstName: user.username, - fullName: user.fullName ? user.fullName : user.username, - companyLogoUrl: companyLogo, - }, - this.settingService.getConfigValue("shouldQueueSms"), ); + const gupshupTemplateAppName = + process.env.COMMON_GUPSHUP_APP_NAME || "Gupshup"; + const whatsappTemplateId = + this.settingService.getConfigValue( + "otpWhatsappTemplateId", + ) || "common_otp"; + const whatsappIndependentEnabled = + this.settingService.getConfigValue( + "otpWhatsappIndependentEnabled", + ) !== false; + + let smsSent = false; + let whatsappSent = false; + let smsErrorMessage: string | undefined; + let whatsappErrorMessage: string | undefined; + + this.logger.debug( + `OTP SMS send attempt: destination=${user.mobile}, whatsappDestination=${whatsappDestination}`, + ); + + try { + const smsService = this.smsFactory.getSmsService(); + await smsService.sendSMSUsingTemplate( + user.mobile, + "otp-on-register", + templateParams, + false, + ); + smsSent = true; + } catch (smsError: any) { + smsErrorMessage = smsError?.message; + this.logger.warn( + `OTP SMS failed: destination=${user.mobile}, message=${smsErrorMessage}`, + ); + } + + if (whatsappIndependentEnabled) { + if (!whatsappDestination) { + whatsappErrorMessage = "Normalized WhatsApp destination is empty"; + this.logger.error( + `Independent OTP WhatsApp skipped: destination=${user.mobile}, message=${whatsappErrorMessage}`, + ); + } else { + this.logger.debug( + `Independent OTP WhatsApp send attempt: destination=${user.mobile}, whatsappDestination=${whatsappDestination}, templateId=${whatsappTemplateId}`, + ); + try { + await this.sendOtpToWhatsappProvider( + whatsappDestination, + String(whatsappTemplateId), + String(gupshupTemplateAppName), + String(templateParams.otp || ""), + ); + whatsappSent = true; + this.logger.log( + `Independent OTP WhatsApp success: destination=${user.mobile}, whatsappDestination=${whatsappDestination}, templateId=${whatsappTemplateId}`, + ); + } catch (waError: any) { + whatsappErrorMessage = waError?.message; + this.logger.error( + `Independent OTP WhatsApp failed: destination=${user.mobile}, whatsappDestination=${whatsappDestination}, templateId=${whatsappTemplateId}, message=${whatsappErrorMessage}`, + ); + } + } + } + + if (!smsSent && !whatsappSent) { + throw new Error( + `OTP delivery failed on both channels. smsError=${smsErrorMessage || "unknown"}, whatsappError=${whatsappErrorMessage || "disabled/unknown"}`, + ); + } + } + } + + private normalizeWhatsAppDestination(rawMobile: string): string { + const raw = (rawMobile || "").trim(); + let sanitized = raw.replace(/[^\d+]/g, ""); + + if (sanitized.startsWith("00")) { + sanitized = `+${sanitized.slice(2)}`; + } + + const defaultDialCode = String( + this.settingService.getConfigValue( + "otpDefaultCountryDialCode", + ) || "", + ).replace(/\D/g, ""); + + if (sanitized.startsWith("+")) { + const e164Digits = sanitized.slice(1).replace(/\D/g, ""); + if (e164Digits.length >= 8 && e164Digits.length <= 15) { + return e164Digits; + } + } + + const digits = sanitized.replace(/\D/g, ""); + + if (digits.length >= 11 && digits.length <= 15) { + return digits; } + + if (digits.length === 10 && defaultDialCode) { + return `${defaultDialCode}${digits}`; + } + + return digits; + } + + private async sendOtpToWhatsappProvider( + destination: string, + templateId: string, + appName: string, + otp: string, + ): Promise { + const whatsappService = this.whatsAppFactory.getWhatsappService(); + await whatsappService.sendWhatsAppMessage(destination, null, { + type: "text", + text: `${appName} OTP is ${otp}. It is valid for 10 mins.`, + }); } async otpConfirmRegistration(confirmSignUpDto: OTPConfirmOTPDto) { @@ -753,7 +923,7 @@ export class AuthenticationService { this.resolvePasswordlessValidationSource(); if ( registrationValidationSource === - PasswordlessLoginValidateWhatSources.EMAIL + PasswordlessRegistrationValidateWhatSources.EMAIL ) { if (!user.emailVerifiedOnRegistrationAt) { return false; @@ -761,7 +931,7 @@ export class AuthenticationService { } if ( registrationValidationSource === - PasswordlessLoginValidateWhatSources.MOBILE + PasswordlessRegistrationValidateWhatSources.MOBILE ) { if (!user.mobileVerifiedOnRegistrationAt) { return false; @@ -876,7 +1046,7 @@ export class AuthenticationService { const dummyOtp = this.getDummyOtpForUser(user); if (!dummyOtp) { await this.assignLoginOtp(user, type); - this.notifyUserOnOtpInititateLogin(user, type); + await this.notifyUserOnOtpInititateLogin(user, type); } return this.buildLoginOtpResponse(user, type); } @@ -1001,21 +1171,88 @@ export class AuthenticationService { ); } if (loginType === PasswordlessLoginValidateWhatSources.MOBILE) { - const smsService = this.smsFactory.getSmsService(); - smsService.sendSMSUsingTemplate( - user.mobile, - "otp-on-login", - { - solidAppName: - this.settingService.getConfigValue("appTitle"), - otp: user.mobileVerificationTokenOnLogin, - mobileVerificationTokenOnLogin: user.mobileVerificationTokenOnLogin, - firstName: user.username, - fullName: user.fullName ? user.fullName : user.username, - companyLogoUrl: companyLogo, - }, - this.settingService.getConfigValue("shouldQueueSms"), + const templateParams = { + solidAppName: + this.settingService.getConfigValue("appTitle"), + otp: user.mobileVerificationTokenOnLogin, + mobileVerificationTokenOnLogin: user.mobileVerificationTokenOnLogin, + firstName: user.username, + fullName: user.fullName ? user.fullName : user.username, + companyLogoUrl: companyLogo, + }; + + const whatsappDestination = this.normalizeWhatsAppDestination(user.mobile); + const gupshupTemplateAppName = + process.env.COMMON_GUPSHUP_APP_NAME || "Gupshup"; + const whatsappTemplateId = + this.settingService.getConfigValue( + "otpWhatsappTemplateId", + ) || "common_otp"; + const whatsappIndependentEnabled = + this.settingService.getConfigValue( + "otpWhatsappIndependentEnabled", + ) !== false; + + let smsSent = false; + let whatsappSent = false; + let smsErrorMessage: string | undefined; + let whatsappErrorMessage: string | undefined; + + this.logger.debug( + `OTP LOGIN SMS send attempt: destination=${user.mobile}, whatsappDestination=${whatsappDestination}`, ); + + try { + const smsService = this.smsFactory.getSmsService(); + await smsService.sendSMSUsingTemplate( + user.mobile, + "otp-on-login", + templateParams, + false, + ); + smsSent = true; + } catch (smsError: any) { + smsErrorMessage = smsError?.message; + this.logger.warn( + `OTP LOGIN SMS failed: destination=${user.mobile}, message=${smsErrorMessage}`, + ); + } + + if (whatsappIndependentEnabled) { + if (!whatsappDestination) { + whatsappErrorMessage = "Normalized WhatsApp destination is empty"; + this.logger.error( + `OTP LOGIN WhatsApp skipped: destination=${user.mobile}, message=${whatsappErrorMessage}`, + ); + } else { + this.logger.debug( + `OTP LOGIN WhatsApp send attempt: destination=${user.mobile}, whatsappDestination=${whatsappDestination}, templateId=${whatsappTemplateId}`, + ); + try { + await this.sendOtpToWhatsappProvider( + whatsappDestination, + String(whatsappTemplateId), + String(gupshupTemplateAppName), + String(templateParams.otp || ""), + ); + whatsappSent = true; + this.logger.log( + `OTP LOGIN WhatsApp success: destination=${user.mobile}, whatsappDestination=${whatsappDestination}, templateId=${whatsappTemplateId}`, + ); + } catch (waError: any) { + whatsappErrorMessage = waError?.message; + this.logger.error( + `OTP LOGIN WhatsApp failed: destination=${user.mobile}, whatsappDestination=${whatsappDestination}, templateId=${whatsappTemplateId}, message=${whatsappErrorMessage}`, + ); + } + } + } + + if (!smsSent && !whatsappSent) { + throw new Error( + `OTP LOGIN delivery failed on both channels. smsError=${smsErrorMessage || "unknown"}, whatsappError=${whatsappErrorMessage || "disabled/unknown"}`, + ); + } } } @@ -1294,7 +1531,7 @@ export class AuthenticationService { // Assuming all users do not have mobile as mandatory. if ( forgotPasswordSendVerificationTokenOn == - ForgotPasswordSendVerificationTokenOn.MOBILE && + ForgotPasswordSendVerificationTokenOn.MOBILE && user.mobile ) { const smsService = this.smsFactory.getSmsService(); @@ -1323,12 +1560,10 @@ export class AuthenticationService { const user = await this.resolveUserByVerificationToken( confirmForgotPasswordDto.verificationToken, ); - if (!user) - throw new UnauthorizedException("Invalid verification token"); + if (!user) throw new UnauthorizedException("Invalid verification token"); if (user.lastLoginProvider !== "local") throw new UnauthorizedException(ERROR_MESSAGES.INVALID_CREDENTIALS); - if (!user.active) - throw new UnauthorizedException("User is inactive"); + if (!user.active) throw new UnauthorizedException("User is inactive"); // 1) Atomically consume the token (only one request can succeed) const { affected } = await m @@ -1420,7 +1655,7 @@ export class AuthenticationService { // Assuming all users do not have mobile as mandatory. if ( forgotPasswordSendVerificationTokenOn == - ForgotPasswordSendVerificationTokenOn.MOBILE && + ForgotPasswordSendVerificationTokenOn.MOBILE && user.mobile ) { const smsService = this.smsFactory.getSmsService(); diff --git a/src/services/whatsapp/GupshupOtpWhatsappService.ts b/src/services/whatsapp/GupshupOtpWhatsappService.ts new file mode 100644 index 00000000..fccfc12b --- /dev/null +++ b/src/services/whatsapp/GupshupOtpWhatsappService.ts @@ -0,0 +1,174 @@ +import { HttpService } from "@nestjs/axios"; +import { Injectable, Logger } from "@nestjs/common"; +import { AxiosError } from "axios"; +import { WhatsAppProvider } from "src/decorators/whatsapp-provider.decorator"; +import { IWhatsAppTransport } from "src/interfaces"; + +@Injectable() +@WhatsAppProvider() +export class GupshupOtpWhatsappService implements IWhatsAppTransport { + private readonly logger = new Logger(GupshupOtpWhatsappService.name); + + constructor(private readonly httpService: HttpService) {} + + async sendWhatsAppMessage( + to: string, + templateId: string, + parameters: any, + parentEntity?: any, + parentEntityId?: any, + ): Promise { + if (!to) { + throw new Error("WhatsApp destination number is required"); + } + + const payload = parameters?.payload; + if (payload?.message?.type === "text" && payload?.message?.text) { + await this.sendTextMessage( + payload.destination || to, + payload.message.text, + payload.source, + payload["src.name"], + ); + return { to, templateId, parameters, parentEntity, parentEntityId }; + } + + if (parameters?.type === "text" && parameters?.text) { + await this.sendTextMessage(to, parameters.text); + return { to, templateId, parameters, parentEntity, parentEntityId }; + } + + if (!templateId) { + throw new Error("WhatsApp templateId is required for template message"); + } + + const bodyParams = Array.isArray(parameters) + ? parameters + : Array.isArray(parameters?.body) + ? parameters.body + : []; + + await this.sendOtpTemplate(to, templateId, bodyParams.map((x) => String(x))); + return { to, templateId, parameters, parentEntity, parentEntityId }; + } + + private async sendTextMessage( + destination: string, + text: string, + sourceOverride?: string, + appNameOverride?: string, + ): Promise { + const apiKey = + process.env.COMMON_GUPSHUP_WHATSAPP_API_KEY || + process.env.GUPSHUP_API_KEY; + const msgUrl = + process.env.COMMON_GUPSHUP_WHATSAPP_API_URL || + process.env.GUPSHUP_API_URL || + "https://api.gupshup.io/wa/api/v1/msg"; + const source = + sourceOverride || + process.env.COMMON_GUPSHUP_WHATSAPP_SOURCE || + process.env.GUPSHUP_SOURCE_NUMBER; + const appName = + appNameOverride || process.env.COMMON_GUPSHUP_APP_NAME || "solidx"; + + if (!apiKey || !msgUrl || !source) { + throw new Error("Missing Gupshup WhatsApp configuration for text message"); + } + + const rawDestination = destination.replace(/\D/g, ""); + const params = new URLSearchParams(); + params.append("channel", "whatsapp"); + params.append("source", source); + params.append("destination", rawDestination); + params.append("src.name", appName); + params.append("message", JSON.stringify({ type: "text", text })); + + await this.httpService.axiosRef.post(msgUrl, params.toString(), { + headers: { + apikey: apiKey, + "Content-Type": "application/x-www-form-urlencoded", + }, + }); + } + + async sendOtpTemplate( + destination: string, + templateId: string, + bodyParams: string[], + ): Promise { + const apiKey = + process.env.COMMON_GUPSHUP_WHATSAPP_API_KEY || + process.env.GUPSHUP_API_KEY; + const baseUrl = + process.env.COMMON_GUPSHUP_WHATSAPP_API_URL || + process.env.GUPSHUP_API_URL || + "https://api.gupshup.io/wa/api/v1/msg"; + const templateUrl = + process.env.COMMON_GUPSHUP_WHATSAPP_TEMPLATE_API_URL || + baseUrl.replace(/\/msg$/, "/template/msg"); + const source = + process.env.COMMON_GUPSHUP_WHATSAPP_SOURCE || + process.env.GUPSHUP_SOURCE_NUMBER; + const appName = process.env.COMMON_GUPSHUP_APP_NAME || "solidx"; + + if (!apiKey || !templateUrl || !source) { + throw new Error("Missing Gupshup OTP WhatsApp configuration"); + } + + if (!destination || !String(destination).trim()) { + throw new Error("WhatsApp destination is empty"); + } + + try { + const rawDestination = destination.replace(/\D/g, ""); + const params = new URLSearchParams(); + params.append("channel", "whatsapp"); + params.append("source", source); + params.append("destination", rawDestination); + params.append( + "to", + rawDestination.startsWith("+") + ? rawDestination + : `+${rawDestination}`, + ); + params.append("src.name", appName); + params.append( + "template", + JSON.stringify({ + id: templateId, + params: bodyParams, + }), + ); + + this.logger.debug( + `Gupshup OTP outbound (template): endpoint=${templateUrl}, source=${source}, destination=${rawDestination}, to=${rawDestination.startsWith("+") ? rawDestination : `+${rawDestination}`}, templateId=${templateId}, params=${JSON.stringify(bodyParams)}`, + ); + + const response = await this.httpService.axiosRef.post( + templateUrl, + params.toString(), + { + headers: { + apikey: apiKey, + "Content-Type": "application/x-www-form-urlencoded", + }, + }, + ); + + this.logger.debug( + `Gupshup response: ${response.status}, data=${JSON.stringify(response.data)}`, + ); + + this.logger.debug( + `Independent OTP WhatsApp sent to ${rawDestination} template=${templateId} via template endpoint`, + ); + } catch (error) { + const axiosError = error as AxiosError; + this.logger.error( + `Independent OTP WhatsApp failed: destination=${destination}, templateId=${templateId}, status=${axiosError.response?.status ?? "unknown"}, response=${typeof axiosError.response?.data === "object" ? JSON.stringify(axiosError.response?.data) : axiosError.response?.data}, url=${templateUrl}`, + ); + throw error; + } + } +} From 9df1bc6ae7f5ac32f1f782f6564565ff48ec904f Mon Sep 17 00:00:00 2001 From: Gaurav Dafale Date: Thu, 28 May 2026 11:02:46 +0530 Subject: [PATCH 059/136] feat: implement WhatsApp Cloud API integration --- .../meta-cloud-whatsapp-webhook.controller.ts | 155 ++++ src/index.ts | 1 + .../default-settings-provider.service.ts | 98 +++ .../whatsapp/MetaCloudWhatsappService.ts | 253 ++++++ src/solid-core.module.ts | 747 +++++++++--------- 5 files changed, 885 insertions(+), 369 deletions(-) create mode 100644 src/controllers/meta-cloud-whatsapp-webhook.controller.ts create mode 100644 src/services/whatsapp/MetaCloudWhatsappService.ts diff --git a/src/controllers/meta-cloud-whatsapp-webhook.controller.ts b/src/controllers/meta-cloud-whatsapp-webhook.controller.ts new file mode 100644 index 00000000..1c97c706 --- /dev/null +++ b/src/controllers/meta-cloud-whatsapp-webhook.controller.ts @@ -0,0 +1,155 @@ +import { + Body, + Controller, + Get, + HttpCode, + HttpStatus, + Logger, + Post, + Query, + Res, +} from "@nestjs/common"; +import { ApiTags } from "@nestjs/swagger"; +import { Response } from "express"; +import { Auth } from "src/decorators/auth.decorator"; +import { Public } from "src/decorators/public.decorator"; +import { AuthType } from "src/enums/auth-type.enum"; +import { SettingService } from "src/services/setting.service"; +import type { SolidCoreSetting } from "src/services/settings/default-settings-provider.service"; + +@Auth(AuthType.None) +@Controller("webhook/whatsapp/meta-cloud") +@ApiTags("Solid Core") +export class MetaCloudWhatsappWebhookController { + private readonly logger = new Logger(MetaCloudWhatsappWebhookController.name); + + constructor(private readonly settingService: SettingService) {} + + @Public() + @Get() + verifyWebhook(@Query() query: Record, @Res() res: Response) { + const mode = this.resolveQueryValue(query, "hub.mode", "hub_mode", "mode"); + const verifyToken = this.resolveQueryValue( + query, + "hub.verify_token", + "hub_verify_token", + "verify_token", + ); + const challenge = this.resolveQueryValue( + query, + "hub.challenge", + "hub_challenge", + "challenge", + ); + + const configuredVerifyToken = + this.settingService.getConfigValue( + "metaWhatsappWebhookVerifyToken", + ) || process.env.COMMON_META_WHATSAPP_WEBHOOK_VERIFY_TOKEN; + + const isVerificationCall = mode === "subscribe"; + const tokenMatches = + !!configuredVerifyToken && verifyToken === configuredVerifyToken; + + if (isVerificationCall && tokenMatches && challenge) { + this.logger.log("Meta Cloud WhatsApp webhook verified successfully."); + res.writeHead(HttpStatus.OK, { + "Content-Type": "text/plain", + "Content-Length": Buffer.byteLength(String(challenge)), + }); + res.write(String(challenge)); + return res.end(); + } + + this.logger.warn( + `Meta Cloud webhook verification failed. mode=${mode ?? "n/a"}, tokenMatch=${tokenMatches}`, + ); + + res.writeHead(HttpStatus.FORBIDDEN, { "Content-Type": "text/plain" }); + return res.end("Webhook verification failed"); + } + + @Public() + @Post() + @HttpCode(HttpStatus.OK) + async receiveWebhook(@Body() body: unknown) { + this.logger.log("Received Meta Cloud WhatsApp webhook"); + this.logger.debug(`Meta Cloud webhook payload: ${JSON.stringify(body)}`); + + const statusInfo = this.extractStatusInfo(body); + if (statusInfo) { + this.logger.log( + `Meta Cloud delivery update: status=${statusInfo.status ?? "unknown"}, messageId=${statusInfo.messageId ?? "n/a"}, destination=${statusInfo.destination ?? "n/a"}, reason=${statusInfo.reason ?? "n/a"}`, + ); + } + + return { + success: true, + message: "Webhook received", + }; + } + + private extractStatusInfo(body: unknown): { + status?: string; + messageId?: string; + destination?: string; + reason?: string; + } | null { + if (!body || typeof body !== "object") { + return null; + } + + const payload = body as Record; + const entries = payload.entry as Array> | undefined; + const changes = entries?.[0]?.changes as Array> | undefined; + const value = changes?.[0]?.value as Record | undefined; + const statuses = value?.statuses as Array> | undefined; + const status = statuses?.[0]; + + if (!status) { + return null; + } + + const errors = status.errors as Array> | undefined; + + return { + status: this.asString(status.status), + messageId: this.asString(status.id), + destination: this.asString(status.recipient_id), + reason: + this.asString(errors?.[0]?.title) || this.asString(errors?.[0]?.message), + }; + } + + private asString(value: unknown): string | undefined { + return typeof value === "string" && value.trim() ? value.trim() : undefined; + } + + private resolveQueryValue( + query: Record, + ...keys: string[] + ): string | undefined { + for (const key of keys) { + const directValue = this.asString(query[key]); + if (directValue) { + return directValue; + } + } + + const hubRaw = query.hub; + if (hubRaw && typeof hubRaw === "object" && !Array.isArray(hubRaw)) { + const hub = hubRaw as Record; + for (const key of ["mode", "verify_token", "challenge"]) { + const value = this.asString(hub[key]); + if ( + value && + keys.some((candidate) => candidate.endsWith(key) || candidate === key) + ) { + return value; + } + } + } + + return undefined; + } +} diff --git a/src/index.ts b/src/index.ts index 9a63c27d..053cb7fc 100755 --- a/src/index.ts +++ b/src/index.ts @@ -318,6 +318,7 @@ export * from './services/solid-introspect.service' export * from './services/user.service' export * from './services/view-metadata.service' export * from './services/whatsapp/Msg91WhatsappService' //rename +export * from './services/whatsapp/GupshupOtpWhatsappService' export * from './services/setting.service' export * from './services/encryption.service' export * from './services/info.service' diff --git a/src/services/settings/default-settings-provider.service.ts b/src/services/settings/default-settings-provider.service.ts index 5a1ac4b1..bdb7d4f3 100644 --- a/src/services/settings/default-settings-provider.service.ts +++ b/src/services/settings/default-settings-provider.service.ts @@ -779,6 +779,46 @@ const getSolidCoreSettings = (isProd: boolean) => value: parseInt(process.env.IAM_OTP_EXPIRY ?? "10"), level: SettingLevel.SystemEnv, }, + { + moduleName: "solid-core", + key: "otpWhatsappFallbackEnabled", + value: (process.env.IAM_OTP_WHATSAPP_FALLBACK_ENABLED ?? "true") === "true", + level: SettingLevel.SystemAdminEditable, + label: "OTP WhatsApp Fallback Enabled", + group: "authentication-settings", + sortOrder: 85, + controlType: "boolean", + }, + { + moduleName: "solid-core", + key: "otpWhatsappTemplateId", + value: process.env.IAM_OTP_WHATSAPP_TEMPLATE_ID ?? "common_otp", + level: SettingLevel.SystemAdminEditable, + label: "OTP WhatsApp Template ID", + group: "authentication-settings", + sortOrder: 86, + controlType: "shortText", + }, + { + moduleName: "solid-core", + key: "otpWhatsappIndependentEnabled", + value: (process.env.IAM_OTP_WHATSAPP_INDEPENDENT_ENABLED ?? "true") === "true", + level: SettingLevel.SystemAdminEditable, + label: "OTP WhatsApp Independent Enabled", + group: "authentication-settings", + sortOrder: 87, + controlType: "boolean", + }, + { + moduleName: "solid-core", + key: "otpDefaultCountryDialCode", + value: process.env.IAM_OTP_DEFAULT_COUNTRY_DIAL_CODE ?? "", + level: SettingLevel.SystemAdminEditable, + label: "OTP Default Country Dial Code", + group: "authentication-settings", + sortOrder: 88, + controlType: "shortText", + }, { moduleName: "solid-core", key: "forgotPasswordVerificationTokenExpiry", @@ -1178,6 +1218,64 @@ const getSolidCoreSettings = (isProd: boolean) => sortOrder: 30, controlType: "shortText", }, + { + moduleName: "solid-core", + key: "metaWhatsappApiUrl", + value: process.env.COMMON_META_WHATSAPP_API_URL || "https://graph.facebook.com", + level: SettingLevel.SystemAdminReadonly, + label: "Meta WhatsApp API URL", + group: "whatsapp-settings", + sortOrder: 40, + controlType: "shortText", + }, + { + moduleName: "solid-core", + key: "metaWhatsappApiVersion", + value: process.env.COMMON_META_WHATSAPP_API_VERSION || "v23.0", + level: SettingLevel.SystemAdminReadonly, + label: "Meta WhatsApp API Version", + group: "whatsapp-settings", + sortOrder: 50, + controlType: "shortText", + }, + { + moduleName: "solid-core", + key: "metaWhatsappPhoneNumberId", + value: process.env.COMMON_META_WHATSAPP_PHONE_NUMBER_ID, + level: SettingLevel.SystemAdminReadonly, + label: "Meta WhatsApp Phone Number ID", + group: "whatsapp-settings", + sortOrder: 60, + controlType: "shortText", + }, + { + moduleName: "solid-core", + key: "metaWhatsappBusinessAccountId", + value: process.env.COMMON_META_WHATSAPP_BUSINESS_ACCOUNT_ID, + level: SettingLevel.SystemAdminReadonly, + label: "Meta WhatsApp Business Account ID", + group: "whatsapp-settings", + sortOrder: 70, + controlType: "shortText", + }, + { + moduleName: "solid-core", + key: "metaWhatsappAccessToken", + value: process.env.COMMON_META_WHATSAPP_ACCESS_TOKEN, + level: SettingLevel.SystemEnv, + }, + { + moduleName: "solid-core", + key: "metaWhatsappWebhookVerifyToken", + value: process.env.COMMON_META_WHATSAPP_WEBHOOK_VERIFY_TOKEN, + level: SettingLevel.SystemEnv, + }, + { + moduleName: "solid-core", + key: "metaWhatsappAppSecret", + value: process.env.COMMON_META_WHATSAPP_APP_SECRET, + level: SettingLevel.SystemEnv, + }, ] as const satisfies SettingDefinition[]; // 2. diff --git a/src/services/whatsapp/MetaCloudWhatsappService.ts b/src/services/whatsapp/MetaCloudWhatsappService.ts new file mode 100644 index 00000000..c709766b --- /dev/null +++ b/src/services/whatsapp/MetaCloudWhatsappService.ts @@ -0,0 +1,253 @@ +import { HttpService } from "@nestjs/axios"; +import { Injectable, Logger } from "@nestjs/common"; +import { AxiosError } from "axios"; +import { WhatsAppProvider } from "src/decorators/whatsapp-provider.decorator"; +import { IWhatsAppTransport } from "src/interfaces"; +import { QueueMessage } from "src/interfaces/mq"; +import { SettingService } from "src/services/setting.service"; +import type { SolidCoreSetting } from "src/services/settings/default-settings-provider.service"; + +type MetaTemplatePayload = { + type?: "template"; + templateName?: string; + templateId?: string; + languageCode?: string; + body?: string[]; + headerText?: string; + imageLink?: string; +}; + +type MetaTextPayload = { + type?: "text"; + text?: string; + previewUrl?: boolean; +}; + +@Injectable() +@WhatsAppProvider() +export class MetaCloudWhatsappService implements IWhatsAppTransport { + readonly logger = new Logger(MetaCloudWhatsappService.name); + + constructor( + private readonly httpService: HttpService, + private readonly settingService: SettingService, + ) {} + + async sendWhatsAppMessage( + to: string, + templateId: string, + parameters: any, + parentEntity?: any, + parentEntityId?: any, + ): Promise { + const message: QueueMessage = { + payload: { + to, + templateId, + ...parameters, + }, + parentEntity, + parentEntityId, + }; + + await this.sendWhatsAppMessageSynchronously(message); + return message; + } + + async sendWhatsAppMessageSynchronously(message: QueueMessage): Promise { + const phoneNumberId = this.settingService.getConfigValue( + "metaWhatsappPhoneNumberId", + ); + const apiVersion = this.settingService.getConfigValue( + "metaWhatsappApiVersion", + ); + const apiBaseUrl = this.settingService.getConfigValue( + "metaWhatsappApiUrl", + ); + const accessToken = this.settingService.getConfigValue( + "metaWhatsappAccessToken", + ); + + if (!phoneNumberId || !apiVersion || !apiBaseUrl || !accessToken) { + throw new Error( + "Missing Meta WhatsApp configuration. Set metaWhatsappApiUrl, metaWhatsappApiVersion, metaWhatsappPhoneNumberId, metaWhatsappAccessToken.", + ); + } + + const requestBody = this.createWhatsappRequest(message); + const url = `${String(apiBaseUrl).replace(/\/$/, "")}/${apiVersion}/${phoneNumberId}/messages`; + + try { + await this.httpService.axiosRef.post(url, requestBody, { + headers: { + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + }, + }); + this.logger.debug( + `Sent Meta WhatsApp message to ${message.payload?.to} with type ${requestBody.type}`, + ); + } catch (error) { + const axiosError = error as AxiosError; + const status = axiosError.response?.status; + const data = axiosError.response?.data; + this.logger.error( + `Meta WhatsApp send failed: status=${status ?? "unknown"}, url=${url}, response=${typeof data === "object" ? JSON.stringify(data) : data}`, + ); + throw error; + } + } + + private createWhatsappRequest(message: QueueMessage): any { + const payload = message?.payload ?? {}; + + if (payload?.payload) { + const normalizedFromWrappedPayload = this.normalizeWrappedPayload(payload.payload); + if (normalizedFromWrappedPayload) { + return normalizedFromWrappedPayload; + } + return payload.payload; + } + + const to = this.normalizePhone(payload.to); + const templatePayload: MetaTemplatePayload = payload; + const textPayload: MetaTextPayload = payload; + + if (textPayload.type === "text" || textPayload.text) { + return { + messaging_product: "whatsapp", + recipient_type: "individual", + to, + type: "text", + text: { + body: textPayload.text ?? "", + preview_url: Boolean(textPayload.previewUrl), + }, + }; + } + + const templateName = payload.templateId || templatePayload.templateId || templatePayload.templateName; + if (!templateName) { + throw new Error( + "Meta WhatsApp template name is missing. Provide templateId or parameters.templateName.", + ); + } + + const components: any[] = []; + if (templatePayload.headerText) { + components.push({ + type: "header", + parameters: [ + { + type: "text", + text: templatePayload.headerText, + }, + ], + }); + } else if (templatePayload.imageLink) { + components.push({ + type: "header", + parameters: [ + { + type: "image", + image: { link: templatePayload.imageLink }, + }, + ], + }); + } + + if (Array.isArray(templatePayload.body) && templatePayload.body.length > 0) { + components.push({ + type: "body", + parameters: templatePayload.body.map((entry) => ({ + type: "text", + text: String(entry), + })), + }); + } + + return { + messaging_product: "whatsapp", + recipient_type: "individual", + to, + type: "template", + template: { + name: templateName, + language: { + code: templatePayload.languageCode || "en", + }, + ...(components.length > 0 ? { components } : {}), + }, + }; + } + + private normalizeWrappedPayload(payload: any): any | null { + if (!payload || typeof payload !== "object") { + return null; + } + + if (payload.messaging_product && payload.to && payload.type) { + return payload; + } + + const destination = payload.destination || payload.to; + const message = payload.message || {}; + if (!destination || !message.type) { + return null; + } + + if (message.type === "text" && message.text) { + return { + messaging_product: "whatsapp", + recipient_type: "individual", + to: this.normalizePhone(destination), + type: "text", + text: { + body: String(message.text), + preview_url: false, + }, + }; + } + + if (message.type === "template" && message.template?.id) { + const params = Array.isArray(message.template?.params) + ? message.template.params + : []; + return { + messaging_product: "whatsapp", + recipient_type: "individual", + to: this.normalizePhone(destination), + type: "template", + template: { + name: String(message.template.id), + language: { + code: "en", + }, + ...(params.length > 0 + ? { + components: [ + { + type: "body", + parameters: params.map((entry: any) => ({ + type: "text", + text: String(entry), + })), + }, + ], + } + : {}), + }, + }; + } + + return null; + } + + private normalizePhone(phone: string): string { + const digits = String(phone || "").replace(/\D/g, ""); + if (!digits) { + throw new Error("Destination phone number is required for WhatsApp message."); + } + return digits; + } +} diff --git a/src/solid-core.module.ts b/src/solid-core.module.ts index e7e48abf..cc0bd30d 100755 --- a/src/solid-core.module.ts +++ b/src/solid-core.module.ts @@ -1,7 +1,7 @@ -import 'multer'; -import { Global, MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; -import * as express from 'express'; -import { ConfigModule, ConfigService } from '@nestjs/config'; +import "multer"; +import { Global, MiddlewareConsumer, Module, NestModule } from "@nestjs/common"; +import * as express from "express"; +import { ConfigModule, ConfigService } from "@nestjs/config"; import { APP_FILTER, APP_GUARD, @@ -9,136 +9,141 @@ import { DiscoveryService, MetadataScanner, Reflector, -} from '@nestjs/core'; -import { MulterModule } from '@nestjs/platform-express'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { RemoveFieldsCommand } from './commands/remove-fields.command'; -import { FieldMetadataController } from './controllers/field-metadata.controller'; -import { MediaStorageProviderMetadataController } from './controllers/media-storage-provider-metadata.controller'; -import { ModelMetadataController } from './controllers/model-metadata.controller'; -import { ModuleMetadataController } from './controllers/module-metadata.controller'; -import { TestController } from './controllers/test.controller'; -import { FieldMetadata } from './entities/field-metadata.entity'; -import { ListOfValues } from './entities/list-of-values.entity'; -import { MediaStorageProviderMetadata } from './entities/media-storage-provider-metadata.entity'; -import { Media } from './entities/media.entity'; -import { ModelMetadata } from './entities/model-metadata.entity'; -import { ModuleMetadata } from './entities/module-metadata.entity'; -import { CommandService } from './helpers/command.service'; -import { SchematicService } from './helpers/schematic.service'; -import { ListOfValuesSelectionProvider } from './services/selection-providers/list-of-values-selection-providers.service'; -import { PseudoForeignKeySelectionProvider } from './services/selection-providers/pseudo-foreign-key-selection-provider.service'; -import { ModuleMetadataSeederService } from './seeders/module-metadata-seeder.service'; -import { ModuleTestDataService } from './seeders/module-test-data.service'; -import { CrudHelperService } from './services/crud-helper.service'; -import { FieldMetadataService } from './services/field-metadata.service'; -import { ListOfValuesService } from './services/list-of-values.service'; +} from "@nestjs/core"; +import { MulterModule } from "@nestjs/platform-express"; +import { TypeOrmModule } from "@nestjs/typeorm"; +import { RemoveFieldsCommand } from "./commands/remove-fields.command"; +import { FieldMetadataController } from "./controllers/field-metadata.controller"; +import { MediaStorageProviderMetadataController } from "./controllers/media-storage-provider-metadata.controller"; +import { ModelMetadataController } from "./controllers/model-metadata.controller"; +import { ModuleMetadataController } from "./controllers/module-metadata.controller"; +import { TestController } from "./controllers/test.controller"; +import { FieldMetadata } from "./entities/field-metadata.entity"; +import { ListOfValues } from "./entities/list-of-values.entity"; +import { MediaStorageProviderMetadata } from "./entities/media-storage-provider-metadata.entity"; +import { Media } from "./entities/media.entity"; +import { ModelMetadata } from "./entities/model-metadata.entity"; +import { ModuleMetadata } from "./entities/module-metadata.entity"; +import { CommandService } from "./helpers/command.service"; +import { SchematicService } from "./helpers/schematic.service"; +import { ListOfValuesSelectionProvider } from "./services/selection-providers/list-of-values-selection-providers.service"; +import { PseudoForeignKeySelectionProvider } from "./services/selection-providers/pseudo-foreign-key-selection-provider.service"; +import { ModuleMetadataSeederService } from "./seeders/module-metadata-seeder.service"; +import { ModuleTestDataService } from "./seeders/module-test-data.service"; +import { CrudHelperService } from "./services/crud-helper.service"; +import { FieldMetadataService } from "./services/field-metadata.service"; +import { ListOfValuesService } from "./services/list-of-values.service"; // import { MediaStorageProviderMetadataSeederService } from './services/media-storage-provider-metadata-seeder.service'; -import { MediaStorageProviderMetadataService } from './services/media-storage-provider-metadata.service'; -import { MediaService } from './services/media.service'; -import { ModelMetadataService } from './services/model-metadata.service'; -import { ModuleMetadataService } from './services/module-metadata.service'; -import { SolidIntrospectService } from './services/solid-introspect.service'; +import { MediaStorageProviderMetadataService } from "./services/media-storage-provider-metadata.service"; +import { MediaService } from "./services/media.service"; +import { ModelMetadataService } from "./services/model-metadata.service"; +import { ModuleMetadataService } from "./services/module-metadata.service"; +import { SolidIntrospectService } from "./services/solid-introspect.service"; // import { ListOfComputedFieldProvider } from './providers/list-of-computed-field-provider.service'; -import { ServeStaticModule } from '@nestjs/serve-static'; -import { join } from 'path'; -import { RefreshModelCommand } from './commands/refresh-model.command'; -import { MediaController } from './controllers/media.controller'; +import { ServeStaticModule } from "@nestjs/serve-static"; +import { join } from "path"; +import { RefreshModelCommand } from "./commands/refresh-model.command"; +import { MediaController } from "./controllers/media.controller"; -import { RefreshModuleCommand } from './commands/refresh-module.command'; -import { ModelMetadataSubscriber } from './subscribers/model-metadata.subscriber'; +import { RefreshModuleCommand } from "./commands/refresh-module.command"; +import { ModelMetadataSubscriber } from "./subscribers/model-metadata.subscriber"; -import { ViewMetadataController } from './controllers/view-metadata.controller'; -import { ViewMetadata } from './entities/view-metadata.entity'; -import { ViewMetadataService } from './services/view-metadata.service'; +import { ViewMetadataController } from "./controllers/view-metadata.controller"; +import { ViewMetadata } from "./entities/view-metadata.entity"; +import { ViewMetadataService } from "./services/view-metadata.service"; -import { ActionMetadataController } from './controllers/action-metadata.controller'; -import { ActionMetadata } from './entities/action-metadata.entity'; -import { ActionMetadataService } from './services/action-metadata.service'; +import { ActionMetadataController } from "./controllers/action-metadata.controller"; +import { ActionMetadata } from "./entities/action-metadata.entity"; +import { ActionMetadataService } from "./services/action-metadata.service"; -import { FacebookAuthenticationController } from './controllers/facebook-authentication.controller'; -import { MicrosoftAuthenticationController } from './controllers/microsoft-authentication.controller'; -import { FacebookOAuthStrategy } from './passport-strategies/facebook-oauth.strategy'; -import { MicrosoftOAuthStrategy } from './passport-strategies/microsoft-oauth.strategy'; +import { FacebookAuthenticationController } from "./controllers/facebook-authentication.controller"; +import { MicrosoftAuthenticationController } from "./controllers/microsoft-authentication.controller"; +import { FacebookOAuthStrategy } from "./passport-strategies/facebook-oauth.strategy"; +import { MicrosoftOAuthStrategy } from "./passport-strategies/microsoft-oauth.strategy"; -import { HttpModule } from '@nestjs/axios'; -import { JwtModule } from '@nestjs/jwt'; -import { SeedCommand } from './commands/seed.command'; -import { TestDataCommand } from './commands/test-data.command'; -import { TestRunCommand } from './commands/run-tests.command'; -import { TestCommand } from './commands/test.command'; -import { AuthenticationController } from './controllers/authentication.controller'; -import { EmailTemplateController } from './controllers/email-template.controller'; -import { GoogleAuthenticationController } from './controllers/google-authentication.controller'; -import { MenuItemMetadataController } from './controllers/menu-item-metadata.controller'; -import { MqMessageQueueController } from './controllers/mq-message-queue.controller'; -import { MqMessageController } from './controllers/mq-message.controller'; -import { OTPAuthenticationController } from './controllers/otp-authentication.controller'; -import { ServiceController } from './controllers/service.controller'; -import { SmsTemplateController } from './controllers/sms-template.controller'; -import { TestQueueController } from './controllers/test-queue.controller'; -import { EmailAttachment } from './entities/email-attachment.entity'; -import { EmailTemplate } from './entities/email-template.entity'; -import { MenuItemMetadata } from './entities/menu-item-metadata.entity'; -import { MqMessageQueue } from './entities/mq-message-queue.entity'; -import { MqMessage } from './entities/mq-message.entity'; -import { SmsTemplate } from './entities/sms-template.entity'; -import { AccessTokenGuard } from './guards/access-token.guard'; -import { ApiKeyGuard } from './guards/api-key.guard'; -import { AuthenticationGuard } from './guards/authentication.guard'; -import { PermissionsGuard } from './guards/permissions.guard'; -import { SolidRegistry } from './helpers/solid-registry'; -import { LoggingInterceptor } from './interceptors/logging.interceptor'; -import { ApiEmailQueuePublisher } from './jobs/rabbitmq/api-email-publisher.service'; -import { ApiEmailQueueSubscriber } from './jobs/rabbitmq/api-email-subscriber.service'; -import { TestQueuePublisherDatabase } from './jobs/database/test-queue-publisher-database.service'; -import { TestQueueSubscriberDatabase } from './jobs/database/test-queue-subscriber-database.service'; -import { TestQueuePublisherRedis } from './jobs/redis/test-queue-publisher-redis.service'; -import { TestQueueSubscriberRedis } from './jobs/redis/test-queue-subscriber-redis.service'; -import { Msg91WhatsappQueuePublisher } from './jobs/rabbitmq/msg91-whatsapp-publisher.service'; -import { Msg91WhatsappQueueSubscriber } from './jobs/rabbitmq/msg91-whatsapp-subscriber.service'; -import { Msg91OTPQueuePublisher } from './jobs/rabbitmq/msg91-otp-publisher.service'; -import { Msg91OTPQueueSubscriber } from './jobs/rabbitmq/msg91-otp-subscriber.service'; -import { Msg91SmsQueuePublisher } from './jobs/rabbitmq/msg91-sms-publisher.service'; -import { Msg91SmsQueueSubscriber } from './jobs/rabbitmq/msg91-sms-subscriber.service'; -import { SmtpEmailQueuePublisherRabbitmq } from './jobs/rabbitmq/smtp-email-publisher.service'; -import { SmtpEmailQueueSubscriberRabbitmq } from './jobs/rabbitmq/smtp-email-subscriber.service'; -import { TestQueuePublisher } from './jobs/rabbitmq/test-queue-publisher.service'; -import { TestQueueSubscriber } from './jobs/rabbitmq/test-queue-subscriber.service'; -import { ChatterQueuePublisherRabbitmq } from './jobs/rabbitmq/chatter-queue-publisher.service'; -import { ChatterQueueSubscriberRabbitmq } from './jobs/rabbitmq/chatter-queue-subscriber.service'; -import { ChatterQueuePublisherDatabase } from './jobs/database/chatter-queue-publisher-database.service'; -import { ChatterQueueSubscriberDatabase } from './jobs/database/chatter-queue-subscriber-database.service'; -import { ApiEmailQueuePublisherRedis } from './jobs/redis/api-email-publisher-redis.service'; -import { ApiEmailQueueSubscriberRedis } from './jobs/redis/api-email-subscriber-redis.service'; -import { ChatterQueuePublisherRedis } from './jobs/redis/chatter-queue-publisher-redis.service'; -import { ChatterQueueSubscriberRedis } from './jobs/redis/chatter-queue-subscriber-redis.service'; -import { ComputedFieldEvaluationPublisherRedis } from './jobs/redis/computed-field-evaluation-publisher-redis.service'; -import { ComputedFieldEvaluationSubscriberRedis } from './jobs/redis/computed-field-evaluation-subscriber-redis.service'; -import { GenerateCodePublisherRedis } from './jobs/redis/generate-code-publisher-redis.service'; -import { GenerateCodeSubscriberRedis } from './jobs/redis/generate-code-subscriber-redis.service'; -import { Msg91OTPQueuePublisherRedis } from './jobs/redis/msg91-otp-publisher-redis.service'; -import { Msg91OTPQueueSubscriberRedis } from './jobs/redis/msg91-otp-subscriber-redis.service'; -import { Msg91SmsQueuePublisherRedis } from './jobs/redis/msg91-sms-publisher-redis.service'; -import { Msg91SmsQueueSubscriberRedis } from './jobs/redis/msg91-sms-subscriber-redis.service'; -import { Msg91WhatsappQueuePublisherRedis } from './jobs/redis/msg91-whatsapp-publisher-redis.service'; -import { Msg91WhatsappQueueSubscriberRedis } from './jobs/redis/msg91-whatsapp-subscriber-redis.service'; -import { SmtpEmailQueuePublisherRedis } from './jobs/redis/smtp-email-publisher-redis.service'; -import { SmtpEmailQueueSubscriberRedis } from './jobs/redis/smtp-email-subscriber-redis.service'; -import { Three60WhatsappQueuePublisherRedis } from './jobs/redis/three60-whatsapp-publisher-redis.service'; -import { Three60WhatsappQueueSubscriberRedis } from './jobs/redis/three60-whatsapp-subscriber-redis.service'; -import { TriggerMcpClientPublisherRedis } from './jobs/redis/trigger-mcp-client-publisher-redis.service'; -import { TriggerMcpClientSubscriberRedis } from './jobs/redis/trigger-mcp-client-subscriber-redis.service'; -import { TwilioSmsQueuePublisherRedis } from './jobs/redis/twilio-sms-publisher-redis.service'; -import { TwilioSmsQueueSubscriberRedis } from './jobs/redis/twilio-sms-subscriber-redis.service'; -import { UserRegistrationListener } from './listeners/user-registration.listener'; -import { GoogleOauthStrategy } from './passport-strategies/google-oauth.strategy'; -import { ApiKeyService } from './services/api-key.service'; -import { AuthenticationService } from './services/authentication.service'; -import { BcryptService } from './services/bcrypt.service'; -import { UuidExternalIdEntityComputedFieldProvider } from './services/computed-fields/entity/uuid-externalid-entity-computed-field-provider.service'; -import { UuidExternalIdComputedFieldProvider } from './services/computed-fields/uuid-external-id-computed-field-provider.service'; -import { EmailTemplateService } from './services/email-template.service'; +import { GupshupOtpWhatsappService } from "./services/whatsapp/GupshupOtpWhatsappService"; +import { MetaCloudWhatsappService } from "./services/whatsapp/MetaCloudWhatsappService"; +import { GupshupWebhookController } from "./controllers/gupshup-webhook.controller"; +import { MetaCloudWhatsappWebhookController } from "./controllers/meta-cloud-whatsapp-webhook.controller"; + +import { HttpModule } from "@nestjs/axios"; +import { JwtModule } from "@nestjs/jwt"; +import { SeedCommand } from "./commands/seed.command"; +import { TestDataCommand } from "./commands/test-data.command"; +import { TestRunCommand } from "./commands/run-tests.command"; +import { TestCommand } from "./commands/test.command"; +import { AuthenticationController } from "./controllers/authentication.controller"; +import { EmailTemplateController } from "./controllers/email-template.controller"; +import { GoogleAuthenticationController } from "./controllers/google-authentication.controller"; +import { MenuItemMetadataController } from "./controllers/menu-item-metadata.controller"; +import { MqMessageQueueController } from "./controllers/mq-message-queue.controller"; +import { MqMessageController } from "./controllers/mq-message.controller"; +import { OTPAuthenticationController } from "./controllers/otp-authentication.controller"; +import { ServiceController } from "./controllers/service.controller"; +import { SmsTemplateController } from "./controllers/sms-template.controller"; +import { TestQueueController } from "./controllers/test-queue.controller"; +import { EmailAttachment } from "./entities/email-attachment.entity"; +import { EmailTemplate } from "./entities/email-template.entity"; +import { MenuItemMetadata } from "./entities/menu-item-metadata.entity"; +import { MqMessageQueue } from "./entities/mq-message-queue.entity"; +import { MqMessage } from "./entities/mq-message.entity"; +import { SmsTemplate } from "./entities/sms-template.entity"; +import { AccessTokenGuard } from "./guards/access-token.guard"; +import { ApiKeyGuard } from "./guards/api-key.guard"; +import { AuthenticationGuard } from "./guards/authentication.guard"; +import { PermissionsGuard } from "./guards/permissions.guard"; +import { SolidRegistry } from "./helpers/solid-registry"; +import { LoggingInterceptor } from "./interceptors/logging.interceptor"; +import { ApiEmailQueuePublisher } from "./jobs/rabbitmq/api-email-publisher.service"; +import { ApiEmailQueueSubscriber } from "./jobs/rabbitmq/api-email-subscriber.service"; +import { TestQueuePublisherDatabase } from "./jobs/database/test-queue-publisher-database.service"; +import { TestQueueSubscriberDatabase } from "./jobs/database/test-queue-subscriber-database.service"; +import { TestQueuePublisherRedis } from "./jobs/redis/test-queue-publisher-redis.service"; +import { TestQueueSubscriberRedis } from "./jobs/redis/test-queue-subscriber-redis.service"; +import { Msg91WhatsappQueuePublisher } from "./jobs/rabbitmq/msg91-whatsapp-publisher.service"; +import { Msg91WhatsappQueueSubscriber } from "./jobs/rabbitmq/msg91-whatsapp-subscriber.service"; +import { Msg91OTPQueuePublisher } from "./jobs/rabbitmq/msg91-otp-publisher.service"; +import { Msg91OTPQueueSubscriber } from "./jobs/rabbitmq/msg91-otp-subscriber.service"; +import { Msg91SmsQueuePublisher } from "./jobs/rabbitmq/msg91-sms-publisher.service"; +import { Msg91SmsQueueSubscriber } from "./jobs/rabbitmq/msg91-sms-subscriber.service"; +import { SmtpEmailQueuePublisherRabbitmq } from "./jobs/rabbitmq/smtp-email-publisher.service"; +import { SmtpEmailQueueSubscriberRabbitmq } from "./jobs/rabbitmq/smtp-email-subscriber.service"; +import { TestQueuePublisher } from "./jobs/rabbitmq/test-queue-publisher.service"; +import { TestQueueSubscriber } from "./jobs/rabbitmq/test-queue-subscriber.service"; +import { ChatterQueuePublisherRabbitmq } from "./jobs/rabbitmq/chatter-queue-publisher.service"; +import { ChatterQueueSubscriberRabbitmq } from "./jobs/rabbitmq/chatter-queue-subscriber.service"; +import { ChatterQueuePublisherDatabase } from "./jobs/database/chatter-queue-publisher-database.service"; +import { ChatterQueueSubscriberDatabase } from "./jobs/database/chatter-queue-subscriber-database.service"; +import { ApiEmailQueuePublisherRedis } from "./jobs/redis/api-email-publisher-redis.service"; +import { ApiEmailQueueSubscriberRedis } from "./jobs/redis/api-email-subscriber-redis.service"; +import { ChatterQueuePublisherRedis } from "./jobs/redis/chatter-queue-publisher-redis.service"; +import { ChatterQueueSubscriberRedis } from "./jobs/redis/chatter-queue-subscriber-redis.service"; +import { ComputedFieldEvaluationPublisherRedis } from "./jobs/redis/computed-field-evaluation-publisher-redis.service"; +import { ComputedFieldEvaluationSubscriberRedis } from "./jobs/redis/computed-field-evaluation-subscriber-redis.service"; +import { GenerateCodePublisherRedis } from "./jobs/redis/generate-code-publisher-redis.service"; +import { GenerateCodeSubscriberRedis } from "./jobs/redis/generate-code-subscriber-redis.service"; +import { Msg91OTPQueuePublisherRedis } from "./jobs/redis/msg91-otp-publisher-redis.service"; +import { Msg91OTPQueueSubscriberRedis } from "./jobs/redis/msg91-otp-subscriber-redis.service"; +import { Msg91SmsQueuePublisherRedis } from "./jobs/redis/msg91-sms-publisher-redis.service"; +import { Msg91SmsQueueSubscriberRedis } from "./jobs/redis/msg91-sms-subscriber-redis.service"; +import { Msg91WhatsappQueuePublisherRedis } from "./jobs/redis/msg91-whatsapp-publisher-redis.service"; +import { Msg91WhatsappQueueSubscriberRedis } from "./jobs/redis/msg91-whatsapp-subscriber-redis.service"; +import { SmtpEmailQueuePublisherRedis } from "./jobs/redis/smtp-email-publisher-redis.service"; +import { SmtpEmailQueueSubscriberRedis } from "./jobs/redis/smtp-email-subscriber-redis.service"; +import { Three60WhatsappQueuePublisherRedis } from "./jobs/redis/three60-whatsapp-publisher-redis.service"; +import { Three60WhatsappQueueSubscriberRedis } from "./jobs/redis/three60-whatsapp-subscriber-redis.service"; +import { TriggerMcpClientPublisherRedis } from "./jobs/redis/trigger-mcp-client-publisher-redis.service"; +import { TriggerMcpClientSubscriberRedis } from "./jobs/redis/trigger-mcp-client-subscriber-redis.service"; +import { TwilioSmsQueuePublisherRedis } from "./jobs/redis/twilio-sms-publisher-redis.service"; +import { TwilioSmsQueueSubscriberRedis } from "./jobs/redis/twilio-sms-subscriber-redis.service"; +import { UserRegistrationListener } from "./listeners/user-registration.listener"; +import { GoogleOauthStrategy } from "./passport-strategies/google-oauth.strategy"; +import { ApiKeyService } from "./services/api-key.service"; +import { AuthenticationService } from "./services/authentication.service"; +import { BcryptService } from "./services/bcrypt.service"; +import { UuidExternalIdEntityComputedFieldProvider } from "./services/computed-fields/entity/uuid-externalid-entity-computed-field-provider.service"; +import { UuidExternalIdComputedFieldProvider } from "./services/computed-fields/uuid-external-id-computed-field-provider.service"; +import { EmailTemplateService } from "./services/email-template.service"; import { DiskFileService, S3FileService, @@ -146,251 +151,251 @@ import { DiskStoragePathBuilder, S3StoragePathBuilder, StoragePathBuilderFactory, -} from './services/file'; -import { HashingService } from './services/hashing.service'; -import { ElasticEmailService } from './services/mail/elastic-email.service'; -import { SMTPEMailService } from './services/mail/smtp-email.service'; -import { MenuItemMetadataService } from './services/menu-item-metadata.service'; -import { MqMessageQueueService } from './services/mq-message-queue.service'; -import { MqMessageService } from './services/mq-message.service'; -import { PdfService } from './services/pdf.service'; -import { RefreshTokenIdsStorageService } from './services/refresh-token-ids-storage.service'; -import { SsoCodeStorageService } from './services/sso-code-storage.service'; -import { ListOfModelsSelectionProvider } from './services/selection-providers/list-of-models-selection-provider.service'; -import { TinyUrlService } from './services/short-url/tiny-url.service'; -import { SmsTemplateService } from './services/sms-template.service'; -import { Msg91OTPService } from './services/sms/Msg91OTPService'; -import { Msg91SMSService } from './services/sms/Msg91SMSService'; +} from "./services/file"; +import { HashingService } from "./services/hashing.service"; +import { ElasticEmailService } from "./services/mail/elastic-email.service"; +import { SMTPEMailService } from "./services/mail/smtp-email.service"; +import { MenuItemMetadataService } from "./services/menu-item-metadata.service"; +import { MqMessageQueueService } from "./services/mq-message-queue.service"; +import { MqMessageService } from "./services/mq-message.service"; +import { PdfService } from "./services/pdf.service"; +import { RefreshTokenIdsStorageService } from "./services/refresh-token-ids-storage.service"; +import { SsoCodeStorageService } from "./services/sso-code-storage.service"; +import { ListOfModelsSelectionProvider } from "./services/selection-providers/list-of-models-selection-provider.service"; +import { TinyUrlService } from "./services/short-url/tiny-url.service"; +import { SmsTemplateService } from "./services/sms-template.service"; +import { Msg91OTPService } from "./services/sms/Msg91OTPService"; +import { Msg91SMSService } from "./services/sms/Msg91SMSService"; // import { UserService } from './services/user.service'; -import { Msg91WhatsappService } from './services/whatsapp/Msg91WhatsappService'; -import { SoftDeleteAwareEventSubscriber } from './subscribers/soft-delete-aware-event.subscriber'; +import { Msg91WhatsappService } from "./services/whatsapp/Msg91WhatsappService"; +import { SoftDeleteAwareEventSubscriber } from "./subscribers/soft-delete-aware-event.subscriber"; -import { PermissionMetadataController } from './controllers/permission-metadata.controller'; -import { PermissionMetadata } from './entities/permission-metadata.entity'; -import { PermissionMetadataService } from './services/permission-metadata.service'; +import { PermissionMetadataController } from "./controllers/permission-metadata.controller"; +import { PermissionMetadata } from "./entities/permission-metadata.entity"; +import { PermissionMetadataService } from "./services/permission-metadata.service"; -import { ScheduleModule } from '@nestjs/schedule'; -import { ClsModule } from 'nestjs-cls'; -import { AiInteractionController } from './controllers/ai-interaction.controller'; -import { ChatterMessageDetailsController } from './controllers/chatter-message-details.controller'; -import { ChatterMessageController } from './controllers/chatter-message.controller'; -import { DashboardQuestionSqlDatasetConfigController } from './controllers/dashboard-question-sql-dataset-config.controller'; -import { DashboardQuestionController } from './controllers/dashboard-question.controller'; -import { DashboardVariableController } from './controllers/dashboard-variable.controller'; -import { DashboardLayoutController } from './controllers/dashboard-layout.controller'; +import { ScheduleModule } from "@nestjs/schedule"; +import { ClsModule } from "nestjs-cls"; +import { AiInteractionController } from "./controllers/ai-interaction.controller"; +import { ChatterMessageDetailsController } from "./controllers/chatter-message-details.controller"; +import { ChatterMessageController } from "./controllers/chatter-message.controller"; +import { DashboardQuestionSqlDatasetConfigController } from "./controllers/dashboard-question-sql-dataset-config.controller"; +import { DashboardQuestionController } from "./controllers/dashboard-question.controller"; +import { DashboardVariableController } from "./controllers/dashboard-variable.controller"; +import { DashboardLayoutController } from "./controllers/dashboard-layout.controller"; -import { DashboardController } from './controllers/dashboard.controller'; -import { ExportTemplateController } from './controllers/export-template.controller'; -import { ExportTransactionController } from './controllers/export-transaction.controller'; -import { ImportTransactionErrorLogController } from './controllers/import-transaction-error-log.controller'; -import { ImportTransactionController } from './controllers/import-transaction.controller'; -import { ListOfValuesController } from './controllers/list-of-values.controller'; -import { LocaleController } from './controllers/locale.controller'; -import { RoleMetadataController } from './controllers/role-metadata.controller'; -import { SavedFiltersController } from './controllers/saved-filters.controller'; -import { ScheduledJobController } from './controllers/scheduled-job.controller'; -import { AgentSessionController } from './controllers/agent-session.controller'; -import { AgentEventController } from './controllers/agent-event.controller'; -import { SecurityRuleController } from './controllers/security-rule.controller'; -import { SettingController } from './controllers/setting.controller'; -import { InfoController } from './controllers/info.controller'; -import { InfoService } from './services/info.service'; -import { UserActivityHistoryController } from './controllers/user-activity-history.controller'; -import { UserViewMetadataController } from './controllers/user-view-metadata.controller'; -import { UserController } from './controllers/user.controller'; -import { AiInteraction } from './entities/ai-interaction.entity'; -import { ChatterMessageDetails } from './entities/chatter-message-details.entity'; -import { ChatterMessage } from './entities/chatter-message.entity'; -import { DashboardQuestionSqlDatasetConfig } from './entities/dashboard-question-sql-dataset-config.entity'; -import { DashboardQuestion } from './entities/dashboard-question.entity'; -import { DashboardVariable } from './entities/dashboard-variable.entity'; -import { DashboardLayout } from './entities/dashboard-layout.entity'; +import { DashboardController } from "./controllers/dashboard.controller"; +import { ExportTemplateController } from "./controllers/export-template.controller"; +import { ExportTransactionController } from "./controllers/export-transaction.controller"; +import { ImportTransactionErrorLogController } from "./controllers/import-transaction-error-log.controller"; +import { ImportTransactionController } from "./controllers/import-transaction.controller"; +import { ListOfValuesController } from "./controllers/list-of-values.controller"; +import { LocaleController } from "./controllers/locale.controller"; +import { RoleMetadataController } from "./controllers/role-metadata.controller"; +import { SavedFiltersController } from "./controllers/saved-filters.controller"; +import { ScheduledJobController } from "./controllers/scheduled-job.controller"; +import { AgentSessionController } from "./controllers/agent-session.controller"; +import { AgentEventController } from "./controllers/agent-event.controller"; +import { SecurityRuleController } from "./controllers/security-rule.controller"; +import { SettingController } from "./controllers/setting.controller"; +import { InfoController } from "./controllers/info.controller"; +import { InfoService } from "./services/info.service"; +import { UserActivityHistoryController } from "./controllers/user-activity-history.controller"; +import { UserViewMetadataController } from "./controllers/user-view-metadata.controller"; +import { UserController } from "./controllers/user.controller"; +import { AiInteraction } from "./entities/ai-interaction.entity"; +import { ChatterMessageDetails } from "./entities/chatter-message-details.entity"; +import { ChatterMessage } from "./entities/chatter-message.entity"; +import { DashboardQuestionSqlDatasetConfig } from "./entities/dashboard-question-sql-dataset-config.entity"; +import { DashboardQuestion } from "./entities/dashboard-question.entity"; +import { DashboardVariable } from "./entities/dashboard-variable.entity"; +import { DashboardLayout } from "./entities/dashboard-layout.entity"; -import { Dashboard } from './entities/dashboard.entity'; -import { ExportTemplate } from './entities/export-template.entity'; -import { ExportTransaction } from './entities/export-transaction.entity'; -import { ImportTransactionErrorLog } from './entities/import-transaction-error-log.entity'; -import { ImportTransaction } from './entities/import-transaction.entity'; -import { Locale } from './entities/locale.entity'; -import { RoleMetadata } from './entities/role-metadata.entity'; -import { SavedFilters } from './entities/saved-filters.entity'; -import { ScheduledJob } from './entities/scheduled-job.entity'; -import { AgentSession } from './entities/agent-session.entity'; -import { AgentEvent } from './entities/agent-event.entity'; -import { SecurityRule } from './entities/security-rule.entity'; -import { Setting } from './entities/setting.entity'; -import { UserActivityHistory } from './entities/user-activity-history.entity'; -import { UserViewMetadata } from './entities/user-view-metadata.entity'; -import { UserApiKey } from './entities/user-api-key.entity'; -import { User } from './entities/user.entity'; -import { HttpExceptionFilter } from './filters/http-exception.filter'; -import { ModelMetadataHelperService } from './helpers/model-metadata-helper.service'; -import { ModuleMetadataHelperService } from './helpers/module-metadata-helper.service'; -import { ApiEmailQueuePublisherDatabase } from './jobs/database/api-email-publisher-database.service'; -import { ApiEmailQueueSubscriberDatabase } from './jobs/database/api-email-subscriber-database.service'; -import { ComputedFieldEvaluationPublisherDatabase } from './jobs/database/computed-field-evaluation-publisher-database.service'; -import { ComputedFieldEvaluationSubscriberDatabase } from './jobs/database/computed-field-evaluation-subscriber-database.service'; -import { GenerateCodePublisherDatabase } from './jobs/database/generate-code-publisher-database.service'; -import { GenerateCodeSubscriberDatabase } from './jobs/database/generate-code-subscriber-database.service'; -import { OTPQueuePublisherDatabase } from './jobs/database/otp-publisher-database.service'; -import { OTPQueueSubscriberDatabase } from './jobs/database/otp-subscriber-database.service'; -import { Msg91SmsQueuePublisherDatabase } from './jobs/database/msg91-sms-publisher-database.service'; -import { Msg91SmsQueueSubscriberDatabase } from './jobs/database/msg91-sms-subscriber-database.service'; -import { SmtpEmailQueuePublisherDatabase } from './jobs/database/smtp-email-publisher-database.service'; -import { SmtpEmailQueueSubscriberDatabase } from './jobs/database/smtp-email-subscriber-database.service'; +import { Dashboard } from "./entities/dashboard.entity"; +import { ExportTemplate } from "./entities/export-template.entity"; +import { ExportTransaction } from "./entities/export-transaction.entity"; +import { ImportTransactionErrorLog } from "./entities/import-transaction-error-log.entity"; +import { ImportTransaction } from "./entities/import-transaction.entity"; +import { Locale } from "./entities/locale.entity"; +import { RoleMetadata } from "./entities/role-metadata.entity"; +import { SavedFilters } from "./entities/saved-filters.entity"; +import { ScheduledJob } from "./entities/scheduled-job.entity"; +import { AgentSession } from "./entities/agent-session.entity"; +import { AgentEvent } from "./entities/agent-event.entity"; +import { SecurityRule } from "./entities/security-rule.entity"; +import { Setting } from "./entities/setting.entity"; +import { UserActivityHistory } from "./entities/user-activity-history.entity"; +import { UserViewMetadata } from "./entities/user-view-metadata.entity"; +import { UserApiKey } from "./entities/user-api-key.entity"; +import { User } from "./entities/user.entity"; +import { HttpExceptionFilter } from "./filters/http-exception.filter"; +import { ModelMetadataHelperService } from "./helpers/model-metadata-helper.service"; +import { ModuleMetadataHelperService } from "./helpers/module-metadata-helper.service"; +import { ApiEmailQueuePublisherDatabase } from "./jobs/database/api-email-publisher-database.service"; +import { ApiEmailQueueSubscriberDatabase } from "./jobs/database/api-email-subscriber-database.service"; +import { ComputedFieldEvaluationPublisherDatabase } from "./jobs/database/computed-field-evaluation-publisher-database.service"; +import { ComputedFieldEvaluationSubscriberDatabase } from "./jobs/database/computed-field-evaluation-subscriber-database.service"; +import { GenerateCodePublisherDatabase } from "./jobs/database/generate-code-publisher-database.service"; +import { GenerateCodeSubscriberDatabase } from "./jobs/database/generate-code-subscriber-database.service"; +import { OTPQueuePublisherDatabase } from "./jobs/database/otp-publisher-database.service"; +import { OTPQueueSubscriberDatabase } from "./jobs/database/otp-subscriber-database.service"; +import { Msg91SmsQueuePublisherDatabase } from "./jobs/database/msg91-sms-publisher-database.service"; +import { Msg91SmsQueueSubscriberDatabase } from "./jobs/database/msg91-sms-subscriber-database.service"; +import { SmtpEmailQueuePublisherDatabase } from "./jobs/database/smtp-email-publisher-database.service"; +import { SmtpEmailQueueSubscriberDatabase } from "./jobs/database/smtp-email-subscriber-database.service"; -import { TwilioSmsQueuePublisherDatabase } from './jobs/database/twilio-sms-publisher-database.service'; -import { TwilioSmsQueueSubscriberDatabase } from './jobs/database/twilio-sms-subscriber-database.service'; +import { TwilioSmsQueuePublisherDatabase } from "./jobs/database/twilio-sms-publisher-database.service"; +import { TwilioSmsQueueSubscriberDatabase } from "./jobs/database/twilio-sms-subscriber-database.service"; // import { ThrottlerModule } from '@nestjs/throttler'; -import { IngestCommand } from './commands/ingest.command'; -import { MailFactory } from './factories/mail.factory'; -import { ErrorMapperService } from './helpers/error-mapper.service'; -import { SolidCoreErrorCodesProvider } from './helpers/solid-core-error-codes-provider.service'; -import { ComputedFieldEvaluationPublisherRabbitmq } from './jobs/rabbitmq/computed-field-evaluation-publisher.service'; -import { ComputedFieldEvaluationSubscriberRabbitmq } from './jobs/rabbitmq/computed-field-evaluation-subscriber.service'; -import { Msg91WhatsappQueuePublisherDatabase } from './jobs/database/msg91-whatsapp-publisher-database.service'; -import { Msg91WhatsappQueueSubscriberDatabase } from './jobs/database/msg91-whatsapp-subscriber-database.service'; -import { Three60WhatsappQueuePublisherDatabase } from './jobs/database/three60-whatsapp-publisher-database.service'; -import { Three60WhatsappQueueSubscriberDatabase } from './jobs/database/three60-whatsapp-subscriber-database.service'; -import { TriggerMcpClientPublisherDatabase } from './jobs/database/trigger-mcp-client-publisher-database.service'; -import { TriggerMcpClientSubscriberDatabase } from './jobs/database/trigger-mcp-client-subscriber-database.service'; -import { GenerateCodePublisherRabbitmq } from './jobs/rabbitmq/generate-code-publisher.service'; -import { GenerateCodeSubscriberRabbitmq } from './jobs/rabbitmq/generate-code-subscriber.service'; -import { Three60WhatsappQueuePublisher } from './jobs/rabbitmq/three60-whatsapp-publisher.service'; -import { Three60WhatsappQueueSubscriber } from './jobs/rabbitmq/three60-whatsapp-subscriber.service'; -import { TriggerMcpClientPublisherRabbitmq } from './jobs/rabbitmq/trigger-mcp-client-publisher.service'; -import { TriggerMcpClientSubscriberRabbitmq } from './jobs/rabbitmq/trigger-mcp-client-subscriber.service'; -import { TwilioSmsQueuePublisherRabbitmq } from './jobs/rabbitmq/twilio-sms-publisher.service'; -import { TwilioSmsQueueSubscriberRabbitmq } from './jobs/rabbitmq/twilio-sms-subscriber.service'; -import { DashboardMapper } from './mappers/dashboard-mapper'; -import { ListOfValuesMapper } from './mappers/list-of-values-mapper'; -import { ActionMetadataRepository } from './repository/action-metadata.repository'; -import { AiInteractionRepository } from './repository/ai-interaction.repository'; -import { ChatterMessageDetailsRepository } from './repository/chatter-message-details.repository'; -import { ChatterMessageRepository } from './repository/chatter-message.repository'; -import { DashboardQuestionSqlDatasetConfigRepository } from './repository/dashboard-question-sql-dataset-config.repository'; -import { DashboardQuestionRepository } from './repository/dashboard-question.repository'; -import { DashboardVariableRepository } from './repository/dashboard-variable.repository'; -import { DashboardRepository } from './repository/dashboard.repository'; -import { DashboardLayoutRepository } from './repository/dashboard-layout.repository'; +import { IngestCommand } from "./commands/ingest.command"; +import { MailFactory } from "./factories/mail.factory"; +import { ErrorMapperService } from "./helpers/error-mapper.service"; +import { SolidCoreErrorCodesProvider } from "./helpers/solid-core-error-codes-provider.service"; +import { ComputedFieldEvaluationPublisherRabbitmq } from "./jobs/rabbitmq/computed-field-evaluation-publisher.service"; +import { ComputedFieldEvaluationSubscriberRabbitmq } from "./jobs/rabbitmq/computed-field-evaluation-subscriber.service"; +import { Msg91WhatsappQueuePublisherDatabase } from "./jobs/database/msg91-whatsapp-publisher-database.service"; +import { Msg91WhatsappQueueSubscriberDatabase } from "./jobs/database/msg91-whatsapp-subscriber-database.service"; +import { Three60WhatsappQueuePublisherDatabase } from "./jobs/database/three60-whatsapp-publisher-database.service"; +import { Three60WhatsappQueueSubscriberDatabase } from "./jobs/database/three60-whatsapp-subscriber-database.service"; +import { TriggerMcpClientPublisherDatabase } from "./jobs/database/trigger-mcp-client-publisher-database.service"; +import { TriggerMcpClientSubscriberDatabase } from "./jobs/database/trigger-mcp-client-subscriber-database.service"; +import { GenerateCodePublisherRabbitmq } from "./jobs/rabbitmq/generate-code-publisher.service"; +import { GenerateCodeSubscriberRabbitmq } from "./jobs/rabbitmq/generate-code-subscriber.service"; +import { Three60WhatsappQueuePublisher } from "./jobs/rabbitmq/three60-whatsapp-publisher.service"; +import { Three60WhatsappQueueSubscriber } from "./jobs/rabbitmq/three60-whatsapp-subscriber.service"; +import { TriggerMcpClientPublisherRabbitmq } from "./jobs/rabbitmq/trigger-mcp-client-publisher.service"; +import { TriggerMcpClientSubscriberRabbitmq } from "./jobs/rabbitmq/trigger-mcp-client-subscriber.service"; +import { TwilioSmsQueuePublisherRabbitmq } from "./jobs/rabbitmq/twilio-sms-publisher.service"; +import { TwilioSmsQueueSubscriberRabbitmq } from "./jobs/rabbitmq/twilio-sms-subscriber.service"; +import { DashboardMapper } from "./mappers/dashboard-mapper"; +import { ListOfValuesMapper } from "./mappers/list-of-values-mapper"; +import { ActionMetadataRepository } from "./repository/action-metadata.repository"; +import { AiInteractionRepository } from "./repository/ai-interaction.repository"; +import { ChatterMessageDetailsRepository } from "./repository/chatter-message-details.repository"; +import { ChatterMessageRepository } from "./repository/chatter-message.repository"; +import { DashboardQuestionSqlDatasetConfigRepository } from "./repository/dashboard-question-sql-dataset-config.repository"; +import { DashboardQuestionRepository } from "./repository/dashboard-question.repository"; +import { DashboardVariableRepository } from "./repository/dashboard-variable.repository"; +import { DashboardRepository } from "./repository/dashboard.repository"; +import { DashboardLayoutRepository } from "./repository/dashboard-layout.repository"; -import { EmailTemplateRepository } from './repository/email-template.repository'; -import { ExportTemplateRepository } from './repository/export-template.repository'; -import { ExportTransactionRepository } from './repository/export-transaction.repository'; -import { FieldMetadataRepository } from './repository/field-metadata.repository'; -import { ImportTransactionErrorLogRepository } from './repository/import-transaction-error-log.repository'; -import { ImportTransactionRepository } from './repository/import-transaction.repository'; -import { ListOfValuesRepository } from './repository/list-of-values.repository'; -import { LocaleRepository } from './repository/locale.repository'; -import { MediaRepository } from './repository/media.repository'; -import { MenuItemMetadataRepository } from './repository/menu-item-metadata.repository'; -import { ModelMetadataRepository } from './repository/model-metadata.repository'; -import { ModuleMetadataRepository } from './repository/module-metadata.repository'; -import { MqMessageQueueRepository } from './repository/mq-message-queue.repository'; -import { MqMessageRepository } from './repository/mq-message.repository'; -import { PermissionMetadataRepository } from './repository/permission-metadata.repository'; -import { RoleMetadataRepository } from './repository/role-metadata.repository'; -import { SavedFiltersRepository } from './repository/saved-filters.repository'; -import { ScheduledJobRepository } from './repository/scheduled-job.repository'; -import { AgentSessionRepository } from './repository/agent-session.repository'; -import { AgentEventRepository } from './repository/agent-event.repository'; -import { SecurityRuleRepository } from './repository/security-rule.repository'; -import { SettingRepository } from './repository/setting.repository'; -import { SmsTemplateRepository } from './repository/sms-template.repository'; -import { UserActivityHistoryRepository } from './repository/user-activity-history.repository'; -import { UserViewMetadataRepository } from './repository/user-view-metadata.repository'; -import { UserApiKeyRepository } from './repository/user-api-key.repository'; -import { UserRepository } from './repository/user.repository'; -import { ViewMetadataRepository } from './repository/view-metadata.repository'; -import { PermissionMetadataSeederService } from './seeders/permission-metadata-seeder.service'; -import { SystemFieldsSeederService } from './seeders/system-fields-seeder.service'; -import { AiInteractionService } from './services/ai-interaction.service'; -import { ChatterMessageDetailsService } from './services/chatter-message-details.service'; -import { ChatterMessageService } from './services/chatter-message.service'; -import { ConcatComputedFieldProvider } from './services/computed-fields/concat-computed-field-provider.service'; -import { AlphaNumExternalIdComputationProvider } from './services/computed-fields/entity/alpha-num-external-id-computed-field-provider'; -import { ConcatEntityComputedFieldProvider } from './services/computed-fields/entity/concat-entity-computed-field-provider.service'; -import { NoopsEntityComputedFieldProviderService } from './services/computed-fields/entity/noops-entity-computed-field-provider.service'; -import { CRUDService } from './services/crud.service'; -import { CsvService } from './services/csv.service'; -import { DashboardQuestionSqlDatasetConfigService } from './services/dashboard-question-sql-dataset-config.service'; -import { DashboardQuestionService } from './services/dashboard-question.service'; -import { DashboardVariableSQLDynamicProvider } from './services/dashboard-selection-providers/dashboard-variable-sql-dynamic-provider.service'; -import { DasbhoardVariableTestDynamicProvider } from './services/dashboard-selection-providers/dashboard-variable-test-dynamic-provider.service'; -import { DashboardVariableService } from './services/dashboard-variable.service'; -import { DashboardService } from './services/dashboard.service'; -import { DashboardLayoutService } from './services/dashboard-layout.service'; +import { EmailTemplateRepository } from "./repository/email-template.repository"; +import { ExportTemplateRepository } from "./repository/export-template.repository"; +import { ExportTransactionRepository } from "./repository/export-transaction.repository"; +import { FieldMetadataRepository } from "./repository/field-metadata.repository"; +import { ImportTransactionErrorLogRepository } from "./repository/import-transaction-error-log.repository"; +import { ImportTransactionRepository } from "./repository/import-transaction.repository"; +import { ListOfValuesRepository } from "./repository/list-of-values.repository"; +import { LocaleRepository } from "./repository/locale.repository"; +import { MediaRepository } from "./repository/media.repository"; +import { MenuItemMetadataRepository } from "./repository/menu-item-metadata.repository"; +import { ModelMetadataRepository } from "./repository/model-metadata.repository"; +import { ModuleMetadataRepository } from "./repository/module-metadata.repository"; +import { MqMessageQueueRepository } from "./repository/mq-message-queue.repository"; +import { MqMessageRepository } from "./repository/mq-message.repository"; +import { PermissionMetadataRepository } from "./repository/permission-metadata.repository"; +import { RoleMetadataRepository } from "./repository/role-metadata.repository"; +import { SavedFiltersRepository } from "./repository/saved-filters.repository"; +import { ScheduledJobRepository } from "./repository/scheduled-job.repository"; +import { AgentSessionRepository } from "./repository/agent-session.repository"; +import { AgentEventRepository } from "./repository/agent-event.repository"; +import { SecurityRuleRepository } from "./repository/security-rule.repository"; +import { SettingRepository } from "./repository/setting.repository"; +import { SmsTemplateRepository } from "./repository/sms-template.repository"; +import { UserActivityHistoryRepository } from "./repository/user-activity-history.repository"; +import { UserViewMetadataRepository } from "./repository/user-view-metadata.repository"; +import { UserApiKeyRepository } from "./repository/user-api-key.repository"; +import { UserRepository } from "./repository/user.repository"; +import { ViewMetadataRepository } from "./repository/view-metadata.repository"; +import { PermissionMetadataSeederService } from "./seeders/permission-metadata-seeder.service"; +import { SystemFieldsSeederService } from "./seeders/system-fields-seeder.service"; +import { AiInteractionService } from "./services/ai-interaction.service"; +import { ChatterMessageDetailsService } from "./services/chatter-message-details.service"; +import { ChatterMessageService } from "./services/chatter-message.service"; +import { ConcatComputedFieldProvider } from "./services/computed-fields/concat-computed-field-provider.service"; +import { AlphaNumExternalIdComputationProvider } from "./services/computed-fields/entity/alpha-num-external-id-computed-field-provider"; +import { ConcatEntityComputedFieldProvider } from "./services/computed-fields/entity/concat-entity-computed-field-provider.service"; +import { NoopsEntityComputedFieldProviderService } from "./services/computed-fields/entity/noops-entity-computed-field-provider.service"; +import { CRUDService } from "./services/crud.service"; +import { CsvService } from "./services/csv.service"; +import { DashboardQuestionSqlDatasetConfigService } from "./services/dashboard-question-sql-dataset-config.service"; +import { DashboardQuestionService } from "./services/dashboard-question.service"; +import { DashboardVariableSQLDynamicProvider } from "./services/dashboard-selection-providers/dashboard-variable-sql-dynamic-provider.service"; +import { DasbhoardVariableTestDynamicProvider } from "./services/dashboard-selection-providers/dashboard-variable-test-dynamic-provider.service"; +import { DashboardVariableService } from "./services/dashboard-variable.service"; +import { DashboardService } from "./services/dashboard.service"; +import { DashboardLayoutService } from "./services/dashboard-layout.service"; -import { ExcelService } from './services/excel.service'; -import { ExportTemplateService } from './services/export-template.service'; -import { ExportTransactionService } from './services/export-transaction.service'; -import { IngestMetadataService } from './services/genai/ingest-metadata.service'; -import { McpHandlerFactory } from './services/genai/mcp-handlers/mcp-handler-factory.service'; -import { R2RHelperService } from './services/genai/r2r-helper.service'; -import { ImportTransactionErrorLogService } from './services/import-transaction-error-log.service'; -import { ImportTransactionService } from './services/import-transaction.service'; -import { LocaleService } from './services/locale.service'; -import { FileS3StorageProvider } from './services/mediaStorageProviders/file-s3-storage-provider'; -import { FileStorageProvider } from './services/mediaStorageProviders/file-storage-provider'; -import { PollerService } from './services/poller.service'; -import { ChartJsSqlDataProvider } from './services/question-data-providers/chartjs-sql-data-provider.service'; -import { PrimeReactDatatableSqlDataProvider } from './services/question-data-providers/prime-react-datatable-sql-data-provider.service'; -import { PrimeReactMeterGroupSqlDataProvider } from './services/question-data-providers/prime-react-meter-group-sql-data-provider.service'; -import { PublisherFactory } from './services/queues/publisher-factory.service'; -import { RequestContextService } from './services/request-context.service'; -import { RoleMetadataService } from './services/role-metadata.service'; -import { SavedFiltersService } from './services/saved-filters.service'; -import { ScheduledJobService } from './services/scheduled-job.service'; -import { AgentSessionService } from './services/agent-session.service'; -import { AgentEventService } from './services/agent-event.service'; -import { SchedulerServiceImpl } from './services/scheduled-jobs/scheduler.service'; -import { SecurityRuleService } from './services/security-rule.service'; -import { ListOfDashboardQuestionProvidersSelectionProvider } from './services/selection-providers/list-of-dashboard-question-providers-selection-provider.service'; -import { ListOfDashboardVariableProvidersSelectionProvider } from './services/selection-providers/list-of-dashboard-variable-providers-selection-provider.service'; -import { ListOfScheduledJobsSelectionProvider } from './services/selection-providers/list-of-scheduled-jobs-selection-provider.service'; -import { LocaleListSelectionProvider } from './services/selection-providers/locale-list-selection-provider.service'; -import { SettingService } from './services/setting.service'; -import { TwilioSMSService } from './services/sms/TwilioSMSService'; -import { SolidTsMorphService } from './services/solid-ts-morph.service'; -import { SqlExpressionResolverService } from './services/sql-expression-resolver.service'; -import { TextractService } from './services/textract.service'; -import { UserActivityHistoryService } from './services/user-activity-history.service'; -import { UserViewMetadataService } from './services/user-view-metadata.service'; -import { UserService } from './services/user.service'; -import { Three60WhatsappService } from './services/whatsapp/Three60WhatsappService'; -import { AuditSubscriber } from './subscribers/audit.subscriber'; -import { ComputedEntityFieldSubscriber } from './subscribers/computed-entity-field.subscriber'; -import { CreatedByUpdatedBySubscriber } from './subscribers/created-by-updated-by.subscriber'; -import { DashboardQuestionSqlDatasetConfigSubscriber } from './subscribers/dashboard-question-sql-dataset-config.subscriber'; -import { DashboardQuestionSubscriber } from './subscribers/dashboard-question.subscriber'; -import { DashboardVariableSubscriber } from './subscribers/dashboard-variable.subscriber'; -import { DashboardSubscriber } from './subscribers/dashboard.subscriber'; -import { ListOfValuesSubscriber } from './subscribers/list-of-values.subscriber'; -import { ScheduledJobSubscriber } from './subscribers/scheduled-job.subscriber'; -import { SecurityRuleSubscriber } from './subscribers/security-rule.subscriber'; -import { ViewMetadataSubsciber } from './subscribers/view-metadata.subscriber'; -import { MediaStorageProviderMetadataRepository } from './repository/media-storage-provider-metadata.repository'; -import { McpCommand } from './commands/mcp.command'; -import { FixturesService } from './services/fixtures.service'; -import { FixturesSetupCommand } from './commands/fixtures/fixtures-setup.command'; -import { FixturesTearDownCommand } from './commands/fixtures/fixtures-tear-down.command'; -import { DatabaseBootstrapService } from './services/database/database-bootstrap.service'; -import { SequenceNumComputedFieldProvider } from './services/computed-fields/entity/sequence-num-computed-field-provider'; -import { ModelSequence } from './entities/model-sequence.entity'; -import { ModelSequenceService } from './services/model-sequence.service'; -import { ModelSequenceController } from './controllers/model-sequence.controller'; -import { ModelSequenceRepository } from './repository/model-sequence.repository'; -import { CacheModule } from '@nestjs/cache-manager'; -import { CacheManagerOptions } from './config/cache.options'; -import { SolidCoreDefaultSettingsProvider } from './services/settings/default-settings-provider.service'; -import { SmsFactory } from './factories/sms.factory'; -import { WhatsAppFactory } from './factories/whatsapp.factory'; -import { ImageEncodingService } from './helpers/image-encoding.helper'; -import { SolidMicroserviceAdapter } from './helpers/solid-microservice-adapter.service'; -import { InfoCommand } from './commands/info.command'; -import { ListOfRolesSelectionProvider } from './services/selection-providers/list-of-roles-selectionproviders.service'; -import { Entity } from 'typeorm'; +import { ExcelService } from "./services/excel.service"; +import { ExportTemplateService } from "./services/export-template.service"; +import { ExportTransactionService } from "./services/export-transaction.service"; +import { IngestMetadataService } from "./services/genai/ingest-metadata.service"; +import { McpHandlerFactory } from "./services/genai/mcp-handlers/mcp-handler-factory.service"; +import { R2RHelperService } from "./services/genai/r2r-helper.service"; +import { ImportTransactionErrorLogService } from "./services/import-transaction-error-log.service"; +import { ImportTransactionService } from "./services/import-transaction.service"; +import { LocaleService } from "./services/locale.service"; +import { FileS3StorageProvider } from "./services/mediaStorageProviders/file-s3-storage-provider"; +import { FileStorageProvider } from "./services/mediaStorageProviders/file-storage-provider"; +import { PollerService } from "./services/poller.service"; +import { ChartJsSqlDataProvider } from "./services/question-data-providers/chartjs-sql-data-provider.service"; +import { PrimeReactDatatableSqlDataProvider } from "./services/question-data-providers/prime-react-datatable-sql-data-provider.service"; +import { PrimeReactMeterGroupSqlDataProvider } from "./services/question-data-providers/prime-react-meter-group-sql-data-provider.service"; +import { PublisherFactory } from "./services/queues/publisher-factory.service"; +import { RequestContextService } from "./services/request-context.service"; +import { RoleMetadataService } from "./services/role-metadata.service"; +import { SavedFiltersService } from "./services/saved-filters.service"; +import { ScheduledJobService } from "./services/scheduled-job.service"; +import { AgentSessionService } from "./services/agent-session.service"; +import { AgentEventService } from "./services/agent-event.service"; +import { SchedulerServiceImpl } from "./services/scheduled-jobs/scheduler.service"; +import { SecurityRuleService } from "./services/security-rule.service"; +import { ListOfDashboardQuestionProvidersSelectionProvider } from "./services/selection-providers/list-of-dashboard-question-providers-selection-provider.service"; +import { ListOfDashboardVariableProvidersSelectionProvider } from "./services/selection-providers/list-of-dashboard-variable-providers-selection-provider.service"; +import { ListOfScheduledJobsSelectionProvider } from "./services/selection-providers/list-of-scheduled-jobs-selection-provider.service"; +import { LocaleListSelectionProvider } from "./services/selection-providers/locale-list-selection-provider.service"; +import { SettingService } from "./services/setting.service"; +import { TwilioSMSService } from "./services/sms/TwilioSMSService"; +import { SolidTsMorphService } from "./services/solid-ts-morph.service"; +import { SqlExpressionResolverService } from "./services/sql-expression-resolver.service"; +import { TextractService } from "./services/textract.service"; +import { UserActivityHistoryService } from "./services/user-activity-history.service"; +import { UserViewMetadataService } from "./services/user-view-metadata.service"; +import { UserService } from "./services/user.service"; +import { Three60WhatsappService } from "./services/whatsapp/Three60WhatsappService"; +import { AuditSubscriber } from "./subscribers/audit.subscriber"; +import { ComputedEntityFieldSubscriber } from "./subscribers/computed-entity-field.subscriber"; +import { CreatedByUpdatedBySubscriber } from "./subscribers/created-by-updated-by.subscriber"; +import { DashboardQuestionSqlDatasetConfigSubscriber } from "./subscribers/dashboard-question-sql-dataset-config.subscriber"; +import { DashboardQuestionSubscriber } from "./subscribers/dashboard-question.subscriber"; +import { DashboardVariableSubscriber } from "./subscribers/dashboard-variable.subscriber"; +import { DashboardSubscriber } from "./subscribers/dashboard.subscriber"; +import { ListOfValuesSubscriber } from "./subscribers/list-of-values.subscriber"; +import { ScheduledJobSubscriber } from "./subscribers/scheduled-job.subscriber"; +import { SecurityRuleSubscriber } from "./subscribers/security-rule.subscriber"; +import { ViewMetadataSubsciber } from "./subscribers/view-metadata.subscriber"; +import { MediaStorageProviderMetadataRepository } from "./repository/media-storage-provider-metadata.repository"; +import { McpCommand } from "./commands/mcp.command"; +import { FixturesService } from "./services/fixtures.service"; +import { FixturesSetupCommand } from "./commands/fixtures/fixtures-setup.command"; +import { FixturesTearDownCommand } from "./commands/fixtures/fixtures-tear-down.command"; +import { DatabaseBootstrapService } from "./services/database/database-bootstrap.service"; +import { SequenceNumComputedFieldProvider } from "./services/computed-fields/entity/sequence-num-computed-field-provider"; +import { ModelSequence } from "./entities/model-sequence.entity"; +import { ModelSequenceService } from "./services/model-sequence.service"; +import { ModelSequenceController } from "./controllers/model-sequence.controller"; +import { ModelSequenceRepository } from "./repository/model-sequence.repository"; +import { CacheModule } from "@nestjs/cache-manager"; +import { CacheManagerOptions } from "./config/cache.options"; +import { SolidCoreDefaultSettingsProvider } from "./services/settings/default-settings-provider.service"; +import { SmsFactory } from "./factories/sms.factory"; +import { WhatsAppFactory } from "./factories/whatsapp.factory"; +import { ImageEncodingService } from "./helpers/image-encoding.helper"; +import { SolidMicroserviceAdapter } from "./helpers/solid-microservice-adapter.service"; +import { InfoCommand } from "./commands/info.command"; +import { ListOfRolesSelectionProvider } from "./services/selection-providers/list-of-roles-selectionproviders.service"; +import { Entity } from "typeorm"; @Global() @Module({ @@ -441,13 +446,13 @@ import { Entity } from 'typeorm'; CacheModule.registerAsync(CacheManagerOptions), ScheduleModule.forRoot(), ServeStaticModule.forRoot({ - rootPath: join(process.cwd(), 'media-files-storage'), - serveRoot: '/media-files-storage', + rootPath: join(process.cwd(), "media-files-storage"), + serveRoot: "/media-files-storage", serveStaticOptions: { setHeaders: (res /*, path, stat*/) => { // Allow use of these files from a different origin (e.g., :3000 UI) // Use 'same-site' if both origins are on the same site (localhost:* counts as same-site) - res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin'); // or 'same-site' + res.setHeader("Cross-Origin-Resource-Policy", "cross-origin"); // or 'same-site' // If you need to load into without tainting or fetch images via XHR, // you can also expose CORS here (not needed for simple ): @@ -458,7 +463,7 @@ import { Entity } from 'typeorm'; MulterModule.registerAsync({ imports: [ConfigModule], useFactory: async (configService: ConfigService) => ({ - dest: process.env.AB_MEDIA_UPLOAD_DIR ?? 'media-uploads', + dest: process.env.AB_MEDIA_UPLOAD_DIR ?? "media-uploads", }), inject: [ConfigService], }), @@ -498,6 +503,8 @@ import { Entity } from 'typeorm'; ModuleMetadataController, MqMessageController, MqMessageQueueController, + GupshupWebhookController, + MetaCloudWhatsappWebhookController, OTPAuthenticationController, PermissionMetadataController, RoleMetadataController, @@ -593,6 +600,8 @@ import { Entity } from 'typeorm'; Msg91SMSService, Msg91OTPService, Msg91WhatsappService, + MetaCloudWhatsappService, + GupshupOtpWhatsappService, TwilioSMSService, SmsTemplateService, EmailTemplateService, @@ -889,9 +898,9 @@ export class SolidCoreModule implements NestModule { configure(consumer: MiddlewareConsumer) { consumer .apply( - express.json({ limit: '10mb' }), - express.urlencoded({ limit: '10mb', extended: true }), + express.json({ limit: "10mb" }), + express.urlencoded({ limit: "10mb", extended: true }), ) - .forRoutes('*'); + .forRoutes("*"); } } From ab36db3ef4a876337dadc8a7e562e069ae581487 Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Thu, 28 May 2026 12:35:00 +0100 Subject: [PATCH 060/136] provision made to unlink data rather than having to teardown everything --- src/commands/test-data.command.ts | 35 +++++++++-- src/seeders/module-test-data.service.ts | 81 ++++++++++++++++++++++++- 2 files changed, 108 insertions(+), 8 deletions(-) diff --git a/src/commands/test-data.command.ts b/src/commands/test-data.command.ts index 1b3e0364..93684bfc 100644 --- a/src/commands/test-data.command.ts +++ b/src/commands/test-data.command.ts @@ -7,6 +7,7 @@ interface TestDataCommandOptions { modulesToTest?: string; setup?: boolean; teardown?: boolean; + unlink?: boolean; } @SubCommand({ @@ -25,17 +26,18 @@ export class TestDataCommand extends CommandRunner { const load = Boolean(options?.load); const setup = Boolean(options?.setup); const teardown = Boolean(options?.teardown); + const unlink = Boolean(options?.unlink); - const selectedModes = [load, setup, teardown].filter(Boolean).length; + const selectedModes = [load, setup, teardown, unlink].filter(Boolean).length; if (selectedModes > 1) { - this.logger.error('Please specify only one of --load, --setup, or --teardown.'); - console.log('Please specify only one of --load, --setup, or --teardown.'); + this.logger.error('Please specify only one of --load, --setup, --teardown, or --unlink.'); + console.log('Please specify only one of --load, --setup, --teardown, or --unlink.'); return; } - if (!load && !setup && !teardown) { - this.logger.error('Please specify one of --load, --setup, or --teardown.'); - console.log('Please specify one of --load, --setup, or --teardown.'); + if (!load && !setup && !teardown && !unlink) { + this.logger.error('Please specify one of --load, --setup, --teardown, or --unlink.'); + console.log('Please specify one of --load, --setup, --teardown, or --unlink.'); return; } @@ -65,6 +67,19 @@ export class TestDataCommand extends CommandRunner { await this.testDataService.setupTestData(modulesToTest ?? undefined); return; } + + if (unlink) { + const modulesToTest = options?.modulesToTest ? options.modulesToTest.split(',').map((m) => m.trim()).filter(Boolean) : null; + if (modulesToTest?.length) { + this.logger.log(`Test data unlink for modules: ${modulesToTest.join(', ')}`); + console.log(`Test data unlink for modules: ${modulesToTest.join(', ')}`); + } else { + this.logger.log('Test data unlink for all modules.'); + console.log('Test data unlink for all modules.'); + } + await this.testDataService.removeTestData(modulesToTest ?? undefined); + return; + } } catch (err: any) { const message = err?.message ?? String(err); this.logger.error(message); @@ -109,4 +124,12 @@ export class TestDataCommand extends CommandRunner { return val; } + @Option({ + flags: '--unlink', + description: 'Delete test data from testing.data sections in reverse order', + }) + parseUnlink(): boolean { + return true; + } + } diff --git a/src/seeders/module-test-data.service.ts b/src/seeders/module-test-data.service.ts index 5e3f1571..14e2ff00 100644 --- a/src/seeders/module-test-data.service.ts +++ b/src/seeders/module-test-data.service.ts @@ -19,7 +19,7 @@ import { ModelMetadataService } from 'src/services/model-metadata.service'; import { RoleMetadataService } from 'src/services/role-metadata.service'; import { UserService } from 'src/services/user.service'; import { getMediaStorageProvider } from 'src/services/mediaStorageProviders'; -import { TestingRoleSpec, TestingUserSpec } from 'src/testing/contracts/testing-metadata.types'; +import { TestingDataRecord, TestingRoleSpec, TestingUserSpec } from 'src/testing/contracts/testing-metadata.types'; @Injectable() export class ModuleTestDataService { @@ -51,6 +51,25 @@ export class ModuleTestDataService { } } + async removeTestData(modulesToTest?: string[]): Promise { + const testDataFiles = this.testDataFiles; + const filteredFiles = modulesToTest?.length ? testDataFiles.filter((file) => modulesToTest.includes(file.moduleMetadata?.name)) : testDataFiles; + + if (filteredFiles.length === 0) { + this.logger.warn('No modules matched the provided modulesToTest list.'); + console.log('No modules matched the provided modulesToTest list.'); + return; + } + + for (const overallMetadata of filteredFiles) { + const moduleName = overallMetadata?.moduleMetadata?.name ?? 'unknown'; + this.logger.log(`Removing test data for module: ${moduleName}`); + console.log(`Removing test data for module: ${moduleName}`); + await this.unlinkTestData(overallMetadata); + console.log(`✔ Test data unlink complete for module: ${moduleName}`); + } + } + async createTestDatasources(): Promise { const manifestPath = path.join(process.cwd(), '.solidx-test-manifest'); if (fs.existsSync(manifestPath)) { @@ -199,7 +218,7 @@ export class ModuleTestDataService { const testingRoles: TestingRoleSpec[] = overallMetadata?.testing?.roles ?? []; const testingUsers: TestingUserSpec[] = overallMetadata?.testing?.users ?? []; - const testingData: Array<{ modelUserKey: string; data: Record }> = overallMetadata?.testing?.data ?? []; + const testingData: TestingDataRecord[] = overallMetadata?.testing?.data ?? []; if (testingRoles.length > 0) { await this.seedTestRoles(testingRoles); @@ -290,16 +309,21 @@ export class ModuleTestDataService { // Upsert entity, capturing the saved result for post-save steps let savedEntity: any; const userKeyField = modelDef.userKeyFieldUserKey; + const recordUserKeyValue = entry.recUserKeyValue ?? payload[userKeyField]; + const recordLabel = `${modelUserKey}.${userKeyField}=${recordUserKeyValue}`; if (userKeyField && payload[userKeyField] !== undefined) { const existing = await entityRepo.findOne({ where: { [userKeyField]: payload[userKeyField] }, }); if (existing) { + console.log(`Updating test data record: ${recordLabel}`); savedEntity = await entityRepo.save(entityRepo.merge(existing, payload)); } else { + console.log(`Creating test data record: ${recordLabel}`); savedEntity = await entityRepo.save(entityRepo.create(payload)); } } else { + console.log(`Creating test data record without user key lookup: ${modelUserKey}`); savedEntity = await entityRepo.save(entityRepo.create(payload)); } @@ -313,6 +337,59 @@ export class ModuleTestDataService { } } + private async unlinkTestData(overallMetadata: any): Promise { + const moduleMetadata: CreateModuleMetadataDto = overallMetadata.moduleMetadata; + if (!moduleMetadata) { + throw new Error('Module metadata missing from test data payload.'); + } + + const testingData: TestingDataRecord[] = overallMetadata?.testing?.data ?? []; + if (testingData.length === 0) { + this.logger.debug(`No test data found for ${moduleMetadata.name}`); + return; + } + + const modelsByName = new Map( + (moduleMetadata.models ?? []).map((m) => [m.singularName, m]), + ); + + for (const entry of [...testingData].reverse()) { + const modelUserKey = entry.modelUserKey; + const modelDef = modelsByName.get(modelUserKey); + if (!modelDef) { + throw new Error(`Test data modelUserKey not found in metadata: ${modelUserKey}`); + } + + const userKeyField = modelDef.userKeyFieldUserKey; + if (!userKeyField) { + throw new Error(`Cannot unlink test data for model ${modelUserKey} because userKeyFieldUserKey is not configured.`); + } + + const userKeyValue = entry.data?.[userKeyField]; + if (userKeyValue === undefined || userKeyValue === null || userKeyValue === '') { + throw new Error( + `Cannot unlink test data for model ${modelUserKey} because testing.data entry is missing the actual user key field "${userKeyField}" in data.`, + ); + } + const recordLabel = `${modelUserKey}.${userKeyField}=${userKeyValue}`; + + const entityRepo = this.resolveRepository(modelUserKey); + const existing = typeof entityRepo.findOneByUserKey === 'function' + ? await entityRepo.findOneByUserKey(userKeyValue) + : await entityRepo.findOne({ where: { [userKeyField]: userKeyValue } }); + + if (!existing) { + console.log(`Skipping unlink; test data record not found: ${recordLabel}`); + this.logger.debug(`Test data record not found for unlink: ${modelUserKey}.${userKeyField}=${userKeyValue}`); + continue; + } + + console.log(`Deleting test data record: ${recordLabel}`); + await entityRepo.remove(existing); + this.logger.debug(`Removed test data record: ${modelUserKey}.${userKeyField}=${userKeyValue}`); + } + } + private async seedTestRoles(roles: TestingRoleSpec[]): Promise { const roleService = this.moduleRef.get(RoleMetadataService, { strict: false }); if (!roleService) { From 4c5140eaec74b7a03d8cb940d6ec08ce22d28305 Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Thu, 28 May 2026 12:35:21 +0100 Subject: [PATCH 061/136] provision made to unlink data rather than having to teardown everything --- src/testing/README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/testing/README.md b/src/testing/README.md index 2b068017..a41cfa5a 100644 --- a/src/testing/README.md +++ b/src/testing/README.md @@ -356,6 +356,21 @@ await runFromMetadata({ }); ``` +## CLI Workflows +Full isolated-database workflow: +1. `npx @solidxai/solidctl@latest test data --setup` +2. `npx @solidxai/solidctl@latest seed` +3. `npx @solidxai/solidctl@latest test data --load` +4. `npx @solidxai/solidctl@latest test run --module ` +5. `npx @solidxai/solidctl@latest test data --teardown` + +Lightweight existing-database workflow: +1. `npx @solidxai/solidctl@latest test data --load` +2. `npx @solidxai/solidctl@latest test run --module ` +3. `npx @solidxai/solidctl@latest test data --unlink` + +`test data --unlink` deletes records declared in `testing.data` in reverse order using each model's `userKeyFieldUserKey` and the entry's `recUserKeyValue`. This assumes `testing.data` is authored in dependency order, with parent records appearing before dependent records. + ## Add A New Step (SOP) 1. Create a new `*.step.ts` in the right domain folder. 2. Implement a `registerXSteps(registry)` function and `registry.register("op.name", handler)`. From b68893093bc59de8049d52be411ae2ace71392e1 Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Thu, 28 May 2026 12:37:58 +0100 Subject: [PATCH 062/136] 0.1.10-beta.16 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 287a6438..954b7064 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.15", + "version": "0.1.10-beta.16", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@solidxai/core", - "version": "0.1.10-beta.15", + "version": "0.1.10-beta.16", "license": "BUSL-1.1", "dependencies": { "@aws-sdk/client-s3": "^3.637.0", diff --git a/package.json b/package.json index a2aaacc0..e12e6c27 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.15", + "version": "0.1.10-beta.16", "description": "This module is a NestJS module containing all the required core providers required by a Solid application", "main": "dist/index.js", "types": "dist/index.d.ts", From 8a9024605f1c78634905071a6c7a51214c0175c4 Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Thu, 28 May 2026 12:38:46 +0100 Subject: [PATCH 063/136] 0.1.10-beta.17 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 954b7064..43547a4a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.16", + "version": "0.1.10-beta.17", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@solidxai/core", - "version": "0.1.10-beta.16", + "version": "0.1.10-beta.17", "license": "BUSL-1.1", "dependencies": { "@aws-sdk/client-s3": "^3.637.0", diff --git a/package.json b/package.json index e12e6c27..8f92e258 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.16", + "version": "0.1.10-beta.17", "description": "This module is a NestJS module containing all the required core providers required by a Solid application", "main": "dist/index.js", "types": "dist/index.d.ts", From d8efdf1a73bb06905edfcd02862c33646d6351fb Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Thu, 28 May 2026 12:44:47 +0100 Subject: [PATCH 064/136] 0.1.10-beta.18 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 43547a4a..8010c62d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.17", + "version": "0.1.10-beta.18", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@solidxai/core", - "version": "0.1.10-beta.17", + "version": "0.1.10-beta.18", "license": "BUSL-1.1", "dependencies": { "@aws-sdk/client-s3": "^3.637.0", diff --git a/package.json b/package.json index 8f92e258..85a1994b 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.17", + "version": "0.1.10-beta.18", "description": "This module is a NestJS module containing all the required core providers required by a Solid application", "main": "dist/index.js", "types": "dist/index.d.ts", From db0438e970eda6698fe4826f04903a983bec6bd1 Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Thu, 28 May 2026 12:49:00 +0100 Subject: [PATCH 065/136] 0.1.10-beta.19 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8010c62d..94c6ff12 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.18", + "version": "0.1.10-beta.19", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@solidxai/core", - "version": "0.1.10-beta.18", + "version": "0.1.10-beta.19", "license": "BUSL-1.1", "dependencies": { "@aws-sdk/client-s3": "^3.637.0", diff --git a/package.json b/package.json index 85a1994b..299f3c70 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.18", + "version": "0.1.10-beta.19", "description": "This module is a NestJS module containing all the required core providers required by a Solid application", "main": "dist/index.js", "types": "dist/index.d.ts", From 278f7fba1e0bca6306bb29cfcd1b3a268d48aecd Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Thu, 28 May 2026 12:50:00 +0100 Subject: [PATCH 066/136] 0.1.10-beta.20 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 94c6ff12..9720afca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.19", + "version": "0.1.10-beta.20", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@solidxai/core", - "version": "0.1.10-beta.19", + "version": "0.1.10-beta.20", "license": "BUSL-1.1", "dependencies": { "@aws-sdk/client-s3": "^3.637.0", diff --git a/package.json b/package.json index 299f3c70..0cceedab 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.19", + "version": "0.1.10-beta.20", "description": "This module is a NestJS module containing all the required core providers required by a Solid application", "main": "dist/index.js", "types": "dist/index.d.ts", From 00da04cd3365724c11970730ba0583f816875a05 Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Thu, 28 May 2026 12:51:57 +0100 Subject: [PATCH 067/136] 0.1.10-beta.21 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9720afca..5b3eeef9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.20", + "version": "0.1.10-beta.21", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@solidxai/core", - "version": "0.1.10-beta.20", + "version": "0.1.10-beta.21", "license": "BUSL-1.1", "dependencies": { "@aws-sdk/client-s3": "^3.637.0", diff --git a/package.json b/package.json index 0cceedab..1c9ae8d5 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.20", + "version": "0.1.10-beta.21", "description": "This module is a NestJS module containing all the required core providers required by a Solid application", "main": "dist/index.js", "types": "dist/index.d.ts", From 4036bc51577e43dced3f58d70410a99e693055cc Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Fri, 29 May 2026 10:18:48 +0100 Subject: [PATCH 068/136] Add manual interaction step for Playwright and update README - Introduced `ui.waitForManual` operation to pause execution for human input during Playwright tests. - Updated README with detailed usage instructions for `ui.waitForManual`. - Registered manual steps in the UI step registry. - Enhanced PlaywrightAdapter with a method to check headless mode. --- src/testing/README.md | 37 ++++++++- src/testing/adapters/ui/playwright-adapter.ts | 4 + src/testing/steps/ui/index.ts | 2 + src/testing/steps/ui/manual.step.ts | 80 +++++++++++++++++++ 4 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 src/testing/steps/ui/manual.step.ts diff --git a/src/testing/README.md b/src/testing/README.md index a41cfa5a..d112b23d 100644 --- a/src/testing/README.md +++ b/src/testing/README.md @@ -227,6 +227,37 @@ Options in `with`: - `selector` (required) - `key` (required) +### **Op: `ui.waitForManual`** +Description: Pauses a headed Playwright run so a human can interact with the live browser, then resumes when Enter is pressed in the terminal. + +Options in `with`: +- `message` (optional, defaults to a generic instruction) +- `prompt` (optional, defaults to `Press Enter to continue...`) +- `waitForSelector` (optional, waits for a visible selector after resume) +- `waitForUrlEquals` (optional, waits until the current URL exactly matches) +- `waitForUrlContains` (optional, waits until the current URL contains a substring) +- `timeoutMs` (optional, timeout for post-resume waits) +- `bringToFront` (optional, defaults to `true`) + +Notes: +- Requires `--headless false`. +- Requires an interactive terminal (TTY). +- Useful for OTP screens or third-party verification challenges that must be completed by a human. + +Example: +```json +{ + "then": { + "op": "ui.waitForManual", + "with": { + "message": "Enter the OTP in the browser, then return here to continue.", + "waitForSelector": "text=Dashboard", + "timeoutMs": 30000 + } + } +} +``` + ### **Op: `ui.expectVisible`** Description: Waits for an element to be visible. @@ -366,10 +397,12 @@ Full isolated-database workflow: Lightweight existing-database workflow: 1. `npx @solidxai/solidctl@latest test data --load` -2. `npx @solidxai/solidctl@latest test run --module ` +2. `npx @solidxai/solidctl@latest test run --module --headless false` 3. `npx @solidxai/solidctl@latest test data --unlink` -`test data --unlink` deletes records declared in `testing.data` in reverse order using each model's `userKeyFieldUserKey` and the entry's `recUserKeyValue`. This assumes `testing.data` is authored in dependency order, with parent records appearing before dependent records. +`test data --unlink` deletes records declared in `testing.data` in reverse order using each model's actual `userKeyFieldUserKey` value from `testing.data[*].data`. This assumes `testing.data` is authored in dependency order, with parent records appearing before dependent records. + +For human-assisted OTP or third-party verification flows, use `ui.waitForManual` in a headed run so the browser remains interactive while the scenario is paused. ## Add A New Step (SOP) 1. Create a new `*.step.ts` in the right domain folder. diff --git a/src/testing/adapters/ui/playwright-adapter.ts b/src/testing/adapters/ui/playwright-adapter.ts index da104661..1f44d3c1 100644 --- a/src/testing/adapters/ui/playwright-adapter.ts +++ b/src/testing/adapters/ui/playwright-adapter.ts @@ -18,6 +18,10 @@ export class PlaywrightAdapter { this.headless = opts?.headless ?? true; } + isHeadless(): boolean { + return this.headless; + } + async start(): Promise { const { chromium } = await import('playwright'); this.browser = await chromium.launch({ headless: this.headless }); diff --git a/src/testing/steps/ui/index.ts b/src/testing/steps/ui/index.ts index cd9a23d2..b4bdfe37 100644 --- a/src/testing/steps/ui/index.ts +++ b/src/testing/steps/ui/index.ts @@ -3,10 +3,12 @@ import { registerNavigationSteps } from "./navigation.step"; import { registerFormSteps } from "./form.step"; import { registerActionSteps } from "./actions.step"; import { registerAssertionSteps } from "./assertions.step"; +import { registerManualSteps } from "./manual.step"; export function registerUiSteps(registry: StepRegistry): void { registerNavigationSteps(registry); registerFormSteps(registry); registerActionSteps(registry); registerAssertionSteps(registry); + registerManualSteps(registry); } diff --git a/src/testing/steps/ui/manual.step.ts b/src/testing/steps/ui/manual.step.ts new file mode 100644 index 00000000..1f363052 --- /dev/null +++ b/src/testing/steps/ui/manual.step.ts @@ -0,0 +1,80 @@ +import { createInterface } from "node:readline/promises"; +import { stdin as input, stdout as output } from "node:process"; + +import type { TestContext } from "../../contracts/runtime-context.types"; +import type { OpStep } from "../../contracts/testing-metadata.types"; +import { StepRegistry } from "../../core/step-registry"; + +type WaitForManualInput = { + message?: string; + prompt?: string; + waitForSelector?: string; + waitForUrlContains?: string; + waitForUrlEquals?: string; + timeoutMs?: number; + bringToFront?: boolean; +}; + +function requirePage(ctx: TestContext, op: string) { + if (!ctx.ui || !ctx.ui.page) { + throw new Error(`Missing UI page on context for op "${op}"`); + } + return ctx.ui.page; +} + +export function registerManualSteps(registry: StepRegistry): void { + registry.register("ui.waitForManual", async (ctx: TestContext, step: OpStep) => { + const page = requirePage(ctx, "ui.waitForManual"); + const inputConfig = (step.with ?? {}) as WaitForManualInput; + const message = inputConfig.message?.trim() || "Manual interaction required."; + const prompt = inputConfig.prompt?.trim() || "Press Enter to continue..."; + const timeoutMs = inputConfig.timeoutMs; + const bringToFront = inputConfig.bringToFront ?? true; + + if (ctx.ui?.isHeadless()) { + throw new Error('Op "ui.waitForManual" requires headed mode. Re-run with --headless false.'); + } + if (!input.isTTY || !output.isTTY) { + throw new Error('Op "ui.waitForManual" requires an interactive terminal (TTY).'); + } + + if (bringToFront) { + await page.bringToFront(); + } + + console.log(""); + console.log("════════ Manual Interaction Required ════════"); + console.log(message); + console.log(`Scenario: ${ctx.scenarioId}`); + console.log(`Current URL: ${page.url()}`); + console.log(prompt); + + const rl = createInterface({ input, output }); + try { + await rl.question(""); + } finally { + rl.close(); + } + + if (inputConfig.waitForSelector) { + await page.waitForSelector(inputConfig.waitForSelector, { + state: "visible", + timeout: timeoutMs, + }); + } + + if (inputConfig.waitForUrlEquals) { + await page.waitForURL( + (url) => url.toString() === inputConfig.waitForUrlEquals, + { timeout: timeoutMs }, + ); + } + + if (inputConfig.waitForUrlContains) { + await page.waitForURL( + (url) => url.toString().includes(String(inputConfig.waitForUrlContains)), + { timeout: timeoutMs }, + ); + } + }); +} From 8001a12ea8273263c545daee4eef8b22d2e67f2b Mon Sep 17 00:00:00 2001 From: Rajesh Chityal Date: Mon, 25 May 2026 15:50:31 +0530 Subject: [PATCH 069/136] Agent table clean up --- .../seed-data/solid-core-metadata.json | 240 ++++++++++++------ 1 file changed, 159 insertions(+), 81 deletions(-) diff --git a/src/seeders/seed-data/solid-core-metadata.json b/src/seeders/seed-data/solid-core-metadata.json index 88bd7b00..855c9e2f 100755 --- a/src/seeders/seed-data/solid-core-metadata.json +++ b/src/seeders/seed-data/solid-core-metadata.json @@ -14000,34 +14000,16 @@ "children": [ { "type": "column", - "attrs": { "name": "col-1", "label": "", "className": "col-12 sm:col-12 md:col-6 lg:col-6" }, + "attrs": { "name": "col-1", "label": "", "className": "col-12" }, "children": [ { "type": "field", "attrs": { "name": "sessionId" } }, { "type": "field", "attrs": { "name": "modelName" } }, { "type": "field", "attrs": { "name": "status" } }, - { "type": "field", "attrs": { "name": "projectRoot" } } - ] - }, - { - "type": "column", - "attrs": { "name": "col-2", "label": "", "className": "col-12 sm:col-12 md:col-6 lg:col-6" }, - "children": [ + { "type": "field", "attrs": { "name": "projectRoot" } }, { "type": "field", "attrs": { "name": "totalSteps" } }, { "type": "field", "attrs": { "name": "totalCost" } }, { "type": "field", "attrs": { "name": "totalInputTokens" } }, - { "type": "field", "attrs": { "name": "totalOutputTokens" } } - ] - } - ] - }, - { - "type": "row", - "attrs": { "name": "row-2" }, - "children": [ - { - "type": "column", - "attrs": { "name": "col-summary", "label": "", "className": "col-12" }, - "children": [ + { "type": "field", "attrs": { "name": "totalOutputTokens" } }, { "type": "field", "attrs": { "name": "summary" } } ] } @@ -14091,46 +14073,94 @@ "attrs": { "name": "sheet-1" }, "children": [ { - "type": "row", - "attrs": { "name": "row-1" }, + "type": "notebook", + "attrs": { "name": "notebook-1" }, "children": [ { - "type": "column", - "attrs": { "name": "col-1", "label": "", "className": "col-12 sm:col-12 md:col-6 lg:col-6" }, + "type": "page", + "attrs": { "name": "page-general", "label": "General" }, "children": [ - { "type": "field", "attrs": { "name": "sessionId" } }, - { "type": "field", "attrs": { "name": "eventType" } }, - { "type": "field", "attrs": { "name": "turnNumber" } }, - { "type": "field", "attrs": { "name": "stepNumber" } }, - { "type": "field", "attrs": { "name": "toolName" } }, - { "type": "field", "attrs": { "name": "modelUsed" } } + { + "type": "row", + "attrs": { "name": "page-general-row-1" }, + "children": [ + { + "type": "column", + "attrs": { "name": "page-general-col-1", "label": "", "className": "col-12" }, + "children": [ + { "type": "field", "attrs": { "name": "sessionId" } }, + { "type": "field", "attrs": { "name": "eventType" } }, + { "type": "field", "attrs": { "name": "turnNumber" } }, + { "type": "field", "attrs": { "name": "stepNumber" } }, + { "type": "field", "attrs": { "name": "toolName" } }, + { "type": "field", "attrs": { "name": "modelUsed" } }, + { "type": "field", "attrs": { "name": "durationMs" } }, + { "type": "field", "attrs": { "name": "cost" } }, + { "type": "field", "attrs": { "name": "inputTokens" } }, + { "type": "field", "attrs": { "name": "outputTokens" } }, + { "type": "field", "attrs": { "name": "toolReturncode" } }, + { "type": "field", "attrs": { "name": "content" } } + ] + } + ] + } ] }, { - "type": "column", - "attrs": { "name": "col-2", "label": "", "className": "col-12 sm:col-12 md:col-6 lg:col-6" }, + "type": "page", + "attrs": { "name": "page-tool-arguments", "label": "Tool Arguments" }, "children": [ - { "type": "field", "attrs": { "name": "durationMs" } }, - { "type": "field", "attrs": { "name": "cost" } }, - { "type": "field", "attrs": { "name": "inputTokens" } }, - { "type": "field", "attrs": { "name": "outputTokens" } }, - { "type": "field", "attrs": { "name": "toolReturncode" } } + { + "type": "row", + "attrs": { "name": "page-tool-arguments-row-1" }, + "children": [ + { + "type": "column", + "attrs": { "name": "page-tool-arguments-col-1", "label": "", "className": "col-12" }, + "children": [ + { "type": "field", "attrs": { "name": "toolArguments", "viewWidget": "SolidJsonFormViewWidget" } } + ] + } + ] + } ] - } - ] - }, - { - "type": "row", - "attrs": { "name": "row-2" }, - "children": [ + }, { - "type": "column", - "attrs": { "name": "col-content", "label": "", "className": "col-12" }, + "type": "page", + "attrs": { "name": "page-tool-output", "label": "Tool Output" }, "children": [ - { "type": "field", "attrs": { "name": "content" } }, - { "type": "field", "attrs": { "name": "toolArguments", "viewWidget": "SolidJsonFormViewWidget" } }, - { "type": "field", "attrs": { "name": "toolOutput", "viewWidget": "SolidJsonFormViewWidget" } }, - { "type": "field", "attrs": { "name": "eventData", "viewWidget": "SolidJsonFormViewWidget" } } + { + "type": "row", + "attrs": { "name": "page-tool-output-row-1" }, + "children": [ + { + "type": "column", + "attrs": { "name": "page-tool-output-col-1", "label": "", "className": "col-12" }, + "children": [ + { "type": "field", "attrs": { "name": "toolOutput", "viewWidget": "SolidJsonFormViewWidget" } } + ] + } + ] + } + ] + }, + { + "type": "page", + "attrs": { "name": "page-event-data", "label": "Event Data" }, + "children": [ + { + "type": "row", + "attrs": { "name": "page-event-data-row-1" }, + "children": [ + { + "type": "column", + "attrs": { "name": "page-event-data-col-1", "label": "", "className": "col-12" }, + "children": [ + { "type": "field", "attrs": { "name": "eventData", "viewWidget": "SolidJsonFormViewWidget" } } + ] + } + ] + } ] } ] @@ -14193,46 +14223,94 @@ "attrs": { "name": "sheet-1" }, "children": [ { - "type": "row", - "attrs": { "name": "row-1" }, + "type": "notebook", + "attrs": { "name": "notebook-1" }, "children": [ { - "type": "column", - "attrs": { "name": "col-1", "label": "", "className": "col-12 sm:col-12 md:col-6 lg:col-6" }, + "type": "page", + "attrs": { "name": "page-general", "label": "General" }, "children": [ - { "type": "field", "attrs": { "name": "method" } }, - { "type": "field", "attrs": { "name": "toolName" } }, - { "type": "field", "attrs": { "name": "status" } }, - { "type": "field", "attrs": { "name": "transport" } }, - { "type": "field", "attrs": { "name": "mcpSessionId" } }, - { "type": "field", "attrs": { "name": "requestId" } } + { + "type": "row", + "attrs": { "name": "page-general-row-1" }, + "children": [ + { + "type": "column", + "attrs": { "name": "page-general-col-1", "label": "", "className": "col-12" }, + "children": [ + { "type": "field", "attrs": { "name": "method" } }, + { "type": "field", "attrs": { "name": "toolName" } }, + { "type": "field", "attrs": { "name": "status" } }, + { "type": "field", "attrs": { "name": "transport" } }, + { "type": "field", "attrs": { "name": "mcpSessionId" } }, + { "type": "field", "attrs": { "name": "requestId" } }, + { "type": "field", "attrs": { "name": "userId" } }, + { "type": "field", "attrs": { "name": "apiKeyId" } }, + { "type": "field", "attrs": { "name": "username" } }, + { "type": "field", "attrs": { "name": "clientAddr" } }, + { "type": "field", "attrs": { "name": "durationMs" } }, + { "type": "field", "attrs": { "name": "errorCode" } } + ] + } + ] + } ] }, { - "type": "column", - "attrs": { "name": "col-2", "label": "", "className": "col-12 sm:col-12 md:col-6 lg:col-6" }, + "type": "page", + "attrs": { "name": "page-request-params", "label": "Request Params" }, "children": [ - { "type": "field", "attrs": { "name": "userId" } }, - { "type": "field", "attrs": { "name": "apiKeyId" } }, - { "type": "field", "attrs": { "name": "username" } }, - { "type": "field", "attrs": { "name": "clientAddr" } }, - { "type": "field", "attrs": { "name": "durationMs" } }, - { "type": "field", "attrs": { "name": "errorCode" } } + { + "type": "row", + "attrs": { "name": "page-request-params-row-1" }, + "children": [ + { + "type": "column", + "attrs": { "name": "page-request-params-col-1", "label": "", "className": "col-12" }, + "children": [ + { "type": "field", "attrs": { "name": "requestParams", "viewWidget": "SolidJsonFormViewWidget" } } + ] + } + ] + } ] - } - ] - }, - { - "type": "row", - "attrs": { "name": "row-2" }, - "children": [ + }, { - "type": "column", - "attrs": { "name": "col-payload", "label": "", "className": "col-12" }, + "type": "page", + "attrs": { "name": "page-response-result", "label": "Response Result" }, + "children": [ + { + "type": "row", + "attrs": { "name": "page-response-result-row-1" }, + "children": [ + { + "type": "column", + "attrs": { "name": "page-response-result-col-1", "label": "", "className": "col-12" }, + "children": [ + { "type": "field", "attrs": { "name": "responseResult", "viewWidget": "SolidJsonFormViewWidget" } } + ] + } + ] + } + ] + }, + { + "type": "page", + "attrs": { "name": "page-error-message", "label": "Error Message" }, "children": [ - { "type": "field", "attrs": { "name": "requestParams", "viewWidget": "SolidJsonFormViewWidget" } }, - { "type": "field", "attrs": { "name": "responseResult", "viewWidget": "SolidJsonFormViewWidget" } }, - { "type": "field", "attrs": { "name": "errorMessage" } } + { + "type": "row", + "attrs": { "name": "page-error-message-row-1" }, + "children": [ + { + "type": "column", + "attrs": { "name": "page-error-message-col-1", "label": "", "className": "col-12" }, + "children": [ + { "type": "field", "attrs": { "name": "errorMessage" } } + ] + } + ] + } ] } ] From 6b338db67d1bd23a98d0bd0cd80a639ec4525962 Mon Sep 17 00:00:00 2001 From: Rajesh Chityal Date: Fri, 29 May 2026 15:23:42 +0530 Subject: [PATCH 070/136] typeo issue --- src/services/module-metadata.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/module-metadata.service.ts b/src/services/module-metadata.service.ts index 2884afb9..ad8c75ae 100755 --- a/src/services/module-metadata.service.ts +++ b/src/services/module-metadata.service.ts @@ -410,7 +410,7 @@ export class ModuleMetadataService { const module = await this.findOne(moduleId); return this.commandService.executeCommandWithArgs({ command: 'npx', - args: ['@solixai/solidctl@latest', 'generate', 'module', `--name=${module.name}`], + args: ['@solidxai/solidctl@latest', 'generate', 'module', `--name=${module.name}`], cwd: path.join(process.cwd(), '..'), }); } From a8fd97792c70771b86c1b18879b6ddb7e5e26ec7 Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Sat, 30 May 2026 07:58:38 +0100 Subject: [PATCH 071/136] Add field quality checks and fixes checklist for backend validation --- .../field-quality-check-fixes.md | 208 ++++++++++++++++++ 1 file changed, 208 insertions(+) create mode 100644 src/helpers/field-crud-managers/field-quality-check-fixes.md diff --git a/src/helpers/field-crud-managers/field-quality-check-fixes.md b/src/helpers/field-crud-managers/field-quality-check-fixes.md new file mode 100644 index 00000000..c2e51f6b --- /dev/null +++ b/src/helpers/field-crud-managers/field-quality-check-fixes.md @@ -0,0 +1,208 @@ +# Field Quality Checks And Fixes + +This checklist tracks backend issues and logical enhancements for each field type in `solid-core-module`. + +Use it for backend concerns only: + +- CRUD validation +- transformation and normalization +- persistence behavior +- relation semantics +- field-level correctness and consistency + +Frontend widget and rendering concerns belong in `solid-core-ui`. + +## `shortText` + +- [ ] Resolve the `length` versus `max` contract for `shortText` and document one clear meaning for each attribute. +- [ ] Decide whether backend validation should also enforce `min`, so `shortText` constraints remain consistent even outside generated form flows. +- [ ] Return the configured regex mismatch message when `regexPatternNotMatchingErrorMsg` is present instead of always using a generic regex error. +- [ ] Clarify and implement the expected behavior for `defaultValue` when a create payload omits a `shortText` field. +- [ ] Decide whether `shortText` should support optional normalization such as trimming or whitespace collapsing, and keep that decision consistent across comparable text field types. +- [ ] Review whether uniqueness should receive any pre-save validation support at the CRUD layer or continue to rely entirely on persistence-layer constraints. +- [ ] Add targeted coverage for partial update behavior, empty string versus null handling, regex validation, and max-length enforcement. +- [ ] Review whether the text-oriented managers share enough behavior to justify a common validation utility or base manager without weakening field-specific semantics. + +## `longText` + +- [ ] Decide whether backend validation should also enforce `min` and `max`, so `longText` constraints remain consistent outside generated form flows. +- [ ] Return the configured regex mismatch message when `regexPatternNotMatchingErrorMsg` is present instead of always using a generic regex error. +- [ ] Clarify and implement the expected behavior for `defaultValue` when a create payload omits a `longText` field. +- [ ] Decide whether `longText` should support optional normalization such as trimming trailing whitespace while preserving intentional line breaks. +- [ ] Review whether uniqueness should receive any pre-save validation support at the CRUD layer or continue to rely entirely on persistence-layer constraints. +- [ ] Add targeted coverage for multiline content, regex validation, partial updates, and empty string versus null handling. +- [ ] Review whether the text-oriented managers share enough behavior to justify a common validation utility or base manager without weakening field-specific semantics. + +## `richText` + +- [ ] Decide whether backend validation should also enforce `min` and `max`, so `richText` constraints remain consistent outside generated form flows. +- [ ] Return the configured regex mismatch message when `regexPatternNotMatchingErrorMsg` is present instead of always using a generic regex error. +- [ ] Clarify and implement the expected behavior for `defaultValue` when a create payload omits a `richText` field. +- [ ] Decide whether `richText` should support optional normalization or sanitization at save time, and define that behavior explicitly. +- [ ] Review whether uniqueness should receive any pre-save validation support at the CRUD layer or continue to rely entirely on persistence-layer constraints. +- [ ] Add targeted coverage for rich HTML-like content, regex validation, partial updates, and empty string versus null handling. +- [ ] Review whether the text-oriented managers share enough behavior to justify a common validation utility or base manager without weakening field-specific semantics. + +## `json` + +- [ ] Clarify and implement the expected behavior for `defaultValue` when a create payload omits a `json` field. +- [ ] Decide whether backend validation should accept only stringified JSON or also handle already-parsed object and array payloads more explicitly. +- [ ] Review whether the current JSON validation error should distinguish between invalid JSON syntax and unsupported runtime value shapes. +- [ ] Decide whether normalization or canonical stringification should happen before persistence for comparable JSON values. +- [ ] Review whether uniqueness should receive any pre-save validation support at the CRUD layer or continue to rely entirely on persistence-layer constraints. +- [ ] Add targeted coverage for object payloads, array payloads, stringified JSON payloads, invalid JSON, partial updates, and empty string versus null handling. + +## `int` + +- [ ] Fix the current min/max activation rule, because backend validation only applies numeric bounds when the configured value is greater than `0`, which skips valid `0` and negative thresholds. +- [ ] Clarify and implement the expected behavior for `defaultValue` when a create payload omits an `int` field. +- [ ] Decide whether the CRUD layer should normalize numeric strings into integers more explicitly before validation and persistence. +- [ ] Review whether integer fields should reject decimal-shaped inputs more clearly when values arrive from metadata-driven clients as strings. +- [ ] Review whether uniqueness should receive any pre-save validation support at the CRUD layer or continue to rely entirely on persistence-layer constraints. +- [ ] Add targeted coverage for `0`, negative values, numeric strings, invalid decimal-like input, min/max bounds, and partial update behavior. + + +## `bigint` + +- [ ] Clarify the authored support surface for `bigint`, because the CRUD manager contains numeric bound logic while the authored field surface does not currently present `min` and `max` in the same way as `int` and `decimal`. +- [ ] Fix or clarify the current numeric-bound activation rule if bigint bounds are intended to be supported, since the logic only applies bounds when the configured value is greater than `0`. +- [ ] Clarify and implement the expected behavior for `defaultValue` when a create payload omits a `bigint` field. +- [ ] Decide what the canonical runtime contract should be for bigint inputs: native `bigint`, numeric strings, or finite JavaScript numbers. +- [ ] Review whether accepting finite JavaScript numbers for bigint validation is sufficient when exact large-integer fidelity matters. +- [ ] Review whether uniqueness should receive any pre-save validation support at the CRUD layer or continue to rely entirely on persistence-layer constraints. +- [ ] Add targeted coverage for numeric strings, very large values, invalid numeric input, bound handling, null handling, and partial update behavior. + + +## `decimal` + +- [ ] Fix the current min/max activation rule, because backend validation only applies numeric bounds when the configured value is greater than `0`, which skips valid `0` and negative thresholds. +- [ ] Clarify and implement the expected behavior for `defaultValue` when a create payload omits a `decimal` field. +- [ ] Decide whether the CRUD layer should normalize numeric strings into decimal numbers more explicitly before validation and persistence. +- [ ] Review whether decimal fields need an explicit precision-and-scale contract instead of relying entirely on the persistence-layer `ormType`. +- [ ] Review whether uniqueness should receive any pre-save validation support at the CRUD layer or continue to rely entirely on persistence-layer constraints. +- [ ] Add targeted coverage for `0`, negative values, fractional values, numeric strings, invalid numeric input, min/max bounds, and partial update behavior. + + +## `boolean` + +- [ ] Clarify and implement the expected behavior for `defaultValue` when a create payload omits a `boolean` field. +- [ ] Decide whether boolean CRUD validation should accept only runtime booleans or also normalize `"true"` and `"false"` string payloads when they arrive from metadata-driven clients. +- [ ] Review whether the required validation path should distinguish more clearly between an omitted value and an explicit `false` value, especially for partial updates. +- [ ] Review whether uniqueness should receive any pre-save validation support at the CRUD layer or continue to rely entirely on persistence-layer constraints. +- [ ] Add targeted coverage for `true`, `false`, null, empty string, string booleans, and partial update behavior. + +## `date` + +- [ ] Clarify and implement the expected behavior for `defaultValue` when a create payload omits a `date` field. +- [ ] Review whether the CRUD layer should accept only runtime `Date` objects or also normalize common serialized date inputs more explicitly before validation. +- [ ] Decide whether `date` and `datetime` should continue to share the same backend validation path or receive more distinct normalization and validation behavior. +- [ ] Review whether uniqueness should receive any pre-save validation support at the CRUD layer or continue to rely entirely on persistence-layer constraints. +- [ ] Add targeted coverage for `Date` objects, serialized date strings, null handling, invalid date inputs, and partial update behavior. + +## `datetime` + +- [ ] Clarify and implement the expected behavior for `defaultValue` when a create payload omits a `datetime` field. +- [ ] Review whether the CRUD layer should accept only runtime `Date` objects or also normalize common serialized datetime inputs more explicitly before validation. +- [ ] Decide whether `date` and `datetime` should continue to share the same backend validation path or receive more distinct normalization and validation behavior. +- [ ] Review whether timezone normalization rules should be documented and enforced more explicitly at save time. +- [ ] Review whether uniqueness should receive any pre-save validation support at the CRUD layer or continue to rely entirely on persistence-layer constraints. +- [ ] Add targeted coverage for `Date` objects, serialized datetime strings, null handling, timezone-sensitive values, invalid datetime inputs, and partial update behavior. + +## `time` + +- [ ] Add an explicit CRUD-manager path for `SolidFieldType.time`, since the field currently has core UI support but is not routed through a dedicated backend field manager. +- [ ] Clarify and implement the expected behavior for `defaultValue` when a create payload omits a `time` field. +- [ ] Decide what canonical persisted shape `time` should use: pure time string, database-native time value, or a normalized timestamp-derived representation. +- [ ] Review whether the CRUD layer should normalize common time inputs such as `HH:mm:ss`, ISO timestamps, and UI-submitted values more explicitly before validation and persistence. +- [ ] Review whether uniqueness should receive any pre-save validation support at the CRUD layer or continue to rely entirely on persistence-layer constraints. +- [ ] Add targeted coverage for `HH:mm:ss` inputs, ISO-style inputs, invalid time values, null handling, and partial update behavior. + +## `email` + +- [ ] Decide whether backend validation should also enforce `min`, so email constraints remain consistent outside generated form flows. +- [ ] Return the configured regex mismatch message when `regexPatternNotMatchingErrorMsg` is present instead of always using a generic regex error. +- [ ] Clarify and implement the expected behavior for `defaultValue` when a create payload omits an `email` field. +- [ ] Decide whether email values should support optional normalization such as trimming and lowercasing before persistence. +- [ ] Review whether uniqueness should receive any pre-save validation support at the CRUD layer or continue to rely entirely on persistence-layer constraints. +- [ ] Add targeted coverage for max-length enforcement, regex overrides, invalid email formats, null handling, and partial update behavior. + +## `password` + +- [ ] Return the configured regex mismatch message when `regexPatternNotMatchingErrorMsg` is present instead of always using a generic password-regex error. +- [ ] Review whether password validation should produce clearer error messages for min, max, regex, and confirm-password mismatch failures. +- [ ] Clarify and implement the expected behavior for `defaultValue` on password fields, including whether it should be ignored entirely for secure create flows. +- [ ] Review whether password hashing and password-update flows should share a more explicit single contract across create and update operations. +- [ ] Decide whether password normalization should include trimming or whether exact raw user input should always be preserved before hashing. +- [ ] Add targeted coverage for create-required behavior, update-optional behavior, confirm-password mismatch, hashing behavior, regex fallback behavior, and partial update behavior. + +## `selectionStatic` + +- [ ] Clarify and implement the expected behavior for `defaultValue` when a create payload omits a `selectionStatic` field, especially for multi-select fields. +- [ ] Decide whether the CRUD layer should normalize single-select and multi-select payloads into one consistent stored shape before persistence. +- [ ] Review whether the current `selectionStaticValues` parsing should validate malformed `value:label` entries more explicitly instead of assuming a valid authored format. +- [ ] Add clearer error messaging when the submitted value has the wrong type versus when it is simply not part of the authored option set. +- [ ] Review whether numeric selection values need more explicit normalization, especially when values can arrive as strings from metadata-driven clients. +- [ ] Add targeted coverage for single-select string values, single-select numeric values, JSON-stringified multi-select arrays, invalid option tokens, malformed arrays, and partial update behavior. + +## `selectionDynamic` + +- [ ] Clarify and implement the expected behavior for `defaultValue` when a create payload omits a `selectionDynamic` field, especially for multi-select fields. +- [ ] Review whether the CRUD layer should normalize single-select and multi-select payloads into one consistent stored shape before persistence. +- [ ] Add clearer error messaging when the submitted value has the wrong type, when the provider rejects the value, and when the provider itself cannot be resolved. +- [ ] Review whether numeric selection values need more explicit normalization, especially when values can arrive as strings from metadata-driven clients. +- [ ] Clarify how `validateOnSave` in `selectionDynamicProviderCtxt` should be treated as part of the supported field contract and whether more provider-context keys should be validated. +- [ ] Add targeted coverage for provider-backed single-select values, JSON-stringified multi-select arrays, invalid provider names, `validateOnSave: false`, invalid option values, and partial update behavior. + +## `many-to-one` + +- [ ] Clarify and document the preferred request contract for single relations, because accepting both `Id` and `UserKey` is useful but easy to apply inconsistently across clients. +- [ ] Review whether the CRUD layer should normalize empty strings for `Id` and `UserKey` more explicitly before required validation runs. +- [ ] Decide whether user-key-based lookup failures should return a more specific error message than generic required or invalid relation errors. +- [ ] Review whether relation resolution should support optional fixed-filter enforcement at save time in addition to UI-level filtering. +- [ ] Consider whether uniqueness on a `many-to-one` field should receive clearer pre-save validation when the authored intent effectively makes the relation one-to-one. +- [ ] Add targeted coverage for id-based lookup, user-key-based lookup, missing required relations, invalid id shapes, invalid user keys, and partial update behavior. + +## `one-to-many` + +- [ ] Clarify and document the command contract for collection relations so `set`, `clear`, `link`, `unlink`, `create`, `update`, and `delete` are easier to use consistently from clients. +- [ ] Review whether malformed or unsupported command values should return more specific validation errors than the current generic failure path. +- [ ] Decide whether create-time and update-time collection mutation semantics should be normalized more explicitly, especially where some commands are intentionally update-only. +- [ ] Review whether `relationCoModelFieldName` should be validated more aggressively, because this field is central to child binding and inverse resolution. +- [ ] Consider whether `relationFieldFixedFilter` should be enforced during relation mutation so child records cannot be linked outside the authored collection scope. +- [ ] Add targeted coverage for `set`, `clear`, `link`, `unlink`, `create`, `update`, `delete`, invalid command payloads, and partial update behavior. + +## `many-to-many` + +- [ ] Clarify and document owner-side versus inverse-side mutation semantics so clients understand which effective field names and id lists are honored during CRUD operations. +- [ ] Review whether malformed owner-side metadata such as missing `isRelationManyToManyOwner` or mismatched inverse names should fail earlier and more clearly. +- [ ] Decide whether `relationJoinTableName` and related ownership attrs need stronger validation at metadata authoring time to avoid ambiguous join behavior later. +- [ ] Review whether `relationFieldFixedFilter` should be enforced during membership mutation so unsupported links cannot be created through direct payloads. +- [ ] Consider whether create, update, and delete operations against related entities should be more clearly separated from plain membership operations such as `set`, `link`, and `unlink`. +- [ ] Add targeted coverage for owner-side mutations, inverse-side mutations, `set`, `clear`, `link`, `unlink`, related-entity create/update/delete flows, and partial update behavior. + +## `mediaSingle` + +- [ ] Clarify and implement the expected behavior for `defaultValue` when a create payload omits a `mediaSingle` field, including whether the field should ever accept persisted references without a new upload. +- [ ] Review whether media validation should normalize authored `mediaTypes` values more explicitly so unsupported tokens fail early and predictably. +- [ ] Consider whether storage-provider resolution should fail with a clearer field-specific error when `mediaStorageProviderUserKey` is missing or invalid. +- [ ] Review whether replace and delete semantics should be more explicit in the CRUD layer when an existing asset is being updated or removed. +- [ ] Consider whether upload validation should be refactored into shared utilities so size, type, and required checks stay consistent across media field types. +- [ ] Add targeted coverage for required create-time uploads, oversized files, unsupported media types, missing providers, replacement flows, and partial update behavior. + +## `mediaMultiple` + +- [ ] Align collection-level validation with the single-media path so `mediaTypes` and `mediaMaxSizeKb` are enforced consistently for every uploaded file. +- [ ] Clarify and implement the expected behavior for `defaultValue` when a create payload omits a `mediaMultiple` field, including whether persisted media references should ever be accepted directly. +- [ ] Review whether storage-provider resolution should fail with a clearer field-specific error when `mediaStorageProviderUserKey` is missing or invalid. +- [ ] Decide what the canonical update semantics should be for attachment collections: append-only, replace-all, explicit remove, or a more command-style contract. +- [ ] Consider whether media collection validation should report per-file errors more explicitly when one file fails and others succeed. +- [ ] Add targeted coverage for required create-time collections, mixed valid and invalid uploads, oversized files, unsupported media types, missing providers, and partial update behavior. + +## `computed` + +- [ ] Clarify and document the supported provider contract more explicitly, including what a computed-field provider must return and how failures should be surfaced to callers. +- [ ] Review whether `computedFieldValueProviderCtxt` should be validated as JSON earlier and with clearer error messaging before provider execution begins. +- [ ] Decide whether inline computation should remain skipped on partial updates by default, or whether some computed fields need an opt-in recompute path during patch-style operations. +- [ ] Review whether `computedFieldTriggerConfig` should be validated more aggressively so unsupported operations or malformed trigger definitions fail earlier. +- [ ] Consider whether the CRUD layer should verify that the provider’s returned value matches `computedFieldValueType` before persistence. +- [ ] Add targeted coverage for inline provider execution, malformed provider context, missing providers, trigger-configured fields, partial updates, and mismatched computed value types. From 23750d6816e7310405b91fd3f44232b3f7dd4021 Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Sat, 30 May 2026 12:15:38 +0100 Subject: [PATCH 072/136] refactor: remove unused dashboard question and variable providers, SQL expression resolver, and related subscribers - Deleted `helpers.ts`, `prime-react-datatable-sql-data-provider.service.ts`, `prime-react-meter-group-sql-data-provider.service.ts`, `list-of-dashboard-question-providers-selection-provider.service.ts`, `list-of-dashboard-variable-providers-selection-provider.service.ts`, `sql-expression-resolver.service.ts`, and their respective subscribers. - Cleaned up `solid-introspect.service.ts` by removing registration logic for dashboard question and variable selection providers. - Updated `solid-core.module.ts` to remove imports and references to deleted services and entities. --- src/commands/info.command.ts | 2 - .../dashboard-layout.controller.ts | 106 - ...-question-sql-dataset-config.controller.ts | 93 - .../dashboard-question.controller.ts | 104 - .../dashboard-variable.controller.ts | 93 - src/controllers/dashboard.controller.ts | 99 - ...hboard-question-data-provider.decorator.ts | 7 - .../dashboard-selection-provider.decorator.ts | 7 - src/dtos/create-dashboard-layout.dto.ts | 31 - ...shboard-question-sql-dataset-config.dto.ts | 42 - src/dtos/create-dashboard-question.dto.ts | 62 - src/dtos/create-dashboard-variable.dto.ts | 60 - src/dtos/create-dashboard.dto.ts | 61 - ...rd-variable-selection-dynamic-query.dto.ts | 29 - src/dtos/update-dashboard-layout.dto.ts | 30 - ...shboard-question-sql-dataset-config.dto.ts | 49 - src/dtos/update-dashboard-question.dto.ts | 67 - src/dtos/update-dashboard-variable.dto.ts | 58 - src/dtos/update-dashboard.dto.ts | 65 - src/entities/dashboard-layout.entity.ts | 18 - ...oard-question-sql-dataset-config.entity.ts | 37 - src/entities/dashboard-question.entity.ts | 46 - src/entities/dashboard-variable.entity.ts | 44 - src/entities/dashboard.entity.ts | 36 - src/helpers/solid-registry.ts | 43 +- src/index.ts | 5 - src/interfaces.ts | 22 +- ...-mcp-client-subscriber-database.service.ts | 35 +- src/mappers/dashboard-mapper.ts | 53 - src/repository/dashboard-layout.repository.ts | 17 - ...-question-sql-dataset-config.repository.ts | 17 - .../dashboard-question.repository.ts | 17 - .../dashboard-variable.repository.ts | 17 - src/repository/dashboard.repository.ts | 101 - src/seeders/module-metadata-seeder.service.ts | 67 - .../seed-data/solid-core-metadata.json | 10202 +++++++--------- src/services/dashboard-layout.service.ts | 111 - ...ard-question-sql-dataset-config.service.ts | 25 - src/services/dashboard-question.service.ts | 150 - ...d-variable-sql-dynamic-provider.service.ts | 56 - ...-variable-test-dynamic-provider.service.ts | 37 - src/services/dashboard-variable.service.ts | 27 - src/services/dashboard.service.ts | 148 - ...estion-to-dashboard-mcp-handler.service.ts | 43 - ...riable-to-dashboard-mcp-handler.service.ts | 44 - ...id-create-dashboard-mcp-handler.service.ts | 114 - ...-dashboard-question-mcp-handler.service.ts | 38 - ...-sql-dataset-config-mcp-handler.service.ts | 40 - ...te-dashboard-widget-mcp-handler.service.ts | 39 - .../chartjs-sql-data-provider.service.ts | 121 - .../question-data-providers/helpers.ts | 30 - ...act-datatable-sql-data-provider.service.ts | 74 - ...t-meter-group-sql-data-provider.service.ts | 115 - ...on-providers-selection-provider.service.ts | 44 - ...le-providers-selection-provider.service.ts | 41 - src/services/solid-introspect.service.ts | 34 - .../sql-expression-resolver.service.ts | 140 - src/solid-core.module.ts | 72 - ...-question-sql-dataset-config.subscriber.ts | 61 - .../dashboard-question.subscriber.ts | 62 - .../dashboard-variable.subscriber.ts | 63 - src/subscribers/dashboard.subscriber.ts | 62 - 62 files changed, 4510 insertions(+), 9123 deletions(-) delete mode 100644 src/controllers/dashboard-layout.controller.ts delete mode 100644 src/controllers/dashboard-question-sql-dataset-config.controller.ts delete mode 100644 src/controllers/dashboard-question.controller.ts delete mode 100644 src/controllers/dashboard-variable.controller.ts delete mode 100644 src/controllers/dashboard.controller.ts delete mode 100644 src/decorators/dashboard-question-data-provider.decorator.ts delete mode 100644 src/decorators/dashboard-selection-provider.decorator.ts delete mode 100644 src/dtos/create-dashboard-layout.dto.ts delete mode 100644 src/dtos/create-dashboard-question-sql-dataset-config.dto.ts delete mode 100644 src/dtos/create-dashboard-question.dto.ts delete mode 100644 src/dtos/create-dashboard-variable.dto.ts delete mode 100644 src/dtos/create-dashboard.dto.ts delete mode 100644 src/dtos/dashboard-variable-selection-dynamic-query.dto.ts delete mode 100644 src/dtos/update-dashboard-layout.dto.ts delete mode 100644 src/dtos/update-dashboard-question-sql-dataset-config.dto.ts delete mode 100644 src/dtos/update-dashboard-question.dto.ts delete mode 100644 src/dtos/update-dashboard-variable.dto.ts delete mode 100644 src/dtos/update-dashboard.dto.ts delete mode 100644 src/entities/dashboard-layout.entity.ts delete mode 100644 src/entities/dashboard-question-sql-dataset-config.entity.ts delete mode 100644 src/entities/dashboard-question.entity.ts delete mode 100644 src/entities/dashboard-variable.entity.ts delete mode 100644 src/entities/dashboard.entity.ts delete mode 100644 src/mappers/dashboard-mapper.ts delete mode 100644 src/repository/dashboard-layout.repository.ts delete mode 100644 src/repository/dashboard-question-sql-dataset-config.repository.ts delete mode 100644 src/repository/dashboard-question.repository.ts delete mode 100644 src/repository/dashboard-variable.repository.ts delete mode 100644 src/repository/dashboard.repository.ts delete mode 100644 src/services/dashboard-layout.service.ts delete mode 100644 src/services/dashboard-question-sql-dataset-config.service.ts delete mode 100644 src/services/dashboard-question.service.ts delete mode 100644 src/services/dashboard-selection-providers/dashboard-variable-sql-dynamic-provider.service.ts delete mode 100644 src/services/dashboard-selection-providers/dashboard-variable-test-dynamic-provider.service.ts delete mode 100644 src/services/dashboard-variable.service.ts delete mode 100644 src/services/dashboard.service.ts delete mode 100644 src/services/genai/mcp-handlers/solid-add-question-to-dashboard-mcp-handler.service.ts delete mode 100644 src/services/genai/mcp-handlers/solid-add-variable-to-dashboard-mcp-handler.service.ts delete mode 100644 src/services/genai/mcp-handlers/solid-create-dashboard-mcp-handler.service.ts delete mode 100644 src/services/genai/mcp-handlers/solid-create-dashboard-question-mcp-handler.service.ts delete mode 100644 src/services/genai/mcp-handlers/solid-create-dashboard-question-sql-dataset-config-mcp-handler.service.ts delete mode 100644 src/services/genai/mcp-handlers/solid-create-dashboard-widget-mcp-handler.service.ts delete mode 100644 src/services/question-data-providers/chartjs-sql-data-provider.service.ts delete mode 100644 src/services/question-data-providers/helpers.ts delete mode 100644 src/services/question-data-providers/prime-react-datatable-sql-data-provider.service.ts delete mode 100644 src/services/question-data-providers/prime-react-meter-group-sql-data-provider.service.ts delete mode 100644 src/services/selection-providers/list-of-dashboard-question-providers-selection-provider.service.ts delete mode 100644 src/services/selection-providers/list-of-dashboard-variable-providers-selection-provider.service.ts delete mode 100644 src/services/sql-expression-resolver.service.ts mode change 100755 => 100644 src/solid-core.module.ts delete mode 100644 src/subscribers/dashboard-question-sql-dataset-config.subscriber.ts delete mode 100644 src/subscribers/dashboard-question.subscriber.ts delete mode 100644 src/subscribers/dashboard-variable.subscriber.ts delete mode 100644 src/subscribers/dashboard.subscriber.ts diff --git a/src/commands/info.command.ts b/src/commands/info.command.ts index 540a86d4..71ca8f9f 100644 --- a/src/commands/info.command.ts +++ b/src/commands/info.command.ts @@ -53,8 +53,6 @@ export class InfoCommand extends CommandRunner { selectionProviders: this.getWrapperNames(this.solidRegistry.getSelectionProviders()), computedFieldProviders: this.getWrapperNames(this.solidRegistry.getComputedFieldProviders()), solidDatabaseModules: this.getWrapperNames(this.solidRegistry.getSolidDatabaseModules()), - dashboardVariableSelectionProviders: this.getWrapperNames(this.solidRegistry.getDashboardVariableSelectionProviders()), - dashboardQuestionDataProviders: this.getWrapperNames(this.solidRegistry.getDashboardQuestionDataProviders()), mailProviders: this.getWrapperNames(this.solidRegistry.getMailProviders()), whatsappProviders: this.getWrapperNames(this.solidRegistry.getWhatsappProviders()), smsProviders: this.getWrapperNames(this.solidRegistry.getSmsProviders()), diff --git a/src/controllers/dashboard-layout.controller.ts b/src/controllers/dashboard-layout.controller.ts deleted file mode 100644 index ea83ef2a..00000000 --- a/src/controllers/dashboard-layout.controller.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { Controller, Post, Body, Param, UploadedFiles, UseInterceptors, Put, Get, Query, Delete, Patch } from '@nestjs/common'; -import { AnyFilesInterceptor } from "@nestjs/platform-express"; -import { ApiBearerAuth, ApiQuery, ApiTags } from '@nestjs/swagger'; -import { CreateDashboardLayoutDto } from 'src/dtos/create-dashboard-layout.dto'; -import { UpdateDashboardLayoutDto } from 'src/dtos/update-dashboard-layout.dto'; -import { DashboardLayoutService } from 'src/services/dashboard-layout.service'; - -enum ShowSoftDeleted { - INCLUSIVE = "inclusive", - EXCLUSIVE = "exclusive", -} - -@ApiTags('Solid Core') -@Controller('dashboard-layout') -export class DashboardLayoutController { - constructor(private readonly service: DashboardLayoutService) { } - - @ApiBearerAuth("jwt") - @Post() - @UseInterceptors(AnyFilesInterceptor()) - create(@Body() createDto: CreateDashboardLayoutDto, @UploadedFiles() files: Array) { - return this.service.create(createDto, files); - } - - @ApiBearerAuth("jwt") - @Post('/bulk') - @UseInterceptors(AnyFilesInterceptor()) - insertMany(@Body() createDtos: CreateDashboardLayoutDto[], @UploadedFiles() filesArray: Express.Multer.File[][] = []) { - return this.service.insertMany(createDtos, filesArray); - } - - @ApiBearerAuth("jwt") - @Post('/upsert-user-dashboard-layout') - @UseInterceptors(AnyFilesInterceptor()) - upsertUserDashboardLayout(@Body() createDtos: CreateDashboardLayoutDto) { - return this.service.upsertUserDashboardLayout(createDtos); - } - - - @ApiBearerAuth("jwt") - @Put(':id') - @UseInterceptors(AnyFilesInterceptor()) - update(@Param('id') id: number, @Body() updateDto: UpdateDashboardLayoutDto, @UploadedFiles() files: Array) { - return this.service.update(id, updateDto, files); - } - - @ApiBearerAuth("jwt") - @Patch(':id') - @UseInterceptors(AnyFilesInterceptor()) - partialUpdate(@Param('id') id: number, @Body() updateDto: UpdateDashboardLayoutDto, @UploadedFiles() files: Array) { - return this.service.update(id, updateDto, files, true); - } - - @ApiBearerAuth("jwt") - @Post('/bulk-recover') - async recoverMany(@Body() ids: number[]) { - return this.service.recoverMany(ids); - } - - @ApiBearerAuth("jwt") - @Get('/user-dashboard-layout/:dashboardId') - async getUserDashboardLayoutByDashboardId(@Param('dashboardId') dashboardId: number) { - return this.service.getUserDashboardLayoutByDashboardId(dashboardId); - } - - @ApiBearerAuth("jwt") - @Get('/recover/:id') - async recover(@Param('id') id: number) { - return this.service.recover(id); - } - - @ApiBearerAuth("jwt") - @ApiQuery({ name: 'showSoftDeleted', required: false, enum: ShowSoftDeleted }) - @ApiQuery({ name: 'limit', required: false, type: Number }) - @ApiQuery({ name: 'offset', required: false, type: Number }) - @ApiQuery({ name: 'fields', required: false, type: Array }) - @ApiQuery({ name: 'sort', required: false, type: Array }) - @ApiQuery({ name: 'groupBy', required: false, type: Array }) - @ApiQuery({ name: 'populate', required: false, type: Array }) - @ApiQuery({ name: 'populateMedia', required: false, type: Array }) - @ApiQuery({ name: 'filters', required: false, type: Array }) - @Get() - async findMany(@Query() query: any) { - return this.service.find(query); - } - - @ApiBearerAuth("jwt") - @Get(':id') - async findOne(@Param('id') id: string, @Query() query: any) { - return this.service.findOne(+id, query); - } - - @ApiBearerAuth("jwt") - @Delete('/bulk') - async deleteMany(@Body() ids: number[]) { - return this.service.deleteMany(ids); - } - - @ApiBearerAuth("jwt") - @Delete(':id') - async delete(@Param('id') id: number) { - return this.service.delete(id); - } - - -} diff --git a/src/controllers/dashboard-question-sql-dataset-config.controller.ts b/src/controllers/dashboard-question-sql-dataset-config.controller.ts deleted file mode 100644 index 6bd2db0a..00000000 --- a/src/controllers/dashboard-question-sql-dataset-config.controller.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { Controller, Post, Body, Param, UploadedFiles, UseInterceptors, Put, Get, Query, Delete, Patch } from '@nestjs/common'; -import { AnyFilesInterceptor } from "@nestjs/platform-express"; -import { ApiBearerAuth, ApiQuery, ApiTags } from '@nestjs/swagger'; -import { DashboardQuestionSqlDatasetConfigService } from '../services/dashboard-question-sql-dataset-config.service'; -import { CreateDashboardQuestionSqlDatasetConfigDto } from '../dtos/create-dashboard-question-sql-dataset-config.dto'; -import { UpdateDashboardQuestionSqlDatasetConfigDto } from '../dtos/update-dashboard-question-sql-dataset-config.dto'; - -enum ShowSoftDeleted { - INCLUSIVE = "inclusive", - EXCLUSIVE = "exclusive", -} - -@ApiTags('Solid Core') -@Controller('dashboard-question-sql-dataset-config') -export class DashboardQuestionSqlDatasetConfigController { - constructor(private readonly service: DashboardQuestionSqlDatasetConfigService) {} - - @ApiBearerAuth("jwt") - @Post() - @UseInterceptors(AnyFilesInterceptor()) - create(@Body() createDto: CreateDashboardQuestionSqlDatasetConfigDto, @UploadedFiles() files: Array) { - return this.service.create(createDto, files); - } - - @ApiBearerAuth("jwt") - @Post('/bulk') - @UseInterceptors(AnyFilesInterceptor()) - insertMany(@Body() createDtos: CreateDashboardQuestionSqlDatasetConfigDto[], @UploadedFiles() filesArray: Express.Multer.File[][] = []) { - return this.service.insertMany(createDtos, filesArray); - } - - - @ApiBearerAuth("jwt") - @Put(':id') - @UseInterceptors(AnyFilesInterceptor()) - update(@Param('id') id: number, @Body() updateDto: UpdateDashboardQuestionSqlDatasetConfigDto, @UploadedFiles() files: Array) { - return this.service.update(id, updateDto, files); - } - - @ApiBearerAuth("jwt") - @Patch(':id') - @UseInterceptors(AnyFilesInterceptor()) - partialUpdate(@Param('id') id: number, @Body() updateDto: UpdateDashboardQuestionSqlDatasetConfigDto, @UploadedFiles() files: Array) { - return this.service.update(id, updateDto, files, true); - } - - @ApiBearerAuth("jwt") - @Post('/bulk-recover') - async recoverMany(@Body() ids: number[]) { - return this.service.recoverMany(ids); - } - - @ApiBearerAuth("jwt") - @Get('/recover/:id') - async recover(@Param('id') id: number) { - return this.service.recover(id); - } - - @ApiBearerAuth("jwt") - @ApiQuery({ name: 'showSoftDeleted', required: false, enum: ShowSoftDeleted }) - @ApiQuery({ name: 'limit', required: false, type: Number }) - @ApiQuery({ name: 'offset', required: false, type: Number }) - @ApiQuery({ name: 'fields', required: false, type: Array }) - @ApiQuery({ name: 'sort', required: false, type: Array }) - @ApiQuery({ name: 'groupBy', required: false, type: Array }) - @ApiQuery({ name: 'populate', required: false, type: Array }) - @ApiQuery({ name: 'populateMedia', required: false, type: Array }) - @ApiQuery({ name: 'filters', required: false, type: Array }) - @Get() - async findMany(@Query() query: any) { - return this.service.find(query); - } - - @ApiBearerAuth("jwt") - @Get(':id') - async findOne(@Param('id') id: string, @Query() query: any) { - return this.service.findOne(+id, query); - } - - @ApiBearerAuth("jwt") - @Delete('/bulk') - async deleteMany(@Body() ids: number[]) { - return this.service.deleteMany(ids); - } - - @ApiBearerAuth("jwt") - @Delete(':id') - async delete(@Param('id') id: number) { - return this.service.delete(id); - } - - -} diff --git a/src/controllers/dashboard-question.controller.ts b/src/controllers/dashboard-question.controller.ts deleted file mode 100644 index 0bece398..00000000 --- a/src/controllers/dashboard-question.controller.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { Controller, Post, Body, Param, UploadedFiles, UseInterceptors, Put, Get, Query, Delete, Patch } from '@nestjs/common'; -import { AnyFilesInterceptor } from "@nestjs/platform-express"; -import { ApiBearerAuth, ApiQuery, ApiTags } from '@nestjs/swagger'; -import { DashboardQuestionService } from '../services/dashboard-question.service'; -import { CreateDashboardQuestionDto } from '../dtos/create-dashboard-question.dto'; -import { UpdateDashboardQuestionDto } from '../dtos/update-dashboard-question.dto'; -import { SqlExpression } from 'src/services/question-data-providers/chartjs-sql-data-provider.service'; - -enum ShowSoftDeleted { - INCLUSIVE = "inclusive", - EXCLUSIVE = "exclusive", -} - -@ApiTags('Solid Core') -@Controller('dashboard-question') -export class DashboardQuestionController { - constructor(private readonly service: DashboardQuestionService) { } - - @ApiBearerAuth("jwt") - @Post() - @UseInterceptors(AnyFilesInterceptor()) - create(@Body() createDto: CreateDashboardQuestionDto, @UploadedFiles() files: Array) { - return this.service.create(createDto, files); - } - - @ApiBearerAuth("jwt") - @Post('/bulk') - @UseInterceptors(AnyFilesInterceptor()) - insertMany(@Body() createDtos: CreateDashboardQuestionDto[], @UploadedFiles() filesArray: Express.Multer.File[][] = []) { - return this.service.insertMany(createDtos, filesArray); - } - - - @ApiBearerAuth("jwt") - @Put(':id') - @UseInterceptors(AnyFilesInterceptor()) - update(@Param('id') id: number, @Body() updateDto: UpdateDashboardQuestionDto, @UploadedFiles() files: Array) { - return this.service.update(id, updateDto, files); - } - - @ApiBearerAuth("jwt") - @Patch(':id') - @UseInterceptors(AnyFilesInterceptor()) - partialUpdate(@Param('id') id: number, @Body() updateDto: UpdateDashboardQuestionDto, @UploadedFiles() files: Array) { - return this.service.update(id, updateDto, files, true); - } - - @ApiBearerAuth("jwt") - @Post('/bulk-recover') - async recoverMany(@Body() ids: number[]) { - return this.service.recoverMany(ids); - } - - @ApiBearerAuth("jwt") - @Get('/recover/:id') - async recover(@Param('id') id: number) { - return this.service.recover(id); - } - - @ApiBearerAuth("jwt") - @ApiQuery({ name: 'showSoftDeleted', required: false, enum: ShowSoftDeleted }) - @ApiQuery({ name: 'limit', required: false, type: Number }) - @ApiQuery({ name: 'offset', required: false, type: Number }) - @ApiQuery({ name: 'fields', required: false, type: Array }) - @ApiQuery({ name: 'sort', required: false, type: Array }) - @ApiQuery({ name: 'groupBy', required: false, type: Array }) - @ApiQuery({ name: 'populate', required: false, type: Array }) - @ApiQuery({ name: 'populateMedia', required: false, type: Array }) - @ApiQuery({ name: 'filters', required: false, type: Array }) - @Get() - async findMany(@Query() query: any) { - return this.service.find(query); - } - - @ApiBearerAuth("jwt") - @Get(':id/data') - async getData( - @Param('id') id: string, - @Query('filters') filters: SqlExpression[], - @Query('isPreview') isPreview?: string - ) { - const previewMode = isPreview === 'true'; - return this.service.getData(+id, filters, previewMode); - } - - @ApiBearerAuth("jwt") - @Get(':id') - async findOne(@Param('id') id: string, @Query() query: any) { - return this.service.findOne(+id, query); - } - - @ApiBearerAuth("jwt") - @Delete('/bulk') - async deleteMany(@Body() ids: number[]) { - return this.service.deleteMany(ids); - } - - @ApiBearerAuth("jwt") - @Delete(':id') - async delete(@Param('id') id: number) { - return this.service.delete(id); - } - -} diff --git a/src/controllers/dashboard-variable.controller.ts b/src/controllers/dashboard-variable.controller.ts deleted file mode 100644 index ec2be829..00000000 --- a/src/controllers/dashboard-variable.controller.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { Controller, Post, Body, Param, UploadedFiles, UseInterceptors, Put, Get, Query, Delete, Patch } from '@nestjs/common'; -import { AnyFilesInterceptor } from "@nestjs/platform-express"; -import { ApiBearerAuth, ApiQuery, ApiTags } from '@nestjs/swagger'; -import { DashboardVariableService } from '../services/dashboard-variable.service'; -import { CreateDashboardVariableDto } from '../dtos/create-dashboard-variable.dto'; -import { UpdateDashboardVariableDto } from '../dtos/update-dashboard-variable.dto'; - -enum ShowSoftDeleted { - INCLUSIVE = "inclusive", - EXCLUSIVE = "exclusive", -} - -@ApiTags('Solid Core') -@Controller('dashboard-variable') -export class DashboardVariableController { - constructor(private readonly service: DashboardVariableService) {} - - @ApiBearerAuth("jwt") - @Post() - @UseInterceptors(AnyFilesInterceptor()) - create(@Body() createDto: CreateDashboardVariableDto, @UploadedFiles() files: Array) { - return this.service.create(createDto, files); - } - - @ApiBearerAuth("jwt") - @Post('/bulk') - @UseInterceptors(AnyFilesInterceptor()) - insertMany(@Body() createDtos: CreateDashboardVariableDto[], @UploadedFiles() filesArray: Express.Multer.File[][] = []) { - return this.service.insertMany(createDtos, filesArray); - } - - - @ApiBearerAuth("jwt") - @Put(':id') - @UseInterceptors(AnyFilesInterceptor()) - update(@Param('id') id: number, @Body() updateDto: UpdateDashboardVariableDto, @UploadedFiles() files: Array) { - return this.service.update(id, updateDto, files); - } - - @ApiBearerAuth("jwt") - @Patch(':id') - @UseInterceptors(AnyFilesInterceptor()) - partialUpdate(@Param('id') id: number, @Body() updateDto: UpdateDashboardVariableDto, @UploadedFiles() files: Array) { - return this.service.update(id, updateDto, files, true); - } - - @ApiBearerAuth("jwt") - @Post('/bulk-recover') - async recoverMany(@Body() ids: number[]) { - return this.service.recoverMany(ids); - } - - @ApiBearerAuth("jwt") - @Get('/recover/:id') - async recover(@Param('id') id: number) { - return this.service.recover(id); - } - - @ApiBearerAuth("jwt") - @ApiQuery({ name: 'showSoftDeleted', required: false, enum: ShowSoftDeleted }) - @ApiQuery({ name: 'limit', required: false, type: Number }) - @ApiQuery({ name: 'offset', required: false, type: Number }) - @ApiQuery({ name: 'fields', required: false, type: Array }) - @ApiQuery({ name: 'sort', required: false, type: Array }) - @ApiQuery({ name: 'groupBy', required: false, type: Array }) - @ApiQuery({ name: 'populate', required: false, type: Array }) - @ApiQuery({ name: 'populateMedia', required: false, type: Array }) - @ApiQuery({ name: 'filters', required: false, type: Array }) - @Get() - async findMany(@Query() query: any) { - return this.service.find(query); - } - - @ApiBearerAuth("jwt") - @Get(':id') - async findOne(@Param('id') id: string, @Query() query: any) { - return this.service.findOne(+id, query); - } - - @ApiBearerAuth("jwt") - @Delete('/bulk') - async deleteMany(@Body() ids: number[]) { - return this.service.deleteMany(ids); - } - - @ApiBearerAuth("jwt") - @Delete(':id') - async delete(@Param('id') id: number) { - return this.service.delete(id); - } - - -} diff --git a/src/controllers/dashboard.controller.ts b/src/controllers/dashboard.controller.ts deleted file mode 100644 index f9a9a7e3..00000000 --- a/src/controllers/dashboard.controller.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query, UploadedFiles, UseInterceptors } from '@nestjs/common'; -import { AnyFilesInterceptor } from "@nestjs/platform-express"; -import { ApiBearerAuth, ApiQuery, ApiTags } from '@nestjs/swagger'; -import { DashboardVariableSelectionDynamicQueryDto } from 'src/dtos/dashboard-variable-selection-dynamic-query.dto'; -import { CreateDashboardDto } from '../dtos/create-dashboard.dto'; -import { UpdateDashboardDto } from '../dtos/update-dashboard.dto'; -import { DashboardService } from '../services/dashboard.service'; - -enum ShowSoftDeleted { - INCLUSIVE = "inclusive", - EXCLUSIVE = "exclusive", -} - -@ApiTags('Solid Core') -@Controller('dashboard') -export class DashboardController { - constructor(private readonly service: DashboardService) { } - - @ApiBearerAuth("jwt") - @Post() - @UseInterceptors(AnyFilesInterceptor()) - create(@Body() createDto: CreateDashboardDto, @UploadedFiles() files: Array) { - return this.service.create(createDto, files); - } - - @ApiBearerAuth("jwt") - @Post('/bulk') - @UseInterceptors(AnyFilesInterceptor()) - insertMany(@Body() createDtos: CreateDashboardDto[], @UploadedFiles() filesArray: Express.Multer.File[][] = []) { - return this.service.insertMany(createDtos, filesArray); - } - - - @ApiBearerAuth("jwt") - @Put(':id') - @UseInterceptors(AnyFilesInterceptor()) - update(@Param('id') id: number, @Body() updateDto: UpdateDashboardDto, @UploadedFiles() files: Array) { - return this.service.update(id, updateDto, files); - } - - @ApiBearerAuth("jwt") - @Patch(':id') - @UseInterceptors(AnyFilesInterceptor()) - partialUpdate(@Param('id') id: number, @Body() updateDto: UpdateDashboardDto, @UploadedFiles() files: Array) { - return this.service.update(id, updateDto, files, true); - } - - @ApiBearerAuth("jwt") - @Post('/bulk-recover') - async recoverMany(@Body() ids: number[]) { - return this.service.recoverMany(ids); - } - - @ApiBearerAuth("jwt") - @Get('/recover/:id') - async recover(@Param('id') id: number) { - return this.service.recover(id); - } - - @ApiBearerAuth("jwt") - @ApiQuery({ name: 'showSoftDeleted', required: false, enum: ShowSoftDeleted }) - @ApiQuery({ name: 'limit', required: false, type: Number }) - @ApiQuery({ name: 'offset', required: false, type: Number }) - @ApiQuery({ name: 'fields', required: false, type: Array }) - @ApiQuery({ name: 'sort', required: false, type: Array }) - @ApiQuery({ name: 'groupBy', required: false, type: Array }) - @ApiQuery({ name: 'populate', required: false, type: Array }) - @ApiQuery({ name: 'populateMedia', required: false, type: Array }) - @ApiQuery({ name: 'filters', required: false, type: Array }) - @Get() - async findMany(@Query() query: any) { - return this.service.find(query); - } - - @ApiBearerAuth("jwt") - @Get('/selection-dynamic-values') - async getSelectionDynamicValues(@Query() query: DashboardVariableSelectionDynamicQueryDto) { - return this.service.getSelectionDynamicValues(query); - } - - @ApiBearerAuth("jwt") - @Get(':id') - async findOne(@Param('id') id: string, @Query() query: any) { - return this.service.findOne(+id, query); - } - - @ApiBearerAuth("jwt") - @Delete('/bulk') - async deleteMany(@Body() ids: number[]) { - return this.service.deleteMany(ids); - } - - @ApiBearerAuth("jwt") - @Delete(':id') - async delete(@Param('id') id: number) { - return this.service.delete(id); - } - -} diff --git a/src/decorators/dashboard-question-data-provider.decorator.ts b/src/decorators/dashboard-question-data-provider.decorator.ts deleted file mode 100644 index 17988e7a..00000000 --- a/src/decorators/dashboard-question-data-provider.decorator.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const IS_DASHBOARD_QUESTION_DATA_PROVIDER = 'IS_DASHBOARD_QUESTION_DATA_PROVIDER'; - -export const DashboardQuestionDataProvider = () => { - return (target: Function) => { - Reflect.defineMetadata(IS_DASHBOARD_QUESTION_DATA_PROVIDER, true, target); - }; -}; \ No newline at end of file diff --git a/src/decorators/dashboard-selection-provider.decorator.ts b/src/decorators/dashboard-selection-provider.decorator.ts deleted file mode 100644 index 034f860d..00000000 --- a/src/decorators/dashboard-selection-provider.decorator.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const IS_DASHBOARD_VARIABLE_SELECTION_PROVIDER = 'IS_DASHBOARD_VARIABLE_SELECTION_PROVIDER'; - -export const DashboardVariableSelectionProvider = () => { - return (target: Function) => { - Reflect.defineMetadata(IS_DASHBOARD_VARIABLE_SELECTION_PROVIDER, true, target); - }; -}; \ No newline at end of file diff --git a/src/dtos/create-dashboard-layout.dto.ts b/src/dtos/create-dashboard-layout.dto.ts deleted file mode 100644 index da15180e..00000000 --- a/src/dtos/create-dashboard-layout.dto.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsString } from 'class-validator'; -import { IsNotEmpty, IsOptional, IsInt } from 'class-validator'; - - -export class CreateDashboardLayoutDto { - @IsNotEmpty() - @IsString() - @ApiProperty() - layout: string; - - @IsOptional() - @IsInt() - @ApiProperty({ description: "Related Dashboard Model" }) - dashboardId: number; - - @IsString() - @IsOptional() - @ApiProperty({ description: "Related Dashboard Model" }) - dashboardUserKey: string; - - @IsOptional() - @IsInt() - @ApiProperty() - userId: number; - - @IsString() - @IsOptional() - @ApiProperty() - userUserKey: string; -} \ No newline at end of file diff --git a/src/dtos/create-dashboard-question-sql-dataset-config.dto.ts b/src/dtos/create-dashboard-question-sql-dataset-config.dto.ts deleted file mode 100644 index f941c628..00000000 --- a/src/dtos/create-dashboard-question-sql-dataset-config.dto.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsString } from 'class-validator'; -import { IsNotEmpty, IsOptional, IsInt, IsJSON } from 'class-validator'; - -export class CreateDashboardQuestionSqlDatasetConfigDto { - @IsNotEmpty() - @IsString() - @ApiProperty() - datasetName: string; - @IsNotEmpty() - @IsString() - @ApiProperty({ description: "This is the display name for the dataset configuration, which can be used in UI components to represent the dataset in a user-friendly manner." }) - datasetDisplayName: string; - @IsOptional() - @IsString() - @ApiProperty({ description: "This is a description of the dataset configuration, providing context and details about its purpose." }) - description: string; - @IsNotEmpty() - @IsString() - @ApiProperty() - sql: string; - @IsNotEmpty() - @IsString() - @ApiProperty() - labelColumnName: string; - @IsNotEmpty() - @IsString() - @ApiProperty() - valueColumnName: string; - @IsOptional() - @IsInt() - @ApiProperty({ description: "Related Question Model" }) - questionId: number; - @IsString() - @IsOptional() - @ApiProperty({ description: "Related Question Model" }) - questionUserKey: string; - @IsOptional() - @IsJSON() - @ApiProperty({ description: "This allows you to set the dataset options e.g border-color, background-color, etc. This is a JSON object that can be used to customize the dataset appearance or behavior in the UI." }) - options: any; -} \ No newline at end of file diff --git a/src/dtos/create-dashboard-question.dto.ts b/src/dtos/create-dashboard-question.dto.ts deleted file mode 100644 index 7efbf5f4..00000000 --- a/src/dtos/create-dashboard-question.dto.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsString } from 'class-validator'; -import { IsNotEmpty, IsOptional, IsJSON, IsInt, ValidateNested, IsArray } from 'class-validator'; -import { Type } from 'class-transformer'; -import { UpdateDashboardQuestionSqlDatasetConfigDto } from 'src/dtos/update-dashboard-question-sql-dataset-config.dto'; - -export class CreateDashboardQuestionDto { - @IsNotEmpty() - @IsString() - @ApiProperty() - name: string; - @IsNotEmpty() - @IsString() - @ApiProperty() - sourceType: string; - @IsNotEmpty() - @IsString() - @ApiProperty() - visualisedAs: string; - @IsOptional() - @IsString() - @ApiProperty({ description: "This is only applicable when sourceType is set to provider. It allows the user to select any pre-existing Dashboard Question Data provider implementation used to fetch a dynamic dropdown of values to choose from when this question is presented to the user." }) - providerName: string; - @IsOptional() - @IsInt() - @ApiProperty({ description: "Related Dashboard Model" }) - dashboardId: number; - @IsString() - @IsOptional() - @ApiProperty({ description: "Related Dashboard Model" }) - dashboardUserKey: string; - @IsOptional() - @ApiProperty({ description: "Related Question SQL Dataset Config Model" }) - @IsArray() - @ValidateNested({ each: true }) - @Type(() => UpdateDashboardQuestionSqlDatasetConfigDto) - questionSqlDatasetConfigs: UpdateDashboardQuestionSqlDatasetConfigDto[]; - @IsOptional() - @IsArray() - @ApiProperty({ description: "Related Question SQL Dataset Config Model" }) - questionSqlDatasetConfigsIds: number[]; - @IsString() - @IsOptional() - @ApiProperty({ description: "Related Question SQL Dataset Config Model" }) - questionSqlDatasetConfigsCommand: string; - @IsOptional() - @IsJSON() - @ApiProperty({ description: "This is a JSON object representing each labels display and color options for the bar chart" }) - chartOptions: any; - @IsOptional() - @IsString() - @ApiProperty({ description: "This is the SQL query to fetch the label values for the question" }) - labelSql: string; - @IsOptional() - @IsString() - @ApiProperty({ description: "This is the SQL query to fetch the KPI value for the question" }) - kpiSql: string; - @IsOptional() - @IsInt() - @ApiProperty() - sequenceNumber: number; -} \ No newline at end of file diff --git a/src/dtos/create-dashboard-variable.dto.ts b/src/dtos/create-dashboard-variable.dto.ts deleted file mode 100644 index c8155d64..00000000 --- a/src/dtos/create-dashboard-variable.dto.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsString } from 'class-validator'; -import { IsNotEmpty, IsJSON, IsOptional, IsBoolean, IsInt } from 'class-validator'; - -export enum SelectionDynamicSourceType { - SQL = "sql", - PROVIDER = "provider", -} - - -export class CreateDashboardVariableDto { - @IsNotEmpty() - @IsString() - @ApiProperty() - variableName: string; - @IsNotEmpty() - @IsString() - @ApiProperty() - variableType: string; - @IsOptional() - @IsJSON() - @ApiProperty() - selectionStaticValues: any; - @IsOptional() - @IsString() - @ApiProperty() - selectionDynamicSourceType: string; - @IsOptional() - @IsString() - @ApiProperty({ description: "SQL query to fetch the data for this variable when it is rendered at runtime. This is only applicable when selectionDynamicSourceType is set to SQL." }) - selectionDynamicSQL: string; - @IsOptional() - @IsString() - @ApiProperty({ description: "This is only applicable when selectionDynamicSourceType is set to provider. It allows the user to select any pre-existing Dashboard SelectionDynamicProvider implementation used to fetch a dynamic dropdown of values to choose from when this variable is presented to the user." }) - selectionDynamicProviderName: string; - @IsOptional() - @IsBoolean() - @ApiProperty({ description: "This is relevant only for variables of type \"selectionStatic\" or \"selectionDynamic\". When set to true, it allows the user to select multiple values from the dropdown." }) - isMultiSelect: boolean = true; - @IsOptional() - @IsInt() - @ApiProperty({ description: "Related Dashboard Model" }) - dashboardId: number; - @IsString() - @IsOptional() - @ApiProperty({ description: "Related Dashboard Model" }) - dashboardUserKey: string; - @IsOptional() - @IsString() - @ApiProperty({ description: "This is the default value for this variable when it is rendered at runtime. It can be a static value for this variable when it is rendered at runtime." }) - defaultValue: string; - @IsOptional() - @IsString() - @ApiProperty({ description: "This is the default operator for this variable when it is rendered at runtime. It can be a static value for this variable when it is rendered at runtime." }) - defaultOperator: string; - @IsString() - @IsOptional() - @ApiProperty() - externalId: string; -} \ No newline at end of file diff --git a/src/dtos/create-dashboard.dto.ts b/src/dtos/create-dashboard.dto.ts deleted file mode 100644 index 11abd1df..00000000 --- a/src/dtos/create-dashboard.dto.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsString } from 'class-validator'; -import { IsNotEmpty, ValidateNested, IsArray, IsOptional, IsJSON, IsInt } from 'class-validator'; -import { Type } from 'class-transformer'; -import { UpdateDashboardVariableDto } from 'src/dtos/update-dashboard-variable.dto'; -import { UpdateDashboardQuestionDto } from 'src/dtos/update-dashboard-question.dto'; - -export class CreateDashboardDto { - @IsNotEmpty() - @IsString() - @ApiProperty() - name: string; - @IsNotEmpty() - @IsJSON() - @ApiProperty() - layoutJson: any; - @IsOptional() - @ApiProperty() - @IsArray() - @ValidateNested({ each: true }) - @Type(() => UpdateDashboardVariableDto) - dashboardVariables: UpdateDashboardVariableDto[]; - @IsOptional() - @IsArray() - @ApiProperty() - dashboardVariablesIds: number[]; - @IsString() - @IsOptional() - @ApiProperty() - dashboardVariablesCommand: string; - @IsOptional() - @ApiProperty() - @IsArray() - @ValidateNested({ each: true }) - @Type(() => UpdateDashboardQuestionDto) - questions: UpdateDashboardQuestionDto[]; - @IsOptional() - @IsArray() - @ApiProperty() - questionsIds: number[]; - @IsString() - @IsOptional() - @ApiProperty() - questionsCommand: string; - @IsOptional() - @IsInt() - @ApiProperty() - moduleId: number; - @IsString() - @IsOptional() - @ApiProperty() - moduleUserKey: string; - @IsOptional() - @IsString() - @ApiProperty() - displayName: string; - @IsOptional() - @IsString() - @ApiProperty({ description: "This is a description of the dashboard configuration, providing context and details about the dashboard." }) - description: string; -} \ No newline at end of file diff --git a/src/dtos/dashboard-variable-selection-dynamic-query.dto.ts b/src/dtos/dashboard-variable-selection-dynamic-query.dto.ts deleted file mode 100644 index c28a509c..00000000 --- a/src/dtos/dashboard-variable-selection-dynamic-query.dto.ts +++ /dev/null @@ -1,29 +0,0 @@ - -import { ApiProperty } from "@nestjs/swagger"; -import { Transform, Type } from "class-transformer"; -import { IsNumber, IsOptional, IsString } from "class-validator"; -import { PaginationQueryDto } from "src/dtos/pagination-query.dto"; -import integerTransformer from "../transformers/integer-transformer"; - -export class DashboardVariableSelectionDynamicQueryDto extends PaginationQueryDto { - constructor(variableId: number, query: string, limit: number, offset: number) { - super(limit, offset); - this.variableId = variableId; - this.query = query; - } - - @ApiProperty({ description: "Field ID of the field against which the dynamic value provider is registered.", type: Number }) - @IsNumber() - @Transform(integerTransformer) - variableId: number; - - @ApiProperty({ description: "Search query string", type: String }) - @IsString() - @IsOptional() - query?: any; - - @ApiProperty({ description: "Value of a single dynamic option", type: String }) - @IsString() - @IsOptional() - optionValue?: string = ''; -} diff --git a/src/dtos/update-dashboard-layout.dto.ts b/src/dtos/update-dashboard-layout.dto.ts deleted file mode 100644 index cdaa8c3f..00000000 --- a/src/dtos/update-dashboard-layout.dto.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { IsInt, IsOptional, IsString, IsNotEmpty } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; - -export class UpdateDashboardLayoutDto { - @IsNotEmpty() - @IsOptional() - @IsString() - @ApiProperty() - layout: string; - - @IsOptional() - @IsInt() - @ApiProperty({ description: "Related Dashboard Model" }) - dashboardId: number; - - @IsString() - @IsOptional() - @ApiProperty({ description: "Related Dashboard Model" }) - dashboardUserKey: string; - - @IsOptional() - @IsInt() - @ApiProperty() - userId: number; - - @IsString() - @IsOptional() - @ApiProperty() - userUserKey: string; -} \ No newline at end of file diff --git a/src/dtos/update-dashboard-question-sql-dataset-config.dto.ts b/src/dtos/update-dashboard-question-sql-dataset-config.dto.ts deleted file mode 100644 index bd271273..00000000 --- a/src/dtos/update-dashboard-question-sql-dataset-config.dto.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { IsInt, IsOptional, IsString, IsNotEmpty, IsJSON } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; - -export class UpdateDashboardQuestionSqlDatasetConfigDto { - @IsOptional() - @IsInt() - id: number; - @IsNotEmpty() - @IsOptional() - @IsString() - @ApiProperty() - datasetName: string; - @IsNotEmpty() - @IsOptional() - @IsString() - @ApiProperty({ description: "This is the display name for the dataset configuration, which can be used in UI components to represent the dataset in a user-friendly manner." }) - datasetDisplayName: string; - @IsOptional() - @IsString() - @ApiProperty({ description: "This is a description of the dataset configuration, providing context and details about its purpose." }) - description: string; - @IsNotEmpty() - @IsOptional() - @IsString() - @ApiProperty() - sql: string; - @IsNotEmpty() - @IsOptional() - @IsString() - @ApiProperty() - labelColumnName: string; - @IsNotEmpty() - @IsOptional() - @IsString() - @ApiProperty() - valueColumnName: string; - @IsOptional() - @IsInt() - @ApiProperty({ description: "Related Question Model" }) - questionId: number; - @IsString() - @IsOptional() - @ApiProperty({ description: "Related Question Model" }) - questionUserKey: string; - @IsOptional() - @IsJSON() - @ApiProperty({ description: "This allows you to set the dataset options e.g border-color, background-color, etc. This is a JSON object that can be used to customize the dataset appearance or behavior in the UI." }) - options: any; -} \ No newline at end of file diff --git a/src/dtos/update-dashboard-question.dto.ts b/src/dtos/update-dashboard-question.dto.ts deleted file mode 100644 index 49dd70e0..00000000 --- a/src/dtos/update-dashboard-question.dto.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { IsInt,IsOptional, IsString, IsNotEmpty, IsJSON, ValidateNested, IsArray } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { UpdateDashboardQuestionSqlDatasetConfigDto } from 'src/dtos/update-dashboard-question-sql-dataset-config.dto'; - -export class UpdateDashboardQuestionDto { - @IsOptional() - @IsInt() - id: number; - @IsNotEmpty() - @IsOptional() - @IsString() - @ApiProperty() - name: string; - @IsNotEmpty() - @IsOptional() - @IsString() - @ApiProperty() - sourceType: string; - @IsNotEmpty() - @IsOptional() - @IsString() - @ApiProperty() - visualisedAs: string; - @IsOptional() - @IsString() - @ApiProperty({ description: "This is only applicable when sourceType is set to provider. It allows the user to select any pre-existing Dashboard Question Data provider implementation used to fetch a dynamic dropdown of values to choose from when this question is presented to the user." }) - providerName: string; - @IsOptional() - @IsInt() - @ApiProperty({ description: "Related Dashboard Model" }) - dashboardId: number; - @IsString() - @IsOptional() - @ApiProperty({ description: "Related Dashboard Model" }) - dashboardUserKey: string; - @IsOptional() - @ApiProperty({ description: "Related Question SQL Dataset Config Model" }) - @IsArray() - @ValidateNested({ each: true }) - @Type(() => UpdateDashboardQuestionSqlDatasetConfigDto) - questionSqlDatasetConfigs: UpdateDashboardQuestionSqlDatasetConfigDto[]; - @IsOptional() - @IsArray() - @ApiProperty({ description: "Related Question SQL Dataset Config Model" }) - questionSqlDatasetConfigsIds: number[]; - @IsString() - @IsOptional() - @ApiProperty({ description: "Related Question SQL Dataset Config Model" }) - questionSqlDatasetConfigsCommand: string; - @IsOptional() - @IsJSON() - @ApiProperty({ description: "This is a JSON object representing each labels display and color options for the bar chart" }) - chartOptions: any; - @IsOptional() - @IsString() - @ApiProperty({ description: "This is the SQL query to fetch the label values for the question" }) - labelSql: string; - @IsOptional() - @IsString() - @ApiProperty({ description: "This is the SQL query to fetch the KPI value for the question" }) - kpiSql: string; - @IsOptional() - @IsInt() - @ApiProperty() - sequenceNumber: number; -} \ No newline at end of file diff --git a/src/dtos/update-dashboard-variable.dto.ts b/src/dtos/update-dashboard-variable.dto.ts deleted file mode 100644 index 5d65f811..00000000 --- a/src/dtos/update-dashboard-variable.dto.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { IsInt, IsOptional, IsString, IsNotEmpty, IsJSON, IsBoolean } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; - -export class UpdateDashboardVariableDto { - @IsOptional() - @IsInt() - id: number; - @IsNotEmpty() - @IsOptional() - @IsString() - @ApiProperty() - variableName: string; - @IsNotEmpty() - @IsOptional() - @IsString() - @ApiProperty() - variableType: string; - @IsOptional() - @IsJSON() - @ApiProperty() - selectionStaticValues: any; - @IsOptional() - @IsString() - @ApiProperty() - selectionDynamicSourceType: string; - @IsOptional() - @IsString() - @ApiProperty({ description: "SQL query to fetch the data for this variable when it is rendered at runtime. This is only applicable when selectionDynamicSourceType is set to SQL." }) - selectionDynamicSQL: string; - @IsOptional() - @IsString() - @ApiProperty({ description: "This is only applicable when selectionDynamicSourceType is set to provider. It allows the user to select any pre-existing Dashboard SelectionDynamicProvider implementation used to fetch a dynamic dropdown of values to choose from when this variable is presented to the user." }) - selectionDynamicProviderName: string; - @IsOptional() - @IsBoolean() - @ApiProperty({ description: "This is relevant only for variables of type \"selectionStatic\" or \"selectionDynamic\". When set to true, it allows the user to select multiple values from the dropdown." }) - isMultiSelect: boolean; - @IsOptional() - @IsInt() - @ApiProperty({ description: "Related Dashboard Model" }) - dashboardId: number; - @IsString() - @IsOptional() - @ApiProperty({ description: "Related Dashboard Model" }) - dashboardUserKey: string; - @IsOptional() - @IsString() - @ApiProperty({ description: "This is the default value for this variable when it is rendered at runtime. It can be a static value for this variable when it is rendered at runtime." }) - defaultValue: string; - @IsOptional() - @IsString() - @ApiProperty({ description: "This is the default operator for this variable when it is rendered at runtime. It can be a static value for this variable when it is rendered at runtime." }) - defaultOperator: string; - @IsString() - @IsOptional() - @ApiProperty() - externalId: string; -} \ No newline at end of file diff --git a/src/dtos/update-dashboard.dto.ts b/src/dtos/update-dashboard.dto.ts deleted file mode 100644 index 60359fc2..00000000 --- a/src/dtos/update-dashboard.dto.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { IsInt,IsOptional, IsString, IsNotEmpty, ValidateNested, IsArray, IsJSON } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { UpdateDashboardVariableDto } from 'src/dtos/update-dashboard-variable.dto'; -import { UpdateDashboardQuestionDto } from 'src/dtos/update-dashboard-question.dto'; - -export class UpdateDashboardDto { - @IsOptional() - @IsInt() - id: number; - @IsNotEmpty() - @IsOptional() - @IsString() - @ApiProperty() - name: string; - @IsNotEmpty() - @IsOptional() - @IsJSON() - @ApiProperty() - layoutJson: any; - @IsOptional() - @ApiProperty() - @IsArray() - @ValidateNested({ each: true }) - @Type(() => UpdateDashboardVariableDto) - dashboardVariables: UpdateDashboardVariableDto[]; - @IsOptional() - @IsArray() - @ApiProperty() - dashboardVariablesIds: number[]; - @IsString() - @IsOptional() - @ApiProperty() - dashboardVariablesCommand: string; - @IsOptional() - @ApiProperty() - @IsArray() - @ValidateNested({ each: true }) - @Type(() => UpdateDashboardQuestionDto) - questions: UpdateDashboardQuestionDto[]; - @IsOptional() - @IsArray() - @ApiProperty() - questionsIds: number[]; - @IsString() - @IsOptional() - @ApiProperty() - questionsCommand: string; - @IsOptional() - @IsInt() - @ApiProperty() - moduleId: number; - @IsString() - @IsOptional() - @ApiProperty() - moduleUserKey: string; - @IsOptional() - @IsString() - @ApiProperty() - displayName: string; - @IsOptional() - @IsString() - @ApiProperty({ description: "This is a description of the dashboard configuration, providing context and details about the dashboard." }) - description: string; -} \ No newline at end of file diff --git a/src/entities/dashboard-layout.entity.ts b/src/entities/dashboard-layout.entity.ts deleted file mode 100644 index c6ba347f..00000000 --- a/src/entities/dashboard-layout.entity.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { CommonEntity } from 'src/entities/common.entity' -import { Entity, Column, JoinColumn, ManyToOne } from 'typeorm'; -import { Dashboard } from 'src/entities/dashboard.entity' -import { User } from './user.entity'; - -@Entity("ss_dashboard_layout") -export class DashboardLayout extends CommonEntity { - @Column({ type: "text", nullable: true }) - layout: string; - - @ManyToOne(() => Dashboard, { nullable: true }) - @JoinColumn() - dashboard: Dashboard; - - @ManyToOne(() => User, { nullable: true }) - @JoinColumn() - user: User; -} \ No newline at end of file diff --git a/src/entities/dashboard-question-sql-dataset-config.entity.ts b/src/entities/dashboard-question-sql-dataset-config.entity.ts deleted file mode 100644 index af0a8ea4..00000000 --- a/src/entities/dashboard-question-sql-dataset-config.entity.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { CommonEntity } from 'src/entities/common.entity' -import { Entity, Column, Index, JoinColumn, ManyToOne } from 'typeorm'; -import { DashboardQuestion } from 'src/entities/dashboard-question.entity' -import { getColumnType } from 'src/helpers/typeorm-db-helper'; - -@Entity("ss_dashboard_question_sql_dataset_config") -export class DashboardQuestionSqlDatasetConfig extends CommonEntity { - @Index() - @Column({ type: "varchar" }) - datasetName: string; - - @Column({ type: "varchar" }) - datasetDisplayName: string; - - @Column({ nullable: true }) - description: string; - - @Column({ ...getColumnType('longText'), nullable: true }) - sql: string; - - @Column({ type: "varchar" }) - labelColumnName: string; - - @Column({ type: "varchar" }) - valueColumnName: string; - - @ManyToOne(() => DashboardQuestion, { nullable: false }) - @JoinColumn() - question: DashboardQuestion; - - @Column({ nullable: true, ...getColumnType('longText') }) - options: any; - - @Index({ unique: true }) - @Column({ type: "varchar", nullable: false }) - externalId: string; -} diff --git a/src/entities/dashboard-question.entity.ts b/src/entities/dashboard-question.entity.ts deleted file mode 100644 index 4618d381..00000000 --- a/src/entities/dashboard-question.entity.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { CommonEntity } from 'src/entities/common.entity' -import { Entity, Column, Index, JoinColumn, ManyToOne, OneToMany } from 'typeorm'; -import { getColumnType } from 'src/helpers/typeorm-db-helper'; -import { Dashboard } from 'src/entities/dashboard.entity'; -import { DashboardQuestionSqlDatasetConfig } from 'src/entities/dashboard-question-sql-dataset-config.entity' - -@Entity("ss_dashboard_question") -export class DashboardQuestion extends CommonEntity { - @Index() - @Column({ type: "varchar" }) - name: string; - - @Index() - @Column({}) - sourceType: string; - - @Index() - @Column({}) - visualisedAs: string; - - @Column({ type: "varchar", nullable: true }) - providerName: string; - - @ManyToOne(() => Dashboard, { nullable: true }) - @JoinColumn() - dashboard: Dashboard; - - @OneToMany(() => DashboardQuestionSqlDatasetConfig, dashboardQuestionSqlDatasetConfig => dashboardQuestionSqlDatasetConfig.question, { cascade: true }) - questionSqlDatasetConfigs: DashboardQuestionSqlDatasetConfig[]; - - @Column({ type: "simple-json", nullable: true, ...getColumnType('simpleJsonLargeText') }) - chartOptions: any; - - @Column({ nullable: true, ...getColumnType('longText') }) - labelSql: string; - - @Column({ nullable: true, ...getColumnType('longText') }) - kpiSql: string; - - @Column({ type: "integer", nullable: true }) - sequenceNumber: number; - - @Index({ unique: true }) - @Column({ type: "varchar", nullable: false }) - externalId: string; -} \ No newline at end of file diff --git a/src/entities/dashboard-variable.entity.ts b/src/entities/dashboard-variable.entity.ts deleted file mode 100644 index 33aa05d1..00000000 --- a/src/entities/dashboard-variable.entity.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { CommonEntity } from 'src/entities/common.entity' -import { Entity, Column, Index, JoinColumn, ManyToOne } from 'typeorm'; -import { Dashboard } from 'src/entities/dashboard.entity' -import { getColumnType } from 'src/helpers/typeorm-db-helper'; - -@Entity("ss_dashboard_variable") -export class DashboardVariable extends CommonEntity { - @Index() - @Column({ type: "varchar" }) - variableName: string; - - @Index() - @Column({ type: "varchar" }) - variableType: string; - - @Column({ type: "simple-json", nullable: true }) - selectionStaticValues: any; - - @Column({ nullable: true }) - selectionDynamicSourceType: string; - - @Column({ nullable: true, ...getColumnType('longText') }) - selectionDynamicSQL: string; - - @Column({ type: "varchar", nullable: true }) - selectionDynamicProviderName: string; - - @Column({ nullable: true, default: true }) - isMultiSelect: boolean = true; - - @ManyToOne(() => Dashboard, { nullable: true }) - @JoinColumn() - dashboard: Dashboard; - - @Column({ nullable: true}) - defaultValue: string; - - @Column({ type: "varchar", nullable: true }) - defaultOperator: string; - - @Index({ unique: true }) - @Column({ type: "varchar" }) - externalId: string; -} \ No newline at end of file diff --git a/src/entities/dashboard.entity.ts b/src/entities/dashboard.entity.ts deleted file mode 100644 index 2eb65092..00000000 --- a/src/entities/dashboard.entity.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { CommonEntity } from 'src/entities/common.entity' -import { Entity, Column, Index, OneToMany, JoinColumn, ManyToOne } from 'typeorm'; -import { getColumnType } from 'src/helpers/typeorm-db-helper'; -import { DashboardVariable } from 'src/entities/dashboard-variable.entity'; -import { DashboardQuestion } from 'src/entities/dashboard-question.entity'; -import { ModuleMetadata } from 'src/entities/module-metadata.entity' -import { DashboardLayout } from './dashboard-layout.entity'; - -@Entity("ss_dashboard") -export class Dashboard extends CommonEntity { - @Index({ unique: true }) - @Column({ type: "varchar" }) - name: string; - - @Column({ ...getColumnType('longText'), nullable: true }) - layoutJson: any; - - @OneToMany(() => DashboardVariable, dashboardVariable => dashboardVariable.dashboard, { cascade: true }) - dashboardVariables: DashboardVariable[]; - - @OneToMany(() => DashboardQuestion, dashboardQuestion => dashboardQuestion.dashboard, { cascade: true }) - questions: DashboardQuestion[]; - - @OneToMany(() => DashboardLayout, dashboardLayout => dashboardLayout.dashboard, { cascade: true }) - dashboardLayouts: DashboardLayout[]; - - @ManyToOne(() => ModuleMetadata, { nullable: false }) - @JoinColumn() - module: ModuleMetadata; - - @Column({ type: "varchar", nullable: true }) - displayName: string; - - @Column({ nullable: true }) - description: string; -} \ No newline at end of file diff --git a/src/helpers/solid-registry.ts b/src/helpers/solid-registry.ts index ee65f982..ea01efb0 100755 --- a/src/helpers/solid-registry.ts +++ b/src/helpers/solid-registry.ts @@ -7,7 +7,7 @@ import { CommonEntity } from 'src/entities/common.entity'; import { Locale } from 'src/entities/locale.entity'; import { SecurityRule } from 'src/entities/security-rule.entity'; import { IScheduledJob } from 'src/services/scheduled-jobs/scheduled-job.interface'; -import { IDashboardQuestionDataProvider, IDashboardVariableSelectionProvider, IErrorCodeProvider, ISecurityRuleConfigProvider, ISelectionProvider, ISelectionProviderContext, ISolidDatabaseModule } from "../interfaces"; +import { IErrorCodeProvider, ISecurityRuleConfigProvider, ISelectionProvider, ISelectionProviderContext, ISolidDatabaseModule } from "../interfaces"; import { ObjectLiteral } from 'typeorm'; type ControllerMetadata = { @@ -78,8 +78,6 @@ export class SolidRegistry { private securityRules: SecurityRule[] = []; private locales: Locale[] = []; private computedFieldMetadata: ComputedFieldMetadata[] = []; - private dashboardVariableSelectionProviders: Set = new Set(); - private dashboardQuestionDataProviders: Set = new Set(); private mailProviders: Set = new Set(); private whatsappProviders: Set = new Set(); private smsProviders: Set = new Set(); @@ -138,14 +136,6 @@ export class SolidRegistry { this.selectionProviders.add(selectionProvider); } - registerDashboardVariableSelectionProvider(dashboardSelectionProvider: InstanceWrapper): void { - this.dashboardVariableSelectionProviders.add(dashboardSelectionProvider); - } - - registerDashboardQuestionDataProvider(dashboardQuestionDataProvider: InstanceWrapper): void { - this.dashboardQuestionDataProviders.add(dashboardQuestionDataProvider); - } - registerComputedFieldProvider(computedFieldProvider: InstanceWrapper): void { this.computedFieldProviders.add(computedFieldProvider); } @@ -243,21 +233,6 @@ export class SolidRegistry { } } - getDashboardVariableSelectionProviders(): Array { - return Array.from(this.dashboardVariableSelectionProviders); - } - - getDashboardVariableSelectionProviderInstance(name: string): IDashboardVariableSelectionProvider { - const dashboardSelectionProviders = this.getDashboardVariableSelectionProviders(); - - for (let i = 0; i < dashboardSelectionProviders.length; i++) { - const dashboardSelectionProvider = dashboardSelectionProviders[i]; - if (dashboardSelectionProvider.instance.name() === name) { - return dashboardSelectionProvider.instance; - } - } - } - getErrorCodeProviders(): Array { return Array.from(this.errorCodeProviders); } @@ -271,21 +246,6 @@ export class SolidRegistry { return undefined; } - getDashboardQuestionDataProviders(): Array { - return Array.from(this.dashboardQuestionDataProviders) - } - - getDashboardQuestionDataProviderInstance(name: string): IDashboardQuestionDataProvider { - const dashboardQuestionDataProviders = this.getDashboardQuestionDataProviders(); - - for (let i = 0; i < dashboardQuestionDataProviders.length; i++) { - const dasbhoardQuestionDataProvider = dashboardQuestionDataProviders[i]; - if (dasbhoardQuestionDataProvider.instance.name() === name) { - return dasbhoardQuestionDataProvider.instance; - } - } - } - getComputedFieldProviders(): Array { return Array.from(this.computedFieldProviders); } @@ -380,4 +340,3 @@ export class SolidRegistry { } } } - diff --git a/src/index.ts b/src/index.ts index 800b4617..4398e219 100755 --- a/src/index.ts +++ b/src/index.ts @@ -146,11 +146,6 @@ export * from './entities/import-transaction.entity' export * from './entities/import-transaction-error-log.entity' export * from './entities/locale.entity' export * from './entities/user-activity-history.entity' -export * from './entities/dashboard.entity' -export * from './entities/dashboard-variable.entity' -export * from './entities/dashboard-question.entity' -export * from './entities/dashboard-layout.entity' -export * from './entities/dashboard-question-sql-dataset-config.entity' export * from './entities/ai-interaction.entity' export * from './entities/model-sequence.entity' export * from './entities/user-api-key.entity' diff --git a/src/interfaces.ts b/src/interfaces.ts index 20e0d511..bb2089f6 100755 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -12,10 +12,7 @@ import { CreateRoleMetadataDto } from './dtos/create-role-metadata.dto'; import { CreateSecurityRuleDto } from './dtos/create-security-rule.dto'; import { FieldMetadata } from './entities/field-metadata.entity'; import { Media } from './entities/media.entity'; -import { DashboardQuestion } from './entities/dashboard-question.entity'; import { ComputedFieldMetadata } from './helpers/solid-registry'; -import { SqlExpression } from './services/question-data-providers/chartjs-sql-data-provider.service'; -import { CreateDashboardDto } from './dtos/create-dashboard.dto'; import { AiInteraction } from './entities/ai-interaction.entity'; import { ActiveUserData } from './interfaces/active-user-data.interface'; import { SecurityRuleConfig } from './dtos/security-rule-config.dto'; @@ -59,7 +56,7 @@ export interface ModuleMetadataConfiguration { smsTemplates?: CreateSmsTemplateDto[], mediaStorageProviders?: CreateMediaStorageProviderMetadataDto[] securityRules?: CreateSecurityRuleDto[], - dashboards?: CreateDashboardDto[], + dashboards?: any[], } export enum SettingLevel { @@ -166,27 +163,10 @@ export interface ISelectionProvider { values(query: any, ctxt: T): Promise; } -export interface IDashboardVariableSelectionProvider extends ISelectionProvider { -} - export interface IMcpToolResponseHandler { apply(aiInteraction: AiInteraction); } -export interface QuestionSqlDataProviderContext { - // questionSqlDatasetConfig: QuestionSqlDatasetConfig; - // questionId: number; - // question: Question; - expressions?: SqlExpression[] -} -export interface IDashboardQuestionDataProvider { - help(): string; - - name(): string; - - getData(question: DashboardQuestion, ctxt?: TContext): Promise; -} - /** * @deprecated Use `IEntityComputedFieldProvider` instead. */ diff --git a/src/jobs/database/trigger-mcp-client-subscriber-database.service.ts b/src/jobs/database/trigger-mcp-client-subscriber-database.service.ts index 0e54ef8a..5fe4cdf5 100644 --- a/src/jobs/database/trigger-mcp-client-subscriber-database.service.ts +++ b/src/jobs/database/trigger-mcp-client-subscriber-database.service.ts @@ -2,12 +2,10 @@ import { Injectable, Logger } from '@nestjs/common'; import { ModelMetadataService } from '../../services/model-metadata.service'; import { SolidRegistry } from 'src/helpers/solid-registry'; import { QueueMessage } from 'src/interfaces/mq'; -import { DashboardRepository } from 'src/repository/dashboard.repository'; import { AiInteractionService } from 'src/services/ai-interaction.service'; import { ModuleMetadataService } from 'src/services/module-metadata.service'; import { PollerService } from 'src/services/poller.service'; import { DatabaseSubscriber } from 'src/services/queues/database-subscriber.service'; -import { Not } from 'typeorm'; import { McpResponse, QueuesModuleOptions, TriggerMcpClientOptions } from "../../interfaces"; import { MqMessageQueueService } from '../../services/mq-message-queue.service'; import { MqMessageService } from '../../services/mq-message.service'; @@ -24,9 +22,7 @@ export class TriggerMcpClientSubscriberDatabase extends DatabaseSubscriber [ - `### ${d.displayName}`, - `- name: ${d.name}`, - `- description: ${d.description ?? ""}`, - ].join('\n')) - .join('\n\n'); - const computationProvidersSection = (existingComputationProviders ?? []) .map(m => [ `### ${m.instance.name()}`, @@ -213,11 +185,6 @@ You need to pull out the singularName for the model from the user prompt to matc ${modelsSection} -## LIST OF EXISTING DASHBOARDS -Use the below list of dashboards to infer which dashboard the user is referring to. - -${dashboardsSection} - ## LIST OF EXISTING COMPUTED FIELD PROVIDERS Use the below list of computed field providers to infer which provider the user is referring to. diff --git a/src/mappers/dashboard-mapper.ts b/src/mappers/dashboard-mapper.ts deleted file mode 100644 index 5372e7fb..00000000 --- a/src/mappers/dashboard-mapper.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { Injectable } from "@nestjs/common"; -import { SelectionDynamicSourceType } from "src/dtos/create-dashboard-variable.dto"; -import { Dashboard } from "src/entities/dashboard.entity"; - -@Injectable() -export class DashboardMapper { - toDto(dashboard: Dashboard): any { - return { - name: dashboard.name, - layoutJson: this.safeParseJSON(dashboard.layoutJson, {}), - moduleUserKey: dashboard.module?.name ?? null, // safer fallback - - dashboardVariables: (dashboard.dashboardVariables || []).map(variable => ({ - variableName: variable.variableName, - variableType: variable.variableType, - selectionStaticValues: this.safeParseJSON(variable.selectionStaticValues, []), - selectionDynamicSourceType: variable.selectionDynamicSourceType as SelectionDynamicSourceType, - selectionDynamicSQL: variable.selectionDynamicSQL ?? null, - selectionDynamicProviderName: variable.selectionDynamicProviderName ?? null, - defaultValue: this.safeParseJSON(variable.defaultValue, []), - defaultOperator: variable.defaultOperator ?? null, - })), - questions: (dashboard.questions || []).map(question => ({ - name: question.name, - sourceType: question.sourceType, - visualisedAs: question.visualisedAs, - providerName: question.providerName ?? null, - chartOptions: question.chartOptions ?? null, - labelSql: question.labelSql ?? null, - kpiSql: question.kpiSql ?? null, - externalId: question.externalId, - questionSqlDatasetConfigs: (question.questionSqlDatasetConfigs || []).map(config => ({ - sql: config.sql, - datasetName: config.datasetName, - datasetDisplayName: config.datasetDisplayName, // 🔧 fixed typo: `daataSetDisplayName` - description: config.description, - labelColumnName: config.labelColumnName, - valueColumnName: config.valueColumnName, - options: this.safeParseJSON(config.options, {}), - externalId: config.externalId - })) - })) - }; - } - - private safeParseJSON(json: string | null | undefined, fallback: any): any { - try { - return json ? JSON.parse(json) : fallback; - } catch { - return fallback; - } - } -} \ No newline at end of file diff --git a/src/repository/dashboard-layout.repository.ts b/src/repository/dashboard-layout.repository.ts deleted file mode 100644 index 22eab3db..00000000 --- a/src/repository/dashboard-layout.repository.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { RequestContextService } from 'src/services/request-context.service'; -import { DataSource } from 'typeorm'; -import { SecurityRuleRepository } from './security-rule.repository'; -import { SolidBaseRepository } from './solid-base.repository'; -import { DashboardLayout } from 'src/entities/dashboard-layout.entity'; - -@Injectable() -export class DashboardLayoutRepository extends SolidBaseRepository { - constructor( - readonly dataSource: DataSource, - readonly requestContextService: RequestContextService, - readonly securityRuleRepository: SecurityRuleRepository, - ) { - super(DashboardLayout, dataSource, requestContextService, securityRuleRepository); - } -} \ No newline at end of file diff --git a/src/repository/dashboard-question-sql-dataset-config.repository.ts b/src/repository/dashboard-question-sql-dataset-config.repository.ts deleted file mode 100644 index a2a7514b..00000000 --- a/src/repository/dashboard-question-sql-dataset-config.repository.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { DashboardQuestionSqlDatasetConfig } from '../entities/dashboard-question-sql-dataset-config.entity'; -import { RequestContextService } from 'src/services/request-context.service'; -import { DataSource } from 'typeorm'; -import { SecurityRuleRepository } from './security-rule.repository'; -import { SolidBaseRepository } from './solid-base.repository'; - -@Injectable() -export class DashboardQuestionSqlDatasetConfigRepository extends SolidBaseRepository { - constructor( - readonly dataSource: DataSource, - readonly requestContextService: RequestContextService, - readonly securityRuleRepository: SecurityRuleRepository, - ) { - super(DashboardQuestionSqlDatasetConfig, dataSource, requestContextService, securityRuleRepository); - } -} \ No newline at end of file diff --git a/src/repository/dashboard-question.repository.ts b/src/repository/dashboard-question.repository.ts deleted file mode 100644 index 998e28e7..00000000 --- a/src/repository/dashboard-question.repository.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { DashboardQuestion } from '../entities/dashboard-question.entity'; -import { RequestContextService } from 'src/services/request-context.service'; -import { DataSource } from 'typeorm'; -import { SecurityRuleRepository } from './security-rule.repository'; -import { SolidBaseRepository } from './solid-base.repository'; - -@Injectable() -export class DashboardQuestionRepository extends SolidBaseRepository { - constructor( - readonly dataSource: DataSource, - readonly requestContextService: RequestContextService, - readonly securityRuleRepository: SecurityRuleRepository, - ) { - super(DashboardQuestion, dataSource, requestContextService, securityRuleRepository); - } -} \ No newline at end of file diff --git a/src/repository/dashboard-variable.repository.ts b/src/repository/dashboard-variable.repository.ts deleted file mode 100644 index f4416f4f..00000000 --- a/src/repository/dashboard-variable.repository.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { DashboardVariable } from '../entities/dashboard-variable.entity'; -import { RequestContextService } from 'src/services/request-context.service'; -import { DataSource } from 'typeorm'; -import { SecurityRuleRepository } from './security-rule.repository'; -import { SolidBaseRepository } from './solid-base.repository'; - -@Injectable() -export class DashboardVariableRepository extends SolidBaseRepository { - constructor( - readonly dataSource: DataSource, - readonly requestContextService: RequestContextService, - readonly securityRuleRepository: SecurityRuleRepository, - ) { - super(DashboardVariable, dataSource, requestContextService, securityRuleRepository); - } -} \ No newline at end of file diff --git a/src/repository/dashboard.repository.ts b/src/repository/dashboard.repository.ts deleted file mode 100644 index f731b3eb..00000000 --- a/src/repository/dashboard.repository.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { Injectable } from "@nestjs/common"; -import { Dashboard } from "src/entities/dashboard.entity"; -import { ModuleMetadata } from "src/entities/module-metadata.entity"; -import { RequestContextService } from "src/services/request-context.service"; -import { DataSource } from "typeorm"; -import { SecurityRuleRepository } from "./security-rule.repository"; -import { SolidBaseRepository } from "./solid-base.repository"; - -@Injectable() -export class DashboardRepository extends SolidBaseRepository { - constructor( - readonly dataSource: DataSource, - readonly requestContextService: RequestContextService, - readonly securityRuleRepository: SecurityRuleRepository, - ) { - super(Dashboard, dataSource, requestContextService, securityRuleRepository); - } - - - async upsertWithDto(createDto: any) { - const moduleMetadataRepository = this.dataSource.getRepository(ModuleMetadata); - const module = await moduleMetadataRepository.findOneBy({ name: createDto.moduleUserKey }); - - if (!module) throw new Error(`Module with key ${createDto.moduleUserKey} not found`); - - const existingDashboard = await this.findOne({ - where: { name: createDto.name }, - relations: ['dashboardVariables', 'questions', 'questions.questionSqlDatasetConfigs'], - }); - - if (existingDashboard) { - // Update basic fields - existingDashboard.layoutJson = JSON.stringify(createDto.layoutJson ?? {}); - existingDashboard.module = module; - - // Upsert dashboard variables - existingDashboard.dashboardVariables = createDto.dashboardVariables?.map(variable => { - const existingVar = existingDashboard.dashboardVariables.find(v => v.variableName === variable.variableName); - if (existingVar) { - return Object.assign(existingVar, { - ...variable, - selectionStaticValues: JSON.stringify(variable.selectionStaticValues ?? []), - defaultValue: JSON.stringify(variable.defaultValue ?? []), - }); - } - return { - ...variable, - selectionStaticValues: JSON.stringify(variable.selectionStaticValues ?? []), - defaultValue: JSON.stringify(variable.defaultValue ?? []), - }; - }) ?? []; - - // Upsert questions and their configs - existingDashboard.questions = createDto.questions?.map(question => { - const existingQuestion = existingDashboard.questions.find(q => q.name === question.name); - const questionData: any = { - ...question, - questionSqlDatasetConfigs: question.questionSqlDatasetConfigs?.map(cfg => { - const existingCfg = existingQuestion?.questionSqlDatasetConfigs.find(c => c.datasetName === cfg.datasetName); - if (existingCfg) { - return Object.assign(existingCfg, { - ...cfg, - options: JSON.stringify(cfg.options ?? {}), - }); - } - return { - ...cfg, - options: JSON.stringify(cfg.options ?? {}), - }; - }) ?? [], - }; - - return existingQuestion ? Object.assign(existingQuestion, questionData) : questionData; - }) ?? []; - - return this.save(existingDashboard); - } - - // Else: new dashboard - const dashboardData = { - ...createDto, - module, - layoutJson: JSON.stringify(createDto.layoutJson ?? {}), - dashboardVariables: createDto.dashboardVariables?.map(variable => ({ - ...variable, - selectionStaticValues: JSON.stringify(variable.selectionStaticValues ?? []), - defaultValue: JSON.stringify(variable.defaultValue ?? []), - })), - questions: createDto.questions?.map(question => ({ - ...question, - questionSqlDatasetConfigs: question.questionSqlDatasetConfigs?.map(cfg => ({ - ...cfg, - options: JSON.stringify(cfg.options ?? {}), - })), - })), - }; - - const newDashboard = this.create(dashboardData); - return this.save(newDashboard); - } -} \ No newline at end of file diff --git a/src/seeders/module-metadata-seeder.service.ts b/src/seeders/module-metadata-seeder.service.ts index d5700452..be5c73fa 100755 --- a/src/seeders/module-metadata-seeder.service.ts +++ b/src/seeders/module-metadata-seeder.service.ts @@ -3,12 +3,10 @@ import * as fs from 'fs'; import * as path from 'path'; import { v4 as uuidv4 } from 'uuid'; -import { CreateDashboardDto } from 'src/dtos/create-dashboard.dto'; import { CreateEmailTemplateDto } from 'src/dtos/create-email-template.dto'; import { CreateListOfValuesDto } from 'src/dtos/create-list-of-values.dto'; import { CreateSecurityRuleDto } from 'src/dtos/create-security-rule.dto'; import { CreateSmsTemplateDto } from 'src/dtos/create-sms-template.dto'; -import { DashboardRepository } from 'src/repository/dashboard.repository'; import { SecurityRuleRepository } from 'src/repository/security-rule.repository'; import { AuthenticationService } from 'src/services/authentication.service'; import { EmailTemplateService } from 'src/services/email-template.service'; @@ -52,7 +50,6 @@ import { SavedFilters } from 'src/entities/saved-filters.entity'; import { ScheduledJob } from 'src/entities/scheduled-job.entity'; import { SecurityRule } from 'src/entities/security-rule.entity'; import { ListOfValues } from 'src/entities/list-of-values.entity'; -import { Dashboard } from 'src/entities/dashboard.entity'; import { FieldMetadata } from 'src/entities/field-metadata.entity'; import { ModelMetadata } from 'src/entities/model-metadata.entity'; import { PermissionMetadata } from 'src/entities/permission-metadata.entity'; @@ -88,7 +85,6 @@ export class ModuleMetadataSeederService { private readonly settingsRepo: SettingRepository, readonly securityRuleRepo: SecurityRuleRepository, readonly systemFieldsSeederService: SystemFieldsSeederService, - readonly dashboardRepo: DashboardRepository, readonly scheduledJobRepository: ScheduledJobRepository, readonly savedFiltersRepo: SavedFiltersRepository, readonly dataSource: DataSource, @@ -207,11 +203,6 @@ export class ModuleMetadataSeederService { const lovCounts = await this.seedListOfValues(moduleMetadata, overallMetadata); console.log(`${this.formatSeedResult(moduleMetadata.name, 'List Of Values', lovCounts)}`); - currentStep = 'seedDashboards'; - this.logger.log(`Seeding Dashboards`); - const dashboardCounts = await this.seedDashboards(moduleMetadata, overallMetadata); - console.log(`${this.formatSeedResult(moduleMetadata.name, 'Dashboards', dashboardCounts)}`); - currentStep = 'seedScheduledJobs'; this.logger.log(`Seeding Scheduled Jobs`); const scheduledJobCounts = await this.seedScheduledJobs(moduleMetadata, overallMetadata); @@ -271,15 +262,6 @@ export class ModuleMetadataSeederService { return { pruned, upserted: savedFilters?.length ?? 0 }; } - private async seedDashboards(moduleMetadata: CreateModuleMetadataDto, overallMetadata: any): Promise<{ pruned: number; upserted: number }> { - this.logger.debug(`[Start] Processing dashboards for ${moduleMetadata.name}`); - const dashboards: CreateDashboardDto[] = overallMetadata.dashboards; - const pruned = this.enablePruning ? await this.pruneDashboards(dashboards, moduleMetadata.name) : 0; - await this.handleSeedDashboards(dashboards); - this.logger.debug(`[End] Processing dashboards for ${moduleMetadata.name}`); - return { pruned, upserted: dashboards?.length ?? 0 }; - } - private async seedListOfValues(moduleMetadata: CreateModuleMetadataDto, overallMetadata: any): Promise<{ pruned: number; upserted: number }> { this.logger.debug(`[Start] Processing List Of Values for ${moduleMetadata.name}`); const listOfValues: CreateListOfValuesDto[] = overallMetadata.listOfValues; @@ -923,16 +905,6 @@ export class ModuleMetadataSeederService { } } - private async handleSeedDashboards(dashboardDtos: CreateDashboardDto[]) { - if (!dashboardDtos || dashboardDtos.length === 0) { - this.logger.debug(`No dashboards found to seed`); - return; - } - for (const dto of dashboardDtos) { - await this.dashboardRepo.upsertWithDto(dto); - } - } - private async handleSeedScheduledJobs(createScheduledJobDto: CreateScheduledJobDto[]) { if (!createScheduledJobDto || createScheduledJobDto.length === 0) { this.logger.debug(`No scheduled jobs found to seed`); @@ -1189,45 +1161,6 @@ export class ModuleMetadataSeederService { return 0; } - private async pruneDashboards(dashboardsDto: CreateDashboardDto[] | undefined, moduleName?: string): Promise { - if (!moduleName) { - this.logger.warn(`Skipping dashboards prune: missing module name in metadata.`); - return 0; - } - const dashboards = dashboardsDto ?? []; - - const module = await this.moduleMetadataService.findOneByUserKey(moduleName); - if (!module) { - this.logger.warn(`Skipping dashboards prune: module not found for ${moduleName}.`); - return 0; - } - - const dashboardNames = [...new Set(dashboards.map(dto => dto.name).filter(Boolean))]; - const repo = this.dataSource.getRepository(Dashboard); - const idsToDeleteQuery = repo - .createQueryBuilder('db') - .select('db.id', 'id') - .innerJoin('db.module', 'module') - .where('module.id = :moduleId', { moduleId: module.id }); - - if (dashboardNames.length > 0) { - idsToDeleteQuery.andWhere('db.name NOT IN (:...dashboardNames)', { dashboardNames }); - } - - const rows = await idsToDeleteQuery.getRawMany(); - const ids = rows.map((row) => row.id); - if (ids.length > 0) { - const result = await repo - .createQueryBuilder() - .delete() - .from(Dashboard) - .whereInIds(ids) - .execute(); - return result.affected ?? 0; - } - return 0; - } - private async pruneMenus(menusDto: any[] | undefined, moduleName?: string): Promise { if (!moduleName) { this.logger.warn(`Skipping menus prune: missing module name in metadata.`); diff --git a/src/seeders/seed-data/solid-core-metadata.json b/src/seeders/seed-data/solid-core-metadata.json index 855c9e2f..d7ae4a77 100755 --- a/src/seeders/seed-data/solid-core-metadata.json +++ b/src/seeders/seed-data/solid-core-metadata.json @@ -4450,242 +4450,211 @@ ] }, { - "singularName": "dashboard", - "pluralName": "dashboards", - "displayName": "Dashboard", - "description": "This is used to maintain dashboards", + "singularName": "aiInteraction", + "pluralName": "aiInteractions", + "displayName": "AI Interaction", + "description": "Stores user and assistant messages in an AI chat session.", + "tableName": "ss_ai_interactions", "dataSource": "default", "dataSourceType": "postgres", - "tableName": "ss_dashboard", - "userKeyFieldUserKey": "name", + "userKeyFieldUserKey": "externalId", + "isSystem": false, "fields": [ { - "name": "name", - "displayName": "Name", - "type": "shortText", + "name": "user", + "displayName": "User", + "type": "relation", + "required": false, + "unique": false, + "index": true, + "private": false, + "encrypt": false, + "relationType": "many-to-one", + "relationCoModelSingularName": "user", + "relationCreateInverse": true, + "relationCascade": "cascade", + "relationModelModuleName": "solid-core", + "isSystem": true + }, + { + "name": "externalId", + "displayName": "External ID", + "description": "Used to track using a reference number of each ai interaction.", + "type": "computed", "ormType": "varchar", - "length": 256, + "isSystem": false, + "computedFieldValueType": "string", + "computedFieldTriggerConfig": [ + { + "modelName": "aiInteraction", + "moduleName": "solid-core", + "operations": [ + "before-insert" + ] + } + ], + "computedFieldValueProvider": "AlphaNumExternalIdComputationProvider", + "computedFieldValueProviderCtxt": "{\n \"prefix\": \"AI\",\n \"length\": \"10\"\n}", "required": true, "unique": true, "index": true, "private": false, "encrypt": false, - "isSystem": true, + "encryptionType": null, + "decryptWhen": null, + "columnName": null, "isUserKey": true }, { - "name": "displayName", - "displayName": "Display Name", + "name": "threadId", + "displayName": "Thread ID", "type": "shortText", "ormType": "varchar", - "length": 256, - "required": false, + "length": 128, + "required": true, "unique": false, - "index": false, + "index": true, "private": false, - "encrypt": false, - "isSystem": true, - "isUserKey": false + "encrypt": false }, { - "name": "description", - "displayName": "Description", - "type": "longText", - "ormType": "text", + "name": "parentInteraction", + "displayName": "Parent Interaction", + "type": "relation", "required": false, "unique": false, - "index": false, + "index": true, "private": false, "encrypt": false, - "isSystem": true, - "description": "This is a description of the dashboard configuration, providing context and details about the dashboard." + "relationType": "many-to-one", + "relationCoModelSingularName": "aiInteraction", + "relationCreateInverse": false, + "relationCascade": "set null", + "relationModelModuleName": "solid-core", + "isSystem": true }, { - "name": "layoutJson", - "displayName": "Layout Json", - "type": "json", - "ormType": "simple-json", + "name": "role", + "displayName": "Role", + "type": "shortText", + "ormType": "varchar", + "length": 32, "required": true, "unique": false, "index": false, "private": false, - "encrypt": false, - "isSystem": true + "encrypt": false }, { - "name": "dashboardVariables", - "displayName": "Dashboard Variables", - "type": "relation", - "ormType": "", - "required": false, + "name": "message", + "displayName": "Message", + "type": "longText", + "ormType": "text", + "required": true, "unique": false, "index": false, - "relationType": "one-to-many", - "relationCoModelFieldName": "dashboard", - "relationCreateInverse": true, - "relationCoModelSingularName": "dashboardVariable", - "relationModelModuleName": "solid-core", - "isSystem": true + "private": false, + "encrypt": false }, { - "name": "questions", - "displayName": "Dashboard Questions", - "type": "relation", + "name": "originalMessage", + "displayName": "Original Message", + "type": "longText", + "ormType": "text", "required": false, "unique": false, "index": false, - "relationType": "one-to-many", - "relationCoModelFieldName": "dashboard", - "relationCreateInverse": true, - "relationCoModelSingularName": "dashboardQuestion", - "relationModelModuleName": "solid-core", - "isSystem": true - }, - { - "name": "module", - "displayName": "Module", - "type": "relation", - "ormType": "integer", - "required": true, - "unique": false, - "index": false, "private": false, - "encrypt": false, - "relationType": "many-to-one", - "relationCreateInverse": false, - "relationCoModelSingularName": "moduleMetadata", - "relationModelModuleName": "solid-core", - "relationCascade": "cascade", - "isSystem": true - } - ] - }, - { - "singularName": "dashboardVariable", - "pluralName": "dashboardVariables", - "displayName": "Dashboard Variable", - "description": "This is used to maintain dashboard variables", - "dataSource": "default", - "dataSourceType": "postgres", - "tableName": "ss_dashboard_variable", - "userKeyFieldUserKey": "externalId", - "fields": [ + "encrypt": false + }, { - "name": "variableName", - "displayName": "Variable Name", + "name": "contentType", + "displayName": "Content Type", "type": "shortText", "ormType": "varchar", - "length": 256, - "required": true, + "length": 64, + "required": false, "unique": false, - "index": true, + "index": false, "private": false, - "encrypt": false, - "isSystem": true, - "isUserKey": false + "encrypt": false }, { - "name": "variableType", - "displayName": "Variable Type", + "name": "status", + "displayName": "Status", "type": "selectionStatic", "ormType": "varchar", - "length": 256, - "required": true, + "length": 64, + "required": false, + "unique": false, "index": true, - "isSystem": true, - "selectionValueType": "string", + "private": false, + "encrypt": false, "selectionStaticValues": [ - "date:Date", - "selectionStatic:Selection Static", - "selectionDynamic:Selection Dynamic" + "pending:Pending", + "failed:Failed", + "succeeded:Succeeded" ] }, { - "name": "selectionStaticValues", - "displayName": "Selection Static Values", - "type": "json", - "ormType": "simple-json", + "name": "errorMessage", + "displayName": "Error Message", + "type": "longText", + "ormType": "text", "required": false, "unique": false, "index": false, - "private": false, - "encrypt": false, - "isSystem": true + "private": true, + "encrypt": false }, { - "name": "selectionDynamicSourceType", - "displayName": "Selection Dynamic Source Type", - "type": "selectionStatic", - "ormType": "", - "length": 256, + "name": "modelUsed", + "displayName": "Model Used", + "type": "shortText", + "ormType": "varchar", + "length": 128, "required": false, "unique": false, "index": false, "private": false, - "encrypt": false, - "isSystem": true, - "selectionValueType": "string", - "selectionStaticValues": [ - "sql:SQL", - "provider:Provider" - ] + "encrypt": false }, { - "name": "selectionDynamicSQL", - "displayName": "Selection Dynamic SQL", - "type": "longText", - "ormType": "text", + "name": "responseTimeMs", + "displayName": "Response Time (ms)", + "type": "int", + "ormType": "integer", "required": false, "unique": false, "index": false, "private": false, - "encrypt": false, - "isSystem": true, - "description": "SQL query to fetch the data for this variable when it is rendered at runtime. This is only applicable when selectionDynamicSourceType is set to SQL." + "encrypt": false }, { - "name": "selectionDynamicProviderName", - "displayName": "Selection Dynamic Provider Name", - "type": "selectionDynamic", - "ormType": "varchar", - "length": 256, + "name": "metadata", + "displayName": "Metadata", + "type": "json", + "ormType": "simple-json", "required": false, "unique": false, "index": false, "private": false, - "encrypt": false, - "isSystem": true, - "selectionValueType": "string", - "selectionDynamicProvider": "ListOfDashboardVariableProvidersSelectionProvider", - "selectionDynamicProviderCtxt": "{}", - "description": "This is only applicable when selectionDynamicSourceType is set to provider. It allows the user to select any pre-existing Dashboard SelectionDynamicProvider implementation used to fetch a dynamic dropdown of values to choose from when this variable is presented to the user." + "encrypt": false }, { - "name": "isMultiSelect", - "displayName": "Is Multi Select", + "name": "isApplied", + "displayName": "Is Applied", "type": "boolean", - "defaultValue": "false", "required": false, "unique": false, "index": false, "private": false, - "encrypt": false, - "isSystem": true, - "description": "This is relevant only for variables of type \"selectionStatic\" or \"selectionDynamic\". When set to true, it allows the user to select multiple values from the dropdown." + "encrypt": false }, { - "name": "dashboard", - "displayName": "Dashboard", - "description": "Related Dashboard Model", - "type": "relation", - "ormType": "integer", - "isSystem": true, - "relationType": "many-to-one", - "relationCoModelFieldName": "dashboardVariables", - "relationCreateInverse": true, - "relationCoModelSingularName": "dashboard", - "relationModelModuleName": "solid-core", - "relationCascade": "cascade", + "name": "isEdited", + "displayName": "Is Edited", + "type": "boolean", "required": false, "unique": false, "index": false, @@ -4693,329 +4662,241 @@ "encrypt": false }, { - "name": "defaultValue", - "displayName": "Default Value", - "type": "longText", - "ormType": "text", + "name": "isAutoApply", + "displayName": "Is Auto Apply", + "type": "boolean", "required": false, "unique": false, "index": false, "private": false, - "encrypt": false, - "isSystem": true, - "description": "This is the default value for this variable when it is rendered at runtime. It can be a static value for this variable when it is rendered at runtime." + "encrypt": false }, { - "name": "defaultOperator", - "displayName": "Default Operator", - "type": "selectionStatic", - "ormType": "varchar", - "length": 256, + "name": "inputTokens", + "displayName": "Input Tokens", + "type": "int", + "ormType": "integer", "required": false, "unique": false, "index": false, "private": false, - "encrypt": false, - "isSystem": true, - "selectionValueType": "string", - "selectionStaticValues": [ - "$equals:Equals", - "$notEquals:Not Equals", - "$contains:Contains", - "$notContains:Not Contains", - "$startsWith:Starts With", - "$endsWith:Ends With", - "$in:In", - "$notIn:Not In", - "$between:Between", - "$lt:Less Than", - "$lte:Less Than or Equal To", - "$gt:Greater Than", - "$gte:Greater Than or Equal To" - ], - "description": "This is the default operator for this variable when it is rendered at runtime. It can be a static value for this variable when it is rendered at runtime." + "encrypt": false }, { - "name": "externalId", - "displayName": "Dashboard Variable User KExternal IDey", - "description": "Concatenation of variable name and dashboard name", - "type": "computed", - "ormType": "varchar", - "isSystem": false, - "computedFieldValueType": "string", - "computedFieldTriggerConfig": [ - { - "modelName": "dashboardVariable", - "moduleName": "solid-core", - "operations": [ - "before-insert" - ] - } - ], - "computedFieldValueProvider": "ConcatEntityComputedFieldProvider", - "computedFieldValueProviderCtxt": "{\n \"fields\": [\n \"variableName\",\n \"dashboard.name\"\n ],\n \"separator\": \"-\",\n \"slugify\": true\n}", + "name": "outputTokens", + "displayName": "Output Tokens", + "type": "int", + "ormType": "integer", "required": false, - "unique": true, - "index": true, + "unique": false, + "index": false, "private": false, - "encrypt": false, - "encryptionType": null, - "decryptWhen": null, - "columnName": null, - "isUserKey": true + "encrypt": false + }, + { + "name": "totalTokens", + "displayName": "Total Tokens", + "type": "int", + "ormType": "integer", + "required": false, + "unique": false, + "index": false, + "private": false, + "encrypt": false } ] }, { - "singularName": "dashboardQuestion", - "pluralName": "dashboardQuestions", - "displayName": "Dashboard Question", - "description": "This is used to maintain dashboard questions for dashboards", + "singularName": "modelSequence", + "tableName": "ss_model_sequence", + "pluralName": "modelSequences", + "displayName": "Model Sequence", + "description": "This is used to maintain model field sequence", "dataSource": "default", "dataSourceType": "postgres", - "tableName": "ss_dashboard_question", - "userKeyFieldUserKey": "externalId", + "isSystem": true, + "userKeyFieldUserKey": "sequenceName", "fields": [ { - "name": "name", - "displayName": "Name", - "type": "shortText", - "ormType": "varchar", - "length": 256, + "name": "module", + "displayName": "Module", + "type": "relation", + "ormType": "integer", "required": true, "unique": false, - "index": false, + "index": true, "private": false, "encrypt": false, - "isSystem": true, - "isUserKey": true - }, - { - "name": "sourceType", - "displayName": "Source Type", - "type": "selectionStatic", - "ormType": "", - "length": 256, - "required": true, - "index": true, - "isSystem": true, - "selectionValueType": "string", - "selectionStaticValues": [ - "sql:SQL", - "provider:Provider" - ] - }, - { - "name": "visualisedAs", - "displayName": "Visualised As", - "type": "selectionStatic", - "ormType": "", - "length": 256, - "required": true, - "index": true, - "isSystem": true, - "selectionValueType": "string", - "selectionStaticValues": [ - "bar:ChartJS - Bar", - "line:ChartJS - Line", - "pie:ChartJS - Pie", - "donut:ChartJS - Donut", - "prime-meter-group:Prime - Meter Group", - "prime-datatable:Prime - Datatable" - ] + "relationType": "many-to-one", + "relationCreateInverse": false, + "relationCoModelSingularName": "moduleMetadata", + "relationModelModuleName": "solid-core", + "relationCascade": "cascade", + "isSystem": true }, { - "name": "sequenceNumber", - "displayName": "Sequence Number", - "type": "int", + "name": "model", + "displayName": "Model", + "type": "relation", "ormType": "integer", - "required": false, + "required": true, "unique": false, - "index": false, + "index": true, "private": false, "encrypt": false, + "relationType": "many-to-one", + "relationCreateInverse": false, + "relationCoModelSingularName": "modelMetadata", + "relationModelModuleName": "solid-core", + "relationCascade": "cascade", "isSystem": true }, { - "name": "labelSql", - "displayName": "Label SQL Query", - "type": "longText", - "ormType": "text", - "required": false, + "name": "field", + "displayName": "Field", + "type": "relation", + "ormType": "integer", + "required": true, "unique": false, - "index": false, + "index": true, "private": false, "encrypt": false, - "isSystem": true, - "description": "This is the SQL query to fetch the label values for the question" + "relationType": "many-to-one", + "relationCreateInverse": false, + "relationCoModelSingularName": "fieldMetadata", + "relationModelModuleName": "solid-core", + "relationCascade": "cascade", + "isSystem": true }, { - "name": "kpiSql", - "displayName": "KPI SQL Query", - "type": "longText", - "ormType": "text", - "required": false, - "unique": false, - "index": false, + "name": "sequenceName", + "displayName": "Sequence Name", + "type": "shortText", + "ormType": "varchar", + "length": 512, + "required": true, + "unique": true, + "index": true, "private": false, "encrypt": false, - "isSystem": true, - "description": "This is the SQL query to fetch the KPI value for the question" + "isSystem": false, + "isUserKey": true }, { - "name": "providerName", - "displayName": "Provider Name", - "type": "selectionDynamic", - "ormType": "varchar", - "length": 256, + "name": "currentValue", + "displayName": "Current Value", + "type": "int", + "ormType": "integer", "required": false, "unique": false, "index": false, "private": false, "encrypt": false, - "isSystem": true, - "selectionValueType": "string", - "selectionDynamicProvider": "ListOfDashboardQuestionProvidersSelectionProvider", - "selectionDynamicProviderCtxt": "{}", - "description": "This is only applicable when sourceType is set to provider. It allows the user to select any pre-existing Dashboard Question Data provider implementation used to fetch a dynamic dropdown of values to choose from when this question is presented to the user." + "isSystem": false, + "defaultValue": "1" }, { - "name": "chartOptions", - "displayName": "Bar Chart Label Options", - "type": "json", - "ormType": "simple-json", + "name": "prefix", + "displayName": "Prefix", + "type": "shortText", + "ormType": "varchar", + "length": 512, "required": false, "unique": false, "index": false, "private": false, "encrypt": false, - "isSystem": true, - "description": "This is a JSON object representing each labels display and color options for the bar chart" + "isSystem": false }, { - "name": "dashboard", - "displayName": "Dashboard", - "description": "Related Dashboard Model", - "type": "relation", + "name": "padding", + "displayName": "Padding", + "type": "int", "ormType": "integer", - "isSystem": true, - "relationType": "many-to-one", - "relationCoModelFieldName": "questions", - "relationCreateInverse": true, - "relationCoModelSingularName": "dashboard", - "relationModelModuleName": "solid-core", - "relationCascade": "cascade", "required": false, "unique": false, "index": false, "private": false, - "encrypt": false + "encrypt": false, + "isSystem": false, + "defaultValue": "5" }, { - "name": "questionSqlDatasetConfigs", - "displayName": "Related Dashboard Question SQL Dataset Config", - "description": "Related Question SQL Dataset Config Model", - "type": "relation", - "ormType": "integer", - "isSystem": true, - "relationType": "one-to-many", - "relationCoModelFieldName": "question", - "relationCreateInverse": true, - "relationCoModelSingularName": "dashboardQuestionSqlDatasetConfig", - "relationModelModuleName": "solid-core", - "relationCascade": "cascade", + "name": "separator", + "displayName": "Separator", + "type": "shortText", + "ormType": "varchar", + "length": 512, "required": false, "unique": false, "index": false, "private": false, - "encrypt": false - }, - { - "name": "externalId", - "displayName": "External ID", - "description": "Concatenation of question name and dashboard name", - "type": "computed", - "ormType": "varchar", - "isSystem": false, - "computedFieldValueType": "string", - "computedFieldTriggerConfig": [ - { - "modelName": "dashboardQuestion", - "moduleName": "solid-core", - "operations": [ - "before-insert" - ] - } - ], - "computedFieldValueProvider": "ConcatEntityComputedFieldProvider", - "computedFieldValueProviderCtxt": "{\n \"fields\": [\n \"name\",\n \"dashboard.name\"\n ],\n \"separator\": \"-\",\n \"slugify\": true\n}", - "required": false, - "unique": true, - "index": true, - "private": false, "encrypt": false, - "encryptionType": null, - "decryptWhen": null, - "columnName": null, - "isUserKey": true + "isSystem": false } ] }, { - "singularName": "dashboardQuestionSqlDatasetConfig", - "pluralName": "dashboardQuestionSqlDatasetConfigs", - "displayName": "Dashboard Question SQL Dataset Config", - "description": "This is used to maintain Dashboard Question SQL dataset configurations", + "singularName": "agentSession", + "pluralName": "agentSessions", + "displayName": "Agent Session", + "description": "AI agent sessions", "dataSource": "default", "dataSourceType": "postgres", - "tableName": "ss_dashboard_question_sql_dataset_config", - "userKeyFieldUserKey": "externalId", + "tableName": "ss_agent_sessions", + "isChild": false, + "isLegacyTable": true, + "isLegacyTableWithId": true, + "enableAuditTracking": false, + "enableSoftDelete": false, + "draftPublishWorkflow": false, + "internationalisation": false, + "isSystem": true, + "userKeyFieldUserKey": "sessionId", "fields": [ { - "name": "datasetName", - "displayName": "Dataset Name", + "name": "sessionId", + "displayName": "Session ID", "type": "shortText", - "ormType": "varchar", - "length": 256, + "columnName": "session_id", + "length": 36, "required": true, - "unique": false, - "index": false, + "unique": true, + "index": true, "private": false, "encrypt": false, "isSystem": true }, { - "name": "datasetDisplayName", - "displayName": "Dataset Display Name", - "type": "shortText", - "ormType": "varchar", - "length": 256, - "required": true, + "name": "userId", + "displayName": "User ID", + "type": "int", + "columnName": "user_id", + "required": false, "unique": false, - "index": false, + "index": true, "private": false, "encrypt": false, - "isSystem": true, - "description": "This is the display name for the dataset configuration, which can be used in UI components to represent the dataset in a user-friendly manner." + "isSystem": true }, { - "name": "description", - "displayName": "Description", + "name": "projectRoot", + "displayName": "Project Root", "type": "longText", - "ormType": "text", + "columnName": "project_root", "required": false, "unique": false, "index": false, "private": false, "encrypt": false, - "isSystem": true, - "description": "This is a description of the dataset configuration, providing context and details about its purpose." + "isSystem": true }, { - "name": "sql", - "displayName": "SQL Query", - "type": "longText", - "ormType": "text", + "name": "modelName", + "displayName": "Model Name", + "type": "shortText", + "columnName": "model_name", + "length": 255, "required": true, "unique": false, "index": false, @@ -5024,587 +4905,408 @@ "isSystem": true }, { - "name": "labelColumnName", - "displayName": "Label Column Name", - "type": "shortText", - "ormType": "varchar", - "length": 256, - "required": true, + "name": "status", + "displayName": "Status", + "type": "selectionStatic", + "columnName": "status", + "length": 32, + "required": true, "unique": false, - "index": false, + "index": true, "private": false, "encrypt": false, - "isSystem": true + "isSystem": true, + "selectionStaticValues": [ + "active:Active", + "completed:Completed", + "failed:Failed", + "cancelled:Cancelled" + ], + "selectionValueType": "string" }, { - "name": "valueColumnName", - "displayName": "Value Column Name", - "type": "shortText", - "ormType": "varchar", - "length": 256, + "name": "totalCost", + "displayName": "Total Cost", + "type": "decimal", + "columnName": "total_cost", "required": true, "unique": false, "index": false, "private": false, "encrypt": false, - "isSystem": true + "isSystem": true, + "defaultValue": "0" }, { - "name": "options", - "displayName": "Dataset options", - "type": "json", - "ormType": "simple-json", - "required": false, + "name": "totalSteps", + "displayName": "Total Steps", + "type": "int", + "columnName": "total_steps", + "required": true, "unique": false, "index": false, "private": false, "encrypt": false, "isSystem": true, - "description": "This allows you to set the dataset options e.g border-color, background-color, etc. This is a JSON object that can be used to customize the dataset appearance or behavior in the UI." + "defaultValue": "0" }, { - "name": "question", - "displayName": "Question", - "description": "Related Question Model", - "type": "relation", - "ormType": "integer", - "isSystem": true, - "relationType": "many-to-one", - "relationCoModelFieldName": "questionSqlDatasetConfigs", - "relationCreateInverse": true, - "relationCoModelSingularName": "dashboardQuestion", - "relationModelModuleName": "solid-core", - "relationCascade": "cascade", + "name": "totalInputTokens", + "displayName": "Total Input Tokens", + "type": "int", + "columnName": "total_input_tokens", "required": true, "unique": false, "index": false, "private": false, - "encrypt": false - }, - { - "name": "externalId", - "displayName": "External ID", - "description": "Concatenation of dataset name, question name and dashboard name", - "type": "computed", - "ormType": "varchar", - "isSystem": false, - "computedFieldValueType": "string", - "computedFieldTriggerConfig": [ - { - "modelName": "dashboardQuestionSqlDatasetConfig", - "moduleName": "solid-core", - "operations": [ - "before-insert" - ] - } - ], - "computedFieldValueProvider": "ConcatEntityComputedFieldProvider", - "computedFieldValueProviderCtxt": "{\n \"fields\": [\n \"datasetName\",\n \"question.name\",\n \"question.dashboard.name\"\n ],\n \"separator\": \"-\",\n \"slugify\": true\n}", - "required": false, - "unique": true, - "index": true, - "private": false, - "encrypt": false, - "encryptionType": null, - "decryptWhen": null, - "columnName": null, - "isUserKey": true - } - ] - }, - { - "singularName": "dashboardLayout", - "pluralName": "dashboardLayouts", - "displayName": "Dashboard Layout", - "description": "This is used to maintain dashboard layouts", - "dataSource": "default", - "dataSourceType": "postgres", - "tableName": "ss_dashboard_layout", - "userKeyFieldUserKey": "externalId", - "fields": [ - { - "name": "layout", - "displayName": "Layout", - "type": "longText", - "ormType": "text", - "required": false, - "unique": false, - "index": false, - "private": false, "encrypt": false, "isSystem": true, - "description": "This stores the dashboard layout" + "defaultValue": "0" }, { - "name": "dashboard", - "displayName": "Dashboard", - "description": "Related Dashboard Model", - "type": "relation", - "ormType": "integer", - "isSystem": true, - "relationType": "many-to-one", - "relationCoModelFieldName": "dashboardLayouts", - "relationCreateInverse": true, - "relationCoModelSingularName": "dashboard", - "relationModelModuleName": "solid-core", - "relationCascade": "cascade", - "required": false, + "name": "totalOutputTokens", + "displayName": "Total Output Tokens", + "type": "int", + "columnName": "total_output_tokens", + "required": true, "unique": false, "index": false, "private": false, - "encrypt": false + "encrypt": false, + "isSystem": true, + "defaultValue": "0" }, { - "name": "user", - "displayName": "User", - "description": null, - "type": "relation", - "ormType": "integer", - "isSystem": false, - "relationType": "many-to-one", - "relationCoModelFieldName": null, - "relationCreateInverse": false, - "relationCoModelSingularName": "user", - "relationCoModelColumnName": "", - "relationModelModuleName": "solid-core", - "relationCascade": "cascade", + "name": "summary", + "displayName": "Summary", + "type": "longText", + "columnName": "summary", "required": false, "unique": false, "index": false, "private": false, "encrypt": false, - "encryptionType": null, - "decryptWhen": null, - "columnName": null, - "relationJoinTableName": "", - "isRelationManyToManyOwner": null + "isSystem": true } ] }, { - "singularName": "aiInteraction", - "pluralName": "aiInteractions", - "displayName": "AI Interaction", - "description": "Stores user and assistant messages in an AI chat session.", - "tableName": "ss_ai_interactions", + "singularName": "agentEvent", + "pluralName": "agentEvents", + "displayName": "Agent Event", + "description": "AI agent events per session", "dataSource": "default", "dataSourceType": "postgres", - "userKeyFieldUserKey": "externalId", - "isSystem": false, + "tableName": "ss_agent_events", + "isChild": true, + "isLegacyTable": true, + "isLegacyTableWithId": true, + "enableAuditTracking": false, + "enableSoftDelete": false, + "draftPublishWorkflow": false, + "internationalisation": false, + "isSystem": true, + "userKeyFieldUserKey": "id", "fields": [ { - "name": "user", - "displayName": "User", - "type": "relation", - "required": false, + "name": "sessionId", + "displayName": "Session ID", + "type": "shortText", + "columnName": "session_id", + "length": 36, + "required": true, "unique": false, "index": true, "private": false, "encrypt": false, - "relationType": "many-to-one", - "relationCoModelSingularName": "user", - "relationCreateInverse": true, - "relationCascade": "cascade", - "relationModelModuleName": "solid-core", "isSystem": true }, { - "name": "externalId", - "displayName": "External ID", - "description": "Used to track using a reference number of each ai interaction.", - "type": "computed", - "ormType": "varchar", - "isSystem": false, - "computedFieldValueType": "string", - "computedFieldTriggerConfig": [ - { - "modelName": "aiInteraction", - "moduleName": "solid-core", - "operations": [ - "before-insert" - ] - } - ], - "computedFieldValueProvider": "AlphaNumExternalIdComputationProvider", - "computedFieldValueProviderCtxt": "{\n \"prefix\": \"AI\",\n \"length\": \"10\"\n}", + "name": "turnNumber", + "displayName": "Turn Number", + "type": "int", + "columnName": "turn_number", "required": true, - "unique": true, - "index": true, + "unique": false, + "index": false, "private": false, "encrypt": false, - "encryptionType": null, - "decryptWhen": null, - "columnName": null, - "isUserKey": true + "isSystem": true }, { - "name": "threadId", - "displayName": "Thread ID", - "type": "shortText", - "ormType": "varchar", - "length": 128, - "required": true, + "name": "stepNumber", + "displayName": "Step Number", + "type": "int", + "columnName": "step_number", + "required": false, "unique": false, - "index": true, + "index": false, "private": false, - "encrypt": false + "encrypt": false, + "isSystem": true }, { - "name": "parentInteraction", - "displayName": "Parent Interaction", - "type": "relation", - "required": false, + "name": "eventType", + "displayName": "Event Type", + "type": "selectionStatic", + "columnName": "event_type", + "length": 64, + "required": true, "unique": false, "index": true, "private": false, "encrypt": false, - "relationType": "many-to-one", - "relationCoModelSingularName": "aiInteraction", - "relationCreateInverse": false, - "relationCascade": "set null", - "relationModelModuleName": "solid-core", - "isSystem": true + "isSystem": true, + "selectionStaticValues": [ + "user_message:User Message", + "assistant_message:Assistant Message", + "tool_use:Tool Use", + "tool_result:Tool Result", + "system_prompt:System Prompt", + "thinking:Thinking", + "error:Error", + "session_start:Session Start", + "session_end:Session End" + ], + "selectionValueType": "string" }, { - "name": "role", - "displayName": "Role", - "type": "shortText", - "ormType": "varchar", - "length": 32, - "required": true, + "name": "eventData", + "displayName": "Event Data", + "type": "json", + "columnName": "event_data", + "required": false, "unique": false, "index": false, "private": false, - "encrypt": false + "encrypt": false, + "isSystem": true }, { - "name": "message", - "displayName": "Message", - "type": "longText", - "ormType": "text", - "required": true, - "unique": false, - "index": false, - "private": false, - "encrypt": false - }, - { - "name": "originalMessage", - "displayName": "Original Message", + "name": "content", + "displayName": "Content", "type": "longText", - "ormType": "text", - "required": false, - "unique": false, - "index": false, - "private": false, - "encrypt": false - }, - { - "name": "contentType", - "displayName": "Content Type", - "type": "shortText", - "ormType": "varchar", - "length": 64, + "columnName": "content", "required": false, "unique": false, "index": false, "private": false, - "encrypt": false - }, - { - "name": "status", - "displayName": "Status", - "type": "selectionStatic", - "ormType": "varchar", - "length": 64, - "required": false, - "unique": false, - "index": true, - "private": false, "encrypt": false, - "selectionStaticValues": [ - "pending:Pending", - "failed:Failed", - "succeeded:Succeeded" - ] - }, - { - "name": "errorMessage", - "displayName": "Error Message", - "type": "longText", - "ormType": "text", - "required": false, - "unique": false, - "index": false, - "private": true, - "encrypt": false + "isSystem": true }, { - "name": "modelUsed", - "displayName": "Model Used", + "name": "toolName", + "displayName": "Tool Name", "type": "shortText", - "ormType": "varchar", + "columnName": "tool_name", "length": 128, "required": false, "unique": false, - "index": false, + "index": true, "private": false, - "encrypt": false + "encrypt": false, + "isSystem": true }, { - "name": "responseTimeMs", - "displayName": "Response Time (ms)", - "type": "int", - "ormType": "integer", + "name": "toolArguments", + "displayName": "Tool Arguments", + "type": "json", + "columnName": "tool_arguments", "required": false, "unique": false, "index": false, "private": false, - "encrypt": false + "encrypt": false, + "isSystem": true }, { - "name": "metadata", - "displayName": "Metadata", + "name": "toolOutput", + "displayName": "Tool Output", "type": "json", - "ormType": "simple-json", + "columnName": "tool_output", "required": false, "unique": false, "index": false, "private": false, - "encrypt": false + "encrypt": false, + "isSystem": true }, { - "name": "isApplied", - "displayName": "Is Applied", - "type": "boolean", + "name": "toolReturncode", + "displayName": "Tool Return Code", + "type": "int", + "columnName": "tool_returncode", "required": false, "unique": false, "index": false, "private": false, - "encrypt": false + "encrypt": false, + "isSystem": true }, { - "name": "isEdited", - "displayName": "Is Edited", - "type": "boolean", + "name": "durationMs", + "displayName": "Duration (ms)", + "type": "decimal", + "columnName": "duration_ms", "required": false, "unique": false, "index": false, "private": false, - "encrypt": false + "encrypt": false, + "isSystem": true }, { - "name": "isAutoApply", - "displayName": "Is Auto Apply", - "type": "boolean", + "name": "cost", + "displayName": "Cost", + "type": "decimal", + "columnName": "cost", "required": false, "unique": false, "index": false, "private": false, - "encrypt": false + "encrypt": false, + "isSystem": true }, { "name": "inputTokens", "displayName": "Input Tokens", "type": "int", - "ormType": "integer", + "columnName": "input_tokens", "required": false, "unique": false, "index": false, "private": false, - "encrypt": false + "encrypt": false, + "isSystem": true }, { "name": "outputTokens", "displayName": "Output Tokens", "type": "int", - "ormType": "integer", + "columnName": "output_tokens", "required": false, "unique": false, "index": false, "private": false, - "encrypt": false + "encrypt": false, + "isSystem": true }, { - "name": "totalTokens", - "displayName": "Total Tokens", - "type": "int", - "ormType": "integer", + "name": "modelUsed", + "displayName": "Model Used", + "type": "shortText", + "columnName": "model_used", + "length": 255, "required": false, "unique": false, "index": false, "private": false, - "encrypt": false + "encrypt": false, + "isSystem": true } ] }, { - "singularName": "modelSequence", - "tableName": "ss_model_sequence", - "pluralName": "modelSequences", - "displayName": "Model Sequence", - "description": "This is used to maintain model field sequence", + "singularName": "mcpAuditLog", + "pluralName": "mcpAuditLogs", + "displayName": "MCP Audit Log", + "description": "Audit trail of MCP requests handled by the server", "dataSource": "default", "dataSourceType": "postgres", + "tableName": "ss_mcp_audit_log", + "isChild": false, + "isLegacyTable": true, + "isLegacyTableWithId": true, + "enableAuditTracking": false, + "enableSoftDelete": false, + "draftPublishWorkflow": false, + "internationalisation": false, "isSystem": true, - "userKeyFieldUserKey": "sequenceName", + "userKeyFieldUserKey": "id", "fields": [ { - "name": "module", - "displayName": "Module", - "type": "relation", - "ormType": "integer", - "required": true, + "name": "userId", + "displayName": "User ID", + "type": "int", + "columnName": "user_id", + "required": false, "unique": false, "index": true, "private": false, "encrypt": false, - "relationType": "many-to-one", - "relationCreateInverse": false, - "relationCoModelSingularName": "moduleMetadata", - "relationModelModuleName": "solid-core", - "relationCascade": "cascade", "isSystem": true }, { - "name": "model", - "displayName": "Model", - "type": "relation", - "ormType": "integer", - "required": true, + "name": "apiKeyId", + "displayName": "API Key ID", + "type": "int", + "columnName": "api_key_id", + "required": false, "unique": false, - "index": true, + "index": false, "private": false, "encrypt": false, - "relationType": "many-to-one", - "relationCreateInverse": false, - "relationCoModelSingularName": "modelMetadata", - "relationModelModuleName": "solid-core", - "relationCascade": "cascade", "isSystem": true }, { - "name": "field", - "displayName": "Field", - "type": "relation", - "ormType": "integer", - "required": true, + "name": "username", + "displayName": "Username", + "type": "shortText", + "columnName": "username", + "length": 128, + "required": false, "unique": false, - "index": true, + "index": false, "private": false, "encrypt": false, - "relationType": "many-to-one", - "relationCreateInverse": false, - "relationCoModelSingularName": "fieldMetadata", - "relationModelModuleName": "solid-core", - "relationCascade": "cascade", "isSystem": true }, { - "name": "sequenceName", - "displayName": "Sequence Name", + "name": "transport", + "displayName": "Transport", "type": "shortText", - "ormType": "varchar", - "length": 512, + "columnName": "transport", + "length": 32, "required": true, - "unique": true, - "index": true, - "private": false, - "encrypt": false, - "isSystem": false, - "isUserKey": true - }, - { - "name": "currentValue", - "displayName": "Current Value", - "type": "int", - "ormType": "integer", - "required": false, "unique": false, "index": false, "private": false, "encrypt": false, - "isSystem": false, - "defaultValue": "1" + "isSystem": true }, { - "name": "prefix", - "displayName": "Prefix", + "name": "mcpSessionId", + "displayName": "MCP Session ID", "type": "shortText", - "ormType": "varchar", - "length": 512, + "columnName": "mcp_session_id", + "length": 64, "required": false, "unique": false, - "index": false, + "index": true, "private": false, "encrypt": false, - "isSystem": false + "isSystem": true }, { - "name": "padding", - "displayName": "Padding", - "type": "int", - "ormType": "integer", + "name": "clientAddr", + "displayName": "Client Address", + "type": "shortText", + "columnName": "client_addr", + "length": 64, "required": false, "unique": false, "index": false, "private": false, "encrypt": false, - "isSystem": false, - "defaultValue": "5" + "isSystem": true }, { - "name": "separator", - "displayName": "Separator", - "type": "shortText", - "ormType": "varchar", - "length": 512, - "required": false, - "unique": false, - "index": false, - "private": false, - "encrypt": false, - "isSystem": false - } - ] - }, - { - "singularName": "agentSession", - "pluralName": "agentSessions", - "displayName": "Agent Session", - "description": "AI agent sessions", - "dataSource": "default", - "dataSourceType": "postgres", - "tableName": "ss_agent_sessions", - "isChild": false, - "isLegacyTable": true, - "isLegacyTableWithId": true, - "enableAuditTracking": false, - "enableSoftDelete": false, - "draftPublishWorkflow": false, - "internationalisation": false, - "isSystem": true, - "userKeyFieldUserKey": "sessionId", - "fields": [ - { - "name": "sessionId", - "displayName": "Session ID", + "name": "method", + "displayName": "Method", "type": "shortText", - "columnName": "session_id", - "length": 36, + "columnName": "method", + "length": 64, "required": true, - "unique": true, - "index": true, - "private": false, - "encrypt": false, - "isSystem": true - }, - { - "name": "userId", - "displayName": "User ID", - "type": "int", - "columnName": "user_id", - "required": false, "unique": false, "index": true, "private": false, @@ -5612,10 +5314,11 @@ "isSystem": true }, { - "name": "projectRoot", - "displayName": "Project Root", - "type": "longText", - "columnName": "project_root", + "name": "requestId", + "displayName": "Request ID", + "type": "shortText", + "columnName": "request_id", + "length": 64, "required": false, "unique": false, "index": false, @@ -5624,135 +5327,36 @@ "isSystem": true }, { - "name": "modelName", - "displayName": "Model Name", + "name": "toolName", + "displayName": "Tool Name", "type": "shortText", - "columnName": "model_name", - "length": 255, - "required": true, - "unique": false, - "index": false, - "private": false, - "encrypt": false, - "isSystem": true - }, - { - "name": "status", - "displayName": "Status", - "type": "selectionStatic", - "columnName": "status", - "length": 32, - "required": true, + "columnName": "tool_name", + "length": 128, + "required": false, "unique": false, "index": true, "private": false, "encrypt": false, - "isSystem": true, - "selectionStaticValues": ["active:Active", "completed:Completed", "failed:Failed", "cancelled:Cancelled"], - "selectionValueType": "string" - }, - { - "name": "totalCost", - "displayName": "Total Cost", - "type": "decimal", - "columnName": "total_cost", - "required": true, - "unique": false, - "index": false, - "private": false, - "encrypt": false, - "isSystem": true, - "defaultValue": "0" - }, - { - "name": "totalSteps", - "displayName": "Total Steps", - "type": "int", - "columnName": "total_steps", - "required": true, - "unique": false, - "index": false, - "private": false, - "encrypt": false, - "isSystem": true, - "defaultValue": "0" - }, - { - "name": "totalInputTokens", - "displayName": "Total Input Tokens", - "type": "int", - "columnName": "total_input_tokens", - "required": true, - "unique": false, - "index": false, - "private": false, - "encrypt": false, - "isSystem": true, - "defaultValue": "0" - }, - { - "name": "totalOutputTokens", - "displayName": "Total Output Tokens", - "type": "int", - "columnName": "total_output_tokens", - "required": true, - "unique": false, - "index": false, - "private": false, - "encrypt": false, - "isSystem": true, - "defaultValue": "0" + "isSystem": true }, { - "name": "summary", - "displayName": "Summary", + "name": "requestParams", + "displayName": "Request Params", "type": "longText", - "columnName": "summary", + "columnName": "request_params", "required": false, "unique": false, "index": false, "private": false, "encrypt": false, "isSystem": true - } - ] - }, - { - "singularName": "agentEvent", - "pluralName": "agentEvents", - "displayName": "Agent Event", - "description": "AI agent events per session", - "dataSource": "default", - "dataSourceType": "postgres", - "tableName": "ss_agent_events", - "isChild": true, - "isLegacyTable": true, - "isLegacyTableWithId": true, - "enableAuditTracking": false, - "enableSoftDelete": false, - "draftPublishWorkflow": false, - "internationalisation": false, - "isSystem": true, - "userKeyFieldUserKey": "id", - "fields": [ - { - "name": "sessionId", - "displayName": "Session ID", - "type": "shortText", - "columnName": "session_id", - "length": 36, - "required": true, - "unique": false, - "index": true, - "private": false, - "encrypt": false, - "isSystem": true }, { - "name": "turnNumber", - "displayName": "Turn Number", - "type": "int", - "columnName": "turn_number", + "name": "status", + "displayName": "Status", + "type": "shortText", + "columnName": "status", + "length": 16, "required": true, "unique": false, "index": false, @@ -5761,10 +5365,10 @@ "isSystem": true }, { - "name": "stepNumber", - "displayName": "Step Number", - "type": "int", - "columnName": "step_number", + "name": "responseResult", + "displayName": "Response Result", + "type": "longText", + "columnName": "response_result", "required": false, "unique": false, "index": false, @@ -5773,35 +5377,10 @@ "isSystem": true }, { - "name": "eventType", - "displayName": "Event Type", - "type": "selectionStatic", - "columnName": "event_type", - "length": 64, - "required": true, - "unique": false, - "index": true, - "private": false, - "encrypt": false, - "isSystem": true, - "selectionStaticValues": [ - "user_message:User Message", - "assistant_message:Assistant Message", - "tool_use:Tool Use", - "tool_result:Tool Result", - "system_prompt:System Prompt", - "thinking:Thinking", - "error:Error", - "session_start:Session Start", - "session_end:Session End" - ], - "selectionValueType": "string" - }, - { - "name": "eventData", - "displayName": "Event Data", - "type": "json", - "columnName": "event_data", + "name": "errorCode", + "displayName": "Error Code", + "type": "int", + "columnName": "error_code", "required": false, "unique": false, "index": false, @@ -5810,10 +5389,10 @@ "isSystem": true }, { - "name": "content", - "displayName": "Content", + "name": "errorMessage", + "displayName": "Error Message", "type": "longText", - "columnName": "content", + "columnName": "error_message", "required": false, "unique": false, "index": false, @@ -5822,316 +5401,10 @@ "isSystem": true }, { - "name": "toolName", - "displayName": "Tool Name", - "type": "shortText", - "columnName": "tool_name", - "length": 128, - "required": false, - "unique": false, - "index": true, - "private": false, - "encrypt": false, - "isSystem": true - }, - { - "name": "toolArguments", - "displayName": "Tool Arguments", - "type": "json", - "columnName": "tool_arguments", - "required": false, - "unique": false, - "index": false, - "private": false, - "encrypt": false, - "isSystem": true - }, - { - "name": "toolOutput", - "displayName": "Tool Output", - "type": "json", - "columnName": "tool_output", - "required": false, - "unique": false, - "index": false, - "private": false, - "encrypt": false, - "isSystem": true - }, - { - "name": "toolReturncode", - "displayName": "Tool Return Code", - "type": "int", - "columnName": "tool_returncode", - "required": false, - "unique": false, - "index": false, - "private": false, - "encrypt": false, - "isSystem": true - }, - { - "name": "durationMs", - "displayName": "Duration (ms)", - "type": "decimal", - "columnName": "duration_ms", - "required": false, - "unique": false, - "index": false, - "private": false, - "encrypt": false, - "isSystem": true - }, - { - "name": "cost", - "displayName": "Cost", - "type": "decimal", - "columnName": "cost", - "required": false, - "unique": false, - "index": false, - "private": false, - "encrypt": false, - "isSystem": true - }, - { - "name": "inputTokens", - "displayName": "Input Tokens", - "type": "int", - "columnName": "input_tokens", - "required": false, - "unique": false, - "index": false, - "private": false, - "encrypt": false, - "isSystem": true - }, - { - "name": "outputTokens", - "displayName": "Output Tokens", - "type": "int", - "columnName": "output_tokens", - "required": false, - "unique": false, - "index": false, - "private": false, - "encrypt": false, - "isSystem": true - }, - { - "name": "modelUsed", - "displayName": "Model Used", - "type": "shortText", - "columnName": "model_used", - "length": 255, - "required": false, - "unique": false, - "index": false, - "private": false, - "encrypt": false, - "isSystem": true - } - ] - }, - { - "singularName": "mcpAuditLog", - "pluralName": "mcpAuditLogs", - "displayName": "MCP Audit Log", - "description": "Audit trail of MCP requests handled by the server", - "dataSource": "default", - "dataSourceType": "postgres", - "tableName": "ss_mcp_audit_log", - "isChild": false, - "isLegacyTable": true, - "isLegacyTableWithId": true, - "enableAuditTracking": false, - "enableSoftDelete": false, - "draftPublishWorkflow": false, - "internationalisation": false, - "isSystem": true, - "userKeyFieldUserKey": "id", - "fields": [ - { - "name": "userId", - "displayName": "User ID", - "type": "int", - "columnName": "user_id", - "required": false, - "unique": false, - "index": true, - "private": false, - "encrypt": false, - "isSystem": true - }, - { - "name": "apiKeyId", - "displayName": "API Key ID", - "type": "int", - "columnName": "api_key_id", - "required": false, - "unique": false, - "index": false, - "private": false, - "encrypt": false, - "isSystem": true - }, - { - "name": "username", - "displayName": "Username", - "type": "shortText", - "columnName": "username", - "length": 128, - "required": false, - "unique": false, - "index": false, - "private": false, - "encrypt": false, - "isSystem": true - }, - { - "name": "transport", - "displayName": "Transport", - "type": "shortText", - "columnName": "transport", - "length": 32, - "required": true, - "unique": false, - "index": false, - "private": false, - "encrypt": false, - "isSystem": true - }, - { - "name": "mcpSessionId", - "displayName": "MCP Session ID", - "type": "shortText", - "columnName": "mcp_session_id", - "length": 64, - "required": false, - "unique": false, - "index": true, - "private": false, - "encrypt": false, - "isSystem": true - }, - { - "name": "clientAddr", - "displayName": "Client Address", - "type": "shortText", - "columnName": "client_addr", - "length": 64, - "required": false, - "unique": false, - "index": false, - "private": false, - "encrypt": false, - "isSystem": true - }, - { - "name": "method", - "displayName": "Method", - "type": "shortText", - "columnName": "method", - "length": 64, - "required": true, - "unique": false, - "index": true, - "private": false, - "encrypt": false, - "isSystem": true - }, - { - "name": "requestId", - "displayName": "Request ID", - "type": "shortText", - "columnName": "request_id", - "length": 64, - "required": false, - "unique": false, - "index": false, - "private": false, - "encrypt": false, - "isSystem": true - }, - { - "name": "toolName", - "displayName": "Tool Name", - "type": "shortText", - "columnName": "tool_name", - "length": 128, - "required": false, - "unique": false, - "index": true, - "private": false, - "encrypt": false, - "isSystem": true - }, - { - "name": "requestParams", - "displayName": "Request Params", - "type": "longText", - "columnName": "request_params", - "required": false, - "unique": false, - "index": false, - "private": false, - "encrypt": false, - "isSystem": true - }, - { - "name": "status", - "displayName": "Status", - "type": "shortText", - "columnName": "status", - "length": 16, - "required": true, - "unique": false, - "index": false, - "private": false, - "encrypt": false, - "isSystem": true - }, - { - "name": "responseResult", - "displayName": "Response Result", - "type": "longText", - "columnName": "response_result", - "required": false, - "unique": false, - "index": false, - "private": false, - "encrypt": false, - "isSystem": true - }, - { - "name": "errorCode", - "displayName": "Error Code", - "type": "int", - "columnName": "error_code", - "required": false, - "unique": false, - "index": false, - "private": false, - "encrypt": false, - "isSystem": true - }, - { - "name": "errorMessage", - "displayName": "Error Message", - "type": "longText", - "columnName": "error_message", - "required": false, - "unique": false, - "index": false, - "private": false, - "encrypt": false, - "isSystem": true - }, - { - "name": "durationMs", - "displayName": "Duration (ms)", - "type": "decimal", - "columnName": "duration_ms", + "name": "durationMs", + "displayName": "Duration (ms)", + "type": "decimal", + "columnName": "duration_ms", "required": false, "unique": false, "index": false, @@ -6663,71 +5936,6 @@ "moduleUserKey": "solid-core", "modelUserKey": "setting" }, - { - "displayName": "Dashboard List Action", - "name": "dashboard-list-action", - "type": "solid", - "domain": "", - "context": "", - "customComponent": "", - "customIsModal": true, - "serverEndpoint": "", - "viewUserKey": "dashboard-list-view", - "moduleUserKey": "solid-core", - "modelUserKey": "dashboard" - }, - { - "displayName": "Dashboard Variable List Action", - "name": "dashboardVariable-list-action", - "type": "solid", - "domain": "", - "context": "", - "customComponent": "", - "customIsModal": true, - "serverEndpoint": "", - "viewUserKey": "dashboardVariable-list-view", - "moduleUserKey": "solid-core", - "modelUserKey": "dashboardVariable" - }, - { - "displayName": "Dashboard Question List Action", - "name": "dashboardQuestion-list-action", - "type": "solid", - "domain": "", - "context": "", - "customComponent": "", - "customIsModal": true, - "serverEndpoint": "", - "viewUserKey": "dashboardQuestion-list-view", - "moduleUserKey": "solid-core", - "modelUserKey": "dashboardQuestion" - }, - { - "displayName": "Dashboard Layout List Action", - "name": "dashboardLayout-list-action", - "type": "solid", - "domain": "", - "context": "", - "customComponent": "", - "customIsModal": true, - "serverEndpoint": "", - "viewUserKey": "dashboardLayout-list-view", - "moduleUserKey": "solid-core", - "modelUserKey": "dashboardLayout" - }, - { - "displayName": "Dashboard Question SQL Dataset Config List Action", - "name": "dashboardQuestionSqlDatasetConfig-list-action", - "type": "solid", - "domain": "", - "context": "", - "customComponent": "", - "customIsModal": true, - "serverEndpoint": "", - "viewUserKey": "dashboardQuestionSqlDatasetConfig-list-view", - "moduleUserKey": "solid-core", - "modelUserKey": "dashboardQuestionSqlDatasetConfig" - }, { "displayName": "AI Interactions", "name": "aiInteraction-list-action", @@ -7008,872 +6216,183 @@ "displayName": "Messages", "name": "mqMessage-menu-item", "sequenceNumber": 1, - "actionUserKey": "mqMessage-list-action", - "moduleUserKey": "solid-core", - "parentMenuItemUserKey": "queues-menu-item" - }, - { - "displayName": "Queues", - "name": "mqMessageQueue-menu-item", - "sequenceNumber": 2, - "actionUserKey": "mqMessageQueue-list-action", - "moduleUserKey": "solid-core", - "parentMenuItemUserKey": "queues-menu-item" - }, - { - "displayName": "Notification", - "name": "notification-menu-item", - "sequenceNumber": 6, - "actionUserKey": "", - "moduleUserKey": "solid-core", - "parentMenuItemUserKey": "", - "iconName": "notification_settings" - }, - { - "displayName": "Email", - "name": "emailTemplate-menu-item", - "sequenceNumber": 1, - "actionUserKey": "emailTemplate-list-action", - "moduleUserKey": "solid-core", - "parentMenuItemUserKey": "notification-menu-item" - }, - { - "displayName": "SMS", - "name": "smsTemplate-menu-item", - "sequenceNumber": 2, - "actionUserKey": "smsTemplate-list-action", - "moduleUserKey": "solid-core", - "parentMenuItemUserKey": "notification-menu-item" - }, - { - "displayName": "Other", - "name": "other-menu-item", - "sequenceNumber": 7, - "actionUserKey": "", - "moduleUserKey": "solid-core", - "parentMenuItemUserKey": "", - "iconName": "other_admission" - }, - { - "displayName": "List of Values", - "name": "listOfValues-menu-item", - "sequenceNumber": 1, - "actionUserKey": "listOfValues-list-action", - "moduleUserKey": "solid-core", - "parentMenuItemUserKey": "other-menu-item" - }, - { - "displayName": "Scheduled Jobs", - "name": "scheduledJob-menu-item", - "sequenceNumber": 2, - "actionUserKey": "scheduledJob-list-action", - "moduleUserKey": "solid-core", - "parentMenuItemUserKey": "other-menu-item" - }, - { - "displayName": "AI Interactions", - "name": "aiInteraction-menu-item", - "sequenceNumber": 3, - "actionUserKey": "aiInteraction-list-action", - "moduleUserKey": "solid-core", - "parentMenuItemUserKey": "other-menu-item" - }, - { - "displayName": "Saved Filters", - "name": "savedFilters-menu-item", - "sequenceNumber": 4, - "actionUserKey": "savedFilters-list-action", - "moduleUserKey": "solid-core", - "parentMenuItemUserKey": "other-menu-item" - }, - { - "displayName": "Chatter Message", - "name": "chatterMessage-menu-item", - "sequenceNumber": 5, - "actionUserKey": "chatterMessage-list-action", - "moduleUserKey": "solid-core", - "parentMenuItemUserKey": "other-menu-item" - }, - { - "displayName": "Chatter Message Details", - "name": "chatterMessageDetails-menu-item", - "sequenceNumber": 6, - "actionUserKey": "chatterMessageDetails-list-action", - "moduleUserKey": "solid-core", - "parentMenuItemUserKey": "other-menu-item" - }, - { - "displayName": "Locale", - "name": "locale-menu-item", - "sequenceNumber": 7, - "actionUserKey": "locale-list-action", - "moduleUserKey": "solid-core", - "parentMenuItemUserKey": "other-menu-item" - }, - { - "displayName": "Import Transactions", - "name": "importTransaction-menu-item", - "sequenceNumber": 8, - "actionUserKey": "importTransaction-list-action", - "moduleUserKey": "solid-core", - "parentMenuItemUserKey": "other-menu-item" - }, - { - "displayName": "Import Error Logs", - "name": "importTransactionErrorLog-menu-item", - "sequenceNumber": 9, - "actionUserKey": "importTransactionErrorLog-list-action", - "moduleUserKey": "solid-core", - "parentMenuItemUserKey": "other-menu-item" - }, - { - "displayName": "Dashboards", - "name": "dashboardManagement-menu-item", - "sequenceNumber": 8, - "actionUserKey": "", - "moduleUserKey": "solid-core", - "parentMenuItemUserKey": "", - "iconName": "dashboard_customize" - }, - { - "displayName": "Dashboard", - "name": "dashboard-menu-item", - "sequenceNumber": 1, - "actionUserKey": "dashboard-list-action", + "actionUserKey": "mqMessage-list-action", "moduleUserKey": "solid-core", - "parentMenuItemUserKey": "dashboardManagement-menu-item" + "parentMenuItemUserKey": "queues-menu-item" }, { - "displayName": "Dashboard Question", - "name": "dashboardQuestion-menu-item", + "displayName": "Queues", + "name": "mqMessageQueue-menu-item", "sequenceNumber": 2, - "actionUserKey": "dashboardQuestion-list-action", + "actionUserKey": "mqMessageQueue-list-action", "moduleUserKey": "solid-core", - "parentMenuItemUserKey": "dashboardManagement-menu-item" + "parentMenuItemUserKey": "queues-menu-item" }, { - "displayName": "Dashboard Layout", - "name": "dashboardLayout-menu-item", - "sequenceNumber": 3, - "actionUserKey": "dashboardLayout-list-action", + "displayName": "Notification", + "name": "notification-menu-item", + "sequenceNumber": 6, + "actionUserKey": "", "moduleUserKey": "solid-core", - "parentMenuItemUserKey": "dashboardManagement-menu-item" + "parentMenuItemUserKey": "", + "iconName": "notification_settings" }, { - "displayName": "Settings", - "name": "settings-menu-item", - "sequenceNumber": 9, - "actionUserKey": "settings-action", + "displayName": "Email", + "name": "emailTemplate-menu-item", + "sequenceNumber": 1, + "actionUserKey": "emailTemplate-list-action", "moduleUserKey": "solid-core", - "parentMenuItemUserKey": "", - "iconName": "settings" + "parentMenuItemUserKey": "notification-menu-item" }, { - "displayName": "Model Sequence", - "name": "modelSequence-menu-item", - "sequenceNumber": 1, - "actionUserKey": "modelSequence-list-action", + "displayName": "SMS", + "name": "smsTemplate-menu-item", + "sequenceNumber": 2, + "actionUserKey": "smsTemplate-list-action", "moduleUserKey": "solid-core", - "parentMenuItemUserKey": "other-menu-item" + "parentMenuItemUserKey": "notification-menu-item" }, { - "displayName": "Agent", - "name": "agent-menu-item", - "sequenceNumber": 8, + "displayName": "Other", + "name": "other-menu-item", + "sequenceNumber": 7, "actionUserKey": "", "moduleUserKey": "solid-core", "parentMenuItemUserKey": "", - "iconName": "smart_toy" + "iconName": "other_admission" }, { - "displayName": "Sessions", - "name": "agentSession-menu-item", + "displayName": "List of Values", + "name": "listOfValues-menu-item", "sequenceNumber": 1, - "actionUserKey": "agentSession-list-action", + "actionUserKey": "listOfValues-list-action", "moduleUserKey": "solid-core", - "parentMenuItemUserKey": "agent-menu-item" + "parentMenuItemUserKey": "other-menu-item" }, { - "displayName": "Events", - "name": "agentEvent-menu-item", + "displayName": "Scheduled Jobs", + "name": "scheduledJob-menu-item", "sequenceNumber": 2, - "actionUserKey": "agentEvent-list-action", + "actionUserKey": "scheduledJob-list-action", "moduleUserKey": "solid-core", - "parentMenuItemUserKey": "agent-menu-item" + "parentMenuItemUserKey": "other-menu-item" }, { - "displayName": "MCP Audit Log", - "name": "mcpAuditLog-menu-item", + "displayName": "AI Interactions", + "name": "aiInteraction-menu-item", "sequenceNumber": 3, - "actionUserKey": "mcpAuditLog-list-action", + "actionUserKey": "aiInteraction-list-action", "moduleUserKey": "solid-core", - "parentMenuItemUserKey": "agent-menu-item" - } - ], - "views": [ + "parentMenuItemUserKey": "other-menu-item" + }, { - "name": "moduleMetadata-list-view", - "displayName": "Module Metadata", - "type": "list", - "context": "{}", + "displayName": "Saved Filters", + "name": "savedFilters-menu-item", + "sequenceNumber": 4, + "actionUserKey": "savedFilters-list-action", "moduleUserKey": "solid-core", - "modelUserKey": "moduleMetadata", - "layout": { - "type": "list", - "attrs": { - "pagination": true, - "pageSizeOptions": [ - 10, - 25, - 50 - ], - "enableGlobalSearch": true, - "create": true, - "edit": true, - "delete": false, - "rowButtons": [ - { - "attrs": { - "className": "p-button-danger p-button-text", - "icon": "pi pi-trash", - "label": "Delete", - "action": "DeleteModuleRowAction", - "openInPopup": true, - "actionInContextMenu": true, - "customComponentIsSystem": true - } - }, - { - "attrs": { - "className": "p-button-text", - "icon": "pi pi-cog", - "label": "Generate Code", - "action": "GenerateModuleCodeRowAction", - "openInPopup": true, - "actionInContextMenu": true, - "customComponentIsSystem": true - } - } - ] - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "displayName", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "name", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "description", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "menuIconUrl", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "menuSequenceNumber", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "defaultDataSource", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "isSystem", - "isSearchable": true - } - } - ] - } + "parentMenuItemUserKey": "other-menu-item" }, { - "name": "moduleMetadata-form-view", - "displayName": "Module Metadata", - "type": "form", - "context": "{}", + "displayName": "Chatter Message", + "name": "chatterMessage-menu-item", + "sequenceNumber": 5, + "actionUserKey": "chatterMessage-list-action", "moduleUserKey": "solid-core", - "modelUserKey": "moduleMetadata", - "layout": { - "type": "form", - "attrs": { - "name": "form-1", - "label": "Module Metadata", - "className": "grid" - }, - "children": [ - { - "type": "sheet", - "attrs": { - "name": "sheet-1" - }, - "children": [ - { - "type": "group", - "attrs": { - "name": "group-1", - "label": "", - "className": "col-12 sm:col-12 md:col-6 lg:col-6" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "displayName", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "name", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "description", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "menuIconUrl", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "menuSequenceNumber", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "defaultDataSource", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "isSystem" - } - } - ] - } - ] - } - ] - } + "parentMenuItemUserKey": "other-menu-item" }, { - "name": "modelMetadata-list-view", - "displayName": "Model Metadata", - "type": "list", - "context": "{}", + "displayName": "Chatter Message Details", + "name": "chatterMessageDetails-menu-item", + "sequenceNumber": 6, + "actionUserKey": "chatterMessageDetails-list-action", "moduleUserKey": "solid-core", - "modelUserKey": "modelMetadata", - "layout": { - "type": "list", - "attrs": { - "pagination": true, - "pageSizeOptions": [ - 10, - 25, - 50 - ], - "enableGlobalSearch": true, - "truncateAfter": 50, - "create": true, - "edit": true, - "delete": false, - "rowButtons": [ - { - "attrs": { - "icon": "pi pi-trash", - "className": "p-button-danger p-button-text", - "label": "Delete", - "action": "DeleteModelRowAction", - "customComponentIsSystem": true, - "actionInContextMenu": true, - "openInPopup": true - } - }, - { - "attrs": { - "icon": "pi pi-cog", - "className": "p-button-text", - "label": "Generate Code", - "action": "GenerateModelCodeRowAction", - "customComponentIsSystem": true, - "actionInContextMenu": true, - "openInPopup": true - } - } - ] - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "singularName", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "tableName", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "pluralName", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "displayName", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "description", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "dataSource", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "enableSoftDelete", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "module", - "isSearchable": true - } - } - ] - } + "parentMenuItemUserKey": "other-menu-item" }, { - "name": "modelMetadata-form-view", - "displayName": "Model Metadata", - "type": "form", - "context": "{}", + "displayName": "Locale", + "name": "locale-menu-item", + "sequenceNumber": 7, + "actionUserKey": "locale-list-action", "moduleUserKey": "solid-core", - "modelUserKey": "modelMetadata", - "layout": { - "type": "form", - "attrs": { - "name": "form-1", - "label": "Model Metadata", - "className": "grid" - }, - "children": [ - { - "type": "sheet", - "attrs": { - "name": "sheet-1" - }, - "children": [ - { - "type": "group", - "attrs": { - "name": "group-1", - "label": "", - "className": "col-12 sm:col-12 md:col-6 lg:col-6" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "singularName", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "tableName", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "pluralName", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "displayName", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "description", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "dataSource", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "enableSoftDelete", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "module", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "isUserKey", - "isSearchable": true - } - } - ] - } - ] - } - ] - } + "parentMenuItemUserKey": "other-menu-item" }, { - "name": "savedFilters-list-view", - "displayName": "Saved Filters list view", - "type": "list", - "context": "{}", - "moduleUserKey": "solid-core", - "modelUserKey": "savedFilters", - "layout": { - "type": "list", - "attrs": { - "pagination": true, - "pageSizeOptions": [ - 10, - 25, - 50 - ], - "enableGlobalSearch": true, - "create": true, - "edit": true, - "delete": true - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "name", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "model", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "view", - "isSearchable": true - } - } - ] - } + "displayName": "Import Transactions", + "name": "importTransaction-menu-item", + "sequenceNumber": 8, + "actionUserKey": "importTransaction-list-action", + "moduleUserKey": "solid-core", + "parentMenuItemUserKey": "other-menu-item" }, { - "name": "savedFilters-form-view", - "displayName": "Saved Filters form view", - "type": "form", - "context": "{}", + "displayName": "Import Error Logs", + "name": "importTransactionErrorLog-menu-item", + "sequenceNumber": 9, + "actionUserKey": "importTransactionErrorLog-list-action", "moduleUserKey": "solid-core", - "modelUserKey": "savedFilters", - "layout": { - "type": "form", - "attrs": { - "name": "form-1", - "label": "Solid Saved Filter Model", - "className": "grid" - }, - "children": [ - { - "type": "sheet", - "attrs": { - "name": "sheet-1" - }, - "children": [ - { - "type": "group", - "attrs": { - "name": "group-1", - "label": "", - "className": "col-12 sm:col-12 md:col-6 lg:col-6" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "name" - } - }, - { - "type": "field", - "attrs": { - "name": "filterQueryJson" - } - }, - { - "type": "field", - "attrs": { - "name": "view" - } - }, - { - "type": "field", - "attrs": { - "name": "model" - } - }, - { - "type": "field", - "attrs": { - "name": "user" - } - }, - { - "type": "field", - "attrs": { - "name": "isPrivate" - } - } - ] - } - ] - } - ] - } + "parentMenuItemUserKey": "other-menu-item" }, { - "name": "listOfValues-form-view", - "displayName": "List of Values form view", - "type": "form", - "context": "{}", + "displayName": "Settings", + "name": "settings-menu-item", + "sequenceNumber": 9, + "actionUserKey": "settings-action", "moduleUserKey": "solid-core", - "modelUserKey": "listOfValues", - "layout": { - "type": "form", - "attrs": { - "name": "form-1", - "label": "Solid List of Values Model", - "className": "grid" - }, - "children": [ - { - "type": "sheet", - "attrs": { - "name": "sheet-1" - }, - "children": [ - { - "type": "group", - "attrs": { - "name": "group-1", - "label": "", - "className": "col-12 sm:col-12 md:col-6 lg:col-6" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "type" - } - }, - { - "type": "field", - "attrs": { - "name": "value" - } - }, - { - "type": "field", - "attrs": { - "name": "display" - } - }, - { - "type": "field", - "attrs": { - "name": "description" - } - }, - { - "type": "field", - "attrs": { - "name": "default" - } - }, - { - "type": "field", - "attrs": { - "name": "sequence" - } - }, - { - "type": "field", - "attrs": { - "name": "module" - } - } - ] - } - ] - } - ] - } + "parentMenuItemUserKey": "", + "iconName": "settings" }, { - "name": "listOfValues-list-view", - "displayName": "List of Values list view", - "type": "list", - "context": "{}", + "displayName": "Model Sequence", + "name": "modelSequence-menu-item", + "sequenceNumber": 1, + "actionUserKey": "modelSequence-list-action", "moduleUserKey": "solid-core", - "modelUserKey": "listOfValues", - "layout": { - "type": "list", - "attrs": { - "pagination": true, - "pageSizeOptions": [ - 10, - 25, - 50 - ], - "enableGlobalSearch": true, - "create": true, - "edit": true, - "delete": true - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "type", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "value", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "display", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "description", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "default", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "sequence", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "module" - } - } - ] - } + "parentMenuItemUserKey": "other-menu-item" + }, + { + "displayName": "Agent", + "name": "agent-menu-item", + "sequenceNumber": 8, + "actionUserKey": "", + "moduleUserKey": "solid-core", + "parentMenuItemUserKey": "", + "iconName": "smart_toy" + }, + { + "displayName": "Sessions", + "name": "agentSession-menu-item", + "sequenceNumber": 1, + "actionUserKey": "agentSession-list-action", + "moduleUserKey": "solid-core", + "parentMenuItemUserKey": "agent-menu-item" + }, + { + "displayName": "Events", + "name": "agentEvent-menu-item", + "sequenceNumber": 2, + "actionUserKey": "agentEvent-list-action", + "moduleUserKey": "solid-core", + "parentMenuItemUserKey": "agent-menu-item" }, { - "name": "mediaStorageProviderMetadata-list-view", - "displayName": "Solid Media Storage Provider Metadata Model", + "displayName": "MCP Audit Log", + "name": "mcpAuditLog-menu-item", + "sequenceNumber": 3, + "actionUserKey": "mcpAuditLog-list-action", + "moduleUserKey": "solid-core", + "parentMenuItemUserKey": "agent-menu-item" + } + ], + "views": [ + { + "name": "moduleMetadata-list-view", + "displayName": "Module Metadata", "type": "list", "context": "{}", "moduleUserKey": "solid-core", - "modelUserKey": "mediaStorageProviderMetadata", + "modelUserKey": "moduleMetadata", "layout": { "type": "list", "attrs": { @@ -7886,73 +6405,97 @@ "enableGlobalSearch": true, "create": true, "edit": true, - "delete": true + "delete": false, + "rowButtons": [ + { + "attrs": { + "className": "p-button-danger p-button-text", + "icon": "pi pi-trash", + "label": "Delete", + "action": "DeleteModuleRowAction", + "openInPopup": true, + "actionInContextMenu": true, + "customComponentIsSystem": true + } + }, + { + "attrs": { + "className": "p-button-text", + "icon": "pi pi-cog", + "label": "Generate Code", + "action": "GenerateModuleCodeRowAction", + "openInPopup": true, + "actionInContextMenu": true, + "customComponentIsSystem": true + } + } + ] }, "children": [ { "type": "field", "attrs": { - "name": "name", + "name": "displayName", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "type", + "name": "name", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "region", + "name": "description", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "bucketName", + "name": "menuIconUrl", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "isPublic", + "name": "menuSequenceNumber", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "localPath", + "name": "defaultDataSource", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "signedUrlExpiry", - "isSearchable": false + "name": "isSystem", + "isSearchable": true } } ] } }, { - "name": "mediaStorageProviderMetadata-form-view", - "displayName": "Solid Media Storage Provider Metadata Model", + "name": "moduleMetadata-form-view", + "displayName": "Module Metadata", "type": "form", "context": "{}", "moduleUserKey": "solid-core", - "modelUserKey": "mediaStorageProviderMetadata", + "modelUserKey": "moduleMetadata", "layout": { "type": "form", "attrs": { "name": "form-1", - "label": "Solid Media Storage Provider Metadata Model", + "label": "Module Metadata", "className": "grid" }, "children": [ @@ -7973,50 +6516,49 @@ { "type": "field", "attrs": { - "name": "name", + "name": "displayName", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "type", + "name": "name", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "region", + "name": "description", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "bucketName", + "name": "menuIconUrl", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "isPublic", + "name": "menuSequenceNumber", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "localPath", + "name": "defaultDataSource", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "signedUrlExpiry", - "isSearchable": false + "name": "isSystem" } } ] @@ -8027,12 +6569,12 @@ } }, { - "name": "chatterMessageDetails-list-view", - "displayName": "Chatter Message Details Model", + "name": "modelMetadata-list-view", + "displayName": "Model Metadata", "type": "list", "context": "{}", "moduleUserKey": "solid-core", - "modelUserKey": "chatterMessageDetails", + "modelUserKey": "modelMetadata", "layout": { "type": "list", "attrs": { @@ -8043,198 +6585,108 @@ 50 ], "enableGlobalSearch": true, - "create": false, - "edit": false, - "delete": false + "truncateAfter": 50, + "create": true, + "edit": true, + "delete": false, + "rowButtons": [ + { + "attrs": { + "icon": "pi pi-trash", + "className": "p-button-danger p-button-text", + "label": "Delete", + "action": "DeleteModelRowAction", + "customComponentIsSystem": true, + "actionInContextMenu": true, + "openInPopup": true + } + }, + { + "attrs": { + "icon": "pi pi-cog", + "className": "p-button-text", + "label": "Generate Code", + "action": "GenerateModelCodeRowAction", + "customComponentIsSystem": true, + "actionInContextMenu": true, + "openInPopup": true + } + } + ] }, "children": [ { "type": "field", "attrs": { - "name": "fieldDisplayName", + "name": "singularName", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "fieldName", + "name": "tableName", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "oldValueDisplay", + "name": "pluralName", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "newValueDisplay", - "isSearchable": true - } - } - ] - } - }, - { - "name": "chatterMessageDetails-form-view", - "displayName": "Chatter Message Details Model", - "type": "form", - "context": "{}", - "moduleUserKey": "solid-core", - "modelUserKey": "chatterMessageDetails", - "layout": { - "type": "form", - "attrs": { - "name": "form-1", - "label": "Chatter Message Details Model", - "className": "grid", - "showEditFormButton": false, - "showAddFormButton": false, - "showDeleteFormButton": false - }, - "children": [ - { - "type": "sheet", - "attrs": { - "name": "sheet-1" - }, - "children": [ - { - "type": "group", - "attrs": { - "name": "group-1", - "label": "", - "className": "col-12 sm:col-12 md:col-6 lg:col-6" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "fieldDisplayName" - } - }, - { - "type": "field", - "attrs": { - "name": "fieldName" - } - }, - { - "type": "field", - "attrs": { - "name": "oldValueDisplay" - } - }, - { - "type": "field", - "attrs": { - "name": "newValueDisplay" - } - }, - { - "type": "field", - "attrs": { - "name": "oldValue" - } - }, - { - "type": "field", - "attrs": { - "name": "newValue" - } - }, - { - "type": "field", - "attrs": { - "name": "chatterMessage" - } - } - ] - } - ] - } - ] - } - }, - { - "name": "chatterMessage-list-view", - "displayName": "Chatter Message Model", - "type": "list", - "context": "{}", - "moduleUserKey": "solid-core", - "modelUserKey": "chatterMessage", - "layout": { - "type": "list", - "attrs": { - "pagination": true, - "pageSizeOptions": [ - 10, - 25, - 50 - ], - "enableGlobalSearch": true, - "create": false, - "edit": false, - "delete": false - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "messageType", + "name": "displayName", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "messageSubType", + "name": "description", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "modelDisplayName", + "name": "dataSource", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "coModelEntityId", + "name": "enableSoftDelete", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "user" + "name": "module", + "isSearchable": true } } ] } }, { - "name": "chatterMessage-form-view", - "displayName": "Chatter Message Model", + "name": "modelMetadata-form-view", + "displayName": "Model Metadata", "type": "form", "context": "{}", "moduleUserKey": "solid-core", - "modelUserKey": "chatterMessage", + "modelUserKey": "modelMetadata", "layout": { "type": "form", "attrs": { "name": "form-1", - "label": "Chatter Message Model", - "className": "grid", - "showEditFormButton": false, - "showAddFormButton": false, - "showDeleteFormButton": false + "label": "Model Metadata", + "className": "grid" }, "children": [ { @@ -8254,55 +6706,64 @@ { "type": "field", "attrs": { - "name": "messageType" + "name": "singularName", + "isSearchable": true } }, { "type": "field", "attrs": { - "name": "messageSubType" + "name": "tableName", + "isSearchable": true } }, { "type": "field", "attrs": { - "name": "modelDisplayName" + "name": "pluralName", + "isSearchable": true } }, { "type": "field", "attrs": { - "name": "coModelEntityId" + "name": "displayName", + "isSearchable": true } }, { "type": "field", "attrs": { - "name": "modelUserKey" + "name": "description", + "isSearchable": true } }, { "type": "field", "attrs": { - "name": "user" + "name": "dataSource", + "isSearchable": true } }, { "type": "field", "attrs": { - "name": "messageBody" + "name": "enableSoftDelete", + "isSearchable": true } }, { "type": "field", "attrs": { - "name": "messageAttachments" + "name": "module", + "isSearchable": true } }, { "type": "field", "attrs": { - "name": "chatterMessageDetails" + "name": "isUserKey", + "isSearchable": true } } ] @@ -8313,12 +6774,12 @@ } }, { - "name": "locale-list-view", - "displayName": "Locale Model", + "name": "savedFilters-list-view", + "displayName": "Saved Filters list view", "type": "list", "context": "{}", "moduleUserKey": "solid-core", - "modelUserKey": "locale", + "modelUserKey": "savedFilters", "layout": { "type": "list", "attrs": { @@ -8337,39 +6798,39 @@ { "type": "field", "attrs": { - "name": "locale", + "name": "name", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "displayName", + "name": "model", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "isDefault", - "isSearchable": false + "name": "view", + "isSearchable": true } } ] } }, { - "name": "locale-form-view", - "displayName": "locale Model", + "name": "savedFilters-form-view", + "displayName": "Saved Filters form view", "type": "form", "context": "{}", "moduleUserKey": "solid-core", - "modelUserKey": "locale", + "modelUserKey": "savedFilters", "layout": { "type": "form", "attrs": { "name": "form-1", - "label": "locale model", + "label": "Solid Saved Filter Model", "className": "grid" }, "children": [ @@ -8390,22 +6851,37 @@ { "type": "field", "attrs": { - "name": "locale", - "isSearchable": true + "name": "name" } }, { "type": "field", "attrs": { - "name": "displayName", - "isSearchable": true + "name": "filterQueryJson" } }, { "type": "field", "attrs": { - "name": "isDefault", - "isSearchable": true + "name": "view" + } + }, + { + "type": "field", + "attrs": { + "name": "model" + } + }, + { + "type": "field", + "attrs": { + "name": "user" + } + }, + { + "type": "field", + "attrs": { + "name": "isPrivate" } } ] @@ -8416,216 +6892,75 @@ } }, { - "name": "viewMetadata-list-view", - "displayName": "View Metadata list view", - "type": "list", - "context": "{}", - "moduleUserKey": "solid-core", - "modelUserKey": "viewMetadata", - "layout": { - "type": "list", - "attrs": { - "pagination": true, - "pageSizeOptions": [ - 10, - 25, - 50 - ], - "enableGlobalSearch": true, - "create": true, - "edit": true, - "delete": true - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "name", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "displayName", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "type", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "context", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "layout", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "module", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "model", - "isSearchable": true - } - } - ] - } - }, - { - "name": "viewMetadata-form-view", - "displayName": "View Metadata form view", + "name": "listOfValues-form-view", + "displayName": "List of Values form view", "type": "form", "context": "{}", "moduleUserKey": "solid-core", - "modelUserKey": "viewMetadata", + "modelUserKey": "listOfValues", "layout": { - "type": "form", - "attrs": { - "name": "form-1", - "label": "View Metadata", - "className": "grid" - }, - "children": [ - { - "type": "sheet", - "attrs": { - "name": "sheet-1" - }, - "children": [ - { - "type": "notebook", - "attrs": { - "name": "notebook-1" - }, - "children": [ - { - "type": "page", - "attrs": { - "name": "page-1", - "label": "General Info" - }, - "children": [ - { - "type": "row", - "attrs": { - "name": "page-1-row-1", - "label": "", - "className": "" - }, - "children": [ - { - "type": "column", - "attrs": { - "name": "page-1-row-1-col-1", - "label": "", - "className": "col-12 sm:col-12 md:col-6 lg:col-6" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "name" - } - }, - { - "type": "field", - "attrs": { - "name": "displayName" - } - }, - { - "type": "field", - "attrs": { - "name": "type" - } - }, - { - "type": "field", - "attrs": { - "name": "module" - } - }, - { - "type": "field", - "attrs": { - "name": "model" - } - } - ] - }, - { - "type": "column", - "attrs": { - "name": "page-1-row-1-col-2", - "label": "", - "className": "col-12 sm:col-12 md:col-6 lg:col-6" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "context" - } - } - ] - } - ] - } - ] + "type": "form", + "attrs": { + "name": "form-1", + "label": "Solid List of Values Model", + "className": "grid" + }, + "children": [ + { + "type": "sheet", + "attrs": { + "name": "sheet-1" + }, + "children": [ + { + "type": "group", + "attrs": { + "name": "group-1", + "label": "", + "className": "col-12 sm:col-12 md:col-6 lg:col-6" + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "type" + } }, { - "type": "page", + "type": "field", "attrs": { - "name": "page-2", - "label": "Layout" - }, - "children": [ - { - "type": "row", - "attrs": { - "name": "page-2-row-1", - "label": "", - "className": "" - }, - "children": [ - { - "type": "column", - "attrs": { - "name": "page-2-row-1-col-1", - "label": "", - "className": "col-12" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "layout", - "height": "80vh" - } - } - ] - } - ] - } - ] + "name": "value" + } + }, + { + "type": "field", + "attrs": { + "name": "display" + } + }, + { + "type": "field", + "attrs": { + "name": "description" + } + }, + { + "type": "field", + "attrs": { + "name": "default" + } + }, + { + "type": "field", + "attrs": { + "name": "sequence" + } + }, + { + "type": "field", + "attrs": { + "name": "module" + } } ] } @@ -8635,12 +6970,12 @@ } }, { - "name": "userViewMetadata-list-view", - "displayName": "User View Metadata list view", + "name": "listOfValues-list-view", + "displayName": "List of Values list view", "type": "list", "context": "{}", "moduleUserKey": "solid-core", - "modelUserKey": "userViewMetadata", + "modelUserKey": "listOfValues", "layout": { "type": "list", "attrs": { @@ -8659,152 +6994,61 @@ { "type": "field", "attrs": { - "name": "viewMetadata" + "name": "type", + "isSearchable": true } }, { "type": "field", "attrs": { - "name": "user" + "name": "value", + "isSearchable": true } }, { "type": "field", "attrs": { - "name": "layout" + "name": "display", + "isSearchable": true } - } - ] - } - }, - { - "name": "userViewMetadata-form-view", - "displayName": "User View Metadata form view", - "type": "form", - "context": "{}", - "moduleUserKey": "solid-core", - "modelUserKey": "userViewMetadata", - "layout": { - "type": "form", - "attrs": { - "name": "form-1", - "label": "User View Metadata", - "className": "grid" - }, - "children": [ + }, { - "type": "sheet", + "type": "field", "attrs": { - "name": "sheet-1" - }, - "children": [ - { - "type": "notebook", - "attrs": { - "name": "notebook-1" - }, - "children": [ - { - "type": "page", - "attrs": { - "name": "page-1", - "label": "General Info" - }, - "children": [ - { - "type": "row", - "attrs": { - "name": "page-1-row-1", - "label": "", - "className": "" - }, - "children": [ - { - "type": "column", - "attrs": { - "name": "page-1-row-1-col-1", - "label": "", - "className": "col-12 sm:col-12 md:col-6 lg:col-6" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "viewMetadata" - } - } - ] - }, - { - "type": "column", - "attrs": { - "name": "page-1-row-1-col-2", - "label": "", - "className": "col-12 sm:col-12 md:col-6 lg:col-6" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "user" - } - } - ] - } - ] - } - ] - }, - { - "type": "page", - "attrs": { - "name": "page-2", - "label": "Layout" - }, - "children": [ - { - "type": "row", - "attrs": { - "name": "page-2-row-1", - "label": "", - "className": "" - }, - "children": [ - { - "type": "column", - "attrs": { - "name": "page-2-row-1-col-1", - "label": "", - "className": "col-12" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "layout", - "height": "80vh" - } - } - ] - } - ] - } - ] - } - ] - } - ] + "name": "description", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "default", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "sequence", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "module" + } } ] } }, { - "name": "actionMetadata-list-view", - "displayName": "Action Metadata list view", + "name": "mediaStorageProviderMetadata-list-view", + "displayName": "Solid Media Storage Provider Metadata Model", "type": "list", "context": "{}", "moduleUserKey": "solid-core", - "modelUserKey": "actionMetadata", + "modelUserKey": "mediaStorageProviderMetadata", "layout": { "type": "list", "attrs": { @@ -8830,53 +7074,60 @@ { "type": "field", "attrs": { - "name": "displayName", + "name": "type", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "type", + "name": "region", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "module", + "name": "bucketName", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "model", + "name": "isPublic", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "view", + "name": "localPath", "isSearchable": true } + }, + { + "type": "field", + "attrs": { + "name": "signedUrlExpiry", + "isSearchable": false + } } ] } }, { - "name": "actionMetadata-form-view", - "displayName": "Action Metadata form view", + "name": "mediaStorageProviderMetadata-form-view", + "displayName": "Solid Media Storage Provider Metadata Model", "type": "form", "context": "{}", "moduleUserKey": "solid-core", - "modelUserKey": "actionMetadata", + "modelUserKey": "mediaStorageProviderMetadata", "layout": { "type": "form", "attrs": { "name": "form-1", - "label": "Action Metadata", + "label": "Solid Media Storage Provider Metadata Model", "className": "grid" }, "children": [ @@ -8887,156 +7138,61 @@ }, "children": [ { - "type": "notebook", + "type": "group", "attrs": { - "name": "notebook-1" + "name": "group-1", + "label": "", + "className": "col-12 sm:col-12 md:col-6 lg:col-6" }, "children": [ { - "type": "page", + "type": "field", "attrs": { - "name": "page-1", - "label": "General Info" - }, - "children": [ - { - "type": "row", - "attrs": { - "name": "page-1-row-1", - "label": "", - "className": "" - }, - "children": [ - { - "type": "column", - "attrs": { - "name": "page-1-row-1-col-1", - "label": "", - "className": "col-12 sm:col-12 md:col-6 lg:col-6" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "name" - } - }, - { - "type": "field", - "attrs": { - "name": "displayName" - } - }, - { - "type": "field", - "attrs": { - "name": "type" - } - }, - { - "type": "field", - "attrs": { - "name": "customComponent" - } - }, - { - "type": "field", - "attrs": { - "name": "customIsModal" - } - }, - { - "type": "field", - "attrs": { - "name": "serverEndpoint" - } - } - ] - }, - { - "type": "column", - "attrs": { - "name": "page-1-row-1-col-2", - "label": "", - "className": "col-12 sm:col-12 md:col-6 lg:col-6" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "module" - } - }, - { - "type": "field", - "attrs": { - "name": "model" - } - }, - { - "type": "field", - "attrs": { - "name": "view" - } - } - ] - } - ] - } - ] + "name": "name", + "isSearchable": true + } }, { - "type": "page", + "type": "field", "attrs": { - "name": "page-2", - "label": "Layout" - }, - "children": [ - { - "type": "row", - "attrs": { - "name": "page-2-row-1", - "label": "", - "className": "" - }, - "children": [ - { - "type": "column", - "attrs": { - "name": "page-2-row-1-col-1", - "label": "", - "className": "col-12 sm:col-12 md:col-6 lg:col-6" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "domain", - "height": "80vh" - } - } - ] - }, - { - "type": "column", - "attrs": { - "name": "page-2-row-1-col-1", - "label": "", - "className": "col-12 sm:col-12 md:col-6 lg:col-6" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "context", - "height": "80vh" - } - } - ] - } - ] - } - ] + "name": "type", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "region", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "bucketName", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "isPublic", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "localPath", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "signedUrlExpiry", + "isSearchable": false + } } ] } @@ -9046,73 +7202,52 @@ } }, { - "name": "menuItemMetadata-list-view", - "displayName": "Menu Item Metadata list view", + "name": "chatterMessageDetails-list-view", + "displayName": "Chatter Message Details Model", "type": "list", "context": "{}", "moduleUserKey": "solid-core", - "modelUserKey": "menuItemMetadata", - "layout": { - "type": "list", - "attrs": { - "pagination": true, - "pageSizeOptions": [ - 10, - 25, - 50 - ], - "enableGlobalSearch": true, - "create": true, - "edit": true, - "delete": true - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "name", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "displayName", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "sequenceNumber", - "isSearchable": true - } - }, + "modelUserKey": "chatterMessageDetails", + "layout": { + "type": "list", + "attrs": { + "pagination": true, + "pageSizeOptions": [ + 10, + 25, + 50 + ], + "enableGlobalSearch": true, + "create": false, + "edit": false, + "delete": false + }, + "children": [ { "type": "field", "attrs": { - "name": "module", + "name": "fieldDisplayName", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "parentMenuItem", + "name": "fieldName", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "action", + "name": "oldValueDisplay", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "roles", + "name": "newValueDisplay", "isSearchable": true } } @@ -9120,18 +7255,21 @@ } }, { - "name": "menuItemMetadata-form-view", - "displayName": "Menu Item Metadata form view", + "name": "chatterMessageDetails-form-view", + "displayName": "Chatter Message Details Model", "type": "form", "context": "{}", "moduleUserKey": "solid-core", - "modelUserKey": "menuItemMetadata", + "modelUserKey": "chatterMessageDetails", "layout": { "type": "form", "attrs": { "name": "form-1", - "label": "Solid Menu Item Model", - "className": "grid" + "label": "Chatter Message Details Model", + "className": "grid", + "showEditFormButton": false, + "showAddFormButton": false, + "showDeleteFormButton": false }, "children": [ { @@ -9151,100 +7289,43 @@ { "type": "field", "attrs": { - "name": "name" + "name": "fieldDisplayName" } }, { "type": "field", "attrs": { - "name": "displayName" + "name": "fieldName" } }, { "type": "field", "attrs": { - "name": "sequenceNumber" + "name": "oldValueDisplay" } }, { "type": "field", "attrs": { - "name": "module" + "name": "newValueDisplay" } }, { "type": "field", "attrs": { - "name": "parentMenuItem" + "name": "oldValue" } }, { "type": "field", "attrs": { - "name": "action" + "name": "newValue" } }, { "type": "field", "attrs": { - "name": "roles", - "widget": "checkbox", - "inlineCreateAutoSave": "true", - "renderModeCheckboxPreprocessor": "MenuItemMetadataRolesFormFieldPreprocessor", - "inlineCreate": "true", - "inlineCreateLayout": { - "type": "form", - "attrs": { - "name": "form-1", - "label": "User", - "className": "grid", - "width": "20vw" - }, - "children": [ - { - "type": "sheet", - "attrs": { - "name": "sheet-1" - }, - "children": [ - { - "type": "group", - "attrs": { - "name": "group-1", - "label": "", - "className": "col-12" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "name" - } - } - ] - } - ] - } - ] - } - } - } - ] - }, - { - "type": "group", - "attrs": { - "name": "group-1", - "label": "", - "className": "col-12 sm:col-12 md:col-6 lg:col-6" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "iconName", - "editWidget": "SolidIconEditWidget", - "viewWidget": "SolidIconViewWidget" + "name": "chatterMessage" } } ] @@ -9255,12 +7336,12 @@ } }, { - "name": "media-list-view", - "displayName": "Solid Media Model", + "name": "chatterMessage-list-view", + "displayName": "Chatter Message Model", "type": "list", "context": "{}", "moduleUserKey": "solid-core", - "modelUserKey": "media", + "modelUserKey": "chatterMessage", "layout": { "type": "list", "attrs": { @@ -9268,165 +7349,67 @@ "pageSizeOptions": [ 10, 25, - 50 - ], - "enableGlobalSearch": true, - "create": true, - "edit": true, - "delete": true, - "allowedViews": [ - "list", - "card" - ] - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "entityId", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "relativeUri", - "widget": "image", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "modelMetadata", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "mediaStorageProviderMetadata", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "fieldMetadata", - "isSearchable": true - } - } - ] - } - }, - { - "name": "media-card-view", - "displayName": "Media Card", - "type": "card", - "context": "{}", - "moduleUserKey": "solid-core", - "modelUserKey": "media", - "layout": { - "type": "card", - "attrs": { - "pagination": true, - "pageSize": 24, - "pageSizeOptions": [ - 12, - 24, - 48 - ], - "enableGlobalSearch": true, - "create": true, - "edit": true, - "delete": true, - "allowedViews": [ - "list", - "card" - ] - }, - "children": [ - { - "type": "card", - "attrs": { - "name": "Card", - "cardWidget": "MediaCardWidget" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "relativeUri", - "label": "relativeUri", - "widget": "image", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "originalFileName", - "label": "originalFileName", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "mimeType", - "label": "mimeType", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "fileSize", - "label": "fileSize" - } - }, - { - "type": "field", - "attrs": { - "name": "modelMetadata", - "label": "modelMetadata", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "mediaStorageProviderMetadata", - "label": "mediaStorageProviderMetadata", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "fieldMetadata", - "label": "fieldMetadata", - "isSearchable": true - } - } - ] + 50 + ], + "enableGlobalSearch": true, + "create": false, + "edit": false, + "delete": false + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "messageType", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "messageSubType", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "modelDisplayName", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "coModelEntityId", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "user" + } } ] } }, { - "name": "media-form-view", - "displayName": "Solid Media Model", + "name": "chatterMessage-form-view", + "displayName": "Chatter Message Model", "type": "form", "context": "{}", "moduleUserKey": "solid-core", - "modelUserKey": "media", + "modelUserKey": "chatterMessage", "layout": { "type": "form", "attrs": { "name": "form-1", - "label": "Solid Media Model", - "className": "grid" + "label": "Chatter Message Model", + "className": "grid", + "showEditFormButton": false, + "showAddFormButton": false, + "showDeleteFormButton": false }, "children": [ { @@ -9446,31 +7429,55 @@ { "type": "field", "attrs": { - "name": "entityId" + "name": "messageType" } }, { "type": "field", "attrs": { - "name": "relativeUri" + "name": "messageSubType" } }, { "type": "field", "attrs": { - "name": "modelMetadata" + "name": "modelDisplayName" } }, { "type": "field", "attrs": { - "name": "mediaStorageProviderMetadata" + "name": "coModelEntityId" } }, { "type": "field", "attrs": { - "name": "fieldMetadata" + "name": "modelUserKey" + } + }, + { + "type": "field", + "attrs": { + "name": "user" + } + }, + { + "type": "field", + "attrs": { + "name": "messageBody" + } + }, + { + "type": "field", + "attrs": { + "name": "messageAttachments" + } + }, + { + "type": "field", + "attrs": { + "name": "chatterMessageDetails" } } ] @@ -9481,12 +7488,12 @@ } }, { - "name": "fieldMetadata-list-view", - "displayName": "Field Metadata", + "name": "locale-list-view", + "displayName": "Locale Model", "type": "list", "context": "{}", "moduleUserKey": "solid-core", - "modelUserKey": "fieldMetadata", + "modelUserKey": "locale", "layout": { "type": "list", "attrs": { @@ -9501,288 +7508,362 @@ "edit": true, "delete": true }, - "children": [ - { - "type": "field", - "attrs": { - "name": "name", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "displayName", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "type", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "ormType", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "model", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "defaultValue", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "regexPattern", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "regexPatternNotMatchingErrorMsg", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "required", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "unique", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "encrypt", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "encryptionType", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "decryptWhen", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "index", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "length", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "max", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "min", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "private", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "mediaTypes", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "mediaMaxSizeKb", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "mediaStorageProvider", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "relationType", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "relationCoModelSingularName", - "isSearchable": true - } - }, + "children": [ { "type": "field", "attrs": { - "name": "relationCreateInverse", + "name": "locale", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "relationCascade", + "name": "displayName", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "relationModelModuleName", - "isSearchable": true + "name": "isDefault", + "isSearchable": false } - }, + } + ] + } + }, + { + "name": "locale-form-view", + "displayName": "locale Model", + "type": "form", + "context": "{}", + "moduleUserKey": "solid-core", + "modelUserKey": "locale", + "layout": { + "type": "form", + "attrs": { + "name": "form-1", + "label": "locale model", + "className": "grid" + }, + "children": [ { - "type": "field", + "type": "sheet", "attrs": { - "name": "relationCoModelFieldName", - "isSearchable": true - } - }, + "name": "sheet-1" + }, + "children": [ + { + "type": "group", + "attrs": { + "name": "group-1", + "label": "", + "className": "col-12 sm:col-12 md:col-6 lg:col-6" + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "locale", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "displayName", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "isDefault", + "isSearchable": true + } + } + ] + } + ] + } + ] + } + }, + { + "name": "viewMetadata-list-view", + "displayName": "View Metadata list view", + "type": "list", + "context": "{}", + "moduleUserKey": "solid-core", + "modelUserKey": "viewMetadata", + "layout": { + "type": "list", + "attrs": { + "pagination": true, + "pageSizeOptions": [ + 10, + 25, + 50 + ], + "enableGlobalSearch": true, + "create": true, + "edit": true, + "delete": true + }, + "children": [ { "type": "field", "attrs": { - "name": "selectionDynamicProvider", + "name": "name", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "selectionDynamicProviderCtxt", + "name": "displayName", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "selectionStaticValues", + "name": "type", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "selectionValueType", + "name": "context", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "computedFieldValueProvider", + "name": "layout", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "computedFieldValueProviderCtxt", + "name": "module", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "computedFieldValueType", + "name": "model", "isSearchable": true } - }, + } + ] + } + }, + { + "name": "viewMetadata-form-view", + "displayName": "View Metadata form view", + "type": "form", + "context": "{}", + "moduleUserKey": "solid-core", + "modelUserKey": "viewMetadata", + "layout": { + "type": "form", + "attrs": { + "name": "form-1", + "label": "View Metadata", + "className": "grid" + }, + "children": [ { - "type": "field", + "type": "sheet", "attrs": { - "name": "uuid", - "isSearchable": true - } - }, + "name": "sheet-1" + }, + "children": [ + { + "type": "notebook", + "attrs": { + "name": "notebook-1" + }, + "children": [ + { + "type": "page", + "attrs": { + "name": "page-1", + "label": "General Info" + }, + "children": [ + { + "type": "row", + "attrs": { + "name": "page-1-row-1", + "label": "", + "className": "" + }, + "children": [ + { + "type": "column", + "attrs": { + "name": "page-1-row-1-col-1", + "label": "", + "className": "col-12 sm:col-12 md:col-6 lg:col-6" + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "name" + } + }, + { + "type": "field", + "attrs": { + "name": "displayName" + } + }, + { + "type": "field", + "attrs": { + "name": "type" + } + }, + { + "type": "field", + "attrs": { + "name": "module" + } + }, + { + "type": "field", + "attrs": { + "name": "model" + } + } + ] + }, + { + "type": "column", + "attrs": { + "name": "page-1-row-1-col-2", + "label": "", + "className": "col-12 sm:col-12 md:col-6 lg:col-6" + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "context" + } + } + ] + } + ] + } + ] + }, + { + "type": "page", + "attrs": { + "name": "page-2", + "label": "Layout" + }, + "children": [ + { + "type": "row", + "attrs": { + "name": "page-2-row-1", + "label": "", + "className": "" + }, + "children": [ + { + "type": "column", + "attrs": { + "name": "page-2-row-1-col-1", + "label": "", + "className": "col-12" + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "layout", + "height": "80vh" + } + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + }, + { + "name": "userViewMetadata-list-view", + "displayName": "User View Metadata list view", + "type": "list", + "context": "{}", + "moduleUserKey": "solid-core", + "modelUserKey": "userViewMetadata", + "layout": { + "type": "list", + "attrs": { + "pagination": true, + "pageSizeOptions": [ + 10, + 25, + 50 + ], + "enableGlobalSearch": true, + "create": true, + "edit": true, + "delete": true + }, + "children": [ { "type": "field", "attrs": { - "name": "isSystem", - "isSearchable": true + "name": "viewMetadata" } }, { "type": "field", "attrs": { - "name": "isMarkedForRemoval", - "isSearchable": true + "name": "user" } }, { "type": "field", "attrs": { - "name": "columnName", - "isSearchable": true + "name": "layout" } } ] } }, { - "name": "fieldMetadata-form-view", - "displayName": "Field Metadata", + "name": "userViewMetadata-form-view", + "displayName": "User View Metadata form view", "type": "form", "context": "{}", "moduleUserKey": "solid-core", - "modelUserKey": "fieldMetadata", + "modelUserKey": "userViewMetadata", "layout": { "type": "form", "attrs": { "name": "form-1", - "label": "Field Metadata", + "label": "User View Metadata", "className": "grid" }, "children": [ @@ -9793,240 +7874,344 @@ }, "children": [ { - "type": "group", + "type": "notebook", "attrs": { - "name": "group-1", - "label": "", - "className": "col-12 sm:col-12 md:col-6 lg:col-6" + "name": "notebook-1" }, "children": [ { - "type": "field", - "attrs": { - "name": "name" - } - }, - { - "type": "field", - "attrs": { - "name": "displayName" - } - }, - { - "type": "field", - "attrs": { - "name": "type" - } - }, - { - "type": "field", - "attrs": { - "name": "ormType" - } - }, - { - "type": "field", - "attrs": { - "name": "model" - } - }, - { - "type": "field", - "attrs": { - "name": "defaultValue" - } - }, - { - "type": "field", - "attrs": { - "name": "regexPattern" - } - }, - { - "type": "field", - "attrs": { - "name": "regexPatternNotMatchingErrorMsg" - } - }, - { - "type": "field", - "attrs": { - "name": "required" - } - }, - { - "type": "field", - "attrs": { - "name": "unique" - } - }, - { - "type": "field", - "attrs": { - "name": "encrypt" - } - }, - { - "type": "field", - "attrs": { - "name": "encryptionType" - } - }, - { - "type": "field", - "attrs": { - "name": "decryptWhen" - } - }, - { - "type": "field", - "attrs": { - "name": "index" - } - }, - { - "type": "field", - "attrs": { - "name": "length" - } - }, - { - "type": "field", - "attrs": { - "name": "max" - } - }, - { - "type": "field", - "attrs": { - "name": "min" - } - }, - { - "type": "field", - "attrs": { - "name": "private" - } - }, - { - "type": "field", - "attrs": { - "name": "mediaTypes" - } - }, - { - "type": "field", - "attrs": { - "name": "mediaMaxSizeKb" - } - }, - { - "type": "field", - "attrs": { - "name": "mediaStorageProvider" - } - }, - { - "type": "field", - "attrs": { - "name": "relationType" - } - }, - { - "type": "field", - "attrs": { - "name": "relationCoModelSingularName" - } - }, - { - "type": "field", - "attrs": { - "name": "relationCreateInverse" - } - }, - { - "type": "field", - "attrs": { - "name": "relationCascade" - } - }, - { - "type": "field", - "attrs": { - "name": "relationModelModuleName" - } - }, - { - "type": "field", - "attrs": { - "name": "relationCoModelFieldName" - } - }, - { - "type": "field", - "attrs": { - "name": "selectionDynamicProvider" - } - }, - { - "type": "field", - "attrs": { - "name": "selectionDynamicProviderCtxt" - } - }, - { - "type": "field", - "attrs": { - "name": "selectionStaticValues" - } - }, - { - "type": "field", - "attrs": { - "name": "selectionValueType" - } - }, - { - "type": "field", - "attrs": { - "name": "computedFieldValueProvider" - } - }, - { - "type": "field", - "attrs": { - "name": "computedFieldValueProviderCtxt" - } - }, - { - "type": "field", - "attrs": { - "name": "computedFieldValueType" - } - }, - { - "type": "field", + "type": "page", "attrs": { - "name": "uuid" - } + "name": "page-1", + "label": "General Info" + }, + "children": [ + { + "type": "row", + "attrs": { + "name": "page-1-row-1", + "label": "", + "className": "" + }, + "children": [ + { + "type": "column", + "attrs": { + "name": "page-1-row-1-col-1", + "label": "", + "className": "col-12 sm:col-12 md:col-6 lg:col-6" + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "viewMetadata" + } + } + ] + }, + { + "type": "column", + "attrs": { + "name": "page-1-row-1-col-2", + "label": "", + "className": "col-12 sm:col-12 md:col-6 lg:col-6" + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "user" + } + } + ] + } + ] + } + ] }, { - "type": "field", + "type": "page", "attrs": { - "name": "isSystem" - } - }, + "name": "page-2", + "label": "Layout" + }, + "children": [ + { + "type": "row", + "attrs": { + "name": "page-2-row-1", + "label": "", + "className": "" + }, + "children": [ + { + "type": "column", + "attrs": { + "name": "page-2-row-1-col-1", + "label": "", + "className": "col-12" + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "layout", + "height": "80vh" + } + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + }, + { + "name": "actionMetadata-list-view", + "displayName": "Action Metadata list view", + "type": "list", + "context": "{}", + "moduleUserKey": "solid-core", + "modelUserKey": "actionMetadata", + "layout": { + "type": "list", + "attrs": { + "pagination": true, + "pageSizeOptions": [ + 10, + 25, + 50 + ], + "enableGlobalSearch": true, + "create": true, + "edit": true, + "delete": true + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "name", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "displayName", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "type", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "module", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "model", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "view", + "isSearchable": true + } + } + ] + } + }, + { + "name": "actionMetadata-form-view", + "displayName": "Action Metadata form view", + "type": "form", + "context": "{}", + "moduleUserKey": "solid-core", + "modelUserKey": "actionMetadata", + "layout": { + "type": "form", + "attrs": { + "name": "form-1", + "label": "Action Metadata", + "className": "grid" + }, + "children": [ + { + "type": "sheet", + "attrs": { + "name": "sheet-1" + }, + "children": [ + { + "type": "notebook", + "attrs": { + "name": "notebook-1" + }, + "children": [ { - "type": "field", + "type": "page", "attrs": { - "name": "isMarkedForRemoval" - } + "name": "page-1", + "label": "General Info" + }, + "children": [ + { + "type": "row", + "attrs": { + "name": "page-1-row-1", + "label": "", + "className": "" + }, + "children": [ + { + "type": "column", + "attrs": { + "name": "page-1-row-1-col-1", + "label": "", + "className": "col-12 sm:col-12 md:col-6 lg:col-6" + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "name" + } + }, + { + "type": "field", + "attrs": { + "name": "displayName" + } + }, + { + "type": "field", + "attrs": { + "name": "type" + } + }, + { + "type": "field", + "attrs": { + "name": "customComponent" + } + }, + { + "type": "field", + "attrs": { + "name": "customIsModal" + } + }, + { + "type": "field", + "attrs": { + "name": "serverEndpoint" + } + } + ] + }, + { + "type": "column", + "attrs": { + "name": "page-1-row-1-col-2", + "label": "", + "className": "col-12 sm:col-12 md:col-6 lg:col-6" + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "module" + } + }, + { + "type": "field", + "attrs": { + "name": "model" + } + }, + { + "type": "field", + "attrs": { + "name": "view" + } + } + ] + } + ] + } + ] }, { - "type": "field", + "type": "page", "attrs": { - "name": "columnName" - } + "name": "page-2", + "label": "Layout" + }, + "children": [ + { + "type": "row", + "attrs": { + "name": "page-2-row-1", + "label": "", + "className": "" + }, + "children": [ + { + "type": "column", + "attrs": { + "name": "page-2-row-1-col-1", + "label": "", + "className": "col-12 sm:col-12 md:col-6 lg:col-6" + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "domain", + "height": "80vh" + } + } + ] + }, + { + "type": "column", + "attrs": { + "name": "page-2-row-1-col-1", + "label": "", + "className": "col-12 sm:col-12 md:col-6 lg:col-6" + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "context", + "height": "80vh" + } + } + ] + } + ] + } + ] } ] } @@ -10036,12 +8221,12 @@ } }, { - "name": "user-list-view", - "displayName": "Users", + "name": "menuItemMetadata-list-view", + "displayName": "Menu Item Metadata list view", "type": "list", "context": "{}", "moduleUserKey": "solid-core", - "modelUserKey": "user", + "modelUserKey": "menuItemMetadata", "layout": { "type": "list", "attrs": { @@ -10060,69 +8245,67 @@ { "type": "field", "attrs": { - "name": "username", - "isSearchable": true, - "widget": "SolidUserNameAvatarWidget" + "name": "name", + "isSearchable": true } }, { "type": "field", "attrs": { - "name": "fullName", + "name": "displayName", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "email", + "name": "sequenceNumber", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "mobile", + "name": "module", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "lastLoginProvider", + "name": "parentMenuItem", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "active", + "name": "action", "isSearchable": true } }, { "type": "field", "attrs": { - "label" : "Blocked / Unblocked", - "name": "failedLoginAttempts", - "viewWidget": "SolidUserBlockedStatusListWidget" + "name": "roles", + "isSearchable": true } } ] } }, { - "name": "user-form-view", - "displayName": "User", + "name": "menuItemMetadata-form-view", + "displayName": "Menu Item Metadata form view", "type": "form", "context": "{}", "moduleUserKey": "solid-core", - "modelUserKey": "user", + "modelUserKey": "menuItemMetadata", "layout": { "type": "form", "attrs": { "name": "form-1", - "label": "User", + "label": "Solid Menu Item Model", "className": "grid" }, "children": [ @@ -10143,163 +8326,100 @@ { "type": "field", "attrs": { - "name": "username" - } - }, - { - "type": "field", - "attrs": { - "name": "email" - } - }, - { - "type": "field", - "attrs": { - "name": "mobile" - } - }, - { - "type": "field", - "attrs": { - "name": "password" - } - }, - { - "type": "field", - "attrs": { - "name": "lastLoginProvider" - } - }, - { - "type": "field", - "attrs": { - "name": "accessCode" - } - }, - { - "type": "field", - "attrs": { - "name": "googleAccessToken" - } - }, - { - "type": "field", - "attrs": { - "name": "googleId" - } - }, - { - "type": "field", - "attrs": { - "name": "googleProfilePicture" - } - }, - { - "type": "field", - "attrs": { - "name": "active" - } - }, - { - "type": "field", - "attrs": { - "name": "roles" - } - }, - { - "type": "field", - "attrs": { - "name": "forgotPasswordConfirmedAt" - } - }, - { - "type": "field", - "attrs": { - "name": "verificationTokenOnForgotPassword" - } - }, - { - "type": "field", - "attrs": { - "name": "verificationTokenOnForgotPasswordExpiresAt" - } - }, - { - "type": "field", - "attrs": { - "name": "emailVerifiedOnRegistrationAt" - } - }, - { - "type": "field", - "attrs": { - "name": "emailVerificationTokenOnRegistration" - } - }, - { - "type": "field", - "attrs": { - "name": "emailVerificationTokenOnRegistrationExpiresAt" - } - }, - { - "type": "field", - "attrs": { - "name": "mobileVerifiedOnRegistrationAt" - } - }, - { - "type": "field", - "attrs": { - "name": "mobileVerificationTokenOnRegistration" - } - }, - { - "type": "field", - "attrs": { - "name": "mobileVerificationTokenOnRegistrationExpiresAt" + "name": "name" } }, { "type": "field", "attrs": { - "name": "emailVerifiedOnLoginAt" + "name": "displayName" } }, { "type": "field", "attrs": { - "name": "emailVerificationTokenOnLogin" + "name": "sequenceNumber" } }, { "type": "field", "attrs": { - "name": "emailVerificationTokenOnLoginExpiresAt" + "name": "module" } }, { "type": "field", "attrs": { - "name": "mobileVerifiedOnLoginAt" + "name": "parentMenuItem" } }, { "type": "field", "attrs": { - "name": "mobileVerificationTokenOnLogin" + "name": "action" } }, { "type": "field", "attrs": { - "name": "mobileVerificationTokenOnLoginExpiresAt" + "name": "roles", + "widget": "checkbox", + "inlineCreateAutoSave": "true", + "renderModeCheckboxPreprocessor": "MenuItemMetadataRolesFormFieldPreprocessor", + "inlineCreate": "true", + "inlineCreateLayout": { + "type": "form", + "attrs": { + "name": "form-1", + "label": "User", + "className": "grid", + "width": "20vw" + }, + "children": [ + { + "type": "sheet", + "attrs": { + "name": "sheet-1" + }, + "children": [ + { + "type": "group", + "attrs": { + "name": "group-1", + "label": "", + "className": "col-12" + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "name" + } + } + ] + } + ] + } + ] + } } - }, + } + ] + }, + { + "type": "group", + "attrs": { + "name": "group-1", + "label": "", + "className": "col-12 sm:col-12 md:col-6 lg:col-6" + }, + "children": [ { "type": "field", "attrs": { - "name": "customPayload" + "name": "iconName", + "editWidget": "SolidIconEditWidget", + "viewWidget": "SolidIconViewWidget" } } ] @@ -10310,12 +8430,12 @@ } }, { - "name": "permissionMetadata-list-view", - "displayName": "Permissions", + "name": "media-list-view", + "displayName": "Solid Media Model", "type": "list", "context": "{}", "moduleUserKey": "solid-core", - "modelUserKey": "permissionMetadata", + "modelUserKey": "media", "layout": { "type": "list", "attrs": { @@ -10326,109 +8446,161 @@ 50 ], "enableGlobalSearch": true, - "create": false, - "edit": false, - "delete": false + "create": true, + "edit": true, + "delete": true, + "allowedViews": [ + "list", + "card" + ] }, "children": [ { "type": "field", "attrs": { - "name": "name", + "name": "entityId", "isSearchable": true } - } - ] - } - }, - { - "name": "permissionMetadata-form-view", - "displayName": "Permission", - "type": "form", - "context": "{}", - "moduleUserKey": "solid-core", - "modelUserKey": "permissionMetadata", - "layout": { - "type": "form", - "attrs": { - "name": "form-1", - "label": "Permission", - "className": "grid", - "disabled": true, - "readonly": true - }, - "children": [ + }, { - "type": "sheet", + "type": "field", "attrs": { - "name": "sheet-1" - }, - "children": [ - { - "type": "group", - "attrs": { - "name": "group-1", - "label": "", - "className": "col-12 sm:col-12 md:col-6 lg:col-6" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "name" - } - } - ] - } - ] + "name": "relativeUri", + "widget": "image", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "modelMetadata", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "mediaStorageProviderMetadata", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "fieldMetadata", + "isSearchable": true + } } ] } }, { - "name": "roleMetadata-list-view", - "displayName": "Roles", - "type": "list", + "name": "media-card-view", + "displayName": "Media Card", + "type": "card", "context": "{}", "moduleUserKey": "solid-core", - "modelUserKey": "roleMetadata", + "modelUserKey": "media", "layout": { - "type": "list", + "type": "card", "attrs": { "pagination": true, + "pageSize": 24, "pageSizeOptions": [ - 10, - 25, - 50 + 12, + 24, + 48 ], "enableGlobalSearch": true, "create": true, "edit": true, - "delete": true + "delete": true, + "allowedViews": [ + "list", + "card" + ] }, "children": [ { - "type": "field", + "type": "card", "attrs": { - "name": "name", - "isSearchable": true - } + "name": "Card", + "cardWidget": "MediaCardWidget" + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "relativeUri", + "label": "relativeUri", + "widget": "image", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "originalFileName", + "label": "originalFileName", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "mimeType", + "label": "mimeType", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "fileSize", + "label": "fileSize" + } + }, + { + "type": "field", + "attrs": { + "name": "modelMetadata", + "label": "modelMetadata", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "mediaStorageProviderMetadata", + "label": "mediaStorageProviderMetadata", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "fieldMetadata", + "label": "fieldMetadata", + "isSearchable": true + } + } + ] } ] } }, { - "name": "roleMetadata-form-view", - "displayName": "Role", + "name": "media-form-view", + "displayName": "Solid Media Model", "type": "form", "context": "{}", "moduleUserKey": "solid-core", - "modelUserKey": "roleMetadata", + "modelUserKey": "media", "layout": { "type": "form", "attrs": { "name": "form-1", - "label": "User", + "label": "Solid Media Model", "className": "grid" }, "children": [ @@ -10439,117 +8611,42 @@ }, "children": [ { - "type": "notebook", + "type": "group", "attrs": { - "name": "notebook-1" - }, - "children": [ - { - "type": "page", - "attrs": { - "name": "page-1", - "label": "Name" - }, - "children": [ - { - "type": "group", - "attrs": { - "name": "group-1", - "label": "", - "className": "col-12 sm:col-12 md:col-6 lg:col-6" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "name", - "showLabel": false - } - } - ] - } - ] + "name": "group-1", + "label": "", + "className": "col-12 sm:col-12 md:col-6 lg:col-6" + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "entityId" + } }, { - "type": "page", + "type": "field", "attrs": { - "name": "page-2", - "label": "Permissions" - }, - "children": [ - { - "type": "group", - "attrs": { - "name": "group-1", - "label": "", - "className": "col-12" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "permissions", - "editWidget": "inputSwitch", - "showLabel": false - } - } - ] - } - ] + "name": "relativeUri" + } }, { - "type": "page", + "type": "field", "attrs": { - "name": "page-3", - "label": "Users" - }, - "children": [ - { - "type": "group", - "attrs": { - "name": "group-1", - "label": "", - "className": "col-12 sm:col-12 md:col-12 lg:col-12" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "users", - "editWidget": "DefaultRelationManyToManyCheckBoxFormEditWidget", - "showLabel": false - } - } - ] - } - ] + "name": "modelMetadata" + } }, { - "type": "page", + "type": "field", "attrs": { - "name": "page-4", - "label": "Menu Items" - }, - "children": [ - { - "type": "group", - "attrs": { - "name": "group-1", - "label": "", - "className": "col-12 sm:col-12 md:col-12 lg:col-12" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "menuItems", - "editWidget": "DefaultRelationManyToManyCheckBoxFormEditWidget", - "showLabel": false - } - } - ] - } - ] + "name": "mediaStorageProviderMetadata" + } + }, + { + "type": "field", + "attrs": { + "name": "fieldMetadata" + } } ] } @@ -10559,12 +8656,12 @@ } }, { - "name": "mqMessage-list-view", - "displayName": "Messages", + "name": "fieldMetadata-list-view", + "displayName": "Field Metadata", "type": "list", "context": "{}", "moduleUserKey": "solid-core", - "modelUserKey": "mqMessage", + "modelUserKey": "fieldMetadata", "layout": { "type": "list", "attrs": { @@ -10575,161 +8672,274 @@ 50 ], "enableGlobalSearch": true, - "create": false, - "edit": false, - "delete": false + "create": true, + "edit": true, + "delete": true }, "children": [ { "type": "field", "attrs": { - "name": "messageId", + "name": "name", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "messageBroker", + "name": "displayName", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "stage", + "name": "type", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "startedAt", + "name": "ormType", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "finishedAt", + "name": "model", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "defaultValue", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "regexPattern", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "regexPatternNotMatchingErrorMsg", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "required", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "unique", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "encrypt", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "encryptionType", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "decryptWhen", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "index", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "length", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "max", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "min", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "private", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "mediaTypes", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "mediaMaxSizeKb", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "mediaStorageProvider", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "relationType", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "relationCoModelSingularName", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "relationCreateInverse", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "relationCascade", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "elapsedMillis", + "name": "relationModelModuleName", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "parentEntityId", + "name": "relationCoModelFieldName", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "parentEntity", + "name": "selectionDynamicProvider", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "mqMessageQueue", - "label": "Queue", + "name": "selectionDynamicProviderCtxt", "isSearchable": true } - } - ] - } - }, - { - "name": "mqMessage-tree-view", - "displayName": "Messages Tree View", - "type": "tree", - "context": "{}", - "moduleUserKey": "solid-core", - "modelUserKey": "mqMessage", - "layout": { - "type": "tree", - "attrs": { - "pagination": true, - "pageSizeOptions": [ - 10, - 25, - 50 - ], - "enableGlobalSearch": true, - "create": false, - "edit": false, - "delete": false - }, - "children": [ + }, { "type": "field", "attrs": { - "name": "messageId", + "name": "selectionStaticValues", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "messageBroker", + "name": "selectionValueType", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "stage", + "name": "computedFieldValueProvider", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "startedAt", + "name": "computedFieldValueProviderCtxt", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "finishedAt", + "name": "computedFieldValueType", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "elapsedMillis", + "name": "uuid", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "parentEntityId", + "name": "isSystem", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "parentEntity", + "name": "isMarkedForRemoval", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "mqMessageQueue", - "label": "Queue", + "name": "columnName", "isSearchable": true } } @@ -10737,147 +8947,18 @@ } }, { - "name": "mqMessage-kanban-view", - "displayName": "Messages Kanban", - "type": "kanban", - "context": "{}", - "moduleUserKey": "solid-core", - "modelUserKey": "mqMessage", - "layout": { - "type": "kanban", - "attrs": { - "swimlanesCount": 5, - "recordsInSwimlane": 10, - "enableGlobalSearch": true, - "create": true, - "edit": true, - "delete": true, - "groupBy": "stage", - "draggable": false, - "allowedViews": [ - "list", - "kanban" - ] - }, - "children": [ - { - "type": "card", - "attrs": { - "name": "Card", - "cardWidget": "MqMessageKanbanCardWidget" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "messageId", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "messageBroker", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "messageType", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "stage", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "startedAt", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "finishedAt", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "elapsedMillis", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "retryCount" - } - }, - { - "type": "field", - "attrs": { - "name": "retryInterval" - } - }, - { - "type": "field", - "attrs": { - "name": "parentEntityId", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "parentEntity", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "error" - } - }, - { - "type": "field", - "attrs": { - "name": "mqMessageQueue", - "label": "Queue", - "isSearchable": true - } - } - ] - } - ] - } - }, - { - "name": "mqMessage-form-view", - "displayName": "Mq Message", + "name": "fieldMetadata-form-view", + "displayName": "Field Metadata", "type": "form", "context": "{}", "moduleUserKey": "solid-core", - "modelUserKey": "mqMessage", + "modelUserKey": "fieldMetadata", "layout": { "type": "form", "attrs": { "name": "form-1", - "label": "Mq Message", - "className": "grid", - "workflowField": "stage", - "workflowFieldUpdateEnabled": true, - "disabled": true, - "readonly": true + "label": "Field Metadata", + "className": "grid" }, "children": [ { @@ -10887,432 +8968,240 @@ }, "children": [ { - "type": "notebook", + "type": "group", "attrs": { - "name": "notebook-1" + "name": "group-1", + "label": "", + "className": "col-12 sm:col-12 md:col-6 lg:col-6" }, "children": [ { - "type": "page", + "type": "field", + "attrs": { + "name": "name" + } + }, + { + "type": "field", + "attrs": { + "name": "displayName" + } + }, + { + "type": "field", + "attrs": { + "name": "type" + } + }, + { + "type": "field", + "attrs": { + "name": "ormType" + } + }, + { + "type": "field", + "attrs": { + "name": "model" + } + }, + { + "type": "field", + "attrs": { + "name": "defaultValue" + } + }, + { + "type": "field", + "attrs": { + "name": "regexPattern" + } + }, + { + "type": "field", + "attrs": { + "name": "regexPatternNotMatchingErrorMsg" + } + }, + { + "type": "field", + "attrs": { + "name": "required" + } + }, + { + "type": "field", + "attrs": { + "name": "unique" + } + }, + { + "type": "field", "attrs": { - "name": "general-info", - "label": "General Info" - }, - "children": [ - { - "type": "row", - "attrs": { - "name": "row-1", - "className": "" - }, - "children": [ - { - "type": "column", - "attrs": { - "name": "col-1", - "className": "col-12" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "messageType", - "label": "Message Type" - } - }, - { - "type": "field", - "attrs": { - "name": "messageId", - "label": "Message Id" - } - }, - { - "type": "field", - "attrs": { - "name": "messageBroker", - "label": "Message Broker" - } - }, - { - "type": "field", - "attrs": { - "name": "retryCount", - "label": "Retry Count" - } - }, - { - "type": "field", - "attrs": { - "name": "retryInterval", - "label": "Retry Interval" - } - }, - { - "type": "field", - "attrs": { - "name": "startedAt", - "label": "Started At" - } - }, - { - "type": "field", - "attrs": { - "name": "finishedAt", - "label": "Finished At" - } - }, - { - "type": "field", - "attrs": { - "name": "elapsedMillis", - "label": "Elapsed Millis" - } - }, - { - "type": "field", - "attrs": { - "name": "parentEntityId", - "label": "Parent Entity Id" - } - }, - { - "type": "field", - "attrs": { - "name": "parentEntity", - "label": "Parent Entity" - } - }, - { - "type": "field", - "attrs": { - "name": "mqMessageQueue", - "label": "Message Queue" - } - } - ] - } - ] - } - ] + "name": "encrypt" + } }, { - "type": "page", + "type": "field", "attrs": { - "name": "input-tab", - "label": "Input" - }, - "children": [ - { - "type": "row", - "attrs": { - "name": "input-row" - }, - "children": [ - { - "type": "column", - "attrs": { - "name": "input-col", - "className": "col-12" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "input", - "label": "Input" - } - } - ] - } - ] - } - ] + "name": "encryptionType" + } }, { - "type": "page", + "type": "field", "attrs": { - "name": "output-tab", - "label": "Output" - }, - "children": [ - { - "type": "row", - "attrs": { - "name": "output-row" - }, - "children": [ - { - "type": "column", - "attrs": { - "name": "output-col", - "className": "col-12" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "output", - "label": "Output" - } - } - ] - } - ] - } - ] + "name": "decryptWhen" + } + }, + { + "type": "field", + "attrs": { + "name": "index" + } + }, + { + "type": "field", + "attrs": { + "name": "length" + } + }, + { + "type": "field", + "attrs": { + "name": "max" + } + }, + { + "type": "field", + "attrs": { + "name": "min" + } + }, + { + "type": "field", + "attrs": { + "name": "private" + } + }, + { + "type": "field", + "attrs": { + "name": "mediaTypes" + } + }, + { + "type": "field", + "attrs": { + "name": "mediaMaxSizeKb" + } + }, + { + "type": "field", + "attrs": { + "name": "mediaStorageProvider" + } + }, + { + "type": "field", + "attrs": { + "name": "relationType" + } + }, + { + "type": "field", + "attrs": { + "name": "relationCoModelSingularName" + } + }, + { + "type": "field", + "attrs": { + "name": "relationCreateInverse" + } + }, + { + "type": "field", + "attrs": { + "name": "relationCascade" + } + }, + { + "type": "field", + "attrs": { + "name": "relationModelModuleName" + } + }, + { + "type": "field", + "attrs": { + "name": "relationCoModelFieldName" + } }, { - "type": "page", + "type": "field", "attrs": { - "name": "error-tab", - "label": "Error" - }, - "children": [ - { - "type": "row", - "attrs": { - "name": "error-row" - }, - "children": [ - { - "type": "column", - "attrs": { - "name": "error-col", - "className": "col-12" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "error", - "label": "Error" - } - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - }, - { - "name": "mqMessageQueue-list-view", - "displayName": "Message Queues", - "type": "list", - "context": "{}", - "moduleUserKey": "solid-core", - "modelUserKey": "mqMessageQueue", - "layout": { - "type": "list", - "attrs": { - "pagination": true, - "pageSizeOptions": [ - 10, - 25, - 50 - ], - "enableGlobalSearch": true, - "create": false, - "edit": false, - "delete": false - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "name", - "label": "Queue Name", - "isSearchable": true - } - } - ] - } - }, - { - "name": "mqMessageQueue-form-view", - "displayName": "Mq Message Queue", - "type": "form", - "context": "{}", - "moduleUserKey": "solid-core", - "modelUserKey": "mqMessageQueue", - "layout": { - "type": "form", - "attrs": { - "name": "form-1", - "label": "Mq Message Queue", - "className": "grid", - "disabled": false, - "readonly": true - }, - "children": [ - { - "type": "sheet", - "attrs": { - "name": "sheet-1" - }, - "children": [ - { - "type": "notebook", - "attrs": { - "name": "notebook-1" - }, - "children": [ + "name": "selectionDynamicProvider" + } + }, { - "type": "page", + "type": "field", "attrs": { - "name": "page-general-info", - "label": "General Info" - }, - "children": [ - { - "type": "row", - "attrs": { - "name": "row-1" - }, - "children": [ - { - "type": "column", - "attrs": { - "name": "col-1", - "className": "col-12 sm:col-12 md:col-6 lg:col-6" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "name", - "label": "Queue Name" - } - } - ] - }, - { - "type": "column", - "attrs": { - "name": "col-2", - "className": "col-12 sm:col-12 md:col-6 lg:col-6" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "description", - "label": "Queue Description" - } - } - ] - } - ] - } - ] + "name": "selectionDynamicProviderCtxt" + } }, { - "type": "page", - "attrs": { - "name": "page-messages", - "label": "Messages" - }, - "children": [ - { - "type": "row", - "attrs": { - "name": "row-2" - }, - "children": [ - { - "type": "column", - "attrs": { - "name": "col-1", - "className": "col-12" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "mqMessages", - "label": "mqMessages", - "inlineCreate": "false", - "showLabel": false, - "inlineListLayout": { - "type": "list", - "attrs": { - "pagination": true, - "pageSizeOptions": [ - 10, - 25, - 50 - ], - "enableGlobalSearch": true, - "create": false, - "edit": false, - "delete": false - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "messageId", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "stage", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "startedAt", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "finishedAt", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "elapsedMillis", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "parentEntityId", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "parentEntity", - "isSearchable": true - } - } - ] - } - } - } - ] - } - ] - } - ] + "type": "field", + "attrs": { + "name": "selectionStaticValues" + } + }, + { + "type": "field", + "attrs": { + "name": "selectionValueType" + } + }, + { + "type": "field", + "attrs": { + "name": "computedFieldValueProvider" + } + }, + { + "type": "field", + "attrs": { + "name": "computedFieldValueProviderCtxt" + } + }, + { + "type": "field", + "attrs": { + "name": "computedFieldValueType" + } + }, + { + "type": "field", + "attrs": { + "name": "uuid" + } + }, + { + "type": "field", + "attrs": { + "name": "isSystem" + } + }, + { + "type": "field", + "attrs": { + "name": "isMarkedForRemoval" + } + }, + { + "type": "field", + "attrs": { + "name": "columnName" + } } ] } @@ -11322,12 +9211,12 @@ } }, { - "name": "scheduledJob-list-view", - "displayName": "Scheduled Job", + "name": "user-list-view", + "displayName": "Users", "type": "list", "context": "{}", "moduleUserKey": "solid-core", - "modelUserKey": "scheduledJob", + "modelUserKey": "user", "layout": { "type": "list", "attrs": { @@ -11346,277 +9235,323 @@ { "type": "field", "attrs": { - "name": "id" + "name": "username", + "isSearchable": true, + "widget": "SolidUserNameAvatarWidget" } }, { "type": "field", "attrs": { - "name": "scheduleName", + "name": "fullName", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "module", + "name": "email", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "isActive" - } - }, - { - "type": "field", - "attrs": { - "name": "frequency", + "name": "mobile", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "startTime" - } - }, - { - "type": "field", - "attrs": { - "name": "endTime" - } - }, - { - "type": "field", - "attrs": { - "name": "startDate" - } - }, - { - "type": "field", - "attrs": { - "name": "endDate" - } - }, - { - "type": "field", - "attrs": { - "name": "dayOfMonth" - } - }, - { - "type": "field", - "attrs": { - "name": "lastRunAt" + "name": "lastLoginProvider", + "isSearchable": true } }, { "type": "field", "attrs": { - "name": "nextRunAt" + "name": "active", + "isSearchable": true } }, { "type": "field", "attrs": { - "name": "dayOfWeek" + "label": "Blocked / Unblocked", + "name": "failedLoginAttempts", + "viewWidget": "SolidUserBlockedStatusListWidget" } - }, + } + ] + } + }, + { + "name": "user-form-view", + "displayName": "User", + "type": "form", + "context": "{}", + "moduleUserKey": "solid-core", + "modelUserKey": "user", + "layout": { + "type": "form", + "attrs": { + "name": "form-1", + "label": "User", + "className": "grid" + }, + "children": [ { - "type": "field", + "type": "sheet", "attrs": { - "name": "job", - "sortable": true, - "filterable": true - } + "name": "sheet-1" + }, + "children": [ + { + "type": "group", + "attrs": { + "name": "group-1", + "label": "", + "className": "col-12 sm:col-12 md:col-6 lg:col-6" + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "username" + } + }, + { + "type": "field", + "attrs": { + "name": "email" + } + }, + { + "type": "field", + "attrs": { + "name": "mobile" + } + }, + { + "type": "field", + "attrs": { + "name": "password" + } + }, + { + "type": "field", + "attrs": { + "name": "lastLoginProvider" + } + }, + { + "type": "field", + "attrs": { + "name": "accessCode" + } + }, + { + "type": "field", + "attrs": { + "name": "googleAccessToken" + } + }, + { + "type": "field", + "attrs": { + "name": "googleId" + } + }, + { + "type": "field", + "attrs": { + "name": "googleProfilePicture" + } + }, + { + "type": "field", + "attrs": { + "name": "active" + } + }, + { + "type": "field", + "attrs": { + "name": "roles" + } + }, + { + "type": "field", + "attrs": { + "name": "forgotPasswordConfirmedAt" + } + }, + { + "type": "field", + "attrs": { + "name": "verificationTokenOnForgotPassword" + } + }, + { + "type": "field", + "attrs": { + "name": "verificationTokenOnForgotPasswordExpiresAt" + } + }, + { + "type": "field", + "attrs": { + "name": "emailVerifiedOnRegistrationAt" + } + }, + { + "type": "field", + "attrs": { + "name": "emailVerificationTokenOnRegistration" + } + }, + { + "type": "field", + "attrs": { + "name": "emailVerificationTokenOnRegistrationExpiresAt" + } + }, + { + "type": "field", + "attrs": { + "name": "mobileVerifiedOnRegistrationAt" + } + }, + { + "type": "field", + "attrs": { + "name": "mobileVerificationTokenOnRegistration" + } + }, + { + "type": "field", + "attrs": { + "name": "mobileVerificationTokenOnRegistrationExpiresAt" + } + }, + { + "type": "field", + "attrs": { + "name": "emailVerifiedOnLoginAt" + } + }, + { + "type": "field", + "attrs": { + "name": "emailVerificationTokenOnLogin" + } + }, + { + "type": "field", + "attrs": { + "name": "emailVerificationTokenOnLoginExpiresAt" + } + }, + { + "type": "field", + "attrs": { + "name": "mobileVerifiedOnLoginAt" + } + }, + { + "type": "field", + "attrs": { + "name": "mobileVerificationTokenOnLogin" + } + }, + { + "type": "field", + "attrs": { + "name": "mobileVerificationTokenOnLoginExpiresAt" + } + }, + { + "type": "field", + "attrs": { + "name": "customPayload" + } + } + ] + } + ] } ] } }, { - "name": "scheduledJob-form-view", - "displayName": "Scheduled Job", - "type": "form", + "name": "permissionMetadata-list-view", + "displayName": "Permissions", + "type": "list", "context": "{}", "moduleUserKey": "solid-core", - "modelUserKey": "scheduledJob", + "modelUserKey": "permissionMetadata", "layout": { - "type": "form", + "type": "list", "attrs": { - "name": "form-1", - "label": "Scheduled Job", - "className": "grid" + "pagination": true, + "pageSizeOptions": [ + 10, + 25, + 50 + ], + "enableGlobalSearch": true, + "create": false, + "edit": false, + "delete": false }, - "onFieldChange": "scheduleFrequencyOnFieldChangeHandler", "children": [ { - "type": "sheet", + "type": "field", "attrs": { - "name": "sheet-1" - }, - "children": [ - { - "type": "notebook", - "attrs": { - "name": "notebook-1" - }, - "children": [ - { - "type": "page", - "attrs": { - "name": "page-general", - "label": "General" - }, - "children": [ - { - "type": "row", - "attrs": { - "name": "row-general" - }, - "children": [ - { - "type": "column", - "attrs": { - "name": "group-basic", - "label": "Basic", - "className": "col-12 sm:col-12 md:col-6 lg:col-6" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "scheduleName" - } - }, - { - "type": "field", - "attrs": { - "name": "job" - } - }, - { - "type": "field", - "attrs": { - "name": "module" - } - }, - { - "type": "field", - "attrs": { - "name": "isActive" - } - } - ] - }, - { - "type": "column", - "attrs": { - "name": "group-config", - "label": "Configuration", - "className": "col-12 sm:col-12 md:col-6 lg:col-6" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "frequency" - } - }, - { - "type": "field", - "attrs": { - "name": "cronExpression", - "visible": false - } - }, - { - "type": "field", - "attrs": { - "name": "startTime" - } - }, - { - "type": "field", - "attrs": { - "name": "endTime" - } - }, - { - "type": "field", - "attrs": { - "name": "startDate" - } - }, - { - "type": "field", - "attrs": { - "name": "endDate" - } - }, - { - "type": "field", - "attrs": { - "name": "dayOfWeek", - "visible": false - } - }, - { - "type": "field", - "attrs": { - "name": "dayOfMonth", - "visible": false - } - } - ] - } - ] - } - ] - }, - { - "type": "page", - "attrs": { - "name": "page-runtime", - "label": "Runtime" - }, - "children": [ - { - "type": "row", - "attrs": { - "name": "row-runtime" - }, - "children": [ - { - "type": "column", - "attrs": { - "name": "group-runtime", - "label": "Execution", - "className": "col-12" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "lastRunAt", - "disabled": true, - "readOnly": true - } - }, - { - "type": "field", - "attrs": { - "name": "nextRunAt", - "disabled": true, - "readOnly": true - } - } - ] - } - ] - } - ] + "name": "name", + "isSearchable": true + } + } + ] + } + }, + { + "name": "permissionMetadata-form-view", + "displayName": "Permission", + "type": "form", + "context": "{}", + "moduleUserKey": "solid-core", + "modelUserKey": "permissionMetadata", + "layout": { + "type": "form", + "attrs": { + "name": "form-1", + "label": "Permission", + "className": "grid", + "disabled": true, + "readonly": true + }, + "children": [ + { + "type": "sheet", + "attrs": { + "name": "sheet-1" + }, + "children": [ + { + "type": "group", + "attrs": { + "name": "group-1", + "label": "", + "className": "col-12 sm:col-12 md:col-6 lg:col-6" + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "name" + } } ] } @@ -11626,12 +9561,12 @@ } }, { - "name": "settings-list-view", - "displayName": "Settings", + "name": "roleMetadata-list-view", + "displayName": "Roles", "type": "list", "context": "{}", "moduleUserKey": "solid-core", - "modelUserKey": "setting", + "modelUserKey": "roleMetadata", "layout": { "type": "list", "attrs": { @@ -11644,14 +9579,13 @@ "enableGlobalSearch": true, "create": true, "edit": true, - "delete": false + "delete": true }, "children": [ { "type": "field", "attrs": { - "name": "appTitle", - "label": "App Name", + "name": "name", "isSearchable": true } } @@ -11659,17 +9593,17 @@ } }, { - "name": "settings-form-view", - "displayName": "Settings", + "name": "roleMetadata-form-view", + "displayName": "Role", "type": "form", "context": "{}", "moduleUserKey": "solid-core", - "modelUserKey": "setting", + "modelUserKey": "roleMetadata", "layout": { "type": "form", "attrs": { "name": "form-1", - "label": "Settings", + "label": "User", "className": "grid" }, "children": [ @@ -11680,117 +9614,117 @@ }, "children": [ { - "type": "group", + "type": "notebook", "attrs": { - "name": "group-1", - "label": "", - "className": "col-12 sm:col-12 md:col-6 lg:col-6" + "name": "notebook-1" }, "children": [ { - "type": "field", - "attrs": { - "name": "authPagesLayout", - "label": "Auth Pages Layout" - } - }, - { - "type": "field", - "attrs": { - "name": "appTitle", - "label": "App Title" - } - }, - { - "type": "field", - "attrs": { - "name": "appLogo", - "label": "App Logo" - } - }, - { - "type": "field", - "attrs": { - "name": "appDescription", - "label": "App Description" - } - }, - { - "type": "field", - "attrs": { - "name": "appTnc", - "label": "App TNC" - } - }, - { - "type": "field", - "attrs": { - "name": "appPrivacyPolicy", - "label": "App Privacy Policy" - } - }, - { - "type": "field", - "attrs": { - "name": "iamAllowPublicRegistration", - "label": "Iam Allow Public Registration" - } - }, - { - "type": "field", - "attrs": { - "name": "forceChangePasswordOnFirstLogin", - "label": "Force Password Change On First Login" - } - }, - { - "type": "field", - "attrs": { - "name": "iamPasswordRegistrationEnabled", - "label": "Iam Password Registration Enabled" - } - }, - { - "type": "field", - "attrs": { - "name": "iamPasswordLessRegistrationEnabled", - "label": "Iam Password Less Registration enabled" - } - }, - { - "type": "field", - "attrs": { - "name": "iamActivateUserOnRegistration", - "label": "Iam Activate User On Registration" - } - }, - { - "type": "field", + "type": "page", "attrs": { - "name": "iamDefaultRole", - "label": "Iam Default Role" - } + "name": "page-1", + "label": "Name" + }, + "children": [ + { + "type": "group", + "attrs": { + "name": "group-1", + "label": "", + "className": "col-12 sm:col-12 md:col-6 lg:col-6" + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "name", + "showLabel": false + } + } + ] + } + ] }, { - "type": "field", + "type": "page", "attrs": { - "name": "iamGoogleOAuthEnabled", - "label": "Iam Google OAuth Enabled" - } + "name": "page-2", + "label": "Permissions" + }, + "children": [ + { + "type": "group", + "attrs": { + "name": "group-1", + "label": "", + "className": "col-12" + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "permissions", + "editWidget": "inputSwitch", + "showLabel": false + } + } + ] + } + ] }, { - "type": "field", + "type": "page", "attrs": { - "name": "shouldQueueEmails", - "label": "Should Queue Emails" - } + "name": "page-3", + "label": "Users" + }, + "children": [ + { + "type": "group", + "attrs": { + "name": "group-1", + "label": "", + "className": "col-12 sm:col-12 md:col-12 lg:col-12" + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "users", + "editWidget": "DefaultRelationManyToManyCheckBoxFormEditWidget", + "showLabel": false + } + } + ] + } + ] }, { - "type": "field", + "type": "page", "attrs": { - "name": "shouldQueueSms", - "label": "Should Queue SMS" - } + "name": "page-4", + "label": "Menu Items" + }, + "children": [ + { + "type": "group", + "attrs": { + "name": "group-1", + "label": "", + "className": "col-12 sm:col-12 md:col-12 lg:col-12" + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "menuItems", + "editWidget": "DefaultRelationManyToManyCheckBoxFormEditWidget", + "showLabel": false + } + } + ] + } + ] } ] } @@ -11800,14 +9734,103 @@ } }, { - "name": "securityRule-list-view", - "displayName": "Security rules", - "type": "list", + "name": "mqMessage-list-view", + "displayName": "Messages", + "type": "list", + "context": "{}", + "moduleUserKey": "solid-core", + "modelUserKey": "mqMessage", + "layout": { + "type": "list", + "attrs": { + "pagination": true, + "pageSizeOptions": [ + 10, + 25, + 50 + ], + "enableGlobalSearch": true, + "create": false, + "edit": false, + "delete": false + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "messageId", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "messageBroker", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "stage", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "startedAt", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "finishedAt", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "elapsedMillis", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "parentEntityId", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "parentEntity", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "mqMessageQueue", + "label": "Queue", + "isSearchable": true + } + } + ] + } + }, + { + "name": "mqMessage-tree-view", + "displayName": "Messages Tree View", + "type": "tree", "context": "{}", "moduleUserKey": "solid-core", - "modelUserKey": "securityRule", + "modelUserKey": "mqMessage", "layout": { - "type": "list", + "type": "tree", "attrs": { "pagination": true, "pageSizeOptions": [ @@ -11816,37 +9839,72 @@ 50 ], "enableGlobalSearch": true, - "create": true, - "edit": true, - "delete": true + "create": false, + "edit": false, + "delete": false }, "children": [ { "type": "field", "attrs": { - "name": "id", - "label": "Id", - "sortable": true, - "filterable": true + "name": "messageId", + "isSearchable": true } }, { "type": "field", "attrs": { - "name": "name", - "label": "Name", - "sortable": true, - "filterable": true, + "name": "messageBroker", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "description", - "label": "Description", - "sortable": true, - "filterable": true, + "name": "stage", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "startedAt", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "finishedAt", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "elapsedMillis", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "parentEntityId", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "parentEntity", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "mqMessageQueue", + "label": "Queue", "isSearchable": true } } @@ -11854,18 +9912,147 @@ } }, { - "name": "securityRule-form-view", - "displayName": "Security rules", + "name": "mqMessage-kanban-view", + "displayName": "Messages Kanban", + "type": "kanban", + "context": "{}", + "moduleUserKey": "solid-core", + "modelUserKey": "mqMessage", + "layout": { + "type": "kanban", + "attrs": { + "swimlanesCount": 5, + "recordsInSwimlane": 10, + "enableGlobalSearch": true, + "create": true, + "edit": true, + "delete": true, + "groupBy": "stage", + "draggable": false, + "allowedViews": [ + "list", + "kanban" + ] + }, + "children": [ + { + "type": "card", + "attrs": { + "name": "Card", + "cardWidget": "MqMessageKanbanCardWidget" + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "messageId", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "messageBroker", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "messageType", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "stage", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "startedAt", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "finishedAt", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "elapsedMillis", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "retryCount" + } + }, + { + "type": "field", + "attrs": { + "name": "retryInterval" + } + }, + { + "type": "field", + "attrs": { + "name": "parentEntityId", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "parentEntity", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "error" + } + }, + { + "type": "field", + "attrs": { + "name": "mqMessageQueue", + "label": "Queue", + "isSearchable": true + } + } + ] + } + ] + } + }, + { + "name": "mqMessage-form-view", + "displayName": "Mq Message", "type": "form", "context": "{}", "moduleUserKey": "solid-core", - "modelUserKey": "securityRule", + "modelUserKey": "mqMessage", "layout": { "type": "form", "attrs": { "name": "form-1", - "label": "Security rules", - "className": "grid" + "label": "Mq Message", + "className": "grid", + "workflowField": "stage", + "workflowFieldUpdateEnabled": true, + "disabled": true, + "readonly": true }, "children": [ { @@ -11875,60 +10062,211 @@ }, "children": [ { - "type": "row", + "type": "notebook", "attrs": { - "name": "group-1", - "label": "", - "className": "" + "name": "notebook-1" }, "children": [ { - "type": "column", + "type": "page", "attrs": { - "name": "group-1", - "label": "", - "className": "col-12 sm:col-12 md:col-6 lg:col-6" + "name": "general-info", + "label": "General Info" }, "children": [ { - "type": "field", - "attrs": { - "name": "name" - } - }, - { - "type": "field", + "type": "row", "attrs": { - "name": "description" - } - }, + "name": "row-1", + "className": "" + }, + "children": [ + { + "type": "column", + "attrs": { + "name": "col-1", + "className": "col-12" + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "messageType", + "label": "Message Type" + } + }, + { + "type": "field", + "attrs": { + "name": "messageId", + "label": "Message Id" + } + }, + { + "type": "field", + "attrs": { + "name": "messageBroker", + "label": "Message Broker" + } + }, + { + "type": "field", + "attrs": { + "name": "retryCount", + "label": "Retry Count" + } + }, + { + "type": "field", + "attrs": { + "name": "retryInterval", + "label": "Retry Interval" + } + }, + { + "type": "field", + "attrs": { + "name": "startedAt", + "label": "Started At" + } + }, + { + "type": "field", + "attrs": { + "name": "finishedAt", + "label": "Finished At" + } + }, + { + "type": "field", + "attrs": { + "name": "elapsedMillis", + "label": "Elapsed Millis" + } + }, + { + "type": "field", + "attrs": { + "name": "parentEntityId", + "label": "Parent Entity Id" + } + }, + { + "type": "field", + "attrs": { + "name": "parentEntity", + "label": "Parent Entity" + } + }, + { + "type": "field", + "attrs": { + "name": "mqMessageQueue", + "label": "Message Queue" + } + } + ] + } + ] + } + ] + }, + { + "type": "page", + "attrs": { + "name": "input-tab", + "label": "Input" + }, + "children": [ { - "type": "field", + "type": "row", "attrs": { - "name": "securityRuleConfig" - } + "name": "input-row" + }, + "children": [ + { + "type": "column", + "attrs": { + "name": "input-col", + "className": "col-12" + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "input", + "label": "Input" + } + } + ] + } + ] } ] }, { - "type": "column", + "type": "page", "attrs": { - "name": "group-2", - "label": "", - "className": "col-12 sm:col-12 md:col-6 lg:col-6" + "name": "output-tab", + "label": "Output" }, "children": [ { - "type": "field", + "type": "row", "attrs": { - "name": "role" - } - }, + "name": "output-row" + }, + "children": [ + { + "type": "column", + "attrs": { + "name": "output-col", + "className": "col-12" + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "output", + "label": "Output" + } + } + ] + } + ] + } + ] + }, + { + "type": "page", + "attrs": { + "name": "error-tab", + "label": "Error" + }, + "children": [ { - "type": "field", + "type": "row", "attrs": { - "name": "modelMetadata" - } + "name": "error-row" + }, + "children": [ + { + "type": "column", + "attrs": { + "name": "error-col", + "className": "col-12" + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "error", + "label": "Error" + } + } + ] + } + ] } ] } @@ -11939,13 +10277,13 @@ ] } }, - { - "name": "emailTemplate-list-view", - "displayName": "Email", + { + "name": "mqMessageQueue-list-view", + "displayName": "Message Queues", "type": "list", "context": "{}", "moduleUserKey": "solid-core", - "modelUserKey": "emailTemplate", + "modelUserKey": "mqMessageQueue", "layout": { "type": "list", "attrs": { @@ -11956,8 +10294,8 @@ 50 ], "enableGlobalSearch": true, - "create": true, - "edit": true, + "create": false, + "edit": false, "delete": false }, "children": [ @@ -11965,32 +10303,29 @@ "type": "field", "attrs": { "name": "name", - "label": "Email", - "sortable": true, - "filterable": true + "label": "Queue Name", + "isSearchable": true } } ] } }, { - "name": "emailTemplate-form-view", - "displayName": "Email", + "name": "mqMessageQueue-form-view", + "displayName": "Mq Message Queue", "type": "form", "context": "{}", "moduleUserKey": "solid-core", - "modelUserKey": "emailTemplate", + "modelUserKey": "mqMessageQueue", "layout": { "type": "form", "attrs": { "name": "form-1", - "label": "Email", + "label": "Mq Message Queue", "className": "grid", "disabled": false, - "readonly": false + "readonly": true }, - "onFieldChange": "emailFormTypeChangeHandler", - "onFormLayoutLoad": "emailFormTypeLoad", "children": [ { "type": "sheet", @@ -11999,71 +10334,160 @@ }, "children": [ { - "type": "group", - "attrs": { - "name": "group-1", - "label": "", - "className": "col-12 sm:col-12 md:col-6 lg:col-6" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "name", - "label": "Name" - } - }, - { - "type": "field", - "attrs": { - "name": "displayName", - "label": "Display Name" - } - }, - { - "type": "field", - "attrs": { - "name": "type", - "label": "Type" - } - }, - { - "type": "field", - "attrs": { - "name": "body", - "label": "Body" - } - } - ] - }, - { - "type": "group", + "type": "notebook", "attrs": { - "name": "group-1", - "label": "", - "className": "col-12 sm:col-12 md:col-6 lg:col-6" + "name": "notebook-1" }, "children": [ { - "type": "field", - "attrs": { - "name": "subject", - "label": "Subject" - } - }, - { - "type": "field", + "type": "page", "attrs": { - "name": "description", - "label": "Description" - } + "name": "page-general-info", + "label": "General Info" + }, + "children": [ + { + "type": "row", + "attrs": { + "name": "row-1" + }, + "children": [ + { + "type": "column", + "attrs": { + "name": "col-1", + "className": "col-12 sm:col-12 md:col-6 lg:col-6" + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "name", + "label": "Queue Name" + } + } + ] + }, + { + "type": "column", + "attrs": { + "name": "col-2", + "className": "col-12 sm:col-12 md:col-6 lg:col-6" + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "description", + "label": "Queue Description" + } + } + ] + } + ] + } + ] }, { - "type": "field", + "type": "page", "attrs": { - "name": "active", - "label": "Active" - } + "name": "page-messages", + "label": "Messages" + }, + "children": [ + { + "type": "row", + "attrs": { + "name": "row-2" + }, + "children": [ + { + "type": "column", + "attrs": { + "name": "col-1", + "className": "col-12" + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "mqMessages", + "label": "mqMessages", + "inlineCreate": "false", + "showLabel": false, + "inlineListLayout": { + "type": "list", + "attrs": { + "pagination": true, + "pageSizeOptions": [ + 10, + 25, + 50 + ], + "enableGlobalSearch": true, + "create": false, + "edit": false, + "delete": false + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "messageId", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "stage", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "startedAt", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "finishedAt", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "elapsedMillis", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "parentEntityId", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "parentEntity", + "isSearchable": true + } + } + ] + } + } + } + ] + } + ] + } + ] } ] } @@ -12073,12 +10497,12 @@ } }, { - "name": "smsTemplate-list-view", - "displayName": "SMS Template", + "name": "scheduledJob-list-view", + "displayName": "Scheduled Job", "type": "list", "context": "{}", "moduleUserKey": "solid-core", - "modelUserKey": "smsTemplate", + "modelUserKey": "scheduledJob", "layout": { "type": "list", "attrs": { @@ -12091,14 +10515,94 @@ "enableGlobalSearch": true, "create": true, "edit": true, - "delete": false + "delete": true }, "children": [ { "type": "field", "attrs": { - "name": "name", - "label": "Name", + "name": "id" + } + }, + { + "type": "field", + "attrs": { + "name": "scheduleName", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "module", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "isActive" + } + }, + { + "type": "field", + "attrs": { + "name": "frequency", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "startTime" + } + }, + { + "type": "field", + "attrs": { + "name": "endTime" + } + }, + { + "type": "field", + "attrs": { + "name": "startDate" + } + }, + { + "type": "field", + "attrs": { + "name": "endDate" + } + }, + { + "type": "field", + "attrs": { + "name": "dayOfMonth" + } + }, + { + "type": "field", + "attrs": { + "name": "lastRunAt" + } + }, + { + "type": "field", + "attrs": { + "name": "nextRunAt" + } + }, + { + "type": "field", + "attrs": { + "name": "dayOfWeek" + } + }, + { + "type": "field", + "attrs": { + "name": "job", "sortable": true, "filterable": true } @@ -12107,22 +10611,20 @@ } }, { - "name": "smsTemplate-form-view", - "displayName": "SMS Template", + "name": "scheduledJob-form-view", + "displayName": "Scheduled Job", "type": "form", "context": "{}", "moduleUserKey": "solid-core", - "modelUserKey": "smsTemplate", + "modelUserKey": "scheduledJob", "layout": { "type": "form", "attrs": { "name": "form-1", - "label": "Email", - "className": "grid", - "disabled": false, - "readonly": false + "label": "Scheduled Job", + "className": "grid" }, - "onFieldChange": "emailFormTypeChangeHandler", + "onFieldChange": "scheduleFrequencyOnFieldChangeHandler", "children": [ { "type": "sheet", @@ -12131,71 +10633,165 @@ }, "children": [ { - "type": "group", - "attrs": { - "name": "group-1", - "label": "", - "className": "col-12 sm:col-12 md:col-6 lg:col-6" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "name", - "label": "Name" - } - }, - { - "type": "field", - "attrs": { - "name": "displayName", - "label": "Display Name" - } - }, - { - "type": "field", - "attrs": { - "name": "type", - "label": "Type" - } - } - ] - }, - { - "type": "group", + "type": "notebook", "attrs": { - "name": "group-1", - "label": "", - "className": "col-12 sm:col-12 md:col-6 lg:col-6" + "name": "notebook-1" }, "children": [ { - "type": "field", - "attrs": { - "name": "body", - "label": "Body" - } - }, - { - "type": "field", - "attrs": { - "name": "smsProviderTemplateId", - "label": "Sms Provider Template Id" - } - }, - { - "type": "field", + "type": "page", "attrs": { - "name": "description", - "label": "Description" - } + "name": "page-general", + "label": "General" + }, + "children": [ + { + "type": "row", + "attrs": { + "name": "row-general" + }, + "children": [ + { + "type": "column", + "attrs": { + "name": "group-basic", + "label": "Basic", + "className": "col-12 sm:col-12 md:col-6 lg:col-6" + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "scheduleName" + } + }, + { + "type": "field", + "attrs": { + "name": "job" + } + }, + { + "type": "field", + "attrs": { + "name": "module" + } + }, + { + "type": "field", + "attrs": { + "name": "isActive" + } + } + ] + }, + { + "type": "column", + "attrs": { + "name": "group-config", + "label": "Configuration", + "className": "col-12 sm:col-12 md:col-6 lg:col-6" + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "frequency" + } + }, + { + "type": "field", + "attrs": { + "name": "cronExpression", + "visible": false + } + }, + { + "type": "field", + "attrs": { + "name": "startTime" + } + }, + { + "type": "field", + "attrs": { + "name": "endTime" + } + }, + { + "type": "field", + "attrs": { + "name": "startDate" + } + }, + { + "type": "field", + "attrs": { + "name": "endDate" + } + }, + { + "type": "field", + "attrs": { + "name": "dayOfWeek", + "visible": false + } + }, + { + "type": "field", + "attrs": { + "name": "dayOfMonth", + "visible": false + } + } + ] + } + ] + } + ] }, { - "type": "field", + "type": "page", "attrs": { - "name": "active", - "label": "Active" - } + "name": "page-runtime", + "label": "Runtime" + }, + "children": [ + { + "type": "row", + "attrs": { + "name": "row-runtime" + }, + "children": [ + { + "type": "column", + "attrs": { + "name": "group-runtime", + "label": "Execution", + "className": "col-12" + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "lastRunAt", + "disabled": true, + "readOnly": true + } + }, + { + "type": "field", + "attrs": { + "name": "nextRunAt", + "disabled": true, + "readOnly": true + } + } + ] + } + ] + } + ] } ] } @@ -12205,12 +10801,12 @@ } }, { - "name": "importTransaction-list-view", - "displayName": "Import Transactions", + "name": "settings-list-view", + "displayName": "Settings", "type": "list", "context": "{}", "moduleUserKey": "solid-core", - "modelUserKey": "importTransaction", + "modelUserKey": "setting", "layout": { "type": "list", "attrs": { @@ -12223,40 +10819,32 @@ "enableGlobalSearch": true, "create": true, "edit": true, - "delete": true + "delete": false }, "children": [ { "type": "field", "attrs": { - "name": "id", - "sortable": true, - "filterable": true - } - }, - { - "type": "field", - "attrs": { - "name": "importTransactionErrorLog", - "sortable": true, - "filterable": true + "name": "appTitle", + "label": "App Name", + "isSearchable": true } } ] } }, { - "name": "importTransaction-form-view", - "displayName": "Import Transactions", + "name": "settings-form-view", + "displayName": "Settings", "type": "form", "context": "{}", "moduleUserKey": "solid-core", - "modelUserKey": "importTransaction", + "modelUserKey": "setting", "layout": { "type": "form", "attrs": { "name": "form-1", - "label": "Import Transactions", + "label": "Settings", "className": "grid" }, "children": [ @@ -12267,35 +10855,117 @@ }, "children": [ { - "type": "row", + "type": "group", "attrs": { - "name": "sheet-1" + "name": "group-1", + "label": "", + "className": "col-12 sm:col-12 md:col-6 lg:col-6" }, "children": [ { - "type": "column", + "type": "field", "attrs": { - "name": "group-1", - "label": "", - "className": "col-12 sm:col-12 md:col-6 lg:col-6" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "importTransactionErrorLog" - } - } - ] + "name": "authPagesLayout", + "label": "Auth Pages Layout" + } }, { - "type": "column", + "type": "field", "attrs": { - "name": "group-2", - "label": "", - "className": "col-12 sm:col-12 md:col-6 lg:col-6" - }, - "children": [] + "name": "appTitle", + "label": "App Title" + } + }, + { + "type": "field", + "attrs": { + "name": "appLogo", + "label": "App Logo" + } + }, + { + "type": "field", + "attrs": { + "name": "appDescription", + "label": "App Description" + } + }, + { + "type": "field", + "attrs": { + "name": "appTnc", + "label": "App TNC" + } + }, + { + "type": "field", + "attrs": { + "name": "appPrivacyPolicy", + "label": "App Privacy Policy" + } + }, + { + "type": "field", + "attrs": { + "name": "iamAllowPublicRegistration", + "label": "Iam Allow Public Registration" + } + }, + { + "type": "field", + "attrs": { + "name": "forceChangePasswordOnFirstLogin", + "label": "Force Password Change On First Login" + } + }, + { + "type": "field", + "attrs": { + "name": "iamPasswordRegistrationEnabled", + "label": "Iam Password Registration Enabled" + } + }, + { + "type": "field", + "attrs": { + "name": "iamPasswordLessRegistrationEnabled", + "label": "Iam Password Less Registration enabled" + } + }, + { + "type": "field", + "attrs": { + "name": "iamActivateUserOnRegistration", + "label": "Iam Activate User On Registration" + } + }, + { + "type": "field", + "attrs": { + "name": "iamDefaultRole", + "label": "Iam Default Role" + } + }, + { + "type": "field", + "attrs": { + "name": "iamGoogleOAuthEnabled", + "label": "Iam Google OAuth Enabled" + } + }, + { + "type": "field", + "attrs": { + "name": "shouldQueueEmails", + "label": "Should Queue Emails" + } + }, + { + "type": "field", + "attrs": { + "name": "shouldQueueSms", + "label": "Should Queue SMS" + } } ] } @@ -12305,12 +10975,12 @@ } }, { - "name": "userActivityHistory-list-view", - "displayName": "User Activity History", + "name": "securityRule-list-view", + "displayName": "Security rules", "type": "list", "context": "{}", "moduleUserKey": "solid-core", - "modelUserKey": "userActivityHistory", + "modelUserKey": "securityRule", "layout": { "type": "list", "attrs": { @@ -12330,6 +11000,7 @@ "type": "field", "attrs": { "name": "id", + "label": "Id", "sortable": true, "filterable": true } @@ -12337,115 +11008,38 @@ { "type": "field", "attrs": { - "name": "user", - "sortable": true, - "filterable": true - } - }, - { - "type": "field", - "attrs": { - "name": "event", - "sortable": true, - "filterable": true - } - }, - { - "type": "field", - "attrs": { - "name": "ipAddress", - "sortable": true, - "filterable": true - } - }, - { - "type": "field", - "attrs": { - "name": "createdAt", - "sortable": true, - "filterable": true - } - } - ] - } - }, - { - "name": "userActivityHistory-tree-view", - "displayName": "User Activity History", - "type": "tree", - "context": "{}", - "moduleUserKey": "solid-core", - "modelUserKey": "userActivityHistory", - "layout": { - "type": "tree", - "attrs": { - "pagination": true, - "pageSizeOptions": [ - 10, - 25, - 50 - ], - "enableGlobalSearch": true, - "create": true, - "edit": true, - "delete": true - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "id", - "sortable": true, - "filterable": true - } - }, - { - "type": "field", - "attrs": { - "name": "user", - "sortable": true, - "filterable": true - } - }, - { - "type": "field", - "attrs": { - "name": "event", - "sortable": true, - "filterable": true - } - }, - { - "type": "field", - "attrs": { - "name": "ipAddress", + "name": "name", + "label": "Name", "sortable": true, - "filterable": true + "filterable": true, + "isSearchable": true } }, { "type": "field", "attrs": { - "name": "createdAt", + "name": "description", + "label": "Description", "sortable": true, - "filterable": true + "filterable": true, + "isSearchable": true } } ] } }, { - "name": "userActivityHistory-form-view", - "displayName": "User Activity History", + "name": "securityRule-form-view", + "displayName": "Security rules", "type": "form", "context": "{}", "moduleUserKey": "solid-core", - "modelUserKey": "userActivityHistory", + "modelUserKey": "securityRule", "layout": { "type": "form", "attrs": { "name": "form-1", - "label": "User Activity History", + "label": "Security rules", "className": "grid" }, "children": [ @@ -12458,7 +11052,9 @@ { "type": "row", "attrs": { - "name": "sheet-1" + "name": "group-1", + "label": "", + "className": "" }, "children": [ { @@ -12472,25 +11068,19 @@ { "type": "field", "attrs": { - "name": "user" - } - }, - { - "type": "field", - "attrs": { - "name": "event" + "name": "name" } }, { "type": "field", "attrs": { - "name": "ipAddress" + "name": "description" } }, { "type": "field", "attrs": { - "name": "userAgent" + "name": "securityRuleConfig" } } ] @@ -12502,189 +11092,18 @@ "label": "", "className": "col-12 sm:col-12 md:col-6 lg:col-6" }, - "children": [] - } - ] - } - ] - } - ] - } - }, - { - "name": "dashboard-list-view", - "displayName": "Dashboard", - "type": "list", - "context": "{}", - "moduleUserKey": "solid-core", - "modelUserKey": "dashboard", - "layout": { - "type": "list", - "attrs": { - "pagination": true, - "pageSizeOptions": [ - 10, - 25, - 50 - ], - "enableGlobalSearch": true, - "create": true, - "edit": true, - "delete": true - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "id" - } - }, - { - "type": "field", - "attrs": { - "name": "name" - } - }, - { - "type": "field", - "attrs": { - "name": "displayName" - } - }, - { - "type": "field", - "attrs": { - "name": "module" - } - } - ] - } - }, - { - "name": "dashboard-form-view", - "displayName": "Dashboard", - "type": "form", - "context": "{}", - "moduleUserKey": "solid-core", - "modelUserKey": "dashboard", - "layout": { - "type": "form", - "attrs": { - "name": "form-1", - "label": "Dashboard", - "className": "grid" - }, - "children": [ - { - "type": "sheet", - "attrs": { - "name": "sheet-1" - }, - "children": [ - { - "type": "notebook", - "attrs": { - "name": "notebook-1" - }, - "children": [ - { - "type": "page", - "attrs": { - "name": "page-1", - "label": "Dashboard" - }, "children": [ { - "type": "row", + "type": "field", "attrs": { - "name": "row-1" - }, - "children": [ - { - "type": "column", - "attrs": { - "name": "group-1", - "label": "", - "className": "col-12 sm:col-12 md:col-6 lg:col-6" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "name" - } - }, - { - "type": "field", - "attrs": { - "name": "displayName" - } - }, - { - "type": "field", - "attrs": { - "name": "description" - } - }, - { - "type": "field", - "attrs": { - "name": "module" - } - } - ] - }, - { - "type": "column", - "attrs": { - "name": "group-2", - "label": "", - "className": "col-12 sm:col-12 md:col-6 lg:col-6" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "layoutJson" - } - } - ] - } - ] - } - ] - }, - { - "type": "page", - "attrs": { - "name": "page-2", - "label": "Dashboard Variables" - }, - "children": [ + "name": "role" + } + }, { - "type": "row", + "type": "field", "attrs": { - "name": "row-2" - }, - "children": [ - { - "type": "column", - "attrs": { - "name": "group-3", - "label": "", - "className": "col-12" - }, - "children": [ - { - "type": "field", - "attrs": { - "showLabel": false, - "name": "dashboardVariables" - } - } - ] - } - ] + "name": "modelMetadata" + } } ] } @@ -12696,12 +11115,12 @@ } }, { - "name": "dashboardVariable-list-view", - "displayName": "Dashboard Variable", + "name": "emailTemplate-list-view", + "displayName": "Email", "type": "list", "context": "{}", "moduleUserKey": "solid-core", - "modelUserKey": "dashboardVariable", + "modelUserKey": "emailTemplate", "layout": { "type": "list", "attrs": { @@ -12714,50 +11133,39 @@ "enableGlobalSearch": true, "create": true, "edit": true, - "delete": true + "delete": false }, "children": [ { "type": "field", "attrs": { - "name": "id" - } - }, - { - "type": "field", - "attrs": { - "name": "variableName" - } - }, - { - "type": "field", - "attrs": { - "name": "variableType" - } - }, - { - "type": "field", - "attrs": { - "name": "isMultiSelect" + "name": "name", + "label": "Email", + "sortable": true, + "filterable": true } } ] } }, { - "name": "dashboardVariable-form-view", - "displayName": "Dashboard Variable", + "name": "emailTemplate-form-view", + "displayName": "Email", "type": "form", "context": "{}", "moduleUserKey": "solid-core", - "modelUserKey": "dashboardVariable", + "modelUserKey": "emailTemplate", "layout": { "type": "form", "attrs": { "name": "form-1", - "label": "Dashboard Variable", - "className": "grid" + "label": "Email", + "className": "grid", + "disabled": false, + "readonly": false }, + "onFieldChange": "emailFormTypeChangeHandler", + "onFormLayoutLoad": "emailFormTypeLoad", "children": [ { "type": "sheet", @@ -12766,76 +11174,71 @@ }, "children": [ { - "type": "row", + "type": "group", "attrs": { - "name": "sheet-1" + "name": "group-1", + "label": "", + "className": "col-12 sm:col-12 md:col-6 lg:col-6" }, "children": [ { - "type": "column", + "type": "field", "attrs": { - "name": "group-1", - "label": "", - "className": "col-12" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "variableName" - } - }, - { - "type": "field", - "attrs": { - "name": "variableType" - } - }, - { - "type": "field", - "attrs": { - "name": "selectionStaticValues" - } - }, - { - "type": "field", - "attrs": { - "name": "selectionDynamicSourceType" - } - }, - { - "type": "field", - "attrs": { - "name": "selectionDynamicSQL" - } - }, - { - "type": "field", - "attrs": { - "name": "selectionDynamicProviderName" - } - }, - { - "type": "field", - "attrs": { - "name": "defaultValue", - "editWidget": "codeEditor", - "editorLanguage": "json" - } - }, - { - "type": "field", - "attrs": { - "name": "defaultOperator" - } - }, - { - "type": "field", - "attrs": { - "name": "dashboard" - } - } - ] + "name": "name", + "label": "Name" + } + }, + { + "type": "field", + "attrs": { + "name": "displayName", + "label": "Display Name" + } + }, + { + "type": "field", + "attrs": { + "name": "type", + "label": "Type" + } + }, + { + "type": "field", + "attrs": { + "name": "body", + "label": "Body" + } + } + ] + }, + { + "type": "group", + "attrs": { + "name": "group-1", + "label": "", + "className": "col-12 sm:col-12 md:col-6 lg:col-6" + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "subject", + "label": "Subject" + } + }, + { + "type": "field", + "attrs": { + "name": "description", + "label": "Description" + } + }, + { + "type": "field", + "attrs": { + "name": "active", + "label": "Active" + } } ] } @@ -12845,12 +11248,12 @@ } }, { - "name": "dashboardQuestion-list-view", - "displayName": "Dashboard Question", + "name": "smsTemplate-list-view", + "displayName": "SMS Template", "type": "list", "context": "{}", "moduleUserKey": "solid-core", - "modelUserKey": "dashboardQuestion", + "modelUserKey": "smsTemplate", "layout": { "type": "list", "attrs": { @@ -12863,58 +11266,38 @@ "enableGlobalSearch": true, "create": true, "edit": true, - "delete": true + "delete": false }, "children": [ { "type": "field", "attrs": { - "name": "id" - } - }, - { - "type": "field", - "attrs": { - "name": "name" - } - }, - { - "type": "field", - "attrs": { - "name": "dashboard" - } - }, - { - "type": "field", - "attrs": { - "name": "visualisedAs" - } - }, - { - "type": "field", - "attrs": { - "name": "sequenceNumber" + "name": "name", + "label": "Name", + "sortable": true, + "filterable": true } } ] } }, { - "name": "dashboardQuestion-form-view", - "displayName": "Dashboard Question", + "name": "smsTemplate-form-view", + "displayName": "SMS Template", "type": "form", "context": "{}", "moduleUserKey": "solid-core", - "modelUserKey": "dashboardQuestion", + "modelUserKey": "smsTemplate", "layout": { "type": "form", "attrs": { "name": "form-1", - "label": "Dashboard Question", - "className": "grid" + "label": "Email", + "className": "grid", + "disabled": false, + "readonly": false }, - "onFieldChange": "dashboardQuestionFieldChangeHandler", - "onFormLoad": "dashboardQuestionOnFormLoadHandler", + "onFieldChange": "emailFormTypeChangeHandler", "children": [ { "type": "sheet", @@ -12923,192 +11306,71 @@ }, "children": [ { - "type": "notebook", + "type": "group", "attrs": { - "name": "notebook-1" + "name": "group-1", + "label": "", + "className": "col-12 sm:col-12 md:col-6 lg:col-6" }, "children": [ { - "type": "page", + "type": "field", "attrs": { - "name": "general", - "label": "General" - }, - "children": [ - { - "type": "row", - "attrs": { - "name": "row-1" - }, - "children": [ - { - "type": "column", - "attrs": { - "name": "group-1", - "label": "General Info", - "className": "col-12 sm:col-12 md:col-6 lg:col-6" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "name" - } - }, - { - "type": "field", - "attrs": { - "name": "dashboard" - } - }, - { - "type": "field", - "attrs": { - "name": "visualisedAs" - } - }, - { - "type": "field", - "attrs": { - "name": "sequenceNumber" - } - } - ] - }, - { - "type": "column", - "attrs": { - "name": "group-1", - "label": "Data Source", - "className": "col-12 sm:col-12 md:col-6 lg:col-6" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "sourceType" - } - }, - { - "type": "field", - "attrs": { - "name": "labelSql", - "editWidget": "codeEditor", - "editorLanguage": "sql" - } - }, - { - "type": "field", - "attrs": { - "name": "kpiSql", - "editWidget": "codeEditor", - "editorLanguage": "sql" - } - }, - { - "type": "field", - "attrs": { - "name": "providerName" - } - } - ] - } - ] - } - ] + "name": "name", + "label": "Name" + } + }, + { + "type": "field", + "attrs": { + "name": "displayName", + "label": "Display Name" + } + }, + { + "type": "field", + "attrs": { + "name": "type", + "label": "Type" + } + } + ] + }, + { + "type": "group", + "attrs": { + "name": "group-1", + "label": "", + "className": "col-12 sm:col-12 md:col-6 lg:col-6" + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "body", + "label": "Body" + } }, { - "type": "page", + "type": "field", "attrs": { - "name": "sql-dataset-config", - "label": "SQL Queries" - }, - "children": [ - { - "type": "row", - "attrs": { - "name": "row-3" - }, - "children": [ - { - "type": "column", - "attrs": { - "name": "group-1", - "label": "", - "className": "col-12" - }, - "children": [ - { - "type": "field", - "attrs": { - "label": "Label SQL", - "name": "sql", - "editWidget": "codeEditor", - "editorLanguage": "sql" - } - }, - { - "type": "field", - "attrs": { - "showLabel": false, - "name": "questionSqlDatasetConfigs" - } - } - ] - } - ] - } - ] + "name": "smsProviderTemplateId", + "label": "Sms Provider Template Id" + } }, { - "type": "page", + "type": "field", "attrs": { - "name": "preview", - "label": "Preview" - }, - "children": [ - { - "type": "row", - "attrs": { - "name": "row-2" - }, - "children": [ - { - "type": "column", - "attrs": { - "name": "chart-settings", - "label": "Chart Settings", - "className": "col-12 xl:col-3" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "chartOptions" - } - } - ] - }, - { - "type": "column", - "attrs": { - "name": "chart-preview", - "label": "Chart Preview", - "className": "col-12 xl:col-9" - }, - "children": [ - { - "type": "custom", - "attrs": { - "name": "page-2-chart-preview-custom", - "widget": "chart" - } - } - ] - } - ] - } - ] + "name": "description", + "label": "Description" + } + }, + { + "type": "field", + "attrs": { + "name": "active", + "label": "Active" + } } ] } @@ -13118,12 +11380,12 @@ } }, { - "name": "dashboardQuestionSqlDatasetConfig-list-view", - "displayName": "Dashboard Question SQL Dataset Config", + "name": "importTransaction-list-view", + "displayName": "Import Transactions", "type": "list", "context": "{}", "moduleUserKey": "solid-core", - "modelUserKey": "dashboardQuestionSqlDatasetConfig", + "modelUserKey": "importTransaction", "layout": { "type": "list", "attrs": { @@ -13142,42 +11404,34 @@ { "type": "field", "attrs": { - "name": "datasetDisplayName" - } - }, - { - "type": "field", - "attrs": { - "name": "labelColumnName" - } - }, - { - "type": "field", - "attrs": { - "name": "valueColumnName" + "name": "id", + "sortable": true, + "filterable": true } }, { "type": "field", "attrs": { - "name": "options" + "name": "importTransactionErrorLog", + "sortable": true, + "filterable": true } } ] } }, { - "name": "dashboardQuestionSqlDatasetConfig-form-view", - "displayName": "Dashboard Question SQL Dataset Config", + "name": "importTransaction-form-view", + "displayName": "Import Transactions", "type": "form", "context": "{}", "moduleUserKey": "solid-core", - "modelUserKey": "dashboardQuestionSqlDatasetConfig", + "modelUserKey": "importTransaction", "layout": { "type": "form", "attrs": { "name": "form-1", - "label": "Dashboard Question SQL Dataset Config", + "label": "Import Transactions", "className": "grid" }, "children": [ @@ -13198,54 +11452,25 @@ "attrs": { "name": "group-1", "label": "", - "className": "col-12" + "className": "col-12 sm:col-12 md:col-6 lg:col-6" }, "children": [ { "type": "field", "attrs": { - "name": "datasetName" - } - }, - { - "type": "field", - "attrs": { - "name": "datasetDisplayName" - } - }, - { - "type": "field", - "attrs": { - "name": "question" - } - }, - { - "type": "field", - "attrs": { - "name": "sql", - "editWidget": "codeEditor", - "editorLanguage": "sql" - } - }, - { - "type": "field", - "attrs": { - "name": "labelColumnName" - } - }, - { - "type": "field", - "attrs": { - "name": "valueColumnName" - } - }, - { - "type": "field", - "attrs": { - "name": "options" + "name": "importTransactionErrorLog" } } ] + }, + { + "type": "column", + "attrs": { + "name": "group-2", + "label": "", + "className": "col-12 sm:col-12 md:col-6 lg:col-6" + }, + "children": [] } ] } @@ -13255,12 +11480,12 @@ } }, { - "name": "dashboardLayout-list-view", - "displayName": "Dashboard Layout", + "name": "userActivityHistory-list-view", + "displayName": "User Activity History", "type": "list", "context": "{}", "moduleUserKey": "solid-core", - "modelUserKey": "dashboardLayout", + "modelUserKey": "userActivityHistory", "layout": { "type": "list", "attrs": { @@ -13279,36 +11504,123 @@ { "type": "field", "attrs": { - "name": "id" + "name": "id", + "sortable": true, + "filterable": true } }, { "type": "field", "attrs": { - "name": "dashboard" + "name": "user", + "sortable": true, + "filterable": true } }, { "type": "field", "attrs": { - "name": "user" + "name": "event", + "sortable": true, + "filterable": true + } + }, + { + "type": "field", + "attrs": { + "name": "ipAddress", + "sortable": true, + "filterable": true + } + }, + { + "type": "field", + "attrs": { + "name": "createdAt", + "sortable": true, + "filterable": true + } + } + ] + } + }, + { + "name": "userActivityHistory-tree-view", + "displayName": "User Activity History", + "type": "tree", + "context": "{}", + "moduleUserKey": "solid-core", + "modelUserKey": "userActivityHistory", + "layout": { + "type": "tree", + "attrs": { + "pagination": true, + "pageSizeOptions": [ + 10, + 25, + 50 + ], + "enableGlobalSearch": true, + "create": true, + "edit": true, + "delete": true + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "id", + "sortable": true, + "filterable": true + } + }, + { + "type": "field", + "attrs": { + "name": "user", + "sortable": true, + "filterable": true + } + }, + { + "type": "field", + "attrs": { + "name": "event", + "sortable": true, + "filterable": true + } + }, + { + "type": "field", + "attrs": { + "name": "ipAddress", + "sortable": true, + "filterable": true + } + }, + { + "type": "field", + "attrs": { + "name": "createdAt", + "sortable": true, + "filterable": true } } ] } }, { - "name": "dashboardLayout-form-view", - "displayName": "Dashboard Layout", + "name": "userActivityHistory-form-view", + "displayName": "User Activity History", "type": "form", "context": "{}", "moduleUserKey": "solid-core", - "modelUserKey": "dashboardLayout", + "modelUserKey": "userActivityHistory", "layout": { "type": "form", "attrs": { "name": "form-1", - "label": "Dashboard Layout", + "label": "User Activity History", "className": "grid" }, "children": [ @@ -13329,28 +11641,43 @@ "attrs": { "name": "group-1", "label": "", - "className": "col-12" + "className": "col-12 sm:col-12 md:col-6 lg:col-6" }, "children": [ { "type": "field", "attrs": { - "name": "layout" + "name": "user" } }, { "type": "field", "attrs": { - "name": "dashboard" + "name": "event" } }, { "type": "field", "attrs": { - "name": "user" + "name": "ipAddress" + } + }, + { + "type": "field", + "attrs": { + "name": "userAgent" } } ] + }, + { + "type": "column", + "attrs": { + "name": "group-2", + "label": "", + "className": "col-12 sm:col-12 md:col-6 lg:col-6" + }, + "children": [] } ] } @@ -13955,23 +12282,81 @@ "type": "list", "attrs": { "pagination": true, - "pageSizeOptions": [10, 25, 50], + "pageSizeOptions": [ + 10, + 25, + 50 + ], "enableGlobalSearch": true, "create": true, "edit": true, "delete": true }, "children": [ - { "type": "field", "attrs": { "name": "sessionId", "isSearchable": true } }, - { "type": "field", "attrs": { "name": "modelName", "isSearchable": true } }, - { "type": "field", "attrs": { "name": "status", "isSearchable": true } }, - { "type": "field", "attrs": { "name": "totalSteps" } }, - { "type": "field", "attrs": { "name": "totalCost" } }, - { "type": "field", "attrs": { "name": "totalInputTokens" } }, - { "type": "field", "attrs": { "name": "totalOutputTokens" } }, - { "type": "field", "attrs": { "name": "projectRoot", "isSearchable": true } }, - { "type": "field", "attrs": { "name": "createdAt" } }, - { "type": "field", "attrs": { "name": "updatedAt" } } + { + "type": "field", + "attrs": { + "name": "sessionId", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "modelName", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "status", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "totalSteps" + } + }, + { + "type": "field", + "attrs": { + "name": "totalCost" + } + }, + { + "type": "field", + "attrs": { + "name": "totalInputTokens" + } + }, + { + "type": "field", + "attrs": { + "name": "totalOutputTokens" + } + }, + { + "type": "field", + "attrs": { + "name": "projectRoot", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "createdAt" + } + }, + { + "type": "field", + "attrs": { + "name": "updatedAt" + } + } ] } }, @@ -13992,25 +12377,78 @@ "children": [ { "type": "sheet", - "attrs": { "name": "sheet-1" }, + "attrs": { + "name": "sheet-1" + }, "children": [ { "type": "row", - "attrs": { "name": "row-1" }, + "attrs": { + "name": "row-1" + }, "children": [ { "type": "column", - "attrs": { "name": "col-1", "label": "", "className": "col-12" }, + "attrs": { + "name": "col-1", + "label": "", + "className": "col-12" + }, "children": [ - { "type": "field", "attrs": { "name": "sessionId" } }, - { "type": "field", "attrs": { "name": "modelName" } }, - { "type": "field", "attrs": { "name": "status" } }, - { "type": "field", "attrs": { "name": "projectRoot" } }, - { "type": "field", "attrs": { "name": "totalSteps" } }, - { "type": "field", "attrs": { "name": "totalCost" } }, - { "type": "field", "attrs": { "name": "totalInputTokens" } }, - { "type": "field", "attrs": { "name": "totalOutputTokens" } }, - { "type": "field", "attrs": { "name": "summary" } } + { + "type": "field", + "attrs": { + "name": "sessionId" + } + }, + { + "type": "field", + "attrs": { + "name": "modelName" + } + }, + { + "type": "field", + "attrs": { + "name": "status" + } + }, + { + "type": "field", + "attrs": { + "name": "projectRoot" + } + }, + { + "type": "field", + "attrs": { + "name": "totalSteps" + } + }, + { + "type": "field", + "attrs": { + "name": "totalCost" + } + }, + { + "type": "field", + "attrs": { + "name": "totalInputTokens" + } + }, + { + "type": "field", + "attrs": { + "name": "totalOutputTokens" + } + }, + { + "type": "field", + "attrs": { + "name": "summary" + } + } ] } ] @@ -14031,25 +12469,93 @@ "type": "list", "attrs": { "pagination": true, - "pageSizeOptions": [10, 25, 50], + "pageSizeOptions": [ + 10, + 25, + 50 + ], "enableGlobalSearch": true, "create": true, "edit": true, "delete": true }, "children": [ - { "type": "field", "attrs": { "name": "id" } }, - { "type": "field", "attrs": { "name": "sessionId", "isSearchable": true } }, - { "type": "field", "attrs": { "name": "eventType", "isSearchable": true } }, - { "type": "field", "attrs": { "name": "turnNumber" } }, - { "type": "field", "attrs": { "name": "stepNumber" } }, - { "type": "field", "attrs": { "name": "toolName", "isSearchable": true } }, - { "type": "field", "attrs": { "name": "durationMs" } }, - { "type": "field", "attrs": { "name": "cost" } }, - { "type": "field", "attrs": { "name": "inputTokens" } }, - { "type": "field", "attrs": { "name": "outputTokens" } }, - { "type": "field", "attrs": { "name": "modelUsed", "isSearchable": true } }, - { "type": "field", "attrs": { "name": "createdAt" } } + { + "type": "field", + "attrs": { + "name": "id" + } + }, + { + "type": "field", + "attrs": { + "name": "sessionId", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "eventType", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "turnNumber" + } + }, + { + "type": "field", + "attrs": { + "name": "stepNumber" + } + }, + { + "type": "field", + "attrs": { + "name": "toolName", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "durationMs" + } + }, + { + "type": "field", + "attrs": { + "name": "cost" + } + }, + { + "type": "field", + "attrs": { + "name": "inputTokens" + } + }, + { + "type": "field", + "attrs": { + "name": "outputTokens" + } + }, + { + "type": "field", + "attrs": { + "name": "modelUsed", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "createdAt" + } + } ] } }, @@ -14070,36 +12576,109 @@ "children": [ { "type": "sheet", - "attrs": { "name": "sheet-1" }, + "attrs": { + "name": "sheet-1" + }, "children": [ { "type": "notebook", - "attrs": { "name": "notebook-1" }, + "attrs": { + "name": "notebook-1" + }, "children": [ { "type": "page", - "attrs": { "name": "page-general", "label": "General" }, + "attrs": { + "name": "page-general", + "label": "General" + }, "children": [ { "type": "row", - "attrs": { "name": "page-general-row-1" }, + "attrs": { + "name": "page-general-row-1" + }, "children": [ { "type": "column", - "attrs": { "name": "page-general-col-1", "label": "", "className": "col-12" }, + "attrs": { + "name": "page-general-col-1", + "label": "", + "className": "col-12" + }, "children": [ - { "type": "field", "attrs": { "name": "sessionId" } }, - { "type": "field", "attrs": { "name": "eventType" } }, - { "type": "field", "attrs": { "name": "turnNumber" } }, - { "type": "field", "attrs": { "name": "stepNumber" } }, - { "type": "field", "attrs": { "name": "toolName" } }, - { "type": "field", "attrs": { "name": "modelUsed" } }, - { "type": "field", "attrs": { "name": "durationMs" } }, - { "type": "field", "attrs": { "name": "cost" } }, - { "type": "field", "attrs": { "name": "inputTokens" } }, - { "type": "field", "attrs": { "name": "outputTokens" } }, - { "type": "field", "attrs": { "name": "toolReturncode" } }, - { "type": "field", "attrs": { "name": "content" } } + { + "type": "field", + "attrs": { + "name": "sessionId" + } + }, + { + "type": "field", + "attrs": { + "name": "eventType" + } + }, + { + "type": "field", + "attrs": { + "name": "turnNumber" + } + }, + { + "type": "field", + "attrs": { + "name": "stepNumber" + } + }, + { + "type": "field", + "attrs": { + "name": "toolName" + } + }, + { + "type": "field", + "attrs": { + "name": "modelUsed" + } + }, + { + "type": "field", + "attrs": { + "name": "durationMs" + } + }, + { + "type": "field", + "attrs": { + "name": "cost" + } + }, + { + "type": "field", + "attrs": { + "name": "inputTokens" + } + }, + { + "type": "field", + "attrs": { + "name": "outputTokens" + } + }, + { + "type": "field", + "attrs": { + "name": "toolReturncode" + } + }, + { + "type": "field", + "attrs": { + "name": "content" + } + } ] } ] @@ -14108,17 +12687,32 @@ }, { "type": "page", - "attrs": { "name": "page-tool-arguments", "label": "Tool Arguments" }, + "attrs": { + "name": "page-tool-arguments", + "label": "Tool Arguments" + }, "children": [ { "type": "row", - "attrs": { "name": "page-tool-arguments-row-1" }, + "attrs": { + "name": "page-tool-arguments-row-1" + }, "children": [ { "type": "column", - "attrs": { "name": "page-tool-arguments-col-1", "label": "", "className": "col-12" }, + "attrs": { + "name": "page-tool-arguments-col-1", + "label": "", + "className": "col-12" + }, "children": [ - { "type": "field", "attrs": { "name": "toolArguments", "viewWidget": "SolidJsonFormViewWidget" } } + { + "type": "field", + "attrs": { + "name": "toolArguments", + "viewWidget": "SolidJsonFormViewWidget" + } + } ] } ] @@ -14127,17 +12721,32 @@ }, { "type": "page", - "attrs": { "name": "page-tool-output", "label": "Tool Output" }, + "attrs": { + "name": "page-tool-output", + "label": "Tool Output" + }, "children": [ { "type": "row", - "attrs": { "name": "page-tool-output-row-1" }, + "attrs": { + "name": "page-tool-output-row-1" + }, "children": [ { "type": "column", - "attrs": { "name": "page-tool-output-col-1", "label": "", "className": "col-12" }, + "attrs": { + "name": "page-tool-output-col-1", + "label": "", + "className": "col-12" + }, "children": [ - { "type": "field", "attrs": { "name": "toolOutput", "viewWidget": "SolidJsonFormViewWidget" } } + { + "type": "field", + "attrs": { + "name": "toolOutput", + "viewWidget": "SolidJsonFormViewWidget" + } + } ] } ] @@ -14146,17 +12755,32 @@ }, { "type": "page", - "attrs": { "name": "page-event-data", "label": "Event Data" }, + "attrs": { + "name": "page-event-data", + "label": "Event Data" + }, "children": [ { "type": "row", - "attrs": { "name": "page-event-data-row-1" }, + "attrs": { + "name": "page-event-data-row-1" + }, "children": [ { "type": "column", - "attrs": { "name": "page-event-data-col-1", "label": "", "className": "col-12" }, + "attrs": { + "name": "page-event-data-col-1", + "label": "", + "className": "col-12" + }, "children": [ - { "type": "field", "attrs": { "name": "eventData", "viewWidget": "SolidJsonFormViewWidget" } } + { + "type": "field", + "attrs": { + "name": "eventData", + "viewWidget": "SolidJsonFormViewWidget" + } + } ] } ] @@ -14181,25 +12805,96 @@ "type": "list", "attrs": { "pagination": true, - "pageSizeOptions": [10, 25, 50], + "pageSizeOptions": [ + 10, + 25, + 50 + ], "enableGlobalSearch": true, "create": true, "edit": true, "delete": false }, "children": [ - { "type": "field", "attrs": { "name": "id" } }, - { "type": "field", "attrs": { "name": "createdAt" } }, - { "type": "field", "attrs": { "name": "method", "isSearchable": true } }, - { "type": "field", "attrs": { "name": "toolName", "isSearchable": true } }, - { "type": "field", "attrs": { "name": "status", "isSearchable": true } }, - { "type": "field", "attrs": { "name": "username", "isSearchable": true } }, - { "type": "field", "attrs": { "name": "userId" } }, - { "type": "field", "attrs": { "name": "transport", "isSearchable": true } }, - { "type": "field", "attrs": { "name": "mcpSessionId", "isSearchable": true } }, - { "type": "field", "attrs": { "name": "clientAddr", "isSearchable": true } }, - { "type": "field", "attrs": { "name": "durationMs" } }, - { "type": "field", "attrs": { "name": "errorCode" } } + { + "type": "field", + "attrs": { + "name": "id" + } + }, + { + "type": "field", + "attrs": { + "name": "createdAt" + } + }, + { + "type": "field", + "attrs": { + "name": "method", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "toolName", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "status", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "username", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "userId" + } + }, + { + "type": "field", + "attrs": { + "name": "transport", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "mcpSessionId", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "clientAddr", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "durationMs" + } + }, + { + "type": "field", + "attrs": { + "name": "errorCode" + } + } ] } }, @@ -14220,36 +12915,109 @@ "children": [ { "type": "sheet", - "attrs": { "name": "sheet-1" }, + "attrs": { + "name": "sheet-1" + }, "children": [ { "type": "notebook", - "attrs": { "name": "notebook-1" }, + "attrs": { + "name": "notebook-1" + }, "children": [ { "type": "page", - "attrs": { "name": "page-general", "label": "General" }, + "attrs": { + "name": "page-general", + "label": "General" + }, "children": [ { "type": "row", - "attrs": { "name": "page-general-row-1" }, + "attrs": { + "name": "page-general-row-1" + }, "children": [ { "type": "column", - "attrs": { "name": "page-general-col-1", "label": "", "className": "col-12" }, + "attrs": { + "name": "page-general-col-1", + "label": "", + "className": "col-12" + }, "children": [ - { "type": "field", "attrs": { "name": "method" } }, - { "type": "field", "attrs": { "name": "toolName" } }, - { "type": "field", "attrs": { "name": "status" } }, - { "type": "field", "attrs": { "name": "transport" } }, - { "type": "field", "attrs": { "name": "mcpSessionId" } }, - { "type": "field", "attrs": { "name": "requestId" } }, - { "type": "field", "attrs": { "name": "userId" } }, - { "type": "field", "attrs": { "name": "apiKeyId" } }, - { "type": "field", "attrs": { "name": "username" } }, - { "type": "field", "attrs": { "name": "clientAddr" } }, - { "type": "field", "attrs": { "name": "durationMs" } }, - { "type": "field", "attrs": { "name": "errorCode" } } + { + "type": "field", + "attrs": { + "name": "method" + } + }, + { + "type": "field", + "attrs": { + "name": "toolName" + } + }, + { + "type": "field", + "attrs": { + "name": "status" + } + }, + { + "type": "field", + "attrs": { + "name": "transport" + } + }, + { + "type": "field", + "attrs": { + "name": "mcpSessionId" + } + }, + { + "type": "field", + "attrs": { + "name": "requestId" + } + }, + { + "type": "field", + "attrs": { + "name": "userId" + } + }, + { + "type": "field", + "attrs": { + "name": "apiKeyId" + } + }, + { + "type": "field", + "attrs": { + "name": "username" + } + }, + { + "type": "field", + "attrs": { + "name": "clientAddr" + } + }, + { + "type": "field", + "attrs": { + "name": "durationMs" + } + }, + { + "type": "field", + "attrs": { + "name": "errorCode" + } + } ] } ] @@ -14258,17 +13026,32 @@ }, { "type": "page", - "attrs": { "name": "page-request-params", "label": "Request Params" }, + "attrs": { + "name": "page-request-params", + "label": "Request Params" + }, "children": [ { "type": "row", - "attrs": { "name": "page-request-params-row-1" }, + "attrs": { + "name": "page-request-params-row-1" + }, "children": [ { "type": "column", - "attrs": { "name": "page-request-params-col-1", "label": "", "className": "col-12" }, + "attrs": { + "name": "page-request-params-col-1", + "label": "", + "className": "col-12" + }, "children": [ - { "type": "field", "attrs": { "name": "requestParams", "viewWidget": "SolidJsonFormViewWidget" } } + { + "type": "field", + "attrs": { + "name": "requestParams", + "viewWidget": "SolidJsonFormViewWidget" + } + } ] } ] @@ -14277,17 +13060,32 @@ }, { "type": "page", - "attrs": { "name": "page-response-result", "label": "Response Result" }, + "attrs": { + "name": "page-response-result", + "label": "Response Result" + }, "children": [ { "type": "row", - "attrs": { "name": "page-response-result-row-1" }, + "attrs": { + "name": "page-response-result-row-1" + }, "children": [ { "type": "column", - "attrs": { "name": "page-response-result-col-1", "label": "", "className": "col-12" }, + "attrs": { + "name": "page-response-result-col-1", + "label": "", + "className": "col-12" + }, "children": [ - { "type": "field", "attrs": { "name": "responseResult", "viewWidget": "SolidJsonFormViewWidget" } } + { + "type": "field", + "attrs": { + "name": "responseResult", + "viewWidget": "SolidJsonFormViewWidget" + } + } ] } ] @@ -14296,17 +13094,31 @@ }, { "type": "page", - "attrs": { "name": "page-error-message", "label": "Error Message" }, + "attrs": { + "name": "page-error-message", + "label": "Error Message" + }, "children": [ { "type": "row", - "attrs": { "name": "page-error-message-row-1" }, + "attrs": { + "name": "page-error-message-row-1" + }, "children": [ { "type": "column", - "attrs": { "name": "page-error-message-col-1", "label": "", "className": "col-12" }, + "attrs": { + "name": "page-error-message-col-1", + "label": "", + "className": "col-12" + }, "children": [ - { "type": "field", "attrs": { "name": "errorMessage" } } + { + "type": "field", + "attrs": { + "name": "errorMessage" + } + } ] } ] @@ -14457,4 +13269,4 @@ } ], "modelSequences": [] -} +} \ No newline at end of file diff --git a/src/services/dashboard-layout.service.ts b/src/services/dashboard-layout.service.ts deleted file mode 100644 index 1d348e26..00000000 --- a/src/services/dashboard-layout.service.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { ModuleRef } from "@nestjs/core"; -import { InjectEntityManager } from '@nestjs/typeorm'; -import { EntityManager } from 'typeorm'; - -import { CRUDService } from 'src/services/crud.service'; -import { DashboardLayout } from 'src/entities/dashboard-layout.entity'; -import { DashboardLayoutRepository } from 'src/repository/dashboard-layout.repository'; -import { CreateDashboardLayoutDto } from 'src/dtos/create-dashboard-layout.dto'; -import { RequestContextService } from './request-context.service'; - - -@Injectable() -export class DashboardLayoutService extends CRUDService { - private readonly logger = new Logger(this.constructor.name); - constructor( - @InjectEntityManager() - readonly entityManager: EntityManager, - readonly repo: DashboardLayoutRepository, - readonly requestContextService: RequestContextService, - readonly moduleRef: ModuleRef, - ) { - super(entityManager, repo, 'dashboardLayout', 'solid-core', moduleRef); - } - - async upsertUserDashboardLayout(createDtos: CreateDashboardLayoutDto) { - const activeUser = this.requestContextService.getActiveUser(); - - if (!activeUser) { - throw new Error('User not found'); - } - - let userId = null; - if (activeUser.roles.includes('Admin')) { - userId = null; - } else { - userId = activeUser?.sub; - } - const existingLayout = await this.repo.findOne({ - where: { - user: { id: userId }, - dashboard: { - id: createDtos.dashboardId - } - }, - relations: { - user: true, - dashboard: true, - } - }); - - if (existingLayout) { - return super.update(existingLayout.id, { layout: createDtos.layout }, [], true); - } else { - const createDto = { - layout: createDtos.layout, - dashboardId: createDtos.dashboardId, - uesrId: userId - } - return super.create(createDto, []); - } - } - - async getUserDashboardLayoutByDashboardId(dashboardId: any) { - const activeUser = this.requestContextService.getActiveUser(); - - if (!activeUser) { - throw new Error('User not found'); - } - const userId = activeUser?.sub; - const existingUserLayout = await this.repo.findOne({ - where: { - user: { id: userId }, - dashboard: { - id: dashboardId - } - }, - relations: { - user: true, - dashboard: true, - } - }); - if (existingUserLayout) { - // if dahsboard for userid exists - return existingUserLayout; - } - - // if not then check for default dashboard - const defaultLayout = await this.repo.findOne({ - where: { - user: { id: null }, - dashboard: { - id: dashboardId - } - }, - relations: { - user: true, - dashboard: true, - } - }); - if (defaultLayout) { - // if default layout exists return it - return defaultLayout; - } else { - // if default layout does not exist return empty layout - return { - layout: null - } - } - } -} diff --git a/src/services/dashboard-question-sql-dataset-config.service.ts b/src/services/dashboard-question-sql-dataset-config.service.ts deleted file mode 100644 index 5698f3a6..00000000 --- a/src/services/dashboard-question-sql-dataset-config.service.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { InjectEntityManager } from '@nestjs/typeorm'; -import { ModuleRef } from "@nestjs/core"; -import { EntityManager } from 'typeorm'; - -import { CRUDService } from 'src/services/crud.service'; - - -import { DashboardQuestionSqlDatasetConfig } from '../entities/dashboard-question-sql-dataset-config.entity'; -import { DashboardQuestionSqlDatasetConfigRepository } from 'src/repository/dashboard-question-sql-dataset-config.repository'; - -@Injectable() -export class DashboardQuestionSqlDatasetConfigService extends CRUDService{ - constructor( - @InjectEntityManager() - readonly entityManager: EntityManager, - // @InjectRepository(DashboardQuestionSqlDatasetConfig, 'default') - // readonly repo: Repository, - readonly repo: DashboardQuestionSqlDatasetConfigRepository, - readonly moduleRef: ModuleRef - - ) { - super(entityManager, repo, 'dashboardQuestionSqlDatasetConfig', 'solid-core', moduleRef); - } -} diff --git a/src/services/dashboard-question.service.ts b/src/services/dashboard-question.service.ts deleted file mode 100644 index a2c23d28..00000000 --- a/src/services/dashboard-question.service.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { BadRequestException, Injectable, Logger, NotImplementedException } from '@nestjs/common'; -import { ModuleRef } from "@nestjs/core"; -import { InjectEntityManager } from '@nestjs/typeorm'; -import { EntityManager } from 'typeorm'; - -import { CRUDService } from 'src/services/crud.service'; - - -import { DashboardVariable } from 'src/entities/dashboard-variable.entity'; -import { SolidRegistry } from 'src/helpers/solid-registry'; -import { DashboardQuestion } from '../entities/dashboard-question.entity'; -import { SqlExpression, SqlExpressionOperator } from './question-data-providers/chartjs-sql-data-provider.service'; -import { DashboardQuestionRepository } from 'src/repository/dashboard-question.repository'; -import { QuestionSqlDataProviderContext } from '../interfaces'; - -enum SOURCE_TYPE { - SQL = 'sql', - PROVIDER = 'provider', -} - -export const CHARTJS_SQL_DATA_PROVIDER_NAME = 'ChartJsSqlDataProvider'; -export const PRIME_REACT_METER_GROUP_SQL_DATA_PROVIDER_NAME = 'PrimeReactMeterGroupSqlDataProvider'; -export const PRIME_REACT_DATATABLE_SQL_DATA_PROVIDER_NAME = 'PrimeReactDatatableSqlDataProvider'; - -export const INBUILT_SQL_DATA_PROVIDERS = [CHARTJS_SQL_DATA_PROVIDER_NAME, PRIME_REACT_METER_GROUP_SQL_DATA_PROVIDER_NAME, PRIME_REACT_DATATABLE_SQL_DATA_PROVIDER_NAME]; - -@Injectable() -export class DashboardQuestionService extends CRUDService { - private readonly logger = new Logger(this.constructor.name); - constructor( - @InjectEntityManager() - readonly entityManager: EntityManager, - // @InjectRepository(DashboardQuestion, 'default') - // readonly repo: Repository, - readonly repo: DashboardQuestionRepository, - readonly moduleRef: ModuleRef, - readonly solidRegistry: SolidRegistry, // Assuming solidRegistry is injected for data providers - ) { - super(entityManager, repo, 'dashboardQuestion', 'solid-core', moduleRef); - } - - // Get the data for a specific question - async getData(id: number, inputExpressions: SqlExpression[] = [], isPreview = false): Promise { - // Load the question - const question = await this.loadQuestion(id); - if (!question) { - throw new BadRequestException(`Question with id ${id} not found`); - } - - // Get the dashbbard variables from the question - const dashboardVariables = question.dashboard?.dashboardVariables || []; - const expressions: SqlExpression[] = this.getExpressions(isPreview, dashboardVariables, inputExpressions); - - // Try to resolve the dataProvider based on a combination of sourceType and visualisedAs - let dataProvider = null; - let context = {}; - - // Decide which data provider to use based on the question visualisation type if sourceType is SQL. - if (question.sourceType === SOURCE_TYPE.SQL && ['bar', 'pie', 'line', 'donut'].includes(question.visualisedAs)) { - dataProvider = this.solidRegistry.getDashboardQuestionDataProviderInstance(CHARTJS_SQL_DATA_PROVIDER_NAME); - context = { - expressions, - } as QuestionSqlDataProviderContext; - } - if (question.sourceType === SOURCE_TYPE.SQL && ['prime-meter-group'].includes(question.visualisedAs)) { - dataProvider = this.solidRegistry.getDashboardQuestionDataProviderInstance(PRIME_REACT_METER_GROUP_SQL_DATA_PROVIDER_NAME); - context = { - expressions, - } as QuestionSqlDataProviderContext; - } - if (question.sourceType === SOURCE_TYPE.SQL && ['prime-datatable'].includes(question.visualisedAs)) { - dataProvider = this.solidRegistry.getDashboardQuestionDataProviderInstance(PRIME_REACT_DATATABLE_SQL_DATA_PROVIDER_NAME); - context = { - expressions, - } as QuestionSqlDataProviderContext; - } - - // If a custom provider is specified, use that one instead - if (question.sourceType === SOURCE_TYPE.PROVIDER) { - dataProvider = this.solidRegistry.getDashboardQuestionDataProviderInstance(question.providerName); - } - - if (!dataProvider) { - throw new NotImplementedException(`Invalid data source type ${question.sourceType}`); - } - - return await dataProvider.getData(question, context); - - } - - private getExpressions(isPreview: boolean, dashboardVariables: DashboardVariable[], inputExpressions: SqlExpression[]) { - const expressions: SqlExpression[] = []; - - if (isPreview) { - // Convert the dashboard variables into objects of interface type SqlExpression using the default value, default operator and the variable name - const expr: SqlExpression[] = dashboardVariables.map(variable => { - return { - variableName: variable.variableName, - operator: variable.defaultOperator as SqlExpressionOperator, // Assuming defaultOperator is a valid SqlExpressionOperator - value: JSON.parse(variable.defaultValue || '[]'), // Assuming defaultValue is a string or can be converted to a string array - }; - }); - expressions.push(...expr); - } - else { - // Loop through the dashboard variables and see if there is a matching input expression - // If there is, use that expression instead of the default value - for (const variable of dashboardVariables) { - const matchingInputExpression = inputExpressions.find(expr => expr.variableName === variable.variableName); - if (matchingInputExpression) { - expressions.push(matchingInputExpression); - } - else { - expressions.push({ - variableName: variable.variableName, - operator: variable.defaultOperator as SqlExpressionOperator, - value: JSON.parse(variable.defaultValue || '[]'), - }); - } - } - expressions.push(...expressions); - } - - // Remove duplicate expressions based on variableName in the expressions array - const deduplicatedExpressions: SqlExpression[] = []; - const variableNames = new Set(); - for (const expr of expressions) { - if (!variableNames.has(expr.variableName)) { - deduplicatedExpressions.push(expr); - variableNames.add(expr.variableName); - } - } - - return deduplicatedExpressions; - } - - private async loadQuestion(id: number) { - const repo = this.entityManager.getRepository(DashboardQuestion); - - // Load the dashboard record using the field - const question = await repo.findOne({ - where: { - id, - }, - relations: ['questionSqlDatasetConfigs', 'dashboard', 'dashboard.dashboardVariables'], - }); - return question; - } - -} diff --git a/src/services/dashboard-selection-providers/dashboard-variable-sql-dynamic-provider.service.ts b/src/services/dashboard-selection-providers/dashboard-variable-sql-dynamic-provider.service.ts deleted file mode 100644 index 85d60a27..00000000 --- a/src/services/dashboard-selection-providers/dashboard-variable-sql-dynamic-provider.service.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Injectable } from "@nestjs/common"; -import { IDashboardVariableSelectionProvider, ISelectionProviderContext, ISelectionProviderValues } from "../../interfaces"; -// import localeCodes from 'locale-codes'; -import { EntityManager } from "typeorm"; -import { DashboardVariableSelectionProvider } from "src/decorators/dashboard-selection-provider.decorator"; - -@DashboardVariableSelectionProvider() -@Injectable() -export class DashboardVariableSQLDynamicProvider implements IDashboardVariableSelectionProvider { - - constructor(private readonly entityManager: EntityManager) { - } - - help(): string { - return "# Get the dashboard variable after executing the SQL query configured for the dashboard variable.\n"; - } - - name(): string { - return 'DashboardVariableSQLDynamicProvider'; - } - - async value(optionValue: string, ctxt: ISelectionProviderContext): Promise { - throw new Error("DashboardVariableSQLDynamicProvider does not support value method. Use values method instead."); - } - - async values(query: string, ctxt: ISelectionProviderContext): Promise { - const { sql, limit, offset } = ctxt as unknown as { sql: string, limit?: number, offset?: number }; - if (!sql) { - throw new Error("DashboardVariableSQLDynamicProvider requires a SQL query to be provided in the context."); - } - - // Here you would execute the SQL query against your database - // For demonstration, let's assume we have a mock database function that executes the SQL query - const results = await this.entityManager.query(this.appendLimitOffset(sql), [limit, offset]); - - // Transform the results into the expected format - return results.map((result: any) => { - const transformedResult: ISelectionProviderValues = { - value: result.value, // Assuming the result has a 'value' field - label: result.label, // Assuming the result has a 'label' field - // Add any other fields you need to transform here - }; - return transformedResult; - }); - } - - appendLimitOffset(sql: string): string { - // Strip trailing semicolon if present - const trimmedSql = sql.trim().replace(/;$/, ''); - - // Append the LIMIT/OFFSET - const finalSql = `${trimmedSql} LIMIT $1 OFFSET $2`; // FIXME This works with PostgreSQL. for mysql use ?. For this we will need to identify the datasource using the model for the particular dashboard variable - - return finalSql; - } -} \ No newline at end of file diff --git a/src/services/dashboard-selection-providers/dashboard-variable-test-dynamic-provider.service.ts b/src/services/dashboard-selection-providers/dashboard-variable-test-dynamic-provider.service.ts deleted file mode 100644 index b9df0d4d..00000000 --- a/src/services/dashboard-selection-providers/dashboard-variable-test-dynamic-provider.service.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Injectable } from "@nestjs/common"; -import { IDashboardVariableSelectionProvider, ISelectionProviderContext, ISelectionProviderValues } from "../../interfaces"; -// import localeCodes from 'locale-codes'; -import { EntityManager } from "typeorm"; -import { DashboardVariableSelectionProvider } from "src/decorators/dashboard-selection-provider.decorator"; - -@DashboardVariableSelectionProvider() -@Injectable() -export class DasbhoardVariableTestDynamicProvider implements IDashboardVariableSelectionProvider { - - constructor(private readonly entityManager: EntityManager) { - } - - help(): string { - return "# Get the dashboard variable values.\n"; - } - - name(): string { - return 'DasbhoardVariableTestDynamicProvider'; - } - - async value(optionValue: string, ctxt: ISelectionProviderContext): Promise { - throw new Error("DasbhoardVariableTestDynamicProvider does not support value method. Use values method instead."); - } - - async values(query: string, ctxt: ISelectionProviderContext): Promise { - // Return some dummy data for testing - const { sql, limit, offset } = ctxt as unknown as { sql: string, limit?: number, offset?: number }; - return [ - { value: '1', label: 'Option 1' }, - { value: '2', label: 'Option 2' }, - { value: '3', label: 'Option 3' }, - { value: '4', label: 'Option 4' }, - ]; - } - -} \ No newline at end of file diff --git a/src/services/dashboard-variable.service.ts b/src/services/dashboard-variable.service.ts deleted file mode 100644 index 15fca3f0..00000000 --- a/src/services/dashboard-variable.service.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { ModuleRef } from "@nestjs/core"; -import { InjectEntityManager } from '@nestjs/typeorm'; -import { EntityManager } from 'typeorm'; - -import { CRUDService } from 'src/services/crud.service'; - - -import { DashboardVariable } from '../entities/dashboard-variable.entity'; -import { DashboardVariableRepository } from 'src/repository/dashboard-variable.repository'; - -@Injectable() -export class DashboardVariableService extends CRUDService { - private readonly logger = new Logger(this.constructor.name); - constructor( - @InjectEntityManager() - readonly entityManager: EntityManager, - // @InjectRepository(DashboardVariable, 'default') - // readonly repo: Repository, - readonly repo: DashboardVariableRepository, - readonly moduleRef: ModuleRef, - ) { - super(entityManager, repo, 'dashboardVariable', 'solid-core', moduleRef); - } - - -} diff --git a/src/services/dashboard.service.ts b/src/services/dashboard.service.ts deleted file mode 100644 index 0d880401..00000000 --- a/src/services/dashboard.service.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { Injectable, Logger, NotFoundException } from '@nestjs/common'; -import { ModuleRef } from "@nestjs/core"; -import { InjectEntityManager } from '@nestjs/typeorm'; -import { EntityManager } from 'typeorm'; - -import { CRUDService } from 'src/services/crud.service'; - - -import * as fs from 'fs/promises'; // Use the Promise-based version of fs for async/await -import { SelectionDynamicSourceType } from 'src/dtos/create-dashboard-variable.dto'; -import { DashboardVariableSelectionDynamicQueryDto } from 'src/dtos/dashboard-variable-selection-dynamic-query.dto'; -import { DashboardVariable } from 'src/entities/dashboard-variable.entity'; -import { ModuleMetadataHelperService } from 'src/helpers/module-metadata-helper.service'; -import { SolidRegistry } from 'src/helpers/solid-registry'; -import { DashboardMapper } from 'src/mappers/dashboard-mapper'; -import { DashboardRepository } from 'src/repository/dashboard.repository'; -import { Dashboard } from '../entities/dashboard.entity'; -import { CreateDashboardDto } from 'src/dtos/create-dashboard.dto'; - - -export const SQL_DYNAMIC_PROVIDER_NAME = 'DashboardVariableSQLDynamicProvider'; -@Injectable() -export class DashboardService extends CRUDService { - private readonly logger = new Logger(this.constructor.name); - constructor( - @InjectEntityManager() - readonly entityManager: EntityManager, - readonly repo: DashboardRepository, // Assuming you have a DashboardRepository for custom queries - readonly moduleRef: ModuleRef, - readonly solidRegistry: SolidRegistry, // Assuming solidRegistry is injected for selection providers - readonly moduleMetadataHelperService: ModuleMetadataHelperService, - readonly dashboardMapper: DashboardMapper, - ) { - super(entityManager, repo, 'dashboard', 'solid-core', moduleRef); - } - - - async create(createDto: CreateDashboardDto, files: Express.Multer.File[]) { - createDto.name = createDto.name.trim().replace(/\s+/g, '-').toLowerCase(); - return super.create(createDto, files); - } - - async getSelectionDynamicValues(query: DashboardVariableSelectionDynamicQueryDto) { - // Get the dashboard variable repo - const dashboardVariable = await this.loadDashboardVariable(query.variableId); - - // Get the providerName and context for the dashboard variable - const [providerName, context] = this.getProviderNameAndContext(dashboardVariable, query); - - // Get hold of the provider instance from the SolidRegistry - const selectionProviderInstance = this.solidRegistry.getDashboardVariableSelectionProviderInstance(providerName); - if (!selectionProviderInstance) { - throw new NotFoundException(`Field incorrectly configured. No provider with name ${providerName} registered in backend.`); - } - - // 4. Call the provider's getSelectionDynamicValues method - return selectionProviderInstance.values(query.query, context); - } - - - private getProviderNameAndContext(dashboardVariable: DashboardVariable, query: DashboardVariableSelectionDynamicQueryDto): [string, any] { - const sourceType = dashboardVariable.selectionDynamicSourceType; - - // Get the appropriate provide name based on the source type - let providerName: string; - const context = { limit: query.limit, offset: query.offset }; - switch (sourceType) { - case SelectionDynamicSourceType.SQL: - providerName = SQL_DYNAMIC_PROVIDER_NAME; - context['sql'] = dashboardVariable.selectionDynamicSQL; - break; - case SelectionDynamicSourceType.PROVIDER: - providerName = dashboardVariable.selectionDynamicProviderName; - break; - default: - throw new Error(`Unsupported selection dynamic source type: ${sourceType}`); - } - return [providerName, context]; - } - - private async loadDashboardVariable(variableId: number) { - const dashboardVariableRepo = this.entityManager.getRepository(DashboardVariable); - - // Load the dashboard record using the field - const dashboardVariable = await dashboardVariableRepo.findOne({ - where: { - id: variableId, - }, - }); - return dashboardVariable; - } - - async saveDashboardToConfig(entity: Dashboard) { - if (!entity) { - this.logger.debug('No entity found in the DashboardSubscriber saveDashboardToConfig method'); - return; - } - - // Validate dashboard details - const dashboard = entity as Dashboard; - const moduleMetadata = entity.module; - if (!moduleMetadata) { - throw new Error(`Module metadata not found for dashboard id ${entity.id}`); - } - - // Get config file details - const { filePath, metaData } = await this.getConfigFileDetails(moduleMetadata.name); - if (!filePath || !metaData) { - throw new Error(`Configuration details not found for module: ${moduleMetadata.name}`); - } - - // Write the dashboard to the config file - await this.writeToConfig(metaData, dashboard, filePath); - } - - private async getConfigFileDetails(moduleName: string): Promise<{ filePath: string; metaData: any }> { - const filePath = await this.moduleMetadataHelperService.getModuleMetadataFilePath(moduleName); - try { - await fs.access(filePath); - } catch (error: any) { - throw new Error(`Configuration file not found for module: ${moduleName}`); - } - const metaData = await this.moduleMetadataHelperService.getModuleMetadataConfiguration(filePath); - return { filePath, metaData }; - } - - private async writeToConfig(metaData: any, dashboard: Dashboard, filePath: string) { - if (metaData.dashboards && Array.isArray(metaData.dashboards)) { - const dashboardIndex = metaData.dashboards?.findIndex((dashboardFromFile: { name: string; }) => dashboardFromFile.name === dashboard.name); - const dto = await this.dashboardMapper.toDto(dashboard); - if (dashboardIndex !== -1) { - metaData.dashboards[dashboardIndex] = dto; - } - else { - metaData.dashboards.push(dto); - } - } - else { - const dashboards = []; - const dto = await this.dashboardMapper.toDto(dashboard); - dashboards.push(dto); - metaData.dashboards = dashboards; - } - // Write the updated object back to the file - const updatedContent = JSON.stringify(metaData, null, 2); - await fs.writeFile(filePath, updatedContent); - } -} \ No newline at end of file diff --git a/src/services/genai/mcp-handlers/solid-add-question-to-dashboard-mcp-handler.service.ts b/src/services/genai/mcp-handlers/solid-add-question-to-dashboard-mcp-handler.service.ts deleted file mode 100644 index 9f7d021c..00000000 --- a/src/services/genai/mcp-handlers/solid-add-question-to-dashboard-mcp-handler.service.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Injectable } from "@nestjs/common"; -import { plainToInstance } from "class-transformer"; -import { CreateDashboardQuestionDto } from "src/dtos/create-dashboard-question.dto"; -import { AiInteraction } from "src/entities/ai-interaction.entity"; -import { IMcpToolResponseHandler } from "../../../interfaces"; -import { DashboardQuestionService } from "../../dashboard-question.service"; - -@Injectable() -export class SolidAddQuestionToDashboardMcpHandler implements IMcpToolResponseHandler { - - constructor( - private readonly dashboardQuestionService: DashboardQuestionService, - ) { - } - - async apply(aiInteraction: AiInteraction) { - const escapedMessage = aiInteraction.message.replace(/\\'/g, "'"); - const aiResponseMessage = JSON.parse(escapedMessage); - - const {data} = aiResponseMessage; - const { dashboardUserKey, schema} = data; - - //FIXME: Replace \' with ' in the response, since the AI response seems to contain \' which is invalid JSON. - // This is a workaround for now, until we find a better solution. - // const aiResponseMessageReplaced = aiResponseMessage['message'].replace(/\\'/g, "'"); - // const dashboardUserKey = aiResponseMessageReplaced['dashboardUserKey']; - // if (!dashboardUserKey) { - // throw new Error("Dashboard User Key is required to create a Dashboard Question."); - // } - const dashboardQuestionDto = plainToInstance(CreateDashboardQuestionDto, schema); - dashboardQuestionDto['questionSqlDatasetConfigsCommand'] = "update"; - dashboardQuestionDto['dashboardUserKey'] = dashboardUserKey; - - const dashboardQuestion = await this.dashboardQuestionService.create(dashboardQuestionDto, []); - - // TODO: decide on some shape to return hre... - return { - seedingRequired: false, - serverRebooting: false, - } - } - -} \ No newline at end of file diff --git a/src/services/genai/mcp-handlers/solid-add-variable-to-dashboard-mcp-handler.service.ts b/src/services/genai/mcp-handlers/solid-add-variable-to-dashboard-mcp-handler.service.ts deleted file mode 100644 index 95a899f9..00000000 --- a/src/services/genai/mcp-handlers/solid-add-variable-to-dashboard-mcp-handler.service.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Injectable } from "@nestjs/common"; -import { plainToInstance } from "class-transformer"; -import { CreateDashboardVariableDto } from "src/dtos/create-dashboard-variable.dto"; -import { AiInteraction } from "src/entities/ai-interaction.entity"; -import { DashboardVariableService } from "src/services/dashboard-variable.service"; -import { IMcpToolResponseHandler } from "../../../interfaces"; - -@Injectable() -export class SolidAddVariableToDashboardMcpHandler implements IMcpToolResponseHandler { - - constructor( - private readonly dashboardVariableService: DashboardVariableService, - ) { - } - - async apply(aiInteraction: AiInteraction) { - const escapedMessage = aiInteraction.message.replace(/\\'/g, "'"); - const aiResponseMessage = JSON.parse(escapedMessage); - - const {data} = aiResponseMessage; - const { dashboardUserKey, schema} = data; - - //FIXME: Replace \' with ' in the response, since the AI response seems to contain \' which is invalid JSON. - // This is a workaround for now, until we find a better solution. - // const aiResponseMessageReplaced = aiResponseMessage['message'].replace(/\\'/g, "'"); - // const dashboardUserKey = aiResponseMessageReplaced['dashboardUserKey']; - // if (!dashboardUserKey) { - // throw new Error("Dashboard User Key is required to create a Dashboard Question."); - // } - const dashboardVariableDto = plainToInstance(CreateDashboardVariableDto, schema); - dashboardVariableDto['selectionStaticValues'] = JSON.stringify(dashboardVariableDto['selectionStaticValues'] || []); - dashboardVariableDto['defaultValue'] = JSON.stringify(dashboardVariableDto['defaultValue'] || []); - dashboardVariableDto['dashboardUserKey'] = dashboardUserKey; - - const dashboardVariable = await this.dashboardVariableService.create(dashboardVariableDto, []); - - // TODO: decide on some shape to return hre... - return { - seedingRequired: false, - serverRebooting: false, - } - } - -} \ No newline at end of file diff --git a/src/services/genai/mcp-handlers/solid-create-dashboard-mcp-handler.service.ts b/src/services/genai/mcp-handlers/solid-create-dashboard-mcp-handler.service.ts deleted file mode 100644 index 4eaf3dcd..00000000 --- a/src/services/genai/mcp-handlers/solid-create-dashboard-mcp-handler.service.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { Injectable } from "@nestjs/common"; -import { plainToInstance } from "class-transformer"; -import { CreateDashboardDto } from "src/dtos/create-dashboard.dto"; -import { UpdateMenuItemMetadataDto } from "src/dtos/update-menu-item-metadata.dto"; -import { AiInteraction } from "src/entities/ai-interaction.entity"; -import { Dashboard } from "src/entities/dashboard.entity"; -import { IMcpToolResponseHandler } from "../../../interfaces"; -import { ActionMetadataService } from "../../action-metadata.service"; -import { DashboardService } from "../../dashboard.service"; -import { MenuItemMetadataService } from "../../menu-item-metadata.service"; -import { ModelMetadataService } from "../../model-metadata.service"; -import { ModuleMetadataService } from "../../module-metadata.service"; -import { RoleMetadataService } from "../../role-metadata.service"; - -@Injectable() -export class SolidCreateDashboardWithWidgetsMcpHandler implements IMcpToolResponseHandler { - - constructor( - private readonly dashboardService: DashboardService, - private readonly actionMetadataService: ActionMetadataService, - private readonly menuItemMetadataService: MenuItemMetadataService, - private readonly moduleMetadataService: ModuleMetadataService, - private readonly modelMetadataService: ModelMetadataService, - private readonly roleService: RoleMetadataService, // Assuming roleService is a Mongoose model, adjust as necessary - ) { - } - - async apply(aiInteraction: AiInteraction) { - const escapedMessage = aiInteraction.message.replace(/\\'/g, "'"); - const aiResponseMessage = JSON.parse(escapedMessage); - const { data } = aiResponseMessage; - const { schema } = data; - const { dashboardDto, dashboard } = await this.createDashboard(schema); - - const { moduleUserKey, actionMetadataEntity } = await this.createActionMetadataEntry(dashboardDto, dashboard); - - await this.createMenuItemEntry(dashboard, moduleUserKey, actionMetadataEntity); - - // TODO: decide on some shape to return hre... - return { - seedingRequired: false, - serverRebooting: false, - } - } - - - private async createMenuItemEntry(dashboard: Dashboard, moduleUserKey: string, actionMetadataEntity: any) { - const menuData = { - displayName: dashboard.displayName || dashboard.name, - name: `${moduleUserKey}-${dashboard.name}-dashboard-menu-item`, - sequenceNumber: 1, - actionUserKey: actionMetadataEntity.name, - moduleUserKey: moduleUserKey, - parentMenuItemUserKey: '', - }; - - const adminRole = await this.roleService.findRoleByName('Admin'); - const specifiedRoles = menuData['roles']; - - // If the developer has specified roles, then resolve them, making sure that admin role is also given. - const specifiedRoleObjects = [adminRole]; - if (specifiedRoles) { - for (let i = 0; i < specifiedRoles.length; i++) { - const specifiedRole = specifiedRoles[i]; - const specifiedRoleObject = await this.roleService.findRoleByName(specifiedRole); - if (!specifiedRoleObject) { - throw new Error(`Invalid role: (${specifiedRole}) specified against menu with display name ${menuData.displayName}.`); - } - specifiedRoleObjects.push(specifiedRoleObject); - } - } - - menuData['roles'] = specifiedRoleObjects; - menuData['action'] = await this.actionMetadataService.findOneByUserKey(menuData.actionUserKey); - menuData['module'] = await this.moduleMetadataService.findOneByUserKey(menuData.moduleUserKey); - - if (menuData.parentMenuItemUserKey) { - menuData['parentMenuItem'] = await this.menuItemMetadataService.findOneByUserKey(menuData.parentMenuItemUserKey); - } else { - menuData['parentMenuItem'] = null; - } - await this.menuItemMetadataService.upsert(menuData as unknown as UpdateMenuItemMetadataDto); - } - - private async createActionMetadataEntry(dashboardDto: CreateDashboardDto, dashboard: Dashboard) { - const moduleUserKey = dashboardDto.moduleUserKey; - const actionMetadata = { - name: `${dashboard.name}-view`, - displayName: dashboard.displayName || dashboard.name, - type: 'custom', - domain: "{}", - context: "{}", - customComponent: `/admin/core/${moduleUserKey}/dashboards?dashboardName=${dashboard.name}`, - customIsModal: true, - serverEndpoint: '', - moduleUserKey: moduleUserKey, - modelUserKey: '', - viewUserKey: `${dashboard.name}-view`, - }; - actionMetadata['module'] = await this.moduleMetadataService.findOneByUserKey(actionMetadata.moduleUserKey); - if (actionMetadata.modelUserKey) { - actionMetadata['model'] = await this.modelMetadataService.findOneByUserKey(actionMetadata.modelUserKey); - } - const actionMetadataEntity = await this.actionMetadataService.upsert(actionMetadata); - return { moduleUserKey, actionMetadataEntity }; - } - - private async createDashboard(aiResponseMessage: any) { - const dashboardDto = plainToInstance(CreateDashboardDto, aiResponseMessage); - dashboardDto['layoutJson'] = JSON.stringify(dashboardDto['layoutJson']); - const dashboard = await this.dashboardService.create(dashboardDto, []); - return { dashboardDto, dashboard }; - } -} \ No newline at end of file diff --git a/src/services/genai/mcp-handlers/solid-create-dashboard-question-mcp-handler.service.ts b/src/services/genai/mcp-handlers/solid-create-dashboard-question-mcp-handler.service.ts deleted file mode 100644 index e8836bba..00000000 --- a/src/services/genai/mcp-handlers/solid-create-dashboard-question-mcp-handler.service.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { Injectable } from "@nestjs/common"; -import { plainToInstance } from "class-transformer"; -import { CreateDashboardQuestionDto } from "src/dtos/create-dashboard-question.dto"; -import { AiInteraction } from "src/entities/ai-interaction.entity"; -import { IMcpToolResponseHandler } from "../../../interfaces"; -import { DashboardQuestionService } from "../../dashboard-question.service"; - -@Injectable() -export class SolidCreateDashboardQuestionMcpHandler implements IMcpToolResponseHandler { - - constructor( - private readonly dashboardQuestionService: DashboardQuestionService, - ) { - } - - async apply(aiInteraction: AiInteraction) { - const escapedMessage = aiInteraction.message.replace(/\\'/g, "'"); - const aiResponseMessage = JSON.parse(escapedMessage); - - //FIXME: Replace \' with ' in the response, since the AI response seems to contain \' which is invalid JSON. - // This is a workaround for now, until we find a better solution. - // const aiResponseMessageReplaced = aiResponseMessage['message'].replace(/\\'/g, "'"); - // const dashboardUserKey = aiResponseMessageReplaced['dashboardUserKey']; - // if (!dashboardUserKey) { - // throw new Error("Dashboard User Key is required to create a Dashboard Question."); - // } - const dashboardQuestionDto = plainToInstance(CreateDashboardQuestionDto, aiResponseMessage); - - const dashboardQuestion = await this.dashboardQuestionService.create(dashboardQuestionDto, []); - - // TODO: decide on some shape to return hre... - return { - seedingRequired: false, - serverRebooting: false, - } - } - -} \ No newline at end of file diff --git a/src/services/genai/mcp-handlers/solid-create-dashboard-question-sql-dataset-config-mcp-handler.service.ts b/src/services/genai/mcp-handlers/solid-create-dashboard-question-sql-dataset-config-mcp-handler.service.ts deleted file mode 100644 index dd8d3d03..00000000 --- a/src/services/genai/mcp-handlers/solid-create-dashboard-question-sql-dataset-config-mcp-handler.service.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Injectable } from "@nestjs/common"; -import { plainToInstance } from "class-transformer"; -import { AiInteraction } from "src/entities/ai-interaction.entity"; -import { IMcpToolResponseHandler } from "../../../interfaces"; -import { DashboardQuestionSqlDatasetConfigService } from "../../dashboard-question-sql-dataset-config.service"; -import { CreateDashboardQuestionSqlDatasetConfigDto } from "src/dtos/create-dashboard-question-sql-dataset-config.dto"; - -@Injectable() -export class SolidCreateDashboardQuestionSqlDatasetConfigMcpHandler implements IMcpToolResponseHandler { - - constructor( - private readonly dashboardQuestionSqlDatasetConfigService: DashboardQuestionSqlDatasetConfigService, - ) { - } - - async apply(aiInteraction: AiInteraction) { - // const aiResponse = JSON.parse(aiInteraction.message); - const escapedMessage = aiInteraction.message.replace(/\\'/g, "'"); - const aiResponseMessage = JSON.parse(escapedMessage); - - // FIXME: Replace \' with ' in the response, since the AI response seems to contain \' which is invalid JSON. - // This is a workaround for now, until we find a better solution. - // const aiResponseMessageReplaced = aiResponse['message'].replace(/\\'/g, "'"); - // const dashboardUserKey = aiResponseMessageReplaced['dashboardUserKey']; - // if (!dashboardUserKey) { - // throw new Error("Dashboard User Key is required to create a Dashboard Question."); - // } - const dto = plainToInstance(CreateDashboardQuestionSqlDatasetConfigDto, aiResponseMessage); - dto['options'] = JSON.stringify(dto['options']); - - const dashboardQuestion = await this.dashboardQuestionSqlDatasetConfigService.create(dto, []); - - // TODO: decide on some shape to return hre... - return { - seedingRequired: false, - serverRebooting: false, - } - } - -} \ No newline at end of file diff --git a/src/services/genai/mcp-handlers/solid-create-dashboard-widget-mcp-handler.service.ts b/src/services/genai/mcp-handlers/solid-create-dashboard-widget-mcp-handler.service.ts deleted file mode 100644 index 25362a0d..00000000 --- a/src/services/genai/mcp-handlers/solid-create-dashboard-widget-mcp-handler.service.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Injectable } from "@nestjs/common"; -import { plainToInstance } from "class-transformer"; -import { CreateDashboardQuestionDto } from "src/dtos/create-dashboard-question.dto"; -import { AiInteraction } from "src/entities/ai-interaction.entity"; -import { IMcpToolResponseHandler } from "../../../interfaces"; -import { DashboardQuestionService } from "../../dashboard-question.service"; - -@Injectable() -export class SolidCreateDashboardWidgetMcpHandler implements IMcpToolResponseHandler { - - constructor( - private readonly dashboardQuestionService: DashboardQuestionService, - ) { - } - - async apply(aiInteraction: AiInteraction) { - const escapedMessage = aiInteraction.message.replace(/\\'/g, "'"); - const aiResponseMessage = JSON.parse(escapedMessage); - - //FIXME: Replace \' with ' in the response, since the AI response seems to contain \' which is invalid JSON. - // This is a workaround for now, until we find a better solution. - // const aiResponseMessageReplaced = aiResponseMessage['message'].replace(/\\'/g, "'"); - // const dashboardUserKey = aiResponseMessageReplaced['dashboardUserKey']; - // if (!dashboardUserKey) { - // throw new Error("Dashboard User Key is required to create a Dashboard Question."); - // } - const dashboardQuestionDto = plainToInstance(CreateDashboardQuestionDto, aiResponseMessage); - dashboardQuestionDto['questionSqlDatasetConfigsCommand'] = "update"; - - const dashboardQuestion = await this.dashboardQuestionService.create(dashboardQuestionDto, []); - - // TODO: decide on some shape to return hre... - return { - seedingRequired: false, - serverRebooting: false, - } - } - -} \ No newline at end of file diff --git a/src/services/question-data-providers/chartjs-sql-data-provider.service.ts b/src/services/question-data-providers/chartjs-sql-data-provider.service.ts deleted file mode 100644 index c6322deb..00000000 --- a/src/services/question-data-providers/chartjs-sql-data-provider.service.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { Injectable, Logger } from "@nestjs/common"; -import { DashboardQuestionDataProvider } from "src/decorators/dashboard-question-data-provider.decorator"; -import { DashboardQuestion } from "src/entities/dashboard-question.entity"; -import { IDashboardQuestionDataProvider, QuestionSqlDataProviderContext } from "src/interfaces"; -import { EntityManager } from "typeorm"; -import { SqlExpressionResolverService } from "../sql-expression-resolver.service"; -import { getKpi, getLabels } from "./helpers"; - - -export enum SqlExpressionOperator { - EQUALS = '$equals', - NOT_EQUALS = '$notEquals', - CONTAINS = '$contains', - NOT_CONTAINS = '$notContains', - STARTS_WITH = '$startsWith', - ENDS_WITH = '$endsWith', - IN = '$in', - NOT_IN = '$notIn', - BETWEEN = '$between', - LT = '$lt', - LTE = '$lte', - GT = '$gt', - GTE = '$gte' -} - -export interface SqlExpression { - variableName: string; // The name of the variable in the SQL query - operator: SqlExpressionOperator; // The operator to use for the replacement (e.g., '=', '>', '<', etc.) - value: string[]; // The value to replace the variable with -} - -@DashboardQuestionDataProvider() -@Injectable() -export class ChartJsSqlDataProvider implements IDashboardQuestionDataProvider { - private readonly logger = new Logger(ChartJsSqlDataProvider.name); - - constructor(private readonly entityManager: EntityManager, private readonly sqlExpressionResolver: SqlExpressionResolverService) { } - - help(): string { - return "Provides data for dashboard questions using a SQL dataset configuration. Configure your SQL dataset in the admin panel, then reference it in your dashboard question to fetch data."; - } - - name(): string { - return "ChartJsSqlDataProvider"; - } - - async getData(question: DashboardQuestion, context?: QuestionSqlDataProviderContext): Promise { - const expressions: SqlExpression[] = context?.expressions || []; - // TODO: put some validation to check if the results of each SQL in each dataset returns the same number of rows - - // This is what we have to return. - // const data = { - // labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July'], - // datasets: [ - // { - // label: 'Dataset 1', - // data: labels.map(() => faker.number.int({ min: 0, max: 1000 })), - // backgroundColor: 'rgba(255, 99, 132, 0.5)', - // }, - // { - // label: 'Dataset 2', - // data: labels.map(() => faker.number.int({ min: 0, max: 1000 })), - // backgroundColor: 'rgba(53, 162, 235, 0.5)', - // }, - // { - // label: 'Dataset 3', - // data: labels.map(() => faker.number.int({ min: 0, max: 1000 })), - // backgroundColor: 'rgba(53, 235, 162, 0.5)', - // }, - // ], - // }; - - // TODO: Load the set of labels by using a separate field on the question entity. - - let datasetIdx = 0; - const datasets = []; - - const labels: string[] = await getLabels(question, expressions, this.entityManager, this.sqlExpressionResolver); - const kpi: string = await getKpi(question, expressions, this.entityManager, this.sqlExpressionResolver); - - // const question = context.question; - for (const questionSqlDatasetConfig of question.questionSqlDatasetConfigs) { - - const sql = questionSqlDatasetConfig.sql; - if (!sql) { - throw new Error(`SQL dataset ${questionSqlDatasetConfig.datasetName} configuration does not contain a valid SQL query.`); - } - - const sqlReplacementResult = this.sqlExpressionResolver.resolveSqlWithExpressions(sql, expressions || []); - this.logger.debug(`Final Sql query for dataset [${questionSqlDatasetConfig.datasetName}] is query=[${sqlReplacementResult.rawSql}]`); - this.logger.debug(`Final Sql query for dataset [${questionSqlDatasetConfig.datasetName}] is parameters=[${JSON.stringify(sqlReplacementResult.parameters)}]`); - const results = await this.entityManager.query(sqlReplacementResult.rawSql, sqlReplacementResult.parameters); - - // Also for each data set we create the dataset object as is expected by ChartJs. - const data = []; - for (let i = 0; i < results.length; i++) { - const result = results[i]; - data.push(result[questionSqlDatasetConfig.valueColumnName]); - } - datasets.push({ - label: questionSqlDatasetConfig.datasetDisplayName, - data: data, - ...JSON.parse(questionSqlDatasetConfig.options || '{}'), - }); - - datasetIdx++; - } - - return { - kpi, - visualizedAs: question.visualisedAs, - visualizationData: { - labels, - datasets - } - }; - - } - - -} \ No newline at end of file diff --git a/src/services/question-data-providers/helpers.ts b/src/services/question-data-providers/helpers.ts deleted file mode 100644 index 39b369ac..00000000 --- a/src/services/question-data-providers/helpers.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { DashboardQuestion } from "src/entities/dashboard-question.entity"; -import { EntityManager } from "typeorm"; -import { SqlExpression } from "./chartjs-sql-data-provider.service"; -import { SqlExpressionResolverService } from "../sql-expression-resolver.service"; - -export async function getLabels(question: DashboardQuestion, expressions: SqlExpression[], entityManager: EntityManager, sqlExpressionResolver: SqlExpressionResolverService): Promise { - const sql = question.labelSql; - if (!sql) { - return []; - } - const sqlReplacementResult = sqlExpressionResolver.resolveSqlWithExpressions(sql, expressions || []); - - const labelResults = await entityManager.query(sqlReplacementResult.rawSql, sqlReplacementResult.parameters); - - // Assuming labelResults has a single row with a 'label' field - // Map the label results to the labels array - const labels: string[] = labelResults.map((result: { [x: string]: string; }) => result['label']); - return labels; -} - -export async function getKpi(question: DashboardQuestion, expressions: SqlExpression[], entityManager: EntityManager, sqlExpressionResolver: SqlExpressionResolverService): Promise { - const sql = question.kpiSql; - if (!sql) { - return ""; - } - const sqlReplacementResult = sqlExpressionResolver.resolveSqlWithExpressions(sql, expressions || []); - const result = await entityManager.query(sqlReplacementResult.rawSql, sqlReplacementResult.parameters); - const kpiResult = result.pop(); - return kpiResult?.kpi || ""; -} \ No newline at end of file diff --git a/src/services/question-data-providers/prime-react-datatable-sql-data-provider.service.ts b/src/services/question-data-providers/prime-react-datatable-sql-data-provider.service.ts deleted file mode 100644 index 07aae620..00000000 --- a/src/services/question-data-providers/prime-react-datatable-sql-data-provider.service.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { Injectable } from "@nestjs/common"; -import { DashboardQuestionDataProvider } from "src/decorators/dashboard-question-data-provider.decorator"; -import { DashboardQuestion } from "src/entities/dashboard-question.entity"; -import { IDashboardQuestionDataProvider, QuestionSqlDataProviderContext } from "src/interfaces"; -import { EntityManager } from "typeorm"; -import { SqlExpressionResolverService } from "../sql-expression-resolver.service"; -import { Logger } from '@nestjs/common'; -import { SqlExpression } from "./chartjs-sql-data-provider.service"; -import { getKpi } from "./helpers"; - -@DashboardQuestionDataProvider() -@Injectable() -export class PrimeReactDatatableSqlDataProvider implements IDashboardQuestionDataProvider { - private readonly logger = new Logger(PrimeReactDatatableSqlDataProvider.name); - - constructor(private readonly entityManager: EntityManager, private readonly sqlExpressionResolver: SqlExpressionResolverService) { } - - help(): string { - return "Provides data for dashboard questions using a SQL dataset configuration. Configure your SQL dataset in the admin panel, then reference it in your dashboard question to fetch data."; - } - - name(): string { - return "PrimeReactDatatableSqlDataProvider"; - } - - async getData(question: DashboardQuestion, context?: QuestionSqlDataProviderContext): Promise { - const expressions: SqlExpression[] = context?.expressions || []; - - // TODO: put some validation to check if the results of each SQL in each dataset returns the same number of rows - - // Check the expected response for prime react data tables to understand what is going on here... - - const kpi: string = await getKpi(question, expressions, this.entityManager, this.sqlExpressionResolver); - // TODO: Load the set of labels by using a separate field on the question entity. - const labelSql = question.labelSql; - const labelResults = await this.entityManager.query(labelSql); - const columns = []; - for (let i = 0; i < labelResults.length; i++) { - const labelResult = labelResults[i]; - columns.push({ - field: labelResult['field'], - header: labelResult['header'], - }); - } - - // Load the chart options as a JSON - // const chartOptions = JSON.parse(question.barChartLabelOptions || '{}'); - - const values = [] - - // For meter group we can assume that we only have one sql dataset config. - const questionSqlDatasetConfig = question.questionSqlDatasetConfigs[0]; - - const sql = questionSqlDatasetConfig.sql; - if (!sql) { - throw new Error(`SQL dataset ${questionSqlDatasetConfig.datasetName} configuration does not contain a valid SQL query.`); - } - - const sqlReplacementResult = this.sqlExpressionResolver.resolveSqlWithExpressions(sql, expressions || []); - this.logger.debug(`Final Sql query for dataset [${questionSqlDatasetConfig.datasetName}] is query=[${sqlReplacementResult.rawSql}]`); - this.logger.debug(`Final Sql query for dataset [${questionSqlDatasetConfig.datasetName}] is parameters=[${JSON.stringify(sqlReplacementResult.parameters)}]`); - const results = await this.entityManager.query(sqlReplacementResult.rawSql, sqlReplacementResult.parameters); - - return { - kpi, - visualisedAs: question.visualisedAs, - visualizationData: { - columns, - rows: results, - } - }; - - } -} \ No newline at end of file diff --git a/src/services/question-data-providers/prime-react-meter-group-sql-data-provider.service.ts b/src/services/question-data-providers/prime-react-meter-group-sql-data-provider.service.ts deleted file mode 100644 index a7af5e74..00000000 --- a/src/services/question-data-providers/prime-react-meter-group-sql-data-provider.service.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { Injectable } from "@nestjs/common"; -import { DashboardQuestionDataProvider } from "src/decorators/dashboard-question-data-provider.decorator"; -import { DashboardQuestion } from "src/entities/dashboard-question.entity"; -import { IDashboardQuestionDataProvider, QuestionSqlDataProviderContext } from "src/interfaces"; -import { EntityManager } from "typeorm"; -import { SqlExpressionResolverService } from "../sql-expression-resolver.service"; -import { Logger } from '@nestjs/common'; -import { SqlExpression } from "./chartjs-sql-data-provider.service"; -import { getKpi } from "./helpers"; - -@DashboardQuestionDataProvider() -@Injectable() -export class PrimeReactMeterGroupSqlDataProvider implements IDashboardQuestionDataProvider { - private readonly logger = new Logger(PrimeReactMeterGroupSqlDataProvider.name); - - constructor(private readonly entityManager: EntityManager, private readonly sqlExpressionResolver: SqlExpressionResolverService) { } - - help(): string { - return "Provides data for dashboard questions using a SQL dataset configuration. Configure your SQL dataset in the admin panel, then reference it in your dashboard question to fetch data."; - } - - name(): string { - return "PrimeReactMeterGroupSqlDataProvider"; - } - - hslToHex(h: number, s: number, l: number): string { - l /= 100; - s /= 100; - - const k = (n: number) => (n + h / 30) % 12; - const a = s * Math.min(l, 1 - l); - const f = (n: number) => - Math.round(255 * (l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1))))); - - return `#${f(0).toString(16).padStart(2, '0')}${f(8).toString(16).padStart(2, '0')}${f(4) - .toString(16) - .padStart(2, '0')}`; - } - - generateDistinctColors(count: number): string[] { - const colors: string[] = []; - - const hueStep = 360 / count; - const saturation = 65; // keep it vibrant - const lightness = 55; // balanced for both light/dark themes - - for (let i = 0; i < count; i++) { - const hue = Math.round(i * hueStep); - colors.push(this.hslToHex(hue, saturation, lightness)); - } - - return colors; - } - - async getData(question: DashboardQuestion, context?: QuestionSqlDataProviderContext): Promise { - const expressions: SqlExpression[] = context?.expressions || []; - - // TODO: put some validation to check if the results of each SQL in each dataset returns the same number of rows - - // This is what we have to return. - // const values = [ - // { label: 'Apps', color: '#34d399', value: 16 }, - // { label: 'Messages', color: '#fbbf24', value: 8 }, - // { label: 'Media', color: '#60a5fa', value: 24 }, - // { label: 'System', color: '#c084fc', value: 10 } - // ]; - - // TODO: Load the set of labels by using a separate field on the question entity. - - const kpi: string = await getKpi(question, expressions, this.entityManager, this.sqlExpressionResolver); - - // Load the chart options as a JSON - const chartOptions = JSON.parse(question.chartOptions || '{}'); - - const values = [] - - // For meter group we can assume that we only have one sql dataset config. - const questionSqlDatasetConfig = question.questionSqlDatasetConfigs[0]; - - const sql = questionSqlDatasetConfig.sql; - if (!sql) { - throw new Error(`SQL dataset ${questionSqlDatasetConfig.datasetName} configuration does not contain a valid SQL query.`); - } - - const sqlReplacementResult = this.sqlExpressionResolver.resolveSqlWithExpressions(sql, expressions || []); - this.logger.debug(`Final Sql query for dataset [${questionSqlDatasetConfig.datasetName}] is query=[${sqlReplacementResult.rawSql}]`); - this.logger.debug(`Final Sql query for dataset [${questionSqlDatasetConfig.datasetName}] is parameters=[${JSON.stringify(sqlReplacementResult.parameters)}]`); - const results = await this.entityManager.query(sqlReplacementResult.rawSql, sqlReplacementResult.parameters); - - const colors = this.generateDistinctColors(results.length); - - // Also for each data set we create the dataset object as is expected by ChartJs. - for (let i = 0; i < results.length; i++) { - const result = results[i]; - - const colorFromChartOptions = chartOptions?.colors?.[result[questionSqlDatasetConfig.labelColumnName]]; - const color = typeof colorFromChartOptions === 'string' ? colorFromChartOptions : colors[i]; - - values.push({ - label: result[questionSqlDatasetConfig.labelColumnName], - color: color, - value: result[questionSqlDatasetConfig.valueColumnName] - }) - } - - return { - kpi, - visualizedAs: question.visualisedAs, - visualizationData: { - dataset: values - }, - }; - - } -} \ No newline at end of file diff --git a/src/services/selection-providers/list-of-dashboard-question-providers-selection-provider.service.ts b/src/services/selection-providers/list-of-dashboard-question-providers-selection-provider.service.ts deleted file mode 100644 index 463a80fd..00000000 --- a/src/services/selection-providers/list-of-dashboard-question-providers-selection-provider.service.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Injectable } from "@nestjs/common"; -import { SelectionProvider } from "src/decorators/selection-provider.decorator"; -import { SolidRegistry } from "src/helpers/solid-registry"; -import { IDashboardQuestionDataProvider, IDashboardVariableSelectionProvider, ISelectionProvider, ISelectionProviderContext, ISelectionProviderValues } from "../../interfaces"; -import { SQL_DYNAMIC_PROVIDER_NAME } from "../dashboard.service"; -import { CHARTJS_SQL_DATA_PROVIDER_NAME, INBUILT_SQL_DATA_PROVIDERS, PRIME_REACT_DATATABLE_SQL_DATA_PROVIDER_NAME, PRIME_REACT_METER_GROUP_SQL_DATA_PROVIDER_NAME } from "../dashboard-question.service"; - - -@SelectionProvider() -@Injectable() -export class ListOfDashboardQuestionProvidersSelectionProvider implements ISelectionProvider { - - constructor( - private readonly solidRegistry: SolidRegistry, - ) { - } - - help(): string { - return "# Allows one to dynamically fetch all the dashboard providers that are registered in the system. "; - } - - name(): string { - return 'ListOfDashboardQuestionProvidersSelectionProvider'; - } - - async value(optionValue: string, ctxt: ISelectionProviderContext): Promise { - const dashboardSelectionProvider: IDashboardQuestionDataProvider | undefined = this.solidRegistry.getDashboardQuestionDataProviderInstance(optionValue); - if (!dashboardSelectionProvider) { - return null; - } - - return { label: dashboardSelectionProvider.name(), value: dashboardSelectionProvider.name() }; - } - - async values(query: string, ctxt: ISelectionProviderContext): Promise { - const dashboardSelectionProviders = this.solidRegistry.getDashboardQuestionDataProviders() - //Exclude the SQL dynamic provider from the list, (since although it is a dashboard selection provider, it is not a valid option for the user to select) - return dashboardSelectionProviders - .filter(provider => !INBUILT_SQL_DATA_PROVIDERS.includes(provider.name())) - .map(i => { - return { label: i.name, value: i.name }; - }); - } -} \ No newline at end of file diff --git a/src/services/selection-providers/list-of-dashboard-variable-providers-selection-provider.service.ts b/src/services/selection-providers/list-of-dashboard-variable-providers-selection-provider.service.ts deleted file mode 100644 index cc910360..00000000 --- a/src/services/selection-providers/list-of-dashboard-variable-providers-selection-provider.service.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Injectable } from "@nestjs/common"; -import { SelectionProvider } from "src/decorators/selection-provider.decorator"; -import { SolidRegistry } from "src/helpers/solid-registry"; -import { IDashboardVariableSelectionProvider, ISelectionProvider, ISelectionProviderContext, ISelectionProviderValues } from "../../interfaces"; -import { SQL_DYNAMIC_PROVIDER_NAME } from "../dashboard.service"; - - -@SelectionProvider() -@Injectable() -export class ListOfDashboardVariableProvidersSelectionProvider implements ISelectionProvider { - - constructor( - private readonly solidRegistry: SolidRegistry, - ) { - } - - help(): string { - return "# Allows one to dynamically fetch all the dashboard providers that are registered in the system. "; - } - - name(): string { - return 'ListOfDashboardVariableProvidersSelectionProvider'; - } - - async value(optionValue: string, ctxt: ISelectionProviderContext): Promise { - const dashboardSelectionProvider: IDashboardVariableSelectionProvider | undefined = this.solidRegistry.getDashboardVariableSelectionProviderInstance(optionValue); - if (!dashboardSelectionProvider) { - return null; - } - - return { label: dashboardSelectionProvider.name(), value: dashboardSelectionProvider.name() }; - } - - async values(query: string, ctxt: ISelectionProviderContext): Promise { - const dashboardSelectionProviders = this.solidRegistry.getDashboardVariableSelectionProviders() - //Exclude the SQL dynamic provider from the list, (since although it is a dashboard selection provider, it is not a valid option for the user to select) - return dashboardSelectionProviders.filter(i => (i.name !== SQL_DYNAMIC_PROVIDER_NAME)).map(i => { - return { label: i.name, value: i.name }; - }); - } -} \ No newline at end of file diff --git a/src/services/solid-introspect.service.ts b/src/services/solid-introspect.service.ts index a58b8b20..4cfd4279 100755 --- a/src/services/solid-introspect.service.ts +++ b/src/services/solid-introspect.service.ts @@ -6,8 +6,6 @@ import { ModelMetadataRepository } from 'src/repository/model-metadata.repositor import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper'; import { getDataSourceToken } from '@nestjs/typeorm'; import { IS_COMPUTED_FIELD_PROVIDER } from 'src/decorators/computed-field-provider.decorator'; -import { IS_DASHBOARD_QUESTION_DATA_PROVIDER } from 'src/decorators/dashboard-question-data-provider.decorator'; -import { IS_DASHBOARD_VARIABLE_SELECTION_PROVIDER } from 'src/decorators/dashboard-selection-provider.decorator'; import { IS_ERROR_CODE_PROVIDER } from 'src/decorators/error-codes-provider.decorator'; import { IS_MAIL_PROVIDER } from 'src/decorators/mail-provider.decorator'; import { IS_SCHEDULED_JOB_PROVIDER } from 'src/decorators/scheduled-job-provider.decorator'; @@ -76,18 +74,6 @@ export class SolidIntrospectService implements OnApplicationBootstrap { this.solidRegistry.registerSettingsProvider(settingsProvider); }); - // Register all IDashboardSelectionProvider implementations - const dashboardVariableSelectionProviders = this.discoveryService.getProviders().filter((provider) => this.isDashboardVariableSelectionProvider(provider)); - dashboardVariableSelectionProviders.forEach((dashboardSelectionProvider) => { - this.solidRegistry.registerDashboardVariableSelectionProvider(dashboardSelectionProvider); - }); - - // Register all IDashboardSelectionProvider implementations - const dashboardQuestionDataProviders = this.discoveryService.getProviders().filter((provider) => this.isDashboardQuestionDataProvider(provider)); - dashboardQuestionDataProviders.forEach((provider) => { - this.solidRegistry.registerDashboardQuestionDataProvider(provider); - }); - // Register all IComputedProvider implementations const computedFieldProviders = this.discoveryService.getProviders().filter((provider) => this.isComputedFieldProvider(provider)); computedFieldProviders.forEach((computedFieldProvider) => { @@ -250,16 +236,6 @@ export class SolidIntrospectService implements OnApplicationBootstrap { } } - isDashboardQuestionDataProvider(providerWrapper: InstanceWrapper) { - const { instance } = providerWrapper; - if (!instance) return false; - const provider = this.reflector.get( - IS_DASHBOARD_QUESTION_DATA_PROVIDER, - instance.constructor, - ); - return !!provider; - } - // This method identifies a provider as a seeder if it has a seed method i.e duck typing private isSeeder(provider: InstanceWrapper) { const { instance } = provider; @@ -312,16 +288,6 @@ export class SolidIntrospectService implements OnApplicationBootstrap { return !!isSettingsProvider; } - private isDashboardVariableSelectionProvider(provider: InstanceWrapper) { - const { instance } = provider; - if (!instance) return false; - const isDashboardSelectionProvider = this.reflector.get( - IS_DASHBOARD_VARIABLE_SELECTION_PROVIDER, - instance.constructor, - ); - return !!isDashboardSelectionProvider; - } - private isComputedFieldProvider(provider: InstanceWrapper) { const { instance } = provider; if (!instance) return false; diff --git a/src/services/sql-expression-resolver.service.ts b/src/services/sql-expression-resolver.service.ts deleted file mode 100644 index 2a9ca298..00000000 --- a/src/services/sql-expression-resolver.service.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { Injectable } from "@nestjs/common"; -import { SqlExpression, SqlExpressionOperator } from "./question-data-providers/chartjs-sql-data-provider.service"; -import { RequestContextService } from "./request-context.service"; -import { ERROR_MESSAGES } from "src/constants/error-messages"; - -export interface SqlReplacementResult { - rawSql: string; - parameters: any[]; // Positional parameters -} - -@Injectable() -export class SqlExpressionResolverService { - constructor(private readonly requestContextService: RequestContextService) { } - resolveSqlWithExpressions(sql: string, expressions: SqlExpression[]): SqlReplacementResult { - const variableToColumnMap: Record = {}; - const variablePattern = /{{\s*(\w+)\s*\[\s*([\w.]+)\s*\]\s*}}/g; - - let paramIndex = 1; - const parameters: any[] = []; - - // Handle sql expression tokens like {{$activeUserId}} in the SQL string - if (sql.includes('{{$activeUserId}}')) { - const activeUser = this.requestContextService.getActiveUser(); - if (activeUser && activeUser.sub) { - // Replace custom placeholder with parameter placeholder ($1) - sql = sql.replace(/\{\{\$activeUserId\}\}/g, `$${paramIndex++}`); - // Add the active user ID to parameters - parameters.push(activeUser.sub); - } - } - - // --- Pass 1: extract variable -> column mappings --- - let simplifiedSql = sql.replace(variablePattern, (_, variableName, columnName) => { - variableToColumnMap[variableName] = columnName; - return `{{${variableName}}}`; - }); - - // --- Pass 2: Replace each variable with positional fragment --- - - for (const expr of expressions) { - const column = variableToColumnMap[expr.variableName]; - if (!column) continue; - - let sqlFragment = ''; - const placeholder = `{{${expr.variableName}}}`; - - switch (expr.operator) { - case SqlExpressionOperator.EQUALS: - sqlFragment = `${column} = $${paramIndex++}`; - parameters.push(expr.value[0]); - break; - - case SqlExpressionOperator.NOT_EQUALS: - sqlFragment = `${column} != $${paramIndex++}`; - parameters.push(expr.value[0]); - break; - - case SqlExpressionOperator.CONTAINS: - sqlFragment = `${column} LIKE $${paramIndex++}`; - parameters.push(`%${expr.value[0]}%`); - break; - - case SqlExpressionOperator.NOT_CONTAINS: - sqlFragment = `${column} NOT LIKE $${paramIndex++}`; - parameters.push(`%${expr.value[0]}%`); - break; - - case SqlExpressionOperator.STARTS_WITH: - sqlFragment = `${column} LIKE $${paramIndex++}`; - parameters.push(`${expr.value[0]}%`); - break; - - case SqlExpressionOperator.ENDS_WITH: - sqlFragment = `${column} LIKE $${paramIndex++}`; - parameters.push(`%${expr.value[0]}`); - break; - - case SqlExpressionOperator.IN: - const inParams = expr.value.map(val => { - parameters.push(val); - return `$${paramIndex++}`; - }); - sqlFragment = `${column} IN (${inParams.join(", ")})`; - break; - - case SqlExpressionOperator.NOT_IN: - const notInParams = expr.value.map(val => { - parameters.push(val); - return `$${paramIndex++}`; - }); - sqlFragment = `${column} NOT IN (${notInParams.join(", ")})`; - break; - - case SqlExpressionOperator.BETWEEN: - sqlFragment = `${column} BETWEEN $${paramIndex} AND $${paramIndex + 1}`; - parameters.push(expr.value[0], expr.value[1]); - paramIndex += 2; - break; - - case SqlExpressionOperator.LT: - sqlFragment = `${column} < $${paramIndex++}`; - parameters.push(expr.value[0]); - break; - - case SqlExpressionOperator.LTE: - sqlFragment = `${column} <= $${paramIndex++}`; - parameters.push(expr.value[0]); - break; - - case SqlExpressionOperator.GT: - sqlFragment = `${column} > $${paramIndex++}`; - parameters.push(expr.value[0]); - break; - - case SqlExpressionOperator.GTE: - sqlFragment = `${column} >= $${paramIndex++}`; - parameters.push(expr.value[0]); - break; - - default: - throw new Error(ERROR_MESSAGES.UNSUPPORTED_SQL_OPERATOR(expr.operator)); - } - simplifiedSql = simplifiedSql.replace(placeholder, sqlFragment); - } - - // --- Final cleanup: remove any remaining placeholders --- - simplifiedSql = simplifiedSql.replace(/{{\s*\w+\s*}}/g, ''); - - // Remove dangling where clause if no expressions were applied - simplifiedSql = simplifiedSql.replace(/\bwhere\b\s*$/i, '').trim(); - - // Need to handle scenarios of complex expression i.e with and / or clauses. probably need to have this logic in the sql expression object itself - - - return { - rawSql: simplifiedSql, - parameters - }; - } -} \ No newline at end of file diff --git a/src/solid-core.module.ts b/src/solid-core.module.ts old mode 100755 new mode 100644 index a2a640d5..893cd4b9 --- a/src/solid-core.module.ts +++ b/src/solid-core.module.ts @@ -179,12 +179,7 @@ import { ClsModule } from "nestjs-cls"; import { AiInteractionController } from "./controllers/ai-interaction.controller"; import { ChatterMessageDetailsController } from "./controllers/chatter-message-details.controller"; import { ChatterMessageController } from "./controllers/chatter-message.controller"; -import { DashboardQuestionSqlDatasetConfigController } from "./controllers/dashboard-question-sql-dataset-config.controller"; -import { DashboardQuestionController } from "./controllers/dashboard-question.controller"; -import { DashboardVariableController } from "./controllers/dashboard-variable.controller"; -import { DashboardLayoutController } from "./controllers/dashboard-layout.controller"; -import { DashboardController } from './controllers/dashboard.controller'; import { ExportTemplateController } from './controllers/export-template.controller'; import { ExportTransactionController } from './controllers/export-transaction.controller'; import { ImportTransactionErrorLogController } from './controllers/import-transaction-error-log.controller'; @@ -207,12 +202,7 @@ import { UserController } from './controllers/user.controller'; import { AiInteraction } from './entities/ai-interaction.entity'; import { ChatterMessageDetails } from './entities/chatter-message-details.entity'; import { ChatterMessage } from './entities/chatter-message.entity'; -import { DashboardQuestionSqlDatasetConfig } from './entities/dashboard-question-sql-dataset-config.entity'; -import { DashboardQuestion } from './entities/dashboard-question.entity'; -import { DashboardVariable } from './entities/dashboard-variable.entity'; -import { DashboardLayout } from './entities/dashboard-layout.entity'; -import { Dashboard } from './entities/dashboard.entity'; import { ExportTemplate } from './entities/export-template.entity'; import { ExportTransaction } from './entities/export-transaction.entity'; import { ImportTransactionErrorLog } from './entities/import-transaction-error-log.entity'; @@ -270,17 +260,11 @@ import { TriggerMcpClientPublisherRabbitmq } from "./jobs/rabbitmq/trigger-mcp-c import { TriggerMcpClientSubscriberRabbitmq } from "./jobs/rabbitmq/trigger-mcp-client-subscriber.service"; import { TwilioSmsQueuePublisherRabbitmq } from "./jobs/rabbitmq/twilio-sms-publisher.service"; import { TwilioSmsQueueSubscriberRabbitmq } from "./jobs/rabbitmq/twilio-sms-subscriber.service"; -import { DashboardMapper } from "./mappers/dashboard-mapper"; import { ListOfValuesMapper } from "./mappers/list-of-values-mapper"; import { ActionMetadataRepository } from "./repository/action-metadata.repository"; import { AiInteractionRepository } from "./repository/ai-interaction.repository"; import { ChatterMessageDetailsRepository } from "./repository/chatter-message-details.repository"; import { ChatterMessageRepository } from "./repository/chatter-message.repository"; -import { DashboardQuestionSqlDatasetConfigRepository } from "./repository/dashboard-question-sql-dataset-config.repository"; -import { DashboardQuestionRepository } from "./repository/dashboard-question.repository"; -import { DashboardVariableRepository } from "./repository/dashboard-variable.repository"; -import { DashboardRepository } from "./repository/dashboard.repository"; -import { DashboardLayoutRepository } from "./repository/dashboard-layout.repository"; import { EmailTemplateRepository } from './repository/email-template.repository'; import { ExportTemplateRepository } from './repository/export-template.repository'; @@ -322,13 +306,6 @@ import { ConcatEntityComputedFieldProvider } from './services/computed-fields/en import { NoopsEntityComputedFieldProviderService } from './services/computed-fields/entity/noops-entity-computed-field-provider.service'; import { CRUDService } from './services/crud.service'; import { CsvService } from './services/csv.service'; -import { DashboardQuestionSqlDatasetConfigService } from './services/dashboard-question-sql-dataset-config.service'; -import { DashboardQuestionService } from './services/dashboard-question.service'; -import { DashboardVariableSQLDynamicProvider } from './services/dashboard-selection-providers/dashboard-variable-sql-dynamic-provider.service'; -import { DasbhoardVariableTestDynamicProvider } from './services/dashboard-selection-providers/dashboard-variable-test-dynamic-provider.service'; -import { DashboardVariableService } from './services/dashboard-variable.service'; -import { DashboardService } from './services/dashboard.service'; -import { DashboardLayoutService } from './services/dashboard-layout.service'; import { ExcelService } from './services/excel.service'; import { ExportTemplateService } from './services/export-template.service'; @@ -342,9 +319,6 @@ import { LocaleService } from './services/locale.service'; import { FileS3StorageProvider } from './services/mediaStorageProviders/file-s3-storage-provider'; import { FileStorageProvider } from './services/mediaStorageProviders/file-storage-provider'; import { PollerService } from './services/poller.service'; -import { ChartJsSqlDataProvider } from './services/question-data-providers/chartjs-sql-data-provider.service'; -import { PrimeReactDatatableSqlDataProvider } from './services/question-data-providers/prime-react-datatable-sql-data-provider.service'; -import { PrimeReactMeterGroupSqlDataProvider } from './services/question-data-providers/prime-react-meter-group-sql-data-provider.service'; import { PublisherFactory } from './services/queues/publisher-factory.service'; import { RequestContextService } from './services/request-context.service'; import { RoleMetadataService } from './services/role-metadata.service'; @@ -355,14 +329,11 @@ import { AgentEventService } from './services/agent-event.service'; import { McpAuditLogService } from './services/mcp-audit-log.service'; import { SchedulerServiceImpl } from './services/scheduled-jobs/scheduler.service'; import { SecurityRuleService } from './services/security-rule.service'; -import { ListOfDashboardQuestionProvidersSelectionProvider } from './services/selection-providers/list-of-dashboard-question-providers-selection-provider.service'; -import { ListOfDashboardVariableProvidersSelectionProvider } from './services/selection-providers/list-of-dashboard-variable-providers-selection-provider.service'; import { ListOfScheduledJobsSelectionProvider } from './services/selection-providers/list-of-scheduled-jobs-selection-provider.service'; import { LocaleListSelectionProvider } from './services/selection-providers/locale-list-selection-provider.service'; import { SettingService } from './services/setting.service'; import { TwilioSMSService } from './services/sms/TwilioSMSService'; import { SolidTsMorphService } from './services/solid-ts-morph.service'; -import { SqlExpressionResolverService } from './services/sql-expression-resolver.service'; import { TextractService } from './services/textract.service'; import { UserActivityHistoryService } from './services/user-activity-history.service'; import { UserViewMetadataService } from './services/user-view-metadata.service'; @@ -371,10 +342,6 @@ import { Three60WhatsappService } from './services/whatsapp/Three60WhatsappServi import { AuditSubscriber } from './subscribers/audit.subscriber'; import { ComputedEntityFieldSubscriber } from './subscribers/computed-entity-field.subscriber'; import { CreatedByUpdatedBySubscriber } from './subscribers/created-by-updated-by.subscriber'; -import { DashboardQuestionSqlDatasetConfigSubscriber } from './subscribers/dashboard-question-sql-dataset-config.subscriber'; -import { DashboardQuestionSubscriber } from './subscribers/dashboard-question.subscriber'; -import { DashboardVariableSubscriber } from './subscribers/dashboard-variable.subscriber'; -import { DashboardSubscriber } from './subscribers/dashboard.subscriber'; import { ListOfValuesSubscriber } from './subscribers/list-of-values.subscriber'; import { ScheduledJobSubscriber } from './subscribers/scheduled-job.subscriber'; import { SecurityRuleSubscriber } from './subscribers/security-rule.subscriber'; @@ -409,11 +376,6 @@ import { Entity } from 'typeorm'; AiInteraction, ChatterMessage, ChatterMessageDetails, - Dashboard, - DashboardQuestion, - DashboardQuestionSqlDatasetConfig, - DashboardVariable, - DashboardLayout, EmailAttachment, EmailTemplate, ExportTemplate, @@ -485,11 +447,6 @@ import { Entity } from 'typeorm'; AuthenticationController, ChatterMessageController, ChatterMessageDetailsController, - DashboardController, - DashboardQuestionController, - DashboardQuestionSqlDatasetConfigController, - DashboardVariableController, - DashboardLayoutController, EmailTemplateController, ExportTemplateController, ExportTransactionController, @@ -753,35 +710,10 @@ import { Entity } from 'typeorm'; ComputedFieldEvaluationSubscriberRabbitmq, ConcatEntityComputedFieldProvider, UserActivityHistoryService, - DashboardService, - DashboardVariableService, - DashboardLayoutService, - DashboardQuestionService, - DashboardVariableSQLDynamicProvider, - DasbhoardVariableTestDynamicProvider, - ListOfDashboardVariableProvidersSelectionProvider, - ListOfDashboardQuestionProvidersSelectionProvider, - DashboardQuestionSqlDatasetConfigService, - ChartJsSqlDataProvider, - PrimeReactMeterGroupSqlDataProvider, - PrimeReactDatatableSqlDataProvider, - SqlExpressionResolverService, AiInteractionService, - DashboardMapper, - DashboardRepository, - DashboardSubscriber, - DashboardVariableSubscriber, - DashboardQuestionSubscriber, - DashboardQuestionSqlDatasetConfigSubscriber, NoopsEntityComputedFieldProviderService, McpHandlerFactory, - // SolidCreateDashboardWithWidgetsMcpHandler, - // SolidCreateDashboardQuestionMcpHandler, - // SolidCreateDashboardQuestionSqlDatasetConfigMcpHandler, - // SolidCreateDashboardWidgetMcpHandler, - // SolidAddVariableToDashboardMcpHandler, - // SolidAddQuestionToDashboardMcpHandler, SolidTsMorphService, @@ -803,10 +735,6 @@ import { Entity } from 'typeorm'; ChatterMessageRepository, ChatterMessageDetailsRepository, AiInteractionRepository, - DashboardQuestionSqlDatasetConfigRepository, - DashboardQuestionRepository, - DashboardVariableRepository, - DashboardLayoutRepository, EmailTemplateRepository, ExportTemplateRepository, ExportTransactionRepository, diff --git a/src/subscribers/dashboard-question-sql-dataset-config.subscriber.ts b/src/subscribers/dashboard-question-sql-dataset-config.subscriber.ts deleted file mode 100644 index 2f738e94..00000000 --- a/src/subscribers/dashboard-question-sql-dataset-config.subscriber.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Injectable, Logger } from "@nestjs/common"; -import { InjectDataSource } from "@nestjs/typeorm"; -import { DashboardQuestionSqlDatasetConfig } from "src/entities/dashboard-question-sql-dataset-config.entity"; -import { DashboardQuestion } from "src/entities/dashboard-question.entity"; -import { ModuleMetadataHelperService } from "src/helpers/module-metadata-helper.service"; -import { DashboardService } from "src/services/dashboard.service"; -import { DataSource, EntityManager, EntitySubscriberInterface, InsertEvent, UpdateEvent } from "typeorm"; - -@Injectable() -export class DashboardQuestionSqlDatasetConfigSubscriber implements EntitySubscriberInterface { - private readonly logger = new Logger(this.constructor.name); - constructor( - @InjectDataSource() - private readonly dataSource: DataSource, - readonly moduleMetadataHelperService: ModuleMetadataHelperService, - readonly dashboardService: DashboardService, // Assuming you have a DashboardService for custom queries - ) { - this.dataSource.subscribers.push(this); - } - - listenTo() { - return DashboardQuestionSqlDatasetConfig; - } - - async afterInsert(event: InsertEvent) { - const question = event.entity.question; - if (!question) { - this.logger.debug('No question found in the QuestionSqlDatasetConfigSubscriber afterInsert method'); - return; - } - await this.saveQuestionToConfig(question, event.queryRunner.manager); - } - - async afterUpdate(event: UpdateEvent) { - const question = event.databaseEntity.question; - if (!question) { - this.logger.debug('No question found in the QuestionSqlDatasetConfigSubscriber afterUpdate method'); - return; - } - await this.saveQuestionToConfig(question, event.queryRunner.manager); - } - - private async saveQuestionToConfig(question: DashboardQuestion, entityManager: EntityManager): Promise { - // Populate the dashboard for the question - const populatedQuestion = await entityManager.findOne(DashboardQuestion, { - where: { - id: question.id, - }, - relations: ['dashboard', 'dashboard.module', 'dashboard.dashboardVariables', 'dashboard.questions', 'dashboard.questions.questionSqlDatasetConfigs'], - }); - const dashboard = populatedQuestion?.dashboard; - - if (!dashboard) { - throw new Error(`Dashboard not found for question id ${question.id}`); - } - - // Call the saveDashboardToConfig method from the DashboardService - await this.dashboardService.saveDashboardToConfig(dashboard); - } - -} \ No newline at end of file diff --git a/src/subscribers/dashboard-question.subscriber.ts b/src/subscribers/dashboard-question.subscriber.ts deleted file mode 100644 index e768e948..00000000 --- a/src/subscribers/dashboard-question.subscriber.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { Injectable, Logger } from "@nestjs/common"; -import { InjectDataSource } from "@nestjs/typeorm"; -import { DashboardQuestion } from "src/entities/dashboard-question.entity"; -import { Dashboard } from "src/entities/dashboard.entity"; -import { ModuleMetadataHelperService } from "src/helpers/module-metadata-helper.service"; -import { DashboardService } from "src/services/dashboard.service"; -import { DataSource, EntityManager, EntitySubscriberInterface, InsertEvent, UpdateEvent } from "typeorm"; - -@Injectable() -export class DashboardQuestionSubscriber implements EntitySubscriberInterface { - private readonly logger = new Logger(this.constructor.name); - constructor( - @InjectDataSource() - private readonly dataSource: DataSource, - readonly moduleMetadataHelperService: ModuleMetadataHelperService, - readonly dashboardService: DashboardService, - ) { - this.dataSource.subscribers.push(this); - } - - listenTo() { - return DashboardQuestion; - } - - async afterInsert(event: InsertEvent) { - if (!event.entity) { - this.logger.debug('No question entity found in the QuestionSubscriber afterInsert method'); - return; - } - await this.saveDashboardToConfig(event.entity, event.queryRunner.manager); - } - - async afterUpdate(event: UpdateEvent) { - if (!event.databaseEntity) { - this.logger.debug('No question entity found in the QuestionSubscriber afterUpdate method'); - return; - } - await this.saveDashboardToConfig(event.databaseEntity, event.queryRunner.manager); - } - - private async saveDashboardToConfig(question: DashboardQuestion, entityManager: EntityManager): Promise { - const dashboard = question.dashboard; - // Get the dashboard from the question & call the saveDashboardToConfig method - if (!dashboard) { - this.logger.debug(`Dashboard is undefined for question id ${question.id}`); - return; - } - - // populate the dashboard with its variables - const populatedDashboard = await entityManager.findOne(Dashboard, { - where: { id: dashboard.id }, - relations: ['module','dashboardVariables', 'questions', 'questions.questionSqlDatasetConfigs'], - }); - - if (!populatedDashboard) { - throw new Error(`Dashboard not found for question id ${populatedDashboard.id}`); - } - - // Call the saveDashboardToConfig method from the DashboardService - await this.dashboardService.saveDashboardToConfig(populatedDashboard); - } -} \ No newline at end of file diff --git a/src/subscribers/dashboard-variable.subscriber.ts b/src/subscribers/dashboard-variable.subscriber.ts deleted file mode 100644 index de803b0f..00000000 --- a/src/subscribers/dashboard-variable.subscriber.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { Injectable, Logger } from "@nestjs/common"; -import { InjectDataSource } from "@nestjs/typeorm"; -import { DashboardVariable } from "src/entities/dashboard-variable.entity"; -import { Dashboard } from "src/entities/dashboard.entity"; -import { ModuleMetadataHelperService } from "src/helpers/module-metadata-helper.service"; -import { DashboardVariableService } from "src/services/dashboard-variable.service"; -import { DashboardService } from "src/services/dashboard.service"; -import { EntitySubscriberInterface, DataSource, InsertEvent, UpdateEvent, EntityManager } from "typeorm"; - -@Injectable() -export class DashboardVariableSubscriber implements EntitySubscriberInterface { - private readonly logger = new Logger(this.constructor.name); - constructor( - @InjectDataSource() - private readonly dataSource: DataSource, - readonly moduleMetadataHelperService: ModuleMetadataHelperService, - readonly dashboardService: DashboardService, // Assuming you have a DashboardService for custom queries - ) { - this.dataSource.subscribers.push(this); - } - - listenTo() { - return DashboardVariable; - } - - async afterInsert(event: InsertEvent) { - if (!event.entity) { - this.logger.debug('No dashboard variable entity found in the DashboardVariableSubscriber afterInsert method'); - return; - } - await this.saveDashboardToConfig(event.entity, event.queryRunner.manager); - } - - async afterUpdate(event: UpdateEvent) { - if (!event.databaseEntity) { - this.logger.debug('No dashboard variable entity found in the DashboardVariableSubscriber afterUpdate method'); - return; - } - await this.saveDashboardToConfig(event.databaseEntity, event.queryRunner.manager); - } - - private async saveDashboardToConfig(dashboardVariable: DashboardVariable, entityManager: EntityManager): Promise { - const dashboard = dashboardVariable.dashboard; - // Get the dashboard from the question & call the saveDashboardToConfig method - if (!dashboard) { - this.logger.debug(`Dashboard is undefined for dashboard variable id ${dashboardVariable.id}`); - return; - } - - // populate the dashboard with its variables - const populatedDashboard = await entityManager.findOne(Dashboard, { - where: { id: dashboard.id }, - relations: ['module','dashboardVariables', 'questions', 'questions.questionSqlDatasetConfigs'], - }); - - if (!populatedDashboard) { - throw new Error(`Dashboard not found for question id ${populatedDashboard.id}`); - } - - // Call the saveDashboardToConfig method from the DashboardService - await this.dashboardService.saveDashboardToConfig(populatedDashboard); - } -} \ No newline at end of file diff --git a/src/subscribers/dashboard.subscriber.ts b/src/subscribers/dashboard.subscriber.ts deleted file mode 100644 index 425d6539..00000000 --- a/src/subscribers/dashboard.subscriber.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { InjectDataSource } from "@nestjs/typeorm"; -import { Dashboard } from 'src/entities/dashboard.entity'; -import { ModuleMetadataHelperService } from "src/helpers/module-metadata-helper.service"; -import { DashboardService } from 'src/services/dashboard.service'; -import { DataSource, EntityManager, EntitySubscriberInterface, InsertEvent, UpdateEvent } from "typeorm"; - -@Injectable() -export class DashboardSubscriber implements EntitySubscriberInterface { - private readonly logger = new Logger(this.constructor.name); - constructor( - @InjectDataSource() - private readonly dataSource: DataSource, - readonly moduleMetadataHelperService: ModuleMetadataHelperService, - readonly dashboardService: DashboardService, // Assuming you have a DashboardService for custom queries - ) { - this.dataSource.subscribers.push(this); - } - - listenTo() { - return Dashboard; - } - - async afterInsert(event: InsertEvent) { - if (!event.entity) { - this.logger.debug('No dashboard entity found in the DashboardSubscriber afterInsert method'); - return; - } - await this.saveDashboardToConfig(event.entity, event.queryRunner.manager); - } - - async afterUpdate(event: UpdateEvent) { - if (!event.entity) { - this.logger.debug('No dashboard entity found in the DashboardSubscriber afterInsert method'); - return; - } - - await this.saveDashboardToConfig(event.databaseEntity, event.queryRunner.manager); - } - - private async saveDashboardToConfig(dashboard: Dashboard, entityManager: EntityManager): Promise { - if (!dashboard || !dashboard.id) { - this.logger.debug('Dashboard or dashboard id is undefined'); - return; - } - - // Load the dashboard with module relation populated - const populatedDashboard = await entityManager.findOne(Dashboard, { - where: { id: dashboard.id }, - relations: ['module','dashboardVariables', 'questions', 'questions.questionSqlDatasetConfigs'], - }); - - if (!populatedDashboard) { - this.logger.error(`Dashboard not found for id ${dashboard.id}`); - return; - } - - // Call the saveDashboardToConfig method from the DashboardService - await this.dashboardService.saveDashboardToConfig(populatedDashboard); - } - -} \ No newline at end of file From 9bcbfdcfb56a98e9f374af3140fa9487c2078afc Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Mon, 1 Jun 2026 13:42:49 +0530 Subject: [PATCH 073/136] always print server startup line regardless of log level Use process.stdout.write so the startup message bypasses Winston level filtering and appears in prod even when log level is error or warn. Co-Authored-By: Claude Sonnet 4.6 --- src/helpers/bootstrap.helper.ts | 2 +- src/testing/README.md | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/helpers/bootstrap.helper.ts b/src/helpers/bootstrap.helper.ts index a540779e..cd06ba7e 100644 --- a/src/helpers/bootstrap.helper.ts +++ b/src/helpers/bootstrap.helper.ts @@ -188,7 +188,7 @@ export async function bootstrapSolidApp( app.useLogger(app.get(WINSTON_MODULE_NEST_PROVIDER)); const elapsed = ((Date.now() - startTime) / 1000).toFixed(2); - app.get(WINSTON_MODULE_NEST_PROVIDER).log(`\x1b[32mServer started on port ${port} in ${elapsed}s\x1b[0m`, 'Bootstrap'); + process.stdout.write(`\x1b[32mServer started on port ${port} in ${elapsed}s\x1b[0m\n`); } // ---- CLI bootstrap ---- diff --git a/src/testing/README.md b/src/testing/README.md index 2b068017..aae189ea 100644 --- a/src/testing/README.md +++ b/src/testing/README.md @@ -15,6 +15,12 @@ { "testing": { "specs": ["path/to/register-test-specs.js"], + "roles": [ + { "name": "Editor", "permissions": ["BookController.*"] } + ], + "users": [ + { "username": "testEditor", "email": "testEditor@test.local", "password": "Test@1234", "roles": ["Editor"] } + ], "data": [ { "modelUserKey": "stateMaster", @@ -340,7 +346,6 @@ await runFromMetadata({ When using `solidctl test run`, specs are loaded from `testing.specs` in module metadata. Use `--skip-scenario-ids` to exclude scenarios by id (comma-separated). Use `--print-api-logs` to print full API request/response details for `api.request` steps. -Use `--print-api-logs` to print full API request/response details for `api.request` steps. ## Run From Metadata ```ts @@ -361,4 +366,4 @@ await runFromMetadata({ 2. Implement a `registerXSteps(registry)` function and `registry.register("op.name", handler)`. 3. Validate required `step.with` fields and throw clear errors. 4. Use adapters via `ctx.api` / `ctx.ui` and update `ctx.last` when helpful. -5. Export and register it from the domain `index.ts`. +5. Export and register it from the domain `index.ts`. \ No newline at end of file From 9365f4810f5211a05c0e49dfd2948efdae1edcdf Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Mon, 1 Jun 2026 13:00:15 +0100 Subject: [PATCH 074/136] feat(dashboard): add various dashboard providers and runtime services - Implemented MqDashboardQueueWiseAvgElapsedProvider for queue-wise average elapsed time. - Added MqDashboardQueueWiseFailuresProvider to count failed messages by queue. - Created MqDashboardRecentFailuresProvider to fetch recent failed messages in a tabular format. - Developed MqDashboardStageDistributionProvider for grouping message counts by stage. - Introduced MqDashboardSucceededMessagesKpiProvider to count succeeded messages. - Added MqDashboardSuccessRateKpiProvider to calculate success rate percentage. - Implemented MqDashboardTotalMessagesKpiProvider for total message count. - Created DashboardRuntimeService to manage dashboard definitions and widget data retrieval. - Added DashboardUserLayoutService for managing user-specific dashboard layouts. - Implemented selection providers for dynamic options in dashboard variables. - Enhanced SolidIntrospectService to register dashboard widget data providers. - Updated solid-core.module.ts to include new services and controllers. --- AGENTIC_DASHBOARD_IMPLEMENTATION_PLAN.md | 412 ++++++++++++ dashboard-curl-smoke-tests.txt | 146 ++++ delete-legacy-dashboard-metadata.sql | 158 +++++ .../dashboard-user-layout.controller.ts | 93 +++ src/controllers/dashboard.controller.ts | 80 +++ ...ashboard-widget-data-provider.decorator.ts | 6 + src/dtos/create-dashboard-user-layout.dto.ts | 41 ++ .../dashboard-variable-options-query.dto.ts | 20 + src/dtos/dashboard-widget-data-request.dto.ts | 34 + src/dtos/update-dashboard-user-layout.dto.ts | 45 ++ src/entities/dashboard-user-layout.entity.ts | 27 + src/helpers/module-metadata-helper.service.ts | 27 +- src/helpers/solid-registry.ts | 22 +- src/index.ts | 21 + src/interfaces.ts | 33 + .../dashboard-user-layout.repository.ts | 19 + .../seed-data/solid-core-metadata.json | 624 +++++++++++++++++- ...hboard-avg-elapsed-kpi-provider.service.ts | 49 ++ ...rd-failed-messages-kpi-provider.service.ts | 45 ++ ...-inflight-messages-kpi-provider.service.ts | 45 ++ ...ashboard-latency-trend-provider.service.ts | 58 ++ ...ard-messages-over-time-provider.service.ts | 79 +++ .../mq-dashboard-provider-utils.ts | 165 +++++ ...queue-wise-avg-elapsed-provider.service.ts | 51 ++ ...rd-queue-wise-failures-provider.service.ts | 51 ++ ...hboard-recent-failures-provider.service.ts | 95 +++ ...ard-stage-distribution-provider.service.ts | 49 ++ ...succeeded-messages-kpi-provider.service.ts | 45 ++ ...board-success-rate-kpi-provider.service.ts | 55 ++ ...ard-total-messages-kpi-provider.service.ts | 44 ++ src/services/dashboard-runtime.service.ts | 369 +++++++++++ src/services/dashboard-user-layout.service.ts | 20 + ...roker-variable-options-provider.service.ts | 52 ++ ...-name-variable-options-provider.service.ts | 51 ++ src/services/solid-introspect.service.ts | 19 + src/solid-core.module.ts | 41 ++ 36 files changed, 3183 insertions(+), 8 deletions(-) create mode 100644 AGENTIC_DASHBOARD_IMPLEMENTATION_PLAN.md create mode 100644 dashboard-curl-smoke-tests.txt create mode 100644 delete-legacy-dashboard-metadata.sql create mode 100644 src/controllers/dashboard-user-layout.controller.ts create mode 100644 src/controllers/dashboard.controller.ts create mode 100644 src/decorators/dashboard-widget-data-provider.decorator.ts create mode 100644 src/dtos/create-dashboard-user-layout.dto.ts create mode 100644 src/dtos/dashboard-variable-options-query.dto.ts create mode 100644 src/dtos/dashboard-widget-data-request.dto.ts create mode 100644 src/dtos/update-dashboard-user-layout.dto.ts create mode 100644 src/entities/dashboard-user-layout.entity.ts create mode 100644 src/repositories/dashboard-user-layout.repository.ts create mode 100644 src/services/dashboard-providers/mq-dashboard-avg-elapsed-kpi-provider.service.ts create mode 100644 src/services/dashboard-providers/mq-dashboard-failed-messages-kpi-provider.service.ts create mode 100644 src/services/dashboard-providers/mq-dashboard-inflight-messages-kpi-provider.service.ts create mode 100644 src/services/dashboard-providers/mq-dashboard-latency-trend-provider.service.ts create mode 100644 src/services/dashboard-providers/mq-dashboard-messages-over-time-provider.service.ts create mode 100644 src/services/dashboard-providers/mq-dashboard-provider-utils.ts create mode 100644 src/services/dashboard-providers/mq-dashboard-queue-wise-avg-elapsed-provider.service.ts create mode 100644 src/services/dashboard-providers/mq-dashboard-queue-wise-failures-provider.service.ts create mode 100644 src/services/dashboard-providers/mq-dashboard-recent-failures-provider.service.ts create mode 100644 src/services/dashboard-providers/mq-dashboard-stage-distribution-provider.service.ts create mode 100644 src/services/dashboard-providers/mq-dashboard-succeeded-messages-kpi-provider.service.ts create mode 100644 src/services/dashboard-providers/mq-dashboard-success-rate-kpi-provider.service.ts create mode 100644 src/services/dashboard-providers/mq-dashboard-total-messages-kpi-provider.service.ts create mode 100644 src/services/dashboard-runtime.service.ts create mode 100644 src/services/dashboard-user-layout.service.ts create mode 100644 src/services/selection-providers/mq-dashboard-message-broker-variable-options-provider.service.ts create mode 100644 src/services/selection-providers/mq-dashboard-queue-name-variable-options-provider.service.ts diff --git a/AGENTIC_DASHBOARD_IMPLEMENTATION_PLAN.md b/AGENTIC_DASHBOARD_IMPLEMENTATION_PLAN.md new file mode 100644 index 00000000..04a3543f --- /dev/null +++ b/AGENTIC_DASHBOARD_IMPLEMENTATION_PLAN.md @@ -0,0 +1,412 @@ +# Agentic Dashboard Implementation Plan + +## 1. Scope and Goals + +### 1.1 Outcome +- [ ] Build a new generic, metadata-driven dashboard framework across `solid-core-module` and `solid-core-ui`. +- [ ] Replace all legacy dashboard implementation code (backend + frontend) with the new architecture. +- [ ] Support existing configured data sources and optional model metadata integration. +- [ ] Provide a clean extension architecture so teams/agents can add new dashboard widgets consistently. + +### 1.2 Core Principles +- [ ] Metadata first: dashboards, variables, widgets, filters, and layout defaults are defined in module metadata JSON. +- [ ] Provider driven: each widget resolves data through a common backend provider contract. +- [ ] UI abstraction: widget rendering uses a typed extension registry entry (`DashboardWidget`) instead of hard-coded branches. +- [ ] User personalization: per-user layout is persisted and merged over metadata defaults. + +--- + +## 2. Reference Patterns to Mirror + +### 2.1 Backend provider registration and resolution pattern +- [x] Follow existing pattern used by: + - `src/services/computed-fields/entity/sequence-num-computed-field-provider.ts` + - `src/services/selection-providers/list-of-values-selection-providers.service.ts` + - `src/services/solid-introspect.service.ts` + - `src/helpers/solid-registry.ts` +- [x] Keep flow: `decorator -> introspection -> registry -> resolver service -> controller endpoint`. + +### 2.2 Frontend extension pattern +- [ ] Mirror existing extension architecture in: + - `solid-core-ui/src/helpers/registry.ts` + - `solid-core-ui/src/types/extension-registry.ts` +- [ ] Add a dedicated extension component type for dashboard widgets. + +--- + +## 3. Legacy Dashboard Decommission (Must happen first) + +### 3.1 Backend legacy removal inventory +- [x] Remove legacy dashboard entities: + - `dashboard.entity.ts`, `dashboard-variable.entity.ts`, `dashboard-question.entity.ts`, `dashboard-question-sql-dataset-config.entity.ts`, `dashboard-layout.entity.ts`. +- [x] Remove legacy controllers/services/repositories/mappers/subscribers/decorators: + - `dashboard*.controller.ts` + - `dashboard*.service.ts` + - `dashboard*.repository.ts` + - `dashboard-mapper.ts` + - `dashboard*.subscriber.ts` + - `dashboard-selection-provider.decorator.ts` + - `dashboard-question-data-provider.decorator.ts` +- [x] Remove old dashboard-specific selection providers and question-data providers currently tied to the old model. +- [x] Remove old exports from `src/index.ts`, `src/interfaces.ts`, and any DTO references. +- [x] Remove all old dashboard wiring from `src/solid-core.module.ts` (imports, entities, providers, controllers). + +### 3.2 Frontend legacy removal inventory +- [x] Remove old dashboard API slices: + - `dashboardApi.ts`, `dashboardQuestionApi.ts`, `dashboardLayoutApi.ts`. +- [x] Remove old dashboard components and route page: + - `components/core/dashboard/*` + - `routes/pages/admin/core/DashboardPage.tsx` (legacy implementation). +- [x] Remove old dashboard extension handlers/widgets under `components/core/extension/solid-core/dashboard*`. +- [x] Remove old store wiring from `redux/store/defaultStoreConfig.ts`. +- [x] Remove stale assets only used by old dashboard implementation. + +### 3.3 Metadata cleanup +- [x] Remove old dashboard-related testing permissions (legacy controller methods) from module metadata examples. +- [x] Update seed metadata (`solid-core-metadata.json`) to remove old dashboard management models/actions/views. + +--- + +## 4. New Metadata Schema Design + +### 4.1 Module metadata structure +- [x] Introduce/replace `dashboards` section schema in module metadata (similar placement as `testing` section). +- [x] Define top-level dashboard fields: + - `name`, `displayName`, `description`, `routeName`, `menu`, `variables`, `widgets`, `defaultLayout`, `permissions`. +- [x] Define variable schema: + - `name`, `label`, `type`, `required`, `defaultValue`, `operators`, `selectionConfig`, `visibilityRules`. +- [x] Define widget schema: + - `name`, `title`, `type`, `dataProvider`, `providerContext`, `visualization`, `layoutRef`, `refreshPolicy`. +- [x] Define layout schema: + - Grid cell contract compatible with Gridstack (`x`, `y`, `w`, `h`, min/max constraints, responsive overrides). + +### 4.2 Validation + compatibility +- [ ] Add runtime validation for dashboard metadata schema (clear error messages for malformed config). +- [x] Introduce schema versioning (`dashboardSchemaVersion`) for future migrations. +- [ ] Add backward-compat guard (disable old schema loading explicitly and log actionable migration hints). + +### 4.3 ADR-001: Dashboard definition source of truth +- [x] **Decision**: + - dashboard definitions remain in module metadata JSON under root-level `dashboards` (peer of `actions`, `menus`, `roles`, `users`, etc.) + - seed only dashboard navigation metadata (`actions`, `menus`) + - do not persist dashboard definitions in dedicated dashboard DB tables + - persist only user-specific layout overrides in the new user layout table. +- [x] **Context**: + - legacy dashboard persistence model has been removed + - new dashboard framework is explicitly metadata-driven + - we need a single source of truth with git versioning and no JSON-vs-DB drift. +- [x] **Consequences**: + - simpler rollout and migrations for v1 + - dashboard updates ship through metadata changes + - runtime must always resolve dashboard definition from module metadata + - no CRUD for dashboard definitions in DB for v1. +- [x] **Alternatives considered (rejected for v1)**: + - persist full dashboard definitions in DB tables + - hybrid model where DB can override JSON definitions. + +### 4.4 ADR-002: Dashboard layout persistence implementation pattern +- [x] **Decision**: + - do not hand-code dashboard layout entity/repository/service in `solid-core-module` source + - define dashboard layout persistence model in module metadata JSON + - generate entity/service/controller artifacts through solid-core metadata code generation flow. +- [x] **Context**: + - project prefers metadata-first model management for framework-owned models + - generated artifacts keep persistence layer consistent with standard solid-core patterns. +- [x] **Consequences**: + - layout persistence endpoint behavior can be stub/fallback until generated model artifacts are available + - consuming projects can regenerate cleanly from metadata without custom table code drift. + +--- + +## 5. Backend Architecture (solid-core-module) + +### 5.1 New provider contracts +- [x] Create new interface: + - `IDashboardWidgetDataProvider` with methods: + - `name()` + - `help()` + - `getData(widgetDefinition, runtimeContext): Promise` +- [x] Optionally split provider contracts: + - widget data provider + - variable options provider (if dynamic filter options are needed separately) +- [x] Define standard response envelope for all widget providers: + - `meta` (widget name/provider/version/time) + - `data` (typed payload) + - `uiHints` (optional rendering hints) + +### 5.2 Decorator + registry + introspection +- [x] Add new decorator for widget data providers (parallel to existing provider decorators). +- [x] Extend `SolidIntrospectService` to auto-register dashboard widget data providers. +- [x] Extend `SolidRegistry` with: + - register/get/list methods for widget providers. +- [x] Keep naming resolution strategy consistent with existing providers. + +### 5.3 Dashboard runtime service layer +- [x] Add new dashboard runtime service: + - resolves dashboard metadata by module + dashboard name + - resolves dashboard variables and validated filter input + - resolves widget provider instance per widget + - executes provider and aggregates response +- [x] Add expression/filter resolver for dashboard variables (provider-level filter utility for date/queue/stage/messageBroker). +- [ ] Add secure execution guardrails: + - provider allowlist + - parameterized SQL only + - timeout + row limits + payload size limits + +### 5.4 New controller endpoints +- [x] Add `DashboardController` (new runtime controller, not CRUD dashboard model controller). +- [x] Proposed endpoints: + - `GET /dashboard/:module/:dashboardName/definition` + - `POST /dashboard/:module/:dashboardName/widgets/:widgetName/data` + - `POST /dashboard/:module/:dashboardName/data` (batch widget fetch) + - `GET /dashboard/:module/:dashboardName/variable-options/:variableName` + - `GET /dashboard/:module/:dashboardName/layout` (resolved default + user override) + - `PUT /dashboard/:module/:dashboardName/layout` (save user layout) +- [x] Add Swagger + permission mapping for new endpoints. + +### 5.5 User layout persistence model +- [x] Add metadata model for personalized layout (and generate entity/service using solid-core code generation): + - `dashboardName` (string/user key reference to metadata dashboard) + - `layoutJson` (long text / json) + - `user` (many-to-one with `User`) + - `module` (many-to-one with `Module`, resolved using `moduleUserKey` from dashboard metadata context) +- [x] Add unique index: + - `(user_id, module_id, dashboard_name)`. +- [x] Add repository/service methods and runtime integration: + - `getUserLayout()` + - `upsertUserLayout()` + - `resetToDefault()` + - wired into `GET/PUT /dashboard/:module/:dashboardName/layout`. + +--- + +## 6. Frontend Architecture (solid-core-ui) + +### 6.1 Extension system for dashboard widgets +- [ ] Add new extension component type in `extension-registry.ts`: + - `dashboardWidget`. +- [ ] Add typed widget props contract (single source of truth for all dashboard widgets), e.g.: + - widget metadata + - resolved variables + - loading/error state + - normalized provider response + - callbacks (refresh, open details, export). +- [ ] Register default widgets via `registry.ts` using the new extension type. + +### 6.2 Dashboard runtime UI +- [ ] Create new generic dashboard route/page: + - `/admin/dashboard/:dashboardName` or `/admin/core/:moduleName/dashboard/:dashboardName` (finalize one canonical route). +- [ ] Build page structure: + - dynamic header + - metadata-driven variable filter bar + - widget grid body + - save/reset layout actions. +- [ ] Resolve dashboard via new backend definition endpoint. + +### 6.3 Layout engine integration +- [ ] Standardize on Gridstack for drag/drop/resize (metadata layout to Gridstack contract adapter). +- [ ] Create layout adapter: + - metadata default layout -> Gridstack nodes + - Gridstack save format -> persisted `layoutJson`. +- [ ] Persist user changes through new layout endpoints. +- [ ] Add responsive behavior and conflict handling for missing/renamed widgets. + +### 6.4 Widget rendering pipeline +- [ ] Implement widget host container that: + - resolves widget extension component by type/name + - fetches provider data + - handles loading/empty/error states consistently + - supports per-widget refresh intervals. +- [ ] Implement first-party default widgets (KPI, line/bar/pie, table, meter/progress). +- [ ] Ensure each widget reads dashboard variables as input params. + +### 6.5 State and API slices +- [ ] Add new RTK Query slices for dashboard runtime endpoints. +- [x] Remove legacy dashboard slices from store config. +- [ ] Add cache keys by `module + dashboard + variable hash + widget`. + +--- + +## 7. Charting Library Recommendation + +### 7.1 Recommended baseline +- [ ] Use **Apache ECharts** as the default charting engine abstraction for v1. + +### 7.2 Why ECharts +- [ ] Broad chart coverage (20+ types and combinable series). +- [ ] Strong visual quality out of the box, plus deep customization. +- [ ] Apache-2.0 license (friendly for framework redistribution/use). +- [ ] Handles large datasets and supports Canvas/SVG rendering modes. + +### 7.3 UI abstraction requirement +- [ ] Do not couple widget contracts directly to ECharts option schema. +- [ ] Add a renderer adapter layer: + - `chartRenderer: "echarts"` (v1) + - future pluggable renderers without metadata breaking changes. + +--- + +## 8. Menu, Routes, and Metadata Navigation + +### 8.1 Metadata-driven menu/action integration +- [x] Add metadata authoring convention for dashboard menu/action pairs: + - action type `custom` + - route template resolved to canonical dashboard route. +- [ ] Update backend menu path generation rules if needed for dashboard route parameters. + +### 8.2 Permission model +- [ ] Define dashboard runtime permissions at dashboard definition and endpoint level. +- [ ] Update testing metadata generation to include new runtime controller permissions. + +--- + +## 9. Migration and Rollout Strategy + +### 9.1 Phase rollout +- [x] Phase A: remove legacy code and compile cleanly. +- [x] Phase B: introduce new backend runtime + metadata schema + provider set for queue-health reference dashboard. +- [ ] Phase C: introduce UI runtime + Gridstack + ECharts adapter + baseline widgets. +- [ ] Phase D: add user layout persistence and menu integration. +- [ ] Phase E: documentation, sample module metadata, agent reference implementation. + +### 9.2 Data/config migration +- [ ] Provide migration script or one-time converter for old dashboard JSON to new metadata schema (where feasible). +- [ ] Explicitly document non-migratable legacy constructs and fallback behavior. + +--- + +## 10. Testing and Quality Gates + +### 10.1 Backend tests +- [ ] Unit tests for provider registry resolution, schema validation, and controller contract. +- [ ] Integration tests for dashboard definition load, widget data batch response, variable option resolution, layout upsert/load. +- [ ] Security tests for SQL/provider guardrails. + +### 10.2 Frontend tests +- [ ] Component tests for dynamic filter rendering from metadata. +- [ ] Widget host tests for loading/error/retry states. +- [ ] Layout persistence tests (save/load/reset). +- [ ] Route/menu rendering tests for dashboard navigation. + +### 10.3 End-to-end smoke +- [x] Seed one reference dashboard in sample metadata. +- [ ] Validate: open dashboard -> apply filters -> render widgets -> drag/resize -> save layout -> reload persistence. + +--- + +## 11. Documentation and Developer Experience + +### 11.1 Framework docs +- [ ] Add “Dashboard Metadata Authoring Guide”. +- [ ] Add “Build a Custom Dashboard Widget Provider” guide. +- [ ] Add “Frontend DashboardWidget extension contract” guide. + +### 11.2 Agent-ready templates +- [ ] Add reference widget provider template files. +- [ ] Add reference dashboard metadata JSON template. +- [ ] Add checklist for creating a new widget end-to-end. + +--- + +## 12. Agent Project Enablement (Skills and Tooling) + +### 12.1 Agent capability goals +- [ ] Enable agents to discover dashboard definitions, variables, widgets, and layout metadata quickly. +- [ ] Enable agents to generate new dashboard/widget scaffolds that conform to framework contracts. +- [ ] Enable agents to validate dashboard metadata and provider wiring before runtime. +- [ ] Enable agents to troubleshoot dashboard rendering/data issues with structured diagnostics. + +### 12.2 New/updated skill surfaces +- [ ] Add a dedicated agent skill guide for dashboard implementation: + - metadata authoring (`dashboards`, `variables`, `widgets`, `defaultLayout`) + - backend provider scaffolding (`IDashboardWidgetDataProvider`) + - frontend widget extension scaffolding (`dashboardWidget` type) + - menu/action/route integration pattern. +- [ ] Add cookbook-style examples in the skill: + - create dashboard from scratch + - add a new widget type + - add variable-driven filtering + - persist and reset user layout. +- [ ] Add anti-patterns and guardrails section: + - avoid hard-coded UI branches per widget + - enforce provider response contract + - enforce parameterized SQL and payload limits. + +### 12.3 Tooling opportunities (optional but recommended) +- [ ] Add CLI-style helper commands (or MCP handlers) for: + - scaffold dashboard metadata block + - scaffold backend widget provider class + - scaffold frontend dashboard widget component + - run dashboard metadata validation. +- [ ] Add validation utility callable by agents: + - checks metadata schema compliance + - checks referenced providers/widgets/routes exist + - checks layout schema compatibility. +- [ ] Add introspection/debug endpoint(s) for agents: + - list registered dashboard widget providers + - preview resolved dashboard definition + - dry-run widget data contract output. + +### 12.4 Agent integration with existing project tooling +- [ ] Extend existing MCP handler ecosystem to support dashboard-specific actions: + - create dashboard + - add widget to dashboard + - add variable to dashboard + - regenerate menu/action links for dashboard routes. +- [ ] Ensure handler outputs are idempotent and metadata-safe (no duplicate entries, deterministic updates). +- [ ] Add structured result payloads so agents can chain operations reliably. + +### 12.5 Agent quality and safety checks +- [ ] Add preflight checks agents must run before patch generation: + - schema validation + - provider registration check + - permission mapping check + - route resolution check. +- [ ] Add post-change verification checklist for agents: + - metadata compiles/loads + - widget provider resolves in registry + - UI widget mounts with mocked data + - layout save/load roundtrip works. +- [ ] Add fail-fast diagnostics format: + - missing provider + - invalid widget type + - malformed layout + - variable expression mismatch. + +### 12.6 Agent adoption rollout +- [ ] Phase 1: publish the dashboard skill + templates with one golden-path example. +- [ ] Phase 2: add scaffold + validation tools for fast and safe agent output. +- [ ] Phase 3: add advanced capabilities (migration assistant, dashboard linting, widget contract tests). +- [ ] Track adoption metrics: + - time to create new dashboard + - first-pass success rate + - number of manual fixes after agent-generated changes. + +--- + +## 13. Immediate Execution Checklist (Sprint-1 Proposal) + +- [ ] Finalize canonical route format (`/admin/core/:module/dashboard/:dashboardName` vs `/admin/dashboard/:dashboardName`). +- [ ] Freeze new metadata schema draft (`dashboards.variables.widgets.layout`). +- [x] Delete legacy dashboard code paths in `solid-core-module`. +- [x] Delete legacy dashboard code paths in `solid-core-ui`. +- [x] Add new provider decorator + registry wiring + introspection. +- [x] Add new runtime controller + definition/data endpoints. +- [ ] Add dashboard layout metadata model and run code generation for persistence service/entity. +- [ ] Add UI dashboard page scaffold with metadata header + variable bar. +- [ ] Add Gridstack integration adapter and persist layout flow. +- [ ] Add ECharts renderer adapter + first 2 widgets (KPI + Bar/Line). +- [ ] Add one sample dashboard metadata entry in `solid-library-management` as reference. + +--- + +## 14. External Library Notes (validated May 30, 2026) + +- [ ] ECharts official feature/license references: + - https://echarts.apache.org/ + - https://echarts.apache.org/en/feature.html + - https://echarts.apache.org/faq +- [ ] Gridstack docs/releases references: + - https://gridstackjs.com/ + - https://gridstackjs.com/doc/html/classes/GridStack.html + - https://github.com/gridstack/gridstack.js/releases diff --git a/dashboard-curl-smoke-tests.txt b/dashboard-curl-smoke-tests.txt new file mode 100644 index 00000000..8fd2f057 --- /dev/null +++ b/dashboard-curl-smoke-tests.txt @@ -0,0 +1,146 @@ +# Dashboard Runtime Smoke Test CURLs +# Purpose: quick re-test suite for queue-health dashboard endpoints. +# Auth note: TOKEN should be a JWT access token generated by /iam/authenticate. + +# ---------------------------------------------------------------------------- +# Setup +# ---------------------------------------------------------------------------- +export BASE_URL="http://localhost:9000/api" +export TOKEN="" + +# Optional: verify token works. +curl -s "$BASE_URL/iam/me" \ + -H "Authorization: Bearer $TOKEN" | jq + +# ---------------------------------------------------------------------------- +# 1) Dashboard Definition +# Expected: 200 + dashboard metadata in .data +# ---------------------------------------------------------------------------- +curl -s "$BASE_URL/dashboard/solid-core/queue-health/definition" \ + -H "Authorization: Bearer $TOKEN" | jq + +# ---------------------------------------------------------------------------- +# 2) Dashboard Variable Options (dynamic/static) +# Expected: 200 + array in .data +# ---------------------------------------------------------------------------- + +# queueName (dynamic provider) +curl -s "$BASE_URL/dashboard/solid-core/queue-health/variable-options/queueName?limit=20&offset=0&query=" \ + -H "Authorization: Bearer $TOKEN" | jq + +# messageBroker (dynamic provider) +curl -s "$BASE_URL/dashboard/solid-core/queue-health/variable-options/messageBroker?limit=20&offset=0&query=" \ + -H "Authorization: Bearer $TOKEN" | jq + +# stage (static selection) +curl -s "$BASE_URL/dashboard/solid-core/queue-health/variable-options/stage" \ + -H "Authorization: Bearer $TOKEN" | jq + +# ---------------------------------------------------------------------------- +# 3) Single Widget Data Endpoints +# Expected: success envelope with .data.meta + .data.data +# ---------------------------------------------------------------------------- + +# KPI: total messages +curl -s -X POST "$BASE_URL/dashboard/solid-core/queue-health/widgets/kpi-total-messages/data" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "variables": { + "date": { "preset": "last_7_days" } + } + }' | jq + +# Line chart: messages over time +curl -s -X POST "$BASE_URL/dashboard/solid-core/queue-health/widgets/chart-messages-over-time/data" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "variables": { + "date": { "preset": "last_30_days" }, + "stage": ["succeeded", "failed"] + } + }' | jq + +# Table: recent failures +curl -s -X POST "$BASE_URL/dashboard/solid-core/queue-health/widgets/table-recent-failures/data" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "variables": { + "date": { "preset": "last_30_days" } + } + }' | jq + +# ---------------------------------------------------------------------------- +# 4) Batch Widget Data (subset) +# Expected: .data.widgets[] with provider-backed results +# ---------------------------------------------------------------------------- +curl -s -X POST "$BASE_URL/dashboard/solid-core/queue-health/data" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "widgetNames": [ + "kpi-total-messages", + "kpi-failed-messages", + "kpi-success-rate", + "chart-stage-distribution" + ], + "variables": { + "date": { "preset": "last_7_days" } + } + }' | jq + +# ---------------------------------------------------------------------------- +# 5) Batch Widget Data (all widgets) +# Expected: all configured widgets execute in one call +# ---------------------------------------------------------------------------- +curl -s -X POST "$BASE_URL/dashboard/solid-core/queue-health/data" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "variables": { + "date": { "preset": "last_30_days" } + } + }' | jq + +# ---------------------------------------------------------------------------- +# 6) Layout Endpoints +# Expected: +# - GET returns defaultLayout and optional userLayout +# - PUT stores user layout once dashboardUserLayout generated model is available +# ---------------------------------------------------------------------------- + +# Read layout +curl -s "$BASE_URL/dashboard/solid-core/queue-health/layout" \ + -H "Authorization: Bearer $TOKEN" | jq + +# Save layout +curl -s -X PUT "$BASE_URL/dashboard/solid-core/queue-health/layout" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "layoutJson": { + "engine": "gridstack", + "columns": 12, + "items": [ + { "widgetId": "kpi-total-messages", "x": 0, "y": 0, "w": 3, "h": 2 } + ] + } + }' | jq + +# ---------------------------------------------------------------------------- +# 7) Useful validation snippets +# ---------------------------------------------------------------------------- + +# Show only provider names from all-widgets batch response +curl -s -X POST "$BASE_URL/dashboard/solid-core/queue-health/data" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"variables":{"date":{"preset":"last_30_days"}}}' | jq '.data.widgets[].meta.providerName' + +# Show only recent failure record sample +curl -s -X POST "$BASE_URL/dashboard/solid-core/queue-health/widgets/table-recent-failures/data" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"variables":{"date":{"preset":"last_30_days"}}}' | jq '.data.data.records[0]' diff --git a/delete-legacy-dashboard-metadata.sql b/delete-legacy-dashboard-metadata.sql new file mode 100644 index 00000000..d498c0ce --- /dev/null +++ b/delete-legacy-dashboard-metadata.sql @@ -0,0 +1,158 @@ +BEGIN; + +-- 0) Scope: solid-core dashboard legacy keys +-- Models: +-- dashboard, dashboardVariable, dashboardQuestion, dashboardQuestionSqlDatasetConfig, dashboardLayout +-- Actions: +-- dashboard-list-action, dashboardVariable-list-action, dashboardQuestion-list-action, +-- dashboardLayout-list-action, dashboardQuestionSqlDatasetConfig-list-action +-- Menus: +-- dashboardManagement-menu-item, dashboard-menu-item, dashboardQuestion-menu-item, dashboardLayout-menu-item +-- Views: +-- dashboard-list-view, dashboard-form-view, +-- dashboardVariable-list-view, dashboardVariable-form-view, +-- dashboardQuestion-list-view, dashboardQuestion-form-view, +-- dashboardQuestionSqlDatasetConfig-list-view, dashboardQuestionSqlDatasetConfig-form-view, +-- dashboardLayout-list-view, dashboardLayout-form-view + +-- 1) Remove role<->permission joins for dashboard controller permissions +-- (table name may vary by naming strategy; this is the common one) +DELETE FROM ss_role_metadata_permissions_ss_permission_metadata +WHERE ss_permission_metadata_id IN ( + SELECT id + FROM ss_permission_metadata + WHERE name ILIKE 'Dashboard%' +); + +-- 2) Remove dashboard permissions +DELETE FROM ss_permission_metadata +WHERE name ILIKE 'Dashboard%'; + +-- 3) Remove menu<->role joins for dashboard menus +DELETE FROM ss_menu_item_metadata_roles_ss_role_metadata +WHERE ss_menu_item_metadata_id IN ( + SELECT m.id + FROM ss_menu_item_metadata m + WHERE m.name IN ( + 'dashboardManagement-menu-item', + 'dashboard-menu-item', + 'dashboardQuestion-menu-item', + 'dashboardLayout-menu-item' + ) +); + +-- 4) Remove dashboard menus +DELETE FROM ss_menu_item_metadata +WHERE name IN ( + 'dashboardManagement-menu-item', + 'dashboard-menu-item', + 'dashboardQuestion-menu-item', + 'dashboardLayout-menu-item' +); + +-- 5) Remove dashboard actions +DELETE FROM ss_action_metadata +WHERE name IN ( + 'dashboard-list-action', + 'dashboardVariable-list-action', + 'dashboardQuestion-list-action', + 'dashboardLayout-list-action', + 'dashboardQuestionSqlDatasetConfig-list-action' +); + +-- 6) Remove user-specific view overrides for dashboard views +DELETE FROM ss_user_view_metadata +WHERE view_metadata_id IN ( + SELECT v.id + FROM ss_view_metadata v + WHERE v.name IN ( + 'dashboard-list-view', + 'dashboard-form-view', + 'dashboardVariable-list-view', + 'dashboardVariable-form-view', + 'dashboardQuestion-list-view', + 'dashboardQuestion-form-view', + 'dashboardQuestionSqlDatasetConfig-list-view', + 'dashboardQuestionSqlDatasetConfig-form-view', + 'dashboardLayout-list-view', + 'dashboardLayout-form-view' + ) +); + +-- 7) Remove saved filters tied to dashboard models/views +DELETE FROM ss_saved_fitlers +WHERE model_id IN ( + SELECT m.id + FROM ss_model_metadata m + JOIN ss_module_metadata mm ON mm.id = m.module_id + WHERE mm.name = 'solid-core' + AND m.singular_name IN ( + 'dashboard', + 'dashboardVariable', + 'dashboardQuestion', + 'dashboardQuestionSqlDatasetConfig', + 'dashboardLayout' + ) +) +OR view_id IN ( + SELECT v.id + FROM ss_view_metadata v + WHERE v.name IN ( + 'dashboard-list-view', + 'dashboard-form-view', + 'dashboardVariable-list-view', + 'dashboardVariable-form-view', + 'dashboardQuestion-list-view', + 'dashboardQuestion-form-view', + 'dashboardQuestionSqlDatasetConfig-list-view', + 'dashboardQuestionSqlDatasetConfig-form-view', + 'dashboardLayout-list-view', + 'dashboardLayout-form-view' + ) +); + +-- 8) Remove security rules attached to dashboard models (if any) +DELETE FROM ss_security_rule +WHERE model_metadata_id IN ( + SELECT m.id + FROM ss_model_metadata m + JOIN ss_module_metadata mm ON mm.id = m.module_id + WHERE mm.name = 'solid-core' + AND m.singular_name IN ( + 'dashboard', + 'dashboardVariable', + 'dashboardQuestion', + 'dashboardQuestionSqlDatasetConfig', + 'dashboardLayout' + ) +); + +-- 9) Remove dashboard views +DELETE FROM ss_view_metadata +WHERE name IN ( + 'dashboard-list-view', + 'dashboard-form-view', + 'dashboardVariable-list-view', + 'dashboardVariable-form-view', + 'dashboardQuestion-list-view', + 'dashboardQuestion-form-view', + 'dashboardQuestionSqlDatasetConfig-list-view', + 'dashboardQuestionSqlDatasetConfig-form-view', + 'dashboardLayout-list-view', + 'dashboardLayout-form-view' +); + +-- 10) Remove dashboard models (fields should cascade via model_id FK) +DELETE FROM ss_model_metadata +WHERE singular_name IN ( + 'dashboard', + 'dashboardVariable', + 'dashboardQuestion', + 'dashboardQuestionSqlDatasetConfig', + 'dashboardLayout' +) +AND module_id IN ( + SELECT id FROM ss_module_metadata WHERE name = 'solid-core' +); + +COMMIT; diff --git a/src/controllers/dashboard-user-layout.controller.ts b/src/controllers/dashboard-user-layout.controller.ts new file mode 100644 index 00000000..b9ac8219 --- /dev/null +++ b/src/controllers/dashboard-user-layout.controller.ts @@ -0,0 +1,93 @@ +import { Controller, Post, Body, Param, UploadedFiles, UseInterceptors, Put, Get, Query, Delete, Patch } from '@nestjs/common'; +import { AnyFilesInterceptor } from "@nestjs/platform-express"; +import { ApiBearerAuth, ApiQuery, ApiTags } from '@nestjs/swagger'; +import { DashboardUserLayoutService } from '../services/dashboard-user-layout.service'; +import { CreateDashboardUserLayoutDto } from '../dtos/create-dashboard-user-layout.dto'; +import { UpdateDashboardUserLayoutDto } from '../dtos/update-dashboard-user-layout.dto'; + +enum ShowSoftDeleted { + INCLUSIVE = "inclusive", + EXCLUSIVE = "exclusive", +} + +@ApiTags('Solid Core') +@Controller('dashboard-user-layout') +export class DashboardUserLayoutController { + constructor(private readonly service: DashboardUserLayoutService) {} + + @ApiBearerAuth("jwt") + @Post() + @UseInterceptors(AnyFilesInterceptor()) + create(@Body() createDto: CreateDashboardUserLayoutDto, @UploadedFiles() files: Array) { + return this.service.create(createDto, files); + } + + @ApiBearerAuth("jwt") + @Post('/bulk') + @UseInterceptors(AnyFilesInterceptor()) + insertMany(@Body() createDtos: CreateDashboardUserLayoutDto[], @UploadedFiles() filesArray: Express.Multer.File[][] = []) { + return this.service.insertMany(createDtos, filesArray); + } + + + @ApiBearerAuth("jwt") + @Put(':id') + @UseInterceptors(AnyFilesInterceptor()) + update(@Param('id') id: number, @Body() updateDto: UpdateDashboardUserLayoutDto, @UploadedFiles() files: Array) { + return this.service.update(id, updateDto, files); + } + + @ApiBearerAuth("jwt") + @Patch(':id') + @UseInterceptors(AnyFilesInterceptor()) + partialUpdate(@Param('id') id: number, @Body() updateDto: UpdateDashboardUserLayoutDto, @UploadedFiles() files: Array) { + return this.service.update(id, updateDto, files, true); + } + + @ApiBearerAuth("jwt") + @Post('/bulk-recover') + async recoverMany(@Body() ids: number[]) { + return this.service.recoverMany(ids); + } + + @ApiBearerAuth("jwt") + @Get('/recover/:id') + async recover(@Param('id') id: number) { + return this.service.recover(id); + } + + @ApiBearerAuth("jwt") + @ApiQuery({ name: 'showSoftDeleted', required: false, enum: ShowSoftDeleted }) + @ApiQuery({ name: 'limit', required: false, type: Number }) + @ApiQuery({ name: 'offset', required: false, type: Number }) + @ApiQuery({ name: 'fields', required: false, type: Array }) + @ApiQuery({ name: 'sort', required: false, type: Array }) + @ApiQuery({ name: 'groupBy', required: false, type: Array }) + @ApiQuery({ name: 'populate', required: false, type: Array }) + @ApiQuery({ name: 'populateMedia', required: false, type: Array }) + @ApiQuery({ name: 'filters', required: false, type: Array }) + @Get() + async findMany(@Query() query: any) { + return this.service.find(query); + } + + @ApiBearerAuth("jwt") + @Get(':id') + async findOne(@Param('id') id: string, @Query() query: any) { + return this.service.findOne(+id, query); + } + + @ApiBearerAuth("jwt") + @Delete('/bulk') + async deleteMany(@Body() ids: number[]) { + return this.service.deleteMany(ids); + } + + @ApiBearerAuth("jwt") + @Delete(':id') + async delete(@Param('id') id: number) { + return this.service.delete(id); + } + + +} diff --git a/src/controllers/dashboard.controller.ts b/src/controllers/dashboard.controller.ts new file mode 100644 index 00000000..615534e0 --- /dev/null +++ b/src/controllers/dashboard.controller.ts @@ -0,0 +1,80 @@ +import { Body, Controller, Get, Param, Post, Put, Query } from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { ActiveUser } from 'src/decorators/active-user.decorator'; +import { DashboardVariableOptionsQueryDto } from 'src/dtos/dashboard-variable-options-query.dto'; +import { DashboardBatchDataRequestDto, DashboardSaveLayoutDto, DashboardWidgetDataRequestDto } from 'src/dtos/dashboard-widget-data-request.dto'; +import { ActiveUserData } from 'src/interfaces/active-user-data.interface'; +import { DashboardRuntimeService } from 'src/services/dashboard-runtime.service'; + +@Controller('dashboard') +@ApiTags('Solid Core') +export class DashboardController { + constructor( + private readonly dashboardRuntimeService: DashboardRuntimeService, + ) { } + + @ApiBearerAuth('jwt') + @Get(':moduleName/:dashboardName/definition') + async getDefinition( + @Param('moduleName') moduleName: string, + @Param('dashboardName') dashboardName: string, + ) { + return this.dashboardRuntimeService.getDashboardDefinition(moduleName, dashboardName); + } + + @ApiBearerAuth('jwt') + @Post(':moduleName/:dashboardName/widgets/:widgetName/data') + async getWidgetData( + @Param('moduleName') moduleName: string, + @Param('dashboardName') dashboardName: string, + @Param('widgetName') widgetName: string, + @Body() request: DashboardWidgetDataRequestDto, + @ActiveUser() activeUser: ActiveUserData, + ) { + return this.dashboardRuntimeService.getWidgetData(moduleName, dashboardName, widgetName, request, activeUser); + } + + @ApiBearerAuth('jwt') + @Post(':moduleName/:dashboardName/data') + async getDashboardData( + @Param('moduleName') moduleName: string, + @Param('dashboardName') dashboardName: string, + @Body() request: DashboardBatchDataRequestDto, + @ActiveUser() activeUser: ActiveUserData, + ) { + return this.dashboardRuntimeService.getDashboardData(moduleName, dashboardName, request, activeUser); + } + + @ApiBearerAuth('jwt') + @Get(':moduleName/:dashboardName/variable-options/:variableName') + async getVariableOptions( + @Param('moduleName') moduleName: string, + @Param('dashboardName') dashboardName: string, + @Param('variableName') variableName: string, + @Query() query: DashboardVariableOptionsQueryDto, + ) { + return this.dashboardRuntimeService.getVariableOptions(moduleName, dashboardName, variableName, query); + } + + @ApiBearerAuth('jwt') + @Get(':moduleName/:dashboardName/layout') + async getLayout( + @Param('moduleName') moduleName: string, + @Param('dashboardName') dashboardName: string, + @ActiveUser() activeUser: ActiveUserData, + ) { + return this.dashboardRuntimeService.getLayout(moduleName, dashboardName, activeUser); + } + + @ApiBearerAuth('jwt') + @Put(':moduleName/:dashboardName/layout') + async saveLayout( + @Param('moduleName') moduleName: string, + @Param('dashboardName') dashboardName: string, + @Body() body: DashboardSaveLayoutDto, + @ActiveUser() activeUser: ActiveUserData, + ) { + return this.dashboardRuntimeService.saveLayout(moduleName, dashboardName, body, activeUser); + } +} + diff --git a/src/decorators/dashboard-widget-data-provider.decorator.ts b/src/decorators/dashboard-widget-data-provider.decorator.ts new file mode 100644 index 00000000..b222876a --- /dev/null +++ b/src/decorators/dashboard-widget-data-provider.decorator.ts @@ -0,0 +1,6 @@ +import { SetMetadata } from "@nestjs/common"; + +export const IS_DASHBOARD_WIDGET_DATA_PROVIDER = "IS_DASHBOARD_WIDGET_DATA_PROVIDER"; + +export const DashboardWidgetDataProvider = () => SetMetadata(IS_DASHBOARD_WIDGET_DATA_PROVIDER, true); + diff --git a/src/dtos/create-dashboard-user-layout.dto.ts b/src/dtos/create-dashboard-user-layout.dto.ts new file mode 100644 index 00000000..d9fd84ab --- /dev/null +++ b/src/dtos/create-dashboard-user-layout.dto.ts @@ -0,0 +1,41 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsInt } from 'class-validator'; +import { IsOptional } from 'class-validator'; +import { IsString, IsNotEmpty } from 'class-validator'; + +export class CreateDashboardUserLayoutDto { + @IsOptional() + @IsInt() + @ApiProperty() + userId: number; + + @IsString() + @IsOptional() + @ApiProperty() + userUserKey: string; + + @IsOptional() + @IsInt() + @ApiProperty() + moduleId: number; + + @IsString() + @IsOptional() + @ApiProperty() + moduleUserKey: string; + + @IsNotEmpty() + @IsString() + @ApiProperty() + dashboardName: string; + + @IsNotEmpty() + @IsString() + @ApiProperty() + layoutJson: string; + + @IsOptional() + @IsInt() + @ApiProperty() + version: number; +} \ No newline at end of file diff --git a/src/dtos/dashboard-variable-options-query.dto.ts b/src/dtos/dashboard-variable-options-query.dto.ts new file mode 100644 index 00000000..b56e0308 --- /dev/null +++ b/src/dtos/dashboard-variable-options-query.dto.ts @@ -0,0 +1,20 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsOptional, IsString } from "class-validator"; +import { PaginationQueryDto } from "src/dtos/pagination-query.dto"; + +export class DashboardVariableOptionsQueryDto extends PaginationQueryDto { + @ApiProperty({ description: "Search query string", type: String, required: false }) + @IsString() + @IsOptional() + query?: string; + + @ApiProperty({ description: "Single option value", type: String, required: false }) + @IsString() + @IsOptional() + optionValue?: string; + + @ApiProperty({ description: "Form values object serialized as JSON or querystring", required: false }) + @IsOptional() + formValues?: Record | string; +} + diff --git a/src/dtos/dashboard-widget-data-request.dto.ts b/src/dtos/dashboard-widget-data-request.dto.ts new file mode 100644 index 00000000..a0596e32 --- /dev/null +++ b/src/dtos/dashboard-widget-data-request.dto.ts @@ -0,0 +1,34 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsArray, IsObject, IsOptional, IsString } from "class-validator"; + +export class DashboardWidgetDataRequestDto { + @ApiProperty({ description: "Runtime variable values", type: Object, required: false }) + @IsOptional() + @IsObject() + variables?: Record; + + @ApiProperty({ description: "Optional provider context overrides", type: Object, required: false }) + @IsOptional() + @IsObject() + providerContext?: Record; +} + +export class DashboardBatchDataRequestDto extends DashboardWidgetDataRequestDto { + @ApiProperty({ description: "Subset of widget ids/names to evaluate", type: [String], required: false }) + @IsOptional() + @IsArray() + @IsString({ each: true }) + widgetNames?: string[]; +} + +export class DashboardSaveLayoutDto { + @ApiProperty({ description: "Grid/layout payload from UI", type: Object }) + layoutJson: Record | any[]; + + @ApiProperty({ + description: "Backward-compatible alias for layoutJson.", + type: Object, + required: false, + }) + layout?: Record | any[]; +} diff --git a/src/dtos/update-dashboard-user-layout.dto.ts b/src/dtos/update-dashboard-user-layout.dto.ts new file mode 100644 index 00000000..ed929129 --- /dev/null +++ b/src/dtos/update-dashboard-user-layout.dto.ts @@ -0,0 +1,45 @@ +import { IsInt,IsOptional, IsString, IsNotEmpty } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; + +export class UpdateDashboardUserLayoutDto { + @IsOptional() + @IsInt() + id: number; + + @IsOptional() + @IsInt() + @ApiProperty() + userId: number; + + @IsString() + @IsOptional() + @ApiProperty() + userUserKey: string; + + @IsOptional() + @IsInt() + @ApiProperty() + moduleId: number; + + @IsString() + @IsOptional() + @ApiProperty() + moduleUserKey: string; + + @IsNotEmpty() + @IsOptional() + @IsString() + @ApiProperty() + dashboardName: string; + + @IsNotEmpty() + @IsOptional() + @IsString() + @ApiProperty() + layoutJson: string; + + @IsOptional() + @IsInt() + @ApiProperty() + version: number; +} \ No newline at end of file diff --git a/src/entities/dashboard-user-layout.entity.ts b/src/entities/dashboard-user-layout.entity.ts new file mode 100644 index 00000000..c6ccdcd9 --- /dev/null +++ b/src/entities/dashboard-user-layout.entity.ts @@ -0,0 +1,27 @@ +import { CommonEntity } from 'src/entities/common.entity'; +import { Entity, JoinColumn, ManyToOne, Index, Column } from 'typeorm'; +import { User } from 'src/entities/user.entity'; +import { ModuleMetadata } from 'src/entities/module-metadata.entity' + +@Entity('ss_dashboard_user_layout') +export class DashboardUserLayout extends CommonEntity { + @Index() + @ManyToOne(() => User, { onDelete: "CASCADE", nullable: false }) + @JoinColumn() + user: User; + + @Index() + @ManyToOne(() => ModuleMetadata, { onDelete: "CASCADE", nullable: false }) + @JoinColumn() + module: ModuleMetadata; + + @Index() + @Column({ name: "dashboard_name", type: "varchar" }) + dashboardName: string; + + @Column({ name: "layout_json", type: "text" }) + layoutJson: string; + + @Column({ type: "integer", nullable: true }) + version: number; +} \ No newline at end of file diff --git a/src/helpers/module-metadata-helper.service.ts b/src/helpers/module-metadata-helper.service.ts index dffdad22..ec59de19 100644 --- a/src/helpers/module-metadata-helper.service.ts +++ b/src/helpers/module-metadata-helper.service.ts @@ -44,19 +44,34 @@ export class ModuleMetadataHelperService { const dashModuleName = kebabCase(moduleName); const folderPath = path.resolve(process.cwd(), 'module-metadata', dashModuleName); const filePath = path.join(folderPath, `${dashModuleName}-metadata.json`); + // Check if filePath exists const fileExists = await this.fileService.exists(filePath); - // this.logger.debug(`File exists: ${fileExists} at ${filePath}`); - if (!fileExists) { - // If the module is solid-core, try the fallback path, in case the current root directory is the solid core project - if (dashModuleName === SOLID_CORE_MODULE_NAME) { - const fallbackPath = path.resolve(process.cwd(), 'src', 'seeders', 'seed-data', `${dashModuleName}-metadata.json`); + if (fileExists) { + return filePath; + } + + // If the module is solid-core, try the fallback path, in case the current root directory is the solid core project + if (dashModuleName === SOLID_CORE_MODULE_NAME) { + const fallbackPath = path.resolve(process.cwd(), 'src', 'seeders', 'seed-data', `${dashModuleName}-metadata.json`); + const fallbackExists = await this.fileService.exists(fallbackPath); + if (fallbackExists) { this.logger.debug(`Fallback path: ${fallbackPath}`); return fallbackPath; } + + const consumingProjectFallbackPath = path.resolve(process.cwd(), 'node_modules', '@solidxai', 'core', 'src', 'seeders', 'seed-data', `${dashModuleName}-metadata.json`); + const consumingProjectFallbackExists = await this.fileService.exists(consumingProjectFallbackPath); + if (consumingProjectFallbackExists) { + this.logger.debug(`Fallback path: ${consumingProjectFallbackPath}`); + return consumingProjectFallbackPath; + } } - return filePath; + + this.logger.error(`Module metadata file not found for module: ${moduleName} at path: ${filePath}`); + return ''; } + async getModuleMetadataFolderPath(moduleName: string): Promise { if (!moduleName) { return ''; diff --git a/src/helpers/solid-registry.ts b/src/helpers/solid-registry.ts index ea01efb0..328cc67d 100755 --- a/src/helpers/solid-registry.ts +++ b/src/helpers/solid-registry.ts @@ -7,7 +7,7 @@ import { CommonEntity } from 'src/entities/common.entity'; import { Locale } from 'src/entities/locale.entity'; import { SecurityRule } from 'src/entities/security-rule.entity'; import { IScheduledJob } from 'src/services/scheduled-jobs/scheduled-job.interface'; -import { IErrorCodeProvider, ISecurityRuleConfigProvider, ISelectionProvider, ISelectionProviderContext, ISolidDatabaseModule } from "../interfaces"; +import { IDashboardWidgetDataProvider, IErrorCodeProvider, ISecurityRuleConfigProvider, ISelectionProvider, ISelectionProviderContext, ISolidDatabaseModule } from "../interfaces"; import { ObjectLiteral } from 'typeorm'; type ControllerMetadata = { @@ -71,6 +71,7 @@ export class SolidRegistry { private seeders: Set = new Set(); private scheduledJobProviders: Set = new Set(); private selectionProviders: Set = new Set(); + private dashboardWidgetDataProviders: Set = new Set(); private computedFieldProviders: Set = new Set(); private solidDatabaseModules: Set = new Set(); private controllers: Set = new Set(); @@ -136,6 +137,10 @@ export class SolidRegistry { this.selectionProviders.add(selectionProvider); } + registerDashboardWidgetDataProvider(provider: InstanceWrapper): void { + this.dashboardWidgetDataProviders.add(provider); + } + registerComputedFieldProvider(computedFieldProvider: InstanceWrapper): void { this.computedFieldProviders.add(computedFieldProvider); } @@ -222,6 +227,10 @@ export class SolidRegistry { return Array.from(this.selectionProviders); } + getDashboardWidgetDataProviders(): Array { + return Array.from(this.dashboardWidgetDataProviders); + } + getSelectionProviderInstance(name: string): ISelectionProvider { const selectionProviders = this.getSelectionProviders(); @@ -233,6 +242,17 @@ export class SolidRegistry { } } + getDashboardWidgetDataProviderInstance(name: string): IDashboardWidgetDataProvider | undefined { + const providers = this.getDashboardWidgetDataProviders(); + for (let i = 0; i < providers.length; i++) { + const provider = providers[i]; + if (provider?.instance?.name?.() === name || provider?.name === name) { + return provider.instance as IDashboardWidgetDataProvider; + } + } + return undefined; + } + getErrorCodeProviders(): Array { return Array.from(this.errorCodeProviders); } diff --git a/src/index.ts b/src/index.ts index 4398e219..28889bab 100755 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,7 @@ export * from './config/cache.options' export * from './decorators/active-user.decorator' export * from './decorators/solid-request-context.decorator' export * from './decorators/auth.decorator' +export * from './decorators/dashboard-widget-data-provider.decorator' export * from './decorators/computed-field-provider.decorator' export * from './decorators/scheduled-job-provider.decorator' export * from './decorators/is-not-in-enum.decorator' @@ -107,6 +108,10 @@ export * from './dtos/update-chatter-message-details.dto' export * from './dtos/update-locale.dto' export * from './dtos/create-user-activity-history.dto' export * from './dtos/update-user-activity-history.dto' +export * from './dtos/dashboard-variable-options-query.dto' +export * from './dtos/dashboard-widget-data-request.dto' +export * from './dtos/create-dashboard-user-layout.dto' +export * from './dtos/update-dashboard-user-layout.dto' export * from './entities/action-metadata.entity' export * from './entities/common.entity' @@ -148,6 +153,7 @@ export * from './entities/locale.entity' export * from './entities/user-activity-history.entity' export * from './entities/ai-interaction.entity' export * from './entities/model-sequence.entity' +export * from './entities/dashboard-user-layout.entity' export * from './entities/user-api-key.entity' export * from './enums/auth-type.enum' @@ -307,6 +313,8 @@ export * from './services/queues/redis-subscriber.service' export * from './services/refresh-token-ids-storage.service' export * from './services/role-metadata.service' export * from './services/selection-providers/list-of-models-selection-provider.service' +export * from './services/selection-providers/mq-dashboard-message-broker-variable-options-provider.service' +export * from './services/selection-providers/mq-dashboard-queue-name-variable-options-provider.service' export * from './services/short-url/tiny-url.service' export * from './services/sms/Msg91BaseSMSService' //rename export * from './services/sms/Msg91OTPService' //rename @@ -334,10 +342,23 @@ export * from './services/import-transaction.service' export * from './services/import-transaction-error-log.service' export * from './services/excel.service' export * from './services/csv.service' +export * from './services/dashboard-runtime.service' export * from './services/queues/publisher-factory.service' export * from './services/queues/database-publisher.service' export * from './services/queues/database-subscriber.service' export * from './services/ai-interaction.service' +export * from './services/dashboard-providers/mq-dashboard-total-messages-kpi-provider.service' +export * from './services/dashboard-providers/mq-dashboard-succeeded-messages-kpi-provider.service' +export * from './services/dashboard-providers/mq-dashboard-failed-messages-kpi-provider.service' +export * from './services/dashboard-providers/mq-dashboard-inflight-messages-kpi-provider.service' +export * from './services/dashboard-providers/mq-dashboard-success-rate-kpi-provider.service' +export * from './services/dashboard-providers/mq-dashboard-avg-elapsed-kpi-provider.service' +export * from './services/dashboard-providers/mq-dashboard-messages-over-time-provider.service' +export * from './services/dashboard-providers/mq-dashboard-stage-distribution-provider.service' +export * from './services/dashboard-providers/mq-dashboard-queue-wise-failures-provider.service' +export * from './services/dashboard-providers/mq-dashboard-queue-wise-avg-elapsed-provider.service' +export * from './services/dashboard-providers/mq-dashboard-latency-trend-provider.service' +export * from './services/dashboard-providers/mq-dashboard-recent-failures-provider.service' // Factories export * from './factories/mail.factory' diff --git a/src/interfaces.ts b/src/interfaces.ts index bb2089f6..9e86e103 100755 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -163,6 +163,39 @@ export interface ISelectionProvider { values(query: any, ctxt: T): Promise; } +export interface IDashboardWidgetDataProviderContext, TProviderContext = Record> { + moduleName: string; + dashboardName: string; + widgetName: string; + variables: TVariables; + providerContext: TProviderContext; + activeUser?: ActiveUserData; +} + +export interface IDashboardWidgetDataResponseEnvelope> { + meta: { + providerName: string; + generatedAt: string; + widgetName: string; + durationMs: number; + [key: string]: any; + }; + data: TData; + uiHints?: TUiHints; +} + +export interface IDashboardWidgetDataProvider< + TContext extends IDashboardWidgetDataProviderContext = IDashboardWidgetDataProviderContext, + TResponse = any, +> { + help(): string; + name(): string; + getData( + widgetDefinition: Record, + ctxt: TContext, + ): Promise | any>; +} + export interface IMcpToolResponseHandler { apply(aiInteraction: AiInteraction); } diff --git a/src/repositories/dashboard-user-layout.repository.ts b/src/repositories/dashboard-user-layout.repository.ts new file mode 100644 index 00000000..bc453466 --- /dev/null +++ b/src/repositories/dashboard-user-layout.repository.ts @@ -0,0 +1,19 @@ +import { Injectable } from '@nestjs/common'; +import { SecurityRuleRepository } from 'src/repository/security-rule.repository'; +import { SolidBaseRepository } from 'src/repository/solid-base.repository' ; +import { RequestContextService } from 'src/services/request-context.service'; +import { DataSource } from 'typeorm'; +import { InjectDataSource } from '@nestjs/typeorm'; +import { DashboardUserLayout } from '../entities/dashboard-user-layout.entity'; + +@Injectable() +export class DashboardUserLayoutRepository extends SolidBaseRepository { + constructor( + @InjectDataSource("default") + readonly dataSource: DataSource, + readonly requestContextService: RequestContextService, + readonly securityRuleRepository: SecurityRuleRepository, + ) { + super(DashboardUserLayout, dataSource, requestContextService, securityRuleRepository); + } +} \ No newline at end of file diff --git a/src/seeders/seed-data/solid-core-metadata.json b/src/seeders/seed-data/solid-core-metadata.json index d7ae4a77..2205f2a1 100755 --- a/src/seeders/seed-data/solid-core-metadata.json +++ b/src/seeders/seed-data/solid-core-metadata.json @@ -1229,6 +1229,92 @@ } ] }, + { + "singularName": "dashboardUserLayout", + "pluralName": "dashboardUserLayouts", + "displayName": "Dashboard User Layout", + "description": "Stores per-user dashboard layout overrides.", + "dataSource": "default", + "dataSourceType": "postgres", + "tableName": "ss_dashboard_user_layout", + "userKeyFieldUserKey": "dashboardName", + "isSystem": true, + "fields": [ + { + "name": "user", + "displayName": "User", + "type": "relation", + "required": true, + "unique": false, + "index": true, + "private": false, + "encrypt": false, + "relationType": "many-to-one", + "relationCoModelSingularName": "user", + "relationCreateInverse": false, + "relationCascade": "cascade", + "relationModelModuleName": "solid-core", + "isSystem": true + }, + { + "name": "module", + "displayName": "Module", + "type": "relation", + "ormType": "integer", + "required": true, + "unique": false, + "index": true, + "private": false, + "encrypt": false, + "relationType": "many-to-one", + "relationCreateInverse": false, + "relationCoModelSingularName": "moduleMetadata", + "relationModelModuleName": "solid-core", + "relationCascade": "cascade", + "isSystem": true + }, + { + "name": "dashboardName", + "displayName": "Dashboard Name", + "type": "shortText", + "ormType": "varchar", + "length": 128, + "required": true, + "unique": false, + "index": true, + "private": false, + "encrypt": false, + "columnName": "dashboard_name", + "isSystem": true + }, + { + "name": "layoutJson", + "displayName": "Layout Json", + "type": "longText", + "ormType": "text", + "required": true, + "unique": false, + "index": false, + "private": false, + "encrypt": false, + "columnName": "layout_json", + "isSystem": true + }, + { + "name": "version", + "displayName": "Version", + "type": "int", + "ormType": "integer", + "required": false, + "unique": false, + "index": false, + "private": false, + "encrypt": false, + "defaultValue": "1", + "isSystem": true + } + ] + }, { "singularName": "actionMetadata", "tableName": "ss_action_metadata", @@ -6052,6 +6138,45 @@ "viewUserKey": "mcpAuditLog-form-view", "moduleUserKey": "solid-core", "modelUserKey": "mcpAuditLog" + }, + { + "displayName": "Queue Health", + "name": "solid-core-queue-health-dashboard-view", + "type": "custom", + "domain": "", + "context": "", + "customComponent": "/admin/dashboard/queue-health", + "customIsModal": false, + "serverEndpoint": "", + "viewUserKey": "", + "moduleUserKey": "solid-core", + "modelUserKey": "mqMessage" + }, + { + "displayName": "Dashboard User Layout List Action", + "name": "dashboardUserLayout-list-action", + "type": "solid", + "domain": "", + "context": "", + "customComponent": "", + "customIsModal": true, + "serverEndpoint": "", + "viewUserKey": "dashboardUserLayout-list-view", + "moduleUserKey": "solid-core", + "modelUserKey": "dashboardUserLayout" + }, + { + "displayName": "Dashboard User Layout Tree View Action", + "name": "dashboardUserLayout-tree-action", + "type": "solid", + "domain": "", + "context": "", + "customComponent": "", + "customIsModal": true, + "serverEndpoint": "", + "viewUserKey": "dashboardUserLayout-tree-view", + "moduleUserKey": "solid-core", + "modelUserKey": "dashboardUserLayout" } ], "menus": [ @@ -6383,6 +6508,23 @@ "actionUserKey": "mcpAuditLog-list-action", "moduleUserKey": "solid-core", "parentMenuItemUserKey": "agent-menu-item" + }, + { + "displayName": "Queue Health", + "name": "solid-core-queue-health-dashboard-menu-item", + "sequenceNumber": 3, + "actionUserKey": "solid-core-queue-health-dashboard-view", + "moduleUserKey": "solid-core", + "parentMenuItemUserKey": "queues-menu-item" + }, + { + "displayName": "Dashboard User Layout", + "name": "dashboardUserLayout-menu-item", + "sequenceNumber": 42, + "actionUserKey": "dashboardUserLayout-list-action", + "moduleUserKey": "solid-core", + "parentMenuItemUserKey": "", + "iconName": "" } ], "views": [ @@ -13131,6 +13273,111 @@ } ] } + }, + { + "name": "dashboardUserLayout-list-view", + "displayName": "Dashboard User Layout", + "type": "list", + "context": "{}", + "moduleUserKey": "solid-core", + "modelUserKey": "dashboardUserLayout", + "layout": { + "type": "list", + "attrs": { + "pagination": true, + "pageSizeOptions": [ + 10, + 25, + 50 + ], + "enableGlobalSearch": true, + "create": true, + "edit": true, + "delete": true + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "id" + } + } + ] + } + }, + { + "name": "dashboardUserLayout-tree-view", + "displayName": "Dashboard User Layout", + "type": "tree", + "context": "{}", + "moduleUserKey": "solid-core", + "modelUserKey": "dashboardUserLayout", + "layout": { + "type": "tree", + "attrs": { + "pagination": true, + "pageSizeOptions": [ + 10, + 25, + 50 + ], + "enableGlobalSearch": true, + "create": true, + "edit": true, + "delete": true + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "id" + } + } + ] + } + }, + { + "name": "dashboardUserLayout-form-view", + "displayName": "Dashboard User Layout", + "type": "form", + "context": "{}", + "moduleUserKey": "solid-core", + "modelUserKey": "dashboardUserLayout", + "layout": { + "type": "form", + "attrs": { + "name": "form-1", + "label": "Dashboard User Layout", + "className": "grid" + }, + "children": [ + { + "type": "sheet", + "attrs": { + "name": "sheet-1" + }, + "children": [ + { + "type": "row", + "attrs": { + "name": "sheet-1" + }, + "children": [ + { + "type": "column", + "attrs": { + "name": "group-1", + "label": "", + "className": "col-12 sm:col-12 md:col-6 lg:col-6" + }, + "children": [] + } + ] + } + ] + } + ] + } } ], "emailTemplates": [ @@ -13268,5 +13515,380 @@ } } ], - "modelSequences": [] + "modelSequences": [], + "dashboards": [ + { + "dashboardSchemaVersion": 1, + "name": "queue-health", + "displayName": "Queue Health Dashboard", + "description": "Operational visibility for MQ queues and messages.", + "routeName": "queue-health", + "routePath": "/admin/dashboard/queue-health", + "moduleUserKey": "solid-core", + "permissions": [ + "DashboardController.getDefinition", + "DashboardController.getVariableOptions", + "DashboardController.getWidgetData", + "DashboardController.getDashboardData", + "DashboardController.getLayout", + "DashboardController.saveLayout" + ], + "variables": [ + { + "name": "date", + "label": "Created At", + "type": "date", + "required": true, + "defaultValue": { + "preset": "last_7_days" + } + }, + { + "name": "queueName", + "label": "Queue", + "type": "selectionDynamic", + "required": false, + "isMultiSelect": false, + "selectionConfig": { + "providerName": "MqDashboardQueueNameVariableOptionsProvider", + "providerContext": { + "labelField": "name", + "valueField": "name" + } + } + }, + { + "name": "stage", + "label": "Stage", + "type": "selectionStatic", + "required": false, + "isMultiSelect": true, + "selectionStaticValues": [ + "pending:Pending", + "scheduled:Scheduled", + "started:Started", + "retry:Retry", + "retrying:Retrying", + "failed:Failed", + "succeeded:Succeeded" + ] + }, + { + "name": "messageBroker", + "label": "Message Broker", + "type": "selectionDynamic", + "required": false, + "isMultiSelect": false, + "selectionConfig": { + "providerName": "MqDashboardMessageBrokerVariableOptionsProvider", + "providerContext": { + "labelField": "messageBroker", + "valueField": "messageBroker" + } + } + } + ], + "widgets": [ + { + "id": "kpi-total-messages", + "name": "Total Messages", + "type": "kpi", + "dataProvider": "MqDashboardTotalMessagesKpiProvider", + "providerContext": { + "metric": "count_total_messages" + }, + "refreshPolicy": { + "mode": "interval", + "intervalSeconds": 60 + } + }, + { + "id": "kpi-succeeded-messages", + "name": "Succeeded", + "type": "kpi", + "dataProvider": "MqDashboardSucceededMessagesKpiProvider", + "providerContext": { + "metric": "count_succeeded_messages" + }, + "refreshPolicy": { + "mode": "interval", + "intervalSeconds": 60 + } + }, + { + "id": "kpi-failed-messages", + "name": "Failed", + "type": "kpi", + "dataProvider": "MqDashboardFailedMessagesKpiProvider", + "providerContext": { + "metric": "count_failed_messages" + }, + "refreshPolicy": { + "mode": "interval", + "intervalSeconds": 60 + } + }, + { + "id": "kpi-in-flight-messages", + "name": "In Flight", + "type": "kpi", + "dataProvider": "MqDashboardInflightMessagesKpiProvider", + "providerContext": { + "metric": "count_inflight_messages" + }, + "refreshPolicy": { + "mode": "interval", + "intervalSeconds": 60 + } + }, + { + "id": "kpi-success-rate", + "name": "Success Rate", + "type": "kpi", + "dataProvider": "MqDashboardSuccessRateKpiProvider", + "providerContext": { + "metric": "success_rate_percentage" + }, + "refreshPolicy": { + "mode": "interval", + "intervalSeconds": 60 + } + }, + { + "id": "kpi-avg-elapsed-ms", + "name": "Avg Elapsed (ms)", + "type": "kpi", + "dataProvider": "MqDashboardAvgElapsedKpiProvider", + "providerContext": { + "metric": "average_elapsed_millis" + }, + "refreshPolicy": { + "mode": "interval", + "intervalSeconds": 60 + } + }, + { + "id": "chart-messages-over-time", + "name": "Messages Over Time", + "type": "lineChart", + "dataProvider": "MqDashboardMessagesOverTimeProvider", + "providerContext": { + "timeField": "createdAt", + "bucket": "hour", + "series": [ + "total", + "succeeded", + "failed", + "retrying" + ] + }, + "refreshPolicy": { + "mode": "interval", + "intervalSeconds": 120 + } + }, + { + "id": "chart-stage-distribution", + "name": "Stage Distribution", + "type": "pieChart", + "dataProvider": "MqDashboardStageDistributionProvider", + "providerContext": { + "groupBy": "stage", + "metric": "count" + }, + "refreshPolicy": { + "mode": "interval", + "intervalSeconds": 120 + } + }, + { + "id": "chart-queue-wise-failures", + "name": "Queue-wise Failures", + "type": "barChart", + "dataProvider": "MqDashboardQueueWiseFailuresProvider", + "providerContext": { + "groupBy": "mqMessageQueue.name", + "metric": "failed_count" + }, + "refreshPolicy": { + "mode": "interval", + "intervalSeconds": 120 + } + }, + { + "id": "chart-queue-wise-avg-elapsed", + "name": "Queue-wise Avg Processing Time", + "type": "barChart", + "dataProvider": "MqDashboardQueueWiseAvgElapsedProvider", + "providerContext": { + "groupBy": "mqMessageQueue.name", + "metric": "avg_elapsed_millis" + }, + "refreshPolicy": { + "mode": "interval", + "intervalSeconds": 120 + } + }, + { + "id": "chart-processing-latency-trend", + "name": "Processing Latency Trend", + "type": "lineChart", + "dataProvider": "MqDashboardLatencyTrendProvider", + "providerContext": { + "timeField": "createdAt", + "bucket": "hour", + "metric": "avg_elapsed_millis" + }, + "refreshPolicy": { + "mode": "interval", + "intervalSeconds": 120 + } + }, + { + "id": "table-recent-failures", + "name": "Recent Failures", + "type": "table", + "dataProvider": "MqDashboardRecentFailuresProvider", + "providerContext": { + "stage": "failed", + "sort": { + "field": "createdAt", + "order": "desc" + }, + "columns": [ + "id", + "messageId", + "mqMessageQueue.name", + "stage", + "retryCount", + "elapsedMillis", + "startedAt", + "finishedAt", + "error", + "createdAt" + ], + "errorMaxLength": 160 + }, + "refreshPolicy": { + "mode": "interval", + "intervalSeconds": 60 + } + } + ], + "defaultLayout": { + "engine": "gridstack", + "columns": 12, + "items": [ + { + "widgetId": "kpi-total-messages", + "x": 0, + "y": 0, + "w": 2, + "h": 2, + "minW": 2, + "minH": 2 + }, + { + "widgetId": "kpi-succeeded-messages", + "x": 2, + "y": 0, + "w": 2, + "h": 2, + "minW": 2, + "minH": 2 + }, + { + "widgetId": "kpi-failed-messages", + "x": 4, + "y": 0, + "w": 2, + "h": 2, + "minW": 2, + "minH": 2 + }, + { + "widgetId": "kpi-in-flight-messages", + "x": 6, + "y": 0, + "w": 2, + "h": 2, + "minW": 2, + "minH": 2 + }, + { + "widgetId": "kpi-success-rate", + "x": 8, + "y": 0, + "w": 2, + "h": 2, + "minW": 2, + "minH": 2 + }, + { + "widgetId": "kpi-avg-elapsed-ms", + "x": 10, + "y": 0, + "w": 2, + "h": 2, + "minW": 2, + "minH": 2 + }, + { + "widgetId": "chart-messages-over-time", + "x": 0, + "y": 2, + "w": 8, + "h": 5, + "minW": 4, + "minH": 4 + }, + { + "widgetId": "chart-stage-distribution", + "x": 8, + "y": 2, + "w": 4, + "h": 5, + "minW": 3, + "minH": 4 + }, + { + "widgetId": "chart-queue-wise-failures", + "x": 0, + "y": 7, + "w": 6, + "h": 5, + "minW": 4, + "minH": 4 + }, + { + "widgetId": "chart-queue-wise-avg-elapsed", + "x": 6, + "y": 7, + "w": 6, + "h": 5, + "minW": 4, + "minH": 4 + }, + { + "widgetId": "chart-processing-latency-trend", + "x": 0, + "y": 12, + "w": 6, + "h": 6, + "minW": 4, + "minH": 4 + }, + { + "widgetId": "table-recent-failures", + "x": 6, + "y": 12, + "w": 6, + "h": 6, + "minW": 4, + "minH": 4 + } + ] + } + } + ] } \ No newline at end of file diff --git a/src/services/dashboard-providers/mq-dashboard-avg-elapsed-kpi-provider.service.ts b/src/services/dashboard-providers/mq-dashboard-avg-elapsed-kpi-provider.service.ts new file mode 100644 index 00000000..ee882da6 --- /dev/null +++ b/src/services/dashboard-providers/mq-dashboard-avg-elapsed-kpi-provider.service.ts @@ -0,0 +1,49 @@ +import { Injectable } from "@nestjs/common"; +import { DashboardWidgetDataProvider } from "src/decorators/dashboard-widget-data-provider.decorator"; +import { MqMessageRepository } from "src/repository/mq-message.repository"; +import { IDashboardWidgetDataProvider, IDashboardWidgetDataProviderContext, IDashboardWidgetDataResponseEnvelope } from "src/interfaces"; +import { applyMqDashboardFilters, toNumber } from "src/services/dashboard-providers/mq-dashboard-provider-utils"; + +@DashboardWidgetDataProvider() +@Injectable() +export class MqDashboardAvgElapsedKpiProvider implements IDashboardWidgetDataProvider { + constructor( + private readonly mqMessageRepository: MqMessageRepository, + ) { } + + name(): string { + return "MqDashboardAvgElapsedKpiProvider"; + } + + help(): string { + return "Returns average elapsed millis after applying dashboard variables."; + } + + async getData( + widgetDefinition: Record, + ctxt: IDashboardWidgetDataProviderContext, + ): Promise> { + const qb = await this.mqMessageRepository.createSecurityRuleAwareQueryBuilder("mqMessage"); + applyMqDashboardFilters(qb, ctxt.variables ?? {}); + qb.andWhere("mqMessage.elapsedMillis IS NOT NULL"); + + const raw = await qb + .select("AVG(mqMessage.elapsedMillis)", "avgElapsed") + .getRawOne<{ avgElapsed?: string | number }>(); + + const value = Number(toNumber(raw?.avgElapsed, 0).toFixed(2)); + return { + meta: { + providerName: this.name(), + generatedAt: new Date().toISOString(), + widgetName: ctxt.widgetName, + durationMs: 0, + }, + data: { + value, + label: widgetDefinition?.name ?? "Avg Elapsed (ms)", + }, + }; + } +} + diff --git a/src/services/dashboard-providers/mq-dashboard-failed-messages-kpi-provider.service.ts b/src/services/dashboard-providers/mq-dashboard-failed-messages-kpi-provider.service.ts new file mode 100644 index 00000000..2b41355e --- /dev/null +++ b/src/services/dashboard-providers/mq-dashboard-failed-messages-kpi-provider.service.ts @@ -0,0 +1,45 @@ +import { Injectable } from "@nestjs/common"; +import { DashboardWidgetDataProvider } from "src/decorators/dashboard-widget-data-provider.decorator"; +import { MqMessageRepository } from "src/repository/mq-message.repository"; +import { IDashboardWidgetDataProvider, IDashboardWidgetDataProviderContext, IDashboardWidgetDataResponseEnvelope } from "src/interfaces"; +import { applyMqDashboardFilters } from "src/services/dashboard-providers/mq-dashboard-provider-utils"; + +@DashboardWidgetDataProvider() +@Injectable() +export class MqDashboardFailedMessagesKpiProvider implements IDashboardWidgetDataProvider { + constructor( + private readonly mqMessageRepository: MqMessageRepository, + ) { } + + name(): string { + return "MqDashboardFailedMessagesKpiProvider"; + } + + help(): string { + return "Returns failed mq message count after applying dashboard variables."; + } + + async getData( + widgetDefinition: Record, + ctxt: IDashboardWidgetDataProviderContext, + ): Promise> { + const qb = await this.mqMessageRepository.createSecurityRuleAwareQueryBuilder("mqMessage"); + applyMqDashboardFilters(qb, ctxt.variables ?? {}, { ignoreStage: true }); + qb.andWhere("mqMessage.stage = :stage", { stage: "failed" }); + + const value = await qb.getCount(); + return { + meta: { + providerName: this.name(), + generatedAt: new Date().toISOString(), + widgetName: ctxt.widgetName, + durationMs: 0, + }, + data: { + value, + label: widgetDefinition?.name ?? "Failed", + }, + }; + } +} + diff --git a/src/services/dashboard-providers/mq-dashboard-inflight-messages-kpi-provider.service.ts b/src/services/dashboard-providers/mq-dashboard-inflight-messages-kpi-provider.service.ts new file mode 100644 index 00000000..90bf5cfd --- /dev/null +++ b/src/services/dashboard-providers/mq-dashboard-inflight-messages-kpi-provider.service.ts @@ -0,0 +1,45 @@ +import { Injectable } from "@nestjs/common"; +import { DashboardWidgetDataProvider } from "src/decorators/dashboard-widget-data-provider.decorator"; +import { MqMessageRepository } from "src/repository/mq-message.repository"; +import { IDashboardWidgetDataProvider, IDashboardWidgetDataProviderContext, IDashboardWidgetDataResponseEnvelope } from "src/interfaces"; +import { applyMqDashboardFilters, MQ_INFLIGHT_STAGES } from "src/services/dashboard-providers/mq-dashboard-provider-utils"; + +@DashboardWidgetDataProvider() +@Injectable() +export class MqDashboardInflightMessagesKpiProvider implements IDashboardWidgetDataProvider { + constructor( + private readonly mqMessageRepository: MqMessageRepository, + ) { } + + name(): string { + return "MqDashboardInflightMessagesKpiProvider"; + } + + help(): string { + return "Returns in-flight mq message count after applying dashboard variables."; + } + + async getData( + widgetDefinition: Record, + ctxt: IDashboardWidgetDataProviderContext, + ): Promise> { + const qb = await this.mqMessageRepository.createSecurityRuleAwareQueryBuilder("mqMessage"); + applyMqDashboardFilters(qb, ctxt.variables ?? {}, { ignoreStage: true }); + qb.andWhere("mqMessage.stage IN (:...stages)", { stages: MQ_INFLIGHT_STAGES }); + + const value = await qb.getCount(); + return { + meta: { + providerName: this.name(), + generatedAt: new Date().toISOString(), + widgetName: ctxt.widgetName, + durationMs: 0, + }, + data: { + value, + label: widgetDefinition?.name ?? "In Flight", + }, + }; + } +} + diff --git a/src/services/dashboard-providers/mq-dashboard-latency-trend-provider.service.ts b/src/services/dashboard-providers/mq-dashboard-latency-trend-provider.service.ts new file mode 100644 index 00000000..657f9158 --- /dev/null +++ b/src/services/dashboard-providers/mq-dashboard-latency-trend-provider.service.ts @@ -0,0 +1,58 @@ +import { Injectable } from "@nestjs/common"; +import { DashboardWidgetDataProvider } from "src/decorators/dashboard-widget-data-provider.decorator"; +import { MqMessageRepository } from "src/repository/mq-message.repository"; +import { IDashboardWidgetDataProvider, IDashboardWidgetDataProviderContext, IDashboardWidgetDataResponseEnvelope } from "src/interfaces"; +import { applyMqDashboardFilters, normalizeBucket, toNumber } from "src/services/dashboard-providers/mq-dashboard-provider-utils"; + +@DashboardWidgetDataProvider() +@Injectable() +export class MqDashboardLatencyTrendProvider implements IDashboardWidgetDataProvider { + constructor( + private readonly mqMessageRepository: MqMessageRepository, + ) { } + + name(): string { + return "MqDashboardLatencyTrendProvider"; + } + + help(): string { + return "Returns average elapsed millis trend over time buckets after applying dashboard variables."; + } + + async getData( + widgetDefinition: Record, + ctxt: IDashboardWidgetDataProviderContext, + ): Promise> { + const bucket = normalizeBucket(ctxt?.providerContext?.bucket); + + const qb = await this.mqMessageRepository.createSecurityRuleAwareQueryBuilder("mqMessage"); + applyMqDashboardFilters(qb, ctxt.variables ?? {}); + qb.andWhere("mqMessage.elapsedMillis IS NOT NULL"); + + const bucketExpr = `DATE_TRUNC('${bucket}', mqMessage.createdAt)`; + const rows = await qb + .select(bucketExpr, "bucket") + .addSelect("AVG(mqMessage.elapsedMillis)", "value") + .groupBy(bucketExpr) + .orderBy("bucket", "ASC") + .getRawMany<{ bucket: string; value: string | number }>(); + + const categories = rows.map((r) => new Date(r.bucket).toISOString()); + const values = rows.map((r) => Number(toNumber(r.value).toFixed(2))); + + return { + meta: { + providerName: this.name(), + generatedAt: new Date().toISOString(), + widgetName: ctxt.widgetName, + durationMs: 0, + }, + data: { + categories, + series: [{ name: "avg_elapsed_millis", data: values }], + bucket, + }, + }; + } +} + diff --git a/src/services/dashboard-providers/mq-dashboard-messages-over-time-provider.service.ts b/src/services/dashboard-providers/mq-dashboard-messages-over-time-provider.service.ts new file mode 100644 index 00000000..9a941a6f --- /dev/null +++ b/src/services/dashboard-providers/mq-dashboard-messages-over-time-provider.service.ts @@ -0,0 +1,79 @@ +import { Injectable } from "@nestjs/common"; +import { DashboardWidgetDataProvider } from "src/decorators/dashboard-widget-data-provider.decorator"; +import { MqMessageRepository } from "src/repository/mq-message.repository"; +import { IDashboardWidgetDataProvider, IDashboardWidgetDataProviderContext, IDashboardWidgetDataResponseEnvelope } from "src/interfaces"; +import { applyMqDashboardFilters, normalizeBucket, toNumber } from "src/services/dashboard-providers/mq-dashboard-provider-utils"; + +@DashboardWidgetDataProvider() +@Injectable() +export class MqDashboardMessagesOverTimeProvider implements IDashboardWidgetDataProvider { + constructor( + private readonly mqMessageRepository: MqMessageRepository, + ) { } + + name(): string { + return "MqDashboardMessagesOverTimeProvider"; + } + + help(): string { + return "Returns time-series message counts by stage (and total) after applying dashboard variables."; + } + + async getData( + widgetDefinition: Record, + ctxt: IDashboardWidgetDataProviderContext, + ): Promise> { + const bucket = normalizeBucket(ctxt?.providerContext?.bucket); + const requestedSeries: string[] = Array.isArray(ctxt?.providerContext?.series) && ctxt.providerContext.series.length > 0 + ? ctxt.providerContext.series + : ["total", "succeeded", "failed", "retrying"]; + + const qb = await this.mqMessageRepository.createSecurityRuleAwareQueryBuilder("mqMessage"); + applyMqDashboardFilters(qb, ctxt.variables ?? {}); + + const bucketExpr = `DATE_TRUNC('${bucket}', mqMessage.createdAt)`; + const rows = await qb + .select(bucketExpr, "bucket") + .addSelect("mqMessage.stage", "stage") + .addSelect("COUNT(mqMessage.id)", "count") + .groupBy(bucketExpr) + .addGroupBy("mqMessage.stage") + .orderBy("bucket", "ASC") + .getRawMany<{ bucket: string; stage: string; count: string | number }>(); + + const bucketToStageMap = new Map>(); + for (const row of rows) { + const key = new Date(row.bucket).toISOString(); + const entry = bucketToStageMap.get(key) ?? {}; + entry[row.stage] = (entry[row.stage] ?? 0) + toNumber(row.count, 0); + bucketToStageMap.set(key, entry); + } + + const categories = Array.from(bucketToStageMap.keys()).sort(); + const series = requestedSeries.map((seriesName) => { + const data = categories.map((category) => { + const stageMap = bucketToStageMap.get(category) ?? {}; + if (seriesName === "total") { + return Object.values(stageMap).reduce((sum, value) => sum + value, 0); + } + return stageMap[seriesName] ?? 0; + }); + return { name: seriesName, data }; + }); + + return { + meta: { + providerName: this.name(), + generatedAt: new Date().toISOString(), + widgetName: ctxt.widgetName, + durationMs: 0, + }, + data: { + categories, + series, + bucket, + }, + }; + } +} + diff --git a/src/services/dashboard-providers/mq-dashboard-provider-utils.ts b/src/services/dashboard-providers/mq-dashboard-provider-utils.ts new file mode 100644 index 00000000..d40ed9b6 --- /dev/null +++ b/src/services/dashboard-providers/mq-dashboard-provider-utils.ts @@ -0,0 +1,165 @@ +import { MqMessage } from "src/entities/mq-message.entity"; +import { SelectQueryBuilder } from "typeorm"; + +export const MQ_INFLIGHT_STAGES = ['pending', 'scheduled', 'started', 'retry', 'retrying']; + +export interface MqDashboardVariables { + date?: any; + queueName?: string | string[]; + stage?: string | string[]; + messageBroker?: string | string[]; +} + +export interface ApplyMqDashboardFiltersOptions { + ignoreStage?: boolean; + ensureQueueJoin?: boolean; +} + +export function applyMqDashboardFilters( + qb: SelectQueryBuilder, + variables: MqDashboardVariables = {}, + options: ApplyMqDashboardFiltersOptions = {}, +): SelectQueryBuilder { + applyDateFilter(qb, variables.date); + applyStringOrArrayFilter(qb, 'mqMessage.messageBroker', variables.messageBroker, 'messageBroker'); + + if (!options.ignoreStage) { + applyStringOrArrayFilter(qb, 'mqMessage.stage', variables.stage, 'stage'); + } + + if ( + options.ensureQueueJoin || + (variables.queueName !== undefined && variables.queueName !== null && variables.queueName !== '') + ) { + qb.leftJoin('mqMessage.mqMessageQueue', 'mqMessageQueue'); + } + + if (variables.queueName !== undefined && variables.queueName !== null && variables.queueName !== '') { + applyStringOrArrayFilter(qb, 'mqMessageQueue.name', variables.queueName, 'queueName'); + } + + return qb; +} + +export function normalizeBucket(value: string | undefined): 'hour' | 'day' | 'week' | 'month' { + const v = `${value ?? ''}`.toLowerCase(); + if (v === 'hour' || v === 'day' || v === 'week' || v === 'month') { + return v; + } + return 'hour'; +} + +export function toNumber(value: any, fallback = 0): number { + const n = Number(value); + return Number.isFinite(n) ? n : fallback; +} + + +function applyDateFilter(qb: SelectQueryBuilder, dateVariable: any): void { + const range = resolveDateRange(dateVariable); + if (range.from) { + qb.andWhere('mqMessage.createdAt >= :dashboardDateFrom', { dashboardDateFrom: range.from.toISOString() }); + } + if (range.to) { + qb.andWhere('mqMessage.createdAt <= :dashboardDateTo', { dashboardDateTo: range.to.toISOString() }); + } +} + +function applyStringOrArrayFilter( + qb: SelectQueryBuilder, + qualifiedColumn: string, + value: string | string[] | undefined, + paramKey: string, +) { + if (value === undefined || value === null || value === '') { + return; + } + + const values = Array.isArray(value) ? value.filter((v) => !!v) : [value]; + if (values.length === 0) { + return; + } + + if (values.length === 1) { + qb.andWhere(`${qualifiedColumn} = :${paramKey}`, { [paramKey]: values[0] }); + return; + } + + qb.andWhere(`${qualifiedColumn} IN (:...${paramKey})`, { [paramKey]: values }); +} + +function resolveDateRange(dateVariable: any): { from?: Date; to?: Date } { + if (!dateVariable) return {}; + + if (Array.isArray(dateVariable) && dateVariable.length >= 2) { + return { + from: parseDate(dateVariable[0]), + to: parseDate(dateVariable[1]), + }; + } + + if (typeof dateVariable === 'object') { + const presetRange = resolvePresetRange(dateVariable.preset); + const from = parseDate(dateVariable.from ?? dateVariable.start ?? dateVariable.startDate) ?? presetRange.from; + const to = parseDate(dateVariable.to ?? dateVariable.end ?? dateVariable.endDate) ?? presetRange.to; + return { from, to }; + } + + if (typeof dateVariable === 'string') { + const presetRange = resolvePresetRange(dateVariable); + if (presetRange.from || presetRange.to) { + return presetRange; + } + const parsed = parseDate(dateVariable); + if (!parsed) return {}; + return { from: parsed, to: parsed }; + } + + return {}; +} + +function parseDate(value: any): Date | undefined { + if (!value) return undefined; + const d = new Date(value); + if (Number.isNaN(d.getTime())) return undefined; + return d; +} + +function resolvePresetRange(preset: string | undefined): { from?: Date; to?: Date } { + if (!preset) return {}; + + const now = new Date(); + const to = new Date(now); + switch (preset) { + case 'today': { + const from = new Date(now); + from.setHours(0, 0, 0, 0); + return { from, to }; + } + case 'yesterday': { + const from = new Date(now); + from.setDate(from.getDate() - 1); + from.setHours(0, 0, 0, 0); + const end = new Date(from); + end.setHours(23, 59, 59, 999); + return { from, to: end }; + } + case 'last_24_hours': { + const from = new Date(now); + from.setHours(from.getHours() - 24); + return { from, to }; + } + case 'last_7_days': { + const from = new Date(now); + from.setDate(from.getDate() - 7); + return { from, to }; + } + case 'last_30_days': { + const from = new Date(now); + from.setDate(from.getDate() - 30); + return { from, to }; + } + default: + return {}; + } +} diff --git a/src/services/dashboard-providers/mq-dashboard-queue-wise-avg-elapsed-provider.service.ts b/src/services/dashboard-providers/mq-dashboard-queue-wise-avg-elapsed-provider.service.ts new file mode 100644 index 00000000..152cb5dc --- /dev/null +++ b/src/services/dashboard-providers/mq-dashboard-queue-wise-avg-elapsed-provider.service.ts @@ -0,0 +1,51 @@ +import { Injectable } from "@nestjs/common"; +import { DashboardWidgetDataProvider } from "src/decorators/dashboard-widget-data-provider.decorator"; +import { MqMessageRepository } from "src/repository/mq-message.repository"; +import { IDashboardWidgetDataProvider, IDashboardWidgetDataProviderContext, IDashboardWidgetDataResponseEnvelope } from "src/interfaces"; +import { applyMqDashboardFilters, toNumber } from "src/services/dashboard-providers/mq-dashboard-provider-utils"; + +@DashboardWidgetDataProvider() +@Injectable() +export class MqDashboardQueueWiseAvgElapsedProvider implements IDashboardWidgetDataProvider { + constructor( + private readonly mqMessageRepository: MqMessageRepository, + ) { } + + name(): string { + return "MqDashboardQueueWiseAvgElapsedProvider"; + } + + help(): string { + return "Returns queue-wise average elapsed millis after applying dashboard variables."; + } + + async getData( + widgetDefinition: Record, + ctxt: IDashboardWidgetDataProviderContext, + ): Promise> { + const qb = await this.mqMessageRepository.createSecurityRuleAwareQueryBuilder("mqMessage"); + applyMqDashboardFilters(qb, ctxt.variables ?? {}, { ensureQueueJoin: true }); + qb.andWhere("mqMessage.elapsedMillis IS NOT NULL"); + + const rows = await qb + .select("mqMessageQueue.name", "category") + .addSelect("AVG(mqMessage.elapsedMillis)", "value") + .groupBy("mqMessageQueue.name") + .orderBy("value", "DESC") + .getRawMany<{ category: string; value: string | number }>(); + + return { + meta: { + providerName: this.name(), + generatedAt: new Date().toISOString(), + widgetName: ctxt.widgetName, + durationMs: 0, + }, + data: { + categories: rows.map((r) => r.category), + series: [{ name: "avg_elapsed_millis", data: rows.map((r) => Number(toNumber(r.value).toFixed(2))) }], + }, + }; + } +} + diff --git a/src/services/dashboard-providers/mq-dashboard-queue-wise-failures-provider.service.ts b/src/services/dashboard-providers/mq-dashboard-queue-wise-failures-provider.service.ts new file mode 100644 index 00000000..df1b03a7 --- /dev/null +++ b/src/services/dashboard-providers/mq-dashboard-queue-wise-failures-provider.service.ts @@ -0,0 +1,51 @@ +import { Injectable } from "@nestjs/common"; +import { DashboardWidgetDataProvider } from "src/decorators/dashboard-widget-data-provider.decorator"; +import { MqMessageRepository } from "src/repository/mq-message.repository"; +import { IDashboardWidgetDataProvider, IDashboardWidgetDataProviderContext, IDashboardWidgetDataResponseEnvelope } from "src/interfaces"; +import { applyMqDashboardFilters, toNumber } from "src/services/dashboard-providers/mq-dashboard-provider-utils"; + +@DashboardWidgetDataProvider() +@Injectable() +export class MqDashboardQueueWiseFailuresProvider implements IDashboardWidgetDataProvider { + constructor( + private readonly mqMessageRepository: MqMessageRepository, + ) { } + + name(): string { + return "MqDashboardQueueWiseFailuresProvider"; + } + + help(): string { + return "Returns queue-wise failed message count after applying dashboard variables."; + } + + async getData( + widgetDefinition: Record, + ctxt: IDashboardWidgetDataProviderContext, + ): Promise> { + const qb = await this.mqMessageRepository.createSecurityRuleAwareQueryBuilder("mqMessage"); + applyMqDashboardFilters(qb, ctxt.variables ?? {}, { ignoreStage: true, ensureQueueJoin: true }); + qb.andWhere("mqMessage.stage = :failedStage", { failedStage: "failed" }); + + const rows = await qb + .select("mqMessageQueue.name", "category") + .addSelect("COUNT(mqMessage.id)", "value") + .groupBy("mqMessageQueue.name") + .orderBy("value", "DESC") + .getRawMany<{ category: string; value: string | number }>(); + + return { + meta: { + providerName: this.name(), + generatedAt: new Date().toISOString(), + widgetName: ctxt.widgetName, + durationMs: 0, + }, + data: { + categories: rows.map((r) => r.category), + series: [{ name: "failed_count", data: rows.map((r) => toNumber(r.value)) }], + }, + }; + } +} + diff --git a/src/services/dashboard-providers/mq-dashboard-recent-failures-provider.service.ts b/src/services/dashboard-providers/mq-dashboard-recent-failures-provider.service.ts new file mode 100644 index 00000000..8d1eb698 --- /dev/null +++ b/src/services/dashboard-providers/mq-dashboard-recent-failures-provider.service.ts @@ -0,0 +1,95 @@ +import { Injectable } from "@nestjs/common"; +import { DashboardWidgetDataProvider } from "src/decorators/dashboard-widget-data-provider.decorator"; +import { MqMessageRepository } from "src/repository/mq-message.repository"; +import { IDashboardWidgetDataProvider, IDashboardWidgetDataProviderContext, IDashboardWidgetDataResponseEnvelope } from "src/interfaces"; +import { applyMqDashboardFilters } from "src/services/dashboard-providers/mq-dashboard-provider-utils"; + +@DashboardWidgetDataProvider() +@Injectable() +export class MqDashboardRecentFailuresProvider implements IDashboardWidgetDataProvider { + constructor( + private readonly mqMessageRepository: MqMessageRepository, + ) { } + + name(): string { + return "MqDashboardRecentFailuresProvider"; + } + + help(): string { + return "Returns recent failed mq messages in a tabular payload after applying dashboard variables."; + } + + async getData( + widgetDefinition: Record, + ctxt: IDashboardWidgetDataProviderContext, + ): Promise> { + const limit = Math.min(Math.max(Number(ctxt?.providerContext?.limit ?? 25), 1), 200); + const errorMaxLength = Math.max(Number(ctxt?.providerContext?.errorMaxLength ?? 160), 0); + + const qb = await this.mqMessageRepository.createSecurityRuleAwareQueryBuilder("mqMessage"); + applyMqDashboardFilters(qb, ctxt.variables ?? {}, { ignoreStage: true, ensureQueueJoin: true }); + qb.andWhere("mqMessage.stage = :failedStage", { failedStage: "failed" }); + + const rows = await qb + .select([ + "mqMessage.id AS id", + "mqMessage.messageId AS messageId", + "mqMessageQueue.name AS queueName", + "mqMessage.stage AS stage", + "mqMessage.retryCount AS retryCount", + "mqMessage.elapsedMillis AS elapsedMillis", + "mqMessage.startedAt AS startedAt", + "mqMessage.finishedAt AS finishedAt", + "mqMessage.error AS error", + "mqMessage.createdAt AS createdAt", + ]) + .orderBy("mqMessage.createdAt", "DESC") + .take(limit) + .getRawMany>(); + + const columns = [ + "id", + "messageId", + "queueName", + "stage", + "retryCount", + "elapsedMillis", + "startedAt", + "finishedAt", + "error", + "createdAt", + ]; + + const records = rows.map((row) => ({ + id: row.id ?? null, + messageId: row.messageId ?? row.messageid ?? null, + queueName: row.queueName ?? row.queuename ?? null, + stage: row.stage ?? null, + retryCount: row.retryCount ?? row.retrycount ?? null, + elapsedMillis: row.elapsedMillis ?? row.elapsedmillis ?? null, + startedAt: row.startedAt ?? row.startedat ?? null, + finishedAt: row.finishedAt ?? row.finishedat ?? null, + error: truncateString(row.error ?? null, errorMaxLength), + createdAt: row.createdAt ?? row.createdat ?? null, + })); + + return { + meta: { + providerName: this.name(), + generatedAt: new Date().toISOString(), + widgetName: ctxt.widgetName, + durationMs: 0, + }, + data: { + columns, + records, + }, + }; + } +} + +function truncateString(value: any, maxLength: number): any { + if (!value || typeof value !== 'string') return value; + if (maxLength <= 0 || value.length <= maxLength) return value; + return `${value.slice(0, maxLength)}...`; +} diff --git a/src/services/dashboard-providers/mq-dashboard-stage-distribution-provider.service.ts b/src/services/dashboard-providers/mq-dashboard-stage-distribution-provider.service.ts new file mode 100644 index 00000000..c945ae40 --- /dev/null +++ b/src/services/dashboard-providers/mq-dashboard-stage-distribution-provider.service.ts @@ -0,0 +1,49 @@ +import { Injectable } from "@nestjs/common"; +import { DashboardWidgetDataProvider } from "src/decorators/dashboard-widget-data-provider.decorator"; +import { MqMessageRepository } from "src/repository/mq-message.repository"; +import { IDashboardWidgetDataProvider, IDashboardWidgetDataProviderContext, IDashboardWidgetDataResponseEnvelope } from "src/interfaces"; +import { applyMqDashboardFilters, toNumber } from "src/services/dashboard-providers/mq-dashboard-provider-utils"; + +@DashboardWidgetDataProvider() +@Injectable() +export class MqDashboardStageDistributionProvider implements IDashboardWidgetDataProvider { + constructor( + private readonly mqMessageRepository: MqMessageRepository, + ) { } + + name(): string { + return "MqDashboardStageDistributionProvider"; + } + + help(): string { + return "Returns grouped count by stage after applying dashboard variables."; + } + + async getData( + widgetDefinition: Record, + ctxt: IDashboardWidgetDataProviderContext, + ): Promise> { + const qb = await this.mqMessageRepository.createSecurityRuleAwareQueryBuilder("mqMessage"); + applyMqDashboardFilters(qb, ctxt.variables ?? {}); + + const rows = await qb + .select("mqMessage.stage", "name") + .addSelect("COUNT(mqMessage.id)", "value") + .groupBy("mqMessage.stage") + .orderBy("value", "DESC") + .getRawMany<{ name: string; value: string | number }>(); + + return { + meta: { + providerName: this.name(), + generatedAt: new Date().toISOString(), + widgetName: ctxt.widgetName, + durationMs: 0, + }, + data: { + items: rows.map((r) => ({ name: r.name, value: toNumber(r.value) })), + }, + }; + } +} + diff --git a/src/services/dashboard-providers/mq-dashboard-succeeded-messages-kpi-provider.service.ts b/src/services/dashboard-providers/mq-dashboard-succeeded-messages-kpi-provider.service.ts new file mode 100644 index 00000000..cbc84beb --- /dev/null +++ b/src/services/dashboard-providers/mq-dashboard-succeeded-messages-kpi-provider.service.ts @@ -0,0 +1,45 @@ +import { Injectable } from "@nestjs/common"; +import { DashboardWidgetDataProvider } from "src/decorators/dashboard-widget-data-provider.decorator"; +import { MqMessageRepository } from "src/repository/mq-message.repository"; +import { IDashboardWidgetDataProvider, IDashboardWidgetDataProviderContext, IDashboardWidgetDataResponseEnvelope } from "src/interfaces"; +import { applyMqDashboardFilters } from "src/services/dashboard-providers/mq-dashboard-provider-utils"; + +@DashboardWidgetDataProvider() +@Injectable() +export class MqDashboardSucceededMessagesKpiProvider implements IDashboardWidgetDataProvider { + constructor( + private readonly mqMessageRepository: MqMessageRepository, + ) { } + + name(): string { + return "MqDashboardSucceededMessagesKpiProvider"; + } + + help(): string { + return "Returns succeeded mq message count after applying dashboard variables."; + } + + async getData( + widgetDefinition: Record, + ctxt: IDashboardWidgetDataProviderContext, + ): Promise> { + const qb = await this.mqMessageRepository.createSecurityRuleAwareQueryBuilder("mqMessage"); + applyMqDashboardFilters(qb, ctxt.variables ?? {}, { ignoreStage: true }); + qb.andWhere("mqMessage.stage = :stage", { stage: "succeeded" }); + + const value = await qb.getCount(); + return { + meta: { + providerName: this.name(), + generatedAt: new Date().toISOString(), + widgetName: ctxt.widgetName, + durationMs: 0, + }, + data: { + value, + label: widgetDefinition?.name ?? "Succeeded", + }, + }; + } +} + diff --git a/src/services/dashboard-providers/mq-dashboard-success-rate-kpi-provider.service.ts b/src/services/dashboard-providers/mq-dashboard-success-rate-kpi-provider.service.ts new file mode 100644 index 00000000..c6206034 --- /dev/null +++ b/src/services/dashboard-providers/mq-dashboard-success-rate-kpi-provider.service.ts @@ -0,0 +1,55 @@ +import { Injectable } from "@nestjs/common"; +import { DashboardWidgetDataProvider } from "src/decorators/dashboard-widget-data-provider.decorator"; +import { MqMessageRepository } from "src/repository/mq-message.repository"; +import { IDashboardWidgetDataProvider, IDashboardWidgetDataProviderContext, IDashboardWidgetDataResponseEnvelope } from "src/interfaces"; +import { applyMqDashboardFilters } from "src/services/dashboard-providers/mq-dashboard-provider-utils"; + +@DashboardWidgetDataProvider() +@Injectable() +export class MqDashboardSuccessRateKpiProvider implements IDashboardWidgetDataProvider { + constructor( + private readonly mqMessageRepository: MqMessageRepository, + ) { } + + name(): string { + return "MqDashboardSuccessRateKpiProvider"; + } + + help(): string { + return "Returns success rate percentage after applying dashboard variables."; + } + + async getData( + widgetDefinition: Record, + ctxt: IDashboardWidgetDataProviderContext, + ): Promise> { + const totalQb = await this.mqMessageRepository.createSecurityRuleAwareQueryBuilder("mqMessage"); + applyMqDashboardFilters(totalQb, ctxt.variables ?? {}, { ignoreStage: true }); + + const successQb = await this.mqMessageRepository.createSecurityRuleAwareQueryBuilder("mqMessage"); + applyMqDashboardFilters(successQb, ctxt.variables ?? {}, { ignoreStage: true }); + successQb.andWhere("mqMessage.stage = :stage", { stage: "succeeded" }); + + const [total, succeeded] = await Promise.all([totalQb.getCount(), successQb.getCount()]); + const value = total > 0 ? Number(((succeeded / total) * 100).toFixed(2)) : 0; + + return { + meta: { + providerName: this.name(), + generatedAt: new Date().toISOString(), + widgetName: ctxt.widgetName, + durationMs: 0, + }, + data: { + value, + label: widgetDefinition?.name ?? "Success Rate", + numerator: succeeded, + denominator: total, + }, + uiHints: { + suffix: "%", + }, + }; + } +} + diff --git a/src/services/dashboard-providers/mq-dashboard-total-messages-kpi-provider.service.ts b/src/services/dashboard-providers/mq-dashboard-total-messages-kpi-provider.service.ts new file mode 100644 index 00000000..e986e0d0 --- /dev/null +++ b/src/services/dashboard-providers/mq-dashboard-total-messages-kpi-provider.service.ts @@ -0,0 +1,44 @@ +import { Injectable } from "@nestjs/common"; +import { DashboardWidgetDataProvider } from "src/decorators/dashboard-widget-data-provider.decorator"; +import { MqMessageRepository } from "src/repository/mq-message.repository"; +import { IDashboardWidgetDataProvider, IDashboardWidgetDataProviderContext, IDashboardWidgetDataResponseEnvelope } from "src/interfaces"; +import { applyMqDashboardFilters } from "src/services/dashboard-providers/mq-dashboard-provider-utils"; + +@DashboardWidgetDataProvider() +@Injectable() +export class MqDashboardTotalMessagesKpiProvider implements IDashboardWidgetDataProvider { + constructor( + private readonly mqMessageRepository: MqMessageRepository, + ) { } + + name(): string { + return "MqDashboardTotalMessagesKpiProvider"; + } + + help(): string { + return "Returns total mq message count after applying dashboard variables."; + } + + async getData( + widgetDefinition: Record, + ctxt: IDashboardWidgetDataProviderContext, + ): Promise> { + const qb = await this.mqMessageRepository.createSecurityRuleAwareQueryBuilder("mqMessage"); + applyMqDashboardFilters(qb, ctxt.variables ?? {}); + + const value = await qb.getCount(); + return { + meta: { + providerName: this.name(), + generatedAt: new Date().toISOString(), + widgetName: ctxt.widgetName, + durationMs: 0, + }, + data: { + value, + label: widgetDefinition?.name ?? "Total Messages", + }, + }; + } +} + diff --git a/src/services/dashboard-runtime.service.ts b/src/services/dashboard-runtime.service.ts new file mode 100644 index 00000000..33c689b2 --- /dev/null +++ b/src/services/dashboard-runtime.service.ts @@ -0,0 +1,369 @@ +import { Injectable, Logger, NotFoundException } from '@nestjs/common'; +import qs from 'qs'; +import { ERROR_MESSAGES } from 'src/constants/error-messages'; +import { DashboardVariableOptionsQueryDto } from 'src/dtos/dashboard-variable-options-query.dto'; +import { DashboardBatchDataRequestDto, DashboardSaveLayoutDto, DashboardWidgetDataRequestDto } from 'src/dtos/dashboard-widget-data-request.dto'; +import { ModuleMetadata } from 'src/entities/module-metadata.entity'; +import { ModuleMetadataHelperService } from 'src/helpers/module-metadata-helper.service'; +import { SolidRegistry } from 'src/helpers/solid-registry'; +import { ActiveUserData } from 'src/interfaces/active-user-data.interface'; +import { IDashboardWidgetDataProviderContext } from 'src/interfaces'; +import { DashboardUserLayoutService } from './dashboard-user-layout.service'; + +@Injectable() +export class DashboardRuntimeService { + private readonly logger = new Logger(DashboardRuntimeService.name); + + constructor( + private readonly moduleMetadataHelperService: ModuleMetadataHelperService, + private readonly solidRegistry: SolidRegistry, + private readonly dashboardUserLayoutService: DashboardUserLayoutService, + ) { } + + async getDashboardDefinition(moduleName: string, dashboardName: string): Promise { + const moduleMetadata = await this.getModuleMetadata(moduleName); + const dashboards = this.getDashboards(moduleMetadata); + const dashboard = dashboards.find((d) => d?.name === dashboardName || d?.routeName === dashboardName); + + if (!dashboard) { + throw new NotFoundException(`Dashboard ${dashboardName} not found for module ${moduleName}.`); + } + + return dashboard; + } + + async getWidgetData( + moduleName: string, + dashboardName: string, + widgetName: string, + request: DashboardWidgetDataRequestDto = {}, + activeUser?: ActiveUserData, + ): Promise { + const dashboardDefinition = await this.getDashboardDefinition(moduleName, dashboardName); + return this.getWidgetDataFromDefinition(dashboardDefinition, moduleName, dashboardName, widgetName, request, activeUser); + } + + async getDashboardData( + moduleName: string, + dashboardName: string, + request: DashboardBatchDataRequestDto = {}, + activeUser?: ActiveUserData, + ): Promise { + const dashboardDefinition = await this.getDashboardDefinition(moduleName, dashboardName); + const allWidgets = Array.isArray(dashboardDefinition.widgets) ? dashboardDefinition.widgets : []; + + const targetWidgets = request.widgetNames?.length + ? allWidgets.filter((widget) => request.widgetNames.includes(widget.id) || request.widgetNames.includes(widget.name)) + : allWidgets; + + const widgets = await Promise.all( + targetWidgets.map((widget) => this.getWidgetDataFromDefinition( + dashboardDefinition, + moduleName, + dashboardName, + widget.id ?? widget.name, + request, + activeUser + )) + ); + + return { + moduleName, + dashboardName: dashboardDefinition.name, + variables: request.variables ?? {}, + widgets, + }; + } + + async getVariableOptions( + moduleName: string, + dashboardName: string, + variableName: string, + query: DashboardVariableOptionsQueryDto, + ): Promise { + const dashboardDefinition = await this.getDashboardDefinition(moduleName, dashboardName); + const variable = (dashboardDefinition.variables ?? []).find((v) => v.name === variableName); + + if (!variable) { + throw new NotFoundException(`Variable ${variableName} not found in dashboard ${dashboardName}.`); + } + + if (variable.type === 'selectionStatic') { + const values = Array.isArray(variable.selectionStaticValues) ? variable.selectionStaticValues : []; + return values.map((entry: string) => { + const [value, label] = `${entry}`.split(':'); + return { label: label ?? value, value }; + }); + } + + if (variable.type !== 'selectionDynamic') { + return []; + } + + const providerName = variable?.selectionConfig?.providerName; + if (!providerName) { + throw new NotFoundException(`Variable ${variableName} does not define a dynamic selection provider.`); + } + + const selectionProvider = this.solidRegistry.getSelectionProviderInstance(providerName); + if (!selectionProvider) { + throw new NotFoundException(ERROR_MESSAGES.PROVIDER_NOT_FOUND(providerName)); + } + + const providerContext = { + ...(variable?.selectionConfig?.providerContext ?? {}), + limit: query?.limit ?? 10, + offset: query?.offset ?? 0, + formValues: this.parseFormValues(query?.formValues), + }; + + if (query.optionValue) { + return selectionProvider.value(query.optionValue, providerContext as any); + } + + return selectionProvider.values(query?.query ?? '', providerContext as any); + } + + async getLayout(moduleName: string, dashboardName: string, activeUser?: ActiveUserData): Promise { + const dashboardDefinition = await this.getDashboardDefinition(moduleName, dashboardName); + const defaultLayout = dashboardDefinition.defaultLayout ?? {}; + const userId = activeUser?.sub ?? null; + let userLayoutRecord = null; + try { + userLayoutRecord = userId + ? await this.dashboardUserLayoutService.repo.findOne({ + where: { + user: { id: userId }, + module: { name: moduleName }, + dashboardName: dashboardDefinition.name, + } as any, + }) + : null; + } catch (err: any) { + const isEntityMetadataMissing = `${err?.message ?? ''}`.includes('No metadata for "DashboardUserLayout" was found'); + if (!isEntityMetadataMissing) throw err; + this.logger.warn('DashboardUserLayout entity metadata is not registered yet. Returning default layout fallback.'); + } + const userLayout = this.parseLayoutJson(userLayoutRecord?.layoutJson); + + return { + moduleName, + dashboardName: dashboardDefinition.name, + userId, + defaultLayout, + userLayout, + effectiveLayout: userLayout ?? defaultLayout, + persisted: !!userLayoutRecord, + }; + } + + async saveLayout( + moduleName: string, + dashboardName: string, + dto: DashboardSaveLayoutDto, + activeUser?: ActiveUserData, + ): Promise { + const dashboardDefinition = await this.getDashboardDefinition(moduleName, dashboardName); + const resolvedDashboardName = dashboardDefinition?.name ?? dashboardName; + const resolvedLayoutJson = this.resolveLayoutPayload(dto); + const userId = activeUser?.sub; + if (!userId) { + return { + moduleName, + dashboardName: resolvedDashboardName, + userId: null, + persisted: false, + message: 'Unable to persist layout without active user context.', + layoutJson: resolvedLayoutJson, + }; + } + const layoutJsonAsString = typeof resolvedLayoutJson === 'string' + ? resolvedLayoutJson + : JSON.stringify(resolvedLayoutJson ?? {}); + let savedLayout: any = null; + try { + const layoutRepo = this.dashboardUserLayoutService.repo; + const existing = await layoutRepo.findOne({ + where: { + user: { id: userId }, + module: { name: moduleName }, + dashboardName: resolvedDashboardName, + } as any, + }); + + const moduleRepo = this.dashboardUserLayoutService.entityManager.getRepository(ModuleMetadata); + const moduleEntity = await moduleRepo.findOne({ where: { name: moduleName } as any }); + + if (!moduleEntity) { + throw new NotFoundException(`Module ${moduleName} not found.`); + } + + const toSave = layoutRepo.create({ + ...(existing ? { id: existing.id } : {}), + user: { id: userId } as any, + module: { id: moduleEntity.id } as any, + dashboardName: resolvedDashboardName, + layoutJson: layoutJsonAsString, + version: (existing?.version ?? 0) + 1, + } as any); + + savedLayout = await layoutRepo.save(toSave); + } catch (err: any) { + const isEntityMetadataMissing = `${err?.message ?? ''}`.includes('No metadata for "DashboardUserLayout" was found'); + if (!isEntityMetadataMissing) throw err; + this.logger.warn('DashboardUserLayout entity metadata is not registered yet. Returning non-persistent save fallback.'); + return { + moduleName, + dashboardName: resolvedDashboardName, + userId, + persisted: false, + message: 'Layout model generated but entity is not registered in TypeORM yet. Please add DashboardUserLayout to module TypeOrm entities and restart.', + layoutJson: resolvedLayoutJson, + }; + } + + return { + moduleName, + dashboardName: resolvedDashboardName, + userId, + persisted: true, + message: 'Layout saved successfully.', + layoutJson: this.parseLayoutJson(savedLayout.layoutJson), + }; + } + + private async getModuleMetadata(moduleName: string): Promise { + const configPath = await this.moduleMetadataHelperService.getModuleMetadataFilePath(moduleName); + const moduleMetadata = await this.moduleMetadataHelperService.getModuleMetadataConfiguration(configPath); + + if (!moduleMetadata) { + throw new NotFoundException(ERROR_MESSAGES.MODULE_NOT_FOUND(moduleName)); + } + + return moduleMetadata; + } + + private getDashboards(moduleMetadata: any): any[] { + return Array.isArray(moduleMetadata?.dashboards) ? moduleMetadata.dashboards : []; + } + + private findWidgetDefinition(dashboardDefinition: any, widgetName: string): any { + const widgets = Array.isArray(dashboardDefinition.widgets) ? dashboardDefinition.widgets : []; + const widgetDefinition = widgets.find((widget) => widget?.id === widgetName || widget?.name === widgetName); + + if (!widgetDefinition) { + throw new NotFoundException(`Widget ${widgetName} not found in dashboard ${dashboardDefinition?.name ?? ''}.`); + } + + return widgetDefinition; + } + + private async getWidgetDataFromDefinition( + dashboardDefinition: any, + moduleName: string, + dashboardName: string, + widgetName: string, + request: DashboardWidgetDataRequestDto = {}, + activeUser?: ActiveUserData, + ): Promise { + const widgetDefinition = this.findWidgetDefinition(dashboardDefinition, widgetName); + const providerName = widgetDefinition.dataProvider; + + if (!providerName) { + throw new NotFoundException(`Widget ${widgetName} is missing dataProvider definition.`); + } + + const provider = this.solidRegistry.getDashboardWidgetDataProviderInstance(providerName); + if (!provider) { + throw new NotFoundException(ERROR_MESSAGES.PROVIDER_NOT_FOUND(providerName)); + } + + const providerContext = { + ...(widgetDefinition.providerContext ?? {}), + ...(request.providerContext ?? {}), + }; + + const runtimeContext: IDashboardWidgetDataProviderContext = { + moduleName, + dashboardName, + widgetName: widgetDefinition.id ?? widgetDefinition.name ?? widgetName, + variables: request.variables ?? {}, + providerContext, + activeUser, + }; + + const startTime = Date.now(); + const providerResponse = await provider.getData(widgetDefinition, runtimeContext); + const durationMs = Date.now() - startTime; + + if (providerResponse && typeof providerResponse === 'object' && providerResponse.data !== undefined) { + const nextMeta = { + ...(providerResponse.meta ?? {}), + providerName, + widgetName: runtimeContext.widgetName, + durationMs: providerResponse?.meta?.durationMs ?? durationMs, + generatedAt: providerResponse?.meta?.generatedAt ?? new Date().toISOString(), + }; + + return { + ...providerResponse, + meta: nextMeta, + }; + } + + this.logger.debug(`Widget provider ${providerName} returned a non-envelope payload for ${runtimeContext.widgetName}. Normalizing response.`); + return { + meta: { + providerName, + widgetName: runtimeContext.widgetName, + durationMs, + generatedAt: new Date().toISOString(), + }, + data: providerResponse, + }; + } + + private parseFormValues(formValues?: Record | string): Record { + if (!formValues) { + return {}; + } + if (typeof formValues === 'object') { + return formValues; + } + + try { + const parsedFromQueryString = qs.parse(formValues); + if (parsedFromQueryString && typeof parsedFromQueryString === 'object') { + if (parsedFromQueryString['formValues'] && typeof parsedFromQueryString['formValues'] === 'object') { + return parsedFromQueryString['formValues'] as Record; + } + return parsedFromQueryString as Record; + } + } catch (err) { + // ignore parse errors and try JSON + } + + try { + return JSON.parse(formValues); + } catch (err) { + return {}; + } + } + + private parseLayoutJson(layoutJson: any): any { + if (layoutJson === null || layoutJson === undefined) return null; + if (typeof layoutJson !== 'string') return layoutJson; + try { + return JSON.parse(layoutJson); + } catch { + return layoutJson; + } + } + + private resolveLayoutPayload(dto: DashboardSaveLayoutDto | Record | null | undefined): any { + if (!dto) return {}; + if (dto.layoutJson !== undefined) return dto.layoutJson; + if ((dto as any).layout !== undefined) return (dto as any).layout; + return dto; + } +} diff --git a/src/services/dashboard-user-layout.service.ts b/src/services/dashboard-user-layout.service.ts new file mode 100644 index 00000000..4744eda3 --- /dev/null +++ b/src/services/dashboard-user-layout.service.ts @@ -0,0 +1,20 @@ +import { Injectable } from '@nestjs/common'; +import { InjectEntityManager } from '@nestjs/typeorm'; +import { ModuleRef } from "@nestjs/core"; +import { EntityManager } from 'typeorm'; +import { CRUDService } from 'src/services/crud.service'; +import { DashboardUserLayout } from '../entities/dashboard-user-layout.entity'; +import { DashboardUserLayoutRepository } from '../repositories/dashboard-user-layout.repository'; + +@Injectable() +export class DashboardUserLayoutService extends CRUDService{ + constructor( + @InjectEntityManager("default") + readonly entityManager: EntityManager, + readonly repo: DashboardUserLayoutRepository, + readonly moduleRef: ModuleRef, + + ) { + super(entityManager, repo, 'dashboardUserLayout', 'solid-core', moduleRef); + } +} \ No newline at end of file diff --git a/src/services/selection-providers/mq-dashboard-message-broker-variable-options-provider.service.ts b/src/services/selection-providers/mq-dashboard-message-broker-variable-options-provider.service.ts new file mode 100644 index 00000000..60814e84 --- /dev/null +++ b/src/services/selection-providers/mq-dashboard-message-broker-variable-options-provider.service.ts @@ -0,0 +1,52 @@ +import { Injectable } from "@nestjs/common"; +import { SelectionProvider } from "src/decorators/selection-provider.decorator"; +import { MqMessageRepository } from "src/repository/mq-message.repository"; +import { ISelectionProvider, ISelectionProviderContext, ISelectionProviderValues } from "src/interfaces"; + +@SelectionProvider() +@Injectable() +export class MqDashboardMessageBrokerVariableOptionsProvider implements ISelectionProvider { + constructor( + private readonly mqMessageRepository: MqMessageRepository, + ) { } + + name(): string { + return "MqDashboardMessageBrokerVariableOptionsProvider"; + } + + help(): string { + return "Dynamic options provider for dashboard messageBroker variable."; + } + + async value(optionValue: string): Promise { + if (!optionValue) return null; + return { label: optionValue, value: optionValue }; + } + + async values(query: string, ctxt: ISelectionProviderContext): Promise { + const qb = await this.mqMessageRepository.createSecurityRuleAwareQueryBuilder("mqMessage"); + const limit = Math.min(Math.max(ctxt?.limit ?? 25, 1), 200); + const offset = Math.max(ctxt?.offset ?? 0, 0); + + qb.select("DISTINCT mqMessage.messageBroker", "value") + .where("mqMessage.messageBroker IS NOT NULL"); + + if (query) { + qb.andWhere("mqMessage.messageBroker ILIKE :query", { query: `%${query}%` }); + } + + const rows = await qb + .orderBy("value", "ASC") + .take(limit) + .skip(offset) + .getRawMany<{ value: string }>(); + + return rows + .filter((r) => !!r?.value) + .map((r) => ({ + label: r.value, + value: r.value, + })); + } +} + diff --git a/src/services/selection-providers/mq-dashboard-queue-name-variable-options-provider.service.ts b/src/services/selection-providers/mq-dashboard-queue-name-variable-options-provider.service.ts new file mode 100644 index 00000000..dc5cbf08 --- /dev/null +++ b/src/services/selection-providers/mq-dashboard-queue-name-variable-options-provider.service.ts @@ -0,0 +1,51 @@ +import { Injectable } from "@nestjs/common"; +import { SelectionProvider } from "src/decorators/selection-provider.decorator"; +import { MqMessageQueueRepository } from "src/repository/mq-message-queue.repository"; +import { ISelectionProvider, ISelectionProviderContext, ISelectionProviderValues } from "src/interfaces"; + +@SelectionProvider() +@Injectable() +export class MqDashboardQueueNameVariableOptionsProvider implements ISelectionProvider { + constructor( + private readonly mqMessageQueueRepository: MqMessageQueueRepository, + ) { } + + name(): string { + return "MqDashboardQueueNameVariableOptionsProvider"; + } + + help(): string { + return "Dynamic options provider for dashboard queueName variable."; + } + + async value(optionValue: string): Promise { + if (!optionValue) return null; + const queue = await this.mqMessageQueueRepository.findOne({ + where: { name: optionValue }, + }); + if (!queue?.name) return null; + return { label: queue.name, value: queue.name }; + } + + async values(query: string, ctxt: ISelectionProviderContext): Promise { + const qb = await this.mqMessageQueueRepository.createSecurityRuleAwareQueryBuilder("mqMessageQueue"); + const limit = Math.min(Math.max(ctxt?.limit ?? 25, 1), 200); + const offset = Math.max(ctxt?.offset ?? 0, 0); + + if (query) { + qb.andWhere("mqMessageQueue.name ILIKE :query", { query: `%${query}%` }); + } + + const records = await qb + .select(["mqMessageQueue.name"]) + .orderBy("mqMessageQueue.name", "ASC") + .take(limit) + .skip(offset) + .getMany(); + + return records + .filter((r) => !!r?.name) + .map((r) => ({ label: r.name, value: r.name })); + } +} + diff --git a/src/services/solid-introspect.service.ts b/src/services/solid-introspect.service.ts index 4cfd4279..c0f67aa7 100755 --- a/src/services/solid-introspect.service.ts +++ b/src/services/solid-introspect.service.ts @@ -11,6 +11,7 @@ import { IS_MAIL_PROVIDER } from 'src/decorators/mail-provider.decorator'; import { IS_SCHEDULED_JOB_PROVIDER } from 'src/decorators/scheduled-job-provider.decorator'; import { IS_SECURITY_RULE_CONFIG_PROVIDER } from 'src/decorators/security-rule-config-provider.decorator'; import { IS_SELECTION_PROVIDER } from 'src/decorators/selection-provider.decorator'; +import { IS_DASHBOARD_WIDGET_DATA_PROVIDER } from 'src/decorators/dashboard-widget-data-provider.decorator'; import { IS_EXTENSION_USER_CREATION_PROVIDER } from 'src/decorators/extension-user-creation-provider.decorator'; import { IS_SOLID_DATABASE_MODULE } from 'src/decorators/solid-database-module.decorator'; import { IS_WA_PROVIDER } from 'src/decorators/whatsapp-provider.decorator'; @@ -68,6 +69,12 @@ export class SolidIntrospectService implements OnApplicationBootstrap { this.solidRegistry.registerSelectionProvider(selectionProvider); }); + // Register all IDashboardWidgetDataProvider implementations + const dashboardWidgetDataProviders = this.discoveryService.getProviders().filter((provider) => this.isDashboardWidgetDataProvider(provider)); + dashboardWidgetDataProviders.forEach((provider) => { + this.solidRegistry.registerDashboardWidgetDataProvider(provider); + }); + // Register all ISettingsProvider implementations const settingsProviders = this.discoveryService.getProviders().filter((provider) => this.isSettingsProvider(provider)); settingsProviders.forEach((settingsProvider) => { @@ -270,6 +277,18 @@ export class SolidIntrospectService implements OnApplicationBootstrap { return !!isSelectionProvider; } + private isDashboardWidgetDataProvider(provider: InstanceWrapper) { + const { instance } = provider; + if (!instance) return false; + + const isDashboardWidgetDataProvider = this.reflector.get( + IS_DASHBOARD_WIDGET_DATA_PROVIDER, + instance.constructor, + ); + + return !!isDashboardWidgetDataProvider; + } + private isExtensionUserCreationProvider(provider: InstanceWrapper): boolean { const { instance } = provider; if (!instance) return false; diff --git a/src/solid-core.module.ts b/src/solid-core.module.ts index 893cd4b9..ed7204d9 100644 --- a/src/solid-core.module.ts +++ b/src/solid-core.module.ts @@ -14,6 +14,7 @@ import { MulterModule } from "@nestjs/platform-express"; import { TypeOrmModule } from "@nestjs/typeorm"; import { RemoveFieldsCommand } from "./commands/remove-fields.command"; import { FieldMetadataController } from "./controllers/field-metadata.controller"; +import { DashboardController } from "./controllers/dashboard.controller"; import { MediaStorageProviderMetadataController } from "./controllers/media-storage-provider-metadata.controller"; import { ModelMetadataController } from "./controllers/model-metadata.controller"; import { ModuleMetadataController } from "./controllers/module-metadata.controller"; @@ -27,11 +28,14 @@ import { ModuleMetadata } from "./entities/module-metadata.entity"; import { CommandService } from "./helpers/command.service"; import { SchematicService } from "./helpers/schematic.service"; import { ListOfValuesSelectionProvider } from "./services/selection-providers/list-of-values-selection-providers.service"; +import { MqDashboardMessageBrokerVariableOptionsProvider } from "./services/selection-providers/mq-dashboard-message-broker-variable-options-provider.service"; +import { MqDashboardQueueNameVariableOptionsProvider } from "./services/selection-providers/mq-dashboard-queue-name-variable-options-provider.service"; import { PseudoForeignKeySelectionProvider } from "./services/selection-providers/pseudo-foreign-key-selection-provider.service"; import { ModuleMetadataSeederService } from "./seeders/module-metadata-seeder.service"; import { ModuleTestDataService } from "./seeders/module-test-data.service"; import { CrudHelperService } from "./services/crud-helper.service"; import { FieldMetadataService } from "./services/field-metadata.service"; +import { DashboardRuntimeService } from "./services/dashboard-runtime.service"; import { ListOfValuesService } from "./services/list-of-values.service"; // import { MediaStorageProviderMetadataSeederService } from './services/media-storage-provider-metadata-seeder.service'; import { MediaStorageProviderMetadataService } from "./services/media-storage-provider-metadata.service"; @@ -303,6 +307,18 @@ import { ChatterMessageService } from './services/chatter-message.service'; import { ConcatComputedFieldProvider } from './services/computed-fields/concat-computed-field-provider.service'; import { AlphaNumExternalIdComputationProvider } from './services/computed-fields/entity/alpha-num-external-id-computed-field-provider'; import { ConcatEntityComputedFieldProvider } from './services/computed-fields/entity/concat-entity-computed-field-provider.service'; +import { MqDashboardFailedMessagesKpiProvider } from './services/dashboard-providers/mq-dashboard-failed-messages-kpi-provider.service'; +import { MqDashboardInflightMessagesKpiProvider } from './services/dashboard-providers/mq-dashboard-inflight-messages-kpi-provider.service'; +import { MqDashboardLatencyTrendProvider } from './services/dashboard-providers/mq-dashboard-latency-trend-provider.service'; +import { MqDashboardMessagesOverTimeProvider } from './services/dashboard-providers/mq-dashboard-messages-over-time-provider.service'; +import { MqDashboardQueueWiseAvgElapsedProvider } from './services/dashboard-providers/mq-dashboard-queue-wise-avg-elapsed-provider.service'; +import { MqDashboardQueueWiseFailuresProvider } from './services/dashboard-providers/mq-dashboard-queue-wise-failures-provider.service'; +import { MqDashboardRecentFailuresProvider } from './services/dashboard-providers/mq-dashboard-recent-failures-provider.service'; +import { MqDashboardStageDistributionProvider } from './services/dashboard-providers/mq-dashboard-stage-distribution-provider.service'; +import { MqDashboardSucceededMessagesKpiProvider } from './services/dashboard-providers/mq-dashboard-succeeded-messages-kpi-provider.service'; +import { MqDashboardSuccessRateKpiProvider } from './services/dashboard-providers/mq-dashboard-success-rate-kpi-provider.service'; +import { MqDashboardTotalMessagesKpiProvider } from './services/dashboard-providers/mq-dashboard-total-messages-kpi-provider.service'; +import { MqDashboardAvgElapsedKpiProvider } from './services/dashboard-providers/mq-dashboard-avg-elapsed-kpi-provider.service'; import { NoopsEntityComputedFieldProviderService } from './services/computed-fields/entity/noops-entity-computed-field-provider.service'; import { CRUDService } from './services/crud.service'; import { CsvService } from './services/csv.service'; @@ -367,6 +383,10 @@ import { SolidMicroserviceAdapter } from './helpers/solid-microservice-adapter.s import { InfoCommand } from './commands/info.command'; import { ListOfRolesSelectionProvider } from './services/selection-providers/list-of-roles-selectionproviders.service'; import { Entity } from 'typeorm'; +import { DashboardUserLayout } from './entities/dashboard-user-layout.entity'; +import { DashboardUserLayoutService } from './services/dashboard-user-layout.service'; +import { DashboardUserLayoutController } from './controllers/dashboard-user-layout.controller'; +import { DashboardUserLayoutRepository } from './repositories/dashboard-user-layout.repository'; @Global() @Module({ @@ -440,6 +460,7 @@ import { Entity } from 'typeorm'; JwtModule.register({ global: true, }), + TypeOrmModule.forFeature([DashboardUserLayout]), ], controllers: [ ActionMetadataController, @@ -451,6 +472,7 @@ import { Entity } from 'typeorm'; ExportTemplateController, ExportTransactionController, FieldMetadataController, + DashboardController, GoogleAuthenticationController, FacebookAuthenticationController, MicrosoftAuthenticationController, @@ -487,6 +509,7 @@ import { Entity } from 'typeorm'; UserViewMetadataController, ViewMetadataController, ModelSequenceController, + DashboardUserLayoutController, ], providers: [ { @@ -514,6 +537,7 @@ import { Entity } from 'typeorm'; ModelMetadataService, ModelMetadataHelperService, FieldMetadataService, + DashboardRuntimeService, RemoveFieldsCommand, RefreshModelCommand, RefreshModuleCommand, @@ -535,6 +559,8 @@ import { Entity } from 'typeorm'; ModuleTestDataService, ListOfValuesService, ListOfValuesSelectionProvider, + MqDashboardQueueNameVariableOptionsProvider, + MqDashboardMessageBrokerVariableOptionsProvider, PseudoForeignKeySelectionProvider, ModelMetadataSubscriber, ViewMetadataService, @@ -679,6 +705,18 @@ import { Entity } from 'typeorm'; UserRepository, SettingService, ConcatComputedFieldProvider, + MqDashboardTotalMessagesKpiProvider, + MqDashboardSucceededMessagesKpiProvider, + MqDashboardFailedMessagesKpiProvider, + MqDashboardInflightMessagesKpiProvider, + MqDashboardSuccessRateKpiProvider, + MqDashboardAvgElapsedKpiProvider, + MqDashboardMessagesOverTimeProvider, + MqDashboardStageDistributionProvider, + MqDashboardQueueWiseFailuresProvider, + MqDashboardQueueWiseAvgElapsedProvider, + MqDashboardLatencyTrendProvider, + MqDashboardRecentFailuresProvider, FileStorageProvider, FileS3StorageProvider, MediaRepository, @@ -698,6 +736,7 @@ import { Entity } from 'typeorm'; ExportTransactionService, ExcelService, CsvService, + DashboardRuntimeService, ImportTransactionService, ImportTransactionErrorLogService, CreatedByUpdatedBySubscriber, @@ -767,6 +806,8 @@ import { Entity } from 'typeorm'; ImageEncodingService, SolidMicroserviceAdapter, ListOfRolesSelectionProvider, + DashboardUserLayoutService, + DashboardUserLayoutRepository, ], exports: [ AiInteractionService, From 494bc67a8dd6d45c38e2ed672c33ef65a2049e9d Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Mon, 1 Jun 2026 17:43:47 +0100 Subject: [PATCH 075/136] changes for new dashboards functionality --- .../seed-data/solid-core-metadata.json | 97 +++++++------------ src/services/dashboard-runtime.service.ts | 51 +++++++++- 2 files changed, 85 insertions(+), 63 deletions(-) diff --git a/src/seeders/seed-data/solid-core-metadata.json b/src/seeders/seed-data/solid-core-metadata.json index 2205f2a1..7461ac33 100755 --- a/src/seeders/seed-data/solid-core-metadata.json +++ b/src/seeders/seed-data/solid-core-metadata.json @@ -6145,7 +6145,7 @@ "type": "custom", "domain": "", "context": "", - "customComponent": "/admin/dashboard/queue-health", + "customComponent": "/admin/core/solid-core/dashboard/queue-health", "customIsModal": false, "serverEndpoint": "", "viewUserKey": "", @@ -13538,10 +13538,7 @@ "name": "date", "label": "Created At", "type": "date", - "required": true, - "defaultValue": { - "preset": "last_7_days" - } + "required": true }, { "name": "queueName", @@ -13784,111 +13781,87 @@ "x": 0, "y": 0, "w": 2, - "h": 2, - "minW": 2, - "minH": 2 + "h": 2 }, { - "widgetId": "kpi-succeeded-messages", + "widgetId": "kpi-in-flight-messages", "x": 2, "y": 0, "w": 2, - "h": 2, - "minW": 2, - "minH": 2 + "h": 2 }, { - "widgetId": "kpi-failed-messages", + "widgetId": "kpi-succeeded-messages", "x": 4, "y": 0, "w": 2, - "h": 2, - "minW": 2, - "minH": 2 + "h": 2 }, { - "widgetId": "kpi-in-flight-messages", + "widgetId": "kpi-success-rate", "x": 6, "y": 0, "w": 2, - "h": 2, - "minW": 2, - "minH": 2 + "h": 2 }, { - "widgetId": "kpi-success-rate", + "widgetId": "kpi-failed-messages", "x": 8, "y": 0, "w": 2, - "h": 2, - "minW": 2, - "minH": 2 + "h": 2 }, { "widgetId": "kpi-avg-elapsed-ms", "x": 10, "y": 0, "w": 2, - "h": 2, - "minW": 2, - "minH": 2 + "h": 2 }, { - "widgetId": "chart-messages-over-time", + "widgetId": "chart-queue-wise-failures", "x": 0, "y": 2, - "w": 8, - "h": 5, - "minW": 4, - "minH": 4 + "w": 4, + "h": 4 }, { - "widgetId": "chart-stage-distribution", - "x": 8, + "widgetId": "chart-messages-over-time", + "x": 4, "y": 2, "w": 4, - "h": 5, - "minW": 3, - "minH": 4 + "h": 4 }, { - "widgetId": "chart-queue-wise-failures", - "x": 0, - "y": 7, - "w": 6, - "h": 5, - "minW": 4, - "minH": 4 + "widgetId": "chart-processing-latency-trend", + "x": 8, + "y": 2, + "w": 4, + "h": 4 }, { "widgetId": "chart-queue-wise-avg-elapsed", - "x": 6, - "y": 7, + "x": 0, + "y": 6, "w": 6, - "h": 5, - "minW": 4, - "minH": 4 + "h": 5 }, { - "widgetId": "chart-processing-latency-trend", - "x": 0, - "y": 12, + "widgetId": "chart-stage-distribution", + "x": 6, + "y": 6, "w": 6, - "h": 6, - "minW": 4, - "minH": 4 + "h": 5 }, { "widgetId": "table-recent-failures", - "x": 6, - "y": 12, - "w": 6, - "h": 6, - "minW": 4, - "minH": 4 + "x": 0, + "y": 11, + "w": 12, + "h": 4 } ] } } ] -} \ No newline at end of file +} diff --git a/src/services/dashboard-runtime.service.ts b/src/services/dashboard-runtime.service.ts index 33c689b2..3c105d86 100644 --- a/src/services/dashboard-runtime.service.ts +++ b/src/services/dashboard-runtime.service.ts @@ -145,6 +145,7 @@ export class DashboardRuntimeService { this.logger.warn('DashboardUserLayout entity metadata is not registered yet. Returning default layout fallback.'); } const userLayout = this.parseLayoutJson(userLayoutRecord?.layoutJson); + const effectiveLayout = this.mergeLayouts(defaultLayout, userLayout); return { moduleName, @@ -152,7 +153,7 @@ export class DashboardRuntimeService { userId, defaultLayout, userLayout, - effectiveLayout: userLayout ?? defaultLayout, + effectiveLayout, persisted: !!userLayoutRecord, }; } @@ -366,4 +367,52 @@ export class DashboardRuntimeService { if ((dto as any).layout !== undefined) return (dto as any).layout; return dto; } + + private mergeLayouts(defaultLayout: any, userLayout: any): any { + if (!defaultLayout && !userLayout) return {}; + if (!defaultLayout) return userLayout ?? {}; + if (!userLayout) return defaultLayout ?? {}; + + const defaultItems = Array.isArray(defaultLayout?.items) ? defaultLayout.items : []; + const userItems = Array.isArray(userLayout?.items) ? userLayout.items : []; + + if (!defaultItems.length) { + return { + ...defaultLayout, + ...userLayout, + items: userItems, + }; + } + + const userByWidget = new Map(); + userItems.forEach((item: any) => { + const key = item?.widgetId ?? item?.id; + if (key) userByWidget.set(key, item); + }); + + const mergedDefaultItems = defaultItems.map((baseItem: any) => { + const key = baseItem?.widgetId ?? baseItem?.id; + if (!key) return baseItem; + const override = userByWidget.get(key); + return override ? { ...baseItem, ...override, widgetId: key } : baseItem; + }); + + const defaultKeys = new Set( + defaultItems + .map((item: any) => item?.widgetId ?? item?.id) + .filter((value: any) => !!value) + ); + + const userOnlyItems = userItems.filter((item: any) => { + const key = item?.widgetId ?? item?.id; + return !!key && !defaultKeys.has(key); + }); + + return { + ...defaultLayout, + ...userLayout, + columns: userLayout?.columns ?? defaultLayout?.columns ?? 12, + items: [...mergedDefaultItems, ...userOnlyItems], + }; + } } From 8155548e167b8e322966826619753ae564bf23df Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Mon, 1 Jun 2026 17:47:05 +0100 Subject: [PATCH 076/136] feat(dashboard): update queue health menu item and reorder messages --- .../seed-data/solid-core-metadata.json | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/seeders/seed-data/solid-core-metadata.json b/src/seeders/seed-data/solid-core-metadata.json index 7461ac33..2bb5131b 100755 --- a/src/seeders/seed-data/solid-core-metadata.json +++ b/src/seeders/seed-data/solid-core-metadata.json @@ -6338,10 +6338,10 @@ "iconName": "low_priority" }, { - "displayName": "Messages", - "name": "mqMessage-menu-item", + "displayName": "Queue Health", + "name": "solid-core-queue-health-dashboard-menu-item", "sequenceNumber": 1, - "actionUserKey": "mqMessage-list-action", + "actionUserKey": "solid-core-queue-health-dashboard-view", "moduleUserKey": "solid-core", "parentMenuItemUserKey": "queues-menu-item" }, @@ -6353,6 +6353,14 @@ "moduleUserKey": "solid-core", "parentMenuItemUserKey": "queues-menu-item" }, + { + "displayName": "Messages", + "name": "mqMessage-menu-item", + "sequenceNumber": 3, + "actionUserKey": "mqMessage-list-action", + "moduleUserKey": "solid-core", + "parentMenuItemUserKey": "queues-menu-item" + }, { "displayName": "Notification", "name": "notification-menu-item", @@ -6509,14 +6517,6 @@ "moduleUserKey": "solid-core", "parentMenuItemUserKey": "agent-menu-item" }, - { - "displayName": "Queue Health", - "name": "solid-core-queue-health-dashboard-menu-item", - "sequenceNumber": 3, - "actionUserKey": "solid-core-queue-health-dashboard-view", - "moduleUserKey": "solid-core", - "parentMenuItemUserKey": "queues-menu-item" - }, { "displayName": "Dashboard User Layout", "name": "dashboardUserLayout-menu-item", From 56d43e86a881a147598bf4f5f7e04e9dbd85d981 Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Mon, 1 Jun 2026 18:00:32 +0100 Subject: [PATCH 077/136] refactor: remove legacy dashboard files and add new dashboarding architecture guide --- AGENTIC_DASHBOARD_IMPLEMENTATION_PLAN.md | 412 ---------------- dashboard-curl-smoke-tests.txt | 146 ------ delete-legacy-dashboard-metadata.sql | 158 ------ src/services/dashboard-providers/README.md | 543 +++++++++++++++++++++ 4 files changed, 543 insertions(+), 716 deletions(-) delete mode 100644 AGENTIC_DASHBOARD_IMPLEMENTATION_PLAN.md delete mode 100644 dashboard-curl-smoke-tests.txt delete mode 100644 delete-legacy-dashboard-metadata.sql create mode 100644 src/services/dashboard-providers/README.md diff --git a/AGENTIC_DASHBOARD_IMPLEMENTATION_PLAN.md b/AGENTIC_DASHBOARD_IMPLEMENTATION_PLAN.md deleted file mode 100644 index 04a3543f..00000000 --- a/AGENTIC_DASHBOARD_IMPLEMENTATION_PLAN.md +++ /dev/null @@ -1,412 +0,0 @@ -# Agentic Dashboard Implementation Plan - -## 1. Scope and Goals - -### 1.1 Outcome -- [ ] Build a new generic, metadata-driven dashboard framework across `solid-core-module` and `solid-core-ui`. -- [ ] Replace all legacy dashboard implementation code (backend + frontend) with the new architecture. -- [ ] Support existing configured data sources and optional model metadata integration. -- [ ] Provide a clean extension architecture so teams/agents can add new dashboard widgets consistently. - -### 1.2 Core Principles -- [ ] Metadata first: dashboards, variables, widgets, filters, and layout defaults are defined in module metadata JSON. -- [ ] Provider driven: each widget resolves data through a common backend provider contract. -- [ ] UI abstraction: widget rendering uses a typed extension registry entry (`DashboardWidget`) instead of hard-coded branches. -- [ ] User personalization: per-user layout is persisted and merged over metadata defaults. - ---- - -## 2. Reference Patterns to Mirror - -### 2.1 Backend provider registration and resolution pattern -- [x] Follow existing pattern used by: - - `src/services/computed-fields/entity/sequence-num-computed-field-provider.ts` - - `src/services/selection-providers/list-of-values-selection-providers.service.ts` - - `src/services/solid-introspect.service.ts` - - `src/helpers/solid-registry.ts` -- [x] Keep flow: `decorator -> introspection -> registry -> resolver service -> controller endpoint`. - -### 2.2 Frontend extension pattern -- [ ] Mirror existing extension architecture in: - - `solid-core-ui/src/helpers/registry.ts` - - `solid-core-ui/src/types/extension-registry.ts` -- [ ] Add a dedicated extension component type for dashboard widgets. - ---- - -## 3. Legacy Dashboard Decommission (Must happen first) - -### 3.1 Backend legacy removal inventory -- [x] Remove legacy dashboard entities: - - `dashboard.entity.ts`, `dashboard-variable.entity.ts`, `dashboard-question.entity.ts`, `dashboard-question-sql-dataset-config.entity.ts`, `dashboard-layout.entity.ts`. -- [x] Remove legacy controllers/services/repositories/mappers/subscribers/decorators: - - `dashboard*.controller.ts` - - `dashboard*.service.ts` - - `dashboard*.repository.ts` - - `dashboard-mapper.ts` - - `dashboard*.subscriber.ts` - - `dashboard-selection-provider.decorator.ts` - - `dashboard-question-data-provider.decorator.ts` -- [x] Remove old dashboard-specific selection providers and question-data providers currently tied to the old model. -- [x] Remove old exports from `src/index.ts`, `src/interfaces.ts`, and any DTO references. -- [x] Remove all old dashboard wiring from `src/solid-core.module.ts` (imports, entities, providers, controllers). - -### 3.2 Frontend legacy removal inventory -- [x] Remove old dashboard API slices: - - `dashboardApi.ts`, `dashboardQuestionApi.ts`, `dashboardLayoutApi.ts`. -- [x] Remove old dashboard components and route page: - - `components/core/dashboard/*` - - `routes/pages/admin/core/DashboardPage.tsx` (legacy implementation). -- [x] Remove old dashboard extension handlers/widgets under `components/core/extension/solid-core/dashboard*`. -- [x] Remove old store wiring from `redux/store/defaultStoreConfig.ts`. -- [x] Remove stale assets only used by old dashboard implementation. - -### 3.3 Metadata cleanup -- [x] Remove old dashboard-related testing permissions (legacy controller methods) from module metadata examples. -- [x] Update seed metadata (`solid-core-metadata.json`) to remove old dashboard management models/actions/views. - ---- - -## 4. New Metadata Schema Design - -### 4.1 Module metadata structure -- [x] Introduce/replace `dashboards` section schema in module metadata (similar placement as `testing` section). -- [x] Define top-level dashboard fields: - - `name`, `displayName`, `description`, `routeName`, `menu`, `variables`, `widgets`, `defaultLayout`, `permissions`. -- [x] Define variable schema: - - `name`, `label`, `type`, `required`, `defaultValue`, `operators`, `selectionConfig`, `visibilityRules`. -- [x] Define widget schema: - - `name`, `title`, `type`, `dataProvider`, `providerContext`, `visualization`, `layoutRef`, `refreshPolicy`. -- [x] Define layout schema: - - Grid cell contract compatible with Gridstack (`x`, `y`, `w`, `h`, min/max constraints, responsive overrides). - -### 4.2 Validation + compatibility -- [ ] Add runtime validation for dashboard metadata schema (clear error messages for malformed config). -- [x] Introduce schema versioning (`dashboardSchemaVersion`) for future migrations. -- [ ] Add backward-compat guard (disable old schema loading explicitly and log actionable migration hints). - -### 4.3 ADR-001: Dashboard definition source of truth -- [x] **Decision**: - - dashboard definitions remain in module metadata JSON under root-level `dashboards` (peer of `actions`, `menus`, `roles`, `users`, etc.) - - seed only dashboard navigation metadata (`actions`, `menus`) - - do not persist dashboard definitions in dedicated dashboard DB tables - - persist only user-specific layout overrides in the new user layout table. -- [x] **Context**: - - legacy dashboard persistence model has been removed - - new dashboard framework is explicitly metadata-driven - - we need a single source of truth with git versioning and no JSON-vs-DB drift. -- [x] **Consequences**: - - simpler rollout and migrations for v1 - - dashboard updates ship through metadata changes - - runtime must always resolve dashboard definition from module metadata - - no CRUD for dashboard definitions in DB for v1. -- [x] **Alternatives considered (rejected for v1)**: - - persist full dashboard definitions in DB tables - - hybrid model where DB can override JSON definitions. - -### 4.4 ADR-002: Dashboard layout persistence implementation pattern -- [x] **Decision**: - - do not hand-code dashboard layout entity/repository/service in `solid-core-module` source - - define dashboard layout persistence model in module metadata JSON - - generate entity/service/controller artifacts through solid-core metadata code generation flow. -- [x] **Context**: - - project prefers metadata-first model management for framework-owned models - - generated artifacts keep persistence layer consistent with standard solid-core patterns. -- [x] **Consequences**: - - layout persistence endpoint behavior can be stub/fallback until generated model artifacts are available - - consuming projects can regenerate cleanly from metadata without custom table code drift. - ---- - -## 5. Backend Architecture (solid-core-module) - -### 5.1 New provider contracts -- [x] Create new interface: - - `IDashboardWidgetDataProvider` with methods: - - `name()` - - `help()` - - `getData(widgetDefinition, runtimeContext): Promise` -- [x] Optionally split provider contracts: - - widget data provider - - variable options provider (if dynamic filter options are needed separately) -- [x] Define standard response envelope for all widget providers: - - `meta` (widget name/provider/version/time) - - `data` (typed payload) - - `uiHints` (optional rendering hints) - -### 5.2 Decorator + registry + introspection -- [x] Add new decorator for widget data providers (parallel to existing provider decorators). -- [x] Extend `SolidIntrospectService` to auto-register dashboard widget data providers. -- [x] Extend `SolidRegistry` with: - - register/get/list methods for widget providers. -- [x] Keep naming resolution strategy consistent with existing providers. - -### 5.3 Dashboard runtime service layer -- [x] Add new dashboard runtime service: - - resolves dashboard metadata by module + dashboard name - - resolves dashboard variables and validated filter input - - resolves widget provider instance per widget - - executes provider and aggregates response -- [x] Add expression/filter resolver for dashboard variables (provider-level filter utility for date/queue/stage/messageBroker). -- [ ] Add secure execution guardrails: - - provider allowlist - - parameterized SQL only - - timeout + row limits + payload size limits - -### 5.4 New controller endpoints -- [x] Add `DashboardController` (new runtime controller, not CRUD dashboard model controller). -- [x] Proposed endpoints: - - `GET /dashboard/:module/:dashboardName/definition` - - `POST /dashboard/:module/:dashboardName/widgets/:widgetName/data` - - `POST /dashboard/:module/:dashboardName/data` (batch widget fetch) - - `GET /dashboard/:module/:dashboardName/variable-options/:variableName` - - `GET /dashboard/:module/:dashboardName/layout` (resolved default + user override) - - `PUT /dashboard/:module/:dashboardName/layout` (save user layout) -- [x] Add Swagger + permission mapping for new endpoints. - -### 5.5 User layout persistence model -- [x] Add metadata model for personalized layout (and generate entity/service using solid-core code generation): - - `dashboardName` (string/user key reference to metadata dashboard) - - `layoutJson` (long text / json) - - `user` (many-to-one with `User`) - - `module` (many-to-one with `Module`, resolved using `moduleUserKey` from dashboard metadata context) -- [x] Add unique index: - - `(user_id, module_id, dashboard_name)`. -- [x] Add repository/service methods and runtime integration: - - `getUserLayout()` - - `upsertUserLayout()` - - `resetToDefault()` - - wired into `GET/PUT /dashboard/:module/:dashboardName/layout`. - ---- - -## 6. Frontend Architecture (solid-core-ui) - -### 6.1 Extension system for dashboard widgets -- [ ] Add new extension component type in `extension-registry.ts`: - - `dashboardWidget`. -- [ ] Add typed widget props contract (single source of truth for all dashboard widgets), e.g.: - - widget metadata - - resolved variables - - loading/error state - - normalized provider response - - callbacks (refresh, open details, export). -- [ ] Register default widgets via `registry.ts` using the new extension type. - -### 6.2 Dashboard runtime UI -- [ ] Create new generic dashboard route/page: - - `/admin/dashboard/:dashboardName` or `/admin/core/:moduleName/dashboard/:dashboardName` (finalize one canonical route). -- [ ] Build page structure: - - dynamic header - - metadata-driven variable filter bar - - widget grid body - - save/reset layout actions. -- [ ] Resolve dashboard via new backend definition endpoint. - -### 6.3 Layout engine integration -- [ ] Standardize on Gridstack for drag/drop/resize (metadata layout to Gridstack contract adapter). -- [ ] Create layout adapter: - - metadata default layout -> Gridstack nodes - - Gridstack save format -> persisted `layoutJson`. -- [ ] Persist user changes through new layout endpoints. -- [ ] Add responsive behavior and conflict handling for missing/renamed widgets. - -### 6.4 Widget rendering pipeline -- [ ] Implement widget host container that: - - resolves widget extension component by type/name - - fetches provider data - - handles loading/empty/error states consistently - - supports per-widget refresh intervals. -- [ ] Implement first-party default widgets (KPI, line/bar/pie, table, meter/progress). -- [ ] Ensure each widget reads dashboard variables as input params. - -### 6.5 State and API slices -- [ ] Add new RTK Query slices for dashboard runtime endpoints. -- [x] Remove legacy dashboard slices from store config. -- [ ] Add cache keys by `module + dashboard + variable hash + widget`. - ---- - -## 7. Charting Library Recommendation - -### 7.1 Recommended baseline -- [ ] Use **Apache ECharts** as the default charting engine abstraction for v1. - -### 7.2 Why ECharts -- [ ] Broad chart coverage (20+ types and combinable series). -- [ ] Strong visual quality out of the box, plus deep customization. -- [ ] Apache-2.0 license (friendly for framework redistribution/use). -- [ ] Handles large datasets and supports Canvas/SVG rendering modes. - -### 7.3 UI abstraction requirement -- [ ] Do not couple widget contracts directly to ECharts option schema. -- [ ] Add a renderer adapter layer: - - `chartRenderer: "echarts"` (v1) - - future pluggable renderers without metadata breaking changes. - ---- - -## 8. Menu, Routes, and Metadata Navigation - -### 8.1 Metadata-driven menu/action integration -- [x] Add metadata authoring convention for dashboard menu/action pairs: - - action type `custom` - - route template resolved to canonical dashboard route. -- [ ] Update backend menu path generation rules if needed for dashboard route parameters. - -### 8.2 Permission model -- [ ] Define dashboard runtime permissions at dashboard definition and endpoint level. -- [ ] Update testing metadata generation to include new runtime controller permissions. - ---- - -## 9. Migration and Rollout Strategy - -### 9.1 Phase rollout -- [x] Phase A: remove legacy code and compile cleanly. -- [x] Phase B: introduce new backend runtime + metadata schema + provider set for queue-health reference dashboard. -- [ ] Phase C: introduce UI runtime + Gridstack + ECharts adapter + baseline widgets. -- [ ] Phase D: add user layout persistence and menu integration. -- [ ] Phase E: documentation, sample module metadata, agent reference implementation. - -### 9.2 Data/config migration -- [ ] Provide migration script or one-time converter for old dashboard JSON to new metadata schema (where feasible). -- [ ] Explicitly document non-migratable legacy constructs and fallback behavior. - ---- - -## 10. Testing and Quality Gates - -### 10.1 Backend tests -- [ ] Unit tests for provider registry resolution, schema validation, and controller contract. -- [ ] Integration tests for dashboard definition load, widget data batch response, variable option resolution, layout upsert/load. -- [ ] Security tests for SQL/provider guardrails. - -### 10.2 Frontend tests -- [ ] Component tests for dynamic filter rendering from metadata. -- [ ] Widget host tests for loading/error/retry states. -- [ ] Layout persistence tests (save/load/reset). -- [ ] Route/menu rendering tests for dashboard navigation. - -### 10.3 End-to-end smoke -- [x] Seed one reference dashboard in sample metadata. -- [ ] Validate: open dashboard -> apply filters -> render widgets -> drag/resize -> save layout -> reload persistence. - ---- - -## 11. Documentation and Developer Experience - -### 11.1 Framework docs -- [ ] Add “Dashboard Metadata Authoring Guide”. -- [ ] Add “Build a Custom Dashboard Widget Provider” guide. -- [ ] Add “Frontend DashboardWidget extension contract” guide. - -### 11.2 Agent-ready templates -- [ ] Add reference widget provider template files. -- [ ] Add reference dashboard metadata JSON template. -- [ ] Add checklist for creating a new widget end-to-end. - ---- - -## 12. Agent Project Enablement (Skills and Tooling) - -### 12.1 Agent capability goals -- [ ] Enable agents to discover dashboard definitions, variables, widgets, and layout metadata quickly. -- [ ] Enable agents to generate new dashboard/widget scaffolds that conform to framework contracts. -- [ ] Enable agents to validate dashboard metadata and provider wiring before runtime. -- [ ] Enable agents to troubleshoot dashboard rendering/data issues with structured diagnostics. - -### 12.2 New/updated skill surfaces -- [ ] Add a dedicated agent skill guide for dashboard implementation: - - metadata authoring (`dashboards`, `variables`, `widgets`, `defaultLayout`) - - backend provider scaffolding (`IDashboardWidgetDataProvider`) - - frontend widget extension scaffolding (`dashboardWidget` type) - - menu/action/route integration pattern. -- [ ] Add cookbook-style examples in the skill: - - create dashboard from scratch - - add a new widget type - - add variable-driven filtering - - persist and reset user layout. -- [ ] Add anti-patterns and guardrails section: - - avoid hard-coded UI branches per widget - - enforce provider response contract - - enforce parameterized SQL and payload limits. - -### 12.3 Tooling opportunities (optional but recommended) -- [ ] Add CLI-style helper commands (or MCP handlers) for: - - scaffold dashboard metadata block - - scaffold backend widget provider class - - scaffold frontend dashboard widget component - - run dashboard metadata validation. -- [ ] Add validation utility callable by agents: - - checks metadata schema compliance - - checks referenced providers/widgets/routes exist - - checks layout schema compatibility. -- [ ] Add introspection/debug endpoint(s) for agents: - - list registered dashboard widget providers - - preview resolved dashboard definition - - dry-run widget data contract output. - -### 12.4 Agent integration with existing project tooling -- [ ] Extend existing MCP handler ecosystem to support dashboard-specific actions: - - create dashboard - - add widget to dashboard - - add variable to dashboard - - regenerate menu/action links for dashboard routes. -- [ ] Ensure handler outputs are idempotent and metadata-safe (no duplicate entries, deterministic updates). -- [ ] Add structured result payloads so agents can chain operations reliably. - -### 12.5 Agent quality and safety checks -- [ ] Add preflight checks agents must run before patch generation: - - schema validation - - provider registration check - - permission mapping check - - route resolution check. -- [ ] Add post-change verification checklist for agents: - - metadata compiles/loads - - widget provider resolves in registry - - UI widget mounts with mocked data - - layout save/load roundtrip works. -- [ ] Add fail-fast diagnostics format: - - missing provider - - invalid widget type - - malformed layout - - variable expression mismatch. - -### 12.6 Agent adoption rollout -- [ ] Phase 1: publish the dashboard skill + templates with one golden-path example. -- [ ] Phase 2: add scaffold + validation tools for fast and safe agent output. -- [ ] Phase 3: add advanced capabilities (migration assistant, dashboard linting, widget contract tests). -- [ ] Track adoption metrics: - - time to create new dashboard - - first-pass success rate - - number of manual fixes after agent-generated changes. - ---- - -## 13. Immediate Execution Checklist (Sprint-1 Proposal) - -- [ ] Finalize canonical route format (`/admin/core/:module/dashboard/:dashboardName` vs `/admin/dashboard/:dashboardName`). -- [ ] Freeze new metadata schema draft (`dashboards.variables.widgets.layout`). -- [x] Delete legacy dashboard code paths in `solid-core-module`. -- [x] Delete legacy dashboard code paths in `solid-core-ui`. -- [x] Add new provider decorator + registry wiring + introspection. -- [x] Add new runtime controller + definition/data endpoints. -- [ ] Add dashboard layout metadata model and run code generation for persistence service/entity. -- [ ] Add UI dashboard page scaffold with metadata header + variable bar. -- [ ] Add Gridstack integration adapter and persist layout flow. -- [ ] Add ECharts renderer adapter + first 2 widgets (KPI + Bar/Line). -- [ ] Add one sample dashboard metadata entry in `solid-library-management` as reference. - ---- - -## 14. External Library Notes (validated May 30, 2026) - -- [ ] ECharts official feature/license references: - - https://echarts.apache.org/ - - https://echarts.apache.org/en/feature.html - - https://echarts.apache.org/faq -- [ ] Gridstack docs/releases references: - - https://gridstackjs.com/ - - https://gridstackjs.com/doc/html/classes/GridStack.html - - https://github.com/gridstack/gridstack.js/releases diff --git a/dashboard-curl-smoke-tests.txt b/dashboard-curl-smoke-tests.txt deleted file mode 100644 index 8fd2f057..00000000 --- a/dashboard-curl-smoke-tests.txt +++ /dev/null @@ -1,146 +0,0 @@ -# Dashboard Runtime Smoke Test CURLs -# Purpose: quick re-test suite for queue-health dashboard endpoints. -# Auth note: TOKEN should be a JWT access token generated by /iam/authenticate. - -# ---------------------------------------------------------------------------- -# Setup -# ---------------------------------------------------------------------------- -export BASE_URL="http://localhost:9000/api" -export TOKEN="" - -# Optional: verify token works. -curl -s "$BASE_URL/iam/me" \ - -H "Authorization: Bearer $TOKEN" | jq - -# ---------------------------------------------------------------------------- -# 1) Dashboard Definition -# Expected: 200 + dashboard metadata in .data -# ---------------------------------------------------------------------------- -curl -s "$BASE_URL/dashboard/solid-core/queue-health/definition" \ - -H "Authorization: Bearer $TOKEN" | jq - -# ---------------------------------------------------------------------------- -# 2) Dashboard Variable Options (dynamic/static) -# Expected: 200 + array in .data -# ---------------------------------------------------------------------------- - -# queueName (dynamic provider) -curl -s "$BASE_URL/dashboard/solid-core/queue-health/variable-options/queueName?limit=20&offset=0&query=" \ - -H "Authorization: Bearer $TOKEN" | jq - -# messageBroker (dynamic provider) -curl -s "$BASE_URL/dashboard/solid-core/queue-health/variable-options/messageBroker?limit=20&offset=0&query=" \ - -H "Authorization: Bearer $TOKEN" | jq - -# stage (static selection) -curl -s "$BASE_URL/dashboard/solid-core/queue-health/variable-options/stage" \ - -H "Authorization: Bearer $TOKEN" | jq - -# ---------------------------------------------------------------------------- -# 3) Single Widget Data Endpoints -# Expected: success envelope with .data.meta + .data.data -# ---------------------------------------------------------------------------- - -# KPI: total messages -curl -s -X POST "$BASE_URL/dashboard/solid-core/queue-health/widgets/kpi-total-messages/data" \ - -H "Authorization: Bearer $TOKEN" \ - -H "Content-Type: application/json" \ - -d '{ - "variables": { - "date": { "preset": "last_7_days" } - } - }' | jq - -# Line chart: messages over time -curl -s -X POST "$BASE_URL/dashboard/solid-core/queue-health/widgets/chart-messages-over-time/data" \ - -H "Authorization: Bearer $TOKEN" \ - -H "Content-Type: application/json" \ - -d '{ - "variables": { - "date": { "preset": "last_30_days" }, - "stage": ["succeeded", "failed"] - } - }' | jq - -# Table: recent failures -curl -s -X POST "$BASE_URL/dashboard/solid-core/queue-health/widgets/table-recent-failures/data" \ - -H "Authorization: Bearer $TOKEN" \ - -H "Content-Type: application/json" \ - -d '{ - "variables": { - "date": { "preset": "last_30_days" } - } - }' | jq - -# ---------------------------------------------------------------------------- -# 4) Batch Widget Data (subset) -# Expected: .data.widgets[] with provider-backed results -# ---------------------------------------------------------------------------- -curl -s -X POST "$BASE_URL/dashboard/solid-core/queue-health/data" \ - -H "Authorization: Bearer $TOKEN" \ - -H "Content-Type: application/json" \ - -d '{ - "widgetNames": [ - "kpi-total-messages", - "kpi-failed-messages", - "kpi-success-rate", - "chart-stage-distribution" - ], - "variables": { - "date": { "preset": "last_7_days" } - } - }' | jq - -# ---------------------------------------------------------------------------- -# 5) Batch Widget Data (all widgets) -# Expected: all configured widgets execute in one call -# ---------------------------------------------------------------------------- -curl -s -X POST "$BASE_URL/dashboard/solid-core/queue-health/data" \ - -H "Authorization: Bearer $TOKEN" \ - -H "Content-Type: application/json" \ - -d '{ - "variables": { - "date": { "preset": "last_30_days" } - } - }' | jq - -# ---------------------------------------------------------------------------- -# 6) Layout Endpoints -# Expected: -# - GET returns defaultLayout and optional userLayout -# - PUT stores user layout once dashboardUserLayout generated model is available -# ---------------------------------------------------------------------------- - -# Read layout -curl -s "$BASE_URL/dashboard/solid-core/queue-health/layout" \ - -H "Authorization: Bearer $TOKEN" | jq - -# Save layout -curl -s -X PUT "$BASE_URL/dashboard/solid-core/queue-health/layout" \ - -H "Authorization: Bearer $TOKEN" \ - -H "Content-Type: application/json" \ - -d '{ - "layoutJson": { - "engine": "gridstack", - "columns": 12, - "items": [ - { "widgetId": "kpi-total-messages", "x": 0, "y": 0, "w": 3, "h": 2 } - ] - } - }' | jq - -# ---------------------------------------------------------------------------- -# 7) Useful validation snippets -# ---------------------------------------------------------------------------- - -# Show only provider names from all-widgets batch response -curl -s -X POST "$BASE_URL/dashboard/solid-core/queue-health/data" \ - -H "Authorization: Bearer $TOKEN" \ - -H "Content-Type: application/json" \ - -d '{"variables":{"date":{"preset":"last_30_days"}}}' | jq '.data.widgets[].meta.providerName' - -# Show only recent failure record sample -curl -s -X POST "$BASE_URL/dashboard/solid-core/queue-health/widgets/table-recent-failures/data" \ - -H "Authorization: Bearer $TOKEN" \ - -H "Content-Type: application/json" \ - -d '{"variables":{"date":{"preset":"last_30_days"}}}' | jq '.data.data.records[0]' diff --git a/delete-legacy-dashboard-metadata.sql b/delete-legacy-dashboard-metadata.sql deleted file mode 100644 index d498c0ce..00000000 --- a/delete-legacy-dashboard-metadata.sql +++ /dev/null @@ -1,158 +0,0 @@ -BEGIN; - --- 0) Scope: solid-core dashboard legacy keys --- Models: --- dashboard, dashboardVariable, dashboardQuestion, dashboardQuestionSqlDatasetConfig, dashboardLayout --- Actions: --- dashboard-list-action, dashboardVariable-list-action, dashboardQuestion-list-action, --- dashboardLayout-list-action, dashboardQuestionSqlDatasetConfig-list-action --- Menus: --- dashboardManagement-menu-item, dashboard-menu-item, dashboardQuestion-menu-item, dashboardLayout-menu-item --- Views: --- dashboard-list-view, dashboard-form-view, --- dashboardVariable-list-view, dashboardVariable-form-view, --- dashboardQuestion-list-view, dashboardQuestion-form-view, --- dashboardQuestionSqlDatasetConfig-list-view, dashboardQuestionSqlDatasetConfig-form-view, --- dashboardLayout-list-view, dashboardLayout-form-view - --- 1) Remove role<->permission joins for dashboard controller permissions --- (table name may vary by naming strategy; this is the common one) -DELETE FROM ss_role_metadata_permissions_ss_permission_metadata -WHERE ss_permission_metadata_id IN ( - SELECT id - FROM ss_permission_metadata - WHERE name ILIKE 'Dashboard%' -); - --- 2) Remove dashboard permissions -DELETE FROM ss_permission_metadata -WHERE name ILIKE 'Dashboard%'; - --- 3) Remove menu<->role joins for dashboard menus -DELETE FROM ss_menu_item_metadata_roles_ss_role_metadata -WHERE ss_menu_item_metadata_id IN ( - SELECT m.id - FROM ss_menu_item_metadata m - WHERE m.name IN ( - 'dashboardManagement-menu-item', - 'dashboard-menu-item', - 'dashboardQuestion-menu-item', - 'dashboardLayout-menu-item' - ) -); - --- 4) Remove dashboard menus -DELETE FROM ss_menu_item_metadata -WHERE name IN ( - 'dashboardManagement-menu-item', - 'dashboard-menu-item', - 'dashboardQuestion-menu-item', - 'dashboardLayout-menu-item' -); - --- 5) Remove dashboard actions -DELETE FROM ss_action_metadata -WHERE name IN ( - 'dashboard-list-action', - 'dashboardVariable-list-action', - 'dashboardQuestion-list-action', - 'dashboardLayout-list-action', - 'dashboardQuestionSqlDatasetConfig-list-action' -); - --- 6) Remove user-specific view overrides for dashboard views -DELETE FROM ss_user_view_metadata -WHERE view_metadata_id IN ( - SELECT v.id - FROM ss_view_metadata v - WHERE v.name IN ( - 'dashboard-list-view', - 'dashboard-form-view', - 'dashboardVariable-list-view', - 'dashboardVariable-form-view', - 'dashboardQuestion-list-view', - 'dashboardQuestion-form-view', - 'dashboardQuestionSqlDatasetConfig-list-view', - 'dashboardQuestionSqlDatasetConfig-form-view', - 'dashboardLayout-list-view', - 'dashboardLayout-form-view' - ) -); - --- 7) Remove saved filters tied to dashboard models/views -DELETE FROM ss_saved_fitlers -WHERE model_id IN ( - SELECT m.id - FROM ss_model_metadata m - JOIN ss_module_metadata mm ON mm.id = m.module_id - WHERE mm.name = 'solid-core' - AND m.singular_name IN ( - 'dashboard', - 'dashboardVariable', - 'dashboardQuestion', - 'dashboardQuestionSqlDatasetConfig', - 'dashboardLayout' - ) -) -OR view_id IN ( - SELECT v.id - FROM ss_view_metadata v - WHERE v.name IN ( - 'dashboard-list-view', - 'dashboard-form-view', - 'dashboardVariable-list-view', - 'dashboardVariable-form-view', - 'dashboardQuestion-list-view', - 'dashboardQuestion-form-view', - 'dashboardQuestionSqlDatasetConfig-list-view', - 'dashboardQuestionSqlDatasetConfig-form-view', - 'dashboardLayout-list-view', - 'dashboardLayout-form-view' - ) -); - --- 8) Remove security rules attached to dashboard models (if any) -DELETE FROM ss_security_rule -WHERE model_metadata_id IN ( - SELECT m.id - FROM ss_model_metadata m - JOIN ss_module_metadata mm ON mm.id = m.module_id - WHERE mm.name = 'solid-core' - AND m.singular_name IN ( - 'dashboard', - 'dashboardVariable', - 'dashboardQuestion', - 'dashboardQuestionSqlDatasetConfig', - 'dashboardLayout' - ) -); - --- 9) Remove dashboard views -DELETE FROM ss_view_metadata -WHERE name IN ( - 'dashboard-list-view', - 'dashboard-form-view', - 'dashboardVariable-list-view', - 'dashboardVariable-form-view', - 'dashboardQuestion-list-view', - 'dashboardQuestion-form-view', - 'dashboardQuestionSqlDatasetConfig-list-view', - 'dashboardQuestionSqlDatasetConfig-form-view', - 'dashboardLayout-list-view', - 'dashboardLayout-form-view' -); - --- 10) Remove dashboard models (fields should cascade via model_id FK) -DELETE FROM ss_model_metadata -WHERE singular_name IN ( - 'dashboard', - 'dashboardVariable', - 'dashboardQuestion', - 'dashboardQuestionSqlDatasetConfig', - 'dashboardLayout' -) -AND module_id IN ( - SELECT id FROM ss_module_metadata WHERE name = 'solid-core' -); - -COMMIT; diff --git a/src/services/dashboard-providers/README.md b/src/services/dashboard-providers/README.md new file mode 100644 index 00000000..e6949cec --- /dev/null +++ b/src/services/dashboard-providers/README.md @@ -0,0 +1,543 @@ +# Dashboarding Architecture and Developer Guide + +This document is the canonical guide for SolidX dashboarding in its new metadata-driven architecture. + +It covers: +- end-to-end architecture (`solid-core-module` + `solid-core-ui`) +- metadata authoring model +- backend widget data providers +- frontend default/custom widget rendering +- layout persistence model +- extension points and troubleshooting + +--- + +## 1. Guiding Principles + +- **Metadata-first**: dashboard definitions live in module metadata JSON under root-level `dashboards`. +- **Provider-driven backend**: every widget resolves data through a backend data provider. +- **Extension-driven frontend**: widgets render through extension registry components (`dashboardWidget` type). +- **User personalization**: only user layout overrides are persisted; dashboard definitions are not persisted. + +--- + +## 2. Source of Truth (Important) + +### 2.1 What is persisted vs what is not + +Persisted: +- user-specific layout override (`ss_dashboard_user_layout`) + +Not persisted: +- dashboard definitions (`dashboards` metadata) +- variables +- widgets +- default layout + +### 2.2 Metadata location resolution + +At runtime, metadata is read from file paths resolved by `ModuleMetadataHelperService`: + +1. `module-metadata//-metadata.json` +2. fallback (solid-core local run): `src/seeders/seed-data/solid-core-metadata.json` +3. fallback (consuming project): `node_modules/@solidxai/core/src/seeders/seed-data/solid-core-metadata.json` + +This is the most common reason for “changes not taking effect”: the running service may be reading the installed package metadata, not local source metadata. + +--- + +## 3. End-to-End Runtime Flow + +1. UI route opens dashboard page: +- `/admin/core/:moduleName/dashboard/:dashboardName` + +2. UI loads definition: +- `GET /dashboard/:module/:dashboardName/definition` + +3. UI loads layout: +- `GET /dashboard/:module/:dashboardName/layout` +- backend returns `{ defaultLayout, userLayout, effectiveLayout }` + +4. UI resolves filters and requests data: +- `POST /dashboard/:module/:dashboardName/data` + +5. Backend runtime service: +- reads metadata dashboard definition +- resolves provider per widget from registry +- executes providers with variables + provider context +- returns normalized provider envelopes + +6. User drag/resize save: +- `PUT /dashboard/:module/:dashboardName/layout` +- persists user layout override + +--- + +## 4. Backend Architecture (`solid-core-module`) + +### 4.1 Core runtime controller + +File: +- `src/controllers/dashboard.controller.ts` + +Endpoints: +- `GET /dashboard/:moduleName/:dashboardName/definition` +- `POST /dashboard/:moduleName/:dashboardName/widgets/:widgetName/data` +- `POST /dashboard/:moduleName/:dashboardName/data` +- `GET /dashboard/:moduleName/:dashboardName/variable-options/:variableName` +- `GET /dashboard/:moduleName/:dashboardName/layout` +- `PUT /dashboard/:moduleName/:dashboardName/layout` + +All runtime dashboard endpoints are JWT-protected (`@ApiBearerAuth('jwt')`). + +### 4.2 Runtime service + +File: +- `src/services/dashboard-runtime.service.ts` + +Responsibilities: +- load dashboard definition from metadata +- resolve and invoke widget providers +- resolve static/dynamic variable options +- compute `effectiveLayout` via default+user merge +- save per-user layout + +### 4.3 Provider contract + +File: +- `src/interfaces.ts` + +Interfaces: +- `IDashboardWidgetDataProviderContext` +- `IDashboardWidgetDataResponseEnvelope` +- `IDashboardWidgetDataProvider` + +Provider contract: +- `name(): string` +- `help(): string` +- `getData(widgetDefinition, ctxt): Promise` + +Envelope shape: +- `meta`: provider name, widget name, duration, generatedAt +- `data`: widget payload +- `uiHints` (optional) + +### 4.4 Provider registration flow + +Files: +- decorator: `src/decorators/dashboard-widget-data-provider.decorator.ts` +- introspection: `src/services/solid-introspect.service.ts` +- registry: `src/helpers/solid-registry.ts` + +Pattern: +- annotate with `@DashboardWidgetDataProvider()` +- introspection discovers providers +- registry stores providers +- runtime resolves by `widget.dataProvider` + +### 4.5 Variable options providers + +Dynamic selection variables reuse existing selection provider infrastructure. + +Reference providers: +- `src/services/selection-providers/mq-dashboard-queue-name-variable-options-provider.service.ts` +- `src/services/selection-providers/mq-dashboard-message-broker-variable-options-provider.service.ts` + +Metadata variable uses: +- `type: "selectionDynamic"` +- `selectionConfig.providerName` + +### 4.6 Queue-health reference providers + +Folder: +- `src/services/dashboard-providers/` + +Includes KPI + charts + table providers: +- `mq-dashboard-total-messages-kpi-provider.service.ts` +- `mq-dashboard-succeeded-messages-kpi-provider.service.ts` +- `mq-dashboard-failed-messages-kpi-provider.service.ts` +- `mq-dashboard-inflight-messages-kpi-provider.service.ts` +- `mq-dashboard-success-rate-kpi-provider.service.ts` +- `mq-dashboard-avg-elapsed-kpi-provider.service.ts` +- `mq-dashboard-messages-over-time-provider.service.ts` +- `mq-dashboard-stage-distribution-provider.service.ts` +- `mq-dashboard-queue-wise-failures-provider.service.ts` +- `mq-dashboard-queue-wise-avg-elapsed-provider.service.ts` +- `mq-dashboard-latency-trend-provider.service.ts` +- `mq-dashboard-recent-failures-provider.service.ts` + +Shared filter utilities: +- `mq-dashboard-provider-utils.ts` + +### 4.7 Layout persistence model + +Generated model and assets: +- entity: `src/entities/dashboard-user-layout.entity.ts` +- repository: `src/repositories/dashboard-user-layout.repository.ts` +- service: `src/services/dashboard-user-layout.service.ts` +- controller: `src/controllers/dashboard-user-layout.controller.ts` + +Table: +- `ss_dashboard_user_layout` + +Uniqueness: +- `(user_id, module_id, dashboard_name)` + +Stored payload: +- `layoutJson` with Gridstack-compatible `items`. + +--- + +## 5. Metadata Contract (`dashboards`) + +Metadata file (solid-core reference): +- `src/seeders/seed-data/solid-core-metadata.json` + +`dashboards` is a root peer of `actions`, `menus`, `roles`, `users`, etc. + +### 5.1 Dashboard definition shape + +Typical fields: +- `dashboardSchemaVersion` +- `name` +- `displayName` +- `description` +- `routeName` +- `routePath` +- `moduleUserKey` +- `permissions[]` +- `variables[]` +- `widgets[]` +- `defaultLayout` + +### 5.2 Variable types in current implementation + +- `date` +- `selectionStatic` +- `selectionDynamic` +- `isMultiSelect` supported for selection types + +Date variable payload sent to backend: +- `{ from?: ISO, to?: ISO }` +- UI preset flow supports `today`, `last_24_hours`, `last_7_days`, `last_30_days`, `custom` + +Static selection: +- `selectionStaticValues: ["value:Label", ...]` + +Dynamic selection: +- `selectionConfig.providerName` +- optional `selectionConfig.providerContext` + +### 5.3 Widget definition shape + +Typical fields: +- `id` +- `name` +- `type` (`kpi`, `lineChart`, `barChart`, `pieChart`, `table`, ...) +- `dataProvider` +- `providerContext` +- `refreshPolicy` +- optional UI override: + - `componentName` (or equivalent custom component key) + +### 5.4 Default layout shape + +`defaultLayout`: +- `engine: "gridstack"` +- `columns: number` +- `items[]` each with: + - `widgetId`, `x`, `y`, `w`, `h`, optional `minW`, `minH` + +--- + +## 6. Frontend Architecture (`solid-core-ui`) + +### 6.1 Route and page + +Route registration: +- `src/routes/solidRoutes.tsx` +- path: `/admin/core/:moduleName/dashboard/:dashboardName` + +Page: +- `src/routes/pages/admin/core/DashboardPage.tsx` + +### 6.2 Dashboard API slice + +File: +- `src/redux/api/dashboardRuntimeApi.ts` + +Hooks: +- `useGetDashboardDefinitionQuery` +- `useGetDashboardLayoutQuery` +- `useSaveDashboardLayoutMutation` +- `useGetDashboardDataMutation` +- `useLazyGetDashboardVariableOptionsQuery` + +### 6.3 Filter UX (current) + +- Filters are rendered in a modal dialog (not inline panel) +- Header has icon CTAs: filter, refresh, save layout +- Filter icon shows applied-filter badge count +- Date variable UX: + - preset dropdown first + - selecting `custom` reveals start/end date pickers +- Dynamic selection uses Solid autocomplete primitives +- Static selection uses Solid primitives (single/multi behavior) + +### 6.4 Layout engine + +- Gridstack integration in `DashboardPage` +- layout source order: + - `effectiveLayout.items` from backend + - fallback computed layout from widget order +- save sends full layout snapshot for all widgets + +### 6.5 Widget extension system + +Types: +- `src/types/extension-registry.ts` +- includes `dashboardWidget` + +Registry: +- `src/helpers/registry.ts` + +Dashboard widget props: +- `src/types/dashboard.ts` (`DashboardWidgetComponentProps`) + +Default registered dashboard widgets: +- `DefaultDashboardKpiWidget` +- `DefaultDashboardLineChartWidget` +- `DefaultDashboardBarChartWidget` +- `DefaultDashboardPieChartWidget` +- `DefaultDashboardTableWidget` +- `DefaultDashboardUnknownWidget` + +### 6.6 Chart abstraction + +- baseline chart engine: Apache ECharts +- mapper lives under: + - `src/components/core/dashboard/mappers/echartsOptionMapper` +- default chart widgets convert provider payload -> ECharts options + +--- + +## 7. Developer Workflows + +## 7.1 Create a new dashboard (metadata-first) + +1. Add dashboard block under root `dashboards[]` +2. Define variables +3. Define widgets with `dataProvider` +4. Define `defaultLayout` +5. Add action/menu entries for navigation +6. ensure route points to `/admin/core//dashboard/` + +## 7.2 Add a new backend widget provider + +1. Create provider class under `src/services/dashboard-providers` +2. Implement `IDashboardWidgetDataProvider` +3. Annotate with `@DashboardWidgetDataProvider()` + `@Injectable()` +4. Inject repositories and use security-rule-aware query builders +5. Return envelope with `meta` and `data` +6. Reference provider name in metadata widget definition + +Minimal skeleton: + +```ts +@DashboardWidgetDataProvider() +@Injectable() +export class MyWidgetProvider implements IDashboardWidgetDataProvider { + name(): string { + return "MyWidgetProvider"; + } + + help(): string { + return "Returns data for My Widget."; + } + + async getData(widgetDefinition: Record, ctxt: IDashboardWidgetDataProviderContext) { + return { + meta: { + providerName: this.name(), + widgetName: ctxt.widgetName, + generatedAt: new Date().toISOString(), + durationMs: 0, + }, + data: { + value: 123, + }, + }; + } +} +``` + +## 7.3 Add dynamic variable options provider + +1. Create `ISelectionProvider` class +2. Decorate with `@SelectionProvider()` +3. Implement `value()` and `values()` +4. Reference provider in variable `selectionConfig.providerName` + +## 7.4 Add a custom frontend widget (optional) + +Custom rendering is optional. + +Model: +- backend provider is mandatory for data +- custom widget component is optional override + +Steps: +1. Create component in UI project +2. Register with `registerExtensionComponent(..., ExtensionComponentTypes.dashboardWidget)` +3. Reference component in widget metadata (`componentName`) +4. If no custom widget is provided, framework default widgets are used + +--- + +## 8. Queue Health Reference Implementation + +Reference dashboard: +- module: `solid-core` +- dashboard: `queue-health` + +Variables: +- `date` (`type: date`) +- `queueName` (`selectionDynamic`) +- `stage` (`selectionStatic`, multi) +- `messageBroker` (`selectionDynamic`) + +Widgets include: +- KPIs (total, succeeded, failed, in-flight, success rate, avg elapsed) +- Charts (time series, stage distribution, queue failures, queue avg elapsed, latency trend) +- Table (recent failures) + +This RI is the baseline for future dashboards and for custom widget extension examples. + +--- + +## 9. Troubleshooting Guide + +### 9.1 Metadata change not reflecting + +Check which metadata file runtime reads (see Section 2.2 precedence). + +In consuming projects, ensure latest package is linked/installed: +- `@solidxai/core` +- `@solidxai/core-ui` + +Then restart backend and frontend dev servers. + +### 9.2 Layout saves but refresh shows old layout + +Checklist: +- verify `GET /dashboard/:module/:dashboard/layout` returns updated `userLayout` / `effectiveLayout` +- confirm UI bundle includes latest `DashboardPage` grid initialization logic +- clear Vite cache and hard refresh browser + +### 9.3 Dynamic options not showing + +Checklist: +- variable `type` is exactly `selectionDynamic` +- `selectionConfig.providerName` matches provider `name()` +- provider class has `@SelectionProvider()` and is registered via module wiring + +### 9.4 Widget shows fallback/unknown renderer + +Checklist: +- provider returns expected shape in `data` +- widget metadata `type` aligns with default mapper support +- if using custom renderer, ensure component is registered as `dashboardWidget` + +### 9.5 Auth / permission issues + +- Dashboard runtime endpoints require JWT +- ensure dashboard permissions include `DashboardController.*` methods in metadata + +--- + +## 10. Security and Guardrails (Current + Near-term) + +Current: +- JWT-protected endpoints +- repository/security-rule-aware query builder usage in reference providers + +Recommended next: +- strict provider allowlist enforcement in runtime layer +- query timeout and payload-size limits +- row limits for heavy chart/table providers + +--- + +## 11. API Examples + +### 11.1 Fetch definition + +```bash +curl -H "Authorization: Bearer $TOKEN" \ + "$BASE_URL/dashboard/solid-core/queue-health/definition" +``` + +### 11.2 Fetch full dashboard data + +```bash +curl -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ + "$BASE_URL/dashboard/solid-core/queue-health/data" \ + -d '{ + "variables": { + "date": { "from": "2026-05-01T00:00:00.000Z", "to": "2026-05-31T23:59:59.999Z" }, + "queueName": "create_sandbox_dev_sandbox_provisioning_queue", + "stage": ["failed", "retrying"] + } + }' +``` + +### 11.3 Save layout + +```bash +curl -X PUT -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ + "$BASE_URL/dashboard/solid-core/queue-health/layout" \ + -d '{ + "layout": { + "engine": "gridstack", + "columns": 12, + "items": [ + { "widgetId": "kpi-total-messages", "x": 0, "y": 0, "w": 2, "h": 2 } + ] + } + }' +``` + +--- + +## 12. What to Reuse vs What to Customize + +Reuse defaults when: +- metric/table/chart fits existing renderer types +- ECharts mapper can render your shape + +Create custom component when: +- interaction/visual behavior is domain-specific +- advanced chart behavior is needed beyond generic renderer + +Always keep backend provider contract stable so UI remains pluggable. + +--- + +## 13. Current Status Snapshot + +Implemented: +- metadata-driven dashboard runtime +- backend provider contracts + registry wiring +- dashboard controller endpoints +- queue-health reference providers +- dynamic variable options providers +- frontend dashboard page, filter modal, Gridstack persistence +- ECharts-based default dashboard widgets +- `dashboardWidget` extension type and default registrations +- user layout persistence via generated model assets + +Pending/next: +- stronger runtime schema validation and guardrails +- richer renderer pluggability contract (`chartRenderer` beyond v1) +- additional documentation templates and automated validation helpers + From 4c2fa95d1a0e617410aede1471eeaf44c56deeba Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Tue, 2 Jun 2026 11:25:18 +0530 Subject: [PATCH 078/136] 0.1.10-beta.22 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5b3eeef9..70e7a539 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.21", + "version": "0.1.10-beta.22", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@solidxai/core", - "version": "0.1.10-beta.21", + "version": "0.1.10-beta.22", "license": "BUSL-1.1", "dependencies": { "@aws-sdk/client-s3": "^3.637.0", diff --git a/package.json b/package.json index 1c9ae8d5..543a8066 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.21", + "version": "0.1.10-beta.22", "description": "This module is a NestJS module containing all the required core providers required by a Solid application", "main": "dist/index.js", "types": "dist/index.d.ts", From 3d97f54ef34d096a8fc16b68755877e10879aa0e Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Tue, 2 Jun 2026 10:28:43 +0100 Subject: [PATCH 079/136] feat(dashboard): add Queue SLA heatmap provider and update metadata --- .../seed-data/solid-core-metadata.json | 244 +++++++++++++----- src/services/dashboard-providers/README.md | 21 +- ...oard-queue-sla-heatmap-provider.service.ts | 155 +++++++++++ src/solid-core.module.ts | 2 + 4 files changed, 346 insertions(+), 76 deletions(-) create mode 100644 src/services/dashboard-providers/mq-dashboard-queue-sla-heatmap-provider.service.ts diff --git a/src/seeders/seed-data/solid-core-metadata.json b/src/seeders/seed-data/solid-core-metadata.json index 2bb5131b..032ea95b 100755 --- a/src/seeders/seed-data/solid-core-metadata.json +++ b/src/seeders/seed-data/solid-core-metadata.json @@ -5505,7 +5505,8 @@ "permissions": [ "mcp:invoke", "agent:invoke", - "settings:view_encrypted" + "settings:view_encrypted", + "dashboard:queue-health.*" ], "roles": [ { @@ -13291,15 +13292,51 @@ 50 ], "enableGlobalSearch": true, - "create": true, - "edit": true, + "create": false, + "edit": false, "delete": true }, "children": [ { "type": "field", "attrs": { - "name": "id" + "name": "id", + "isSearchable": false + } + }, + { + "type": "field", + "attrs": { + "name": "dashboardName", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "module", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "user", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "version", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "updatedAt", + "isSearchable": false } } ] @@ -13322,8 +13359,8 @@ 50 ], "enableGlobalSearch": true, - "create": true, - "edit": true, + "create": false, + "edit": false, "delete": true }, "children": [ @@ -13332,6 +13369,36 @@ "attrs": { "name": "id" } + }, + { + "type": "field", + "attrs": { + "name": "dashboardName" + } + }, + { + "type": "field", + "attrs": { + "name": "module" + } + }, + { + "type": "field", + "attrs": { + "name": "user" + } + }, + { + "type": "field", + "attrs": { + "name": "version" + } + }, + { + "type": "field", + "attrs": { + "name": "updatedAt" + } } ] } @@ -13345,10 +13412,14 @@ "modelUserKey": "dashboardUserLayout", "layout": { "type": "form", + "edit": false, "attrs": { "name": "form-1", "label": "Dashboard User Layout", - "className": "grid" + "className": "grid", + "showAddFormButton": false, + "showEditFormButton": false, + "showDeleteFormButton": true }, "children": [ { @@ -13366,11 +13437,58 @@ { "type": "column", "attrs": { - "name": "group-1", - "label": "", + "name": "dashboard-user-layout-meta", + "label": "Layout Metadata", "className": "col-12 sm:col-12 md:col-6 lg:col-6" }, - "children": [] + "children": [ + { + "type": "field", + "attrs": { + "name": "dashboardName" + } + }, + { + "type": "field", + "attrs": { + "name": "module" + } + }, + { + "type": "field", + "attrs": { + "name": "user" + } + }, + { + "type": "field", + "attrs": { + "name": "version" + } + }, + { + "type": "field", + "attrs": { + "name": "updatedAt" + } + } + ] + }, + { + "type": "column", + "attrs": { + "name": "dashboard-user-layout-json", + "label": "Layout Payload", + "className": "col-12 sm:col-12 md:col-6 lg:col-6" + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "layoutJson" + } + } + ] } ] } @@ -13522,17 +13640,7 @@ "name": "queue-health", "displayName": "Queue Health Dashboard", "description": "Operational visibility for MQ queues and messages.", - "routeName": "queue-health", - "routePath": "/admin/dashboard/queue-health", "moduleUserKey": "solid-core", - "permissions": [ - "DashboardController.getDefinition", - "DashboardController.getVariableOptions", - "DashboardController.getWidgetData", - "DashboardController.getDashboardData", - "DashboardController.getLayout", - "DashboardController.saveLayout" - ], "variables": [ { "name": "date", @@ -13593,10 +13701,6 @@ "dataProvider": "MqDashboardTotalMessagesKpiProvider", "providerContext": { "metric": "count_total_messages" - }, - "refreshPolicy": { - "mode": "interval", - "intervalSeconds": 60 } }, { @@ -13606,10 +13710,6 @@ "dataProvider": "MqDashboardSucceededMessagesKpiProvider", "providerContext": { "metric": "count_succeeded_messages" - }, - "refreshPolicy": { - "mode": "interval", - "intervalSeconds": 60 } }, { @@ -13619,10 +13719,6 @@ "dataProvider": "MqDashboardFailedMessagesKpiProvider", "providerContext": { "metric": "count_failed_messages" - }, - "refreshPolicy": { - "mode": "interval", - "intervalSeconds": 60 } }, { @@ -13632,10 +13728,6 @@ "dataProvider": "MqDashboardInflightMessagesKpiProvider", "providerContext": { "metric": "count_inflight_messages" - }, - "refreshPolicy": { - "mode": "interval", - "intervalSeconds": 60 } }, { @@ -13645,10 +13737,6 @@ "dataProvider": "MqDashboardSuccessRateKpiProvider", "providerContext": { "metric": "success_rate_percentage" - }, - "refreshPolicy": { - "mode": "interval", - "intervalSeconds": 60 } }, { @@ -13658,10 +13746,6 @@ "dataProvider": "MqDashboardAvgElapsedKpiProvider", "providerContext": { "metric": "average_elapsed_millis" - }, - "refreshPolicy": { - "mode": "interval", - "intervalSeconds": 60 } }, { @@ -13678,10 +13762,6 @@ "failed", "retrying" ] - }, - "refreshPolicy": { - "mode": "interval", - "intervalSeconds": 120 } }, { @@ -13692,10 +13772,6 @@ "providerContext": { "groupBy": "stage", "metric": "count" - }, - "refreshPolicy": { - "mode": "interval", - "intervalSeconds": 120 } }, { @@ -13706,10 +13782,6 @@ "providerContext": { "groupBy": "mqMessageQueue.name", "metric": "failed_count" - }, - "refreshPolicy": { - "mode": "interval", - "intervalSeconds": 120 } }, { @@ -13720,10 +13792,6 @@ "providerContext": { "groupBy": "mqMessageQueue.name", "metric": "avg_elapsed_millis" - }, - "refreshPolicy": { - "mode": "interval", - "intervalSeconds": 120 } }, { @@ -13735,10 +13803,45 @@ "timeField": "createdAt", "bucket": "hour", "metric": "avg_elapsed_millis" - }, - "refreshPolicy": { - "mode": "interval", - "intervalSeconds": 120 + } + }, + { + "id": "chart-queue-sla-heatmap", + "name": "Queue SLA Heatmap", + "type": "customChart", + "dataProvider": "MqDashboardQueueSlaHeatmapProvider", + "componentName": "QueueSlaHeatmapWidget", + "providerContext": { + "bucket": "hour", + "tooltipFields": [ + "avgElapsedMillis", + "messageCount", + "peakElapsedMillis" + ], + "legendThresholds": [ + { + "label": "0-5s", + "color": "#22c55e", + "lte": 5000 + }, + { + "label": "5-15s", + "color": "#f59e0b", + "gt": 5000, + "lte": 15000 + }, + { + "label": "15-30s", + "color": "#f97316", + "gt": 15000, + "lte": 30000 + }, + { + "label": "30s+", + "color": "#ef4444", + "gt": 30000 + } + ] } }, { @@ -13765,10 +13868,6 @@ "createdAt" ], "errorMaxLength": 160 - }, - "refreshPolicy": { - "mode": "interval", - "intervalSeconds": 60 } } ], @@ -13854,10 +13953,17 @@ "h": 5 }, { - "widgetId": "table-recent-failures", + "widgetId": "chart-queue-sla-heatmap", "x": 0, "y": 11, "w": 12, + "h": 5 + }, + { + "widgetId": "table-recent-failures", + "x": 0, + "y": 16, + "w": 12, "h": 4 } ] diff --git a/src/services/dashboard-providers/README.md b/src/services/dashboard-providers/README.md index e6949cec..33c3e0ea 100644 --- a/src/services/dashboard-providers/README.md +++ b/src/services/dashboard-providers/README.md @@ -202,10 +202,7 @@ Typical fields: - `name` - `displayName` - `description` -- `routeName` -- `routePath` - `moduleUserKey` -- `permissions[]` - `variables[]` - `widgets[]` - `defaultLayout` @@ -233,10 +230,9 @@ Dynamic selection: Typical fields: - `id` - `name` -- `type` (`kpi`, `lineChart`, `barChart`, `pieChart`, `table`, ...) +- `type` (`kpi`, `lineChart`, `barChart`, `pieChart`, `table`, `customChart`, ...) - `dataProvider` - `providerContext` -- `refreshPolicy` - optional UI override: - `componentName` (or equivalent custom component key) @@ -413,6 +409,18 @@ Widgets include: This RI is the baseline for future dashboards and for custom widget extension examples. +It also now includes the canonical 100% custom widget example: +- backend provider: `MqDashboardQueueSlaHeatmapProvider` +- frontend widget: `QueueSlaHeatmapWidget` + +Heatmap provider response contract: +- `xCategories` +- `yCategories` +- `points` (`[xIndex, yIndex, metricValue]`) +- optional `tooltipFields` +- optional `pointDetails` +- optional `legendThresholds` + --- ## 9. Troubleshooting Guide @@ -451,7 +459,7 @@ Checklist: ### 9.5 Auth / permission issues - Dashboard runtime endpoints require JWT -- ensure dashboard permissions include `DashboardController.*` methods in metadata +- verify the authenticated user can access the module and dashboard route in the consuming app --- @@ -540,4 +548,3 @@ Pending/next: - stronger runtime schema validation and guardrails - richer renderer pluggability contract (`chartRenderer` beyond v1) - additional documentation templates and automated validation helpers - diff --git a/src/services/dashboard-providers/mq-dashboard-queue-sla-heatmap-provider.service.ts b/src/services/dashboard-providers/mq-dashboard-queue-sla-heatmap-provider.service.ts new file mode 100644 index 00000000..da5eaf27 --- /dev/null +++ b/src/services/dashboard-providers/mq-dashboard-queue-sla-heatmap-provider.service.ts @@ -0,0 +1,155 @@ +import { Injectable } from "@nestjs/common"; +import { DashboardWidgetDataProvider } from "src/decorators/dashboard-widget-data-provider.decorator"; +import { IDashboardWidgetDataProvider, IDashboardWidgetDataProviderContext, IDashboardWidgetDataResponseEnvelope } from "src/interfaces"; +import { MqMessageRepository } from "src/repository/mq-message.repository"; +import { applyMqDashboardFilters, normalizeBucket, toNumber } from "src/services/dashboard-providers/mq-dashboard-provider-utils"; + +type HeatmapLegendThreshold = { + label: string; + color: string; + lte?: number; + gte?: number; + gt?: number; +}; + +type HeatmapPointDetail = { + xIndex: number; + yIndex: number; + bucket: string; + queueName: string; + avgElapsedMillis: number; + messageCount: number; + peakElapsedMillis: number; +}; + +type QueueSlaHeatmapResponse = { + xCategories: string[]; + yCategories: string[]; + points: [number, number, number][]; + metric: string; + bucket: string; + tooltipFields?: string[]; + pointDetails?: HeatmapPointDetail[]; + legendThresholds?: HeatmapLegendThreshold[]; +}; + +const DEFAULT_LEGEND_THRESHOLDS: HeatmapLegendThreshold[] = [ + { label: "0-5s", color: "#22c55e", lte: 5000 }, + { label: "5-15s", color: "#f59e0b", gt: 5000, lte: 15000 }, + { label: "15-30s", color: "#f97316", gt: 15000, lte: 30000 }, + { label: "30s+", color: "#ef4444", gt: 30000 }, +]; + +@DashboardWidgetDataProvider() +@Injectable() +export class MqDashboardQueueSlaHeatmapProvider implements IDashboardWidgetDataProvider { + constructor( + private readonly mqMessageRepository: MqMessageRepository, + ) { } + + name(): string { + return "MqDashboardQueueSlaHeatmapProvider"; + } + + help(): string { + return "Returns queue-wise SLA heatmap data using average elapsed millis across time buckets."; + } + + async getData( + widgetDefinition: Record, + ctxt: IDashboardWidgetDataProviderContext, + ): Promise> { + const bucket = normalizeBucket(ctxt?.providerContext?.bucket); + const legendThresholds = Array.isArray(ctxt?.providerContext?.legendThresholds) && ctxt.providerContext.legendThresholds.length > 0 + ? ctxt.providerContext.legendThresholds + : DEFAULT_LEGEND_THRESHOLDS; + const tooltipFields = Array.isArray(ctxt?.providerContext?.tooltipFields) && ctxt.providerContext.tooltipFields.length > 0 + ? ctxt.providerContext.tooltipFields + : ["avgElapsedMillis", "messageCount", "peakElapsedMillis"]; + + const qb = await this.mqMessageRepository.createSecurityRuleAwareQueryBuilder("mqMessage"); + applyMqDashboardFilters(qb, ctxt.variables ?? {}, { ensureQueueJoin: true }); + qb.andWhere("mqMessage.elapsedMillis IS NOT NULL"); + + const bucketExpr = `DATE_TRUNC('${bucket}', mqMessage.createdAt)`; + const rows = await qb + .select(bucketExpr, "bucket") + .addSelect("mqMessageQueue.name", "queueName") + .addSelect("AVG(mqMessage.elapsedMillis)", "avgElapsedMillis") + .addSelect("COUNT(mqMessage.id)", "messageCount") + .addSelect("MAX(mqMessage.elapsedMillis)", "peakElapsedMillis") + .groupBy(bucketExpr) + .addGroupBy("mqMessageQueue.name") + .orderBy("bucket", "ASC") + .addOrderBy("mqMessageQueue.name", "ASC") + .getRawMany<{ + bucket: string; + queueName: string; + avgElapsedMillis: string | number; + messageCount: string | number; + peakElapsedMillis: string | number; + }>(); + + const xCategories = Array.from( + new Set( + rows + .map((row) => row?.bucket ? new Date(row.bucket).toISOString() : "") + .filter((bucketValue) => !!bucketValue) + ) + ).sort(); + const yCategories = Array.from( + new Set(rows.map((row) => `${row?.queueName ?? ""}`.trim()).filter((queueName) => !!queueName)) + ).sort((left, right) => left.localeCompare(right)); + + const xIndexMap = new Map(xCategories.map((value, index) => [value, index])); + const yIndexMap = new Map(yCategories.map((value, index) => [value, index])); + + const points: [number, number, number][] = []; + const pointDetails: HeatmapPointDetail[] = []; + + for (const row of rows) { + const bucketValue = row?.bucket ? new Date(row.bucket).toISOString() : ""; + const queueName = `${row?.queueName ?? ""}`.trim(); + const xIndex = xIndexMap.get(bucketValue); + const yIndex = yIndexMap.get(queueName); + + if (xIndex === undefined || yIndex === undefined) { + continue; + } + + const avgElapsedMillis = Number(toNumber(row.avgElapsedMillis).toFixed(2)); + const messageCount = toNumber(row.messageCount); + const peakElapsedMillis = Number(toNumber(row.peakElapsedMillis).toFixed(2)); + + points.push([xIndex, yIndex, avgElapsedMillis]); + pointDetails.push({ + xIndex, + yIndex, + bucket: bucketValue, + queueName, + avgElapsedMillis, + messageCount, + peakElapsedMillis, + }); + } + + return { + meta: { + providerName: this.name(), + generatedAt: new Date().toISOString(), + widgetName: ctxt.widgetName, + durationMs: 0, + }, + data: { + xCategories, + yCategories, + points, + metric: "avg_elapsed_millis", + bucket, + tooltipFields, + pointDetails, + legendThresholds, + }, + }; + } +} diff --git a/src/solid-core.module.ts b/src/solid-core.module.ts index ed7204d9..e572689b 100644 --- a/src/solid-core.module.ts +++ b/src/solid-core.module.ts @@ -313,6 +313,7 @@ import { MqDashboardLatencyTrendProvider } from './services/dashboard-providers/ import { MqDashboardMessagesOverTimeProvider } from './services/dashboard-providers/mq-dashboard-messages-over-time-provider.service'; import { MqDashboardQueueWiseAvgElapsedProvider } from './services/dashboard-providers/mq-dashboard-queue-wise-avg-elapsed-provider.service'; import { MqDashboardQueueWiseFailuresProvider } from './services/dashboard-providers/mq-dashboard-queue-wise-failures-provider.service'; +import { MqDashboardQueueSlaHeatmapProvider } from './services/dashboard-providers/mq-dashboard-queue-sla-heatmap-provider.service'; import { MqDashboardRecentFailuresProvider } from './services/dashboard-providers/mq-dashboard-recent-failures-provider.service'; import { MqDashboardStageDistributionProvider } from './services/dashboard-providers/mq-dashboard-stage-distribution-provider.service'; import { MqDashboardSucceededMessagesKpiProvider } from './services/dashboard-providers/mq-dashboard-succeeded-messages-kpi-provider.service'; @@ -715,6 +716,7 @@ import { DashboardUserLayoutRepository } from './repositories/dashboard-user-lay MqDashboardStageDistributionProvider, MqDashboardQueueWiseFailuresProvider, MqDashboardQueueWiseAvgElapsedProvider, + MqDashboardQueueSlaHeatmapProvider, MqDashboardLatencyTrendProvider, MqDashboardRecentFailuresProvider, FileStorageProvider, From eed97fb838f0760bd1cfd6b410df02adf68df058 Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Tue, 2 Jun 2026 11:48:27 +0100 Subject: [PATCH 080/136] feat(dashboard): enhance widget permission checks and add unauthorized response handling --- .../seed-data/solid-core-metadata.json | 4 +- src/services/dashboard-runtime.service.ts | 72 ++++++++++++++++++- 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/src/seeders/seed-data/solid-core-metadata.json b/src/seeders/seed-data/solid-core-metadata.json index 032ea95b..6d427895 100755 --- a/src/seeders/seed-data/solid-core-metadata.json +++ b/src/seeders/seed-data/solid-core-metadata.json @@ -5506,7 +5506,9 @@ "mcp:invoke", "agent:invoke", "settings:view_encrypted", - "dashboard:queue-health.*" + "dashboard:queue-health:kpi-.*", + "dashboard:queue-health:chart-queue-.*", + "dashboard:queue-health:chart-processing-latency-trend" ], "roles": [ { diff --git a/src/services/dashboard-runtime.service.ts b/src/services/dashboard-runtime.service.ts index 3c105d86..8303cf6e 100644 --- a/src/services/dashboard-runtime.service.ts +++ b/src/services/dashboard-runtime.service.ts @@ -268,8 +268,17 @@ export class DashboardRuntimeService { activeUser?: ActiveUserData, ): Promise { const widgetDefinition = this.findWidgetDefinition(dashboardDefinition, widgetName); + const resolvedWidgetName = widgetDefinition.id ?? widgetDefinition.name ?? widgetName; const providerName = widgetDefinition.dataProvider; + if (!this.hasDashboardWidgetPermission(activeUser, dashboardDefinition?.name ?? dashboardName, resolvedWidgetName)) { + return this.buildUnauthorizedWidgetResponse( + dashboardDefinition?.name ?? dashboardName, + resolvedWidgetName, + providerName, + ); + } + if (!providerName) { throw new NotFoundException(`Widget ${widgetName} is missing dataProvider definition.`); } @@ -287,7 +296,7 @@ export class DashboardRuntimeService { const runtimeContext: IDashboardWidgetDataProviderContext = { moduleName, dashboardName, - widgetName: widgetDefinition.id ?? widgetDefinition.name ?? widgetName, + widgetName: resolvedWidgetName, variables: request.variables ?? {}, providerContext, activeUser, @@ -351,6 +360,67 @@ export class DashboardRuntimeService { } } + private hasDashboardWidgetPermission( + activeUser: ActiveUserData | undefined, + dashboardName: string, + widgetName: string, + ): boolean { + const permissions = Array.isArray(activeUser?.permissions) ? activeUser.permissions : []; + if (!dashboardName || !widgetName || permissions.length === 0) { + return false; + } + + return permissions.some((permission) => this.matchesDashboardWidgetPermission(permission, dashboardName, widgetName)); + } + + private matchesDashboardWidgetPermission(permission: string, dashboardName: string, widgetName: string): boolean { + const match = /^dashboard:([^:]+):(.+)$/.exec(`${permission ?? ''}`.trim()); + if (!match) { + return false; + } + + const [, permissionDashboardName, widgetPattern] = match; + if (permissionDashboardName !== dashboardName || !widgetPattern) { + return false; + } + + if (widgetPattern === '*') { + return true; + } + + if (widgetPattern === widgetName) { + return true; + } + + try { + return new RegExp(widgetPattern).test(widgetName); + } catch { + return false; + } + } + + private buildUnauthorizedWidgetResponse( + dashboardName: string, + widgetName: string, + providerName?: string, + ): any { + return { + meta: { + providerName: providerName ?? null, + widgetName, + durationMs: 0, + generatedAt: new Date().toISOString(), + unauthorized: true, + permissionExpression: `dashboard:${dashboardName}:${widgetName}`, + }, + data: null, + uiHints: { + state: 'unauthorized', + message: 'Unauthorized', + }, + }; + } + private parseLayoutJson(layoutJson: any): any { if (layoutJson === null || layoutJson === undefined) return null; if (typeof layoutJson !== 'string') return layoutJson; From c5424718fa7f8182ac1401472972dd2ddee8d9c3 Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Tue, 2 Jun 2026 12:15:05 +0100 Subject: [PATCH 081/136] feat(dashboard): implement explicit widget permissions and unauthorized response handling --- src/services/dashboard-providers/README.md | 57 +++++++++++++++++++++- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/src/services/dashboard-providers/README.md b/src/services/dashboard-providers/README.md index 33c3e0ea..e29dc760 100644 --- a/src/services/dashboard-providers/README.md +++ b/src/services/dashboard-providers/README.md @@ -64,7 +64,8 @@ This is the most common reason for “changes not taking effect”: the running 5. Backend runtime service: - reads metadata dashboard definition - resolves provider per widget from registry -- executes providers with variables + provider context +- checks widget permissions for the active user +- executes providers with variables + provider context only for authorized widgets - returns normalized provider envelopes 6. User drag/resize save: @@ -244,6 +245,37 @@ Typical fields: - `items[]` each with: - `widgetId`, `x`, `y`, `w`, `h`, optional `minW`, `minH` +### 5.5 Explicit widget permissions + +Dashboards use SolidX's existing explicit permission mechanism for widget-level authorization. + +Permission format: +- `dashboard::` + +Rules: +- the first segment is always `dashboard` +- the dashboard name must match exactly +- the widget segment supports: + - exact widget names + - `*` + - regex-style patterns + +Queue-health reference example: +- `dashboard:queue-health:kpi-.*` +- `dashboard:queue-health:chart-queue-.*` +- `dashboard:queue-health:chart-processing-latency-trend` + +What this means: +- all KPI widgets are authorized by `kpi-.*` +- queue-oriented chart widgets such as `chart-queue-wise-failures`, `chart-queue-wise-avg-elapsed`, and `chart-queue-sla-heatmap` are authorized by `chart-queue-.*` +- `chart-processing-latency-trend` is authorized explicitly +- widgets like `chart-stage-distribution`, `chart-messages-over-time`, and `table-recent-failures` remain unauthorized unless extra permissions are granted + +Important: +- dashboard developers must declare these permissions explicitly in module metadata +- they must then assign them to roles +- users receive them through standard SolidX RBAC + --- ## 6. Frontend Architecture (`solid-core-ui`) @@ -308,7 +340,19 @@ Default registered dashboard widgets: - `DefaultDashboardTableWidget` - `DefaultDashboardUnknownWidget` -### 6.6 Chart abstraction +### 6.6 Unauthorized widget rendering + +Authorization is enforced on the backend first and reflected in the frontend second. + +Current behavior: +- if the active user lacks widget permission, the backend does not invoke the widget provider at all +- the backend returns a normalized unauthorized envelope +- the frontend still renders the widget card in the dashboard grid +- the widget body shows a compact centered `Unauthorized` state + +This preserves layout stability while ensuring protected widget data never leaves the server. + +### 6.7 Chart abstraction - baseline chart engine: Apache ECharts - mapper lives under: @@ -405,6 +449,7 @@ Variables: Widgets include: - KPIs (total, succeeded, failed, in-flight, success rate, avg elapsed) - Charts (time series, stage distribution, queue failures, queue avg elapsed, latency trend) +- Custom chart (Queue SLA Heatmap) - Table (recent failures) This RI is the baseline for future dashboards and for custom widget extension examples. @@ -421,6 +466,11 @@ Heatmap provider response contract: - optional `pointDetails` - optional `legendThresholds` +Queue-health permission example used for validating the authorization flow: +- `dashboard:queue-health:kpi-.*` +- `dashboard:queue-health:chart-queue-.*` +- `dashboard:queue-health:chart-processing-latency-trend` + --- ## 9. Troubleshooting Guide @@ -460,6 +510,9 @@ Checklist: - Dashboard runtime endpoints require JWT - verify the authenticated user can access the module and dashboard route in the consuming app +- verify explicit dashboard widget permissions are declared in metadata +- verify those permissions are assigned to the correct roles +- remember that unauthorized widgets still render their card, but the provider will not run and the body will show `Unauthorized` --- From ad7ba59752431debad5ddd2d7397f0d167dc4d28 Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Tue, 2 Jun 2026 12:17:19 +0100 Subject: [PATCH 082/136] feat(dashboard): simplify queue health permissions by consolidating patterns --- src/seeders/seed-data/solid-core-metadata.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/seeders/seed-data/solid-core-metadata.json b/src/seeders/seed-data/solid-core-metadata.json index 6d427895..9803da89 100755 --- a/src/seeders/seed-data/solid-core-metadata.json +++ b/src/seeders/seed-data/solid-core-metadata.json @@ -5506,9 +5506,7 @@ "mcp:invoke", "agent:invoke", "settings:view_encrypted", - "dashboard:queue-health:kpi-.*", - "dashboard:queue-health:chart-queue-.*", - "dashboard:queue-health:chart-processing-latency-trend" + "dashboard:queue-health:*" ], "roles": [ { From 9aac2d5605e880fc19bfaf2c839f803641ca22b4 Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Tue, 2 Jun 2026 16:50:33 +0530 Subject: [PATCH 083/136] 0.1.10-beta.23 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 70e7a539..28eeb573 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.22", + "version": "0.1.10-beta.23", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@solidxai/core", - "version": "0.1.10-beta.22", + "version": "0.1.10-beta.23", "license": "BUSL-1.1", "dependencies": { "@aws-sdk/client-s3": "^3.637.0", diff --git a/package.json b/package.json index 543a8066..a8ce7a34 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.22", + "version": "0.1.10-beta.23", "description": "This module is a NestJS module containing all the required core providers required by a Solid application", "main": "dist/index.js", "types": "dist/index.d.ts", From 1d332a1d69d28d3063cd26a95a980775f9a03e04 Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Wed, 3 Jun 2026 13:49:23 +0530 Subject: [PATCH 084/136] changes to cleanup legacy table related fields --- src/dtos/create-model-metadata.dto.ts | 14 +++++------- src/entities/legacy-common-with-id.entity.ts | 4 ++-- src/entities/legacy-common.entity.ts | 2 +- src/entities/model-metadata.entity.ts | 8 +++---- src/enums/legacy-table-type.enum.ts | 5 +++++ src/helpers/model-metadata-helper.service.ts | 7 +++--- src/index.ts | 1 + src/seeders/system-fields-seeder.service.ts | 2 +- .../file-s3-storage-provider.ts | 22 +++++++++---------- src/services/model-metadata.service.ts | 3 +-- src/subscribers/model-metadata.subscriber.ts | 4 +--- 11 files changed, 35 insertions(+), 37 deletions(-) create mode 100644 src/enums/legacy-table-type.enum.ts diff --git a/src/dtos/create-model-metadata.dto.ts b/src/dtos/create-model-metadata.dto.ts index 4acc9602..04487186 100755 --- a/src/dtos/create-model-metadata.dto.ts +++ b/src/dtos/create-model-metadata.dto.ts @@ -1,6 +1,7 @@ import { ApiProperty } from "@nestjs/swagger"; import { Type } from "class-transformer"; -import { IsArray, IsBoolean, IsInt, IsOptional, IsString, Matches, ValidateNested } from "class-validator"; +import { IsArray, IsBoolean, IsEnum, IsInt, IsOptional, IsString, Matches, ValidateNested } from "class-validator"; +import { LegacyTableType } from "src/enums/legacy-table-type.enum"; import { CreateFieldMetadataDto } from "./create-field-metadata.dto"; import { IsNotInEnum } from "src/decorators/is-not-in-enum.decorator"; import { RESERVED_SOLID_KEYWORDS } from "src/helpers/solid-registry"; @@ -113,13 +114,8 @@ export class CreateModelMetadataDto { @IsOptional() parentModelUserKey: string; - @ApiProperty({ description: 'Is legacy table' }) - @IsBoolean() - @IsOptional() - isLegacyTable?: boolean; - - @ApiProperty({ description: 'Is legacy table with id' }) - @IsBoolean() + @ApiProperty({ enum: LegacyTableType, description: 'Legacy table ID strategy' }) + @IsEnum(LegacyTableType) @IsOptional() - isLegacyTableWithId?: boolean; + legacyTableType?: LegacyTableType; } diff --git a/src/entities/legacy-common-with-id.entity.ts b/src/entities/legacy-common-with-id.entity.ts index 402b9708..d4fe6864 100644 --- a/src/entities/legacy-common-with-id.entity.ts +++ b/src/entities/legacy-common-with-id.entity.ts @@ -1,9 +1,9 @@ import { Exclude, Expose } from "class-transformer"; import { Column, Generated } from "typeorm"; -import { LEGACY_TABLE_FIELDS_PREFIX, LegacyCommonEntity } from "./legacy-common.entity"; +import { LEGACY_TABLE_FIELDS_PREFIX, LegacyCommonEntityWithExistingId } from "./legacy-common.entity"; @Exclude() -export abstract class LegacyCommonWithIdEntity extends LegacyCommonEntity { +export abstract class LegacyCommonEntityWithGeneratedId extends LegacyCommonEntityWithExistingId { @Expose() @Column({ type: 'integer', name: `${LEGACY_TABLE_FIELDS_PREFIX}_id`, unique: true }) @Generated("increment") diff --git a/src/entities/legacy-common.entity.ts b/src/entities/legacy-common.entity.ts index 6ba2f8d8..85acb0c1 100644 --- a/src/entities/legacy-common.entity.ts +++ b/src/entities/legacy-common.entity.ts @@ -5,7 +5,7 @@ import { Column, CreateDateColumn, DeleteDateColumn, Index, UpdateDateColumn } f export const LEGACY_TABLE_FIELDS_PREFIX = 'ss'; @Exclude() -export abstract class LegacyCommonEntity { +export abstract class LegacyCommonEntityWithExistingId { // @Expose() // @Column({ type: 'integer', name: `${LEGACY_TABLE_FIELDS_PREFIX}_id`, unique: true }) // @Generated("increment") diff --git a/src/entities/model-metadata.entity.ts b/src/entities/model-metadata.entity.ts index bb3e5309..b69638ff 100755 --- a/src/entities/model-metadata.entity.ts +++ b/src/entities/model-metadata.entity.ts @@ -1,4 +1,5 @@ import { CommonEntity } from "src/entities/common.entity"; +import { LegacyTableType } from "src/enums/legacy-table-type.enum"; import { Column, Entity, Index, JoinColumn, ManyToOne, OneToMany } from "typeorm"; import { FieldMetadata } from "./field-metadata.entity"; import { ModuleMetadata } from "./module-metadata.entity"; @@ -66,10 +67,7 @@ export class ModelMetadata extends CommonEntity { @ManyToOne(() => ModelMetadata, {}) parentModel: ModelMetadata; - @Column({ default: false }) - isLegacyTable: boolean; - - @Column({ default: false }) - isLegacyTableWithId: boolean; + @Column({ type: 'varchar', default: LegacyTableType.NONE }) + legacyTableType: LegacyTableType; } diff --git a/src/enums/legacy-table-type.enum.ts b/src/enums/legacy-table-type.enum.ts new file mode 100644 index 00000000..112c8d48 --- /dev/null +++ b/src/enums/legacy-table-type.enum.ts @@ -0,0 +1,5 @@ +export enum LegacyTableType { + NONE = 'none', + EXISTING_ID = 'existing_id', + GENERATED_ID = 'generated_id', +} diff --git a/src/helpers/model-metadata-helper.service.ts b/src/helpers/model-metadata-helper.service.ts index 3c88becf..63526926 100644 --- a/src/helpers/model-metadata-helper.service.ts +++ b/src/helpers/model-metadata-helper.service.ts @@ -3,6 +3,7 @@ import { forwardRef, Inject, Injectable, Logger } from "@nestjs/common"; import { _ } from "lodash"; import { LEGACY_TABLE_FIELDS_PREFIX } from "src/entities/legacy-common.entity"; +import { LegacyTableType } from "src/enums/legacy-table-type.enum"; import { ModelMetadataRepository } from "src/repository/model-metadata.repository"; import { SolidRegistry } from "./solid-registry"; @@ -19,12 +20,12 @@ export class ModelMetadataHelperService { ) { } - getSystemFieldsMetadata(isLegacyTable: boolean=false, isLegacyTableWithId: boolean=false): any[] { + getSystemFieldsMetadata(legacyTableType: LegacyTableType = LegacyTableType.NONE): any[] { let systemFieldsMetadata: any[]; - if (isLegacyTableWithId) { + if (legacyTableType === LegacyTableType.GENERATED_ID) { systemFieldsMetadata = this.getSystemFieldsMetadataMappingForLegacyTable(true); } - else if (isLegacyTable) { + else if (legacyTableType === LegacyTableType.EXISTING_ID) { systemFieldsMetadata = this.getSystemFieldsMetadataMappingForLegacyTable(false); } else { diff --git a/src/index.ts b/src/index.ts index 28889bab..4cf4a4fd 100755 --- a/src/index.ts +++ b/src/index.ts @@ -157,6 +157,7 @@ export * from './entities/dashboard-user-layout.entity' export * from './entities/user-api-key.entity' export * from './enums/auth-type.enum' +export * from './enums/legacy-table-type.enum' export * from './decorators/disallow-in-production.decorator' export * from './filters/http-exception.filter' diff --git a/src/seeders/system-fields-seeder.service.ts b/src/seeders/system-fields-seeder.service.ts index 4c75d311..3e55259b 100644 --- a/src/seeders/system-fields-seeder.service.ts +++ b/src/seeders/system-fields-seeder.service.ts @@ -37,7 +37,7 @@ export class SystemFieldsSeederService { private async seedMissingSystemFields(model: ModelMetadata) { this.logger.debug(`Checking system fields for model: ${model.singularName}`); const existingSystemFields = model.fields.filter(field => field.isSystem); - const systemFieldsMetadata = this.modelHelperService.getSystemFieldsMetadata(model.isLegacyTable, model.isLegacyTableWithId); + const systemFieldsMetadata = this.modelHelperService.getSystemFieldsMetadata(model.legacyTableType); this.logger.debug(`Model: ${model.singularName} has ${existingSystemFields.length} existing system fields.`); // Find out which system fields are missing const missingFields = systemFieldsMetadata.filter( diff --git a/src/services/mediaStorageProviders/file-s3-storage-provider.ts b/src/services/mediaStorageProviders/file-s3-storage-provider.ts index 4e1da188..cba2343f 100755 --- a/src/services/mediaStorageProviders/file-s3-storage-provider.ts +++ b/src/services/mediaStorageProviders/file-s3-storage-provider.ts @@ -2,8 +2,8 @@ import { Injectable, Logger } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { CommonEntity } from "src/entities/common.entity"; import { FieldMetadata } from "src/entities/field-metadata.entity"; -import { LegacyCommonEntity } from "src/entities/legacy-common.entity"; -import { LegacyCommonWithIdEntity } from "src/entities/legacy-common-with-id.entity"; +import { LegacyCommonEntityWithExistingId } from "src/entities/legacy-common.entity"; +import { LegacyCommonEntityWithGeneratedId } from "src/entities/legacy-common-with-id.entity"; import { Media } from "src/entities/media.entity"; import { MediaStorageProvider } from "src/interfaces"; import { DiskFileService, S3FileService } from "src/services/file"; @@ -27,10 +27,10 @@ export class FileS3StorageProvider implements MediaStorageProvider { async retrieve(entity: T, mediaFieldMetadata: FieldMetadata): Promise { const isSupportedEntity = entity instanceof CommonEntity - || entity instanceof LegacyCommonEntity - || entity instanceof LegacyCommonWithIdEntity; + || entity instanceof LegacyCommonEntityWithExistingId + || entity instanceof LegacyCommonEntityWithGeneratedId; if (!isSupportedEntity) { - throw new Error("Entity must be an instance of CommonEntity, LegacyCommonEntity or LegacyCommonWithIdEntity"); // FIXME This needs to be handled through generics. e.g T extends CommonEntity + throw new Error("Entity must be an instance of CommonEntity, LegacyCommonEntityWithExistingId or LegacyCommonEntityWithGeneratedId"); // FIXME This needs to be handled through generics. e.g T extends CommonEntity } // @ts-ignore const media = await this.mediaRepository.findByEntityIdAndFieldIdAndModelMetadataId(entity.id, mediaFieldMetadata.id, mediaFieldMetadata.model.id, ['mediaStorageProviderMetadata']); @@ -60,10 +60,10 @@ export class FileS3StorageProvider implements MediaStorageProvider { async store(files: Express.Multer.File[], entity: T, mediaFieldMetadata: FieldMetadata): Promise { const isSupportedEntity = entity instanceof CommonEntity - || entity instanceof LegacyCommonEntity - || entity instanceof LegacyCommonWithIdEntity; + || entity instanceof LegacyCommonEntityWithExistingId + || entity instanceof LegacyCommonEntityWithGeneratedId; if (!isSupportedEntity) { - throw new Error("Entity must be an instance of CommonEntity, LegacyCommonEntity or LegacyCommonWithIdEntity"); // FIXME This needs to be handled through generics. e.g T extends CommonEntity + throw new Error("Entity must be an instance of CommonEntity, LegacyCommonEntityWithExistingId or LegacyCommonEntityWithGeneratedId"); // FIXME This needs to be handled through generics. e.g T extends CommonEntity } const result: Media[] = []; const storageProvider = mediaFieldMetadata.mediaStorageProvider; @@ -102,10 +102,10 @@ export class FileS3StorageProvider implements MediaStorageProvider { async delete(entity: T, mediaFieldMetadata: FieldMetadata): Promise { const isSupportedEntity = entity instanceof CommonEntity - || entity instanceof LegacyCommonEntity - || entity instanceof LegacyCommonWithIdEntity; + || entity instanceof LegacyCommonEntityWithExistingId + || entity instanceof LegacyCommonEntityWithGeneratedId; if (!isSupportedEntity) { - throw new Error("Entity must be an instance of CommonEntity, LegacyCommonEntity or LegacyCommonWithIdEntity"); // FIXME This needs to be handled through generics. e.g T extends CommonEntity + throw new Error("Entity must be an instance of CommonEntity, LegacyCommonEntityWithExistingId or LegacyCommonEntityWithGeneratedId"); // FIXME This needs to be handled through generics. e.g T extends CommonEntity } const storageProvider = mediaFieldMetadata.mediaStorageProvider; const region = this.getEffectiveRegion(storageProvider.region); diff --git a/src/services/model-metadata.service.ts b/src/services/model-metadata.service.ts index b0fb0b1d..c8f0f76f 100755 --- a/src/services/model-metadata.service.ts +++ b/src/services/model-metadata.service.ts @@ -288,8 +288,7 @@ export class ModelMetadataService { tableName: model.tableName, userKeyFieldUserKey: model.fields.find(field => field.isUserKey)?.name, isChild: model?.isChild, - isLegacyTable: model?.isLegacyTable, - isLegacyTableWithId: model?.isLegacyTableWithId, + legacyTableType: model?.legacyTableType, parentModelUserKey: model?.parentModel?.singularName, enableAuditTracking: model?.enableAuditTracking, enableSoftDelete: model?.enableSoftDelete, diff --git a/src/subscribers/model-metadata.subscriber.ts b/src/subscribers/model-metadata.subscriber.ts index 2cf6f092..3c45717b 100755 --- a/src/subscribers/model-metadata.subscriber.ts +++ b/src/subscribers/model-metadata.subscriber.ts @@ -35,9 +35,7 @@ export class ModelMetadataSubscriber implements EntitySubscriberInterface) { - const isLegacyTable = event.entity.isLegacyTable; - const isLegacyTableWithId = event.entity.isLegacyTableWithId; - const systemFieldsDefaultMetadata = this.modelHelperService.getSystemFieldsMetadata(isLegacyTable, isLegacyTableWithId); + const systemFieldsDefaultMetadata = this.modelHelperService.getSystemFieldsMetadata(event.entity.legacyTableType); // map and add the model as event.entity for the above metadata const systemFieldsMetadata = systemFieldsDefaultMetadata.map(field => ({ ...field, From b6c9d4c7bd5ba6f9473ea5b78db24408e5a8a600 Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Wed, 3 Jun 2026 13:59:41 +0100 Subject: [PATCH 085/136] feat(dashboard): add new dashboard controller actions to permissions --- src/seeders/seed-data/solid-core-metadata.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/seeders/seed-data/solid-core-metadata.json b/src/seeders/seed-data/solid-core-metadata.json index 9803da89..a56cfdee 100755 --- a/src/seeders/seed-data/solid-core-metadata.json +++ b/src/seeders/seed-data/solid-core-metadata.json @@ -5569,6 +5569,12 @@ "AgentEventController.findOne", "McpAuditLogController.findMany", "McpAuditLogController.findOne", + "DashboardController.getDefinition", + "DashboardController.getWidgetData", + "DashboardController.getDashboardData", + "DashboardController.getVariableOptions", + "DashboardController.getLayout", + "DashboardController.saveLayout", "mcp:invoke", "agent:invoke" ] From 64884a2a6665fea3b2191020c1a41b7c82266aac Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Thu, 4 Jun 2026 10:24:28 +0530 Subject: [PATCH 086/136] minor cleanup --- src/seeders/seed-data/solid-core-metadata.json | 4 ++-- src/services/model-metadata.service.ts | 11 +++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/seeders/seed-data/solid-core-metadata.json b/src/seeders/seed-data/solid-core-metadata.json index a56cfdee..3f716304 100755 --- a/src/seeders/seed-data/solid-core-metadata.json +++ b/src/seeders/seed-data/solid-core-metadata.json @@ -6148,7 +6148,7 @@ }, { "displayName": "Queue Health", - "name": "solid-core-queue-health-dashboard-view", + "name": "solid-core-queue-health-dashboard-action", "type": "custom", "domain": "", "context": "", @@ -6348,7 +6348,7 @@ "displayName": "Queue Health", "name": "solid-core-queue-health-dashboard-menu-item", "sequenceNumber": 1, - "actionUserKey": "solid-core-queue-health-dashboard-view", + "actionUserKey": "solid-core-queue-health-dashboard-action", "moduleUserKey": "solid-core", "parentMenuItemUserKey": "queues-menu-item" }, diff --git a/src/services/model-metadata.service.ts b/src/services/model-metadata.service.ts index c8f0f76f..4971628c 100755 --- a/src/services/model-metadata.service.ts +++ b/src/services/model-metadata.service.ts @@ -2,17 +2,16 @@ import { BadRequestException, forwardRef, Inject, Injectable, Logger, NotFoundEx import { InjectDataSource } from '@nestjs/typeorm'; import * as fs from 'fs/promises'; // Use the Promise-based version of fs for async/await import * as path from 'path'; -import { DataSource, EntityManager, In, Repository, SelectQueryBuilder } from 'typeorm'; +import { DataSource, EntityManager, Repository, SelectQueryBuilder } from 'typeorm'; import { CreateModelMetadataDto } from '../dtos/create-model-metadata.dto'; import { ModelMetadata } from '../entities/model-metadata.entity'; import { ModuleMetadata } from '../entities/module-metadata.entity'; import { kebabCase } from 'lodash'; -import { classify } from '../helpers/string.helper'; import { ERROR_MESSAGES } from 'src/constants/error-messages'; import { DisallowInProduction } from 'src/decorators/disallow-in-production.decorator'; import { SolidFieldType } from 'src/dtos/create-field-metadata.dto'; -import { PermissionMetadata } from 'src/entities/permission-metadata.entity'; +import { NavigationDto } from 'src/dtos/navigation.dto'; import { ModuleMetadataHelperService } from 'src/helpers/module-metadata-helper.service'; import { FieldMetadataRepository } from 'src/repository/field-metadata.repository'; import { ModelMetadataRepository } from 'src/repository/model-metadata.repository'; @@ -22,20 +21,20 @@ import { ActionMetadata } from '../entities/action-metadata.entity'; import { FieldMetadata } from '../entities/field-metadata.entity'; import { MenuItemMetadata } from '../entities/menu-item-metadata.entity'; import { ViewMetadata } from '../entities/view-metadata.entity'; +import { CommandService } from '../helpers/command.service'; import { REFRESH_MODEL_COMMAND, REMOVE_FIELDS_COMMAND, SchematicService } from '../helpers/schematic.service'; -import { CommandService } from '../helpers/command.service'; +import { classify } from '../helpers/string.helper'; import { CodeGenerationOptions } from '../interfaces'; import { CrudHelperService } from './crud-helper.service'; +import { CRUDService } from './crud.service'; import { FieldMetadataService } from './field-metadata.service'; import { MediaStorageProviderMetadataService } from './media-storage-provider-metadata.service'; import { RoleMetadataService } from './role-metadata.service'; -import { NavigationDto } from 'src/dtos/navigation.dto'; import { SolidIntrospectService } from './solid-introspect.service'; -import { CRUDService } from './crud.service'; import { SolidTsMorphService } from './solid-ts-morph.service'; @Injectable() From c694a0c6393ff9f6795f3653bd6223b8807191ca Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Thu, 4 Jun 2026 10:25:23 +0530 Subject: [PATCH 087/136] 0.1.10-beta.24 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 28eeb573..96828a3f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.23", + "version": "0.1.10-beta.24", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@solidxai/core", - "version": "0.1.10-beta.23", + "version": "0.1.10-beta.24", "license": "BUSL-1.1", "dependencies": { "@aws-sdk/client-s3": "^3.637.0", diff --git a/package.json b/package.json index a8ce7a34..44b507f6 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.23", + "version": "0.1.10-beta.24", "description": "This module is a NestJS module containing all the required core providers required by a Solid application", "main": "dist/index.js", "types": "dist/index.d.ts", From fb3da0c59fbe82073a34018cdc32ba23e9d5d124 Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Thu, 4 Jun 2026 16:56:07 +0530 Subject: [PATCH 088/136] added frontend base url as well to the default settings --- .../settings/default-settings-provider.service.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/services/settings/default-settings-provider.service.ts b/src/services/settings/default-settings-provider.service.ts index bdb7d4f3..99728e7e 100644 --- a/src/services/settings/default-settings-provider.service.ts +++ b/src/services/settings/default-settings-provider.service.ts @@ -363,6 +363,16 @@ const getSolidCoreSettings = (isProd: boolean) => group: "system-settings", sortOrder: 30, controlType: "shortText", + }, + { + moduleName: "solid-core", + key: "frontendAdminBaseUrl", + value: process.env.FRONTEND_BASE_URL, + level: SettingLevel.SystemAdminReadonly, + label: "Frontend Admin Base URL", + group: "system-settings", + sortOrder: 35, + controlType: "shortText", }, { moduleName: "solid-core", From fa1b0ef70dfa73db9d9e336cb692b55df9628063 Mon Sep 17 00:00:00 2001 From: Rajesh Chityal Date: Fri, 5 Jun 2026 11:46:44 +0530 Subject: [PATCH 089/136] Bug fixs --- src/services/crud-helper.service.ts | 8 +++++++- src/services/crud.service.ts | 30 ++++++++++++++--------------- 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/services/crud-helper.service.ts b/src/services/crud-helper.service.ts index f1676de2..723a76a3 100755 --- a/src/services/crud-helper.service.ts +++ b/src/services/crud-helper.service.ts @@ -282,7 +282,13 @@ export class CrudHelperService { const orderOptionKeys = Object.keys(orderOptions) as Array; orderOptionKeys.forEach((key) => { const value = orderOptions[key] as 'ASC' | 'DESC'; - qb.addOrderBy(`${entityAlias}.${key}`, value); + const field = String(key); + if (field.includes('.')) { + const { alias, property } = this.ensureRelationPathJoined(qb, entityAlias, field.split('.')); + qb.addOrderBy(`${alias}.${property}`, value); + } else { + qb.addOrderBy(`${entityAlias}.${field}`, value); + } }); } } diff --git a/src/services/crud.service.ts b/src/services/crud.service.ts index d5af26ab..30c452bf 100755 --- a/src/services/crud.service.ts +++ b/src/services/crud.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, InternalServerErrorException, NotFoundException } from "@nestjs/common"; +import { BadRequestException, ConflictException, HttpException, InternalServerErrorException, NotFoundException } from "@nestjs/common"; import { DiscoveryService, ModuleRef } from "@nestjs/core"; import { isArray } from "class-validator"; import { CommonEntity } from "../entities/common.entity"; @@ -911,7 +911,7 @@ export class CRUDService { // Add two generic value i.e }); if (!softDeletedRows) { - throw new Error(ERROR_MESSAGES.NO_SOFT_DELETED_RECORD_FOUND); + throw new NotFoundException(ERROR_MESSAGES.NO_SOFT_DELETED_RECORD_FOUND); } await this.repo.update(id, { @@ -921,13 +921,13 @@ export class CRUDService { // Add two generic value i.e return { message: SUCCESS_MESSAGES.RECORD_RECOVERED, data: softDeletedRows }; } catch (error: any) { - if (error instanceof QueryFailedError) { - if ((error as any).code === '23505') { - throw new Error(ERROR_MESSAGES.CONFLICTING_RECORD_ON_UNARCHIVE); - } + if (error instanceof QueryFailedError && (error as any).code === '23505') { + throw new ConflictException(ERROR_MESSAGES.CONFLICTING_RECORD_ON_UNARCHIVE); } - - throw new Error(error); + if (error instanceof HttpException) { + throw error; + } + throw new InternalServerErrorException(error?.message ?? String(error)); } } @@ -956,7 +956,7 @@ export class CRUDService { // Add two generic value i.e }); if (softDeletedRows.length === 0) { - throw new Error(ERROR_MESSAGES.NO_SOFT_DELETED_RECORDS_FOUND); + throw new NotFoundException(ERROR_MESSAGES.NO_SOFT_DELETED_RECORDS_FOUND); } // Recover the specific records by setting deletedAt to null @@ -967,13 +967,13 @@ export class CRUDService { // Add two generic value i.e return { message: SUCCESS_MESSAGES.SELECTED_RECORDS_RECOVERED, recoveredIds: ids }; } catch (error: any) { - if (error instanceof QueryFailedError) { - if ((error as any).code === "23505") { - throw new Error(ERROR_MESSAGES.CONFLICTING_RECORD_ON_UNARCHIVE); - } + if (error instanceof QueryFailedError && (error as any).code === "23505") { + throw new ConflictException(ERROR_MESSAGES.CONFLICTING_RECORD_ON_UNARCHIVE); } - - throw new Error(error); + if (error instanceof HttpException) { + throw error; + } + throw new InternalServerErrorException(error?.message ?? String(error)); } } From b11aacc17ada42e3303275c65b3ded6a35a9f3a1 Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Fri, 5 Jun 2026 18:10:28 +0530 Subject: [PATCH 090/136] 0.1.10-beta.25 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 96828a3f..cc8c9cb3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.24", + "version": "0.1.10-beta.25", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@solidxai/core", - "version": "0.1.10-beta.24", + "version": "0.1.10-beta.25", "license": "BUSL-1.1", "dependencies": { "@aws-sdk/client-s3": "^3.637.0", diff --git a/package.json b/package.json index 44b507f6..d71daf96 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.24", + "version": "0.1.10-beta.25", "description": "This module is a NestJS module containing all the required core providers required by a Solid application", "main": "dist/index.js", "types": "dist/index.d.ts", From d7f76b802557ada9912523ed3d7aa27bbbf93c40 Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Fri, 5 Jun 2026 19:54:07 +0100 Subject: [PATCH 091/136] Refactor module metadata paths to use a consistent directory structure and improve file resolution logic --- src/helpers/module-metadata-helper.service.ts | 33 +++++++------------ src/helpers/module.helper.ts | 21 +++++++----- src/seeders/module-metadata-seeder.service.ts | 6 ++-- src/seeders/module-test-data.service.ts | 2 +- src/services/dashboard-providers/README.md | 2 +- src/services/genai/ingest-metadata.service.ts | 6 ++-- src/services/model-metadata.service.ts | 2 +- src/services/module-metadata.service.ts | 3 +- 8 files changed, 33 insertions(+), 42 deletions(-) diff --git a/src/helpers/module-metadata-helper.service.ts b/src/helpers/module-metadata-helper.service.ts index ec59de19..11eef721 100644 --- a/src/helpers/module-metadata-helper.service.ts +++ b/src/helpers/module-metadata-helper.service.ts @@ -37,21 +37,21 @@ export class ModuleMetadataHelperService { return path.resolve(process.cwd(), 'src', moduleName); } + private resolveModuleMetadataFolderPath(moduleName: string): string { + const dashModuleName = kebabCase(moduleName); + return path.resolve(process.cwd(), 'src', dashModuleName, 'metadata'); + } + async getModuleMetadataFilePath(moduleName: string): Promise { if (!moduleName) { return ''; } const dashModuleName = kebabCase(moduleName); - const folderPath = path.resolve(process.cwd(), 'module-metadata', dashModuleName); - const filePath = path.join(folderPath, `${dashModuleName}-metadata.json`); - - // Check if filePath exists - const fileExists = await this.fileService.exists(filePath); - if (fileExists) { - return filePath; - } + const filePath = path.join( + this.resolveModuleMetadataFolderPath(dashModuleName), + `${dashModuleName}-metadata.json`, + ); - // If the module is solid-core, try the fallback path, in case the current root directory is the solid core project if (dashModuleName === SOLID_CORE_MODULE_NAME) { const fallbackPath = path.resolve(process.cwd(), 'src', 'seeders', 'seed-data', `${dashModuleName}-metadata.json`); const fallbackExists = await this.fileService.exists(fallbackPath); @@ -68,25 +68,14 @@ export class ModuleMetadataHelperService { } } - this.logger.error(`Module metadata file not found for module: ${moduleName} at path: ${filePath}`); - return ''; + return filePath; } async getModuleMetadataFolderPath(moduleName: string): Promise { if (!moduleName) { return ''; } - - const dashModuleName = kebabCase(moduleName); - - const folderPath = path.resolve( - process.cwd(), - 'module-metadata', - dashModuleName, - ); - - const exists = await this.fileService.exists(folderPath); - return exists ? folderPath : ''; + return this.resolveModuleMetadataFolderPath(moduleName); } } diff --git a/src/helpers/module.helper.ts b/src/helpers/module.helper.ts index 48e40fc4..bc39103e 100755 --- a/src/helpers/module.helper.ts +++ b/src/helpers/module.helper.ts @@ -38,26 +38,29 @@ export const getDynamicModuleNames = (): string[] => { export const getDynamicModuleNamesBasedOnMetadata = (): string[] => { const dynamicModulesToExclude = process.env.SOLID_DYNAMIC_MODULES_TO_EXCLUDE?.split(',') || []; - // Find a path that is ../${srcPath}/module-metadata save it in a variable. - // const srcPath = path.join(process.cwd(), 'src'); - const moduleMetadataPath = path.join(process.cwd(), 'module-metadata'); + const srcPath = path.join(process.cwd(), 'src'); const coreModuleNames = getCoreModuleNames(); const allExcludedModules = [...new Set([...coreModuleNames, ...dynamicModulesToExclude])]; - // if module-metadata path does not exist, return empty array - if (!fs.existsSync(moduleMetadataPath)) { - logger.warn(`Module metadata path does not exist: ${moduleMetadataPath}`); + if (!fs.existsSync(srcPath)) { + logger.warn(`Source path does not exist: ${srcPath}`); return []; } - const moduleMetadataDirectories = fs.readdirSync(moduleMetadataPath, { withFileTypes: true }); - const enabledModules = moduleMetadataDirectories + const moduleDirectories = fs.readdirSync(srcPath, { withFileTypes: true }); + const enabledModules = moduleDirectories .filter(dirent => { const isValidDirectory = dirent.isDirectory() && !allExcludedModules.includes(dirent.name); if (!isValidDirectory) return false; - const fullPath = path.join(moduleMetadataPath, dirent.name, `${dirent.name}-metadata.json`); + const moduleManifestPath = path.join(srcPath, dirent.name, `${dirent.name}.module.ts`); + const moduleManifestStats = fs.statSync(moduleManifestPath, { throwIfNoEntry: false }); + if (!moduleManifestStats || !moduleManifestStats.isFile()) { + return false; + } + + const fullPath = path.join(srcPath, dirent.name, 'metadata', `${dirent.name}-metadata.json`); const stats = fs.statSync(fullPath, { throwIfNoEntry: false }); return !!stats && stats.isFile(); diff --git a/src/seeders/module-metadata-seeder.service.ts b/src/seeders/module-metadata-seeder.service.ts index be5c73fa..32249786 100755 --- a/src/seeders/module-metadata-seeder.service.ts +++ b/src/seeders/module-metadata-seeder.service.ts @@ -388,7 +388,7 @@ export class ModuleMetadataSeederService { // const enabledModules = getDynamicModuleNames(); const enabledModules = getDynamicModuleNamesBasedOnMetadata(); for (const enabledModule of enabledModules) { - const enabledModuleSeedFile = `module-metadata/${enabledModule}/${enabledModule}-metadata.json`; + const enabledModuleSeedFile = `src/${enabledModule}/metadata/${enabledModule}-metadata.json`; const fullPath = path.join(process.cwd(), enabledModuleSeedFile); if (fs.existsSync(fullPath)) { @@ -558,7 +558,7 @@ export class ModuleMetadataSeederService { } else { // Check if file exists - const emailTemplateHandlebar = `module-metadata/${moduleName}/email-templates/${emailTemplate.body}` + const emailTemplateHandlebar = `src/${moduleName}/metadata/email-templates/${emailTemplate.body}` const fullPath = path.join(process.cwd(), emailTemplateHandlebar); // this.logger.log(`Seeding custom email template from consuming model at path: ${fullPath}`); if (fs.existsSync(fullPath)) { @@ -624,7 +624,7 @@ export class ModuleMetadataSeederService { } else { // Check if file exists - const emailTemplateHandlebar = `module-metadata/${moduleName}/sms-templates/${smsTemplate.body}` + const emailTemplateHandlebar = `src/${moduleName}/metadata/sms-templates/${smsTemplate.body}` const fullPath = path.join(process.cwd(), emailTemplateHandlebar); // this.logger.log(`Seeding custom sms template from consuming model at path: ${fullPath}`); if (fs.existsSync(fullPath)) { diff --git a/src/seeders/module-test-data.service.ts b/src/seeders/module-test-data.service.ts index 14e2ff00..b697d4bc 100644 --- a/src/seeders/module-test-data.service.ts +++ b/src/seeders/module-test-data.service.ts @@ -196,7 +196,7 @@ export class ModuleTestDataService { const testDataFiles = [typedSolidCoreMetadata]; const enabledModules = getDynamicModuleNamesBasedOnMetadata(); for (const enabledModule of enabledModules) { - const enabledModuleSeedFile = `module-metadata/${enabledModule}/${enabledModule}-metadata.json`; + const enabledModuleSeedFile = `src/${enabledModule}/metadata/${enabledModule}-metadata.json`; const fullPath = path.join(process.cwd(), enabledModuleSeedFile); if (fs.existsSync(fullPath)) { diff --git a/src/services/dashboard-providers/README.md b/src/services/dashboard-providers/README.md index e29dc760..9b65735b 100644 --- a/src/services/dashboard-providers/README.md +++ b/src/services/dashboard-providers/README.md @@ -38,7 +38,7 @@ Not persisted: At runtime, metadata is read from file paths resolved by `ModuleMetadataHelperService`: -1. `module-metadata//-metadata.json` +1. `src//metadata/-metadata.json` 2. fallback (solid-core local run): `src/seeders/seed-data/solid-core-metadata.json` 3. fallback (consuming project): `node_modules/@solidxai/core/src/seeders/seed-data/solid-core-metadata.json` diff --git a/src/services/genai/ingest-metadata.service.ts b/src/services/genai/ingest-metadata.service.ts index 3266f2a8..8f1b997c 100644 --- a/src/services/genai/ingest-metadata.service.ts +++ b/src/services/genai/ingest-metadata.service.ts @@ -97,14 +97,14 @@ export class IngestMetadataService { for (let i = 0; i < enabledModules.length; i++) { const enabledModule = enabledModules[i]; const fileName = `${enabledModule}-metadata.json`; - const enabledModuleSeedFile = `module-metadata/${enabledModule}/${fileName}`; + const enabledModuleSeedFile = `src/${enabledModule}/metadata/${fileName}`; const fullPath = path.join(process.cwd(), enabledModuleSeedFile); const overallMetadata: any = JSON.parse(fs.readFileSync(fullPath, 'utf-8').toString()); const moduleMetadata: CreateModuleMetadataDto = overallMetadata.moduleMetadata; // Manage all the ingestion info file paths... - const enabledModulIngestionInfoFile = `module-metadata/${enabledModule}/genai/${enabledModule}-ingested-info.json`; + const enabledModulIngestionInfoFile = `src/${enabledModule}/metadata/genai/${enabledModule}-ingested-info.json`; const enabledModulIngestionInfoFullPath = path.join(process.cwd(), enabledModulIngestionInfoFile); const ingestionInfo: ModuleRAGIngestionInfo = fs.existsSync(enabledModulIngestionInfoFullPath) ? JSON.parse(fs.readFileSync(enabledModulIngestionInfoFullPath, 'utf-8').toString()) : {}; const enabledModulIngestionInfoDir = path.dirname(enabledModulIngestionInfoFullPath); @@ -795,4 +795,4 @@ ${JSON.stringify(model)} this.logger.log(`[OK] Ingested field ${moduleName}.${modelName}.${fieldName} → id=${newId}, hash=${schemaHash.slice(0, 8)}…`); return newId; } -} \ No newline at end of file +} diff --git a/src/services/model-metadata.service.ts b/src/services/model-metadata.service.ts index 4971628c..aa3f44a1 100755 --- a/src/services/model-metadata.service.ts +++ b/src/services/model-metadata.service.ts @@ -740,7 +740,7 @@ export class ModelMetadataService { const model = await this.findOne(modelId); return this.commandService.executeCommandWithArgs({ command: 'npx', - args: ['@solixai/solidctl@latest', 'generate', 'model', `--name=${model.singularName}`], + args: ['@solidxai/solidctl@latest', 'generate', 'model', `--name=${model.singularName}`], cwd: path.join(process.cwd(), '..'), }); } diff --git a/src/services/module-metadata.service.ts b/src/services/module-metadata.service.ts index ad8c75ae..79827353 100755 --- a/src/services/module-metadata.service.ts +++ b/src/services/module-metadata.service.ts @@ -196,9 +196,8 @@ export class ModuleMetadataService { // Convert the object to JSON string const metadataJson = JSON.stringify(moduleMetaDataJson, null, 2); - // Create the folder path inside 'module-metadata' - const folderPath = path.resolve(process.cwd(), 'module-metadata', module.name); const filePath = await this.moduleMetadataHelperService.getModuleMetadataFilePath(module.name); + const folderPath = path.dirname(filePath); // Ensure the folder exists await fs.mkdir(folderPath, { recursive: true }); From 24b5e8b93d803ab115f176907a6aad336a9825a8 Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Sat, 6 Jun 2026 16:44:59 +0100 Subject: [PATCH 092/136] Implement module metadata explorer with CRUD operations and search functionality --- .../module-metadata-explorer.controller.ts | 83 +++ src/controllers/module-metadata.controller.ts | 6 + .../metadata-explorer-references-query.dto.ts | 39 + .../metadata-explorer-search-query.dto.ts | 28 + src/dtos/metadata-explorer-write.dto.ts | 11 + src/index.ts | 5 + src/seeders/module-metadata-seeder.service.ts | 9 +- .../module-metadata-explorer.service.ts | 670 ++++++++++++++++++ src/services/module-metadata.service.ts | 25 + src/solid-core.module.ts | 5 + 10 files changed, 879 insertions(+), 2 deletions(-) create mode 100644 src/controllers/module-metadata-explorer.controller.ts create mode 100644 src/dtos/metadata-explorer-references-query.dto.ts create mode 100644 src/dtos/metadata-explorer-search-query.dto.ts create mode 100644 src/dtos/metadata-explorer-write.dto.ts create mode 100644 src/services/module-metadata-explorer.service.ts diff --git a/src/controllers/module-metadata-explorer.controller.ts b/src/controllers/module-metadata-explorer.controller.ts new file mode 100644 index 00000000..4466320b --- /dev/null +++ b/src/controllers/module-metadata-explorer.controller.ts @@ -0,0 +1,83 @@ +import { Body, Controller, Get, Param, Post, Put, Query } from '@nestjs/common'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { MetadataExplorerReferencesQueryDto } from '../dtos/metadata-explorer-references-query.dto'; +import { MetadataExplorerSearchQueryDto } from '../dtos/metadata-explorer-search-query.dto'; +import { MetadataExplorerWriteDto } from '../dtos/metadata-explorer-write.dto'; +import { ModuleMetadataExplorerService } from '../services/module-metadata-explorer.service'; + +@ApiTags('Solid Core') +@ApiBearerAuth('jwt') +@Controller('module-metadata-explorer') +export class ModuleMetadataExplorerController { + constructor( + private readonly moduleMetadataExplorerService: ModuleMetadataExplorerService, + ) { } + + @Get(':moduleName/manifest') + async getManifest(@Param('moduleName') moduleName: string) { + return this.moduleMetadataExplorerService.getManifest(moduleName); + } + + @Get(':moduleName/document') + async getDocument(@Param('moduleName') moduleName: string) { + return this.moduleMetadataExplorerService.getDocument(moduleName); + } + + @Put(':moduleName/document') + async updateDocument( + @Param('moduleName') moduleName: string, + @Body() body: MetadataExplorerWriteDto, + ) { + return this.moduleMetadataExplorerService.updateDocument(moduleName, body); + } + + @Post(':moduleName/document/validate') + async validateDocument( + @Param('moduleName') moduleName: string, + @Body() body: MetadataExplorerWriteDto, + ) { + return this.moduleMetadataExplorerService.validateDocument(moduleName, body); + } + + @Get(':moduleName/sections/:sectionKey') + async getSection( + @Param('moduleName') moduleName: string, + @Param('sectionKey') sectionKey: string, + ) { + return this.moduleMetadataExplorerService.getSection(moduleName, sectionKey); + } + + @Put(':moduleName/sections/:sectionKey') + async updateSection( + @Param('moduleName') moduleName: string, + @Param('sectionKey') sectionKey: string, + @Body() body: MetadataExplorerWriteDto, + ) { + return this.moduleMetadataExplorerService.updateSection(moduleName, sectionKey, body); + } + + @Post(':moduleName/sections/:sectionKey/validate') + async validateSection( + @Param('moduleName') moduleName: string, + @Param('sectionKey') sectionKey: string, + @Body() body: MetadataExplorerWriteDto, + ) { + return this.moduleMetadataExplorerService.validateSection(moduleName, sectionKey, body); + } + + @Get(':moduleName/search') + async search( + @Param('moduleName') moduleName: string, + @Query() query: MetadataExplorerSearchQueryDto, + ) { + return this.moduleMetadataExplorerService.search(moduleName, query); + } + + @Get(':moduleName/references') + async findReferences( + @Param('moduleName') moduleName: string, + @Query() query: MetadataExplorerReferencesQueryDto, + ) { + return this.moduleMetadataExplorerService.findReferences(moduleName, query); + } +} diff --git a/src/controllers/module-metadata.controller.ts b/src/controllers/module-metadata.controller.ts index e5d3120d..629ba774 100755 --- a/src/controllers/module-metadata.controller.ts +++ b/src/controllers/module-metadata.controller.ts @@ -64,6 +64,12 @@ export class ModuleMetadataController { generateCode(@Param('id', ParseIntPipe) id: number) { return this.moduleMetadataService.generateCodeViaCtl(id); } + + @ApiBearerAuth("jwt") + @Post(':id/seed') + seedModule(@Param('id', ParseIntPipe) id: number) { + return this.moduleMetadataService.seedModuleFromMetadata(id); + } diff --git a/src/dtos/metadata-explorer-references-query.dto.ts b/src/dtos/metadata-explorer-references-query.dto.ts new file mode 100644 index 00000000..2bd7554b --- /dev/null +++ b/src/dtos/metadata-explorer-references-query.dto.ts @@ -0,0 +1,39 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { Type } from "class-transformer"; +import { IsBoolean, IsOptional, IsString } from "class-validator"; +import { PaginationQueryDto } from "./pagination-query.dto"; + +export class MetadataExplorerReferencesQueryDto extends PaginationQueryDto { + @ApiProperty({ description: "Value or key to locate references for", required: false, type: String }) + @IsString() + @IsOptional() + needle?: string; + + @ApiProperty({ description: "Optional section key to scope the reference search", required: false, type: String }) + @IsString() + @IsOptional() + sectionKey?: string; + + @ApiProperty({ description: "Optional JSON path to exclude from results", required: false, type: String }) + @IsString() + @IsOptional() + excludePath?: string; + + @ApiProperty({ description: "Whether to match the full key/value exactly", required: false, type: Boolean, default: true }) + @Type(() => Boolean) + @IsBoolean() + @IsOptional() + exact?: boolean = true; + + @ApiProperty({ description: "Whether object keys should be considered when finding references", required: false, type: Boolean, default: true }) + @Type(() => Boolean) + @IsBoolean() + @IsOptional() + matchKeys?: boolean = true; + + @ApiProperty({ description: "Whether primitive values should be considered when finding references", required: false, type: Boolean, default: true }) + @Type(() => Boolean) + @IsBoolean() + @IsOptional() + matchValues?: boolean = true; +} diff --git a/src/dtos/metadata-explorer-search-query.dto.ts b/src/dtos/metadata-explorer-search-query.dto.ts new file mode 100644 index 00000000..c4a5cd7d --- /dev/null +++ b/src/dtos/metadata-explorer-search-query.dto.ts @@ -0,0 +1,28 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { Type } from "class-transformer"; +import { IsBoolean, IsOptional, IsString, Min } from "class-validator"; +import { PaginationQueryDto } from "./pagination-query.dto"; + +export class MetadataExplorerSearchQueryDto extends PaginationQueryDto { + @ApiProperty({ description: "Search query string", required: false, type: String }) + @IsString() + @IsOptional() + query?: string; + + @ApiProperty({ description: "Optional section key to scope the search", required: false, type: String }) + @IsString() + @IsOptional() + sectionKey?: string; + + @ApiProperty({ description: "Whether to match the full value exactly", required: false, type: Boolean, default: false }) + @Type(() => Boolean) + @IsBoolean() + @IsOptional() + exact?: boolean = false; + + @ApiProperty({ description: "Maximum preview length for match snippets", required: false, type: Number, default: 120 }) + @Type(() => Number) + @Min(20) + @IsOptional() + previewLength?: number = 120; +} diff --git a/src/dtos/metadata-explorer-write.dto.ts b/src/dtos/metadata-explorer-write.dto.ts new file mode 100644 index 00000000..89025ba5 --- /dev/null +++ b/src/dtos/metadata-explorer-write.dto.ts @@ -0,0 +1,11 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { IsOptional } from "class-validator"; + +export class MetadataExplorerWriteDto { + @ApiProperty({ + description: "Arbitrary JSON value to persist for the full metadata document or a section", + required: false, + }) + @IsOptional() + value?: any; +} diff --git a/src/index.ts b/src/index.ts index 4cf4a4fd..9ab99315 100755 --- a/src/index.ts +++ b/src/index.ts @@ -110,6 +110,9 @@ export * from './dtos/create-user-activity-history.dto' export * from './dtos/update-user-activity-history.dto' export * from './dtos/dashboard-variable-options-query.dto' export * from './dtos/dashboard-widget-data-request.dto' +export * from './dtos/metadata-explorer-references-query.dto' +export * from './dtos/metadata-explorer-search-query.dto' +export * from './dtos/metadata-explorer-write.dto' export * from './dtos/create-dashboard-user-layout.dto' export * from './dtos/update-dashboard-user-layout.dto' @@ -298,6 +301,7 @@ export * from './services/mediaStorageProviders/file-storage-provider' export * from './services/mediaStorageProviders/index' export * from './services/menu-item-metadata.service' export * from './services/model-metadata.service' +export * from './services/module-metadata-explorer.service' export * from './services/module-metadata.service' export * from './services/mq-message-queue.service' export * from './services/mq-message.service' @@ -332,6 +336,7 @@ export * from './services/setting.service' export * from './services/encryption.service' export * from './services/info.service' export * from './controllers/info.controller' +export * from './controllers/module-metadata-explorer.controller' export * from './services/settings/default-settings-provider.service' export * from './services/security-rule.service' export * from './services/request-context.service' diff --git a/src/seeders/module-metadata-seeder.service.ts b/src/seeders/module-metadata-seeder.service.ts index 32249786..b6fda72f 100755 --- a/src/seeders/module-metadata-seeder.service.ts +++ b/src/seeders/module-metadata-seeder.service.ts @@ -95,14 +95,19 @@ export class ModuleMetadataSeederService { let currentModule = 'global'; let currentStep = 'bootstrap'; let modulesToSeed: string[] | null = null; + const shouldSeedGlobalMetadata = conf?.seedGlobalMetadata !== false; try { this.enablePruning = Boolean(conf?.pruneMetadata); console.log(this.enablePruning ? '▶ Pruning enabled: metadata not present in JSON will be removed.' : '▶ Pruning disabled: existing metadata will be kept.'); // Global seeding steps i.e across all modules - currentStep = 'seedGlobalMetadata'; - await this.seedGlobalMetadata(); + if (shouldSeedGlobalMetadata) { + currentStep = 'seedGlobalMetadata'; + await this.seedGlobalMetadata(); + } else { + this.logger.log(`Skipping global metadata seeding.`); + } // Module specific seeding steps. // Get all the module metadata files which needs to be seeded. diff --git a/src/services/module-metadata-explorer.service.ts b/src/services/module-metadata-explorer.service.ts new file mode 100644 index 00000000..1966ac21 --- /dev/null +++ b/src/services/module-metadata-explorer.service.ts @@ -0,0 +1,670 @@ +import { BadRequestException, Injectable, Logger, NotFoundException } from '@nestjs/common'; +import * as fs from 'fs/promises'; +import * as path from 'path'; +import { DisallowInProduction } from 'src/decorators/disallow-in-production.decorator'; +import { ModuleMetadataHelperService } from 'src/helpers/module-metadata-helper.service'; +import { MetadataExplorerReferencesQueryDto } from '../dtos/metadata-explorer-references-query.dto'; +import { MetadataExplorerSearchQueryDto } from '../dtos/metadata-explorer-search-query.dto'; +import { MetadataExplorerWriteDto } from '../dtos/metadata-explorer-write.dto'; + +type MetadataExplorerSectionType = 'object' | 'array'; + +type MetadataExplorerSectionDefinition = { + key: string; + title: string; + jsonPath: string; + type: MetadataExplorerSectionType; + description: string; +}; + +type MetadataValidationIssue = { + path: string; + message: string; + severity: 'error' | 'warning'; +}; + +type MetadataSearchMatch = { + path: string; + sectionKey: string | null; + matchType: 'key' | 'value'; + valueType: string; + preview: string; +}; + +const SECTION_DEFINITIONS: MetadataExplorerSectionDefinition[] = [ + { key: 'module', title: 'Module', jsonPath: 'moduleMetadata', type: 'object', description: 'Top-level module metadata excluding nested models.' }, + { key: 'models', title: 'Models', jsonPath: 'moduleMetadata.models', type: 'array', description: 'Model metadata definitions for the module.' }, + { key: 'roles', title: 'Roles', jsonPath: 'roles', type: 'array', description: 'Role metadata definitions.' }, + { key: 'users', title: 'Users', jsonPath: 'users', type: 'array', description: 'Seed users and user-role assignments.' }, + { key: 'permissions', title: 'Permissions', jsonPath: 'permissions', type: 'array', description: 'Permission declarations and explicit additions.' }, + { key: 'actions', title: 'Actions', jsonPath: 'actions', type: 'array', description: 'Action metadata definitions.' }, + { key: 'menus', title: 'Menus', jsonPath: 'menus', type: 'array', description: 'Menu item metadata definitions.' }, + { key: 'views', title: 'Views', jsonPath: 'views', type: 'array', description: 'List, form, kanban, tree, and related view metadata.' }, + { key: 'emailTemplates', title: 'Email Templates', jsonPath: 'emailTemplates', type: 'array', description: 'Email template metadata.' }, + { key: 'smsTemplates', title: 'SMS Templates', jsonPath: 'smsTemplates', type: 'array', description: 'SMS template metadata.' }, + { key: 'mediaStorageProviders', title: 'Media Storage Providers', jsonPath: 'mediaStorageProviders', type: 'array', description: 'Configured media storage providers.' }, + { key: 'savedFilters', title: 'Saved Filters', jsonPath: 'savedFilters', type: 'array', description: 'Saved filter definitions.' }, + { key: 'securityRules', title: 'Security Rules', jsonPath: 'securityRules', type: 'array', description: 'Security rule definitions.' }, + { key: 'listOfValues', title: 'List Of Values', jsonPath: 'listOfValues', type: 'array', description: 'List of values entries.' }, + { key: 'dashboards', title: 'Dashboards', jsonPath: 'dashboards', type: 'array', description: 'Dashboard metadata definitions.' }, + { key: 'testing', title: 'Testing', jsonPath: 'testing', type: 'object', description: 'Testing roles, users, specs, and scenarios.' }, + { key: 'scheduledJobs', title: 'Scheduled Jobs', jsonPath: 'scheduledJobs', type: 'array', description: 'Scheduled job metadata definitions.' }, + { key: 'modelSequences', title: 'Model Sequences', jsonPath: 'modelSequences', type: 'array', description: 'Model sequence definitions.' }, +]; + +@Injectable() +export class ModuleMetadataExplorerService { + private readonly logger = new Logger(ModuleMetadataExplorerService.name); + + constructor( + private readonly moduleMetadataHelperService: ModuleMetadataHelperService, + ) { } + + /** + * Input: module name string. + * Output: explorer manifest containing file details, section summaries, and document validation state. + * Logic: loads the module metadata JSON, computes section-level summary information, and returns + * the high-level descriptor the explorer UI can use to render navigation and status badges. + */ + async getManifest(moduleName: string) { + const { document, filePath, stats } = await this.loadMetadataDocument(moduleName); + const validation = this.validateDocumentValue(document); + + const sections = SECTION_DEFINITIONS.map((definition) => { + const value = this.getValueAtPath(document, definition.jsonPath); + return { + ...definition, + exists: value !== undefined, + itemCount: Array.isArray(value) ? value.length : undefined, + nodeCount: this.countNodes(value), + preview: this.buildPreview(value, 96), + }; + }); + + return { + moduleName, + filePath, + lastModifiedAt: stats.mtime.toISOString(), + sections, + validation, + }; + } + + /** + * Input: module name string. + * Output: full metadata document payload with file metadata and validation results. + * Logic: reads the resolved module metadata JSON from disk and returns it as the canonical + * document payload for full-document editing scenarios. + */ + async getDocument(moduleName: string) { + const { document, filePath, stats } = await this.loadMetadataDocument(moduleName); + return { + moduleName, + filePath, + lastModifiedAt: stats.mtime.toISOString(), + value: document, + validation: this.validateDocumentValue(document), + }; + } + + /** + * Input: module name string and a DTO containing the replacement JSON document in `value`. + * Output: refreshed full document payload after a successful write. + * Logic: validates the incoming JSON document at a coarse structural level, writes it back to + * the resolved metadata file, and then reloads the saved document to return the persisted state. + */ + @DisallowInProduction() + async updateDocument(moduleName: string, dto: MetadataExplorerWriteDto) { + this.assertValueProvided(dto?.value); + const validation = this.validateDocumentValue(dto.value); + if (!validation.valid) { + throw new BadRequestException(validation); + } + + const filePath = await this.moduleMetadataHelperService.getModuleMetadataFilePath(moduleName); + await this.writeMetadataDocument(filePath, dto.value); + return this.getDocument(moduleName); + } + + /** + * Input: module name string and an optional DTO containing a document candidate in `value`. + * Output: validation result object with `valid` flag and issue list. + * Logic: validates either the supplied document candidate or the currently persisted metadata + * file without mutating anything, so the UI can perform pre-save checks. + */ + async validateDocument(moduleName: string, dto?: MetadataExplorerWriteDto) { + const filePath = await this.moduleMetadataHelperService.getModuleMetadataFilePath(moduleName); + const value = dto && Object.prototype.hasOwnProperty.call(dto, 'value') + ? dto.value + : (await this.loadMetadataDocument(moduleName)).document; + return { + moduleName, + filePath, + ...this.validateDocumentValue(value), + }; + } + + /** + * Input: module name string and a section key such as `models` or `views`. + * Output: section payload containing metadata about the section, its JSON value, and validation. + * Logic: resolves the section definition, extracts that fragment from the full metadata document, + * and returns the section in a UI-friendly envelope. + */ + async getSection(moduleName: string, sectionKey: string) { + const definition = this.getSectionDefinition(sectionKey); + const { document, filePath, stats } = await this.loadMetadataDocument(moduleName); + const value = this.getValueAtPath(document, definition.jsonPath); + + return { + moduleName, + filePath, + lastModifiedAt: stats.mtime.toISOString(), + section: this.buildSectionResponse(definition, value), + validation: this.validateSectionValue(definition, value), + }; + } + + /** + * Input: module name string, section key, and a DTO containing the replacement section JSON in `value`. + * Output: refreshed section payload after the section is written back into the document. + * Logic: validates the incoming section shape, merges it into the current document at the + * configured JSON path, validates the resulting document, writes the file, and reloads the section. + */ + @DisallowInProduction() + async updateSection(moduleName: string, sectionKey: string, dto: MetadataExplorerWriteDto) { + this.assertValueProvided(dto?.value); + const definition = this.getSectionDefinition(sectionKey); + const sectionValidation = this.validateSectionValue(definition, dto.value); + if (!sectionValidation.valid) { + throw new BadRequestException(sectionValidation); + } + + const { document, filePath } = await this.loadMetadataDocument(moduleName); + this.setValueAtPath(document, definition.jsonPath, dto.value); + + const documentValidation = this.validateDocumentValue(document); + if (!documentValidation.valid) { + throw new BadRequestException(documentValidation); + } + + await this.writeMetadataDocument(filePath, document); + return this.getSection(moduleName, sectionKey); + } + + /** + * Input: module name string, section key, and an optional DTO containing a section candidate in `value`. + * Output: validation result scoped to that section. + * Logic: validates either the supplied section candidate or the persisted section value against + * the configured section definition without performing any writes. + */ + async validateSection(moduleName: string, sectionKey: string, dto?: MetadataExplorerWriteDto) { + const definition = this.getSectionDefinition(sectionKey); + const value = dto && Object.prototype.hasOwnProperty.call(dto, 'value') + ? dto.value + : this.getValueAtPath((await this.loadMetadataDocument(moduleName)).document, definition.jsonPath); + + return { + moduleName, + sectionKey, + section: { + key: definition.key, + title: definition.title, + jsonPath: definition.jsonPath, + type: definition.type, + }, + ...this.validateSectionValue(definition, value), + }; + } + + /** + * Input: module name string plus search query DTO with query text, optional section scope, + * exact-match toggle, preview length, limit, and offset. + * Output: paginated list of key/value matches with JSON paths and section attribution. + * Logic: loads the document, optionally narrows the search root to a specific section, and then + * recursively scans keys and primitive values for textual matches. + */ + async search(moduleName: string, query: MetadataExplorerSearchQueryDto) { + const needle = `${query?.query ?? ''}`.trim(); + if (!needle) { + throw new BadRequestException('Search query is required.'); + } + + const { document } = await this.loadMetadataDocument(moduleName); + const targetSection = query?.sectionKey ? this.getSectionDefinition(query.sectionKey) : null; + const searchRoot = targetSection ? this.getValueAtPath(document, targetSection.jsonPath) : document; + const rootPath = targetSection ? targetSection.jsonPath : ''; + const limit = query?.limit ?? 10; + const offset = query?.offset ?? 0; + const matches = this.collectMatches(searchRoot, rootPath, { + needle, + exact: query?.exact ?? false, + limit, + offset, + previewLength: query?.previewLength ?? 120, + matchKeys: true, + matchValues: true, + }); + + return { + moduleName, + query: needle, + sectionKey: targetSection?.key ?? null, + meta: { + totalMatches: matches.total, + returned: matches.items.length, + limit, + offset, + }, + matches: matches.items, + }; + } + + /** + * Input: module name string plus reference-query DTO with needle, optional section scope, + * match strategy flags, exclusion path, limit, and offset. + * Output: paginated list of reference-like matches across keys and/or values. + * Logic: performs the same recursive traversal engine as search, but with defaults tuned for + * exact reference lookups and optional exclusion of the source path being inspected. + */ + async findReferences(moduleName: string, query: MetadataExplorerReferencesQueryDto) { + const needle = `${query?.needle ?? ''}`.trim(); + if (!needle) { + throw new BadRequestException('Reference needle is required.'); + } + + const { document } = await this.loadMetadataDocument(moduleName); + const targetSection = query?.sectionKey ? this.getSectionDefinition(query.sectionKey) : null; + const searchRoot = targetSection ? this.getValueAtPath(document, targetSection.jsonPath) : document; + const rootPath = targetSection ? targetSection.jsonPath : ''; + const limit = query?.limit ?? 10; + const offset = query?.offset ?? 0; + const matches = this.collectMatches(searchRoot, rootPath, { + needle, + exact: query?.exact ?? true, + limit, + offset, + previewLength: 140, + matchKeys: query?.matchKeys ?? true, + matchValues: query?.matchValues ?? true, + excludePath: query?.excludePath, + }); + + return { + moduleName, + needle, + sectionKey: targetSection?.key ?? null, + meta: { + totalMatches: matches.total, + returned: matches.items.length, + limit, + offset, + }, + matches: matches.items, + }; + } + + /** + * Input: module name string. + * Output: parsed JSON document, resolved file path, and file stats. + * Logic: resolves the module metadata file location, reads it from disk, parses JSON, and maps + * common file and syntax errors into explorer-specific HTTP exceptions. + */ + private async loadMetadataDocument(moduleName: string) { + const filePath = await this.moduleMetadataHelperService.getModuleMetadataFilePath(moduleName); + if (!filePath) { + throw new NotFoundException(`Unable to resolve metadata file for module: ${moduleName}`); + } + + try { + const [raw, stats] = await Promise.all([ + fs.readFile(filePath, 'utf-8'), + fs.stat(filePath), + ]); + const document = JSON.parse(raw); + return { document, filePath, stats }; + } catch (error: any) { + if (error?.code === 'ENOENT') { + throw new NotFoundException(`Metadata file not found for module: ${moduleName}`); + } + + if (error instanceof SyntaxError) { + throw new BadRequestException(`Metadata file for module ${moduleName} contains invalid JSON.`); + } + + this.logger.error(`Failed to load metadata for module ${moduleName}`, error); + throw error; + } + } + + /** + * Input: absolute metadata file path and the JSON document value to persist. + * Output: none. + * Logic: ensures the target directory exists and writes a formatted JSON file with a trailing + * newline so the explorer remains the canonical persistence path for metadata edits. + */ + private async writeMetadataDocument(filePath: string, document: any) { + await fs.mkdir(path.dirname(filePath), { recursive: true }); + await fs.writeFile(filePath, `${JSON.stringify(document, null, 2)}\n`, 'utf-8'); + } + + /** + * Input: section key string. + * Output: matching section definition object. + * Logic: looks up the fixed explorer section registry and throws a 404 when the caller asks for + * a section the explorer does not expose. + */ + private getSectionDefinition(sectionKey: string): MetadataExplorerSectionDefinition { + const definition = SECTION_DEFINITIONS.find((item) => item.key === sectionKey); + if (!definition) { + throw new NotFoundException(`Metadata explorer section not found: ${sectionKey}`); + } + return definition; + } + + /** + * Input: section definition and extracted section JSON value. + * Output: normalized section response payload for the API. + * Logic: enriches the raw section value with descriptive metadata, existence flags, counts, and + * the original JSON so the UI can render a section panel without extra computation. + */ + private buildSectionResponse(definition: MetadataExplorerSectionDefinition, value: any) { + return { + key: definition.key, + title: definition.title, + jsonPath: definition.jsonPath, + type: definition.type, + description: definition.description, + exists: value !== undefined, + itemCount: Array.isArray(value) ? value.length : undefined, + nodeCount: this.countNodes(value), + value, + }; + } + + /** + * Input: candidate full metadata document. + * Output: validation result containing a boolean validity flag and issue list. + * Logic: performs coarse structural checks for the overall document contract and also validates + * each known explorer section against its expected container type. + */ + private validateDocumentValue(document: any) { + const issues: MetadataValidationIssue[] = []; + + if (!document || typeof document !== 'object' || Array.isArray(document)) { + issues.push({ + path: '', + message: 'Metadata document must be a JSON object.', + severity: 'error', + }); + } + + const moduleMetadata = document?.moduleMetadata; + if (!moduleMetadata || typeof moduleMetadata !== 'object' || Array.isArray(moduleMetadata)) { + issues.push({ + path: 'moduleMetadata', + message: 'moduleMetadata must be present as an object.', + severity: 'error', + }); + } + + if (moduleMetadata && !Array.isArray(moduleMetadata.models)) { + issues.push({ + path: 'moduleMetadata.models', + message: 'moduleMetadata.models must be an array.', + severity: 'error', + }); + } + + for (const definition of SECTION_DEFINITIONS) { + const sectionValue = this.getValueAtPath(document, definition.jsonPath); + const sectionValidation = this.validateSectionValue(definition, sectionValue, true); + issues.push(...sectionValidation.issues); + } + + return { + valid: issues.every((issue) => issue.severity !== 'error'), + issues, + }; + } + + /** + * Input: section definition, candidate section value, and optional allow-undefined toggle. + * Output: validation result for that section. + * Logic: checks whether a section is present when required and whether it matches the configured + * array or object shape expected by the explorer contract. + */ + private validateSectionValue( + definition: MetadataExplorerSectionDefinition, + value: any, + allowUndefined = false, + ) { + const issues: MetadataValidationIssue[] = []; + + if (value === undefined) { + if (!allowUndefined) { + issues.push({ + path: definition.jsonPath, + message: `${definition.title} is missing from the metadata document.`, + severity: 'error', + }); + } + return { + valid: issues.every((issue) => issue.severity !== 'error'), + issues, + }; + } + + const isArray = Array.isArray(value); + if (definition.type === 'array' && !isArray) { + issues.push({ + path: definition.jsonPath, + message: `${definition.title} must be an array.`, + severity: 'error', + }); + } + + if (definition.type === 'object' && (value === null || typeof value !== 'object' || isArray)) { + issues.push({ + path: definition.jsonPath, + message: `${definition.title} must be an object.`, + severity: 'error', + }); + } + + return { + valid: issues.every((issue) => issue.severity !== 'error'), + issues, + }; + } + + /** + * Input: source object and dot-delimited JSON path. + * Output: value found at that path, or undefined if any segment is missing. + * Logic: walks the object graph one segment at a time and safely stops when an intermediate node + * does not exist. + */ + private getValueAtPath(source: any, jsonPath: string): any { + if (!jsonPath) return source; + return jsonPath.split('.').reduce((acc, key) => (acc == null ? undefined : acc[key]), source); + } + + /** + * Input: target object, dot-delimited JSON path, and replacement value. + * Output: none. + * Logic: creates missing object containers along the path as needed and then assigns the final + * value at the terminal segment. + */ + private setValueAtPath(target: any, jsonPath: string, value: any) { + const parts = jsonPath.split('.'); + let current = target; + + for (let index = 0; index < parts.length - 1; index++) { + const part = parts[index]; + if (!current[part] || typeof current[part] !== 'object' || Array.isArray(current[part])) { + current[part] = {}; + } + current = current[part]; + } + + current[parts[parts.length - 1]] = value; + } + + /** + * Input: any JSON value. + * Output: integer node count representing the size of that subtree. + * Logic: recursively counts container nodes and primitive leaves so the explorer can show rough + * section complexity metrics in manifests and section responses. + */ + private countNodes(value: any): number { + if (value === undefined || value === null) return 0; + if (Array.isArray(value)) { + return value.reduce((sum, item) => sum + this.countNodes(item), 1); + } + if (typeof value === 'object') { + return Object.values(value).reduce((sum, item) => sum + this.countNodes(item), 1); + } + return 1; + } + + /** + * Input: any JSON value and a maximum preview length. + * Output: string preview suitable for manifest/search display. + * Logic: converts the value to a string representation and truncates it to a bounded length for + * compact display in explorer summaries. + */ + private buildPreview(value: any, maxLength: number): string { + if (value === undefined) return ''; + + const normalized = typeof value === 'string' + ? value + : JSON.stringify(value); + + if (!normalized) return ''; + return normalized.length > maxLength ? `${normalized.slice(0, maxLength)}...` : normalized; + } + + /** + * Input: search root value, its root JSON path, and traversal options. + * Output: total match count plus the paginated slice of matches requested by the caller. + * Logic: recursively traverses arrays, objects, and primitives, recording key and/or value + * matches together with their JSON paths and preview text. + */ + private collectMatches( + root: any, + rootPath: string, + opts: { + needle: string; + exact: boolean; + limit: number; + offset: number; + previewLength: number; + matchKeys: boolean; + matchValues: boolean; + excludePath?: string; + }, + ) { + const results: MetadataSearchMatch[] = []; + const normalizedNeedle = opts.exact ? opts.needle : opts.needle.toLowerCase(); + + const visit = (value: any, currentPath: string) => { + if (value === undefined) return; + + if (Array.isArray(value)) { + value.forEach((item, index) => { + visit(item, this.joinPath(currentPath, `${index}`)); + }); + return; + } + + if (value && typeof value === 'object') { + Object.entries(value).forEach(([key, child]) => { + const childPath = this.joinPath(currentPath, key); + + if (opts.matchKeys && this.matchesNeedle(key, normalizedNeedle, opts.exact) && childPath !== opts.excludePath) { + results.push({ + path: childPath, + sectionKey: this.resolveSectionKey(childPath), + matchType: 'key', + valueType: 'key', + preview: this.buildPreview(child, opts.previewLength), + }); + } + + visit(child, childPath); + }); + return; + } + + if (!opts.matchValues) return; + const comparable = value == null ? '' : `${value}`; + if (this.matchesNeedle(comparable, normalizedNeedle, opts.exact) && currentPath !== opts.excludePath) { + results.push({ + path: currentPath, + sectionKey: this.resolveSectionKey(currentPath), + matchType: 'value', + valueType: typeof value, + preview: this.buildPreview(value, opts.previewLength), + }); + } + }; + + visit(root, rootPath); + + return { + total: results.length, + items: results.slice(opts.offset, opts.offset + opts.limit), + }; + } + + /** + * Input: candidate string value, normalized search needle, and exact-match flag. + * Output: boolean indicating whether the value matches the search. + * Logic: performs either exact comparison or case-insensitive containment matching, depending on + * the caller's intent. + */ + private matchesNeedle(value: string, normalizedNeedle: string, exact: boolean): boolean { + if (exact) { + return value === normalizedNeedle; + } + return value.toLowerCase().includes(normalizedNeedle); + } + + /** + * Input: JSON path string from a match result. + * Output: matching explorer section key or null when the path is outside known sections. + * Logic: finds the deepest section definition whose JSON path prefixes the supplied match path so + * search results can be attributed back to a sidebar section. + */ + private resolveSectionKey(jsonPath: string): string | null { + const sorted = [...SECTION_DEFINITIONS].sort((a, b) => b.jsonPath.length - a.jsonPath.length); + const matched = sorted.find((section) => + jsonPath === section.jsonPath + || jsonPath.startsWith(`${section.jsonPath}.`) + || jsonPath.startsWith(`${section.jsonPath}[`) + ); + return matched?.key ?? null; + } + + /** + * Input: current base JSON path and next property/index segment. + * Output: composed JSON path string. + * Logic: appends either dot notation or array index notation so traversal results use a stable, + * readable path format. + */ + private joinPath(base: string, next: string): string { + if (!base) return /^\d+$/.test(next) ? `[${next}]` : next; + return /^\d+$/.test(next) ? `${base}[${next}]` : `${base}.${next}`; + } + + /** + * Input: arbitrary candidate value. + * Output: none; throws when the value is missing. + * Logic: enforces that write and validation endpoints receive an explicit JSON payload instead of + * silently treating an omitted body as intentional input. + */ + private assertValueProvided(value: any) { + if (value === undefined) { + throw new BadRequestException('A JSON value payload is required.'); + } + } +} diff --git a/src/services/module-metadata.service.ts b/src/services/module-metadata.service.ts index 79827353..d6e698da 100755 --- a/src/services/module-metadata.service.ts +++ b/src/services/module-metadata.service.ts @@ -404,6 +404,31 @@ export class ModuleMetadataService { return true } + @DisallowInProduction() + async seedModuleFromMetadata(moduleId: number) { + const module = await this.findOne(moduleId); + const seeder = this.solidRegistry + .getSeeders() + .filter((registeredSeeder) => registeredSeeder.name === 'ModuleMetadataSeederService') + .map((registeredSeeder) => registeredSeeder.instance) + .pop(); + + if (!seeder || typeof seeder.seed !== 'function') { + throw new NotFoundException('ModuleMetadataSeederService not found. Cannot seed module metadata.'); + } + + await seeder.seed({ + modulesToSeed: [module.name], + seedGlobalMetadata: false, + }); + + return { + success: true, + moduleName: module.name, + message: `Seeded metadata for module ${module.name}.`, + }; + } + @DisallowInProduction() async generateCodeViaCtl(moduleId: number): Promise { const module = await this.findOne(moduleId); diff --git a/src/solid-core.module.ts b/src/solid-core.module.ts index e572689b..63e99e89 100644 --- a/src/solid-core.module.ts +++ b/src/solid-core.module.ts @@ -17,6 +17,7 @@ import { FieldMetadataController } from "./controllers/field-metadata.controller import { DashboardController } from "./controllers/dashboard.controller"; import { MediaStorageProviderMetadataController } from "./controllers/media-storage-provider-metadata.controller"; import { ModelMetadataController } from "./controllers/model-metadata.controller"; +import { ModuleMetadataExplorerController } from "./controllers/module-metadata-explorer.controller"; import { ModuleMetadataController } from "./controllers/module-metadata.controller"; import { TestController } from "./controllers/test.controller"; import { FieldMetadata } from "./entities/field-metadata.entity"; @@ -41,6 +42,7 @@ import { ListOfValuesService } from "./services/list-of-values.service"; import { MediaStorageProviderMetadataService } from "./services/media-storage-provider-metadata.service"; import { MediaService } from "./services/media.service"; import { ModelMetadataService } from "./services/model-metadata.service"; +import { ModuleMetadataExplorerService } from "./services/module-metadata-explorer.service"; import { ModuleMetadataService } from "./services/module-metadata.service"; import { SolidIntrospectService } from "./services/solid-introspect.service"; // import { ListOfComputedFieldProvider } from './providers/list-of-computed-field-provider.service'; @@ -485,6 +487,7 @@ import { DashboardUserLayoutRepository } from './repositories/dashboard-user-lay MediaStorageProviderMetadataController, MenuItemMetadataController, ModelMetadataController, + ModuleMetadataExplorerController, ModuleMetadataController, MqMessageController, MqMessageQueueController, @@ -534,6 +537,7 @@ import { DashboardUserLayoutRepository } from './repositories/dashboard-user-lay useClass: HttpExceptionFilter, }, ModuleMetadataService, + ModuleMetadataExplorerService, ModuleMetadataHelperService, ModelMetadataService, ModelMetadataHelperService, @@ -844,6 +848,7 @@ import { DashboardUserLayoutRepository } from './repositories/dashboard-user-lay ModelMetadataHelperService, ModelMetadataService, ModuleMetadataService, + ModuleMetadataExplorerService, MqMessageQueueService, MqMessageService, Msg91OTPService, From e6251ae1046500c55e8a018c1f549332cca39025 Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Tue, 9 Jun 2026 08:03:30 +0530 Subject: [PATCH 093/136] Enhance solid-core metadata structure for module mdtadata by removing import/export options and adding sortable attributes for fields --- src/seeders/seed-data/solid-core-metadata.json | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/seeders/seed-data/solid-core-metadata.json b/src/seeders/seed-data/solid-core-metadata.json index 3f716304..0cb54182 100755 --- a/src/seeders/seed-data/solid-core-metadata.json +++ b/src/seeders/seed-data/solid-core-metadata.json @@ -6578,20 +6578,23 @@ "customComponentIsSystem": true } } - ] + ], + "import": false, + "export": false }, "children": [ { "type": "field", "attrs": { - "name": "displayName", - "isSearchable": true + "name": "name", + "isSearchable": true, + "sortable": true } }, { "type": "field", "attrs": { - "name": "name", + "name": "displayName", "isSearchable": true } }, @@ -6613,7 +6616,8 @@ "type": "field", "attrs": { "name": "menuSequenceNumber", - "isSearchable": true + "isSearchable": true, + "sortable": true } }, { @@ -13976,4 +13980,4 @@ } } ] -} +} \ No newline at end of file From 5e8e3f07188e6be408a0a84cc93633542da111ee Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Tue, 9 Jun 2026 08:24:06 +0530 Subject: [PATCH 094/136] Update solid-core metadata: remove import/export options, rename fields, and add sortable attribute --- .../seed-data/solid-core-metadata.json | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/src/seeders/seed-data/solid-core-metadata.json b/src/seeders/seed-data/solid-core-metadata.json index 0cb54182..d475027e 100755 --- a/src/seeders/seed-data/solid-core-metadata.json +++ b/src/seeders/seed-data/solid-core-metadata.json @@ -6765,27 +6765,30 @@ "openInPopup": true } } - ] + ], + "import": false, + "export": false }, "children": [ { "type": "field", "attrs": { - "name": "singularName", + "name": "module", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "tableName", - "isSearchable": true + "name": "singularName", + "isSearchable": true, + "sortable": true } }, { "type": "field", "attrs": { - "name": "pluralName", + "name": "tableName", "isSearchable": true } }, @@ -6807,7 +6810,7 @@ "type": "field", "attrs": { "name": "dataSource", - "isSearchable": true + "isSearchable": false } }, { @@ -6816,13 +6819,6 @@ "name": "enableSoftDelete", "isSearchable": true } - }, - { - "type": "field", - "attrs": { - "name": "module", - "isSearchable": true - } } ] } From 182d22e3ee8555accaffc7919b41b622afcd6076 Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Tue, 9 Jun 2026 09:37:46 +0530 Subject: [PATCH 095/136] Refactor CRUD and model metadata services to enhance group filtering and pagination; streamline response handling and improve query structure. configuration changes to support tree view in model metadata --- .../seed-data/solid-core-metadata.json | 103 +++++++++++++++- src/services/crud-helper.service.ts | 72 +++++++++++ src/services/crud.service.ts | 115 ++---------------- src/services/model-metadata.service.ts | 44 +++---- 4 files changed, 198 insertions(+), 136 deletions(-) diff --git a/src/seeders/seed-data/solid-core-metadata.json b/src/seeders/seed-data/solid-core-metadata.json index d475027e..d45ef9b0 100755 --- a/src/seeders/seed-data/solid-core-metadata.json +++ b/src/seeders/seed-data/solid-core-metadata.json @@ -5616,10 +5616,10 @@ { "displayName": "Model List Action", "name": "modelMetadata-list-action", - "type": "custom", + "type": "solid", "domain": "", "context": "", - "customComponent": "/admin/core/solid-core/model-metadata/list", + "customComponent": "", "customIsModal": true, "serverEndpoint": "", "viewUserKey": "modelMetadata-list-view", @@ -6184,6 +6184,19 @@ "viewUserKey": "dashboardUserLayout-tree-view", "moduleUserKey": "solid-core", "modelUserKey": "dashboardUserLayout" + }, + { + "displayName": "Model Tree Action", + "name": "modelMetadata-tree-action", + "type": "solid", + "domain": "", + "context": "", + "customComponent": "", + "customIsModal": true, + "serverEndpoint": "", + "viewUserKey": "modelMetadata-tree-view", + "moduleUserKey": "solid-core", + "modelUserKey": "modelMetadata" } ], "menus": [ @@ -6739,7 +6752,7 @@ ], "enableGlobalSearch": true, "truncateAfter": 50, - "create": true, + "create": false, "edit": true, "delete": false, "rowButtons": [ @@ -6767,7 +6780,11 @@ } ], "import": false, - "export": false + "export": false, + "allowedViews": [ + "list", + "tree" + ] }, "children": [ { @@ -6835,7 +6852,8 @@ "attrs": { "name": "form-1", "label": "Model Metadata", - "className": "grid" + "className": "grid", + "showAddFormButton": false }, "children": [ { @@ -13502,6 +13520,81 @@ } ] } + }, + { + "name": "modelMetadata-tree-view", + "displayName": "Model Metadata Tree View", + "type": "tree", + "context": "{}", + "moduleUserKey": "solid-core", + "modelUserKey": "modelMetadata", + "layout": { + "type": "tree", + "attrs": { + "pagination": true, + "pageSizeOptions": [ + 10, + 25, + 50 + ], + "enableGlobalSearch": true, + "create": false, + "edit": true, + "delete": false + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "module", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "singularName", + "isSearchable": true, + "sortable": true + } + }, + { + "type": "field", + "attrs": { + "name": "tableName", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "displayName", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "description", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "dataSource", + "isSearchable": false + } + }, + { + "type": "field", + "attrs": { + "name": "enableSoftDelete", + "isSearchable": true + } + } + ] + } } ], "emailTemplates": [ diff --git a/src/services/crud-helper.service.ts b/src/services/crud-helper.service.ts index 723a76a3..314a885c 100755 --- a/src/services/crud-helper.service.ts +++ b/src/services/crud-helper.service.ts @@ -749,4 +749,76 @@ export class CrudHelperService { return matchingPermssions.length > 0 } + pagedResponse(offset: number | undefined, limit: number | undefined, count: number, entities: T[]) { + const safeLimit = limit ?? count ?? 0; + const safeOffset = offset ?? 0; + const currentPage = safeLimit ? Math.floor(safeOffset / safeLimit) + 1 : 1; + const totalPages = safeLimit ? Math.ceil(count / safeLimit) : 1; + const nextPage = safeLimit && currentPage < totalPages ? currentPage + 1 : null; + const prevPage = safeLimit && currentPage > 1 ? currentPage - 1 : null; + return { + meta: { + totalRecords: count, + currentPage, + nextPage, + prevPage, + totalPages, + perPage: safeLimit ? +safeLimit : 0, + }, + records: entities, + }; + } + + async executeGroupPipeline( + filterQb: SelectQueryBuilder, + basicFilterDto: BasicFilterDto, + alias: string, + createQbFn: () => Promise>, + postProcessEntities?: (entities: T[]) => Promise + ): Promise<{ meta: { totalRecords: number }; groupMeta: any[]; groupRecords: any[] }> { + const groupByFields = this.normalize(basicFilterDto.groupBy); + if (!groupByFields.length) throw new BadRequestException(ERROR_MESSAGES.INVALID_GROUP_BY_COUNT); + + if (basicFilterDto.populateGroup) { + const hasRelationGroup = groupByFields.some(f => f.includes('.')); + if (hasRelationGroup) throw new BadRequestException('populateGroup is not supported when grouping on relation fields. Fetch group metadata first and retrieve records in a separate call.'); + } + + const { aliasMap: groupAliasMap, formatMap: groupFormatMap, expressionMap: groupExpressionMap } = + this.applyGroupBySelections(filterQb, groupByFields, alias); + const aggregateAliasMap = this.applyAggregates(filterQb, basicFilterDto.aggregates, alias); + this.applyGroupSortingAndPagination(filterQb, basicFilterDto.sort, { ...groupAliasMap, ...aggregateAliasMap }, basicFilterDto.limit, basicFilterDto.offset); + + const groupByResult = await filterQb.getRawMany(); + const totalGroups = await this.countGroups(filterQb); + const aggregateAliasSet = new Set(Object.values(aggregateAliasMap)); + + const groupMeta = []; + const groupRecords = []; + + for (const group of groupByResult) { + groupMeta.push(this.createGroupMeta(group, aggregateAliasSet, groupByFields, groupAliasMap, groupFormatMap)); + + if (basicFilterDto.populateGroup) { + let groupQb = await createQbFn(); + const { groupBy: _gb, aggregates: _agg, ...rest } = basicFilterDto; + const groupFilterDto: BasicFilterDto = { + ...rest, + ...basicFilterDto.groupFilter, + groupBy: undefined, + aggregates: undefined, + sort: basicFilterDto.groupFilter?.sort, + }; + groupQb = this.buildFilterQuery(groupQb, groupFilterDto, alias); + groupQb = this.buildGroupByRecordsQuery(groupQb, group, alias, groupAliasMap, aggregateAliasMap, groupExpressionMap); + const [entities, count] = await groupQb.getManyAndCount(); + if (postProcessEntities) await postProcessEntities(entities); + const groupData = this.pagedResponse(basicFilterDto.groupFilter?.offset, basicFilterDto.groupFilter?.limit, count, entities); + groupRecords.push(this.createGroupRecords(group, aggregateAliasSet, groupData, groupByFields, groupAliasMap, groupFormatMap)); + } + } + + return { meta: { totalRecords: totalGroups }, groupMeta, groupRecords }; + } + } diff --git a/src/services/crud.service.ts b/src/services/crud.service.ts index 30c452bf..a2f074ce 100755 --- a/src/services/crud.service.ts +++ b/src/services/crud.service.ts @@ -41,7 +41,6 @@ import { SolidRegistry } from "src/helpers/solid-registry"; import { getMediaStorageProvider } from "./mediaStorageProviders"; import { ModelMetadataService } from "./model-metadata.service"; import { RequestContextService } from "./request-context.service"; -import { BasicGroupFilterDto } from "src/dtos/basic-group-filters.dto"; export class CRUDService { // Add two generic value i.e Person,CreatePersonDto, so we get the proper types in our service @@ -480,10 +479,10 @@ export class CRUDService { // Add two generic value i.e return false; // If it is not a partial update, then do not skip computation } - async find(basicFilterDto: BasicFilterDto, solidRequestContext: any = {}) { + async find(basicFilterDto: BasicFilterDto, solidRequestContext: any = {}): Promise { const alias = 'entity'; // Extract the required keys from the input query - let { limit, offset, populateMedia, populateGroup, groupFilter } = basicFilterDto; + let { limit, offset, populateMedia } = basicFilterDto; const populateUserIdFields = this.crudHelperService.extractUserIdFieldsFromPopulate(basicFilterDto.populate); const { singularName, internationalisation, draftPublishWorkflow } = await this.loadModel(); @@ -502,41 +501,19 @@ export class CRUDService { // Add two generic value i.e // Create above query on pincode table using query builder var qb: SelectQueryBuilder = await this.repo.createSecurityRuleAwareQueryBuilder(alias) - if (basicFilterDto.groupBy) { + if (basicFilterDto.groupBy?.length) { const groupFilterQb = (internationalisation && draftPublishWorkflow) ? this.crudHelperService.buildFilterQuery(qb, basicFilterDto, alias, internationalisation, draftPublishWorkflow, this.moduleRef, FilterCombinator.AND, false, false) : this.crudHelperService.buildFilterQuery(qb, basicFilterDto, alias, undefined, undefined, undefined, FilterCombinator.AND, false, false); - const groupByFields = this.crudHelperService.normalize(basicFilterDto.groupBy); - if (!groupByFields.length) { - throw new BadRequestException(ERROR_MESSAGES.INVALID_GROUP_BY_COUNT); - } - - if (basicFilterDto.populateGroup) { - const hasRelationGroup = groupByFields.some(field => field.includes('.')); - if (hasRelationGroup) { - throw new BadRequestException('populateGroup is not supported when grouping on relation fields. Fetch group metadata first and retrieve records in a separate call.'); + return this.crudHelperService.executeGroupPipeline( + groupFilterQb, basicFilterDto, alias, + () => this.repo.createSecurityRuleAwareQueryBuilder(alias), + async (entities) => { + if (populateUserIdFields?.length) await this.handlePopulateUserIdFields(populateUserIdFields, entities); + if (populateMedia?.length) await this.handlePopulateMedia(populateMedia, entities); } - } - - const { aliasMap: groupAliasMap, formatMap: groupFormatMap, expressionMap: groupExpressionMap } = this.crudHelperService.applyGroupBySelections(groupFilterQb, groupByFields, alias); - const aggregateAliasMap = this.crudHelperService.applyAggregates(groupFilterQb, basicFilterDto.aggregates, alias); - const sortAliasMap = { ...groupAliasMap, ...aggregateAliasMap }; - this.crudHelperService.applyGroupSortingAndPagination(groupFilterQb, basicFilterDto.sort, sortAliasMap, limit, offset); - - const groupByResult = await groupFilterQb.getRawMany(); - const totalGroups = await this.crudHelperService.countGroups(groupFilterQb); - - const groupByFieldsOrdered = this.crudHelperService.normalize(basicFilterDto.groupBy || []); - const { groupMeta, groupRecords } = await this.handleGroupFind(groupByResult, groupFilter, populateGroup, alias, populateUserIdFields, populateMedia, basicFilterDto, groupAliasMap, aggregateAliasMap, groupByFieldsOrdered, groupFormatMap, groupExpressionMap); - - return { - meta: { - "totalRecords": totalGroups - }, - groupMeta, - groupRecords, - } + ); } else { qb = (internationalisation && draftPublishWorkflow) @@ -566,78 +543,8 @@ export class CRUDService { // Add two generic value i.e return this.wrapFindResponse(offset, limit, count, entities); } - private async handleGroupFind( - groupByResult: any[], - groupFilter: BasicGroupFilterDto | undefined, - populateGroup: boolean, - alias: string, - populateUserIdFields: UserIdFields[], - populateMedia: string[], - baseFilterDto: BasicFilterDto, - groupAliasMap: Record, - aggregateAliasMap: Record, - groupByFieldsOrdered: string[], - groupFormatMap: Record, - groupExpressionMap: Record - ) { - const groupMeta = []; - const groupRecords = []; - const aggregateAliasSet = new Set(Object.values(aggregateAliasMap)); - // For each group, get the records and the count - for (const group of groupByResult) { - if (populateGroup) { - let groupByQb: SelectQueryBuilder = await this.repo.createSecurityRuleAwareQueryBuilder(alias); - const groupFilterDto: BasicFilterDto = { - ...baseFilterDto, - ...groupFilter, - groupBy: undefined, - aggregates: undefined, - // Only use explicit groupFilter.sort for record ordering; group-level sorts can contain - // group expressions (e.g. createdAt:day) that are invalid on record queries. - sort: groupFilter?.sort, - }; - groupByQb = this.crudHelperService.buildFilterQuery(groupByQb, groupFilterDto, alias); - groupByQb = this.crudHelperService.buildGroupByRecordsQuery(groupByQb, group, alias, groupAliasMap, aggregateAliasMap, groupExpressionMap); - const [entities, count] = await groupByQb.getManyAndCount(); - - // Populate the entity with the userId fields - if (populateUserIdFields && populateUserIdFields.length > 0) { - await this.handlePopulateUserIdFields(populateUserIdFields, entities); - } - - // Populate the entity with the media - if (populateMedia && populateMedia.length > 0) { - await this.handlePopulateMedia(populateMedia, entities); - } - const groupData = this.wrapFindResponse(groupFilter?.offset, groupFilter?.limit, count, entities); - groupRecords.push(this.crudHelperService.createGroupRecords(group, aggregateAliasSet, groupData, groupByFieldsOrdered, groupAliasMap, groupFormatMap)); - } - groupMeta.push(this.crudHelperService.createGroupMeta(group, aggregateAliasSet, groupByFieldsOrdered, groupAliasMap, groupFormatMap)); - } - return { groupMeta, groupRecords }; - } - private wrapFindResponse(offset: number | undefined, limit: number | undefined, count: number, entities: T[]) { - const safeLimit = limit ?? count ?? 0; - const safeOffset = offset ?? 0; - const currentPage = safeLimit ? Math.floor(safeOffset / safeLimit) + 1 : 1; - const totalPages = safeLimit ? Math.ceil(count / safeLimit) : 1; - - const nextPage = safeLimit && currentPage < totalPages ? currentPage + 1 : null; - const prevPage = safeLimit && currentPage > 1 ? currentPage - 1 : null; - - const r = { - meta: { - totalRecords: count, - currentPage: currentPage, - nextPage: nextPage, - prevPage: prevPage, - totalPages: totalPages, - perPage: safeLimit ? +safeLimit : 0, - }, - records: entities - }; - return r; + return this.crudHelperService.pagedResponse(offset, limit, count, entities); } // entities is an array of T diff --git a/src/services/model-metadata.service.ts b/src/services/model-metadata.service.ts index aa3f44a1..d704006d 100755 --- a/src/services/model-metadata.service.ts +++ b/src/services/model-metadata.service.ts @@ -29,7 +29,7 @@ import { } from '../helpers/schematic.service'; import { classify } from '../helpers/string.helper'; import { CodeGenerationOptions } from '../interfaces'; -import { CrudHelperService } from './crud-helper.service'; +import { CrudHelperService, FilterCombinator } from './crud-helper.service'; import { CRUDService } from './crud.service'; import { FieldMetadataService } from './field-metadata.service'; import { MediaStorageProviderMetadataService } from './media-storage-provider-metadata.service'; @@ -68,36 +68,26 @@ export class ModelMetadataService { return this.findMany(basicFilterDto); } - async findMany(basicFilterDto: BasicFilterDto) { + async findMany(basicFilterDto: BasicFilterDto): Promise { const alias = 'modelMetadata'; - // Extract the required keys from the input query - let { limit, offset } = basicFilterDto; + const { limit, offset } = basicFilterDto; - // Create above query on pincode table using query builder - var qb: SelectQueryBuilder = await this.modelMetadataRepo.createSecurityRuleAwareQueryBuilder(alias) - qb = await this.crudHelperService.buildFilterQuery(qb, basicFilterDto, alias); + const qb: SelectQueryBuilder = await this.modelMetadataRepo.createSecurityRuleAwareQueryBuilder(alias); - // Get the records and the count - const [entities, count] = await qb.getManyAndCount(); - - const currentPage = Math.floor(offset / limit) + 1; - const totalPages = Math.ceil(count / limit); - - const nextPage = currentPage < totalPages ? currentPage + 1 : null; - const prevPage = currentPage > 1 ? currentPage - 1 : null; + if (basicFilterDto.groupBy?.length) { + const groupFilterQb = this.crudHelperService.buildFilterQuery( + qb, basicFilterDto, alias, undefined, undefined, undefined, + FilterCombinator.AND, false, false + ); + return this.crudHelperService.executeGroupPipeline( + groupFilterQb, basicFilterDto, alias, + () => this.modelMetadataRepo.createSecurityRuleAwareQueryBuilder(alias) + ); + } - const r = { - meta: { - totalRecords: count, - currentPage: currentPage, - nextPage: nextPage, - prevPage: prevPage, - totalPages: totalPages, - perPage: +limit, - }, - records: entities - }; - return r + const filteredQb = this.crudHelperService.buildFilterQuery(qb, basicFilterDto, alias); + const [entities, count] = await filteredQb.getManyAndCount(); + return this.crudHelperService.pagedResponse(offset, limit, count, entities); } async findOne(id: any, query?: any) { From 5df64e8860b12d2840d9c96ee1d5938d71d907ef Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Tue, 9 Jun 2026 14:52:26 +0530 Subject: [PATCH 096/136] Refactor field metadata actions and views: update action types, enhance searchability, and adjust permissions for create, edit, and delete operations. --- .../seed-data/solid-core-metadata.json | 249 ++++++------------ 1 file changed, 86 insertions(+), 163 deletions(-) diff --git a/src/seeders/seed-data/solid-core-metadata.json b/src/seeders/seed-data/solid-core-metadata.json index d45ef9b0..9ba61aac 100755 --- a/src/seeders/seed-data/solid-core-metadata.json +++ b/src/seeders/seed-data/solid-core-metadata.json @@ -5629,16 +5629,29 @@ { "displayName": "Field List Action", "name": "fieldMetadata-list-action", - "type": "custom", + "type": "solid", "domain": "", "context": "", - "customComponent": "/admin/core/solid-core/field-metadata/list", + "customComponent": "", "customIsModal": true, "serverEndpoint": "", "viewUserKey": "fieldMetadata-list-view", "moduleUserKey": "solid-core", "modelUserKey": "fieldMetadata" }, + { + "displayName": "Field Metadata Tree Action", + "name": "fieldMetadata-tree-action", + "type": "solid", + "domain": "", + "context": "", + "customComponent": "", + "customIsModal": true, + "serverEndpoint": "", + "viewUserKey": "fieldMetadata-tree-view", + "moduleUserKey": "solid-core", + "modelUserKey": "fieldMetadata" + }, { "displayName": "View Metadata List Action", "name": "viewMetadata-list-action", @@ -6853,7 +6866,8 @@ "name": "form-1", "label": "Model Metadata", "className": "grid", - "showAddFormButton": false + "showAddFormButton": false, + "showEditFormButton": false }, "children": [ { @@ -8839,275 +8853,182 @@ 50 ], "enableGlobalSearch": true, - "create": true, - "edit": true, - "delete": true + "create": false, + "edit": false, + "delete": false, + "import": false, + "export": false, + "allowedViews": [ + "list", + "tree" + ] }, "children": [ { "type": "field", "attrs": { - "name": "name", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "displayName", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "type", + "name": "model", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "ormType", - "isSearchable": true + "name": "name", + "isSearchable": true, + "sortable": true } }, { "type": "field", "attrs": { - "name": "model", + "name": "displayName", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "defaultValue", + "name": "type", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "regexPattern", - "isSearchable": true + "name": "ormType", + "isSearchable": false } }, { "type": "field", "attrs": { - "name": "regexPatternNotMatchingErrorMsg", - "isSearchable": true + "name": "relationType", + "isSearchable": false } }, { "type": "field", "attrs": { "name": "required", - "isSearchable": true + "isSearchable": false } }, { "type": "field", "attrs": { "name": "unique", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "encrypt", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "encryptionType", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "decryptWhen", - "isSearchable": true + "isSearchable": false } }, { "type": "field", "attrs": { "name": "index", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "length", - "isSearchable": true + "isSearchable": false } }, { "type": "field", "attrs": { - "name": "max", - "isSearchable": true + "name": "columnName", + "isSearchable": false } - }, + } + ] + } + }, + { + "name": "fieldMetadata-tree-view", + "displayName": "Field Metadata", + "type": "list", + "context": "{}", + "moduleUserKey": "solid-core", + "modelUserKey": "fieldMetadata", + "layout": { + "type": "list", + "attrs": { + "pagination": true, + "pageSizeOptions": [ + 10, + 25, + 50 + ], + "enableGlobalSearch": true, + "create": false, + "edit": false, + "delete": false + }, + "children": [ { "type": "field", "attrs": { - "name": "min", + "name": "model", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "private", - "isSearchable": true + "name": "name", + "isSearchable": true, + "sortable": true } }, { "type": "field", "attrs": { - "name": "mediaTypes", + "name": "displayName", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "mediaMaxSizeKb", + "name": "type", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "mediaStorageProvider", - "isSearchable": true + "name": "ormType", + "isSearchable": false } }, { "type": "field", "attrs": { "name": "relationType", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "relationCoModelSingularName", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "relationCreateInverse", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "relationCascade", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "relationModelModuleName", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "relationCoModelFieldName", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "selectionDynamicProvider", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "selectionDynamicProviderCtxt", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "selectionStaticValues", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "selectionValueType", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "computedFieldValueProvider", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "computedFieldValueProviderCtxt", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "computedFieldValueType", - "isSearchable": true + "isSearchable": false } }, { "type": "field", "attrs": { - "name": "uuid", - "isSearchable": true + "name": "required", + "isSearchable": false } }, { "type": "field", "attrs": { - "name": "isSystem", - "isSearchable": true + "name": "unique", + "isSearchable": false } }, { "type": "field", "attrs": { - "name": "isMarkedForRemoval", - "isSearchable": true + "name": "index", + "isSearchable": false } }, { "type": "field", "attrs": { "name": "columnName", - "isSearchable": true + "isSearchable": false } } ] @@ -9125,7 +9046,9 @@ "attrs": { "name": "form-1", "label": "Field Metadata", - "className": "grid" + "className": "grid", + "showAddFormButton": false, + "showEditFormButton": false }, "children": [ { From 937f84b45f196d40e51779fa9a0565216028879d Mon Sep 17 00:00:00 2001 From: Rajesh Chityal Date: Tue, 9 Jun 2026 16:48:14 +0530 Subject: [PATCH 097/136] many to one left join issue fixed --- src/services/crud-helper.service.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/services/crud-helper.service.ts b/src/services/crud-helper.service.ts index 723a76a3..dc46b76e 100755 --- a/src/services/crud-helper.service.ts +++ b/src/services/crud-helper.service.ts @@ -284,8 +284,10 @@ export class CrudHelperService { const value = orderOptions[key] as 'ASC' | 'DESC'; const field = String(key); if (field.includes('.')) { - const { alias, property } = this.ensureRelationPathJoined(qb, entityAlias, field.split('.')); - qb.addOrderBy(`${alias}.${property}`, value); + const { alias, property, created } = this.ensureRelationPathJoined(qb, entityAlias, field.split('.')); + const orderColumn = `${alias}.${property}`; + qb.addOrderBy(orderColumn, value); + if (created) qb.addSelect(orderColumn); } else { qb.addOrderBy(`${entityAlias}.${field}`, value); } @@ -347,6 +349,7 @@ export class CrudHelperService { qb.expressionMap?.aliases?.find(a => a.metadata)?.name || qb.expressionMap?.aliases?.[0]?.name; let parentAlias = mainAlias || rootAlias; + let leafJoinCreated = false; for (let i = 0; i < pathParts.length - 1; i++) { const part = pathParts[i]; const joinProperty = `${parentAlias}.${part}`; @@ -354,10 +357,13 @@ export class CrudHelperService { const joinAlias = existingAlias ?? this.sanitizeAlias(`${parentAlias}_${part}`); if (!existingAlias && !this.isRelationJoined(qb, joinProperty) && !this.isAliasJoined(qb, joinAlias)) { qb.leftJoin(joinProperty, joinAlias); + leafJoinCreated = true; + } else { + leafJoinCreated = false; } parentAlias = joinAlias; } - return { alias: parentAlias, property: pathParts[pathParts.length - 1] }; + return { alias: parentAlias, property: pathParts[pathParts.length - 1], created: leafJoinCreated }; } private getDriver(qb: SelectQueryBuilder) { From d45d7fc305b3989e0a8d38274e2c47ccddef724a Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Tue, 9 Jun 2026 14:46:53 +0100 Subject: [PATCH 098/136] Implement module package management: add controller, service, and DTOs for import, export, and build operations. --- src/controllers/module-package.controller.ts | 82 ++ src/dtos/confirm-module-package-import.dto.ts | 12 + src/dtos/run-module-package-build.dto.ts | 14 + src/dtos/run-module-package-seed.dto.ts | 12 + src/helpers/command.service.ts | 2 +- src/index.ts | 2 + .../seed-data/solid-core-metadata.json | 16 +- src/services/module-package.service.ts | 877 ++++++++++++++++++ src/solid-core.module.ts | 5 + 9 files changed, 1020 insertions(+), 2 deletions(-) create mode 100644 src/controllers/module-package.controller.ts create mode 100644 src/dtos/confirm-module-package-import.dto.ts create mode 100644 src/dtos/run-module-package-build.dto.ts create mode 100644 src/dtos/run-module-package-seed.dto.ts create mode 100644 src/services/module-package.service.ts diff --git a/src/controllers/module-package.controller.ts b/src/controllers/module-package.controller.ts new file mode 100644 index 00000000..ae820681 --- /dev/null +++ b/src/controllers/module-package.controller.ts @@ -0,0 +1,82 @@ +import { + Controller, + Get, + Param, + Post, + Body, + Res, + UploadedFiles, + UseInterceptors, +} from '@nestjs/common'; +import { AnyFilesInterceptor } from '@nestjs/platform-express'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { Response } from 'express'; +import { ConfirmModulePackageImportDto } from 'src/dtos/confirm-module-package-import.dto'; +import { RunModulePackageBuildDto } from 'src/dtos/run-module-package-build.dto'; +import { RunModulePackageSeedDto } from 'src/dtos/run-module-package-seed.dto'; +import { ModulePackageService } from 'src/services/module-package.service'; + +@Controller('module-packages') +@ApiTags('Solid Core') +export class ModulePackageController { + constructor( + private readonly modulePackageService: ModulePackageService, + ) { } + + @ApiBearerAuth('jwt') + @Post('import/validate') + @UseInterceptors(AnyFilesInterceptor()) + async validateImportPackage( + @UploadedFiles() files: Array, + ) { + return this.modulePackageService.validateUpload(files?.[0]); + } + + @ApiBearerAuth('jwt') + @Get('export/:moduleName') + async exportModulePackage( + @Param('moduleName') moduleName: string, + @Res() res: Response, + ) { + const archive = await this.modulePackageService.exportModulePackage(moduleName); + res.setHeader('Content-Disposition', `attachment; filename="${archive.fileName}"`); + res.setHeader('Content-Type', archive.mimeType); + res.setHeader('Access-Control-Expose-Headers', 'Content-Disposition, Content-Type'); + res.sendFile(archive.filePath); + } + + @ApiBearerAuth('jwt') + @Post('import/:id/confirm') + async confirmImport( + @Param('id') id: string, + @Body() dto: ConfirmModulePackageImportDto, + ) { + return this.modulePackageService.confirmImport(id, dto); + } + + @ApiBearerAuth('jwt') + @Get('import/:id/status') + async getImportStatus( + @Param('id') id: string, + ) { + return this.modulePackageService.getStatus(id); + } + + @ApiBearerAuth('jwt') + @Post('import/:id/build') + async runBuild( + @Param('id') id: string, + @Body() dto: RunModulePackageBuildDto, + ) { + return this.modulePackageService.runBuild(id, dto); + } + + @ApiBearerAuth('jwt') + @Post('import/:id/seed') + async runSeed( + @Param('id') id: string, + @Body() dto: RunModulePackageSeedDto, + ) { + return this.modulePackageService.runSeed(id, dto); + } +} diff --git a/src/dtos/confirm-module-package-import.dto.ts b/src/dtos/confirm-module-package-import.dto.ts new file mode 100644 index 00000000..9d16cefb --- /dev/null +++ b/src/dtos/confirm-module-package-import.dto.ts @@ -0,0 +1,12 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsBoolean, IsOptional } from 'class-validator'; + +export class ConfirmModulePackageImportDto { + @ApiPropertyOptional({ + description: 'Allow overwriting an existing module folder in solid-api and solid-ui.', + default: false, + }) + @IsOptional() + @IsBoolean() + overwriteExisting?: boolean = false; +} diff --git a/src/dtos/run-module-package-build.dto.ts b/src/dtos/run-module-package-build.dto.ts new file mode 100644 index 00000000..0006cade --- /dev/null +++ b/src/dtos/run-module-package-build.dto.ts @@ -0,0 +1,14 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsBoolean, IsOptional } from 'class-validator'; + +export class RunModulePackageBuildDto { + @ApiPropertyOptional({ default: true }) + @IsOptional() + @IsBoolean() + buildSolidApi?: boolean = true; + + @ApiPropertyOptional({ default: true }) + @IsOptional() + @IsBoolean() + buildSolidUi?: boolean = true; +} diff --git a/src/dtos/run-module-package-seed.dto.ts b/src/dtos/run-module-package-seed.dto.ts new file mode 100644 index 00000000..8d645811 --- /dev/null +++ b/src/dtos/run-module-package-seed.dto.ts @@ -0,0 +1,12 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsBoolean, IsOptional } from 'class-validator'; + +export class RunModulePackageSeedDto { + @ApiPropertyOptional({ + description: 'Whether to seed global metadata as part of the module seed run.', + default: false, + }) + @IsOptional() + @IsBoolean() + seedGlobalMetadata?: boolean = false; +} diff --git a/src/helpers/command.service.ts b/src/helpers/command.service.ts index 7bc3afcb..9ab604b0 100755 --- a/src/helpers/command.service.ts +++ b/src/helpers/command.service.ts @@ -66,7 +66,7 @@ export class CommandService { child.on('close', (code) => { if (code !== 0) { this.logger.error(`Command failed with code ${code}: ${command}`, stderr); - reject(new Error(stderr || `Command failed with exit code ${code}`)); + reject(new Error([stderr, stdout].filter(Boolean).join('\n').trim() || `Command failed with exit code ${code}`)); return; } resolve(stdout); diff --git a/src/index.ts b/src/index.ts index 9ab99315..b89a1127 100755 --- a/src/index.ts +++ b/src/index.ts @@ -303,6 +303,7 @@ export * from './services/menu-item-metadata.service' export * from './services/model-metadata.service' export * from './services/module-metadata-explorer.service' export * from './services/module-metadata.service' +export * from './services/module-package.service' export * from './services/mq-message-queue.service' export * from './services/mq-message.service' export * from './services/agent-session.service' @@ -337,6 +338,7 @@ export * from './services/encryption.service' export * from './services/info.service' export * from './controllers/info.controller' export * from './controllers/module-metadata-explorer.controller' +export * from './controllers/module-package.controller' export * from './services/settings/default-settings-provider.service' export * from './services/security-rule.service' export * from './services/request-context.service' diff --git a/src/seeders/seed-data/solid-core-metadata.json b/src/seeders/seed-data/solid-core-metadata.json index 9ba61aac..7d73eb23 100755 --- a/src/seeders/seed-data/solid-core-metadata.json +++ b/src/seeders/seed-data/solid-core-metadata.json @@ -6581,6 +6581,20 @@ "create": true, "edit": true, "delete": false, + "headerButtons": [ + { + "attrs": { + "label": "Import Module", + "action": "ModuleImportListHeaderAction", + "actionInContextMenu": false, + "openInPopup": true, + "icon": "si-upload", + "closable": false, + "popupWidth": "min(1180px, calc(100vw - 32px))", + "customComponentIsSystem": true + } + } + ], "rowButtons": [ { "attrs": { @@ -13992,4 +14006,4 @@ } } ] -} \ No newline at end of file +} diff --git a/src/services/module-package.service.ts b/src/services/module-package.service.ts new file mode 100644 index 00000000..83922689 --- /dev/null +++ b/src/services/module-package.service.ts @@ -0,0 +1,877 @@ +import { + BadRequestException, + Injectable, + Logger, + NotFoundException, +} from '@nestjs/common'; +import { createHash } from 'crypto'; +import { DisallowInProduction } from 'src/decorators/disallow-in-production.decorator'; +import { ConfirmModulePackageImportDto } from 'src/dtos/confirm-module-package-import.dto'; +import { RunModulePackageBuildDto } from 'src/dtos/run-module-package-build.dto'; +import { RunModulePackageSeedDto } from 'src/dtos/run-module-package-seed.dto'; +import { CommandService } from 'src/helpers/command.service'; +import { ModuleMetadataHelperService } from 'src/helpers/module-metadata-helper.service'; +import { SolidRegistry } from 'src/helpers/solid-registry'; +import * as fs from 'fs/promises'; +import * as path from 'path'; +import { v4 as uuidv4 } from 'uuid'; + +type ModulePackageManifest = { + schemaVersion: string; + packageType: string; + exportedAt?: string; + generatedBy?: { + name: string; + version?: string; + }; + module: { + name: string; + displayName?: string; + description?: string; + }; + contents: { + metadataPath: string; + apiModulePath: string; + uiModulePath: string; + }; + compatibility?: Record; + postImport?: Record; + checksums?: Record; +}; + +type ValidationPayload = { + valid: boolean; + errors: string[]; + warnings: string[]; +}; + +type PreviewPayload = { + module: { + name: string; + displayName?: string; + description?: string; + }; + manifest: ModulePackageManifest; + contentsSummary: { + totalEntries: number; + solidApiEntries: number; + solidUiEntries: number; + }; + requiredPaths: { + metadataPath: string; + apiModulePath: string; + uiModulePath: string; + }; + conflicts: string[]; + fileTree: string[]; + nextActions: string[]; +}; + +type ParsedArchive = { + entries: string[]; + archiveRootPrefix: string | null; + manifest: ModulePackageManifest; + metadataDocument: any; + validation: ValidationPayload; + preview: PreviewPayload; +}; + +type ModulePackageStatusFile = { + transactionKey: string; + status: string; + currentStep: string; + moduleName: string | null; + moduleDisplayName: string | null; + archiveFileName: string | null; + archiveFilePath: string | null; + extractDirPath: string | null; + archiveRootPrefix: string | null; + manifest: ModulePackageManifest | null; + metadataDocument: any; + preview: PreviewPayload | null; + validation: ValidationPayload | null; + conflicts: string[]; + outputs: { + import: string | null; + build: string | null; + seed: string | null; + }; + errorMessage: string | null; + createdAt: string; + updatedAt: string; +}; + +type ModulePackageExportFile = { + fileName: string; + filePath: string; + mimeType: string; +}; + +enum ModulePackageStatus { + uploaded = 'uploaded', + validated = 'validated', + awaiting_confirmation = 'awaiting_confirmation', + import_running = 'import_running', + awaiting_restart = 'awaiting_restart', + build_running = 'build_running', + build_failed = 'build_failed', + build_succeeded = 'build_succeeded', + seed_running = 'seed_running', + seed_failed = 'seed_failed', + completed = 'completed', + failed = 'failed', +} + +@Injectable() +export class ModulePackageService { + private readonly logger = new Logger(ModulePackageService.name); + private static readonly SUPPORTED_SCHEMA_VERSION = '1.0'; + private static readonly SUPPORTED_PACKAGE_TYPE = 'solidx-module'; + + constructor( + private readonly commandService: CommandService, + private readonly solidRegistry: SolidRegistry, + private readonly moduleMetadataHelperService: ModuleMetadataHelperService, + ) { } + + @DisallowInProduction() + async validateUpload(file: Express.Multer.File) { + if (!file) { + throw new BadRequestException('A .sldx archive file is required.'); + } + + const transactionKey = uuidv4(); + const workingDir = await this.createWorkingDir(transactionKey); + const archiveFileName = file.originalname || path.basename(file.path); + const archiveFilePath = path.join(workingDir, archiveFileName); + const extractDirPath = path.join(workingDir, 'extract'); + + await fs.mkdir(extractDirPath, { recursive: true }); + await fs.rename(file.path, archiveFilePath); + + const parsed = await this.parseArchive(archiveFilePath); + await this.extractArchive(archiveFilePath, extractDirPath); + + const transaction: ModulePackageStatusFile = { + transactionKey, + status: parsed.validation.valid ? ModulePackageStatus.awaiting_confirmation : ModulePackageStatus.failed, + currentStep: 'preview', + moduleName: parsed.manifest.module.name, + moduleDisplayName: parsed.manifest.module.displayName ?? null, + archiveFileName, + archiveFilePath, + extractDirPath, + archiveRootPrefix: parsed.archiveRootPrefix, + manifest: parsed.manifest, + metadataDocument: parsed.metadataDocument, + preview: parsed.preview, + validation: parsed.validation, + conflicts: parsed.preview.conflicts, + outputs: { + import: null, + build: null, + seed: null, + }, + errorMessage: parsed.validation.valid ? null : parsed.validation.errors.join('\n'), + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }; + + await this.writeStatusFile(transactionKey, transaction); + return this.toStatusResponse(transaction); + } + + async getStatus(transactionKey: string) { + const transaction = await this.loadTransaction(transactionKey); + return this.toStatusResponse(transaction); + } + + @DisallowInProduction() + async exportModulePackage(moduleNameInput: string): Promise { + const moduleName = (moduleNameInput ?? '').trim(); + if (!moduleName) { + throw new BadRequestException('A module name is required to export a module package.'); + } + + const expectedPaths = this.buildExpectedPaths(moduleName); + const solidApiModulePath = this.getSolidApiModuleTargetPath(moduleName); + const solidUiModulePath = this.getSolidUiModuleTargetPath(moduleName); + const metadataFilePath = await this.moduleMetadataHelperService.getModuleMetadataFilePath(moduleName); + + await this.assertPathExists(solidApiModulePath, 'solid-api module path'); + await this.assertPathExists(solidUiModulePath, 'solid-ui module path'); + await this.assertPathExists(metadataFilePath, 'module metadata file'); + + const metadataDocument = this.parseMetadataDocument( + await fs.readFile(metadataFilePath, 'utf-8'), + [], + ); + + const transactionKey = uuidv4(); + const exportRoot = path.join(this.getModulePackageExportsRoot(), transactionKey); + const stagingDir = path.join(exportRoot, 'staging'); + const archiveFileName = `${moduleName}.sldx`; + const archiveFilePath = path.join(exportRoot, archiveFileName); + + await fs.mkdir(path.join(stagingDir, 'solid-api', 'src'), { recursive: true }); + await fs.mkdir(path.join(stagingDir, 'solid-ui', 'src'), { recursive: true }); + + await fs.cp(solidApiModulePath, path.join(stagingDir, 'solid-api', 'src', moduleName), { recursive: true }); + await fs.cp(solidUiModulePath, path.join(stagingDir, 'solid-ui', 'src', moduleName), { recursive: true }); + + const manifest = await this.buildExportManifest(moduleName, metadataDocument); + await fs.writeFile( + path.join(stagingDir, 'manifest.json'), + JSON.stringify(manifest, null, 2), + 'utf-8', + ); + + await this.commandService.executeCommandWithArgs({ + command: 'zip', + args: ['-rq', archiveFilePath, '.'], + cwd: stagingDir, + }); + + return { + fileName: archiveFileName, + filePath: archiveFilePath, + mimeType: 'application/octet-stream', + }; + } + + @DisallowInProduction() + async confirmImport(transactionKey: string, dto: ConfirmModulePackageImportDto = {}) { + const transaction = await this.loadTransaction(transactionKey); + const preview = transaction.preview; + const validation = transaction.validation; + + if (!preview || !validation) { + throw new BadRequestException('The module package transaction is missing preview or validation data.'); + } + + if (!validation.valid) { + throw new BadRequestException({ + message: 'The uploaded module package is invalid and cannot be imported.', + validation, + }); + } + + const overwriteExisting = dto.overwriteExisting ?? false; + if (preview.conflicts.length > 0 && !overwriteExisting) { + throw new BadRequestException({ + message: 'Conflicts were detected. Re-submit with overwriteExisting=true to continue.', + conflicts: preview.conflicts, + }); + } + + transaction.status = ModulePackageStatus.import_running; + transaction.currentStep = 'import'; + transaction.errorMessage = null; + await this.writeStatusFile(transactionKey, transaction); + + try { + const moduleName = preview.module.name; + const solidApiTarget = this.getSolidApiModuleTargetPath(moduleName); + const solidUiTarget = this.getSolidUiModuleTargetPath(moduleName); + const extractRoot = transaction.archiveRootPrefix + ? path.join(transaction.extractDirPath as string, transaction.archiveRootPrefix.replace(/\/$/, '')) + : transaction.extractDirPath as string; + const solidApiSource = path.join(extractRoot, 'solid-api', 'src', moduleName); + const solidUiSource = path.join(extractRoot, 'solid-ui', 'src', moduleName); + + await this.assertPathExists(solidApiSource, 'solid-api module source'); + await this.assertPathExists(solidUiSource, 'solid-ui module source'); + + if (overwriteExisting) { + await fs.rm(solidApiTarget, { recursive: true, force: true }); + await fs.rm(solidUiTarget, { recursive: true, force: true }); + } + + await fs.mkdir(path.dirname(solidApiTarget), { recursive: true }); + await fs.mkdir(path.dirname(solidUiTarget), { recursive: true }); + + await fs.cp(solidApiSource, solidApiTarget, { recursive: true, force: overwriteExisting }); + await fs.cp(solidUiSource, solidUiTarget, { recursive: true, force: overwriteExisting }); + + if (transaction.extractDirPath) { + await fs.rm(transaction.extractDirPath, { recursive: true, force: true }); + transaction.extractDirPath = null; + } + + transaction.status = ModulePackageStatus.awaiting_restart; + transaction.currentStep = 'restart'; + transaction.outputs.import = [ + `Placed solid-api module at ${solidApiTarget}`, + `Placed solid-ui module at ${solidUiTarget}`, + `Metadata file placed at ${preview.requiredPaths.metadataPath}`, + ].join('\n'); + await this.writeStatusFile(transactionKey, transaction); + + return this.toStatusResponse(transaction); + } catch (error: any) { + transaction.status = ModulePackageStatus.failed; + transaction.currentStep = 'import'; + transaction.errorMessage = error.message ?? 'Failed to import the module package.'; + await this.writeStatusFile(transactionKey, transaction); + throw error; + } + } + + @DisallowInProduction() + async runBuild(transactionKey: string, dto: RunModulePackageBuildDto = {}) { + const transaction = await this.loadTransaction(transactionKey); + transaction.status = ModulePackageStatus.build_running; + transaction.currentStep = 'build'; + transaction.errorMessage = null; + await this.writeStatusFile(transactionKey, transaction); + + try { + const buildSolidApi = dto.buildSolidApi ?? true; + const buildSolidUi = dto.buildSolidUi ?? true; + const outputs: string[] = []; + const failedTargets: string[] = []; + + if (buildSolidApi) { + try { + const output = await this.commandService.executeCommandWithArgs({ + command: 'npm', + args: ['run', 'build'], + cwd: this.getSolidApiRoot(), + }); + outputs.push(`solid-api build [success]\n${output}`.trim()); + } catch (error: any) { + failedTargets.push('solid-api'); + outputs.push(`solid-api build [failed]\n${error?.message ?? 'Build failed.'}`.trim()); + } + } + + if (buildSolidUi) { + try { + const output = await this.commandService.executeCommandWithArgs({ + command: 'npm', + args: ['run', 'build'], + cwd: this.getSolidUiRoot(), + }); + outputs.push(`solid-ui build [success]\n${output}`.trim()); + } catch (error: any) { + failedTargets.push('solid-ui'); + outputs.push(`solid-ui build [failed]\n${error?.message ?? 'Build failed.'}`.trim()); + } + } + + transaction.status = failedTargets.length > 0 + ? ModulePackageStatus.build_failed + : ModulePackageStatus.build_succeeded; + transaction.currentStep = 'build'; + transaction.outputs.build = outputs.join('\n\n'); + transaction.errorMessage = failedTargets.length > 0 + ? `Build completed with errors in: ${failedTargets.join(', ')}` + : null; + await this.writeStatusFile(transactionKey, transaction); + + return this.toStatusResponse(transaction); + } catch (error: any) { + transaction.status = ModulePackageStatus.build_failed; + transaction.currentStep = 'build'; + transaction.errorMessage = error.message ?? 'Build failed.'; + transaction.outputs.build = error.message ?? ''; + await this.writeStatusFile(transactionKey, transaction); + throw error; + } + } + + @DisallowInProduction() + async runSeed(transactionKey: string, dto: RunModulePackageSeedDto = {}) { + const transaction = await this.loadTransaction(transactionKey); + const moduleName = transaction.moduleName; + + if (!moduleName) { + throw new BadRequestException('Unable to seed because the transaction is missing moduleName.'); + } + + transaction.status = ModulePackageStatus.seed_running; + transaction.currentStep = 'seed'; + transaction.errorMessage = null; + await this.writeStatusFile(transactionKey, transaction); + + try { + const seeder = this.solidRegistry + .getSeeders() + .filter((registeredSeeder) => registeredSeeder.name === 'ModuleMetadataSeederService') + .map((registeredSeeder) => registeredSeeder.instance) + .pop(); + + if (!seeder || typeof seeder.seed !== 'function') { + throw new NotFoundException('ModuleMetadataSeederService not found. Cannot seed imported module.'); + } + + await seeder.seed({ + modulesToSeed: [moduleName], + seedGlobalMetadata: dto.seedGlobalMetadata ?? false, + }); + + transaction.status = ModulePackageStatus.completed; + transaction.currentStep = 'done'; + transaction.outputs.seed = [ + `Seeded module metadata for ${moduleName}.`, + `seedGlobalMetadata=${dto.seedGlobalMetadata ?? false}`, + ].join('\n'); + await this.writeStatusFile(transactionKey, transaction); + + return this.toStatusResponse(transaction); + } catch (error: any) { + transaction.status = ModulePackageStatus.seed_failed; + transaction.currentStep = 'seed'; + transaction.errorMessage = error.message ?? 'Seed failed.'; + transaction.outputs.seed = error.message ?? ''; + await this.writeStatusFile(transactionKey, transaction); + return this.toStatusResponse(transaction); + } + } + + private async parseArchive(archiveFilePath: string): Promise { + const entries = await this.listArchiveEntries(archiveFilePath); + const archiveRootPrefix = this.detectArchiveRootPrefix(entries); + const normalizedEntries = this.normalizeArchiveEntries(entries, archiveRootPrefix); + const validationErrors: string[] = []; + const validationWarnings: string[] = []; + + if (path.extname(archiveFilePath).toLowerCase() !== '.sldx') { + validationErrors.push('Only .sldx archives are supported.'); + } + + const unsafeEntries = normalizedEntries.filter((entry) => this.isUnsafeArchivePath(entry)); + if (unsafeEntries.length > 0) { + validationErrors.push(`Archive contains unsafe paths: ${unsafeEntries.join(', ')}`); + } + + if (!normalizedEntries.includes('manifest.json')) { + validationErrors.push('Archive is missing manifest.json at the root.'); + } + + const manifestArchivePath = this.toArchiveEntryPath(archiveRootPrefix, 'manifest.json'); + const manifestText = normalizedEntries.includes('manifest.json') + ? await this.readArchiveEntry(archiveFilePath, manifestArchivePath) + : ''; + const manifest = this.parseManifest(manifestText, validationErrors); + + if (!manifest) { + return { + entries: normalizedEntries, + archiveRootPrefix, + manifest: this.getFallbackManifest(), + metadataDocument: null, + validation: { + valid: false, + errors: validationErrors, + warnings: validationWarnings, + }, + preview: { + module: { name: '', displayName: '' }, + manifest: this.getFallbackManifest(), + contentsSummary: { + totalEntries: normalizedEntries.length, + solidApiEntries: normalizedEntries.filter((entry) => entry.startsWith('solid-api/')).length, + solidUiEntries: normalizedEntries.filter((entry) => entry.startsWith('solid-ui/')).length, + }, + requiredPaths: { + metadataPath: '', + apiModulePath: '', + uiModulePath: '', + }, + conflicts: [], + fileTree: normalizedEntries, + nextActions: ['Fix archive structure and upload again.'], + }, + }; + } + + if (manifest.packageType !== ModulePackageService.SUPPORTED_PACKAGE_TYPE) { + validationErrors.push(`manifest.json packageType must be "${ModulePackageService.SUPPORTED_PACKAGE_TYPE}".`); + } + if (manifest.schemaVersion !== ModulePackageService.SUPPORTED_SCHEMA_VERSION) { + validationErrors.push(`manifest.json schemaVersion must be "${ModulePackageService.SUPPORTED_SCHEMA_VERSION}".`); + } + if (!manifest.module?.name) { + validationErrors.push('manifest.json module.name is required.'); + } + + const moduleName = manifest.module?.name ?? ''; + const expectedPaths = this.buildExpectedPaths(moduleName); + + if (manifest.contents?.metadataPath !== expectedPaths.metadataPath) { + validationErrors.push(`manifest.json contents.metadataPath must be "${expectedPaths.metadataPath}".`); + } + if (manifest.contents?.apiModulePath !== expectedPaths.apiModulePath) { + validationErrors.push(`manifest.json contents.apiModulePath must be "${expectedPaths.apiModulePath}".`); + } + if (manifest.contents?.uiModulePath !== expectedPaths.uiModulePath) { + validationErrors.push(`manifest.json contents.uiModulePath must be "${expectedPaths.uiModulePath}".`); + } + + if (!normalizedEntries.includes(expectedPaths.metadataPath)) { + validationErrors.push(`Archive is missing ${expectedPaths.metadataPath}.`); + } + if (!normalizedEntries.includes(expectedPaths.apiModulePath)) { + validationErrors.push(`Archive is missing ${expectedPaths.apiModulePath}.`); + } + if (!normalizedEntries.includes(expectedPaths.uiModulePath)) { + validationErrors.push(`Archive is missing ${expectedPaths.uiModulePath}.`); + } + + const metadataDocument = normalizedEntries.includes(expectedPaths.metadataPath) + ? this.parseMetadataDocument( + await this.readArchiveEntry(archiveFilePath, this.toArchiveEntryPath(archiveRootPrefix, expectedPaths.metadataPath)), + validationErrors, + ) + : null; + + const metadataModuleName = metadataDocument?.moduleMetadata?.name; + if (metadataModuleName && metadataModuleName !== moduleName) { + validationErrors.push(`Metadata module name "${metadataModuleName}" does not match manifest module name "${moduleName}".`); + } + + if (!manifest.checksums || Object.keys(manifest.checksums).length === 0) { + validationWarnings.push('manifest.json does not include checksums. Checksums are recommended.'); + } + + const conflicts = await this.detectConflicts(moduleName); + if (conflicts.length > 0) { + validationWarnings.push('The target module already exists locally and will require overwrite confirmation.'); + } + + const validation: ValidationPayload = { + valid: validationErrors.length === 0, + errors: validationErrors, + warnings: validationWarnings, + }; + + const preview: PreviewPayload = { + module: manifest.module, + manifest, + contentsSummary: { + totalEntries: normalizedEntries.length, + solidApiEntries: normalizedEntries.filter((entry) => entry.startsWith('solid-api/')).length, + solidUiEntries: normalizedEntries.filter((entry) => entry.startsWith('solid-ui/')).length, + }, + requiredPaths: expectedPaths, + conflicts, + fileTree: normalizedEntries, + nextActions: [ + 'Review archive preview.', + 'Confirm import to place files locally.', + 'Wait for service restart, then run build and seed.', + ], + }; + + return { + entries: normalizedEntries, + archiveRootPrefix, + manifest, + metadataDocument, + validation, + preview, + }; + } + + private parseManifest(manifestText: string, validationErrors: string[]): ModulePackageManifest | null { + if (!manifestText) { + return null; + } + + try { + return JSON.parse(manifestText) as ModulePackageManifest; + } catch (error) { + validationErrors.push('manifest.json is not valid JSON.'); + return null; + } + } + + private parseMetadataDocument(documentText: string, validationErrors: string[]) { + try { + return JSON.parse(documentText); + } catch (error) { + validationErrors.push('Module metadata JSON is not valid JSON.'); + return null; + } + } + + private async listArchiveEntries(archiveFilePath: string): Promise { + const output = await this.commandService.executeCommandWithArgs({ + command: 'unzip', + args: ['-Z1', archiveFilePath], + }); + + return output + .split(/\r?\n/) + .map((entry) => entry.trim()) + .filter(Boolean); + } + + private async readArchiveEntry(archiveFilePath: string, entryPath: string): Promise { + return this.commandService.executeCommandWithArgs({ + command: 'unzip', + args: ['-p', archiveFilePath, entryPath], + }); + } + + private async extractArchive(archiveFilePath: string, extractDirPath: string) { + await this.commandService.executeCommandWithArgs({ + command: 'unzip', + args: ['-o', archiveFilePath, '-d', extractDirPath], + }); + } + + private async detectConflicts(moduleName: string): Promise { + const conflicts: string[] = []; + const solidApiTarget = this.getSolidApiModuleTargetPath(moduleName); + const solidUiTarget = this.getSolidUiModuleTargetPath(moduleName); + const metadataFilePath = await this.moduleMetadataHelperService.getModuleMetadataFilePath(moduleName); + + if (await this.pathExists(solidApiTarget)) { + conflicts.push(`solid-api module already exists at ${solidApiTarget}`); + } + if (await this.pathExists(solidUiTarget)) { + conflicts.push(`solid-ui module already exists at ${solidUiTarget}`); + } + if (await this.pathExists(metadataFilePath)) { + conflicts.push(`module metadata already exists at ${metadataFilePath}`); + } + + return conflicts; + } + + private buildExpectedPaths(moduleName: string) { + return { + metadataPath: `solid-api/src/${moduleName}/metadata/${moduleName}-metadata.json`, + apiModulePath: `solid-api/src/${moduleName}/${moduleName}.module.ts`, + uiModulePath: `solid-ui/src/${moduleName}/${moduleName}.ui-module.ts`, + }; + } + + private detectArchiveRootPrefix(entries: string[]): string | null { + const normalized = entries + .map((entry) => entry.trim()) + .filter(Boolean) + .filter((entry) => !this.isMacOsMetadataEntry(entry)); + + if (normalized.length === 0) { + return null; + } + + const topLevelSegments = normalized + .map((entry) => entry.split('/')[0]) + .filter(Boolean); + + const uniqueTopLevelSegments = Array.from(new Set(topLevelSegments)); + if (uniqueTopLevelSegments.length !== 1) { + return null; + } + + const onlySegment = uniqueTopLevelSegments[0]; + const hasNestedEntries = normalized.some((entry) => entry.startsWith(`${onlySegment}/`)); + return hasNestedEntries ? `${onlySegment}/` : null; + } + + private normalizeArchiveEntries(entries: string[], archiveRootPrefix: string | null) { + const filteredEntries = entries.filter((entry) => !this.isMacOsMetadataEntry(entry)); + + if (!archiveRootPrefix) { + return filteredEntries; + } + + return filteredEntries + .map((entry) => entry.startsWith(archiveRootPrefix) ? entry.slice(archiveRootPrefix.length) : entry) + .filter(Boolean); + } + + private toArchiveEntryPath(archiveRootPrefix: string | null, normalizedEntryPath: string) { + return archiveRootPrefix ? `${archiveRootPrefix}${normalizedEntryPath}` : normalizedEntryPath; + } + + private isUnsafeArchivePath(entry: string) { + return entry.startsWith('/') || entry.includes('..') || /^[A-Za-z]:/.test(entry); + } + + private isMacOsMetadataEntry(entry: string) { + const normalizedEntry = entry.trim(); + if (!normalizedEntry) { + return false; + } + + const segments = normalizedEntry.split('/').filter(Boolean); + return segments.some((segment) => segment === '__MACOSX' || segment.startsWith('._')); + } + + private getFallbackManifest(): ModulePackageManifest { + return { + schemaVersion: ModulePackageService.SUPPORTED_SCHEMA_VERSION, + packageType: ModulePackageService.SUPPORTED_PACKAGE_TYPE, + module: { name: '', displayName: '' }, + contents: { + metadataPath: '', + apiModulePath: '', + uiModulePath: '', + }, + }; + } + + private async loadTransaction(transactionKey: string) { + this.assertSafeTransactionKey(transactionKey); + const statusFilePath = this.getStatusFilePath(transactionKey); + + if (!(await this.pathExists(statusFilePath))) { + throw new NotFoundException(`Module package transaction ${transactionKey} was not found.`); + } + + return JSON.parse(await fs.readFile(statusFilePath, 'utf-8')) as ModulePackageStatusFile; + } + + private async createWorkingDir(transactionKey: string) { + const baseDir = path.join( + this.getModulePackageImportsRoot(), + transactionKey, + ); + + await fs.mkdir(baseDir, { recursive: true }); + return baseDir; + } + + private getSolidApiRoot() { + return process.cwd(); + } + + private getProjectRoot() { + return path.resolve(this.getSolidApiRoot(), '..'); + } + + private getSolidUiRoot() { + return path.join(this.getProjectRoot(), 'solid-ui'); + } + + private getModulePackageImportsRoot() { + return path.join( + this.getModulePackageRuntimeRoot(), + 'module-package-imports', + ); + } + + private getModulePackageExportsRoot() { + return path.join( + this.getModulePackageRuntimeRoot(), + 'module-package-exports', + ); + } + + private getModulePackageRuntimeRoot() { + const configuredRuntimeRoot = process.env.SOLIDX_MODULE_PACKAGE_RUNTIME_DIR?.trim(); + + if (configuredRuntimeRoot) { + return path.resolve(configuredRuntimeRoot); + } + + return path.join( + this.getProjectRoot(), + '.solidx-runtime', + ); + } + + private getSolidApiModuleTargetPath(moduleName: string) { + return path.join(this.getSolidApiRoot(), 'src', moduleName); + } + + private getSolidUiModuleTargetPath(moduleName: string) { + return path.join(this.getSolidUiRoot(), 'src', moduleName); + } + + private async pathExists(targetPath: string) { + try { + await fs.access(targetPath); + return true; + } catch (error) { + return false; + } + } + + private async assertPathExists(targetPath: string, label: string) { + if (!(await this.pathExists(targetPath))) { + throw new BadRequestException(`Expected ${label} at ${targetPath}, but it was not found in the extracted archive.`); + } + } + + private assertSafeTransactionKey(transactionKey: string) { + if (!/^[a-zA-Z0-9-]+$/.test(transactionKey)) { + throw new BadRequestException('Invalid module package transaction key.'); + } + } + + private getStatusFilePath(transactionKey: string) { + this.assertSafeTransactionKey(transactionKey); + return path.join(this.getModulePackageImportsRoot(), transactionKey, 'status.json'); + } + + private async writeStatusFile(transactionKey: string, payload: ModulePackageStatusFile) { + payload.updatedAt = new Date().toISOString(); + const statusFilePath = this.getStatusFilePath(transactionKey); + await fs.mkdir(path.dirname(statusFilePath), { recursive: true }); + await fs.writeFile(statusFilePath, JSON.stringify(payload, null, 2)); + } + + private toStatusResponse(transaction: ModulePackageStatusFile) { + return { + transactionKey: transaction.transactionKey, + status: transaction.status, + currentStep: transaction.currentStep, + moduleName: transaction.moduleName, + moduleDisplayName: transaction.moduleDisplayName, + archiveFileName: transaction.archiveFileName, + manifest: transaction.manifest, + preview: transaction.preview, + validation: transaction.validation, + conflicts: transaction.conflicts, + outputs: transaction.outputs, + errorMessage: transaction.errorMessage, + createdAt: transaction.createdAt, + updatedAt: transaction.updatedAt, + }; + } + + private async buildExportManifest(moduleName: string, metadataDocument: any): Promise { + const expectedPaths = this.buildExpectedPaths(moduleName); + const stagingPaths = [ + expectedPaths.metadataPath, + expectedPaths.apiModulePath, + expectedPaths.uiModulePath, + ]; + const checksums: Record = {}; + + for (const relativePath of stagingPaths) { + const absolutePath = path.join(this.getSolidApiRoot(), '..', relativePath); + checksums[relativePath] = await this.computeSha256(absolutePath); + } + + return { + schemaVersion: ModulePackageService.SUPPORTED_SCHEMA_VERSION, + packageType: ModulePackageService.SUPPORTED_PACKAGE_TYPE, + exportedAt: new Date().toISOString(), + generatedBy: { + name: '@solidxai/core', + }, + module: { + name: moduleName, + displayName: metadataDocument?.moduleMetadata?.displayName, + description: metadataDocument?.moduleMetadata?.description, + }, + contents: expectedPaths, + postImport: { + recommendedSteps: ['restart', 'build', 'seed'], + }, + checksums, + }; + } + + private async computeSha256(filePath: string): Promise { + const fileBuffer = await fs.readFile(filePath); + return createHash('sha256').update(fileBuffer).digest('hex'); + } +} diff --git a/src/solid-core.module.ts b/src/solid-core.module.ts index 63e99e89..1b87a062 100644 --- a/src/solid-core.module.ts +++ b/src/solid-core.module.ts @@ -19,6 +19,7 @@ import { MediaStorageProviderMetadataController } from "./controllers/media-stor import { ModelMetadataController } from "./controllers/model-metadata.controller"; import { ModuleMetadataExplorerController } from "./controllers/module-metadata-explorer.controller"; import { ModuleMetadataController } from "./controllers/module-metadata.controller"; +import { ModulePackageController } from "./controllers/module-package.controller"; import { TestController } from "./controllers/test.controller"; import { FieldMetadata } from "./entities/field-metadata.entity"; import { ListOfValues } from "./entities/list-of-values.entity"; @@ -44,6 +45,7 @@ import { MediaService } from "./services/media.service"; import { ModelMetadataService } from "./services/model-metadata.service"; import { ModuleMetadataExplorerService } from "./services/module-metadata-explorer.service"; import { ModuleMetadataService } from "./services/module-metadata.service"; +import { ModulePackageService } from "./services/module-package.service"; import { SolidIntrospectService } from "./services/solid-introspect.service"; // import { ListOfComputedFieldProvider } from './providers/list-of-computed-field-provider.service'; import { ServeStaticModule } from "@nestjs/serve-static"; @@ -489,6 +491,7 @@ import { DashboardUserLayoutRepository } from './repositories/dashboard-user-lay ModelMetadataController, ModuleMetadataExplorerController, ModuleMetadataController, + ModulePackageController, MqMessageController, MqMessageQueueController, GupshupWebhookController, @@ -539,6 +542,7 @@ import { DashboardUserLayoutRepository } from './repositories/dashboard-user-lay ModuleMetadataService, ModuleMetadataExplorerService, ModuleMetadataHelperService, + ModulePackageService, ModelMetadataService, ModelMetadataHelperService, FieldMetadataService, @@ -849,6 +853,7 @@ import { DashboardUserLayoutRepository } from './repositories/dashboard-user-lay ModelMetadataService, ModuleMetadataService, ModuleMetadataExplorerService, + ModulePackageService, MqMessageQueueService, MqMessageService, Msg91OTPService, From 457324371cec55c24a5f18219e771546d3484ed2 Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Tue, 9 Jun 2026 14:53:58 +0100 Subject: [PATCH 099/136] Enhance module metadata seeder: add validation for module name, streamline array handling for scheduled jobs, saved filters, list of values, security rules, sms templates, email templates, menus, actions, views, users, roles, media storage providers, model sequences, and models metadata. --- src/seeders/module-metadata-seeder.service.ts | 86 ++++++++++++------- 1 file changed, 53 insertions(+), 33 deletions(-) diff --git a/src/seeders/module-metadata-seeder.service.ts b/src/seeders/module-metadata-seeder.service.ts index b6fda72f..6b1cbdbc 100755 --- a/src/seeders/module-metadata-seeder.service.ts +++ b/src/seeders/module-metadata-seeder.service.ts @@ -144,6 +144,11 @@ export class ModuleMetadataSeederService { const moduleMetadata: CreateModuleMetadataDto = overallMetadata.moduleMetadata; currentModule = moduleMetadata?.name ?? 'unknown'; + if (!moduleMetadata?.name) { + this.logger.warn(`Skipping seed metadata file because moduleMetadata.name is missing.`); + continue; + } + console.log(`▶ Seeding Metadata for Module: ${moduleMetadata.name}`); this.logger.log(`Seeding Metadata for Module: ${moduleMetadata.name}`); @@ -247,33 +252,33 @@ export class ModuleMetadataSeederService { private async seedScheduledJobs(moduleMetadata: CreateModuleMetadataDto, overallMetadata: any): Promise<{ pruned: number; upserted: number }> { this.logger.debug(`[Start] Processing scheduled jobs for ${moduleMetadata.name}`); - const scheduledJobs: CreateScheduledJobDto[] = overallMetadata.scheduledJobs; + const scheduledJobs = this.getSeedArray(overallMetadata?.scheduledJobs); const pruned = this.enablePruning ? await this.pruneScheduledJobs(scheduledJobs, moduleMetadata.name) : 0; - if (scheduledJobs?.length > 0) { + if (scheduledJobs.length > 0) { await this.handleSeedScheduledJobs(scheduledJobs); } this.logger.debug(`[End] Processing scheduled jobs for ${moduleMetadata.name}`); - return { pruned, upserted: scheduledJobs?.length ?? 0 }; + return { pruned, upserted: scheduledJobs.length }; } private async seedSavedFilters(moduleMetadata: CreateModuleMetadataDto, overallMetadata: any): Promise<{ pruned: number; upserted: number }> { this.logger.debug(`[Start] Processing saved filters for ${moduleMetadata.name}`); - const savedFilters: CreateSavedFiltersDto[] = overallMetadata.savedFilters; + const savedFilters = this.getSeedArray(overallMetadata?.savedFilters); const pruned = this.enablePruning ? await this.pruneSavedFilters(savedFilters, moduleMetadata.name) : 0; - if (savedFilters?.length > 0) { + if (savedFilters.length > 0) { await this.handleSeedSavedFilters(savedFilters); } this.logger.debug(`[End] Processing saved filters for ${moduleMetadata.name}`); - return { pruned, upserted: savedFilters?.length ?? 0 }; + return { pruned, upserted: savedFilters.length }; } private async seedListOfValues(moduleMetadata: CreateModuleMetadataDto, overallMetadata: any): Promise<{ pruned: number; upserted: number }> { this.logger.debug(`[Start] Processing List Of Values for ${moduleMetadata.name}`); - const listOfValues: CreateListOfValuesDto[] = overallMetadata.listOfValues; + const listOfValues = this.getSeedArray(overallMetadata?.listOfValues); const pruned = this.enablePruning ? await this.pruneListOfValues(listOfValues, moduleMetadata.name) : 0; await this.handleSeedListOfValues(listOfValues); this.logger.debug(`[End] Processing List Of Values for ${moduleMetadata.name}`); - return { pruned, upserted: listOfValues?.length ?? 0 }; + return { pruned, upserted: listOfValues.length }; } private async setupDefaultRolesWithPermissions() { @@ -291,11 +296,11 @@ export class ModuleMetadataSeederService { private async seedSecurityRules(overallMetadata: any): Promise<{ pruned: number; upserted: number }> { this.logger.debug(`[Start] Processing security rules`); - const securityRules: CreateSecurityRuleDto[] = overallMetadata.securityRules; + const securityRules = this.getSeedArray(overallMetadata?.securityRules); const pruned = this.enablePruning ? await this.pruneSecurityRules(securityRules, overallMetadata?.moduleMetadata?.name) : 0; await this.handleSeedSecurityRules(securityRules); this.logger.debug(`[End] Processing security rules`); - return { pruned, upserted: securityRules?.length ?? 0 }; + return { pruned, upserted: securityRules.length }; } // Ok @@ -307,75 +312,85 @@ export class ModuleMetadataSeederService { private async seedSmsTemplates(overallMetadata: any, moduleName: string): Promise<{ pruned: number; upserted: number }> { this.logger.debug(`[Start] Processing sms templates`); - const smsTemplates: CreateSmsTemplateDto[] = overallMetadata.smsTemplates; + const smsTemplates = this.getSeedArray(overallMetadata?.smsTemplates); await this.handleSeedSmsTemplates(smsTemplates, moduleName); this.logger.debug(`[End] Processing sms templates`); - return { pruned: 0, upserted: smsTemplates?.length ?? 0 }; + return { pruned: 0, upserted: smsTemplates.length }; } // OK private async seedEmailTemplates(overallMetadata: any, moduleName: string): Promise<{ pruned: number; upserted: number }> { this.logger.debug(`[Start] Processing email templates`); - const emailTemplates: CreateEmailTemplateDto[] = overallMetadata.emailTemplates; + const emailTemplates = this.getSeedArray(overallMetadata?.emailTemplates); await this.handleSeedEmailTemplates(emailTemplates, moduleName); this.logger.debug(`[End] Processing email templates`); - return { pruned: 0, upserted: emailTemplates?.length ?? 0 }; + return { pruned: 0, upserted: emailTemplates.length }; } // Ok private async seedMenus(overallMetadata: any): Promise<{ pruned: number; upserted: number }> { this.logger.debug(`[Start] Processing menus`); - const menus = overallMetadata.menus; + const menus = this.getSeedArray(overallMetadata?.menus); const pruned = this.enablePruning ? await this.pruneMenus(menus, overallMetadata?.moduleMetadata?.name) : 0; await this.handleSeedMenus(menus); this.logger.debug(`[End] Processing menus`); - return { pruned, upserted: menus?.length ?? 0 }; + return { pruned, upserted: menus.length }; } // Ok private async seedActions(overallMetadata: any): Promise<{ pruned: number; upserted: number }> { this.logger.debug(`[Start] Processing actions`); - const actions = overallMetadata.actions; + const actions = this.getSeedArray(overallMetadata?.actions); const pruned = this.enablePruning ? await this.pruneActions(actions, overallMetadata?.moduleMetadata?.name) : 0; await this.handleSeedActions(actions); this.logger.debug(`[End] Processing actions`); - return { pruned, upserted: actions?.length ?? 0 }; + return { pruned, upserted: actions.length }; } // Ok private async seedViews(overallMetadata: any): Promise<{ pruned: number; upserted: number }> { this.logger.debug(`[Start] Processing views`); - const views = overallMetadata.views; + const views = this.getSeedArray(overallMetadata?.views); const pruned = this.enablePruning ? await this.pruneViews(views, overallMetadata?.moduleMetadata?.name) : 0; await this.handleSeedViews(views); this.logger.debug(`[End] Processing views`); - return { pruned, upserted: views?.length ?? 0 }; + return { pruned, upserted: views.length }; } // Ok private async seedUsers(overallMetadata: any): Promise<{ pruned: number; upserted: number }> { this.logger.debug(`[Start] Processing users`); - const users = overallMetadata.users; + const users = this.getSeedArray(overallMetadata?.users); // usersDetail = users; await this.handleSeedUsers(users); this.logger.debug(`[End] Processing users`); - return { pruned: 0, upserted: users?.length ?? 0 }; + return { pruned: 0, upserted: users.length }; } // OK private async seedRoles(overallMetadata: any): Promise<{ pruned: number; upserted: number }> { this.logger.debug(`[Start] Processing roles`); + const roles = this.getSeedArray(overallMetadata?.roles); // While creating roles we are only passing the role name to be used. - await this.roleService.createRolesIfNotExists(overallMetadata.roles.map(role => { return { name: role.name }; })); + await this.roleService.createRolesIfNotExists( + roles + .filter((role) => role?.name) + .map((role) => ({ name: role.name } as any)), + ); // After roles are created, we iterate over all roles and attach permissions (if specified in the seeder json) to the respective role. // Every role configuration in the seeder json can optionally have a permissions attribute. - for (const role of overallMetadata.roles) { + for (const role of roles) { if (role.permissions) { - await this.roleService.addPermissionsToRole(role.name, role.permissions); + await this.roleService.addPermissionsToRole( + role.name, + role.permissions + .map((permission: any) => typeof permission === 'string' ? permission : permission?.name) + .filter(Boolean), + ); } } this.logger.debug(`[End] Processing roles`); - return { pruned: 0, upserted: overallMetadata.roles?.length ?? 0 }; + return { pruned: 0, upserted: roles.length }; } private async seedPermissions(overallMetadata: any): Promise<{ pruned: number; upserted: number }> { @@ -494,22 +509,23 @@ export class ModuleMetadataSeederService { // OK private async seedMediaStorageProviders(mediaStorageProviders: any[]): Promise<{ pruned: number; upserted: number }> { this.logger.debug(`[Start] Processing Media Storage Provider`); + const providers = this.getSeedArray(mediaStorageProviders); - for (let i = 0; i < mediaStorageProviders.length; i++) { - const mediaStorageProvider = mediaStorageProviders[i]; + for (let i = 0; i < providers.length; i++) { + const mediaStorageProvider = providers[i]; await this.mediaStorageProviderMetadataService.upsert(mediaStorageProvider); } this.logger.debug(`[End] Processing Media Storage Provider`); - return { pruned: 0, upserted: mediaStorageProviders?.length ?? 0 }; + return { pruned: 0, upserted: providers.length }; } private async seedModelSequences(overallMetadata: any): Promise<{ pruned: number; upserted: number }> { this.logger.debug(`[Start] Processing model sequences`); - const modelSequences: CreateModelSequenceDto[] = overallMetadata.modelSequences; + const modelSequences = this.getSeedArray(overallMetadata?.modelSequences); const pruned = this.enablePruning ? await this.pruneModelSequences(modelSequences, overallMetadata?.moduleMetadata?.name) : 0; await this.handleSeedModelSequences(modelSequences); this.logger.debug(`[End] Processing model sequences`); - return { pruned, upserted: modelSequences?.length ?? 0 }; + return { pruned, upserted: modelSequences.length }; } // OK @@ -832,8 +848,8 @@ export class ModuleMetadataSeederService { let upserted = 1; // Next create all the models. - const modelsMetadata: CreateModelMetadataDto[] = moduleMetadata.models; - upserted += modelsMetadata?.length ?? 0; + const modelsMetadata = this.getSeedArray(moduleMetadata?.models); + upserted += modelsMetadata.length; for (let j = 0; j < modelsMetadata.length; j++) { const modelMetadata = modelsMetadata[j]; @@ -887,6 +903,10 @@ export class ModuleMetadataSeederService { return { pruned, upserted }; } + private getSeedArray(value: T[] | null | undefined): T[] { + return Array.isArray(value) ? value : []; + } + private async handleSeedSecurityRules(rulesDto: CreateSecurityRuleDto[]) { if (!rulesDto || rulesDto.length === 0) { this.logger.debug(`No security rules found to seed`); From 66b9eb56e8266f5674cada40d1edf2fbbc5c6167 Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Tue, 9 Jun 2026 16:11:20 +0100 Subject: [PATCH 100/136] Add method to retrieve Solid UI module path and enhance cleanup logging --- src/helpers/module-metadata-helper.service.ts | 9 +++++++++ src/services/module-metadata.service.ts | 15 ++++++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/helpers/module-metadata-helper.service.ts b/src/helpers/module-metadata-helper.service.ts index 11eef721..99036973 100644 --- a/src/helpers/module-metadata-helper.service.ts +++ b/src/helpers/module-metadata-helper.service.ts @@ -37,6 +37,15 @@ export class ModuleMetadataHelperService { return path.resolve(process.cwd(), 'src', moduleName); } + async getSolidUiModulePath(moduleName: string): Promise { + if (!moduleName) { + return ''; + } + + const dashModuleName = kebabCase(moduleName); + return path.resolve(process.cwd(), '..', 'solid-ui', 'src', dashModuleName); + } + private resolveModuleMetadataFolderPath(moduleName: string): string { const dashModuleName = kebabCase(moduleName); return path.resolve(process.cwd(), 'src', dashModuleName, 'metadata'); diff --git a/src/services/module-metadata.service.ts b/src/services/module-metadata.service.ts index d6e698da..9b46de4c 100755 --- a/src/services/module-metadata.service.ts +++ b/src/services/module-metadata.service.ts @@ -354,9 +354,11 @@ export class ModuleMetadataService { this.logger.log(`Cleaning up for module: ${moduleEntity.name}.`); const modulePath = await this.moduleMetadataHelperService.getModulePath(moduleEntity.name); + const solidUiModulePath = await this.moduleMetadataHelperService.getSolidUiModulePath(moduleEntity.name); if (modulePath) { this.logger.log(`Module path: ${modulePath}`); + this.logger.log(`Solid UI module path: ${solidUiModulePath}`); // Metadata file to be deleted const moduleMetadataPAth = await this.moduleMetadataHelperService.getModuleMetadataFolderPath(moduleEntity.name) @@ -364,6 +366,10 @@ export class ModuleMetadataService { try { await fs.rm(modulePath, { recursive: true, force: true }); + if (solidUiModulePath) { + await fs.rm(solidUiModulePath, { recursive: true, force: true }); + this.logger.log(`Deleted solid-ui module path: ${solidUiModulePath}`); + } await fs.rm(moduleMetadataPAth, { recursive: true, force: true }); this.logger.log(`Deleted file: ${moduleMetadataPAth}`); } catch (error: any) { @@ -390,9 +396,12 @@ export class ModuleMetadataService { id: id, } }); - // if (!entity) { - // throw new Error(`Entity with id ${id} not found`); - // } + if (!entity) { + this.logger.warn(`Module metadata with id ${id} not found. Skipping deleteMany cleanup for this id.`); + continue; + } + + await this.cleanupOnDelete(entity.id); removedEntities.push(await this.moduleMetadataRepo.remove(entity)); } From 87862a849c83aa11fb59346b11f2078563e9aa8c Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Tue, 9 Jun 2026 21:43:41 +0100 Subject: [PATCH 101/136] Add resumable import handling and transaction management in module package service --- src/controllers/module-package.controller.ts | 14 ++ .../seed-data/solid-core-metadata.json | 4 + src/services/module-package.service.ts | 199 ++++++++++++++++++ 3 files changed, 217 insertions(+) diff --git a/src/controllers/module-package.controller.ts b/src/controllers/module-package.controller.ts index ae820681..a7b2095f 100644 --- a/src/controllers/module-package.controller.ts +++ b/src/controllers/module-package.controller.ts @@ -45,6 +45,12 @@ export class ModulePackageController { res.sendFile(archive.filePath); } + @ApiBearerAuth('jwt') + @Get('import/resumable/latest') + async getLatestResumableImport() { + return this.modulePackageService.getLatestResumableImport(); + } + @ApiBearerAuth('jwt') @Post('import/:id/confirm') async confirmImport( @@ -54,6 +60,14 @@ export class ModulePackageController { return this.modulePackageService.confirmImport(id, dto); } + @ApiBearerAuth('jwt') + @Post('import/:id/dismiss') + async dismissImport( + @Param('id') id: string, + ) { + return this.modulePackageService.dismissImport(id); + } + @ApiBearerAuth('jwt') @Get('import/:id/status') async getImportStatus( diff --git a/src/seeders/seed-data/solid-core-metadata.json b/src/seeders/seed-data/solid-core-metadata.json index 7d73eb23..9248db2f 100755 --- a/src/seeders/seed-data/solid-core-metadata.json +++ b/src/seeders/seed-data/solid-core-metadata.json @@ -6570,6 +6570,7 @@ "modelUserKey": "moduleMetadata", "layout": { "type": "list", + "onListLoad": "moduleMetadataListOnLoad", "attrs": { "pagination": true, "pageSizeOptions": [ @@ -6587,6 +6588,9 @@ "label": "Import Module", "action": "ModuleImportListHeaderAction", "actionInContextMenu": false, + "env": [ + "dev" + ], "openInPopup": true, "icon": "si-upload", "closable": false, diff --git a/src/services/module-package.service.ts b/src/services/module-package.service.ts index 83922689..c7bfecaa 100644 --- a/src/services/module-package.service.ts +++ b/src/services/module-package.service.ts @@ -107,6 +107,11 @@ type ModulePackageExportFile = { mimeType: string; }; +type ModulePackageActiveTransactionFile = { + transactionKey: string; + updatedAt: string; +}; + enum ModulePackageStatus { uploaded = 'uploaded', validated = 'validated', @@ -140,6 +145,8 @@ export class ModulePackageService { throw new BadRequestException('A .sldx archive file is required.'); } + await this.cleanupModulePackageTransactions(); + const transactionKey = uuidv4(); const workingDir = await this.createWorkingDir(transactionKey); const archiveFileName = file.originalname || path.basename(file.path); @@ -186,6 +193,20 @@ export class ModulePackageService { return this.toStatusResponse(transaction); } + @DisallowInProduction() + async getLatestResumableImport() { + await this.cleanupModulePackageTransactions(); + const transaction = await this.resolveLatestResumableTransaction(); + return transaction ? this.toStatusResponse(transaction) : null; + } + + @DisallowInProduction() + async dismissImport(transactionKey: string) { + const transaction = await this.loadTransaction(transactionKey); + await this.clearActiveTransactionIfMatches(transaction.transactionKey); + return this.toStatusResponse(transaction); + } + @DisallowInProduction() async exportModulePackage(moduleNameInput: string): Promise { const moduleName = (moduleNameInput ?? '').trim(); @@ -306,6 +327,7 @@ export class ModulePackageService { `Metadata file placed at ${preview.requiredPaths.metadataPath}`, ].join('\n'); await this.writeStatusFile(transactionKey, transaction); + await this.syncActiveTransactionPointer(transaction); return this.toStatusResponse(transaction); } catch (error: any) { @@ -313,6 +335,7 @@ export class ModulePackageService { transaction.currentStep = 'import'; transaction.errorMessage = error.message ?? 'Failed to import the module package.'; await this.writeStatusFile(transactionKey, transaction); + await this.syncActiveTransactionPointer(transaction); throw error; } } @@ -368,6 +391,7 @@ export class ModulePackageService { ? `Build completed with errors in: ${failedTargets.join(', ')}` : null; await this.writeStatusFile(transactionKey, transaction); + await this.syncActiveTransactionPointer(transaction); return this.toStatusResponse(transaction); } catch (error: any) { @@ -376,6 +400,7 @@ export class ModulePackageService { transaction.errorMessage = error.message ?? 'Build failed.'; transaction.outputs.build = error.message ?? ''; await this.writeStatusFile(transactionKey, transaction); + await this.syncActiveTransactionPointer(transaction); throw error; } } @@ -417,6 +442,7 @@ export class ModulePackageService { `seedGlobalMetadata=${dto.seedGlobalMetadata ?? false}`, ].join('\n'); await this.writeStatusFile(transactionKey, transaction); + await this.syncActiveTransactionPointer(transaction); return this.toStatusResponse(transaction); } catch (error: any) { @@ -425,6 +451,7 @@ export class ModulePackageService { transaction.errorMessage = error.message ?? 'Seed failed.'; transaction.outputs.seed = error.message ?? ''; await this.writeStatusFile(transactionKey, transaction); + await this.syncActiveTransactionPointer(transaction); return this.toStatusResponse(transaction); } } @@ -727,6 +754,18 @@ export class ModulePackageService { return JSON.parse(await fs.readFile(statusFilePath, 'utf-8')) as ModulePackageStatusFile; } + private isResumableStatus(status: string | null | undefined) { + return [ + ModulePackageStatus.import_running, + ModulePackageStatus.awaiting_restart, + ModulePackageStatus.build_running, + ModulePackageStatus.build_failed, + ModulePackageStatus.build_succeeded, + ModulePackageStatus.seed_running, + ModulePackageStatus.seed_failed, + ].includes(status as ModulePackageStatus); + } + private async createWorkingDir(transactionKey: string) { const baseDir = path.join( this.getModulePackageImportsRoot(), @@ -810,6 +849,10 @@ export class ModulePackageService { return path.join(this.getModulePackageImportsRoot(), transactionKey, 'status.json'); } + private getActiveTransactionFilePath() { + return path.join(this.getModulePackageImportsRoot(), 'active-transaction.json'); + } + private async writeStatusFile(transactionKey: string, payload: ModulePackageStatusFile) { payload.updatedAt = new Date().toISOString(); const statusFilePath = this.getStatusFilePath(transactionKey); @@ -817,6 +860,162 @@ export class ModulePackageService { await fs.writeFile(statusFilePath, JSON.stringify(payload, null, 2)); } + private async markActiveTransaction(transactionKey: string) { + const activeTransactionFilePath = this.getActiveTransactionFilePath(); + await fs.mkdir(path.dirname(activeTransactionFilePath), { recursive: true }); + const payload: ModulePackageActiveTransactionFile = { + transactionKey, + updatedAt: new Date().toISOString(), + }; + await fs.writeFile(activeTransactionFilePath, JSON.stringify(payload, null, 2)); + } + + private async clearActiveTransactionIfMatches(transactionKey: string) { + const activeTransaction = await this.loadActiveTransactionPointer(); + if (!activeTransaction || activeTransaction.transactionKey !== transactionKey) { + return; + } + + await fs.rm(this.getActiveTransactionFilePath(), { force: true }); + } + + private async loadActiveTransactionPointer(): Promise { + try { + return JSON.parse(await fs.readFile(this.getActiveTransactionFilePath(), 'utf-8')) as ModulePackageActiveTransactionFile; + } catch (error) { + return null; + } + } + + private async syncActiveTransactionPointer(transaction: ModulePackageStatusFile) { + if (this.isResumableStatus(transaction.status)) { + await this.markActiveTransaction(transaction.transactionKey); + return; + } + + await this.clearActiveTransactionIfMatches(transaction.transactionKey); + } + + private getCompletedTransactionRetentionMs() { + const retentionDays = Number(process.env.SOLIDX_MODULE_PACKAGE_RETENTION_DAYS ?? 7); + const safeDays = Number.isFinite(retentionDays) && retentionDays > 0 ? retentionDays : 7; + return safeDays * 24 * 60 * 60 * 1000; + } + + private getPendingTransactionRetentionMs() { + const retentionHours = Number(process.env.SOLIDX_MODULE_PACKAGE_PENDING_RETENTION_HOURS ?? 24); + const safeHours = Number.isFinite(retentionHours) && retentionHours > 0 ? retentionHours : 24; + return safeHours * 60 * 60 * 1000; + } + + private async cleanupModulePackageTransactions() { + const importsRoot = this.getModulePackageImportsRoot(); + const activePointer = await this.loadActiveTransactionPointer(); + const now = Date.now(); + + try { + await fs.mkdir(importsRoot, { recursive: true }); + const entries = await fs.readdir(importsRoot, { withFileTypes: true }); + + for (const entry of entries) { + if (!entry.isDirectory()) { + continue; + } + + const transactionKey = entry.name; + if (!/^[a-zA-Z0-9-]+$/.test(transactionKey)) { + continue; + } + + const statusFilePath = this.getStatusFilePath(transactionKey); + let transaction: ModulePackageStatusFile | null = null; + + try { + transaction = JSON.parse(await fs.readFile(statusFilePath, 'utf-8')) as ModulePackageStatusFile; + } catch (error) { + continue; + } + + const updatedAtMs = transaction.updatedAt ? new Date(transaction.updatedAt).getTime() : 0; + const ageMs = updatedAtMs > 0 ? now - updatedAtMs : Number.MAX_SAFE_INTEGER; + const isResumable = this.isResumableStatus(transaction.status); + const maxAgeMs = isResumable + ? this.getPendingTransactionRetentionMs() + : this.getCompletedTransactionRetentionMs(); + const shouldDelete = ageMs > maxAgeMs; + + if (!shouldDelete) { + continue; + } + + await fs.rm(path.join(importsRoot, transactionKey), { recursive: true, force: true }); + + if (activePointer?.transactionKey === transactionKey) { + await fs.rm(this.getActiveTransactionFilePath(), { force: true }); + } + } + } catch (error) { + this.logger.warn(`Failed to clean up module package transactions: ${String(error)}`); + } + } + + private async resolveLatestResumableTransaction(): Promise { + const activePointer = await this.loadActiveTransactionPointer(); + if (activePointer?.transactionKey) { + try { + const activeTransaction = await this.loadTransaction(activePointer.transactionKey); + if (this.isResumableStatus(activeTransaction.status)) { + return activeTransaction; + } + + await this.clearActiveTransactionIfMatches(activePointer.transactionKey); + } catch (error) { + await this.clearActiveTransactionIfMatches(activePointer.transactionKey); + } + } + + const importsRoot = this.getModulePackageImportsRoot(); + const candidates: ModulePackageStatusFile[] = []; + + try { + const entries = await fs.readdir(importsRoot, { withFileTypes: true }); + + for (const entry of entries) { + if (!entry.isDirectory()) { + continue; + } + + const transactionKey = entry.name; + if (!/^[a-zA-Z0-9-]+$/.test(transactionKey)) { + continue; + } + + try { + const transaction = await this.loadTransaction(transactionKey); + if (this.isResumableStatus(transaction.status)) { + candidates.push(transaction); + } + } catch (error) { + continue; + } + } + } catch (error) { + return null; + } + + candidates.sort((left, right) => { + const leftUpdatedAt = new Date(left.updatedAt ?? left.createdAt ?? 0).getTime(); + const rightUpdatedAt = new Date(right.updatedAt ?? right.createdAt ?? 0).getTime(); + return rightUpdatedAt - leftUpdatedAt; + }); + + const latest = candidates[0] ?? null; + if (latest) { + await this.markActiveTransaction(latest.transactionKey); + } + return latest; + } + private toStatusResponse(transaction: ModulePackageStatusFile) { return { transactionKey: transaction.transactionKey, From 824cf5cf81c611d9eed089e17d9d9c913c51ceab Mon Sep 17 00:00:00 2001 From: sundaram Date: Wed, 10 Jun 2026 11:36:05 +0530 Subject: [PATCH 102/136] fix: correct package name in model generation command and update error handling in module metadata retrieval --- src/helpers/module-metadata-helper.service.ts | 3 +-- src/services/model-metadata.service.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/helpers/module-metadata-helper.service.ts b/src/helpers/module-metadata-helper.service.ts index ec59de19..4d0281ba 100644 --- a/src/helpers/module-metadata-helper.service.ts +++ b/src/helpers/module-metadata-helper.service.ts @@ -68,8 +68,7 @@ export class ModuleMetadataHelperService { } } - this.logger.error(`Module metadata file not found for module: ${moduleName} at path: ${filePath}`); - return ''; + return filePath; } async getModuleMetadataFolderPath(moduleName: string): Promise { diff --git a/src/services/model-metadata.service.ts b/src/services/model-metadata.service.ts index 4971628c..aa3f44a1 100755 --- a/src/services/model-metadata.service.ts +++ b/src/services/model-metadata.service.ts @@ -740,7 +740,7 @@ export class ModelMetadataService { const model = await this.findOne(modelId); return this.commandService.executeCommandWithArgs({ command: 'npx', - args: ['@solixai/solidctl@latest', 'generate', 'model', `--name=${model.singularName}`], + args: ['@solidxai/solidctl@latest', 'generate', 'model', `--name=${model.singularName}`], cwd: path.join(process.cwd(), '..'), }); } From ef54e1560e1d861de21991e9a0dc64c6e7a4c066 Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Wed, 10 Jun 2026 11:57:22 +0530 Subject: [PATCH 103/136] menu item model cleanup --- .../seed-data/solid-core-metadata.json | 147 ++++++++++++++++-- 1 file changed, 131 insertions(+), 16 deletions(-) diff --git a/src/seeders/seed-data/solid-core-metadata.json b/src/seeders/seed-data/solid-core-metadata.json index 9248db2f..6400ff25 100755 --- a/src/seeders/seed-data/solid-core-metadata.json +++ b/src/seeders/seed-data/solid-core-metadata.json @@ -6210,6 +6210,19 @@ "viewUserKey": "modelMetadata-tree-view", "moduleUserKey": "solid-core", "modelUserKey": "modelMetadata" + }, + { + "displayName": "Menu Item Metadata Tree Action", + "name": "menuItemMetadata-tree-action", + "type": "solid", + "domain": "", + "context": "", + "customComponent": "", + "customIsModal": true, + "serverEndpoint": "", + "viewUserKey": "menuItemMetadata-tree-view", + "moduleUserKey": "solid-core", + "modelUserKey": "menuItemMetadata" } ], "menus": [ @@ -8436,44 +8449,60 @@ 50 ], "enableGlobalSearch": true, - "create": true, - "edit": true, - "delete": true + "create": false, + "edit": false, + "delete": false, + "import": false, + "export": false, + "allowedViews": [ + "list", + "tree" + ] }, "children": [ { "type": "field", "attrs": { - "name": "name", + "name": "module", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "displayName", + "name": "parentMenuItem", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "sequenceNumber", - "isSearchable": true + "name": "name", + "isSearchable": true, + "sortable": true } }, { "type": "field", "attrs": { - "name": "module", - "isSearchable": true + "name": "displayName", + "isSearchable": true, + "sortable": true } }, { "type": "field", "attrs": { - "name": "parentMenuItem", - "isSearchable": true + "name": "sequenceNumber", + "isSearchable": true, + "sortable": true + } + }, + { + "type": "field", + "attrs": { + "name": "roles", + "isSearchable": false } }, { @@ -8486,8 +8515,8 @@ { "type": "field", "attrs": { - "name": "roles", - "isSearchable": true + "name": "iconName", + "isSearchable": false } } ] @@ -8505,7 +8534,9 @@ "attrs": { "name": "form-1", "label": "Solid Menu Item Model", - "className": "grid" + "className": "grid", + "showAddFormButton": false, + "showEditFormButton": false }, "children": [ { @@ -8959,7 +8990,7 @@ { "name": "fieldMetadata-tree-view", "displayName": "Field Metadata", - "type": "list", + "type": "tree", "context": "{}", "moduleUserKey": "solid-core", "modelUserKey": "fieldMetadata", @@ -13536,6 +13567,90 @@ } ] } + }, + { + "name": "menuItemMetadata-tree-view", + "displayName": "Menu Item Metadata Tree View", + "type": "tree", + "context": "{}", + "moduleUserKey": "solid-core", + "modelUserKey": "menuItemMetadata", + "layout": { + "type": "list", + "attrs": { + "pagination": true, + "pageSizeOptions": [ + 10, + 25, + 50 + ], + "enableGlobalSearch": true, + "create": false, + "edit": false, + "delete": false + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "module", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "parentMenuItem", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "name", + "isSearchable": true, + "sortable": true + } + }, + { + "type": "field", + "attrs": { + "name": "displayName", + "isSearchable": true, + "sortable": true + } + }, + { + "type": "field", + "attrs": { + "name": "sequenceNumber", + "isSearchable": true, + "sortable": true + } + }, + { + "type": "field", + "attrs": { + "name": "roles", + "isSearchable": false + } + }, + { + "type": "field", + "attrs": { + "name": "action", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "iconName", + "isSearchable": false + } + } + ] + } } ], "emailTemplates": [ @@ -14010,4 +14125,4 @@ } } ] -} +} \ No newline at end of file From 5250243d8a291457a4dac9b82d000124ab449101 Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Wed, 10 Jun 2026 12:31:09 +0530 Subject: [PATCH 104/136] Add view and action metadata tree views with updated permissions and searchable fields --- .../seed-data/solid-core-metadata.json | 342 ++++++++++-------- 1 file changed, 200 insertions(+), 142 deletions(-) diff --git a/src/seeders/seed-data/solid-core-metadata.json b/src/seeders/seed-data/solid-core-metadata.json index 6400ff25..7067e20c 100755 --- a/src/seeders/seed-data/solid-core-metadata.json +++ b/src/seeders/seed-data/solid-core-metadata.json @@ -6223,6 +6223,32 @@ "viewUserKey": "menuItemMetadata-tree-view", "moduleUserKey": "solid-core", "modelUserKey": "menuItemMetadata" + }, + { + "displayName": "View Metadata Tree Action", + "name": "viewMetadata-tree-action", + "type": "solid", + "domain": "", + "context": "", + "customComponent": "", + "customIsModal": true, + "serverEndpoint": "", + "viewUserKey": "viewMetadata-tree-view", + "moduleUserKey": "solid-core", + "modelUserKey": "viewMetadata" + }, + { + "displayName": "Action Metadata Tree Action", + "name": "actionMetadata-tree-action", + "type": "solid", + "domain": "", + "context": "", + "customComponent": "", + "customIsModal": true, + "serverEndpoint": "", + "viewUserKey": "actionMetadata-tree-view", + "moduleUserKey": "solid-core", + "modelUserKey": "actionMetadata" } ], "menus": [ @@ -7819,57 +7845,51 @@ 50 ], "enableGlobalSearch": true, - "create": true, - "edit": true, - "delete": true + "create": false, + "edit": false, + "delete": false, + "import": false, + "export": false, + "allowedViews": [ + "list", + "tree" + ] }, "children": [ { "type": "field", "attrs": { - "name": "name", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "displayName", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "type", + "name": "module", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "context", + "name": "model", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "layout", - "isSearchable": true + "name": "name", + "isSearchable": true, + "sortable": true } }, { "type": "field", "attrs": { - "name": "module", - "isSearchable": true + "name": "displayName", + "isSearchable": true, + "sortable": true } }, { "type": "field", "attrs": { - "name": "model", + "name": "type", "isSearchable": true } } @@ -7888,7 +7908,9 @@ "attrs": { "name": "form-1", "label": "View Metadata", - "className": "grid" + "className": "grid", + "showAddFormButton": false, + "showEditFormButton": false }, "children": [ { @@ -7957,58 +7979,6 @@ } } ] - }, - { - "type": "column", - "attrs": { - "name": "page-1-row-1-col-2", - "label": "", - "className": "col-12 sm:col-12 md:col-6 lg:col-6" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "context" - } - } - ] - } - ] - } - ] - }, - { - "type": "page", - "attrs": { - "name": "page-2", - "label": "Layout" - }, - "children": [ - { - "type": "row", - "attrs": { - "name": "page-2-row-1", - "label": "", - "className": "" - }, - "children": [ - { - "type": "column", - "attrs": { - "name": "page-2-row-1-col-1", - "label": "", - "className": "col-12" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "layout", - "height": "80vh" - } - } - ] } ] } @@ -8202,50 +8172,58 @@ 50 ], "enableGlobalSearch": true, - "create": true, - "edit": true, - "delete": true + "create": false, + "edit": false, + "delete": false, + "import": false, + "export": false, + "allowedViews": [ + "list", + "tree" + ] }, "children": [ { "type": "field", "attrs": { - "name": "name", + "name": "module", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "displayName", + "name": "model", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "type", + "name": "view", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "module", - "isSearchable": true + "name": "name", + "isSearchable": true, + "sortable": true } }, { "type": "field", "attrs": { - "name": "model", - "isSearchable": true + "name": "displayName", + "isSearchable": true, + "sortable": true } }, { "type": "field", "attrs": { - "name": "view", + "name": "type", "isSearchable": true } } @@ -8264,7 +8242,9 @@ "attrs": { "name": "form-1", "label": "Action Metadata", - "className": "grid" + "className": "grid", + "showAddFormButton": false, + "showEditFormButton": false }, "children": [ { @@ -8371,59 +8351,6 @@ ] } ] - }, - { - "type": "page", - "attrs": { - "name": "page-2", - "label": "Layout" - }, - "children": [ - { - "type": "row", - "attrs": { - "name": "page-2-row-1", - "label": "", - "className": "" - }, - "children": [ - { - "type": "column", - "attrs": { - "name": "page-2-row-1-col-1", - "label": "", - "className": "col-12 sm:col-12 md:col-6 lg:col-6" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "domain", - "height": "80vh" - } - } - ] - }, - { - "type": "column", - "attrs": { - "name": "page-2-row-1-col-1", - "label": "", - "className": "col-12 sm:col-12 md:col-6 lg:col-6" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "context", - "height": "80vh" - } - } - ] - } - ] - } - ] } ] } @@ -13651,6 +13578,137 @@ } ] } + }, + { + "name": "viewMetadata-tree-view", + "displayName": "View Metadata Tree View", + "type": "tree", + "context": "{}", + "moduleUserKey": "solid-core", + "modelUserKey": "viewMetadata", + "layout": { + "type": "list", + "attrs": { + "pagination": true, + "pageSizeOptions": [ + 10, + 25, + 50 + ], + "enableGlobalSearch": true, + "create": false, + "edit": false, + "delete": false + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "module", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "model", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "name", + "isSearchable": true, + "sortable": true + } + }, + { + "type": "field", + "attrs": { + "name": "displayName", + "isSearchable": true, + "sortable": true + } + }, + { + "type": "field", + "attrs": { + "name": "type", + "isSearchable": true + } + } + ] + } + }, + { + "name": "actionMetadata-tree-view", + "displayName": "Action Metadata Tree View", + "type": "tree", + "context": "{}", + "moduleUserKey": "solid-core", + "modelUserKey": "actionMetadata", + "layout": { + "type": "list", + "attrs": { + "pagination": true, + "pageSizeOptions": [ + 10, + 25, + 50 + ], + "enableGlobalSearch": true, + "create": false, + "edit": false, + "delete": false + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "module", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "model", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "view", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "name", + "isSearchable": true, + "sortable": true + } + }, + { + "type": "field", + "attrs": { + "name": "displayName", + "isSearchable": true, + "sortable": true + } + }, + { + "type": "field", + "attrs": { + "name": "type", + "isSearchable": true + } + } + ] + } } ], "emailTemplates": [ From 20c617794de2f5956fa42f6bceb4cd1e693916b0 Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Wed, 10 Jun 2026 13:24:40 +0530 Subject: [PATCH 105/136] Add view and action metadata tree views with updated permissions and searchable fields --- .../seed-data/solid-core-metadata.json | 138 +++++++++++++++++- 1 file changed, 131 insertions(+), 7 deletions(-) diff --git a/src/seeders/seed-data/solid-core-metadata.json b/src/seeders/seed-data/solid-core-metadata.json index 7067e20c..7a9225ea 100755 --- a/src/seeders/seed-data/solid-core-metadata.json +++ b/src/seeders/seed-data/solid-core-metadata.json @@ -6249,6 +6249,19 @@ "viewUserKey": "actionMetadata-tree-view", "moduleUserKey": "solid-core", "modelUserKey": "actionMetadata" + }, + { + "displayName": "User View Metadata Tree Action", + "name": "userViewMetadata-tree-action", + "type": "solid", + "domain": "", + "context": "", + "customComponent": "", + "customIsModal": true, + "serverEndpoint": "", + "viewUserKey": "userViewMetadata-tree-view", + "moduleUserKey": "solid-core", + "modelUserKey": "userViewMetadata" } ], "menus": [ @@ -8008,27 +8021,60 @@ 50 ], "enableGlobalSearch": true, - "create": true, - "edit": true, - "delete": true + "create": false, + "edit": false, + "delete": true, + "import": false, + "export": false, + "allowedViews": [ + "list", + "tree" + ] }, "children": [ { "type": "field", "attrs": { - "name": "viewMetadata" + "name": "viewMetadata", + "label": "Name", + "coModelFieldToDisplay": "name", + "isSearchable": true, + "searchField": "viewMetadata.name" } }, { "type": "field", "attrs": { - "name": "user" + "name": "viewMetadata", + "label": "Display Name", + "coModelFieldToDisplay": "displayName", + "isSearchable": true, + "searchField": "viewMetadata.displayName" } }, { "type": "field", "attrs": { - "name": "layout" + "name": "viewMetadata", + "label": "Type", + "coModelFieldToDisplay": "type", + "isSearchable": false + } + }, + { + "type": "field", + "attrs": { + "name": "user", + "isSearchable": true, + "searchField": "user.fullName", + "coModelFieldToDisplay": "fullName" + } + }, + { + "type": "field", + "attrs": { + "name": "createdAt", + "isSearchable": false } } ] @@ -8046,7 +8092,9 @@ "attrs": { "name": "form-1", "label": "User View Metadata", - "className": "grid" + "className": "grid", + "showAddFormButton": false, + "showEditFormButton": false }, "children": [ { @@ -9387,6 +9435,12 @@ "name": "username" } }, + { + "type": "field", + "attrs": { + "name": "fullName" + } + }, { "type": "field", "attrs": { @@ -13709,6 +13763,76 @@ } ] } + }, + { + "name": "userViewMetadata-tree-view", + "displayName": "User View Metadata Tree View", + "type": "tree", + "context": "{}", + "moduleUserKey": "solid-core", + "modelUserKey": "userViewMetadata", + "layout": { + "type": "list", + "attrs": { + "pagination": true, + "pageSizeOptions": [ + 10, + 25, + 50 + ], + "enableGlobalSearch": true, + "create": false, + "edit": false, + "delete": false + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "viewMetadata", + "label": "Name", + "coModelFieldToDisplay": "name", + "isSearchable": true, + "searchField": "viewMetadata.name" + } + }, + { + "type": "field", + "attrs": { + "name": "viewMetadata", + "label": "Display Name", + "coModelFieldToDisplay": "displayName", + "isSearchable": true, + "searchField": "viewMetadata.displayName" + } + }, + { + "type": "field", + "attrs": { + "name": "viewMetadata", + "label": "Type", + "coModelFieldToDisplay": "type", + "isSearchable": false + } + }, + { + "type": "field", + "attrs": { + "name": "user", + "isSearchable": true, + "searchField": "user.fullName", + "coModelFieldToDisplay": "fullName" + } + }, + { + "type": "field", + "attrs": { + "name": "createdAt", + "isSearchable": false + } + } + ] + } } ], "emailTemplates": [ From cc0e5e5606d99ad64f6e94783dea5466d5742ddf Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Wed, 10 Jun 2026 14:01:25 +0530 Subject: [PATCH 106/136] cleanup --- src/seeders/seed-data/solid-core-metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/seeders/seed-data/solid-core-metadata.json b/src/seeders/seed-data/solid-core-metadata.json index 7a9225ea..b6afd881 100755 --- a/src/seeders/seed-data/solid-core-metadata.json +++ b/src/seeders/seed-data/solid-core-metadata.json @@ -8038,7 +8038,7 @@ "name": "viewMetadata", "label": "Name", "coModelFieldToDisplay": "name", - "isSearchable": true, + "isSearchable": false, "searchField": "viewMetadata.name" } }, From 51004699f339cb66f2fce2077e39f9af915f1571 Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Wed, 10 Jun 2026 09:46:34 +0100 Subject: [PATCH 107/136] Remove unused database bootstrap service and related SQL cleanup procedures --- .../mariadb/proc_CleanupModelMetadata.sql | 153 -------- .../mysql/proc_CleanupModelMetadata.sql | 153 -------- .../postgres/proc_CleanupModelMetadata.sql | 148 -------- sql/default/postgres/scratchpad.sql.txt | 12 - .../database/database-bootstrap.service.ts | 91 ----- src/services/model-metadata.service.ts | 331 ++++++++++++++---- src/solid-core.module.ts | 2 - 7 files changed, 264 insertions(+), 626 deletions(-) delete mode 100644 sql/default/mariadb/proc_CleanupModelMetadata.sql delete mode 100644 sql/default/mysql/proc_CleanupModelMetadata.sql delete mode 100644 sql/default/postgres/proc_CleanupModelMetadata.sql delete mode 100644 sql/default/postgres/scratchpad.sql.txt delete mode 100644 src/services/database/database-bootstrap.service.ts diff --git a/sql/default/mariadb/proc_CleanupModelMetadata.sql b/sql/default/mariadb/proc_CleanupModelMetadata.sql deleted file mode 100644 index 19627346..00000000 --- a/sql/default/mariadb/proc_CleanupModelMetadata.sql +++ /dev/null @@ -1,153 +0,0 @@ -DROP PROCEDURE IF EXISTS cleanup_model_metadata; - -DELIMITER $$ - -CREATE PROCEDURE cleanup_model_metadata( - IN p_model_singular_name VARCHAR(255), - IN p_also_drop_model TINYINT(1) -- 0 = false, 1 = true (DEFAULT 0) -) -BEGIN - DECLARE v_model_id INT; - DECLARE v_controller_name VARCHAR(512); - - -- Default p_also_drop_model to 0 when NULL is passed - IF p_also_drop_model IS NULL THEN - SET p_also_drop_model = 0; - END IF; - - -------------------------------------------------------------------- - -- Derive ControllerName from model singular name - -- e.g. 'city' -> 'CityController' - -- 'bankIfscMaster' -> 'BankIfscMasterController' - -------------------------------------------------------------------- - SET v_controller_name = CONCAT( - UPPER(LEFT(p_model_singular_name, 1)), - SUBSTR(p_model_singular_name, 2), - 'Controller' - ); - - SELECT m.id - INTO v_model_id - FROM ss_model_metadata AS m - WHERE m.singular_name = p_model_singular_name - LIMIT 1; - - IF v_model_id IS NULL THEN - SIGNAL SQLSTATE '45000' - SET MESSAGE_TEXT = CONCAT('CleanupModelMetadata: Model with singular_name "', p_model_singular_name, '" not found.'); - END IF; - - -------------------------------------------------------------------- - -- Temp tables for IDs (session-scoped, dropped at end of session) - -------------------------------------------------------------------- - DROP TEMPORARY TABLE IF EXISTS tmp_view_ids; - DROP TEMPORARY TABLE IF EXISTS tmp_action_ids; - DROP TEMPORARY TABLE IF EXISTS tmp_permission_ids; - - CREATE TEMPORARY TABLE tmp_view_ids (id INT PRIMARY KEY); - CREATE TEMPORARY TABLE tmp_action_ids (id INT PRIMARY KEY); - CREATE TEMPORARY TABLE tmp_permission_ids (id INT PRIMARY KEY); - - INSERT INTO tmp_view_ids (id) - SELECT v.id - FROM ss_view_metadata AS v - WHERE v.model_id = v_model_id; - - INSERT INTO tmp_action_ids (id) - SELECT a.id - FROM ss_action_metadata AS a - WHERE a.model_id = v_model_id; - - -------------------------------------------------------------------- - -- Collect permissions for this controller: - -- e.g. CityController.create, CityController.update, etc. - -------------------------------------------------------------------- - INSERT INTO tmp_permission_ids (id) - SELECT p.id - FROM ss_permission_metadata AS p - WHERE p.name LIKE CONCAT(v_controller_name, '.%'); - - -------------------------------------------------------------------- - -- 1) Delete user-view metadata - -------------------------------------------------------------------- - DELETE FROM ss_user_view_metadata - WHERE view_metadata_id IN (SELECT id FROM tmp_view_ids); - - -------------------------------------------------------------------- - -- 2) Delete menu items linked via action_id - -------------------------------------------------------------------- - DELETE FROM ss_menu_item_metadata - WHERE action_id IN (SELECT id FROM tmp_action_ids); - - -------------------------------------------------------------------- - -- 3) Delete actions - -------------------------------------------------------------------- - DELETE FROM ss_action_metadata - WHERE id IN (SELECT id FROM tmp_action_ids); - - -------------------------------------------------------------------- - -- 4) Delete views - -------------------------------------------------------------------- - DELETE FROM ss_view_metadata - WHERE id IN (SELECT id FROM tmp_view_ids); - - -------------------------------------------------------------------- - -- 5) Delete import transaction error logs for this model's imports - -------------------------------------------------------------------- - DELETE FROM ss_import_transaction_error_log - WHERE import_transaction_id IN ( - SELECT it.id - FROM ss_import_transaction AS it - WHERE it.model_metadata_id = v_model_id - ); - - -------------------------------------------------------------------- - -- 6) Delete import transactions linked to the model - -------------------------------------------------------------------- - DELETE FROM ss_import_transaction - WHERE model_metadata_id = v_model_id; - - -------------------------------------------------------------------- - -- 7) Delete permissions for this controller - -- a) Delete from role-permission join table - -- b) Delete from permission metadata - -------------------------------------------------------------------- - DELETE FROM ss_role_metadata_permissions_ss_permission_metadata - WHERE ss_permission_metadata_id IN (SELECT id FROM tmp_permission_ids); - - DELETE FROM ss_permission_metadata - WHERE id IN (SELECT id FROM tmp_permission_ids); - - -------------------------------------------------------------------- - -- 8) Optionally delete the model row itself (+ fields) - -------------------------------------------------------------------- - IF p_also_drop_model = 1 THEN - - -- 8a) Null user_key_field_id pointing to fields of this model - UPDATE ss_model_metadata AS mm - SET user_key_field_id = NULL - WHERE user_key_field_id IN ( - SELECT f.id - FROM ss_field_metadata AS f - WHERE f.model_id = v_model_id - ); - - -- 8b) Null parent_model_id pointing to this model - UPDATE ss_model_metadata - SET parent_model_id = NULL - WHERE parent_model_id = v_model_id; - - -- 8c) Delete model (fields deleted via ON DELETE CASCADE) - DELETE FROM ss_model_metadata - WHERE id = v_model_id; - - END IF; - - -- Cleanup temp tables - DROP TEMPORARY TABLE IF EXISTS tmp_view_ids; - DROP TEMPORARY TABLE IF EXISTS tmp_action_ids; - DROP TEMPORARY TABLE IF EXISTS tmp_permission_ids; - -END$$ - -DELIMITER ; diff --git a/sql/default/mysql/proc_CleanupModelMetadata.sql b/sql/default/mysql/proc_CleanupModelMetadata.sql deleted file mode 100644 index 19627346..00000000 --- a/sql/default/mysql/proc_CleanupModelMetadata.sql +++ /dev/null @@ -1,153 +0,0 @@ -DROP PROCEDURE IF EXISTS cleanup_model_metadata; - -DELIMITER $$ - -CREATE PROCEDURE cleanup_model_metadata( - IN p_model_singular_name VARCHAR(255), - IN p_also_drop_model TINYINT(1) -- 0 = false, 1 = true (DEFAULT 0) -) -BEGIN - DECLARE v_model_id INT; - DECLARE v_controller_name VARCHAR(512); - - -- Default p_also_drop_model to 0 when NULL is passed - IF p_also_drop_model IS NULL THEN - SET p_also_drop_model = 0; - END IF; - - -------------------------------------------------------------------- - -- Derive ControllerName from model singular name - -- e.g. 'city' -> 'CityController' - -- 'bankIfscMaster' -> 'BankIfscMasterController' - -------------------------------------------------------------------- - SET v_controller_name = CONCAT( - UPPER(LEFT(p_model_singular_name, 1)), - SUBSTR(p_model_singular_name, 2), - 'Controller' - ); - - SELECT m.id - INTO v_model_id - FROM ss_model_metadata AS m - WHERE m.singular_name = p_model_singular_name - LIMIT 1; - - IF v_model_id IS NULL THEN - SIGNAL SQLSTATE '45000' - SET MESSAGE_TEXT = CONCAT('CleanupModelMetadata: Model with singular_name "', p_model_singular_name, '" not found.'); - END IF; - - -------------------------------------------------------------------- - -- Temp tables for IDs (session-scoped, dropped at end of session) - -------------------------------------------------------------------- - DROP TEMPORARY TABLE IF EXISTS tmp_view_ids; - DROP TEMPORARY TABLE IF EXISTS tmp_action_ids; - DROP TEMPORARY TABLE IF EXISTS tmp_permission_ids; - - CREATE TEMPORARY TABLE tmp_view_ids (id INT PRIMARY KEY); - CREATE TEMPORARY TABLE tmp_action_ids (id INT PRIMARY KEY); - CREATE TEMPORARY TABLE tmp_permission_ids (id INT PRIMARY KEY); - - INSERT INTO tmp_view_ids (id) - SELECT v.id - FROM ss_view_metadata AS v - WHERE v.model_id = v_model_id; - - INSERT INTO tmp_action_ids (id) - SELECT a.id - FROM ss_action_metadata AS a - WHERE a.model_id = v_model_id; - - -------------------------------------------------------------------- - -- Collect permissions for this controller: - -- e.g. CityController.create, CityController.update, etc. - -------------------------------------------------------------------- - INSERT INTO tmp_permission_ids (id) - SELECT p.id - FROM ss_permission_metadata AS p - WHERE p.name LIKE CONCAT(v_controller_name, '.%'); - - -------------------------------------------------------------------- - -- 1) Delete user-view metadata - -------------------------------------------------------------------- - DELETE FROM ss_user_view_metadata - WHERE view_metadata_id IN (SELECT id FROM tmp_view_ids); - - -------------------------------------------------------------------- - -- 2) Delete menu items linked via action_id - -------------------------------------------------------------------- - DELETE FROM ss_menu_item_metadata - WHERE action_id IN (SELECT id FROM tmp_action_ids); - - -------------------------------------------------------------------- - -- 3) Delete actions - -------------------------------------------------------------------- - DELETE FROM ss_action_metadata - WHERE id IN (SELECT id FROM tmp_action_ids); - - -------------------------------------------------------------------- - -- 4) Delete views - -------------------------------------------------------------------- - DELETE FROM ss_view_metadata - WHERE id IN (SELECT id FROM tmp_view_ids); - - -------------------------------------------------------------------- - -- 5) Delete import transaction error logs for this model's imports - -------------------------------------------------------------------- - DELETE FROM ss_import_transaction_error_log - WHERE import_transaction_id IN ( - SELECT it.id - FROM ss_import_transaction AS it - WHERE it.model_metadata_id = v_model_id - ); - - -------------------------------------------------------------------- - -- 6) Delete import transactions linked to the model - -------------------------------------------------------------------- - DELETE FROM ss_import_transaction - WHERE model_metadata_id = v_model_id; - - -------------------------------------------------------------------- - -- 7) Delete permissions for this controller - -- a) Delete from role-permission join table - -- b) Delete from permission metadata - -------------------------------------------------------------------- - DELETE FROM ss_role_metadata_permissions_ss_permission_metadata - WHERE ss_permission_metadata_id IN (SELECT id FROM tmp_permission_ids); - - DELETE FROM ss_permission_metadata - WHERE id IN (SELECT id FROM tmp_permission_ids); - - -------------------------------------------------------------------- - -- 8) Optionally delete the model row itself (+ fields) - -------------------------------------------------------------------- - IF p_also_drop_model = 1 THEN - - -- 8a) Null user_key_field_id pointing to fields of this model - UPDATE ss_model_metadata AS mm - SET user_key_field_id = NULL - WHERE user_key_field_id IN ( - SELECT f.id - FROM ss_field_metadata AS f - WHERE f.model_id = v_model_id - ); - - -- 8b) Null parent_model_id pointing to this model - UPDATE ss_model_metadata - SET parent_model_id = NULL - WHERE parent_model_id = v_model_id; - - -- 8c) Delete model (fields deleted via ON DELETE CASCADE) - DELETE FROM ss_model_metadata - WHERE id = v_model_id; - - END IF; - - -- Cleanup temp tables - DROP TEMPORARY TABLE IF EXISTS tmp_view_ids; - DROP TEMPORARY TABLE IF EXISTS tmp_action_ids; - DROP TEMPORARY TABLE IF EXISTS tmp_permission_ids; - -END$$ - -DELIMITER ; diff --git a/sql/default/postgres/proc_CleanupModelMetadata.sql b/sql/default/postgres/proc_CleanupModelMetadata.sql deleted file mode 100644 index ed5067e3..00000000 --- a/sql/default/postgres/proc_CleanupModelMetadata.sql +++ /dev/null @@ -1,148 +0,0 @@ -CREATE OR REPLACE PROCEDURE cleanup_model_metadata( - IN p_model_singular_name text, - IN p_also_drop_model boolean DEFAULT false -) -LANGUAGE plpgsql -AS $$ -DECLARE - v_model_id int; - v_controller_name text; -BEGIN - -------------------------------------------------------------------- - -- Derive ControllerName from model singular name - -- e.g. 'city' -> 'CityController' - -- 'bankIfscMaster' -> 'BankIfscMasterController' - -------------------------------------------------------------------- - v_controller_name := - upper(left(p_model_singular_name, 1)) - || substr(p_model_singular_name, 2) - || 'Controller'; - - SELECT m.id - INTO v_model_id - FROM ss_model_metadata AS m - WHERE m.singular_name = p_model_singular_name; - - IF v_model_id IS NULL THEN - RAISE EXCEPTION - 'CleanupModelMetadata: Model with singular_name "%" not found.', - p_model_singular_name; - END IF; - - -------------------------------------------------------------------- - -- Temp tables for IDs (session-scoped) - -------------------------------------------------------------------- - DROP TABLE IF EXISTS view_ids; - DROP TABLE IF EXISTS action_ids; - DROP TABLE IF EXISTS permission_ids; - - CREATE TEMP TABLE view_ids (id int PRIMARY KEY) ON COMMIT DROP; - CREATE TEMP TABLE action_ids (id int PRIMARY KEY) ON COMMIT DROP; - CREATE TEMP TABLE permission_ids (id int PRIMARY KEY) ON COMMIT DROP; - - INSERT INTO view_ids (id) - SELECT v.id - FROM ss_view_metadata AS v - WHERE v.model_id = v_model_id; - - INSERT INTO action_ids (id) - SELECT a.id - FROM ss_action_metadata AS a - WHERE a.model_id = v_model_id; - - -------------------------------------------------------------------- - -- Collect permissions for this controller: - -- e.g. CityController.create, CityController.update, etc. - -------------------------------------------------------------------- - INSERT INTO permission_ids (id) - SELECT p.id - FROM ss_permission_metadata AS p - WHERE p.name LIKE (v_controller_name || '.%'); - - -------------------------------------------------------------------- - -- 1) Delete user-view metadata - -------------------------------------------------------------------- - DELETE FROM ss_user_view_metadata AS uvm - WHERE uvm.view_metadata_id IN (SELECT id FROM view_ids); - - -------------------------------------------------------------------- - -- 2) Delete menu items linked via action_id - -- Role join table is handled by ON DELETE CASCADE (assumed) - -------------------------------------------------------------------- - DELETE FROM ss_menu_item_metadata AS mi - WHERE mi.action_id IN (SELECT id FROM action_ids); - - -------------------------------------------------------------------- - -- 3) Delete actions - -------------------------------------------------------------------- - DELETE FROM ss_action_metadata AS a - WHERE a.id IN (SELECT id FROM action_ids); - - -------------------------------------------------------------------- - -- 4) Delete views - -------------------------------------------------------------------- - DELETE FROM ss_view_metadata AS v - WHERE v.id IN (SELECT id FROM view_ids); - - -------------------------------------------------------------------- - -- 5) Delete import transaction error logs for this model's imports - -------------------------------------------------------------------- - DELETE FROM ss_import_transaction_error_log AS itel - WHERE itel.import_transaction_id IN ( - SELECT it.id - FROM ss_import_transaction AS it - WHERE it.model_metadata_id = v_model_id - ); - - -------------------------------------------------------------------- - -- 6) Delete import transactions linked to the model - -------------------------------------------------------------------- - DELETE FROM ss_import_transaction AS it - WHERE it.model_metadata_id = v_model_id; - - -------------------------------------------------------------------- - -- 7) Delete permissions for this controller - -- a) Delete from role-permission join table - -- b) Delete from permission metadata - -------------------------------------------------------------------- - DELETE FROM ss_role_metadata_permissions_ss_permission_metadata AS rp - WHERE rp.ss_permission_metadata_id IN (SELECT id FROM permission_ids); - - DELETE FROM ss_permission_metadata AS p - WHERE p.id IN (SELECT id FROM permission_ids); - - -------------------------------------------------------------------- - -- 8) Optionally delete the model row itself (+ fields) - -------------------------------------------------------------------- - IF p_also_drop_model THEN - -------------------------------------------------------------- - -- 8a) Null user_key_field_id pointing to fields of this model - -------------------------------------------------------------- - UPDATE ss_model_metadata AS mm - SET user_key_field_id = NULL - WHERE mm.user_key_field_id IN ( - SELECT f.id - FROM ss_field_metadata AS f - WHERE f.model_id = v_model_id - ); - - -------------------------------------------------------------- - -- 8b) Null parent_model_id pointing to this model - -------------------------------------------------------------- - UPDATE ss_model_metadata AS mm - SET parent_model_id = NULL - WHERE mm.parent_model_id = v_model_id; - - -------------------------------------------------------------- - -- 8c) Delete model - -- Fields are deleted automatically via ON DELETE CASCADE - -------------------------------------------------------------- - DELETE FROM ss_model_metadata AS m - WHERE m.id = v_model_id; - END IF; - -EXCEPTION - WHEN others THEN - RAISE; -END; -$$; \ No newline at end of file diff --git a/sql/default/postgres/scratchpad.sql.txt b/sql/default/postgres/scratchpad.sql.txt deleted file mode 100644 index df26098d..00000000 --- a/sql/default/postgres/scratchpad.sql.txt +++ /dev/null @@ -1,12 +0,0 @@ --- delete models -CALL cleanup_model_metadata('myrusPincodeMaster', false); -CALL cleanup_model_metadata('subSubCategoryMaster', true); -CALL cleanup_model_metadata('subCategoryMaster', true); -CALL cleanup_model_metadata('categoryMaster', true); -CALL cleanup_model_metadata('city', true); -CALL cleanup_model_metadata('state', true); -CALL cleanup_model_metadata('bankIfscMaster', true); -CALL cleanup_model_metadata('applicationDocumentVerificationMob2Vtb', true); - --- delete module -CALL cleanup_module_metadata('myrus-address-master', false); \ No newline at end of file diff --git a/src/services/database/database-bootstrap.service.ts b/src/services/database/database-bootstrap.service.ts deleted file mode 100644 index 0b55d225..00000000 --- a/src/services/database/database-bootstrap.service.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; -import { DataSource } from 'typeorm'; -import { readdir, readFile } from 'fs/promises'; -import * as path from 'path'; -import { InjectDataSource } from '@nestjs/typeorm'; - -@Injectable() -export class DatabaseBootstrapService implements OnModuleInit { - private readonly logger = new Logger(DatabaseBootstrapService.name); - - constructor( - @InjectDataSource() - private readonly dataSource: DataSource, - ) { } - - async onModuleInit() { - if (!this.dataSource.isInitialized) { - this.logger.warn(`[${this.dataSource.name}] DataSource not initialized. Skipping SQL bootstrap.`); - return; - } - - this.logger.debug(`[${this.dataSource.name}] Bootstrapping stored procedures...`); - - await this.applyAllSqlFiles(); - - this.logger.debug(`[${this.dataSource.name}] SQL bootstrap completed`); - } - - private resolveSqlDirectory(): string { - const datasourceName = this.dataSource.name || 'default'; - const dbType = this.dataSource.options.type; - - const sqlFilePath = path.resolve(__dirname, `../../../sql/${datasourceName}/${dbType}`); - - return sqlFilePath - } - - private async applyAllSqlFiles() { - const sqlDir = this.resolveSqlDirectory(); - - this.logger.debug(`[${this.dataSource.name}] SQL directory: ${sqlDir}`); - - let files: string[]; - try { - files = await readdir(sqlDir); - } catch { - this.logger.warn( - `[${this.dataSource.name}] No SQL directory found. Skipping.`, - ); - return; - } - - const sqlFiles = files - .filter(file => file.endsWith('.sql')) - .sort(); - - if (!sqlFiles.length) { - this.logger.warn( - `[${this.dataSource.name}] No SQL files found`, - ); - return; - } - - for (const file of sqlFiles) { - await this.applySqlFileSafely( - path.join(sqlDir, file), - file, - ); - } - } - - private async applySqlFileSafely( - filePath: string, - fileName: string, - ) { - this.logger.debug(`[${this.dataSource.name}] Applying ${fileName}`); - - try { - const sql = await readFile(filePath, 'utf8'); - await this.dataSource.query(sql); - - this.logger.debug(`[${this.dataSource.name}] Applied ${fileName}`); - } catch (error: any) { - // DO NOT THROW — continue with next file - this.logger.error( - `[${this.dataSource.name}] Failed ${fileName}`, - error instanceof Error ? error.stack : String(error), - ); - } - } -} diff --git a/src/services/model-metadata.service.ts b/src/services/model-metadata.service.ts index d704006d..bec4c359 100755 --- a/src/services/model-metadata.service.ts +++ b/src/services/model-metadata.service.ts @@ -2,7 +2,7 @@ import { BadRequestException, forwardRef, Inject, Injectable, Logger, NotFoundEx import { InjectDataSource } from '@nestjs/typeorm'; import * as fs from 'fs/promises'; // Use the Promise-based version of fs for async/await import * as path from 'path'; -import { DataSource, EntityManager, Repository, SelectQueryBuilder } from 'typeorm'; +import { DataSource, EntityManager, In, Repository, SelectQueryBuilder } from 'typeorm'; import { CreateModelMetadataDto } from '../dtos/create-model-metadata.dto'; import { ModelMetadata } from '../entities/model-metadata.entity'; import { ModuleMetadata } from '../entities/module-metadata.entity'; @@ -19,7 +19,11 @@ import { BasicFilterDto } from '../dtos/basic-filters.dto'; import { UpdateModelMetaDataDto } from '../dtos/update-model-metadata.dto'; import { ActionMetadata } from '../entities/action-metadata.entity'; import { FieldMetadata } from '../entities/field-metadata.entity'; +import { ImportTransactionErrorLog } from '../entities/import-transaction-error-log.entity'; +import { ImportTransaction } from '../entities/import-transaction.entity'; import { MenuItemMetadata } from '../entities/menu-item-metadata.entity'; +import { UserViewMetadata } from '../entities/user-view-metadata.entity'; +import { PermissionMetadata } from '../entities/permission-metadata.entity'; import { ViewMetadata } from '../entities/view-metadata.entity'; import { CommandService } from '../helpers/command.service'; import { @@ -556,7 +560,7 @@ export class ModelMetadataService { // @ts-ignore id: modelEntityId, }, - relations: ['module'] + relations: ['module', 'fields'] }); if (!modelEntity) { @@ -617,67 +621,20 @@ export class ModelMetadataService { } } - await this.dataSource.query( - `CALL cleanup_model_metadata($1, $2)`, - [modelEntity.singularName, true], - ); - - // Delete the permissions, menu, actions & views related to this model. - // const controllerName = `${classify(modelEntity.singularName)}Controller`; - // const permissionNames = [ - // `${controllerName}.delete`, - // `${controllerName}.deleteMany`, - // `${controllerName}.findOne`, - // `${controllerName}.findMany`, - // `${controllerName}.recover`, - // `${controllerName}.recoverMany`, - // `${controllerName}.partialUpdate`, - // `${controllerName}.update`, - // `${controllerName}.insertMany`, - // `${controllerName}.create`, - // ]; - // const permissionsRepo = this.dataSource.getRepository(PermissionMetadata); - // const permissionsToDelete = await permissionsRepo.find({ - // where: { name: In(permissionNames) }, - // relations: ['roles'], - // }); - - // Remove role associations first - // for (const permission of permissionsToDelete) { - // if (permission.roles?.length) { - // await this.dataSource - // .createQueryBuilder() - // .relation(PermissionMetadata, 'roles') - // .of(permission) // permission instance or its ID - // .remove(permission.roles); // remove all linked roles - // } - // } - - // await permissionsRepo.remove(permissionsToDelete); - - // Delete actions - // const actionRepo = this.dataSource.getRepository(ActionMetadata); - // const action = await actionRepo.findOne({ where: { model: { id: modelEntity.id } } }); - // await actionRepo.delete({ model: { id: modelEntity.id } }); - - // // Delete menu items - // const menuItemRepo = this.dataSource.getRepository(MenuItemMetadata); - // if (action) { - // const menuItems = await menuItemRepo.find({ where: { action: { id: action.id } } }); - // for (let i = 0; i < menuItems.length; i++) { - // const menuItem = menuItems[i]; - // await menuItemRepo.remove(menuItem); - // } - // } - - // Delete view - // const viewRepo = this.dataSource.getRepository(ViewMetadata); - // await viewRepo.delete({ model: { id: modelEntity.id } }) + const { removedActionNames, removedMenuNames, removedViewNames } = await this.cleanupAssociatedViewsActionsAndMenus(modelEntity.id); + await this.cleanupAssociatedImports(modelEntity.id); + await this.cleanupAssociatedPermissions(modelEntity.singularName); + await this.clearModelReferencesBeforeDelete(modelEntity); // -metadata.json | Remove references to this model in the model metadata, menu, action & view sections. | Automatic const filePath = await this.moduleMetadataHelperService.getModuleMetadataFilePath(modelEntity.module?.name); const metaData = await this.moduleMetadataHelperService.getModuleMetadataConfiguration(filePath); if (metaData) { + const moduleMetadata = metaData?.moduleMetadata ?? metaData; + const removedActionNameSet = new Set(removedActionNames); + const removedMenuNameSet = new Set(removedMenuNames); + const removedViewNameSet = new Set(removedViewNames); + const existingModelIndex = metaData.moduleMetadata.models.findIndex( (existingModel: any) => existingModel.singularName === modelEntity.singularName ); @@ -688,15 +645,51 @@ export class ModelMetadataService { } // Remove references to this model in the menu, action & view sections. - metaData.moduleMetadata.menus = metaData.moduleMetadata?.menus?.filter( - (menu: any) => menu.modelUserKey !== modelEntity.singularName - ); - metaData.moduleMetadata.actions = metaData.moduleMetadata?.actions?.filter( - (action: any) => action.modelUserKey !== modelEntity.singularName - ); - metaData.moduleMetadata.views = metaData.moduleMetadata?.views?.filter( - (view: any) => view.modelUserKey !== modelEntity.singularName - ); + const existingViews = Array.isArray(moduleMetadata?.views) ? moduleMetadata.views : []; + moduleMetadata.views = existingViews.filter((view: any) => { + const shouldRemove = view?.modelUserKey === modelEntity.singularName || removedViewNameSet.has(view?.name); + if (shouldRemove && view?.name) { + removedViewNameSet.add(view.name); + } + return !shouldRemove; + }); + + const existingActions = Array.isArray(moduleMetadata?.actions) ? moduleMetadata.actions : []; + moduleMetadata.actions = existingActions.filter((action: any) => { + const shouldRemove = + action?.modelUserKey === modelEntity.singularName || + removedActionNameSet.has(action?.name) || + removedViewNameSet.has(action?.viewUserKey); + + if (shouldRemove && action?.name) { + removedActionNameSet.add(action.name); + } + return !shouldRemove; + }); + + let pendingMenus = Array.isArray(moduleMetadata?.menus) ? [...moduleMetadata.menus] : []; + let menuRemovedOnPass = true; + while (menuRemovedOnPass) { + menuRemovedOnPass = false; + pendingMenus = pendingMenus.filter((menu: any) => { + const shouldRemove = + menu?.modelUserKey === modelEntity.singularName || + removedMenuNameSet.has(menu?.name) || + removedActionNameSet.has(menu?.actionUserKey) || + removedMenuNameSet.has(menu?.parentMenuItemUserKey); + + if (shouldRemove) { + if (menu?.name) { + removedMenuNameSet.add(menu.name); + } + menuRemovedOnPass = true; + return false; + } + + return true; + }); + } + moduleMetadata.menus = pendingMenus; const updatedContent = JSON.stringify(metaData, null, 2); await fs.writeFile(filePath, updatedContent); @@ -725,6 +718,210 @@ export class ModelMetadataService { } + private async cleanupAssociatedViewsActionsAndMenus(modelId: number) { + const viewRepo = this.dataSource.getRepository(ViewMetadata); + const actionRepo = this.dataSource.getRepository(ActionMetadata); + const menuRepo = this.dataSource.getRepository(MenuItemMetadata); + const userViewRepo = this.dataSource.getRepository(UserViewMetadata); + + const views = await viewRepo.find({ + where: { + model: { id: modelId }, + }, + }); + const viewIds = views.map((view) => view.id); + const removedViewNames = views.map((view) => view.name).filter(Boolean); + + const actions = await actionRepo.find({ + where: [ + { + model: { id: modelId }, + }, + ...(viewIds.length > 0 + ? [ + { + view: { id: In(viewIds) }, + }, + ] + : []), + ], + relations: ['view'], + }); + + const uniqueActions = Array.from( + new Map(actions.map((action) => [action.id, action])).values(), + ); + const actionIds = uniqueActions.map((action) => action.id); + const removedActionNames = uniqueActions.map((action) => action.name).filter(Boolean); + + const menus = await this.findMenusForActionIds(actionIds); + const removedMenuNames = menus.map((menu) => menu.name).filter(Boolean); + + if (menus.length > 0) { + const orderedMenus = [...menus].reverse(); + for (const menu of orderedMenus) { + if (menu.roles?.length) { + await this.dataSource + .createQueryBuilder() + .relation(MenuItemMetadata, 'roles') + .of(menu.id) + .remove(menu.roles.map((role) => role.id)); + } + } + + for (const menu of orderedMenus) { + await menuRepo.remove(menu); + } + this.logger.log(`Deleted ${menus.length} menu metadata record(s) for model id ${modelId}`); + } + + if (uniqueActions.length > 0) { + await actionRepo.remove(uniqueActions); + this.logger.log(`Deleted ${uniqueActions.length} action metadata record(s) for model id ${modelId}`); + } + + if (viewIds.length > 0) { + const userViews = await userViewRepo.find({ + where: { + viewMetadata: { id: In(viewIds) }, + }, + relations: ['viewMetadata'], + }); + if (userViews.length > 0) { + await userViewRepo.remove(userViews); + this.logger.log(`Deleted ${userViews.length} user view metadata record(s) for model id ${modelId}`); + } + + await viewRepo.remove(views); + this.logger.log(`Deleted ${views.length} view metadata record(s) for model id ${modelId}`); + } + + return { + removedActionNames, + removedMenuNames, + removedViewNames, + }; + } + + private async cleanupAssociatedImports(modelId: number) { + const importTransactionRepo = this.dataSource.getRepository(ImportTransaction); + const importTransactionErrorLogRepo = this.dataSource.getRepository(ImportTransactionErrorLog); + + const importTransactions = await importTransactionRepo.find({ + where: { + modelMetadata: { id: modelId }, + }, + }); + + if (importTransactions.length === 0) { + return; + } + + const importTransactionIds = importTransactions.map((transaction) => transaction.id); + const importTransactionErrorLogs = await importTransactionErrorLogRepo.find({ + where: { + importTransaction: { id: In(importTransactionIds) }, + }, + relations: ['importTransaction'], + }); + + if (importTransactionErrorLogs.length > 0) { + await importTransactionErrorLogRepo.remove(importTransactionErrorLogs); + this.logger.log(`Deleted ${importTransactionErrorLogs.length} import transaction error log record(s) for model id ${modelId}`); + } + + await importTransactionRepo.remove(importTransactions); + this.logger.log(`Deleted ${importTransactions.length} import transaction record(s) for model id ${modelId}`); + } + + private async cleanupAssociatedPermissions(modelSingularName: string) { + const permissionsRepo = this.dataSource.getRepository(PermissionMetadata); + const controllerName = `${classify(modelSingularName)}Controller`; + const permissions = await permissionsRepo + .createQueryBuilder('permission') + .leftJoinAndSelect('permission.roles', 'role') + .where('permission.name LIKE :pattern', { pattern: `${controllerName}.%` }) + .getMany(); + + if (permissions.length === 0) { + return; + } + + for (const permission of permissions) { + if (permission.roles?.length) { + await this.dataSource + .createQueryBuilder() + .relation(PermissionMetadata, 'roles') + .of(permission.id) + .remove(permission.roles.map((role) => role.id)); + } + } + + await permissionsRepo.remove(permissions); + this.logger.log(`Deleted ${permissions.length} permission metadata record(s) for model '${modelSingularName}'`); + } + + private async clearModelReferencesBeforeDelete(modelEntity: ModelMetadata) { + const modelRepo = this.dataSource.getRepository(ModelMetadata); + const fieldIds = (modelEntity.fields ?? []).map((field) => field.id).filter(Boolean); + + if (fieldIds.length > 0) { + await modelRepo + .createQueryBuilder() + .update(ModelMetadata) + .set({ userKeyField: null as any }) + .where('user_key_field_id IN (:...fieldIds)', { fieldIds }) + .execute(); + } + + await modelRepo + .createQueryBuilder() + .update(ModelMetadata) + .set({ parentModel: null as any }) + .where('parent_model_id = :modelId', { modelId: modelEntity.id }) + .execute(); + } + + private async findMenusForActionIds(actionIds: number[]) { + if (!actionIds?.length) { + return []; + } + + const menuRepo = this.dataSource.getRepository(MenuItemMetadata); + const menusById = new Map(); + + const rootMenus = await menuRepo.find({ + where: { + action: { id: In(actionIds) }, + }, + relations: ['roles', 'action', 'parentMenuItem'], + }); + + rootMenus.forEach((menu) => menusById.set(menu.id, menu)); + + let parentIds = rootMenus.map((menu) => menu.id); + while (parentIds.length > 0) { + const childMenus = await menuRepo.find({ + where: { + parentMenuItem: { id: In(parentIds) }, + }, + relations: ['roles', 'action', 'parentMenuItem'], + }); + + const nextParentIds: number[] = []; + for (const childMenu of childMenus) { + if (!menusById.has(childMenu.id)) { + menusById.set(childMenu.id, childMenu); + nextParentIds.push(childMenu.id); + } + } + + parentIds = nextParentIds; + } + + return Array.from(menusById.values()); + } + @DisallowInProduction() async generateCodeViaCtl(modelId: number): Promise { const model = await this.findOne(modelId); diff --git a/src/solid-core.module.ts b/src/solid-core.module.ts index 1b87a062..147583ab 100644 --- a/src/solid-core.module.ts +++ b/src/solid-core.module.ts @@ -372,7 +372,6 @@ import { McpCommand } from './commands/mcp.command'; import { FixturesService } from './services/fixtures.service'; import { FixturesSetupCommand } from './commands/fixtures/fixtures-setup.command'; import { FixturesTearDownCommand } from './commands/fixtures/fixtures-tear-down.command'; -import { DatabaseBootstrapService } from './services/database/database-bootstrap.service'; import { SequenceNumComputedFieldProvider } from './services/computed-fields/entity/sequence-num-computed-field-provider'; import { ModelSequence } from './entities/model-sequence.entity'; import { ModelSequenceService } from './services/model-sequence.service'; @@ -808,7 +807,6 @@ import { DashboardUserLayoutRepository } from './repositories/dashboard-user-lay FixturesService, FixturesSetupCommand, FixturesTearDownCommand, - DatabaseBootstrapService, SequenceNumComputedFieldProvider, ModelSequenceService, ModelSequenceRepository, From 267c2521777622f19a5b4f000ba992bb83750175 Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Wed, 10 Jun 2026 09:54:01 +0100 Subject: [PATCH 108/136] Remove cleanup procedures for module metadata across all SQL dialects and implement menu and action cleanup in the service layer. --- .../mariadb/proc_CleanupModuleMetadata.sql | 56 ------------ .../mssql/proc_CleanupModuleMetadata.sql | 78 ---------------- .../mysql/proc_CleanupModuleMetadata.sql | 56 ------------ .../postgres/proc_CleanupModuleMetadata.sql | 50 ---------- src/services/module-metadata.service.ts | 91 ++++++++++++++++++- 5 files changed, 86 insertions(+), 245 deletions(-) delete mode 100644 sql/default/mariadb/proc_CleanupModuleMetadata.sql delete mode 100644 sql/default/mssql/proc_CleanupModuleMetadata.sql delete mode 100644 sql/default/mysql/proc_CleanupModuleMetadata.sql delete mode 100644 sql/default/postgres/proc_CleanupModuleMetadata.sql diff --git a/sql/default/mariadb/proc_CleanupModuleMetadata.sql b/sql/default/mariadb/proc_CleanupModuleMetadata.sql deleted file mode 100644 index 97c81206..00000000 --- a/sql/default/mariadb/proc_CleanupModuleMetadata.sql +++ /dev/null @@ -1,56 +0,0 @@ -DROP PROCEDURE IF EXISTS cleanup_module_metadata; - -DELIMITER $$ - -CREATE PROCEDURE cleanup_module_metadata( - IN p_module_name VARCHAR(255), - IN p_also_drop_module TINYINT(1) -- 0 = false, 1 = true (DEFAULT 0) -) -BEGIN - DECLARE v_module_id INT; - - -- Default p_also_drop_module to 0 when NULL is passed - IF p_also_drop_module IS NULL THEN - SET p_also_drop_module = 0; - END IF; - - SELECT m.id - INTO v_module_id - FROM ss_module_metadata AS m - WHERE m.name = p_module_name - LIMIT 1; - - IF v_module_id IS NULL THEN - SIGNAL SQLSTATE '45000' - SET MESSAGE_TEXT = CONCAT('CleanupModuleMetadata: Module "', p_module_name, '" not found.'); - END IF; - - ---------------------------------------------------------------- - -- 1) Delete menus for this module (children first, then roots) - ---------------------------------------------------------------- - DELETE FROM ss_menu_item_metadata - WHERE module_id = v_module_id - AND parent_menu_item_id IS NOT NULL; - - DELETE FROM ss_menu_item_metadata - WHERE module_id = v_module_id - AND parent_menu_item_id IS NULL; - - ---------------------------------------------------------------- - -- 2) Delete actions for this module - ---------------------------------------------------------------- - DELETE FROM ss_action_metadata - WHERE module_id = v_module_id; - - ---------------------------------------------------------------- - -- 3) Optionally delete the module row itself - -- (assumes ss_model_metadata.module_id has ON DELETE SET NULL) - ---------------------------------------------------------------- - IF p_also_drop_module = 1 THEN - DELETE FROM ss_module_metadata - WHERE id = v_module_id; - END IF; - -END$$ - -DELIMITER ; diff --git a/sql/default/mssql/proc_CleanupModuleMetadata.sql b/sql/default/mssql/proc_CleanupModuleMetadata.sql deleted file mode 100644 index d0f78976..00000000 --- a/sql/default/mssql/proc_CleanupModuleMetadata.sql +++ /dev/null @@ -1,78 +0,0 @@ -CREATE PROCEDURE [dbo].[CleanupModuleMetadata] - @ModuleName NVARCHAR(255), - @AlsoDropModule BIT = 0 -- 0 = only menus + actions, 1 = also delete module row -AS -BEGIN - SET NOCOUNT ON; - - DECLARE @ModuleId INT; - - SELECT @ModuleId = m.id - FROM dbo.ss_module_metadata AS m - WHERE m.name = @ModuleName; - - IF @ModuleId IS NULL - BEGIN - RAISERROR ('CleanupModuleMetadata: Module "%s" not found.', 16, 1, @ModuleName); - RETURN; - END - - BEGIN TRY - BEGIN TRAN; - - ---------------------------------------------------------------- - -- 1) Delete menu items for this module - -- (children first because parent FK has NO cascade) - ---------------------------------------------------------------- - -- Children - DELETE mi - FROM dbo.ss_menu_item_metadata AS mi - WHERE mi.module_id = @ModuleId - AND mi.parent_menu_item_id IS NOT NULL; - - -- Roots - DELETE mi - FROM dbo.ss_menu_item_metadata AS mi - WHERE mi.module_id = @ModuleId - AND mi.parent_menu_item_id IS NULL; - - -- Note: Role join table - -- dbo.ss_menu_item_metadata_roles_ss_role_metadata - -- is cleaned automatically via ON DELETE CASCADE from menu items. - - ---------------------------------------------------------------- - -- 2) Delete actions for this module - ---------------------------------------------------------------- - DELETE a - FROM dbo.ss_action_metadata AS a - WHERE a.module_id = @ModuleId; - - ---------------------------------------------------------------- - -- 3) Optionally delete the module row itself - -- Models keep their rows; module_id is set to NULL on delete - -- because of ON DELETE SET NULL in ss_model_metadata. - ---------------------------------------------------------------- - IF @AlsoDropModule = 1 - BEGIN - DELETE m - FROM dbo.ss_module_metadata AS m - WHERE m.id = @ModuleId; - END - - COMMIT TRAN; - END TRY - BEGIN CATCH - IF @@TRANCOUNT > 0 - ROLLBACK TRAN; - - DECLARE @ErrorMessage NVARCHAR(4000), - @ErrorSeverity INT, - @ErrorState INT; - - SELECT @ErrorMessage = ERROR_MESSAGE(), - @ErrorSeverity = ERROR_SEVERITY(), - @ErrorState = ERROR_STATE(); - - RAISERROR(@ErrorMessage, @ErrorSeverity, @ErrorState); - END CATCH -END diff --git a/sql/default/mysql/proc_CleanupModuleMetadata.sql b/sql/default/mysql/proc_CleanupModuleMetadata.sql deleted file mode 100644 index 97c81206..00000000 --- a/sql/default/mysql/proc_CleanupModuleMetadata.sql +++ /dev/null @@ -1,56 +0,0 @@ -DROP PROCEDURE IF EXISTS cleanup_module_metadata; - -DELIMITER $$ - -CREATE PROCEDURE cleanup_module_metadata( - IN p_module_name VARCHAR(255), - IN p_also_drop_module TINYINT(1) -- 0 = false, 1 = true (DEFAULT 0) -) -BEGIN - DECLARE v_module_id INT; - - -- Default p_also_drop_module to 0 when NULL is passed - IF p_also_drop_module IS NULL THEN - SET p_also_drop_module = 0; - END IF; - - SELECT m.id - INTO v_module_id - FROM ss_module_metadata AS m - WHERE m.name = p_module_name - LIMIT 1; - - IF v_module_id IS NULL THEN - SIGNAL SQLSTATE '45000' - SET MESSAGE_TEXT = CONCAT('CleanupModuleMetadata: Module "', p_module_name, '" not found.'); - END IF; - - ---------------------------------------------------------------- - -- 1) Delete menus for this module (children first, then roots) - ---------------------------------------------------------------- - DELETE FROM ss_menu_item_metadata - WHERE module_id = v_module_id - AND parent_menu_item_id IS NOT NULL; - - DELETE FROM ss_menu_item_metadata - WHERE module_id = v_module_id - AND parent_menu_item_id IS NULL; - - ---------------------------------------------------------------- - -- 2) Delete actions for this module - ---------------------------------------------------------------- - DELETE FROM ss_action_metadata - WHERE module_id = v_module_id; - - ---------------------------------------------------------------- - -- 3) Optionally delete the module row itself - -- (assumes ss_model_metadata.module_id has ON DELETE SET NULL) - ---------------------------------------------------------------- - IF p_also_drop_module = 1 THEN - DELETE FROM ss_module_metadata - WHERE id = v_module_id; - END IF; - -END$$ - -DELIMITER ; diff --git a/sql/default/postgres/proc_CleanupModuleMetadata.sql b/sql/default/postgres/proc_CleanupModuleMetadata.sql deleted file mode 100644 index e37b2343..00000000 --- a/sql/default/postgres/proc_CleanupModuleMetadata.sql +++ /dev/null @@ -1,50 +0,0 @@ -CREATE OR REPLACE PROCEDURE cleanup_module_metadata( - IN p_module_name text, - IN p_also_drop_module boolean DEFAULT false -) -LANGUAGE plpgsql -AS $$ -DECLARE - v_module_id int; -BEGIN - SELECT m.id - INTO v_module_id - FROM ss_module_metadata AS m - WHERE m.name = p_module_name; - - IF v_module_id IS NULL THEN - RAISE EXCEPTION 'CleanupModuleMetadata: Module "%" not found.', p_module_name; - END IF; - - ---------------------------------------------------------------- - -- 1) Delete menus for this module (children first, then roots) - ---------------------------------------------------------------- - DELETE FROM ss_menu_item_metadata AS mi - WHERE mi.module_id = v_module_id - AND mi.parent_menu_item_id IS NOT NULL; - - DELETE FROM ss_menu_item_metadata AS mi - WHERE mi.module_id = v_module_id - AND mi.parent_menu_item_id IS NULL; - - ---------------------------------------------------------------- - -- 2) Delete actions for this module - ---------------------------------------------------------------- - DELETE FROM ss_action_metadata AS a - WHERE a.module_id = v_module_id; - - ---------------------------------------------------------------- - -- 3) Optionally delete the module row itself - -- (assumes ss_model_metadata.module_id has ON DELETE SET NULL) - ---------------------------------------------------------------- - IF p_also_drop_module THEN - DELETE FROM ss_module_metadata AS m - WHERE m.id = v_module_id; - END IF; - -EXCEPTION - WHEN others THEN - -- preserve original error message/SQLSTATE - RAISE; -END; -$$; \ No newline at end of file diff --git a/src/services/module-metadata.service.ts b/src/services/module-metadata.service.ts index 9b46de4c..d9b4d4b4 100755 --- a/src/services/module-metadata.service.ts +++ b/src/services/module-metadata.service.ts @@ -2,8 +2,10 @@ import { BadRequestException, forwardRef, Inject, Injectable, Logger, NotFoundEx import { InjectDataSource } from '@nestjs/typeorm'; import { DEFAULT_MEDIA_FILE_STORAGE_DIR } from "src/services/settings/default-settings-provider.service"; import type { SolidCoreSetting } from "src/services/settings/default-settings-provider.service"; -import { DataSource, EntityManager, SelectQueryBuilder } from 'typeorm'; +import { DataSource, EntityManager, In, SelectQueryBuilder } from 'typeorm'; +import { ActionMetadata } from '../entities/action-metadata.entity'; import { CreateModuleMetadataDto } from '../dtos/create-module-metadata.dto'; +import { MenuItemMetadata } from '../entities/menu-item-metadata.entity'; import { ModuleMetadata } from '../entities/module-metadata.entity'; import { classify } from '../helpers/string.helper'; @@ -377,10 +379,89 @@ export class ModuleMetadataService { throw new Error(ERROR_MESSAGES.FILE_DELETE_FAILED); // Trigger rollback } } - await this.dataSource.query( - `CALL cleanup_module_metadata($1, $2)`, - [moduleEntity.name, true], - ); + await this.cleanupAssociatedMenusAndActions(moduleEntity.id); + } + + private async cleanupAssociatedMenusAndActions(moduleId: number) { + const actionRepo = this.dataSource.getRepository(ActionMetadata); + const menuRepo = this.dataSource.getRepository(MenuItemMetadata); + + const actions = await actionRepo.find({ + where: { + module: { id: moduleId }, + }, + }); + const actionIds = actions.map((action) => action.id); + + const menus = await this.findMenusForModuleCleanup(moduleId, actionIds); + if (menus.length > 0) { + const orderedMenus = [...menus].reverse(); + for (const menu of orderedMenus) { + if (menu.roles?.length) { + await this.dataSource + .createQueryBuilder() + .relation(MenuItemMetadata, 'roles') + .of(menu.id) + .remove(menu.roles.map((role) => role.id)); + } + } + + for (const menu of orderedMenus) { + await menuRepo.remove(menu); + } + + this.logger.log(`Deleted ${menus.length} menu metadata record(s) for module id ${moduleId}`); + } + + if (actions.length > 0) { + await actionRepo.remove(actions); + this.logger.log(`Deleted ${actions.length} action metadata record(s) for module id ${moduleId}`); + } + } + + private async findMenusForModuleCleanup(moduleId: number, actionIds: number[]) { + const menuRepo = this.dataSource.getRepository(MenuItemMetadata); + const menusById = new Map(); + + const rootMenus = await menuRepo.find({ + where: [ + { + module: { id: moduleId }, + }, + ...(actionIds.length > 0 + ? [ + { + action: { id: In(actionIds) }, + }, + ] + : []), + ], + relations: ['roles', 'action', 'parentMenuItem'], + }); + + rootMenus.forEach((menu) => menusById.set(menu.id, menu)); + + let parentIds = rootMenus.map((menu) => menu.id); + while (parentIds.length > 0) { + const childMenus = await menuRepo.find({ + where: { + parentMenuItem: { id: In(parentIds) }, + }, + relations: ['roles', 'action', 'parentMenuItem'], + }); + + const nextParentIds: number[] = []; + for (const childMenu of childMenus) { + if (!menusById.has(childMenu.id)) { + menusById.set(childMenu.id, childMenu); + nextParentIds.push(childMenu.id); + } + } + + parentIds = nextParentIds; + } + + return Array.from(menusById.values()); } async deleteMany(ids: number[]): Promise { From 5188c7b4290ea6794ae7a45d2a6c4f5fa47e01be Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Wed, 10 Jun 2026 15:54:35 +0530 Subject: [PATCH 109/136] Add media tree view and action & media view cleanup, enhance file storage provider with file size tracking, implemented storeStreams for s3 --- .../seed-data/solid-core-metadata.json | 121 +++++++++++++++--- .../file-s3-storage-provider.ts | 39 +++++- .../file-storage-provider.ts | 6 +- 3 files changed, 148 insertions(+), 18 deletions(-) diff --git a/src/seeders/seed-data/solid-core-metadata.json b/src/seeders/seed-data/solid-core-metadata.json index b6afd881..c110a6b5 100755 --- a/src/seeders/seed-data/solid-core-metadata.json +++ b/src/seeders/seed-data/solid-core-metadata.json @@ -6262,6 +6262,19 @@ "viewUserKey": "userViewMetadata-tree-view", "moduleUserKey": "solid-core", "modelUserKey": "userViewMetadata" + }, + { + "displayName": "Media Tree Action", + "name": "media-tree-action", + "type": "solid", + "domain": "", + "context": "", + "customComponent": "", + "customIsModal": true, + "serverEndpoint": "", + "viewUserKey": "media-tree-view", + "moduleUserKey": "solid-core", + "modelUserKey": "media" } ], "menus": [ @@ -8651,49 +8664,57 @@ 50 ], "enableGlobalSearch": true, - "create": true, - "edit": true, + "create": false, + "edit": false, "delete": true, "allowedViews": [ "list", - "card" - ] + "card", + "tree" + ], + "import": false, + "export": false }, "children": [ { "type": "field", "attrs": { - "name": "entityId", - "isSearchable": true + "name": "modelMetadata", + "isSearchable": true, + "sortable": true, + "searchField": "modelMetadata.name" } }, { "type": "field", "attrs": { - "name": "relativeUri", - "widget": "image", - "isSearchable": true + "name": "fieldMetadata", + "isSearchable": true, + "sortable": true, + "searchField": "fieldMetadata.name" } }, { "type": "field", "attrs": { - "name": "modelMetadata", - "isSearchable": true + "name": "entityId", + "isSearchable": true, + "sortable": false } }, { "type": "field", "attrs": { "name": "mediaStorageProviderMetadata", - "isSearchable": true + "isSearchable": false, + "sortable": false } }, { "type": "field", "attrs": { - "name": "fieldMetadata", - "isSearchable": true + "name": "fileSize", + "sortable": true } } ] @@ -8806,7 +8827,9 @@ "attrs": { "name": "form-1", "label": "Solid Media Model", - "className": "grid" + "className": "grid", + "showAddFormButton": false, + "showEditFormButton": false }, "children": [ { @@ -13833,6 +13856,74 @@ } ] } + }, + { + "name": "media-tree-view", + "displayName": "Media Tree View", + "type": "tree", + "context": "{}", + "moduleUserKey": "solid-core", + "modelUserKey": "media", + "layout": { + "type": "list", + "attrs": { + "pagination": true, + "pageSizeOptions": [ + 10, + 25, + 50 + ], + "enableGlobalSearch": true, + "create": false, + "edit": false, + "delete": false + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "modelMetadata", + "isSearchable": true, + "sortable": true, + "searchField": "modelMetadata.name" + } + }, + { + "type": "field", + "attrs": { + "name": "fieldMetadata", + "isSearchable": true, + "sortable": true, + "searchField": "fieldMetadata.name" + } + }, + { + "type": "field", + "attrs": { + "name": "entityId", + "isSearchable": true, + "sortable": false + } + }, + { + "type": "field", + "attrs": { + "name": "mediaStorageProviderMetadata", + "isSearchable": false, + "sortable": false + } + }, + { + "type": "field", + "attrs": { + "name": "relativeUri", + "widget": "image", + "isSearchable": false, + "sortable": false + } + } + ] + } } ], "emailTemplates": [ diff --git a/src/services/mediaStorageProviders/file-s3-storage-provider.ts b/src/services/mediaStorageProviders/file-s3-storage-provider.ts index cba2343f..fe3ebd43 100755 --- a/src/services/mediaStorageProviders/file-s3-storage-provider.ts +++ b/src/services/mediaStorageProviders/file-s3-storage-provider.ts @@ -21,8 +21,43 @@ export class FileS3StorageProvider implements MediaStorageProvider { readonly mediaRepository: MediaRepository, ) { } - storeStreams(streamPairs: [Readable, string][], entity: T, mediaFieldMetadata: FieldMetadata): Promise { - throw new Error("Method not implemented."); + async storeStreams(streamPairs: [Readable, string][], entity: T, mediaFieldMetadata: FieldMetadata): Promise { + const isSupportedEntity = entity instanceof CommonEntity + || entity instanceof LegacyCommonEntityWithExistingId + || entity instanceof LegacyCommonEntityWithGeneratedId; + if (!isSupportedEntity) { + throw new Error("Entity must be an instance of CommonEntity, LegacyCommonEntityWithExistingId or LegacyCommonEntityWithGeneratedId"); + } + const result: Media[] = []; + const storageProvider = mediaFieldMetadata.mediaStorageProvider; + const region = this.getEffectiveRegion(storageProvider.region); + + for (const [stream, fileName] of streamPairs) { + const bucketName = storageProvider.bucketName; + + // Buffer the stream so we can get the byte count and upload in one pass + const chunks: Buffer[] = []; + for await (const chunk of stream) { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + } + const fileData = Buffer.concat(chunks); + const fileSize = fileData.length; + + await this.s3FileService.write(`${bucketName}:${fileName}`, fileData, { region }); + + const mediaEntity = await this.mediaRepository.createMedia({ + // @ts-ignore + entityId: entity.id, + modelMetadataId: mediaFieldMetadata.model.id, + relativeUri: fileName, + fileSize, + mediaStorageProviderMetadataId: mediaFieldMetadata.mediaStorageProvider.id, + fieldMetadataId: mediaFieldMetadata.id + }) as unknown as Media; + result.push(mediaEntity); + this.logger.debug(`Stored media with`, mediaEntity); + } + return result; } async retrieve(entity: T, mediaFieldMetadata: FieldMetadata): Promise { diff --git a/src/services/mediaStorageProviders/file-storage-provider.ts b/src/services/mediaStorageProviders/file-storage-provider.ts index 8e5be961..53f9b053 100755 --- a/src/services/mediaStorageProviders/file-storage-provider.ts +++ b/src/services/mediaStorageProviders/file-storage-provider.ts @@ -8,6 +8,7 @@ import { MediaRepository } from "src/repository/media.repository"; import { DiskFileService } from "src/services/file"; import { Readable } from "stream"; import * as path from "path"; +import * as fs from "fs"; import { SettingService } from "../setting.service"; import { DEFAULT_MEDIA_FILE_STORAGE_DIR } from "src/services/settings/default-settings-provider.service"; import type { SolidCoreSetting } from "src/services/settings/default-settings-provider.service"; @@ -80,12 +81,15 @@ export class FileStorageProvider implements MediaStorageProvider { for (const pair of streamPairs) { const stream = pair[0]; const fileName = pair[1]; - await this.fileService.writeStream(this.getFullFilePath(fileName), stream); + const fullPath = this.getFullFilePath(fileName); + await this.fileService.writeStream(fullPath, stream); + const { size: fileSize } = await fs.promises.stat(fullPath); const mediaEntity = await this.mediaRepository.createMedia({ //@ts-ignore entityId: entity.id, modelMetadataId: mediaFieldMetadata.model.id, relativeUri: fileName, + fileSize, mediaStorageProviderMetadataId: mediaFieldMetadata.mediaStorageProvider.id, fieldMetadataId: mediaFieldMetadata.id }) as unknown as Media; From fe247510f0c181024fe8bb4e82c2822c977b9adf Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Wed, 10 Jun 2026 16:52:45 +0530 Subject: [PATCH 110/136] Update solid-core-metadata.json to disable create, edit, and delete actions, and set searchable attributes to false for specific fields for media storage provider --- .../seed-data/solid-core-metadata.json | 37 +++++-------------- 1 file changed, 10 insertions(+), 27 deletions(-) diff --git a/src/seeders/seed-data/solid-core-metadata.json b/src/seeders/seed-data/solid-core-metadata.json index c110a6b5..b50b18fc 100755 --- a/src/seeders/seed-data/solid-core-metadata.json +++ b/src/seeders/seed-data/solid-core-metadata.json @@ -7323,9 +7323,11 @@ 50 ], "enableGlobalSearch": true, - "create": true, - "edit": true, - "delete": true + "create": false, + "edit": false, + "delete": false, + "import": false, + "export": false }, "children": [ { @@ -7339,35 +7341,14 @@ "type": "field", "attrs": { "name": "type", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "region", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "bucketName", - "isSearchable": true + "isSearchable": false } }, { "type": "field", "attrs": { "name": "isPublic", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "localPath", - "isSearchable": true + "isSearchable": false } }, { @@ -7392,7 +7373,9 @@ "attrs": { "name": "form-1", "label": "Solid Media Storage Provider Metadata Model", - "className": "grid" + "className": "grid", + "showAddFormButton": false, + "showEditFormButton": false }, "children": [ { From d89f0f59a6d7aa4bd12c530335805a554cf8cd07 Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Wed, 10 Jun 2026 12:37:00 +0100 Subject: [PATCH 111/136] Refactor menu cleanup logic in ModelMetadataService and ModuleMetadataService; improve error handling for file deletion and update build commands in ModulePackageService to use solidctl. --- src/services/model-metadata.service.ts | 31 +++++++++++---- src/services/module-metadata.service.ts | 20 ++++++++-- src/services/module-package.service.ts | 51 ++++++++++--------------- 3 files changed, 60 insertions(+), 42 deletions(-) diff --git a/src/services/model-metadata.service.ts b/src/services/model-metadata.service.ts index bec4c359..90d8379e 100755 --- a/src/services/model-metadata.service.ts +++ b/src/services/model-metadata.service.ts @@ -616,6 +616,10 @@ export class ModelMetadataService { await fs.unlink(fileToDelete); this.logger.log(`Deleted file: ${fileToDelete}`); } catch (error: any) { + if (error?.code === 'ENOENT') { + this.logger.warn(`File already absent, skipping delete: ${fileToDelete}`); + continue; + } this.logger.error(`Error deleting file: ${fileToDelete}`, error); } } @@ -758,8 +762,8 @@ export class ModelMetadataService { const removedMenuNames = menus.map((menu) => menu.name).filter(Boolean); if (menus.length > 0) { - const orderedMenus = [...menus].reverse(); - for (const menu of orderedMenus) { + const menuIds = menus.map((menu) => menu.id).filter(Boolean); + for (const menu of menus) { if (menu.roles?.length) { await this.dataSource .createQueryBuilder() @@ -769,9 +773,22 @@ export class ModelMetadataService { } } - for (const menu of orderedMenus) { - await menuRepo.remove(menu); + if (menuIds.length > 0) { + await menuRepo + .createQueryBuilder() + .update(MenuItemMetadata) + .set({ parentMenuItem: null as any }) + .where('id IN (:...menuIds)', { menuIds }) + .execute(); + + await menuRepo + .createQueryBuilder() + .delete() + .from(MenuItemMetadata) + .where('id IN (:...menuIds)', { menuIds }) + .execute(); } + this.logger.log(`Deleted ${menus.length} menu metadata record(s) for model id ${modelId}`); } @@ -1426,11 +1443,11 @@ export class ModelMetadataService { const removeOutput = await this.executeRemoveFieldsCommand(model, fieldsForRemoval, options.dryRun); // Remove the fields from the database as well. This also checks, if the field is marked for removal - fieldsForRemoval.forEach((field: FieldMetadata) => { + for (const field of fieldsForRemoval) { if (field.isMarkedForRemoval) { - this.fieldMetadataService.delete(field.id); + await this.fieldMetadataService.delete(field.id); } - }); + } // Remove the fields from metadata json file diff --git a/src/services/module-metadata.service.ts b/src/services/module-metadata.service.ts index d9b4d4b4..fb2c0485 100755 --- a/src/services/module-metadata.service.ts +++ b/src/services/module-metadata.service.ts @@ -395,8 +395,8 @@ export class ModuleMetadataService { const menus = await this.findMenusForModuleCleanup(moduleId, actionIds); if (menus.length > 0) { - const orderedMenus = [...menus].reverse(); - for (const menu of orderedMenus) { + const menuIds = menus.map((menu) => menu.id).filter(Boolean); + for (const menu of menus) { if (menu.roles?.length) { await this.dataSource .createQueryBuilder() @@ -406,8 +406,20 @@ export class ModuleMetadataService { } } - for (const menu of orderedMenus) { - await menuRepo.remove(menu); + if (menuIds.length > 0) { + await menuRepo + .createQueryBuilder() + .update(MenuItemMetadata) + .set({ parentMenuItem: null as any }) + .where('id IN (:...menuIds)', { menuIds }) + .execute(); + + await menuRepo + .createQueryBuilder() + .delete() + .from(MenuItemMetadata) + .where('id IN (:...menuIds)', { menuIds }) + .execute(); } this.logger.log(`Deleted ${menus.length} menu metadata record(s) for module id ${moduleId}`); diff --git a/src/services/module-package.service.ts b/src/services/module-package.service.ts index c7bfecaa..798677c4 100644 --- a/src/services/module-package.service.ts +++ b/src/services/module-package.service.ts @@ -11,7 +11,6 @@ import { RunModulePackageBuildDto } from 'src/dtos/run-module-package-build.dto' import { RunModulePackageSeedDto } from 'src/dtos/run-module-package-seed.dto'; import { CommandService } from 'src/helpers/command.service'; import { ModuleMetadataHelperService } from 'src/helpers/module-metadata-helper.service'; -import { SolidRegistry } from 'src/helpers/solid-registry'; import * as fs from 'fs/promises'; import * as path from 'path'; import { v4 as uuidv4 } from 'uuid'; @@ -135,7 +134,6 @@ export class ModulePackageService { constructor( private readonly commandService: CommandService, - private readonly solidRegistry: SolidRegistry, private readonly moduleMetadataHelperService: ModuleMetadataHelperService, ) { } @@ -353,32 +351,33 @@ export class ModulePackageService { const buildSolidUi = dto.buildSolidUi ?? true; const outputs: string[] = []; const failedTargets: string[] = []; + const projectRoot = this.getProjectRoot(); if (buildSolidApi) { try { const output = await this.commandService.executeCommandWithArgs({ - command: 'npm', - args: ['run', 'build'], - cwd: this.getSolidApiRoot(), + command: 'npx', + args: ['-y', '@solidxai/solidctl@latest', 'build'], + cwd: projectRoot, }); - outputs.push(`solid-api build [success]\n${output}`.trim()); + outputs.push(`solidctl build [success]\n${output}`.trim()); } catch (error: any) { - failedTargets.push('solid-api'); - outputs.push(`solid-api build [failed]\n${error?.message ?? 'Build failed.'}`.trim()); + failedTargets.push('solidctl build'); + outputs.push(`solidctl build [failed]\n${error?.message ?? 'Build failed.'}`.trim()); } } if (buildSolidUi) { try { const output = await this.commandService.executeCommandWithArgs({ - command: 'npm', - args: ['run', 'build'], - cwd: this.getSolidUiRoot(), + command: 'npx', + args: ['-y', '@solidxai/solidctl@latest', 'build', '--ui-only'], + cwd: projectRoot, }); - outputs.push(`solid-ui build [success]\n${output}`.trim()); + outputs.push(`solidctl build --ui-only [success]\n${output}`.trim()); } catch (error: any) { - failedTargets.push('solid-ui'); - outputs.push(`solid-ui build [failed]\n${error?.message ?? 'Build failed.'}`.trim()); + failedTargets.push('solidctl build --ui-only'); + outputs.push(`solidctl build --ui-only [failed]\n${error?.message ?? 'Build failed.'}`.trim()); } } @@ -420,27 +419,17 @@ export class ModulePackageService { await this.writeStatusFile(transactionKey, transaction); try { - const seeder = this.solidRegistry - .getSeeders() - .filter((registeredSeeder) => registeredSeeder.name === 'ModuleMetadataSeederService') - .map((registeredSeeder) => registeredSeeder.instance) - .pop(); - - if (!seeder || typeof seeder.seed !== 'function') { - throw new NotFoundException('ModuleMetadataSeederService not found. Cannot seed imported module.'); - } - - await seeder.seed({ - modulesToSeed: [moduleName], - seedGlobalMetadata: dto.seedGlobalMetadata ?? false, + const output = await this.commandService.executeCommandWithArgs({ + command: 'npx', + args: ['-y', '@solidxai/solidctl@latest', 'seed', '--modules-to-seed', moduleName], + cwd: this.getProjectRoot(), }); - transaction.status = ModulePackageStatus.completed; transaction.currentStep = 'done'; transaction.outputs.seed = [ - `Seeded module metadata for ${moduleName}.`, - `seedGlobalMetadata=${dto.seedGlobalMetadata ?? false}`, - ].join('\n'); + `solidctl seed --modules-to-seed ${moduleName} [success]`, + output, + ].filter(Boolean).join('\n'); await this.writeStatusFile(transactionKey, transaction); await this.syncActiveTransactionPointer(transaction); From 6b80d81632443c0d62908c5f50b77617befef8d2 Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Wed, 10 Jun 2026 13:36:57 +0100 Subject: [PATCH 112/136] Add archiver dependency and implement module package export functionality --- package.json | 1 + .../seed-data/solid-core-metadata.json | 17 +++- src/services/module-package.service.ts | 83 ++++++++++++++----- 3 files changed, 80 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index d71daf96..faa56fd3 100755 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "@nestjs/throttler": "^6.4.0", "@types/passport-apple": "^2.0.3", "amqplib": "^0.10.4", + "archiver": "^5.3.2", "axios": "^1.7.0", "bcrypt": "^5.1.1", "bson": "^6.10.1", diff --git a/src/seeders/seed-data/solid-core-metadata.json b/src/seeders/seed-data/solid-core-metadata.json index b50b18fc..2cbd0306 100755 --- a/src/seeders/seed-data/solid-core-metadata.json +++ b/src/seeders/seed-data/solid-core-metadata.json @@ -6686,6 +6686,21 @@ "actionInContextMenu": true, "customComponentIsSystem": true } + }, + { + "attrs": { + "className": "p-button-text", + "icon": "pi pi-download", + "label": "Export Module", + "action": "ExportModulePackageRowAction", + "openInPopup": true, + "actionInContextMenu": true, + "customComponentIsSystem": true, + "env": [ + "dev" + ], + "popupWidth": "32vw" + } } ], "import": false, @@ -14381,4 +14396,4 @@ } } ] -} \ No newline at end of file +} diff --git a/src/services/module-package.service.ts b/src/services/module-package.service.ts index 798677c4..3f738e69 100644 --- a/src/services/module-package.service.ts +++ b/src/services/module-package.service.ts @@ -5,6 +5,8 @@ import { NotFoundException, } from '@nestjs/common'; import { createHash } from 'crypto'; +import archiver from 'archiver'; +import { createWriteStream } from 'fs'; import { DisallowInProduction } from 'src/decorators/disallow-in-production.decorator'; import { ConfirmModulePackageImportDto } from 'src/dtos/confirm-module-package-import.dto'; import { RunModulePackageBuildDto } from 'src/dtos/run-module-package-build.dto'; @@ -212,7 +214,6 @@ export class ModulePackageService { throw new BadRequestException('A module name is required to export a module package.'); } - const expectedPaths = this.buildExpectedPaths(moduleName); const solidApiModulePath = this.getSolidApiModuleTargetPath(moduleName); const solidUiModulePath = this.getSolidUiModuleTargetPath(moduleName); const metadataFilePath = await this.moduleMetadataHelperService.getModuleMetadataFilePath(moduleName); @@ -238,23 +239,19 @@ export class ModulePackageService { await fs.cp(solidApiModulePath, path.join(stagingDir, 'solid-api', 'src', moduleName), { recursive: true }); await fs.cp(solidUiModulePath, path.join(stagingDir, 'solid-ui', 'src', moduleName), { recursive: true }); - const manifest = await this.buildExportManifest(moduleName, metadataDocument); + const manifest = await this.buildExportManifest(moduleName, metadataDocument, stagingDir); await fs.writeFile( path.join(stagingDir, 'manifest.json'), JSON.stringify(manifest, null, 2), 'utf-8', ); - await this.commandService.executeCommandWithArgs({ - command: 'zip', - args: ['-rq', archiveFilePath, '.'], - cwd: stagingDir, - }); + await this.createArchiveFromDirectory(stagingDir, archiveFilePath); return { fileName: archiveFileName, filePath: archiveFilePath, - mimeType: 'application/octet-stream', + mimeType: 'application/zip', }; } @@ -1024,19 +1021,9 @@ export class ModulePackageService { }; } - private async buildExportManifest(moduleName: string, metadataDocument: any): Promise { + private async buildExportManifest(moduleName: string, metadataDocument: any, stagingDir: string): Promise { const expectedPaths = this.buildExpectedPaths(moduleName); - const stagingPaths = [ - expectedPaths.metadataPath, - expectedPaths.apiModulePath, - expectedPaths.uiModulePath, - ]; - const checksums: Record = {}; - - for (const relativePath of stagingPaths) { - const absolutePath = path.join(this.getSolidApiRoot(), '..', relativePath); - checksums[relativePath] = await this.computeSha256(absolutePath); - } + const checksums = await this.computeDirectoryChecksums(stagingDir, ['manifest.json']); return { schemaVersion: ModulePackageService.SUPPORTED_SCHEMA_VERSION, @@ -1058,6 +1045,62 @@ export class ModulePackageService { }; } + private async computeDirectoryChecksums(rootDir: string, excludedRelativePaths: string[] = []): Promise> { + const excludedPaths = new Set(excludedRelativePaths.map((entry) => entry.replace(/\\/g, '/'))); + const filePaths = await this.collectFilePathsRecursively(rootDir); + const checksums: Record = {}; + + for (const filePath of filePaths) { + const relativePath = path.relative(rootDir, filePath).replace(/\\/g, '/'); + if (excludedPaths.has(relativePath)) { + continue; + } + + checksums[relativePath] = await this.computeSha256(filePath); + } + + return checksums; + } + + private async collectFilePathsRecursively(rootDir: string): Promise { + const entries = await fs.readdir(rootDir, { withFileTypes: true }); + const nestedPaths = await Promise.all(entries.map(async (entry) => { + const entryPath = path.join(rootDir, entry.name); + if (entry.isDirectory()) { + return this.collectFilePathsRecursively(entryPath); + } + + return [entryPath]; + })); + + return nestedPaths.flat(); + } + + private async createArchiveFromDirectory(sourceDir: string, archiveFilePath: string): Promise { + await fs.mkdir(path.dirname(archiveFilePath), { recursive: true }); + + await new Promise((resolve, reject) => { + const output = createWriteStream(archiveFilePath); + const archive = archiver('zip', { + zlib: { level: 9 }, + }); + + const rejectOnce = (error: Error) => { + output.destroy(); + reject(error); + }; + + output.on('close', () => resolve()); + output.on('error', rejectOnce); + archive.on('warning', rejectOnce); + archive.on('error', rejectOnce); + + archive.pipe(output); + archive.directory(sourceDir, false); + void archive.finalize(); + }); + } + private async computeSha256(filePath: string): Promise { const fileBuffer = await fs.readFile(filePath); return createHash('sha256').update(fileBuffer).digest('hex'); From 3045dfb21c9b3017f55856c8942ead81fdae164c Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Wed, 10 Jun 2026 17:29:46 +0100 Subject: [PATCH 113/136] Add clearPackageRuntime functionality and corresponding metadata action --- src/controllers/module-package.controller.ts | 6 ++ .../seed-data/solid-core-metadata.json | 16 +++++ src/services/module-package.service.ts | 63 +++++++++++++++++++ 3 files changed, 85 insertions(+) diff --git a/src/controllers/module-package.controller.ts b/src/controllers/module-package.controller.ts index a7b2095f..e90dea43 100644 --- a/src/controllers/module-package.controller.ts +++ b/src/controllers/module-package.controller.ts @@ -51,6 +51,12 @@ export class ModulePackageController { return this.modulePackageService.getLatestResumableImport(); } + @ApiBearerAuth('jwt') + @Post('runtime/clear') + async clearPackageRuntime() { + return this.modulePackageService.clearPackageRuntime(); + } + @ApiBearerAuth('jwt') @Post('import/:id/confirm') async confirmImport( diff --git a/src/seeders/seed-data/solid-core-metadata.json b/src/seeders/seed-data/solid-core-metadata.json index 2cbd0306..92391b0e 100755 --- a/src/seeders/seed-data/solid-core-metadata.json +++ b/src/seeders/seed-data/solid-core-metadata.json @@ -6662,6 +6662,22 @@ "popupWidth": "min(1180px, calc(100vw - 32px))", "customComponentIsSystem": true } + }, + { + "attrs": { + "label": "Clear Package Runtime", + "action": "ClearModulePackageRuntimeHeaderAction", + "actionInContextMenu": true, + "env": [ + "dev" + ], + "openInPopup": true, + "icon": "pi pi-trash", + "closable": true, + "popupWidth": "min(820px, calc(100vw - 32px))", + "buttonClassName": "solid-header-dropdown-item-danger", + "customComponentIsSystem": true + } } ], "rowButtons": [ diff --git a/src/services/module-package.service.ts b/src/services/module-package.service.ts index 3f738e69..2faf14cf 100644 --- a/src/services/module-package.service.ts +++ b/src/services/module-package.service.ts @@ -108,6 +108,16 @@ type ModulePackageExportFile = { mimeType: string; }; +type ModulePackageRuntimeCleanupResult = { + runtimeRoot: string; + removedImportTransactions: number; + removedExportTransactions: number; + removedImportLooseEntries: number; + removedExportLooseEntries: number; + clearedActiveTransactionPointer: boolean; + clearedAt: string; +}; + type ModulePackageActiveTransactionFile = { transactionKey: string; updatedAt: string; @@ -207,6 +217,24 @@ export class ModulePackageService { return this.toStatusResponse(transaction); } + @DisallowInProduction() + async clearPackageRuntime(): Promise { + const runtimeRoot = this.getModulePackageRuntimeRoot(); + const importsRoot = this.getModulePackageImportsRoot(); + const exportsRoot = this.getModulePackageExportsRoot(); + const snapshot = await this.collectRuntimeCleanupSnapshot(); + + await fs.rm(runtimeRoot, { recursive: true, force: true }); + await fs.mkdir(importsRoot, { recursive: true }); + await fs.mkdir(exportsRoot, { recursive: true }); + + return { + runtimeRoot, + ...snapshot, + clearedAt: new Date().toISOString(), + }; + } + @DisallowInProduction() async exportModulePackage(moduleNameInput: string): Promise { const moduleName = (moduleNameInput ?? '').trim(); @@ -824,6 +852,14 @@ export class ModulePackageService { } } + private async readDirSafe(targetPath: string) { + try { + return await fs.readdir(targetPath, { withFileTypes: true }); + } catch (error) { + return []; + } + } + private assertSafeTransactionKey(transactionKey: string) { if (!/^[a-zA-Z0-9-]+$/.test(transactionKey)) { throw new BadRequestException('Invalid module package transaction key.'); @@ -882,6 +918,33 @@ export class ModulePackageService { await this.clearActiveTransactionIfMatches(transaction.transactionKey); } + private async collectRuntimeCleanupSnapshot() { + const importEntries = await this.readDirSafe(this.getModulePackageImportsRoot()); + const exportEntries = await this.readDirSafe(this.getModulePackageExportsRoot()); + + const importTransactionEntries = importEntries.filter( + (entry) => entry.isDirectory() && /^[a-zA-Z0-9-]+$/.test(entry.name), + ); + const exportTransactionEntries = exportEntries.filter( + (entry) => entry.isDirectory() && /^[a-zA-Z0-9-]+$/.test(entry.name), + ); + + const importLooseEntries = importEntries.filter( + (entry) => !(entry.isDirectory() && /^[a-zA-Z0-9-]+$/.test(entry.name)), + ); + const exportLooseEntries = exportEntries.filter( + (entry) => !(entry.isDirectory() && /^[a-zA-Z0-9-]+$/.test(entry.name)), + ); + + return { + removedImportTransactions: importTransactionEntries.length, + removedExportTransactions: exportTransactionEntries.length, + removedImportLooseEntries: importLooseEntries.length, + removedExportLooseEntries: exportLooseEntries.length, + clearedActiveTransactionPointer: importEntries.some((entry) => entry.name === 'active-transaction.json'), + }; + } + private getCompletedTransactionRetentionMs() { const retentionDays = Number(process.env.SOLIDX_MODULE_PACKAGE_RETENTION_DAYS ?? 7); const safeDays = Number.isFinite(retentionDays) && retentionDays > 0 ? retentionDays : 7; From a6cfee6bc7bda460652bff781b3ad74b045a4d78 Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Thu, 11 Jun 2026 09:29:20 +0530 Subject: [PATCH 114/136] changes for IAM --- .../seed-data/solid-core-metadata.json | 272 +++++++++++++----- 1 file changed, 203 insertions(+), 69 deletions(-) diff --git a/src/seeders/seed-data/solid-core-metadata.json b/src/seeders/seed-data/solid-core-metadata.json index 92391b0e..b24e0d94 100755 --- a/src/seeders/seed-data/solid-core-metadata.json +++ b/src/seeders/seed-data/solid-core-metadata.json @@ -1028,7 +1028,7 @@ }, { "name": "signedUrlExpiry", - "displayName": "Signed Url Expiry Time", + "displayName": "Signed Url Expiry Time (In Minutes)", "type": "int", "ormType": "integer", "required": false, @@ -6275,6 +6275,22 @@ "viewUserKey": "media-tree-view", "moduleUserKey": "solid-core", "modelUserKey": "media" + }, + { + "name": "roleMetadata-tree-action", + "displayName": "Roles Tree", + "type": "solid", + "moduleUserKey": "solid-core", + "modelUserKey": "roleMetadata", + "viewUserKey": "roleMetadata-tree-view" + }, + { + "name": "securityRule-tree-action", + "displayName": "Security Rules Tree", + "type": "solid", + "moduleUserKey": "solid-core", + "modelUserKey": "securityRule", + "viewUserKey": "securityRule-tree-view" } ], "menus": [ @@ -9660,14 +9676,17 @@ "enableGlobalSearch": true, "create": false, "edit": false, - "delete": false + "delete": false, + "import": false, + "export": false }, "children": [ { "type": "field", "attrs": { "name": "name", - "isSearchable": true + "isSearchable": true, + "sortable": true } } ] @@ -9734,16 +9753,23 @@ 50 ], "enableGlobalSearch": true, - "create": true, - "edit": true, - "delete": true + "create": false, + "edit": false, + "delete": false, + "import": false, + "export": false, + "allowedViews": [ + "list", + "tree" + ] }, "children": [ { "type": "field", "attrs": { "name": "name", - "isSearchable": true + "isSearchable": true, + "sortable": true } } ] @@ -9761,7 +9787,9 @@ "attrs": { "name": "form-1", "label": "User", - "className": "grid" + "className": "grid", + "showAddFormButton": false, + "showEditFormButton": false }, "children": [ { @@ -11148,27 +11176,23 @@ 50 ], "enableGlobalSearch": true, - "create": true, - "edit": true, - "delete": true + "create": false, + "edit": false, + "delete": false, + "import": false, + "export": false, + "allowedViews": [ + "list", + "tree" + ] }, "children": [ - { - "type": "field", - "attrs": { - "name": "id", - "label": "Id", - "sortable": true, - "filterable": true - } - }, { "type": "field", "attrs": { "name": "name", "label": "Name", "sortable": true, - "filterable": true, "isSearchable": true } }, @@ -11178,9 +11202,26 @@ "name": "description", "label": "Description", "sortable": true, - "filterable": true, "isSearchable": true } + }, + { + "type": "field", + "attrs": { + "name": "role", + "label": "Role", + "isSearchable": true, + "searchField": "role.name" + } + }, + { + "type": "field", + "attrs": { + "name": "modelMetadata", + "label": "Model", + "isSearchable": true, + "searchField": "modelMetadata.singularName" + } } ] } @@ -11197,7 +11238,9 @@ "attrs": { "name": "form-1", "label": "Security rules", - "className": "grid" + "className": "grid", + "showAddFormButton": false, + "showEditFormButton": false }, "children": [ { @@ -11239,17 +11282,7 @@ "attrs": { "name": "securityRuleConfig" } - } - ] - }, - { - "type": "column", - "attrs": { - "name": "group-2", - "label": "", - "className": "col-12 sm:col-12 md:col-6 lg:col-6" - }, - "children": [ + }, { "type": "field", "attrs": { @@ -11653,49 +11686,53 @@ 50 ], "enableGlobalSearch": true, - "create": true, - "edit": true, - "delete": true + "create": false, + "edit": false, + "delete": false, + "import": false, + "export": false, + "allowedViews": [ + "list", + "tree" + ] }, "children": [ { "type": "field", "attrs": { - "name": "id", + "name": "event", + "label": "Event", "sortable": true, - "filterable": true + "isSearchable": true } }, { "type": "field", "attrs": { "name": "user", - "sortable": true, - "filterable": true - } - }, - { - "type": "field", - "attrs": { - "name": "event", - "sortable": true, - "filterable": true + "label": "User", + "sortable": false, + "isSearchable": true, + "searchField": "user.fullName", + "coModelFieldToDisplay": "fullName" } }, { "type": "field", "attrs": { "name": "ipAddress", + "label": "IP Address", "sortable": true, - "filterable": true + "isSearchable": true } }, { "type": "field", "attrs": { "name": "createdAt", + "label": "Created At", "sortable": true, - "filterable": true + "isSearchable": false } } ] @@ -11718,49 +11755,47 @@ 50 ], "enableGlobalSearch": true, - "create": true, - "edit": true, - "delete": true + "create": false, + "edit": false, + "delete": false, + "import": false, + "export": false }, "children": [ { "type": "field", "attrs": { - "name": "id", + "name": "event", + "label": "Event", "sortable": true, - "filterable": true + "isSearchable": true } }, { "type": "field", "attrs": { "name": "user", - "sortable": true, - "filterable": true - } - }, - { - "type": "field", - "attrs": { - "name": "event", - "sortable": true, - "filterable": true + "label": "User", + "isSearchable": true, + "searchField": "user.fullName" } }, { "type": "field", "attrs": { "name": "ipAddress", + "label": "IP Address", "sortable": true, - "filterable": true + "isSearchable": true } }, { "type": "field", "attrs": { "name": "createdAt", + "label": "Created At", "sortable": true, - "filterable": true + "isSearchable": false } } ] @@ -11778,7 +11813,9 @@ "attrs": { "name": "form-1", "label": "User Activity History", - "className": "grid" + "className": "grid", + "showAddFormButton": false, + "showEditFormButton": false }, "children": [ { @@ -13938,6 +13975,103 @@ } ] } + }, + { + "name": "roleMetadata-tree-view", + "displayName": "Roles", + "type": "tree", + "context": "{}", + "moduleUserKey": "solid-core", + "modelUserKey": "roleMetadata", + "layout": { + "type": "list", + "attrs": { + "pagination": true, + "pageSizeOptions": [ + 10, + 25, + 50 + ], + "enableGlobalSearch": true, + "create": false, + "edit": false, + "delete": false, + "import": false, + "export": false + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "name", + "isSearchable": true, + "sortable": true + } + } + ] + } + }, + { + "name": "securityRule-tree-view", + "displayName": "Security Rules", + "type": "tree", + "context": "{}", + "moduleUserKey": "solid-core", + "modelUserKey": "securityRule", + "layout": { + "type": "list", + "attrs": { + "pagination": true, + "pageSizeOptions": [ + 10, + 25, + 50 + ], + "enableGlobalSearch": true, + "create": false, + "edit": false, + "delete": false, + "import": false, + "export": false + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "name", + "label": "Name", + "sortable": true, + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "description", + "label": "Description", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "role", + "label": "Role", + "isSearchable": true, + "searchField": "role.name" + } + }, + { + "type": "field", + "attrs": { + "name": "modelMetadata", + "label": "Model", + "isSearchable": true, + "searchField": "modelMetadata.name" + } + } + ] + } } ], "emailTemplates": [ From a34bae10c983d99e1b13e9b849d654efc46c0fff Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Thu, 11 Jun 2026 11:06:21 +0530 Subject: [PATCH 115/136] Add model sequence and list of values tree views to metadata, other cleanup changes --- .../seed-data/solid-core-metadata.json | 317 +++++++++++++++--- 1 file changed, 274 insertions(+), 43 deletions(-) diff --git a/src/seeders/seed-data/solid-core-metadata.json b/src/seeders/seed-data/solid-core-metadata.json index b24e0d94..288e378b 100755 --- a/src/seeders/seed-data/solid-core-metadata.json +++ b/src/seeders/seed-data/solid-core-metadata.json @@ -6291,6 +6291,22 @@ "moduleUserKey": "solid-core", "modelUserKey": "securityRule", "viewUserKey": "securityRule-tree-view" + }, + { + "name": "modelSequence-tree-action", + "displayName": "Model Sequences Tree", + "type": "solid", + "moduleUserKey": "solid-core", + "modelUserKey": "modelSequence", + "viewUserKey": "modelSequence-tree-view" + }, + { + "name": "listOfValues-tree-action", + "displayName": "List Of Values Tree", + "type": "solid", + "moduleUserKey": "solid-core", + "modelUserKey": "listOfValues", + "viewUserKey": "listOfValues-tree-view" } ], "menus": [ @@ -7214,7 +7230,9 @@ "attrs": { "name": "form-1", "label": "Solid List of Values Model", - "className": "grid" + "className": "grid", + "showAddFormButton": false, + "showEditFormButton": false }, "children": [ { @@ -7297,57 +7315,72 @@ 50 ], "enableGlobalSearch": true, - "create": true, - "edit": true, - "delete": true + "create": false, + "edit": false, + "delete": false, + "import": false, + "export": false, + "allowedViews": [ + "list", + "tree" + ] }, "children": [ { "type": "field", "attrs": { - "name": "type", - "isSearchable": true + "name": "module", + "label": "Module", + "isSearchable": true, + "searchField": "module.name" } }, { "type": "field", "attrs": { - "name": "value", + "name": "type", + "label": "Type", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "display", + "name": "value", + "label": "Value", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "description", + "name": "display", + "label": "Display Value", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "default", - "isSearchable": true + "name": "description", + "label": "Description", + "isSearchable": false } }, { "type": "field", "attrs": { "name": "sequence", - "isSearchable": true + "label": "Sequence", + "isSearchable": false } }, { "type": "field", "attrs": { - "name": "module" + "name": "default", + "label": "Default", + "isSearchable": false } } ] @@ -11321,18 +11354,36 @@ 50 ], "enableGlobalSearch": true, - "create": true, - "edit": true, - "delete": false + "create": false, + "edit": false, + "delete": false, + "import": false, + "export": false }, "children": [ { "type": "field", "attrs": { "name": "name", - "label": "Email", + "label": "Name", "sortable": true, - "filterable": true + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "displayName", + "label": "Display Name", + "sortable": true, + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "active", + "isSearchable": false } } ] @@ -11352,7 +11403,9 @@ "label": "Email", "className": "grid", "disabled": false, - "readonly": false + "readonly": false, + "showAddFormButton": false, + "showEditFormButton": false }, "onFieldChange": "emailFormTypeChangeHandler", "onFormLayoutLoad": "emailFormTypeLoad", @@ -11454,9 +11507,11 @@ 50 ], "enableGlobalSearch": true, - "create": true, - "edit": true, - "delete": false + "create": false, + "edit": false, + "delete": false, + "import": false, + "export": false }, "children": [ { @@ -11465,7 +11520,32 @@ "name": "name", "label": "Name", "sortable": true, - "filterable": true + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "displayName", + "label": "Display Name", + "sortable": true, + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "active", + "isSearchable": false + } + }, + { + "type": "field", + "attrs": { + "name": "smsProviderTemplateId", + "label": "Template Provider Id", + "sortable": true, + "isSearchable": true } } ] @@ -11485,7 +11565,9 @@ "label": "Email", "className": "grid", "disabled": false, - "readonly": false + "readonly": false, + "showAddFormButton": false, + "showEditFormButton": false }, "onFieldChange": "emailFormTypeChangeHandler", "children": [ @@ -12310,63 +12392,81 @@ 50 ], "enableGlobalSearch": true, - "create": true, - "edit": true, - "delete": true + "create": false, + "edit": false, + "delete": false, + "import": false, + "export": false, + "allowedViews": [ + "list", + "tree" + ] }, "children": [ { "type": "field", "attrs": { - "name": "id" - } - }, - { - "type": "field", - "attrs": { - "name": "sequenceName" + "name": "module", + "label": "Module", + "isSearchable": true, + "searchField": "module.name" } }, { "type": "field", "attrs": { - "name": "currentValue" + "name": "model", + "label": "Model", + "isSearchable": true, + "searchField": "model.name" } }, { "type": "field", "attrs": { - "name": "prefix" + "name": "field", + "label": "Field", + "isSearchable": false } }, { "type": "field", "attrs": { - "name": "padding" + "name": "sequenceName", + "label": "Sequence", + "isSearchable": false } }, { "type": "field", "attrs": { - "name": "separator" + "name": "prefix", + "label": "Prefix", + "isSearchable": false } }, { "type": "field", "attrs": { - "name": "field" + "name": "separator", + "label": "Separator", + "isSearchable": false } }, { "type": "field", "attrs": { - "name": "model" + "name": "padding", + "label": "Padding", + "isSearchable": false } }, { "type": "field", "attrs": { - "name": "module" + "name": "currentValue", + "label": "Current Value", + "isSearchable": false } } ] @@ -12384,7 +12484,9 @@ "attrs": { "name": "form-1", "label": "Model Sequence", - "className": "grid" + "className": "grid", + "showAddFormButton": false, + "showEditFormButton": false }, "onFieldChange": "modelSequenceFormViewChangeHandler", "children": [ @@ -14072,6 +14174,135 @@ } ] } + }, + { + "name": "modelSequence-tree-view", + "displayName": "Model Sequences", + "type": "tree", + "context": "{}", + "moduleUserKey": "solid-core", + "modelUserKey": "modelSequence", + "layout": { + "type": "list", + "attrs": { + "pagination": true, + "pageSizeOptions": [ + 10, + 25, + 50 + ], + "enableGlobalSearch": true, + "create": false, + "edit": false, + "delete": false, + "import": false, + "export": false + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "module", + "label": "Module", + "isSearchable": true, + "searchField": "module.name" + } + }, + { + "type": "field", + "attrs": { + "name": "model", + "label": "Model", + "isSearchable": true, + "searchField": "model.name" + } + }, + { + "type": "field", + "attrs": { + "name": "field", + "label": "Field", + "isSearchable": false + } + }, + { + "type": "field", + "attrs": { + "name": "sequenceName", + "label": "Sequence", + "isSearchable": false + } + }, + { + "type": "field", + "attrs": { + "name": "currentValue", + "label": "Current Value", + "isSearchable": false + } + } + ] + } + }, + { + "name": "listOfValues-tree-view", + "displayName": "List Of Values", + "type": "tree", + "context": "{}", + "moduleUserKey": "solid-core", + "modelUserKey": "listOfValues", + "layout": { + "type": "list", + "attrs": { + "pagination": true, + "pageSizeOptions": [ + 10, + 25, + 50 + ], + "enableGlobalSearch": true, + "create": false, + "edit": false, + "delete": false, + "import": false, + "export": false + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "module", + "label": "Module", + "isSearchable": true, + "searchField": "module.name" + } + }, + { + "type": "field", + "attrs": { + "name": "type", + "label": "Type", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "value", + "label": "Value", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "display", + "label": "Display Value", + "isSearchable": true + } + } + ] + } } ], "emailTemplates": [ @@ -14546,4 +14777,4 @@ } } ] -} +} \ No newline at end of file From 0f87f7a3f56f36ad254751e376420d1c852f092c Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Thu, 11 Jun 2026 12:15:14 +0530 Subject: [PATCH 116/136] Remove ChatterMessageDetails and ImportTransactionErrorLog menu items; add Chatter Messages tree view and action. Other menu sub menus cleanup done --- .../seed-data/remove-standalone-menus.sql | 10 + .../seed-data/solid-core-metadata.json | 357 ++++++++++++------ 2 files changed, 252 insertions(+), 115 deletions(-) create mode 100644 src/seeders/seed-data/remove-standalone-menus.sql diff --git a/src/seeders/seed-data/remove-standalone-menus.sql b/src/seeders/seed-data/remove-standalone-menus.sql new file mode 100644 index 00000000..f3194216 --- /dev/null +++ b/src/seeders/seed-data/remove-standalone-menus.sql @@ -0,0 +1,10 @@ +-- Delete script: remove ChatterMessageDetails and ImportTransactionErrorLog menu artifacts +-- Run this against your database after deploying the updated metadata. + +-- 1. Chatter Message Details +DELETE FROM ss_menu_item_metadata WHERE name = 'chatterMessageDetails-menu-item'; +DELETE FROM ss_action_metadata WHERE name = 'chatterMessageDetails-list-action'; + +-- 2. Import Transaction Error Logs +DELETE FROM ss_menu_item_metadata WHERE name = 'importTransactionErrorLog-menu-item'; +DELETE FROM ss_action_metadata WHERE name = 'importTransactionErrorLog-list-action'; diff --git a/src/seeders/seed-data/solid-core-metadata.json b/src/seeders/seed-data/solid-core-metadata.json index 288e378b..5cf70f1b 100755 --- a/src/seeders/seed-data/solid-core-metadata.json +++ b/src/seeders/seed-data/solid-core-metadata.json @@ -6307,6 +6307,14 @@ "moduleUserKey": "solid-core", "modelUserKey": "listOfValues", "viewUserKey": "listOfValues-tree-view" + }, + { + "name": "chatterMessage-tree-action", + "displayName": "Chatter Messages Tree", + "type": "solid", + "moduleUserKey": "solid-core", + "modelUserKey": "chatterMessage", + "viewUserKey": "chatterMessage-tree-view" } ], "menus": [ @@ -6565,14 +6573,6 @@ "moduleUserKey": "solid-core", "parentMenuItemUserKey": "other-menu-item" }, - { - "displayName": "Chatter Message Details", - "name": "chatterMessageDetails-menu-item", - "sequenceNumber": 6, - "actionUserKey": "chatterMessageDetails-list-action", - "moduleUserKey": "solid-core", - "parentMenuItemUserKey": "other-menu-item" - }, { "displayName": "Locale", "name": "locale-menu-item", @@ -6589,14 +6589,6 @@ "moduleUserKey": "solid-core", "parentMenuItemUserKey": "other-menu-item" }, - { - "displayName": "Import Error Logs", - "name": "importTransactionErrorLog-menu-item", - "sequenceNumber": 9, - "actionUserKey": "importTransactionErrorLog-list-action", - "moduleUserKey": "solid-core", - "parentMenuItemUserKey": "other-menu-item" - }, { "displayName": "Settings", "name": "settings-menu-item", @@ -7117,30 +7109,55 @@ 50 ], "enableGlobalSearch": true, - "create": true, - "edit": true, - "delete": true + "create": false, + "edit": false, + "delete": false, + "import": false, + "export": false }, "children": [ + { + "type": "field", + "attrs": { + "name": "model", + "label": "Model", + "isSearchable": true, + "searchField": "model.singularName" + } + }, + { + "type": "field", + "attrs": { + "name": "view", + "label": "View", + "isSearchable": true, + "searchField": "view.name" + } + }, { "type": "field", "attrs": { "name": "name", - "isSearchable": true + "label": "Name", + "isSearchable": true, + "sortable": true } }, { "type": "field", "attrs": { - "name": "model", - "isSearchable": true + "name": "isPrivate", + "isSearchable": false } }, { "type": "field", "attrs": { - "name": "view", - "isSearchable": true + "name": "user", + "label": "User", + "isSearchable": true, + "searchField": "user.fullName", + "coModelFieldToDisplay": "fullName" } } ] @@ -7158,7 +7175,9 @@ "attrs": { "name": "form-1", "label": "Solid Saved Filter Model", - "className": "grid" + "className": "grid", + "showAddFormButton": false, + "showEditFormButton": false }, "children": [ { @@ -7681,7 +7700,13 @@ "enableGlobalSearch": true, "create": false, "edit": false, - "delete": false + "delete": false, + "import": false, + "export": false, + "allowedViews": [ + "list", + "tree" + ] }, "children": [ { @@ -7715,7 +7740,9 @@ { "type": "field", "attrs": { - "name": "user" + "name": "user", + "isSearchable": true, + "searchField": "user.fullName" } } ] @@ -10731,98 +10758,54 @@ 50 ], "enableGlobalSearch": true, - "create": true, - "edit": true, - "delete": true + "create": false, + "edit": false, + "delete": false, + "import": false, + "export": false }, "children": [ { "type": "field", "attrs": { - "name": "id" + "name": "module", + "label": "Module", + "isSearchable": true, + "searchField": "module.name" } }, { "type": "field", "attrs": { "name": "scheduleName", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "module", - "isSearchable": true + "label": "Schedule Name", + "isSearchable": true, + "sortable": true } }, { "type": "field", "attrs": { - "name": "isActive" + "name": "job", + "label": "Job Name", + "isSearchable": true, + "sortable": true } }, { "type": "field", "attrs": { "name": "frequency", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "startTime" - } - }, - { - "type": "field", - "attrs": { - "name": "endTime" - } - }, - { - "type": "field", - "attrs": { - "name": "startDate" - } - }, - { - "type": "field", - "attrs": { - "name": "endDate" - } - }, - { - "type": "field", - "attrs": { - "name": "dayOfMonth" - } - }, - { - "type": "field", - "attrs": { - "name": "lastRunAt" - } - }, - { - "type": "field", - "attrs": { - "name": "nextRunAt" - } - }, - { - "type": "field", - "attrs": { - "name": "dayOfWeek" + "label": "Frequency", + "isSearchable": false, + "sortable": true } }, { "type": "field", "attrs": { - "name": "job", - "sortable": true, - "filterable": true + "name": "isActive", + "isSearchable": false } } ] @@ -10840,7 +10823,9 @@ "attrs": { "name": "form-1", "label": "Scheduled Job", - "className": "grid" + "className": "grid", + "showAddFormButton": false, + "showEditFormButton": false }, "onFieldChange": "scheduleFrequencyOnFieldChangeHandler", "children": [ @@ -11668,25 +11653,55 @@ 50 ], "enableGlobalSearch": true, - "create": true, - "edit": true, - "delete": true + "create": false, + "edit": false, + "delete": false, + "import": false, + "export": false }, "children": [ { "type": "field", "attrs": { "name": "id", + "label": "ID", "sortable": true, - "filterable": true + "isSearchable": false } }, { "type": "field", "attrs": { - "name": "importTransactionErrorLog", - "sortable": true, - "filterable": true + "name": "modelMetadata", + "label": "Model", + "isSearchable": true, + "searchField": "modelMetadata.name" + } + }, + { + "type": "field", + "attrs": { + "name": "status", + "label": "Status", + "isSearchable": true, + "sortable": true + } + }, + { + "type": "field", + "attrs": { + "name": "createdAt", + "label": "Created At", + "isSearchable": false, + "sortable": true + } + }, + { + "type": "field", + "attrs": { + "name": "createdBy", + "label": "Created By", + "isSearchable": false } } ] @@ -11704,7 +11719,9 @@ "attrs": { "name": "form-1", "label": "Import Transactions", - "className": "grid" + "className": "grid", + "showAddFormButton": false, + "showEditFormButton": false }, "children": [ { @@ -11714,35 +11731,85 @@ }, "children": [ { - "type": "row", + "type": "notebook", "attrs": { - "name": "sheet-1" + "name": "notebook-1" }, "children": [ { - "type": "column", + "type": "page", "attrs": { - "name": "group-1", - "label": "", - "className": "col-12 sm:col-12 md:col-6 lg:col-6" + "name": "page-1", + "label": "Details" }, "children": [ { - "type": "field", + "type": "group", "attrs": { - "name": "importTransactionErrorLog" - } + "name": "group-1", + "label": "", + "className": "col-12 sm:col-12 md:col-6 lg:col-6" + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "modelMetadata" + } + }, + { + "type": "field", + "attrs": { + "name": "status" + } + }, + { + "type": "field", + "attrs": { + "name": "mapping" + } + }, + { + "type": "field", + "attrs": { + "name": "createdAt" + } + }, + { + "type": "field", + "attrs": { + "name": "createdBy" + } + } + ] } ] }, { - "type": "column", + "type": "page", "attrs": { - "name": "group-2", - "label": "", - "className": "col-12 sm:col-12 md:col-6 lg:col-6" + "name": "page-2", + "label": "Error Logs" }, - "children": [] + "children": [ + { + "type": "group", + "attrs": { + "name": "group-1", + "label": "", + "className": "col-12" + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "importTransactionErrorLog", + "showLabel": false + } + } + ] + } + ] } ] } @@ -14303,6 +14370,66 @@ } ] } + }, + { + "name": "chatterMessage-tree-view", + "displayName": "Chatter Messages", + "type": "tree", + "context": "{}", + "moduleUserKey": "solid-core", + "modelUserKey": "chatterMessage", + "layout": { + "type": "list", + "attrs": { + "pagination": true, + "pageSizeOptions": [ + 10, + 25, + 50 + ], + "enableGlobalSearch": true, + "create": false, + "edit": false, + "delete": false, + "import": false, + "export": false + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "messageType", + "label": "Type", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "messageSubType", + "label": "Sub Type", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "modelDisplayName", + "label": "Model", + "isSearchable": true + } + }, + { + "type": "field", + "attrs": { + "name": "user", + "label": "User", + "isSearchable": true, + "searchField": "user.fullName" + } + } + ] + } } ], "emailTemplates": [ From f7117986408d5e7f4d61b0037472a2bdc8816094 Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Thu, 11 Jun 2026 12:26:11 +0530 Subject: [PATCH 117/136] Refactor metadata: remove standalone menu items and add scheduled jobs and saved filters tree views --- .../seed-data/remove-standalone-menus.sql | 6 +- .../seed-data/solid-core-metadata.json | 370 +++++++++++------- 2 files changed, 238 insertions(+), 138 deletions(-) diff --git a/src/seeders/seed-data/remove-standalone-menus.sql b/src/seeders/seed-data/remove-standalone-menus.sql index f3194216..1bb47d7d 100644 --- a/src/seeders/seed-data/remove-standalone-menus.sql +++ b/src/seeders/seed-data/remove-standalone-menus.sql @@ -1,4 +1,4 @@ --- Delete script: remove ChatterMessageDetails and ImportTransactionErrorLog menu artifacts +-- Delete script: remove standalone menu items, actions, and views -- Run this against your database after deploying the updated metadata. -- 1. Chatter Message Details @@ -8,3 +8,7 @@ DELETE FROM ss_action_metadata WHERE name = 'chatterMessageDetails-list-actio -- 2. Import Transaction Error Logs DELETE FROM ss_menu_item_metadata WHERE name = 'importTransactionErrorLog-menu-item'; DELETE FROM ss_action_metadata WHERE name = 'importTransactionErrorLog-list-action'; + +-- 3. Agent Events (removed from menu; views retained for embedded use) +DELETE FROM ss_menu_item_metadata WHERE name = 'agentEvent-menu-item'; +DELETE FROM ss_action_metadata WHERE name IN ('agentEvent-list-action'); \ No newline at end of file diff --git a/src/seeders/seed-data/solid-core-metadata.json b/src/seeders/seed-data/solid-core-metadata.json index 5cf70f1b..ea830262 100755 --- a/src/seeders/seed-data/solid-core-metadata.json +++ b/src/seeders/seed-data/solid-core-metadata.json @@ -6315,6 +6315,22 @@ "moduleUserKey": "solid-core", "modelUserKey": "chatterMessage", "viewUserKey": "chatterMessage-tree-view" + }, + { + "name": "scheduledJob-tree-action", + "displayName": "Scheduled Jobs Tree", + "type": "solid", + "moduleUserKey": "solid-core", + "modelUserKey": "scheduledJob", + "viewUserKey": "scheduledJob-tree-view" + }, + { + "name": "savedFilters-tree-action", + "displayName": "Saved Filters Tree", + "type": "solid", + "moduleUserKey": "solid-core", + "modelUserKey": "savedFilters", + "viewUserKey": "savedFilters-tree-view" } ], "menus": [ @@ -6351,15 +6367,6 @@ "moduleUserKey": "solid-core", "parentMenuItemUserKey": "appBuilder-menu-item" }, - { - "displayName": "Layout Builder", - "name": "layoutBuilder-menu-item", - "sequenceNumber": 2, - "actionUserKey": "", - "moduleUserKey": "solid-core", - "parentMenuItemUserKey": "", - "iconName": "space_dashboard" - }, { "displayName": "Menu Item", "name": "menuItemMetadata-menu-item", @@ -6606,6 +6613,15 @@ "moduleUserKey": "solid-core", "parentMenuItemUserKey": "other-menu-item" }, + { + "displayName": "Dashboard User Layout", + "name": "dashboardUserLayout-menu-item", + "sequenceNumber": 42, + "actionUserKey": "dashboardUserLayout-list-action", + "moduleUserKey": "solid-core", + "parentMenuItemUserKey": "", + "iconName": "" + }, { "displayName": "Agent", "name": "agent-menu-item", @@ -6623,14 +6639,6 @@ "moduleUserKey": "solid-core", "parentMenuItemUserKey": "agent-menu-item" }, - { - "displayName": "Events", - "name": "agentEvent-menu-item", - "sequenceNumber": 2, - "actionUserKey": "agentEvent-list-action", - "moduleUserKey": "solid-core", - "parentMenuItemUserKey": "agent-menu-item" - }, { "displayName": "MCP Audit Log", "name": "mcpAuditLog-menu-item", @@ -6638,15 +6646,6 @@ "actionUserKey": "mcpAuditLog-list-action", "moduleUserKey": "solid-core", "parentMenuItemUserKey": "agent-menu-item" - }, - { - "displayName": "Dashboard User Layout", - "name": "dashboardUserLayout-menu-item", - "sequenceNumber": 42, - "actionUserKey": "dashboardUserLayout-list-action", - "moduleUserKey": "solid-core", - "parentMenuItemUserKey": "", - "iconName": "" } ], "views": [ @@ -7113,7 +7112,11 @@ "edit": false, "delete": false, "import": false, - "export": false + "export": false, + "allowedViews": [ + "list", + "tree" + ] }, "children": [ { @@ -10762,7 +10765,11 @@ "edit": false, "delete": false, "import": false, - "export": false + "export": false, + "allowedViews": [ + "list", + "tree" + ] }, "children": [ { @@ -12651,9 +12658,11 @@ 50 ], "enableGlobalSearch": true, - "create": true, - "edit": true, - "delete": true + "create": false, + "edit": false, + "delete": false, + "import": false, + "export": false }, "children": [ { @@ -12667,6 +12676,7 @@ "type": "field", "attrs": { "name": "modelName", + "label": "Model", "isSearchable": true } }, @@ -12680,7 +12690,8 @@ { "type": "field", "attrs": { - "name": "totalSteps" + "name": "projectRoot", + "isSearchable": false } }, { @@ -12700,25 +12711,6 @@ "attrs": { "name": "totalOutputTokens" } - }, - { - "type": "field", - "attrs": { - "name": "projectRoot", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "createdAt" - } - }, - { - "type": "field", - "attrs": { - "name": "updatedAt" - } } ] } @@ -12735,7 +12727,9 @@ "attrs": { "name": "form-1", "label": "Agent Session", - "className": "grid" + "className": "grid", + "showAddFormButton": false, + "showEditFormButton": false }, "children": [ { @@ -12838,21 +12832,18 @@ 50 ], "enableGlobalSearch": true, - "create": true, - "edit": true, - "delete": true + "create": false, + "edit": false, + "delete": false, + "import": false, + "export": false }, "children": [ - { - "type": "field", - "attrs": { - "name": "id" - } - }, { "type": "field", "attrs": { "name": "sessionId", + "label": "Session", "isSearchable": true } }, @@ -12863,18 +12854,6 @@ "isSearchable": true } }, - { - "type": "field", - "attrs": { - "name": "turnNumber" - } - }, - { - "type": "field", - "attrs": { - "name": "stepNumber" - } - }, { "type": "field", "attrs": { @@ -12885,7 +12864,9 @@ { "type": "field", "attrs": { - "name": "durationMs" + "name": "modelUsed", + "label": "Model Used", + "isSearchable": true } }, { @@ -12909,14 +12890,7 @@ { "type": "field", "attrs": { - "name": "modelUsed", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "createdAt" + "name": "turnNumber" } } ] @@ -12934,7 +12908,9 @@ "attrs": { "name": "form-1", "label": "Agent Event", - "className": "grid" + "className": "grid", + "showAddFormButton": false, + "showEditFormButton": false }, "children": [ { @@ -13174,9 +13150,11 @@ 50 ], "enableGlobalSearch": true, - "create": true, - "edit": true, - "delete": false + "create": false, + "edit": false, + "delete": false, + "import": false, + "export": false }, "children": [ { @@ -13219,12 +13197,6 @@ "isSearchable": true } }, - { - "type": "field", - "attrs": { - "name": "userId" - } - }, { "type": "field", "attrs": { @@ -13232,13 +13204,6 @@ "isSearchable": true } }, - { - "type": "field", - "attrs": { - "name": "mcpSessionId", - "isSearchable": true - } - }, { "type": "field", "attrs": { @@ -13273,7 +13238,9 @@ "attrs": { "name": "form-1", "label": "MCP Audit Log", - "className": "grid" + "className": "grid", + "showAddFormButton": false, + "showEditFormButton": false }, "children": [ { @@ -13514,49 +13481,40 @@ "enableGlobalSearch": true, "create": false, "edit": false, - "delete": true + "delete": false, + "import": false, + "export": false }, "children": [ { "type": "field", "attrs": { - "name": "id", - "isSearchable": false + "name": "module", + "isSearchable": true, + "searchField": "module.name" } }, { "type": "field", "attrs": { "name": "dashboardName", + "label": "Name", "isSearchable": true } }, { "type": "field", "attrs": { - "name": "module", - "isSearchable": true + "name": "version" } }, { "type": "field", "attrs": { "name": "user", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "version", - "isSearchable": true - } - }, - { - "type": "field", - "attrs": { - "name": "updatedAt", - "isSearchable": false + "isSearchable": true, + "searchField": "user.fullName", + "coModelFieldToDisplay": "fullName" } } ] @@ -13581,31 +13539,25 @@ "enableGlobalSearch": true, "create": false, "edit": false, - "delete": true + "delete": false, + "import": false, + "export": false }, "children": [ { "type": "field", "attrs": { - "name": "id" - } - }, - { - "type": "field", - "attrs": { - "name": "dashboardName" - } - }, - { - "type": "field", - "attrs": { - "name": "module" + "name": "module", + "isSearchable": true, + "searchField": "module.name" } }, { "type": "field", "attrs": { - "name": "user" + "name": "dashboardName", + "label": "Name", + "isSearchable": true } }, { @@ -13617,7 +13569,10 @@ { "type": "field", "attrs": { - "name": "updatedAt" + "name": "user", + "isSearchable": true, + "searchField": "user.fullName", + "coModelFieldToDisplay": "fullName" } } ] @@ -13639,7 +13594,7 @@ "className": "grid", "showAddFormButton": false, "showEditFormButton": false, - "showDeleteFormButton": true + "showDeleteFormButton": false }, "children": [ { @@ -14430,6 +14385,147 @@ } ] } + }, + { + "name": "scheduledJob-tree-view", + "displayName": "Scheduled Jobs", + "type": "tree", + "context": "{}", + "moduleUserKey": "solid-core", + "modelUserKey": "scheduledJob", + "layout": { + "type": "list", + "attrs": { + "pagination": true, + "pageSizeOptions": [ + 10, + 25, + 50 + ], + "enableGlobalSearch": true, + "create": false, + "edit": false, + "delete": false, + "import": false, + "export": false + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "module", + "label": "Module", + "isSearchable": true, + "searchField": "module.name" + } + }, + { + "type": "field", + "attrs": { + "name": "scheduleName", + "label": "Schedule Name", + "isSearchable": true, + "sortable": true + } + }, + { + "type": "field", + "attrs": { + "name": "job", + "label": "Job Name", + "isSearchable": true, + "sortable": true + } + }, + { + "type": "field", + "attrs": { + "name": "frequency", + "label": "Frequency", + "isSearchable": false, + "sortable": true + } + }, + { + "type": "field", + "attrs": { + "name": "isActive", + "isSearchable": false + } + } + ] + } + }, + { + "name": "savedFilters-tree-view", + "displayName": "Saved Filters", + "type": "tree", + "context": "{}", + "moduleUserKey": "solid-core", + "modelUserKey": "savedFilters", + "layout": { + "type": "list", + "attrs": { + "pagination": true, + "pageSizeOptions": [ + 10, + 25, + 50 + ], + "enableGlobalSearch": true, + "create": false, + "edit": false, + "delete": false, + "import": false, + "export": false + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "model", + "label": "Model", + "isSearchable": true, + "searchField": "model.singularName" + } + }, + { + "type": "field", + "attrs": { + "name": "view", + "label": "View", + "isSearchable": true, + "searchField": "view.name" + } + }, + { + "type": "field", + "attrs": { + "name": "name", + "label": "Name", + "isSearchable": true, + "sortable": true + } + }, + { + "type": "field", + "attrs": { + "name": "isPrivate", + "isSearchable": false + } + }, + { + "type": "field", + "attrs": { + "name": "user", + "label": "User", + "isSearchable": true, + "searchField": "user.fullName", + "coModelFieldToDisplay": "fullName" + } + } + ] + } } ], "emailTemplates": [ From e7878a232b8e3445e3a83bd0f2318478b77de718 Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Thu, 11 Jun 2026 12:34:22 +0530 Subject: [PATCH 118/136] pending cleanup of solid core menu items --- src/seeders/seed-data/remove-standalone-menus.sql | 2 +- src/seeders/seed-data/solid-core-metadata.json | 13 ------------- 2 files changed, 1 insertion(+), 14 deletions(-) diff --git a/src/seeders/seed-data/remove-standalone-menus.sql b/src/seeders/seed-data/remove-standalone-menus.sql index 1bb47d7d..7c4f125c 100644 --- a/src/seeders/seed-data/remove-standalone-menus.sql +++ b/src/seeders/seed-data/remove-standalone-menus.sql @@ -5,7 +5,7 @@ DELETE FROM ss_menu_item_metadata WHERE name = 'chatterMessageDetails-menu-item'; DELETE FROM ss_action_metadata WHERE name = 'chatterMessageDetails-list-action'; --- 2. Import Transaction Error Logs +-- 2. Import Transaction Error Logs (views kept — embedded in importTransaction form) DELETE FROM ss_menu_item_metadata WHERE name = 'importTransactionErrorLog-menu-item'; DELETE FROM ss_action_metadata WHERE name = 'importTransactionErrorLog-list-action'; diff --git a/src/seeders/seed-data/solid-core-metadata.json b/src/seeders/seed-data/solid-core-metadata.json index ea830262..b9a203e7 100755 --- a/src/seeders/seed-data/solid-core-metadata.json +++ b/src/seeders/seed-data/solid-core-metadata.json @@ -6055,19 +6055,6 @@ "moduleUserKey": "solid-core", "modelUserKey": "aiInteraction" }, - { - "displayName": "Import Error Logs List Action", - "name": "importTransactionErrorLog-list-action", - "type": "solid", - "domain": "", - "context": "", - "customComponent": "", - "customIsModal": true, - "serverEndpoint": "", - "viewUserKey": "importTransactionErrorLog-list-view", - "moduleUserKey": "solid-core", - "modelUserKey": "importTransactionErrorLog" - }, { "displayName": "Model Sequence List Action", "name": "modelSequence-list-action", From 9f1e1a441c04941cd1980bd899366edb01e7b31b Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Thu, 11 Jun 2026 13:21:09 +0530 Subject: [PATCH 119/136] reverted accidental removal of the layout builder root menu --- src/seeders/seed-data/solid-core-metadata.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/seeders/seed-data/solid-core-metadata.json b/src/seeders/seed-data/solid-core-metadata.json index b9a203e7..b5f6d4b4 100755 --- a/src/seeders/seed-data/solid-core-metadata.json +++ b/src/seeders/seed-data/solid-core-metadata.json @@ -6330,6 +6330,15 @@ "parentMenuItemUserKey": "", "iconName": "app_registration" }, + { + "displayName": "Layout Builder", + "name": "layoutBuilder-menu-item", + "sequenceNumber": 2, + "actionUserKey": "", + "moduleUserKey": "solid-core", + "parentMenuItemUserKey": "", + "iconName": "space_dashboard" + }, { "displayName": "Module", "name": "moduleMetadata-menu-item", From fe005ff2005e83d2dc44991c2523bf197083117d Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Thu, 11 Jun 2026 15:27:50 +0530 Subject: [PATCH 120/136] fixes --- .../seed-data/solid-core-metadata.json | 796 ++++++++++++------ 1 file changed, 517 insertions(+), 279 deletions(-) diff --git a/src/seeders/seed-data/solid-core-metadata.json b/src/seeders/seed-data/solid-core-metadata.json index b5f6d4b4..a0df0225 100755 --- a/src/seeders/seed-data/solid-core-metadata.json +++ b/src/seeders/seed-data/solid-core-metadata.json @@ -7186,48 +7186,84 @@ }, "children": [ { - "type": "group", + "type": "notebook", "attrs": { - "name": "group-1", - "label": "", - "className": "col-12 sm:col-12 md:col-6 lg:col-6" + "name": "notebook-1" }, "children": [ { - "type": "field", - "attrs": { - "name": "name" - } - }, - { - "type": "field", - "attrs": { - "name": "filterQueryJson" - } - }, - { - "type": "field", - "attrs": { - "name": "view" - } - }, - { - "type": "field", - "attrs": { - "name": "model" - } - }, - { - "type": "field", + "type": "page", "attrs": { - "name": "user" - } + "name": "page-general", + "label": "General" + }, + "children": [ + { + "type": "group", + "attrs": { + "name": "group-1", + "label": "", + "className": "col-12 sm:col-12 md:col-6 lg:col-6" + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "name" + } + }, + { + "type": "field", + "attrs": { + "name": "filterQueryJson" + } + }, + { + "type": "field", + "attrs": { + "name": "view" + } + }, + { + "type": "field", + "attrs": { + "name": "model" + } + }, + { + "type": "field", + "attrs": { + "name": "user" + } + }, + { + "type": "field", + "attrs": { + "name": "isPrivate" + } + } + ] + } + ] }, { - "type": "field", + "type": "page", "attrs": { - "name": "isPrivate" - } + "name": "page-explorer", + "label": "Explorer" + }, + "children": [ + { + "type": "custom", + "attrs": { + "name": "metadata-explorer-widget", + "widget": "MetadataExplorerFormWidget", + "moduleName": "solid-core", + "arrayPath": "savedFilters", + "itemNameField": "name" + } + } + ] } ] } @@ -8098,6 +8134,25 @@ ] } ] + }, + { + "type": "page", + "attrs": { + "name": "page-explorer", + "label": "Explorer" + }, + "children": [ + { + "type": "custom", + "attrs": { + "name": "metadata-explorer-widget", + "widget": "MetadataExplorerFormWidget", + "moduleName": "solid-core", + "arrayPath": "views", + "itemNameField": "name" + } + } + ] } ] } @@ -8501,6 +8556,25 @@ ] } ] + }, + { + "type": "page", + "attrs": { + "name": "page-explorer", + "label": "Explorer" + }, + "children": [ + { + "type": "custom", + "attrs": { + "name": "metadata-explorer-widget", + "widget": "MetadataExplorerFormWidget", + "moduleName": "solid-core", + "arrayPath": "actions", + "itemNameField": "name" + } + } + ] } ] } @@ -8623,111 +8697,147 @@ }, "children": [ { - "type": "group", + "type": "notebook", "attrs": { - "name": "group-1", - "label": "", - "className": "col-12 sm:col-12 md:col-6 lg:col-6" + "name": "notebook-1" }, "children": [ { - "type": "field", - "attrs": { - "name": "name" - } - }, - { - "type": "field", - "attrs": { - "name": "displayName" - } - }, - { - "type": "field", - "attrs": { - "name": "sequenceNumber" - } - }, - { - "type": "field", - "attrs": { - "name": "module" - } - }, - { - "type": "field", - "attrs": { - "name": "parentMenuItem" - } - }, - { - "type": "field", - "attrs": { - "name": "action" - } - }, - { - "type": "field", + "type": "page", "attrs": { - "name": "roles", - "widget": "checkbox", - "inlineCreateAutoSave": "true", - "renderModeCheckboxPreprocessor": "MenuItemMetadataRolesFormFieldPreprocessor", - "inlineCreate": "true", - "inlineCreateLayout": { - "type": "form", + "name": "page-general", + "label": "General" + }, + "children": [ + { + "type": "group", "attrs": { - "name": "form-1", - "label": "User", - "className": "grid", - "width": "20vw" + "name": "group-1", + "label": "", + "className": "col-12 sm:col-12 md:col-6 lg:col-6" }, "children": [ { - "type": "sheet", + "type": "field", + "attrs": { + "name": "name" + } + }, + { + "type": "field", "attrs": { - "name": "sheet-1" - }, - "children": [ - { - "type": "group", + "name": "displayName" + } + }, + { + "type": "field", + "attrs": { + "name": "sequenceNumber" + } + }, + { + "type": "field", + "attrs": { + "name": "module" + } + }, + { + "type": "field", + "attrs": { + "name": "parentMenuItem" + } + }, + { + "type": "field", + "attrs": { + "name": "action" + } + }, + { + "type": "field", + "attrs": { + "name": "roles", + "widget": "checkbox", + "inlineCreateAutoSave": "true", + "renderModeCheckboxPreprocessor": "MenuItemMetadataRolesFormFieldPreprocessor", + "inlineCreate": "true", + "inlineCreateLayout": { + "type": "form", "attrs": { - "name": "group-1", - "label": "", - "className": "col-12" + "name": "form-1", + "label": "User", + "className": "grid", + "width": "20vw" }, "children": [ { - "type": "field", + "type": "sheet", "attrs": { - "name": "name" - } + "name": "sheet-1" + }, + "children": [ + { + "type": "group", + "attrs": { + "name": "group-1", + "label": "", + "className": "col-12" + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "name" + } + } + ] + } + ] } ] } - ] + } + } + ] + }, + { + "type": "group", + "attrs": { + "name": "group-1", + "label": "", + "className": "col-12 sm:col-12 md:col-6 lg:col-6" + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "iconName", + "editWidget": "SolidIconEditWidget", + "viewWidget": "SolidIconViewWidget" + } } ] } - } - } - ] - }, - { - "type": "group", - "attrs": { - "name": "group-1", - "label": "", - "className": "col-12 sm:col-12 md:col-6 lg:col-6" - }, - "children": [ + ] + }, { - "type": "field", + "type": "page", "attrs": { - "name": "iconName", - "editWidget": "SolidIconEditWidget", - "viewWidget": "SolidIconViewWidget" - } + "name": "page-explorer", + "label": "Explorer" + }, + "children": [ + { + "type": "custom", + "attrs": { + "name": "metadata-explorer-widget", + "widget": "MetadataExplorerFormWidget", + "moduleName": "solid-core", + "arrayPath": "menus", + "itemNameField": "name" + } + } + ] } ] } @@ -10998,6 +11108,25 @@ ] } ] + }, + { + "type": "page", + "attrs": { + "name": "page-explorer", + "label": "Explorer" + }, + "children": [ + { + "type": "custom", + "attrs": { + "name": "metadata-explorer-widget", + "widget": "MetadataExplorerFormWidget", + "moduleName": "solid-core", + "arrayPath": "scheduledJobs", + "itemNameField": "scheduleName" + } + } + ] } ] } @@ -11405,71 +11534,107 @@ }, "children": [ { - "type": "group", + "type": "notebook", "attrs": { - "name": "group-1", - "label": "", - "className": "col-12 sm:col-12 md:col-6 lg:col-6" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "name", - "label": "Name" - } - }, - { - "type": "field", - "attrs": { - "name": "displayName", - "label": "Display Name" - } - }, - { - "type": "field", - "attrs": { - "name": "type", - "label": "Type" - } - }, - { - "type": "field", - "attrs": { - "name": "body", - "label": "Body" - } - } - ] - }, - { - "type": "group", - "attrs": { - "name": "group-1", - "label": "", - "className": "col-12 sm:col-12 md:col-6 lg:col-6" + "name": "notebook-1" }, "children": [ { - "type": "field", - "attrs": { - "name": "subject", - "label": "Subject" - } - }, - { - "type": "field", + "type": "page", "attrs": { - "name": "description", - "label": "Description" - } + "name": "page-general", + "label": "General" + }, + "children": [ + { + "type": "group", + "attrs": { + "name": "group-1", + "label": "", + "className": "col-12 sm:col-12 md:col-6 lg:col-6" + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "name", + "label": "Name" + } + }, + { + "type": "field", + "attrs": { + "name": "displayName", + "label": "Display Name" + } + }, + { + "type": "field", + "attrs": { + "name": "type", + "label": "Type" + } + }, + { + "type": "field", + "attrs": { + "name": "body", + "label": "Body" + } + } + ] + }, + { + "type": "group", + "attrs": { + "name": "group-1", + "label": "", + "className": "col-12 sm:col-12 md:col-6 lg:col-6" + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "subject", + "label": "Subject" + } + }, + { + "type": "field", + "attrs": { + "name": "description", + "label": "Description" + } + }, + { + "type": "field", + "attrs": { + "name": "active", + "label": "Active" + } + } + ] + } + ] }, { - "type": "field", + "type": "page", "attrs": { - "name": "active", - "label": "Active" - } + "name": "page-explorer", + "label": "Explorer" + }, + "children": [ + { + "type": "custom", + "attrs": { + "name": "metadata-explorer-widget", + "widget": "MetadataExplorerFormWidget", + "moduleName": "solid-core", + "arrayPath": "emailTemplates", + "itemNameField": "name" + } + } + ] } ] } @@ -11566,71 +11731,107 @@ }, "children": [ { - "type": "group", - "attrs": { - "name": "group-1", - "label": "", - "className": "col-12 sm:col-12 md:col-6 lg:col-6" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "name", - "label": "Name" - } - }, - { - "type": "field", - "attrs": { - "name": "displayName", - "label": "Display Name" - } - }, - { - "type": "field", - "attrs": { - "name": "type", - "label": "Type" - } - } - ] - }, - { - "type": "group", + "type": "notebook", "attrs": { - "name": "group-1", - "label": "", - "className": "col-12 sm:col-12 md:col-6 lg:col-6" + "name": "notebook-1" }, "children": [ { - "type": "field", - "attrs": { - "name": "body", - "label": "Body" - } - }, - { - "type": "field", - "attrs": { - "name": "smsProviderTemplateId", - "label": "Sms Provider Template Id" - } - }, - { - "type": "field", + "type": "page", "attrs": { - "name": "description", - "label": "Description" - } + "name": "page-general", + "label": "General" + }, + "children": [ + { + "type": "group", + "attrs": { + "name": "group-1", + "label": "", + "className": "col-12 sm:col-12 md:col-6 lg:col-6" + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "name", + "label": "Name" + } + }, + { + "type": "field", + "attrs": { + "name": "displayName", + "label": "Display Name" + } + }, + { + "type": "field", + "attrs": { + "name": "type", + "label": "Type" + } + } + ] + }, + { + "type": "group", + "attrs": { + "name": "group-1", + "label": "", + "className": "col-12 sm:col-12 md:col-6 lg:col-6" + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "body", + "label": "Body" + } + }, + { + "type": "field", + "attrs": { + "name": "smsProviderTemplateId", + "label": "Sms Provider Template Id" + } + }, + { + "type": "field", + "attrs": { + "name": "description", + "label": "Description" + } + }, + { + "type": "field", + "attrs": { + "name": "active", + "label": "Active" + } + } + ] + } + ] }, { - "type": "field", + "type": "page", "attrs": { - "name": "active", - "label": "Active" - } + "name": "page-explorer", + "label": "Explorer" + }, + "children": [ + { + "type": "custom", + "attrs": { + "name": "metadata-explorer-widget", + "widget": "MetadataExplorerFormWidget", + "moduleName": "solid-core", + "arrayPath": "smsTemplates", + "itemNameField": "name" + } + } + ] } ] } @@ -12567,65 +12768,101 @@ }, "children": [ { - "type": "row", + "type": "notebook", "attrs": { - "name": "sheet-1" + "name": "notebook-1" }, "children": [ { - "type": "column", + "type": "page", "attrs": { - "name": "group-1", - "label": "", - "className": "col-12 sm:col-12 md:col-6 lg:col-6" + "name": "page-general", + "label": "General" }, "children": [ { - "type": "field", - "attrs": { - "name": "sequenceName" - } - }, - { - "type": "field", - "attrs": { - "name": "prefix" - } - }, - { - "type": "field", - "attrs": { - "name": "separator" - } - }, - { - "type": "field", - "attrs": { - "name": "module" - } - }, - { - "type": "field", - "attrs": { - "name": "model" - } - }, - { - "type": "field", - "attrs": { - "name": "field" - } - }, - { - "type": "field", + "type": "row", "attrs": { - "name": "currentValue" - } - }, + "name": "sheet-1" + }, + "children": [ + { + "type": "column", + "attrs": { + "name": "group-1", + "label": "", + "className": "col-12 sm:col-12 md:col-6 lg:col-6" + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "sequenceName" + } + }, + { + "type": "field", + "attrs": { + "name": "prefix" + } + }, + { + "type": "field", + "attrs": { + "name": "separator" + } + }, + { + "type": "field", + "attrs": { + "name": "module" + } + }, + { + "type": "field", + "attrs": { + "name": "model" + } + }, + { + "type": "field", + "attrs": { + "name": "field" + } + }, + { + "type": "field", + "attrs": { + "name": "currentValue" + } + }, + { + "type": "field", + "attrs": { + "name": "padding" + } + } + ] + } + ] + } + ] + }, + { + "type": "page", + "attrs": { + "name": "page-explorer", + "label": "Explorer" + }, + "children": [ { - "type": "field", + "type": "custom", "attrs": { - "name": "padding" + "name": "metadata-explorer-widget", + "widget": "MetadataExplorerFormWidget", + "moduleName": "solid-core", + "arrayPath": "modelSequences", + "itemNameField": "sequenceName" } } ] @@ -14995,5 +15232,6 @@ ] } } - ] + ], + "savedFilters": [] } \ No newline at end of file From 48d7605cbfc0cb7274d1a80e884e2e36a297f29f Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Thu, 11 Jun 2026 11:11:00 +0100 Subject: [PATCH 121/136] Add TypeORM migration helper functions and export in index --- src/helpers/typeorm-migration-helpers.ts | 163 +++++++++++++++++++++++ src/index.ts | 1 + 2 files changed, 164 insertions(+) create mode 100644 src/helpers/typeorm-migration-helpers.ts diff --git a/src/helpers/typeorm-migration-helpers.ts b/src/helpers/typeorm-migration-helpers.ts new file mode 100644 index 00000000..4ff58bb3 --- /dev/null +++ b/src/helpers/typeorm-migration-helpers.ts @@ -0,0 +1,163 @@ +import { QueryRunner } from 'typeorm'; + +export async function addColumnIfNotExists( + queryRunner: QueryRunner, + table: string, + column: string, + ddl: string, +): Promise { + await queryRunner.query(` +IF COL_LENGTH('${table}', '${column}') IS NULL +BEGIN + ${ddl} +END + `); +} + +export async function createUniqueIndexIfNotExists( + queryRunner: QueryRunner, + table: string, + indexName: string, + ddl: string, +): Promise { + await queryRunner.query(` +IF NOT EXISTS ( + SELECT 1 + FROM sys.indexes + WHERE name = '${indexName}' + AND object_id = OBJECT_ID('${table}') +) +BEGIN + ${ddl} +END + `); +} + +export async function addConstraintIfNotExists( + queryRunner: QueryRunner, + constraintName: string, + ddl: string, +): Promise { + await queryRunner.query(` +IF NOT EXISTS ( + SELECT 1 FROM sys.objects + WHERE name = '${constraintName}' +) +BEGIN + ${ddl} +END + `); +} + +export async function dropColumnIfExists( + queryRunner: QueryRunner, + table: string, + column: string, + ddl: string, +): Promise { + await queryRunner.query(` +IF COL_LENGTH('${table}', '${column}') IS NOT NULL +BEGIN + ${ddl} +END + `); +} + +export async function dropUniqueIndexIfExists( + queryRunner: QueryRunner, + table: string, + indexName: string, + ddl: string, +): Promise { + await queryRunner.query(` +IF EXISTS ( + SELECT 1 + FROM sys.indexes + WHERE name = '${indexName}' + AND object_id = OBJECT_ID('${table}') +) +BEGIN + ${ddl} +END + `); +} + +export async function dropConstraintIfExists( + queryRunner: QueryRunner, + constraintName: string, + ddl: string, +): Promise { + await queryRunner.query(` +IF EXISTS ( + SELECT 1 FROM sys.objects + WHERE name = '${constraintName}' +) +BEGIN + ${ddl} +END + `); +} + +export async function createTableIfNotExists( + queryRunner: QueryRunner, + table: string, + ddl: string, +): Promise { + await queryRunner.query(` +IF OBJECT_ID('${table}', 'U') IS NULL +BEGIN + ${ddl} +END + `); +} + +export async function dropTableIfExists( + queryRunner: QueryRunner, + table: string, + ddl: string, +): Promise { + await queryRunner.query(` +IF OBJECT_ID('${table}', 'U') IS NOT NULL +BEGIN + ${ddl} +END + `); +} + +export async function createIndexIfNotExists( + queryRunner: QueryRunner, + table: string, + indexName: string, + ddl: string, +): Promise { + await queryRunner.query(` +IF NOT EXISTS ( + SELECT 1 + FROM sys.indexes + WHERE name = '${indexName}' + AND object_id = OBJECT_ID('${table}') +) +BEGIN + ${ddl} +END + `); +} + +export async function dropIndexIfExists( + queryRunner: QueryRunner, + table: string, + indexName: string, + ddl: string, +): Promise { + await queryRunner.query(` +IF EXISTS ( + SELECT 1 + FROM sys.indexes + WHERE name = '${indexName}' + AND object_id = OBJECT_ID('${table}') +) +BEGIN + ${ddl} +END + `); +} diff --git a/src/index.ts b/src/index.ts index b89a1127..2fd247e4 100755 --- a/src/index.ts +++ b/src/index.ts @@ -203,6 +203,7 @@ export * from './helpers/model-metadata-helper.service' export * from './helpers/image-encoding.helper' export * from './helpers/solid-microservice-adapter.service' export * from './helpers/typeorm-db-helper'; +export * from './helpers/typeorm-migration-helpers'; export * from './services/crud.service' export * from './interceptors/logging.interceptor' From bf02c9a8c0dab5d6f640c82c9ba55a66c8b6ab6c Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Thu, 11 Jun 2026 15:49:40 +0530 Subject: [PATCH 122/136] removed unnecessary seed file --- src/seeders/seed-data/remove-standalone-menus.sql | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 src/seeders/seed-data/remove-standalone-menus.sql diff --git a/src/seeders/seed-data/remove-standalone-menus.sql b/src/seeders/seed-data/remove-standalone-menus.sql deleted file mode 100644 index 7c4f125c..00000000 --- a/src/seeders/seed-data/remove-standalone-menus.sql +++ /dev/null @@ -1,14 +0,0 @@ --- Delete script: remove standalone menu items, actions, and views --- Run this against your database after deploying the updated metadata. - --- 1. Chatter Message Details -DELETE FROM ss_menu_item_metadata WHERE name = 'chatterMessageDetails-menu-item'; -DELETE FROM ss_action_metadata WHERE name = 'chatterMessageDetails-list-action'; - --- 2. Import Transaction Error Logs (views kept — embedded in importTransaction form) -DELETE FROM ss_menu_item_metadata WHERE name = 'importTransactionErrorLog-menu-item'; -DELETE FROM ss_action_metadata WHERE name = 'importTransactionErrorLog-list-action'; - --- 3. Agent Events (removed from menu; views retained for embedded use) -DELETE FROM ss_menu_item_metadata WHERE name = 'agentEvent-menu-item'; -DELETE FROM ss_action_metadata WHERE name IN ('agentEvent-list-action'); \ No newline at end of file From 285be6ae507cd78bb048c4e78c1d5f5e05ec0e32 Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Thu, 11 Jun 2026 11:50:38 +0100 Subject: [PATCH 123/136] Add archiver package to dependencies --- package-lock.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package-lock.json b/package-lock.json index cc8c9cb3..7e542cd4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "@nestjs/throttler": "^6.4.0", "@types/passport-apple": "^2.0.3", "amqplib": "^0.10.4", + "archiver": "^5.3.2", "axios": "^1.7.0", "bcrypt": "^5.1.1", "bson": "^6.10.1", From 4347cdd45fd6e7495f0fb4a6048294e74290f447 Mon Sep 17 00:00:00 2001 From: Harish Patel Date: Thu, 11 Jun 2026 11:51:04 +0100 Subject: [PATCH 124/136] 0.1.10-beta.26 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7e542cd4..be67affb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.25", + "version": "0.1.10-beta.26", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@solidxai/core", - "version": "0.1.10-beta.25", + "version": "0.1.10-beta.26", "license": "BUSL-1.1", "dependencies": { "@aws-sdk/client-s3": "^3.637.0", diff --git a/package.json b/package.json index faa56fd3..067ecd69 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.25", + "version": "0.1.10-beta.26", "description": "This module is a NestJS module containing all the required core providers required by a Solid application", "main": "dist/index.js", "types": "dist/index.d.ts", From 5137787f5010509294cae4ee4c2f6052a2553587 Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Fri, 12 Jun 2026 15:13:52 +0530 Subject: [PATCH 125/136] added index to fields newly added as sortable and searchable --- src/entities/action-metadata.entity.ts | 1 + src/entities/agent-event.entity.ts | 1 + src/entities/agent-session.entity.ts | 1 + src/entities/chatter-message.entity.ts | 2 + src/entities/email-template.entity.ts | 1 + src/entities/field-metadata.entity.ts | 5 +- src/entities/list-of-values.entity.ts | 6 +- src/entities/locale.entity.ts | 1 + src/entities/media.entity.ts | 1 + src/entities/menu-item-metadata.entity.ts | 2 + src/entities/model-metadata.entity.ts | 3 + src/entities/module-metadata.entity.ts | 2 + src/entities/saved-filters.entity.ts | 3 + src/entities/scheduled-job.entity.ts | 2 + src/entities/security-rule.entity.ts | 1 + src/entities/sms-template.entity.ts | 2 + src/entities/user-activity-history.entity.ts | 1 + src/entities/user.entity.ts | 2 + src/entities/view-metadata.entity.ts | 2 + .../seed-data/solid-core-metadata.json | 280 ++++++++++-------- 20 files changed, 187 insertions(+), 132 deletions(-) diff --git a/src/entities/action-metadata.entity.ts b/src/entities/action-metadata.entity.ts index 55e94532..279c8169 100755 --- a/src/entities/action-metadata.entity.ts +++ b/src/entities/action-metadata.entity.ts @@ -9,6 +9,7 @@ export class ActionMetadata extends CommonEntity { @Column({ name: "name", type: "varchar", unique: true }) name: string; + @Index() @Column({ name: "display_name", type: "varchar" }) displayName: string; diff --git a/src/entities/agent-event.entity.ts b/src/entities/agent-event.entity.ts index d7f066f8..2e357ddb 100644 --- a/src/entities/agent-event.entity.ts +++ b/src/entities/agent-event.entity.ts @@ -49,6 +49,7 @@ export class AgentEvent extends CommonEntity { @Column({ nullable: true }) outputTokens: number; + @Index() @Column({ nullable: true }) modelUsed: string; } diff --git a/src/entities/agent-session.entity.ts b/src/entities/agent-session.entity.ts index 3bb80ffa..8b1be1a0 100644 --- a/src/entities/agent-session.entity.ts +++ b/src/entities/agent-session.entity.ts @@ -15,6 +15,7 @@ export class AgentSession extends CommonEntity { @Column({ nullable: true, ...getColumnType('longText') }) projectRoot: string; + @Index() @Column({ }) modelName: string; diff --git a/src/entities/chatter-message.entity.ts b/src/entities/chatter-message.entity.ts index e328173a..cd84be31 100644 --- a/src/entities/chatter-message.entity.ts +++ b/src/entities/chatter-message.entity.ts @@ -11,6 +11,7 @@ export class ChatterMessage extends CommonEntity { @Column({ type: "varchar" }) messageType: string; // audit | custom + @Index() @Column({ type: "varchar" }) messageSubType: string; // audit_update | audit_insert | audit_delete | custom | note | task @@ -33,6 +34,7 @@ export class ChatterMessage extends CommonEntity { @OneToMany(() => ChatterMessageDetails, (chatterMessageDetails) => chatterMessageDetails.chatterMessage, { cascade: true }) chatterMessageDetails: ChatterMessageDetails[]; + @Index() @Column({ nullable: true }) modelDisplayName: string; diff --git a/src/entities/email-template.entity.ts b/src/entities/email-template.entity.ts index da6b8b97..515b5b5d 100755 --- a/src/entities/email-template.entity.ts +++ b/src/entities/email-template.entity.ts @@ -8,6 +8,7 @@ export class EmailTemplate extends CommonEntity { @Index({ unique: true }) @Column({ name: "name", type: "varchar"}) name: string; + @Index() @Column({ name: "display_name", type: "varchar" }) displayName: string; @Column({ name: "body", default: '', ...getColumnType('longText') }) diff --git a/src/entities/field-metadata.entity.ts b/src/entities/field-metadata.entity.ts index c2f4a00a..aad78cc2 100755 --- a/src/entities/field-metadata.entity.ts +++ b/src/entities/field-metadata.entity.ts @@ -8,10 +8,11 @@ import { ERROR_MESSAGES } from "src/constants/error-messages"; @Entity("ss_field_metadata") export class FieldMetadata extends CommonEntity { - // @Index({ unique: true }) + @Index() @Column({ name: "name" }) name: string; + @Index() @Column({ name: "display_name" }) displayName: string; @@ -19,6 +20,7 @@ export class FieldMetadata extends CommonEntity { description: string; /** int, char etc... */ + @Index() @Column({ name: 'type' }) type: string; @@ -75,6 +77,7 @@ export class FieldMetadata extends CommonEntity { @Column({ name: 'media_max_size_kb', nullable: true }) mediaMaxSizeKb: number; + @Index() @ManyToOne(() => MediaStorageProviderMetadata, { onDelete: 'SET NULL' }) @JoinColumn({ name: 'media_storage_provider_id' }) mediaStorageProvider: MediaStorageProviderMetadata; diff --git a/src/entities/list-of-values.entity.ts b/src/entities/list-of-values.entity.ts index 45ea5baf..bb15a256 100644 --- a/src/entities/list-of-values.entity.ts +++ b/src/entities/list-of-values.entity.ts @@ -1,16 +1,19 @@ import { CommonEntity } from 'src/entities/common.entity' -import { Entity, Column, ManyToOne, JoinColumn, Unique } from 'typeorm' +import { Entity, Column, Index, ManyToOne, JoinColumn, Unique } from 'typeorm' import { ModuleMetadata } from 'src/entities/module-metadata.entity' @Entity("ss_list_of_values") @Unique(['type', 'value']) export class ListOfValues extends CommonEntity { + @Index() @Column({ type: "varchar" }) type: string; + @Index() @Column({ type: "varchar" }) value: string; + @Index() @Column({ type: "varchar" }) display: string; @@ -23,6 +26,7 @@ export class ListOfValues extends CommonEntity { @Column({ type: "integer", nullable: true }) sequence: number; + @Index() @ManyToOne(() => ModuleMetadata, { nullable: true }) @JoinColumn() module: ModuleMetadata; diff --git a/src/entities/locale.entity.ts b/src/entities/locale.entity.ts index 6fa9fd63..f9fcd08e 100644 --- a/src/entities/locale.entity.ts +++ b/src/entities/locale.entity.ts @@ -6,6 +6,7 @@ export class Locale extends CommonEntity { @Index({ unique: true }) @Column({ type: "varchar" }) locale: string; + @Index() @Column({ type: "varchar" }) displayName: string; @Index() diff --git a/src/entities/media.entity.ts b/src/entities/media.entity.ts index f08e522d..a0f73b0a 100644 --- a/src/entities/media.entity.ts +++ b/src/entities/media.entity.ts @@ -12,6 +12,7 @@ export class Media extends CommonEntity { @Column({ type: "varchar", nullable: true }) relativeUri: string; + @Index() @Column({ type: "integer", nullable: true }) fileSize: number; diff --git a/src/entities/menu-item-metadata.entity.ts b/src/entities/menu-item-metadata.entity.ts index 0eb0521a..240ad544 100755 --- a/src/entities/menu-item-metadata.entity.ts +++ b/src/entities/menu-item-metadata.entity.ts @@ -10,6 +10,7 @@ export class MenuItemMetadata extends CommonEntity { @Column({ name: "name", type: "varchar" }) name: string; + @Index() @Column({ name: "display_name", type: "varchar" }) displayName: string; @@ -32,6 +33,7 @@ export class MenuItemMetadata extends CommonEntity { @JoinTable() roles: RoleMetadata[]; + @Index() @Column({ name: "sequence_number", type: "int", nullable: true }) sequenceNumber: number; diff --git a/src/entities/model-metadata.entity.ts b/src/entities/model-metadata.entity.ts index b69638ff..dc5fb1a2 100755 --- a/src/entities/model-metadata.entity.ts +++ b/src/entities/model-metadata.entity.ts @@ -18,9 +18,11 @@ export class ModelMetadata extends CommonEntity { @Column({ name: "plural_name" }) pluralName: string; + @Index() @Column({ name: "display_name" }) displayName: string; + @Index() @Column({ name: "description", nullable: true }) description: string; @@ -55,6 +57,7 @@ export class ModelMetadata extends CommonEntity { // 1. Single field. // 2. Composite field. // 3. Auto generated human readable sequence. + @Index() @ManyToOne(() => FieldMetadata, {}) userKeyField: FieldMetadata; diff --git a/src/entities/module-metadata.entity.ts b/src/entities/module-metadata.entity.ts index f9f2eac0..647bd7b6 100755 --- a/src/entities/module-metadata.entity.ts +++ b/src/entities/module-metadata.entity.ts @@ -4,6 +4,7 @@ import { ModelMetadata } from "./model-metadata.entity"; @Entity("ss_module_metadata") export class ModuleMetadata extends CommonEntity { + @Index() @Column({ name: "display_name" }) displayName: string; @@ -17,6 +18,7 @@ export class ModuleMetadata extends CommonEntity { @Column({ nullable: true }) menuIconUrl: string; + @Index() @Column({ nullable: true }) menuSequenceNumber: number; diff --git a/src/entities/saved-filters.entity.ts b/src/entities/saved-filters.entity.ts index e960efb6..7d62ce7a 100644 --- a/src/entities/saved-filters.entity.ts +++ b/src/entities/saved-filters.entity.ts @@ -17,14 +17,17 @@ export class SavedFilters extends CommonEntity { @Column({ nullable: true, default: false }) isPrivate: boolean = false; + @Index() @ManyToOne(() => User, { nullable: true }) @JoinColumn() user: User; + @Index() @ManyToOne(() => ModelMetadata, { nullable: false }) @JoinColumn() model: ModelMetadata; + @Index() @ManyToOne(() => ViewMetadata, { nullable: false }) @JoinColumn() view: ViewMetadata; diff --git a/src/entities/scheduled-job.entity.ts b/src/entities/scheduled-job.entity.ts index 5bbfb0b7..51cc35dd 100644 --- a/src/entities/scheduled-job.entity.ts +++ b/src/entities/scheduled-job.entity.ts @@ -11,6 +11,7 @@ export class ScheduledJob extends CommonEntity { @Column({ default: false }) isActive: boolean = false; + @Index() @Column({ type: "varchar" }) frequency: string; @@ -38,6 +39,7 @@ export class ScheduledJob extends CommonEntity { @Column({ type: "varchar", nullable: true }) dayOfWeek: string; + @Index() @Column({ type: "varchar" }) job: string; diff --git a/src/entities/security-rule.entity.ts b/src/entities/security-rule.entity.ts index a1db924f..708ca714 100644 --- a/src/entities/security-rule.entity.ts +++ b/src/entities/security-rule.entity.ts @@ -10,6 +10,7 @@ export class SecurityRule extends CommonEntity { @Column({ type: "varchar" }) name: string; + @Index() @Column({ type: "varchar" }) description: string; diff --git a/src/entities/sms-template.entity.ts b/src/entities/sms-template.entity.ts index 822acc05..c8781619 100755 --- a/src/entities/sms-template.entity.ts +++ b/src/entities/sms-template.entity.ts @@ -7,10 +7,12 @@ export class SmsTemplate extends CommonEntity { @Index({ unique: true }) @Column({ name: "name", type: "varchar" }) name: string; + @Index() @Column({ name: "display_name", type: "varchar" }) displayName: string; @Column({ name: "body", ...getColumnType('longText'), nullable: true }) body: string; + @Index() @Column({ type: "varchar", nullable: true }) smsProviderTemplateId: string; @Column({ name: "description", nullable: true }) diff --git a/src/entities/user-activity-history.entity.ts b/src/entities/user-activity-history.entity.ts index e88a178b..f03117f1 100644 --- a/src/entities/user-activity-history.entity.ts +++ b/src/entities/user-activity-history.entity.ts @@ -3,6 +3,7 @@ import { Entity, JoinColumn, ManyToOne, Column, Index } from 'typeorm'; import { User } from 'src/entities/user.entity' @Entity("ss_user_activity_history") export class UserActivityHistory extends CommonEntity { + @Index() @ManyToOne(() => User, { nullable: true }) @JoinColumn() user: User; diff --git a/src/entities/user.entity.ts b/src/entities/user.entity.ts index 67478f1d..26df82e9 100755 --- a/src/entities/user.entity.ts +++ b/src/entities/user.entity.ts @@ -9,6 +9,7 @@ import { Exclude, Expose } from "class-transformer"; @TableInheritance({ column: { type: "varchar", name: "type", default: "User" } }) @Exclude() export class User extends CommonEntity { + @Index() @Column({ type: "varchar", nullable: true }) @Expose() fullName: string; @@ -88,6 +89,7 @@ export class User extends CommonEntity { // don't send to client microsoftProfilePicture: string; + @Index() @Column({ default: true }) @Expose() active: boolean = true; diff --git a/src/entities/view-metadata.entity.ts b/src/entities/view-metadata.entity.ts index e787dc84..94658d2a 100755 --- a/src/entities/view-metadata.entity.ts +++ b/src/entities/view-metadata.entity.ts @@ -11,9 +11,11 @@ export class ViewMetadata extends CommonEntity { @Column({ name: "name", type: "varchar"}) name: string; + @Index() @Column({ name: "display_name", type: "varchar" }) displayName: string; + @Index() @Column({ name: "type", type: "varchar" }) type: string; diff --git a/src/seeders/seed-data/solid-core-metadata.json b/src/seeders/seed-data/solid-core-metadata.json index a0df0225..eddacb4b 100755 --- a/src/seeders/seed-data/solid-core-metadata.json +++ b/src/seeders/seed-data/solid-core-metadata.json @@ -27,7 +27,7 @@ "length": 512, "required": true, "unique": false, - "index": false, + "index": true, "private": false, "encrypt": false, "columnName": "display_name", @@ -54,7 +54,6 @@ "length": 512, "required": false, "unique": false, - "index": false, "private": false, "encrypt": false, "isSystem": true @@ -67,7 +66,6 @@ "length": 512, "required": false, "unique": false, - "index": false, "private": false, "encrypt": false, "isSystem": true @@ -80,7 +78,7 @@ "length": 512, "required": false, "unique": false, - "index": false, + "index": true, "private": false, "encrypt": false, "isSystem": true @@ -93,7 +91,6 @@ "length": 512, "required": false, "unique": false, - "index": false, "private": false, "encrypt": false, "isSystem": true @@ -105,7 +102,6 @@ "defaultValue": "false", "required": false, "unique": false, - "index": true, "private": false, "encrypt": false, "isSystem": true @@ -173,7 +169,7 @@ "length": 512, "required": true, "unique": false, - "index": false, + "index": true, "private": false, "encrypt": false, "columnName": "display_name", @@ -186,7 +182,7 @@ "ormType": "text", "required": false, "unique": false, - "index": false, + "index": true, "private": false, "encrypt": false, "columnName": "description", @@ -227,7 +223,6 @@ "defaultValue": "true", "required": false, "unique": false, - "index": false, "private": false, "encrypt": false, "columnName": "enable_soft_delete", @@ -311,7 +306,6 @@ "defaultValue": "false", "required": false, "unique": false, - "index": true, "private": false, "encrypt": false, "isSystem": true @@ -364,7 +358,7 @@ "ormType": "varchar", "required": true, "unique": false, - "index": false, + "index": true, "private": false, "encrypt": false, "columnName": "name", @@ -377,7 +371,7 @@ "ormType": "varchar", "required": true, "unique": false, - "index": false, + "index": true, "private": false, "encrypt": false, "columnName": "display_name", @@ -390,7 +384,7 @@ "ormType": "varchar", "required": true, "unique": false, - "index": false, + "index": true, "private": false, "encrypt": false, "columnName": "type", @@ -955,7 +949,7 @@ "ormType": "varchar", "required": true, "unique": true, - "index": false, + "index": true, "private": false, "encrypt": false, "columnName": "name", @@ -1059,7 +1053,7 @@ "ormType": "varchar", "required": true, "unique": true, - "index": false, + "index": true, "private": false, "encrypt": false, "columnName": "name", @@ -1074,7 +1068,7 @@ "ormType": "varchar", "required": true, "unique": false, - "index": false, + "index": true, "private": false, "encrypt": false, "columnName": "display_name", @@ -1087,7 +1081,7 @@ "ormType": "varchar", "required": true, "unique": false, - "index": false, + "index": true, "private": false, "encrypt": false, "columnName": "type", @@ -1333,7 +1327,7 @@ "ormType": "varchar", "required": true, "unique": true, - "index": false, + "index": true, "private": false, "encrypt": false, "columnName": "name", @@ -1348,7 +1342,7 @@ "ormType": "varchar", "required": true, "unique": false, - "index": false, + "index": true, "private": false, "encrypt": false, "columnName": "display_name", @@ -1504,7 +1498,7 @@ "ormType": "varchar", "required": true, "unique": true, - "index": false, + "index": true, "private": false, "encrypt": false, "columnName": "name", @@ -1519,7 +1513,7 @@ "ormType": "varchar", "required": true, "unique": false, - "index": false, + "index": true, "private": false, "encrypt": false, "columnName": "display_name", @@ -1532,7 +1526,7 @@ "ormType": "integer", "required": false, "unique": false, - "index": false, + "index": true, "private": false, "encrypt": false, "columnName": "sequence_number", @@ -1666,7 +1660,7 @@ "required": false, "defaultValue": "0", "unique": false, - "index": false, + "index": true, "private": false, "encrypt": false, "columnName": "file_size", @@ -1774,7 +1768,7 @@ "length": 512, "required": false, "unique": false, - "index": false, + "index": true, "private": false, "encrypt": false, "isSystem": true, @@ -1881,7 +1875,6 @@ "required": true, "defaultValue": "local", "unique": false, - "index": false, "private": false, "encrypt": false, "isSystem": true @@ -1946,7 +1939,7 @@ "defaultValue": "false", "required": true, "unique": false, - "index": false, + "index": true, "private": false, "encrypt": false, "isSystem": true @@ -2383,7 +2376,7 @@ "relationCascade": "cascade", "required": false, "unique": false, - "index": false, + "index": true, "private": false, "encrypt": false, "encryptionType": null, @@ -2480,7 +2473,7 @@ "ormType": "varchar", "required": true, "unique": true, - "index": false, + "index": true, "private": false, "encrypt": false, "columnName": "name", @@ -2561,7 +2554,8 @@ "length": 128, "required": true, "unique": true, - "isSystem": true + "isSystem": true, + "index": true }, { "name": "description", @@ -2610,7 +2604,6 @@ "length": 128, "required": false, "unique": false, - "index": false, "private": false, "encrypt": false, "isSystem": true @@ -2680,7 +2673,6 @@ "type": "datetime", "length": 512, "required": false, - "index": false, "isSystem": true }, { @@ -2689,7 +2681,6 @@ "type": "datetime", "length": 512, "required": false, - "index": false, "isSystem": true }, { @@ -2756,7 +2747,6 @@ "length": 128, "required": false, "unique": false, - "index": false, "private": false, "encrypt": false, "selectionValueType": "string", @@ -2806,7 +2796,7 @@ "max": null, "required": true, "unique": true, - "index": false, + "index": true, "private": false, "encrypt": false, "encryptionType": null, @@ -2846,7 +2836,7 @@ "selectionValueType": "string", "required": true, "unique": false, - "index": false, + "index": true, "private": false, "encrypt": false, "encryptionType": null, @@ -3004,7 +2994,7 @@ "max": null, "required": true, "unique": false, - "index": false, + "index": true, "private": false, "encrypt": false, "encryptionType": null, @@ -3190,7 +3180,7 @@ "relationCascade": "restrict", "required": false, "unique": false, - "index": false, + "index": true, "private": false, "encrypt": false, "encryptionType": null, @@ -3215,7 +3205,7 @@ "relationCascade": "restrict", "required": true, "unique": false, - "index": false, + "index": true, "private": false, "encrypt": false, "encryptionType": null, @@ -3240,7 +3230,7 @@ "relationCascade": "restrict", "required": true, "unique": false, - "index": false, + "index": true, "private": false, "encrypt": false, "encryptionType": null, @@ -3269,7 +3259,7 @@ "length": 512, "required": true, "unique": true, - "index": false, + "index": true, "private": false, "encrypt": false, "isSystem": false @@ -3343,7 +3333,7 @@ "length": 128, "required": true, "unique": false, - "index": false, + "index": true, "private": false, "encrypt": false, "isSystem": true @@ -3428,7 +3418,7 @@ "ormType": "varchar", "required": true, "unique": true, - "index": false, + "index": true, "private": false, "encrypt": false, "columnName": "name", @@ -3443,7 +3433,7 @@ "ormType": "varchar", "required": true, "unique": false, - "index": false, + "index": true, "private": false, "encrypt": false, "columnName": "display_name", @@ -3634,7 +3624,7 @@ "ormType": "varchar", "required": true, "unique": true, - "index": false, + "index": true, "private": false, "encrypt": false, "columnName": "name", @@ -3649,7 +3639,7 @@ "ormType": "varchar", "required": true, "unique": false, - "index": false, + "index": true, "private": false, "encrypt": false, "columnName": "display_name", @@ -3691,7 +3681,7 @@ "ormType": "varchar", "required": false, "unique": false, - "index": false, + "index": true, "private": false, "encrypt": false, "isSystem": true @@ -3742,7 +3732,7 @@ "length": 512, "required": true, "unique": false, - "index": false, + "index": true, "private": false, "encrypt": false, "isSystem": false @@ -3755,7 +3745,7 @@ "length": 512, "required": true, "unique": false, - "index": false, + "index": true, "private": false, "encrypt": false, "isSystem": false @@ -3768,7 +3758,7 @@ "length": 512, "required": true, "unique": false, - "index": false, + "index": true, "private": false, "encrypt": false, "isSystem": false @@ -3829,7 +3819,7 @@ "ormType": "integer", "required": false, "unique": false, - "index": false, + "index": true, "private": false, "encrypt": false, "relationType": "many-to-one", @@ -3872,7 +3862,7 @@ "ormType": "varchar", "length": 256, "required": true, - "index": false, + "index": true, "isSystem": false, "selectionValueType": "string", "selectionStaticValues": [ @@ -3960,7 +3950,7 @@ "relationCascade": "cascade", "required": false, "unique": false, - "index": false, + "index": true, "private": false, "encrypt": false, "encryptionType": null, @@ -3976,7 +3966,7 @@ "ormType": "text", "required": false, "unique": false, - "index": false, + "index": true, "private": false, "encrypt": false, "isSystem": false @@ -4051,7 +4041,6 @@ "length": 512, "required": false, "unique": false, - "index": false, "private": false, "encrypt": false, "isSystem": false @@ -4077,7 +4066,6 @@ "length": 512, "required": true, "unique": false, - "index": false, "private": false, "encrypt": false, "isSystem": false @@ -4090,7 +4078,6 @@ "length": 512, "required": false, "unique": false, - "index": false, "private": false, "encrypt": false, "isSystem": false @@ -4985,7 +4972,7 @@ "length": 255, "required": true, "unique": false, - "index": false, + "index": true, "private": false, "encrypt": false, "isSystem": true @@ -5285,7 +5272,7 @@ "length": 255, "required": false, "unique": false, - "index": false, + "index": true, "private": false, "encrypt": false, "isSystem": true @@ -5342,7 +5329,6 @@ "length": 128, "required": false, "unique": false, - "index": false, "private": false, "encrypt": false, "isSystem": true @@ -5355,7 +5341,6 @@ "length": 32, "required": true, "unique": false, - "index": false, "private": false, "encrypt": false, "isSystem": true @@ -5381,7 +5366,6 @@ "length": 64, "required": false, "unique": false, - "index": false, "private": false, "encrypt": false, "isSystem": true @@ -5445,7 +5429,6 @@ "length": 16, "required": true, "unique": false, - "index": false, "private": false, "encrypt": false, "isSystem": true @@ -6760,37 +6743,32 @@ { "type": "field", "attrs": { - "name": "description", - "isSearchable": true + "name": "description" } }, { "type": "field", "attrs": { - "name": "menuIconUrl", - "isSearchable": true + "name": "menuIconUrl" } }, { "type": "field", "attrs": { "name": "menuSequenceNumber", - "isSearchable": true, "sortable": true } }, { "type": "field", "attrs": { - "name": "defaultDataSource", - "isSearchable": true + "name": "defaultDataSource" } }, { "type": "field", "attrs": { - "name": "isSystem", - "isSearchable": true + "name": "isSystem" } } ] @@ -6979,8 +6957,7 @@ { "type": "field", "attrs": { - "name": "enableSoftDelete", - "isSearchable": true + "name": "enableSoftDelete" } } ] @@ -7802,72 +7779,116 @@ }, "children": [ { - "type": "sheet", + "type": "notebook", "attrs": { - "name": "sheet-1" + "name": "notebook-1" }, "children": [ { - "type": "group", + "type": "page", "attrs": { - "name": "group-1", - "label": "", - "className": "col-12 sm:col-12 md:col-6 lg:col-6" + "name": "page-general", + "label": "General" }, "children": [ { - "type": "field", - "attrs": { - "name": "messageType" - } - }, - { - "type": "field", - "attrs": { - "name": "messageSubType" - } - }, - { - "type": "field", - "attrs": { - "name": "modelDisplayName" - } - }, - { - "type": "field", - "attrs": { - "name": "coModelEntityId" - } - }, - { - "type": "field", - "attrs": { - "name": "modelUserKey" - } - }, - { - "type": "field", - "attrs": { - "name": "user" - } - }, - { - "type": "field", - "attrs": { - "name": "messageBody" - } - }, - { - "type": "field", + "type": "sheet", "attrs": { - "name": "messageAttachments" - } - }, + "name": "sheet-1" + }, + "children": [ + { + "type": "group", + "attrs": { + "name": "group-1", + "label": "", + "className": "col-12 sm:col-12 md:col-6 lg:col-6" + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "messageType" + } + }, + { + "type": "field", + "attrs": { + "name": "messageSubType" + } + }, + { + "type": "field", + "attrs": { + "name": "modelDisplayName" + } + }, + { + "type": "field", + "attrs": { + "name": "coModelEntityId" + } + }, + { + "type": "field", + "attrs": { + "name": "modelUserKey" + } + }, + { + "type": "field", + "attrs": { + "name": "user" + } + }, + { + "type": "field", + "attrs": { + "name": "messageBody" + } + }, + { + "type": "field", + "attrs": { + "name": "messageAttachments" + } + } + ] + } + ] + } + ] + }, + { + "type": "page", + "attrs": { + "name": "page-details", + "label": "Details" + }, + "children": [ { - "type": "field", + "type": "sheet", "attrs": { - "name": "chatterMessageDetails" - } + "name": "sheet-details" + }, + "children": [ + { + "type": "group", + "attrs": { + "name": "group-details", + "label": "", + "className": "col-12" + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "chatterMessageDetails" + } + } + ] + } + ] } ] } @@ -9600,8 +9621,7 @@ { "type": "field", "attrs": { - "name": "lastLoginProvider", - "isSearchable": true + "name": "lastLoginProvider" } }, { From 42964b3dfe8c49fd9ca0f3bd564db076c0979eb6 Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Tue, 16 Jun 2026 13:21:29 +0530 Subject: [PATCH 126/136] roles group by module changes --- package-lock.json | 10 +-- package.json | 6 +- src/dtos/create-permission-metadata.dto.ts | 3 + src/dtos/create-role-metadata.dto.ts | 12 ++- src/dtos/update-permission-metadata.dto.ts | 5 +- src/dtos/update-role-metadata.dto.ts | 22 ++++- src/entities/permission-metadata.entity.ts | 4 +- src/entities/role-metadata.entity.ts | 14 ++- .../seed-data/solid-core-metadata.json | 86 +++++++++++++++++-- 9 files changed, 138 insertions(+), 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index be67affb..3cb0285e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -86,7 +86,7 @@ "@nestjs/testing": "^10.0.0", "@nestjs/typeorm": "^10.0.1", "@nestjs/websockets": "^10.0.0", - "@solidxai/code-builder": "^0.0.2", + "@solidxai/code-builder": "^0.1.8", "@types/express": "^4.17.17", "@types/hapi__joi": "^17.1.12", "@types/jest": "^29.5.2", @@ -5298,11 +5298,11 @@ } }, "node_modules/@solidxai/code-builder": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@solidxai/code-builder/-/code-builder-0.0.2.tgz", - "integrity": "sha512-W+JLL+V5LJjn2zYyHuoyZMJFm7g5ahzjiJ+XtM+bnMF+1PjHKXcu2h/vC3Nq8c72Alz25aNu03nqbQPBBhnhiA==", + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@solidxai/code-builder/-/code-builder-0.1.8.tgz", + "integrity": "sha512-rrHZdPufn0RCtTbSoWPqWCePXmZOLSC+uyYqFSpSZsZB23WDcBRV9oYFB8i44rhIeOS/lgL+C1EgFylKetT6vQ==", "dev": true, - "license": "MIT", + "license": "BUSL-1.1", "dependencies": { "@angular-devkit/core": "^18.0.1", "@angular-devkit/schematics": "^18.0.1", diff --git a/package.json b/package.json index 067ecd69..cbb8bc39 100755 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "passport-local": "^1.0.0", "passport-microsoft": "^2.1.0", "pg": "^8.11.3", + "playwright": ">=1.0.0", "pluralize": "^8.0.0", "puppeteer": "^23.2.0", "qs": "^6.12.0", @@ -80,8 +81,7 @@ "ts-morph": "^27.0.2", "twilio": "^5.8.0", "uuid": "^9.0.1", - "xlsx": "^0.18.5", - "playwright": ">=1.0.0" + "xlsx": "^0.18.5" }, "peerDependenciesMeta": {}, "peerDependencies": { @@ -130,7 +130,7 @@ "@nestjs/testing": "^10.0.0", "@nestjs/typeorm": "^10.0.1", "@nestjs/websockets": "^10.0.0", - "@solidxai/code-builder": "^0.0.2", + "@solidxai/code-builder": "^0.1.8", "@types/express": "^4.17.17", "@types/hapi__joi": "^17.1.12", "@types/jest": "^29.5.2", diff --git a/src/dtos/create-permission-metadata.dto.ts b/src/dtos/create-permission-metadata.dto.ts index 60aa11d3..370d8ee2 100755 --- a/src/dtos/create-permission-metadata.dto.ts +++ b/src/dtos/create-permission-metadata.dto.ts @@ -2,9 +2,12 @@ import { IsString } from 'class-validator'; import { IsNotEmpty, ValidateNested, IsArray, IsOptional } from 'class-validator'; import { Type } from 'class-transformer'; import { UpdateRoleMetadataDto } from 'src/dtos/update-role-metadata.dto'; +import { ApiProperty } from '@nestjs/swagger'; + export class CreatePermissionMetadataDto { @IsNotEmpty() @IsString() + @ApiProperty() name: string; @IsArray() diff --git a/src/dtos/create-role-metadata.dto.ts b/src/dtos/create-role-metadata.dto.ts index c966e1ba..e14e9156 100755 --- a/src/dtos/create-role-metadata.dto.ts +++ b/src/dtos/create-role-metadata.dto.ts @@ -1,5 +1,5 @@ import { IsString } from 'class-validator'; -import { IsNotEmpty, ValidateNested, IsArray, IsOptional } from 'class-validator'; +import { IsNotEmpty, ValidateNested, IsArray, IsOptional, IsInt } from 'class-validator'; import { Type } from 'class-transformer'; import { UpdatePermissionMetadataDto } from 'src/dtos/update-permission-metadata.dto'; import { UpdateUserDto } from 'src/dtos/update-user.dto'; @@ -66,4 +66,14 @@ export class CreateRoleMetadataDto { @IsOptional() @ApiProperty() menuItemsCommand: string; + + @IsOptional() + @IsInt() + @ApiProperty() + moduleId: number; + + @IsString() + @IsOptional() + @ApiProperty() + moduleUserKey: string; } diff --git a/src/dtos/update-permission-metadata.dto.ts b/src/dtos/update-permission-metadata.dto.ts index bb0e9b12..09847ca3 100755 --- a/src/dtos/update-permission-metadata.dto.ts +++ b/src/dtos/update-permission-metadata.dto.ts @@ -1,14 +1,17 @@ import { IsInt, IsOptional, IsString, IsNotEmpty, ValidateNested, IsArray } from 'class-validator'; import { Type } from 'class-transformer'; import { UpdateRoleMetadataDto } from 'src/dtos/update-role-metadata.dto'; +import { ApiProperty } from '@nestjs/swagger'; + export class UpdatePermissionMetadataDto { @IsOptional() @IsInt() id: number; - @IsOptional() @IsNotEmpty() + @IsOptional() @IsString() + @ApiProperty() name: string; @IsArray() diff --git a/src/dtos/update-role-metadata.dto.ts b/src/dtos/update-role-metadata.dto.ts index d61daddc..b643dc08 100755 --- a/src/dtos/update-role-metadata.dto.ts +++ b/src/dtos/update-role-metadata.dto.ts @@ -1,4 +1,4 @@ -import { IsInt,IsOptional, IsString, IsNotEmpty, ValidateNested, IsArray } from 'class-validator'; +import { IsInt, IsOptional, IsString, IsNotEmpty, ValidateNested, IsArray } from 'class-validator'; import { Type } from 'class-transformer'; import { UpdatePermissionMetadataDto } from 'src/dtos/update-permission-metadata.dto'; import { UpdateUserDto } from 'src/dtos/update-user.dto'; @@ -9,51 +9,71 @@ export class UpdateRoleMetadataDto { @IsOptional() @IsInt() id: number; + @IsNotEmpty() @IsOptional() @IsString() @ApiProperty() name: string; + @IsOptional() @ApiProperty() @IsArray() @ValidateNested({ each: true }) @Type(() => UpdatePermissionMetadataDto) permissions: UpdatePermissionMetadataDto[]; + @IsOptional() @IsArray() @ApiProperty() permissionsIds: number[]; + @IsString() @IsOptional() @ApiProperty() permissionsCommand: string; + @IsOptional() @ApiProperty() @IsArray() @ValidateNested({ each: true }) @Type(() => UpdateUserDto) users: UpdateUserDto[]; + @IsOptional() @IsArray() @ApiProperty() usersIds: number[]; + @IsString() @IsOptional() @ApiProperty() usersCommand: string; + @IsOptional() @ApiProperty() @IsArray() @ValidateNested({ each: true }) @Type(() => UpdateMenuItemMetadataDto) menuItems: UpdateMenuItemMetadataDto[]; + @IsOptional() @IsArray() @ApiProperty() menuItemsIds: number[]; + @IsString() @IsOptional() @ApiProperty() menuItemsCommand: string; + + @IsOptional() + @IsInt() + @ApiProperty() + moduleId: number; + + @IsString() + @IsOptional() + @ApiProperty() + moduleUserKey: string; } \ No newline at end of file diff --git a/src/entities/permission-metadata.entity.ts b/src/entities/permission-metadata.entity.ts index d7e36cf8..6dc58fe8 100755 --- a/src/entities/permission-metadata.entity.ts +++ b/src/entities/permission-metadata.entity.ts @@ -1,10 +1,10 @@ import { CommonEntity } from "src/entities/common.entity" import { Entity, Column, Index, ManyToMany } from "typeorm"; import { RoleMetadata } from 'src/entities/role-metadata.entity' + @Entity("ss_permission_metadata") export class PermissionMetadata extends CommonEntity { - - @Index({unique: true}) + @Index({ unique: true }) @Column({ name: "name", type: "varchar" }) name: string; diff --git a/src/entities/role-metadata.entity.ts b/src/entities/role-metadata.entity.ts index 31e56e61..6a431da4 100755 --- a/src/entities/role-metadata.entity.ts +++ b/src/entities/role-metadata.entity.ts @@ -1,11 +1,14 @@ import { CommonEntity } from "src/entities/common.entity" -import { Entity, Column, JoinTable, ManyToMany } from "typeorm"; +import { Entity, Column, JoinTable, ManyToMany, Index, JoinColumn, ManyToOne } from "typeorm"; import { PermissionMetadata } from 'src/entities/permission-metadata.entity'; import { User } from 'src/entities/user.entity'; -import { MenuItemMetadata } from 'src/entities/menu-item-metadata.entity' +import { MenuItemMetadata } from 'src/entities/menu-item-metadata.entity'; +import { ModuleMetadata } from 'src/entities/module-metadata.entity' + @Entity("ss_role_metadata") export class RoleMetadata extends CommonEntity { - @Column({ name: "name", type: "varchar", unique: true }) + @Index({ unique: true }) + @Column({ name: "name", type: "varchar"}) name: string; @ManyToMany(() => PermissionMetadata, permissionMetadata => permissionMetadata.roles, { cascade: true }) @@ -17,4 +20,9 @@ export class RoleMetadata extends CommonEntity { @ManyToMany(() => MenuItemMetadata, menuItemMetadata => menuItemMetadata.roles, { cascade: ['insert', 'update'] }) menuItems: MenuItemMetadata[]; + + @Index() + @ManyToOne(() => ModuleMetadata, { onDelete: "SET NULL", nullable: true }) + @JoinColumn() + module: ModuleMetadata; } \ No newline at end of file diff --git a/src/seeders/seed-data/solid-core-metadata.json b/src/seeders/seed-data/solid-core-metadata.json index eddacb4b..cfdeae61 100755 --- a/src/seeders/seed-data/solid-core-metadata.json +++ b/src/seeders/seed-data/solid-core-metadata.json @@ -2532,6 +2532,22 @@ "relationModelModuleName": "solid-core", "isSystem": true, "isRelationManyToManyOwner": false + }, + { + "name": "module", + "displayName": "Module", + "type": "relation", + "required": false, + "unique": false, + "index": true, + "private": false, + "encrypt": false, + "relationType": "many-to-one", + "relationCoModelSingularName": "moduleMetadata", + "relationCreateInverse": false, + "relationCascade": "set null", + "relationModelModuleName": "solid-core", + "isSystem": true } ] }, @@ -6301,6 +6317,19 @@ "moduleUserKey": "solid-core", "modelUserKey": "savedFilters", "viewUserKey": "savedFilters-tree-view" + }, + { + "displayName": "Role Tree View Action", + "name": "roleMetadata-tree-action", + "type": "solid", + "domain": "", + "context": "", + "customComponent": "", + "customIsModal": true, + "serverEndpoint": "", + "viewUserKey": "roleMetadata-tree-view", + "moduleUserKey": "solid-core", + "modelUserKey": "roleMetadata" } ], "menus": [ @@ -9740,7 +9769,9 @@ { "type": "field", "attrs": { - "name": "roles" + "name": "roles", + "editWidget": "RolesGroupedByModuleWidget", + "showLabel": false } }, { @@ -9942,11 +9973,11 @@ 50 ], "enableGlobalSearch": true, - "create": false, - "edit": false, - "delete": false, - "import": false, - "export": false, + "create": true, + "edit": true, + "delete": true, + "import": true, + "export": true, "allowedViews": [ "list", "tree" @@ -9977,8 +10008,8 @@ "name": "form-1", "label": "User", "className": "grid", - "showAddFormButton": false, - "showEditFormButton": false + "showAddFormButton": true, + "showEditFormButton": true }, "children": [ { @@ -10014,6 +10045,14 @@ "name": "name", "showLabel": false } + }, + { + "type": "field", + "attrs": { + "name": "module", + "label": "Module", + "showLabel": true + } } ] } @@ -14779,6 +14818,37 @@ } ] } + }, + { + "name": "roleMetadata-tree-view", + "displayName": "Role", + "type": "tree", + "context": "{}", + "moduleUserKey": "solid-core", + "modelUserKey": "roleMetadata", + "layout": { + "type": "tree", + "attrs": { + "pagination": true, + "pageSizeOptions": [ + 10, + 25, + 50 + ], + "enableGlobalSearch": true, + "create": true, + "edit": true, + "delete": true + }, + "children": [ + { + "type": "field", + "attrs": { + "name": "id" + } + } + ] + } } ], "emailTemplates": [ From 0352f3740c5cd2729ba9aa7517b4d53d037dcd76 Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Tue, 16 Jun 2026 13:30:33 +0530 Subject: [PATCH 127/136] added module to rolemetadata list view --- src/seeders/seed-data/solid-core-metadata.json | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/seeders/seed-data/solid-core-metadata.json b/src/seeders/seed-data/solid-core-metadata.json index cfdeae61..9cb3c311 100755 --- a/src/seeders/seed-data/solid-core-metadata.json +++ b/src/seeders/seed-data/solid-core-metadata.json @@ -9979,8 +9979,7 @@ "import": true, "export": true, "allowedViews": [ - "list", - "tree" + "list" ] }, "children": [ @@ -9991,6 +9990,13 @@ "isSearchable": true, "sortable": true } + }, + { + "type": "field", + "attrs": { + "name": "module", + "isSearchable": false + } } ] } From 52a228cf634e9cdeefdab07d7e4ea9c8ec5154a6 Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Tue, 16 Jun 2026 17:13:26 +0530 Subject: [PATCH 128/136] enabling create,edit,import,export on permission and lov modules --- .../seed-data/solid-core-metadata.json | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/src/seeders/seed-data/solid-core-metadata.json b/src/seeders/seed-data/solid-core-metadata.json index 9cb3c311..e92ca711 100755 --- a/src/seeders/seed-data/solid-core-metadata.json +++ b/src/seeders/seed-data/solid-core-metadata.json @@ -7291,8 +7291,8 @@ "name": "form-1", "label": "Solid List of Values Model", "className": "grid", - "showAddFormButton": false, - "showEditFormButton": false + "showAddFormButton": true, + "showEditFormButton": true }, "children": [ { @@ -7375,11 +7375,11 @@ 50 ], "enableGlobalSearch": true, - "create": false, - "edit": false, - "delete": false, - "import": false, - "export": false, + "create": true, + "edit": true, + "delete": true, + "import": true, + "export": true, "allowedViews": [ "list", "tree" @@ -9894,11 +9894,11 @@ 50 ], "enableGlobalSearch": true, - "create": false, - "edit": false, - "delete": false, - "import": false, - "export": false + "create": true, + "edit": true, + "delete": true, + "import": true, + "export": true }, "children": [ { @@ -9926,7 +9926,9 @@ "label": "Permission", "className": "grid", "disabled": true, - "readonly": true + "readonly": true, + "showAddFormButton": true, + "showEditFormButton": true }, "children": [ { @@ -14581,11 +14583,11 @@ 50 ], "enableGlobalSearch": true, - "create": false, - "edit": false, - "delete": false, - "import": false, - "export": false + "create": true, + "edit": true, + "delete": true, + "import": true, + "export": true }, "children": [ { From bc77e17b6f5c072ca3c9f6d0d01f919e9163ca74 Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Wed, 17 Jun 2026 13:12:35 +0530 Subject: [PATCH 129/136] ai interaction cleanup changes --- ai-interaction-cleanup.sql | 53 ++ src/commands/ingest.command.ts | 36 - src/controllers/ai-interaction.controller.ts | 112 --- src/controllers/service.controller.ts | 4 - src/controllers/test.controller.ts | 9 - src/dtos/create-ai-interaction.dto.ts | 89 -- src/dtos/invoke-ai-prompt.dto.ts | 10 - src/dtos/update-ai-interaction.dto.ts | 93 -- src/entities/ai-interaction.entity.ts | 71 -- src/index.ts | 5 - src/interfaces.ts | 8 - ...r-mcp-client-publisher-database.service.ts | 22 - .../trigger-mcp-client-queue-options.ts | 9 - ...-mcp-client-subscriber-database.service.ts | 278 ------ .../trigger-mcp-client-publisher.service.ts | 22 - .../trigger-mcp-client-queue-options.ts | 9 - .../trigger-mcp-client-subscriber.service.ts | 104 --- ...gger-mcp-client-publisher-redis.service.ts | 23 - .../trigger-mcp-client-queue-options-redis.ts | 9 - ...ger-mcp-client-subscriber-redis.service.ts | 98 --- src/repository/ai-interaction.repository.ts | 17 - .../seed-data/solid-core-metadata.json | 595 ------------- src/services/ai-interaction.service.ts | 239 ------ src/services/genai/ingest-metadata.service.ts | 798 ------------------ .../mcp-handler-factory.service.ts | 33 - src/services/genai/r2r-helper.service.ts | 37 - src/solid-core.module.ts | 31 - 27 files changed, 53 insertions(+), 2761 deletions(-) create mode 100644 ai-interaction-cleanup.sql delete mode 100755 src/commands/ingest.command.ts delete mode 100644 src/controllers/ai-interaction.controller.ts delete mode 100644 src/dtos/create-ai-interaction.dto.ts delete mode 100644 src/dtos/invoke-ai-prompt.dto.ts delete mode 100644 src/dtos/update-ai-interaction.dto.ts delete mode 100644 src/entities/ai-interaction.entity.ts delete mode 100644 src/jobs/database/trigger-mcp-client-publisher-database.service.ts delete mode 100644 src/jobs/database/trigger-mcp-client-queue-options.ts delete mode 100644 src/jobs/database/trigger-mcp-client-subscriber-database.service.ts delete mode 100644 src/jobs/rabbitmq/trigger-mcp-client-publisher.service.ts delete mode 100644 src/jobs/rabbitmq/trigger-mcp-client-queue-options.ts delete mode 100644 src/jobs/rabbitmq/trigger-mcp-client-subscriber.service.ts delete mode 100644 src/jobs/redis/trigger-mcp-client-publisher-redis.service.ts delete mode 100644 src/jobs/redis/trigger-mcp-client-queue-options-redis.ts delete mode 100644 src/jobs/redis/trigger-mcp-client-subscriber-redis.service.ts delete mode 100644 src/repository/ai-interaction.repository.ts delete mode 100644 src/services/ai-interaction.service.ts delete mode 100644 src/services/genai/ingest-metadata.service.ts delete mode 100644 src/services/genai/mcp-handlers/mcp-handler-factory.service.ts delete mode 100644 src/services/genai/r2r-helper.service.ts diff --git a/ai-interaction-cleanup.sql b/ai-interaction-cleanup.sql new file mode 100644 index 00000000..c8f3e4c6 --- /dev/null +++ b/ai-interaction-cleanup.sql @@ -0,0 +1,53 @@ +-- ============================================================= +-- AI Interaction Metadata Cleanup Script +-- Run this against your PostgreSQL database to remove all +-- metadata and data associated with the aiInteraction model. +-- ============================================================= + +BEGIN; + +-- 1. Remove menu item +DELETE FROM ss_menu_item_metadata +WHERE name = 'aiInteraction-menu-item'; + +-- 2. Remove action +DELETE FROM ss_action_metadata +WHERE name = 'aiInteraction-list-action'; + +-- 3. Remove user-level view customisations for aiInteraction views +DELETE FROM ss_user_view_metadata +WHERE view_metadata_id IN ( + SELECT id FROM ss_view_metadata + WHERE name IN ('aiInteraction-list-view', 'aiInteraction-form-view') +); + +-- 4. Remove saved filters pointing at the aiInteraction model +DELETE FROM ss_saved_fitlers +WHERE model_id = ( + SELECT id FROM ss_model_metadata WHERE singular_name = 'aiInteraction' +); + +-- 5. Remove security rules scoped to the aiInteraction model +DELETE FROM ss_security_rule +WHERE model_metadata_id = ( + SELECT id FROM ss_model_metadata WHERE singular_name = 'aiInteraction' +); + +-- 6. Remove views +DELETE FROM ss_view_metadata +WHERE name IN ('aiInteraction-list-view', 'aiInteraction-form-view'); + +-- 7. Remove field metadata belonging to the aiInteraction model +DELETE FROM ss_field_metadata +WHERE model_id = ( + SELECT id FROM ss_model_metadata WHERE singular_name = 'aiInteraction' +); + +-- 8. Remove the model metadata record itself +DELETE FROM ss_model_metadata +WHERE singular_name = 'aiInteraction'; + +-- 9. Drop the actual data table +DROP TABLE IF EXISTS ss_ai_interactions; + +COMMIT; \ No newline at end of file diff --git a/src/commands/ingest.command.ts b/src/commands/ingest.command.ts deleted file mode 100755 index 7d1d9a29..00000000 --- a/src/commands/ingest.command.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Logger } from '@nestjs/common'; -import { Command, CommandRunner, Option } from 'nest-commander'; -import { SolidRegistry } from 'src/helpers/solid-registry'; -import { IngestMetadataService } from 'src/services/genai/ingest-metadata.service'; - -interface IngestCommandOptions { - module?: string; - // seeder?: string; -} - -@Command({ name: 'ingest', description: 'Ingests solid metadata json for all modules deployed in the consuming project' }) -export class IngestCommand extends CommandRunner { - private readonly logger = new Logger(IngestCommand.name); - - constructor( - private readonly solidRegistry: SolidRegistry, - private readonly ingestMetadataService: IngestMetadataService, - ) { - super(); - } - - async run(passedParam: string[], options?: IngestCommandOptions): Promise { - this.logger.log(`Running the solid ingest for module ${options.module} at ${new Date()}`); - await this.ingestMetadataService.ingest(); - } - - @Option({ - flags: '-m, --module [module name]', - description: 'The seeder to run.', - required: false, - defaultValue: '' - }) - parseString(val: string): string { - return val; - } -} diff --git a/src/controllers/ai-interaction.controller.ts b/src/controllers/ai-interaction.controller.ts deleted file mode 100644 index eb168c93..00000000 --- a/src/controllers/ai-interaction.controller.ts +++ /dev/null @@ -1,112 +0,0 @@ -import { Controller, Post, Body, Param, UploadedFiles, UseInterceptors, Put, Get, Query, Delete, Patch } from '@nestjs/common'; -import { AnyFilesInterceptor } from "@nestjs/platform-express"; -import { ApiBearerAuth, ApiQuery, ApiTags } from '@nestjs/swagger'; -import { AiInteractionService } from '../services/ai-interaction.service'; -import { CreateAiInteractionDto } from '../dtos/create-ai-interaction.dto'; -import { UpdateAiInteractionDto } from '../dtos/update-ai-interaction.dto'; -import { InvokeAiPromptDto } from '../dtos/invoke-ai-prompt.dto'; -import { ActiveUser } from 'src/decorators/active-user.decorator'; -import { ActiveUserData } from 'src/interfaces/active-user-data.interface'; - -enum ShowSoftDeleted { - INCLUSIVE = "inclusive", - EXCLUSIVE = "exclusive", -} - -@ApiTags('Solid Core') -@Controller('ai-interaction') -export class AiInteractionController { - constructor(private readonly service: AiInteractionService) { } - - @ApiBearerAuth("jwt") - @Post() - @UseInterceptors(AnyFilesInterceptor()) - create(@Body() createDto: CreateAiInteractionDto, @UploadedFiles() files: Array) { - return this.service.create(createDto, files); - } - - @ApiBearerAuth("jwt") - @Post('/bulk') - @UseInterceptors(AnyFilesInterceptor()) - insertMany(@Body() createDtos: CreateAiInteractionDto[], @UploadedFiles() filesArray: Express.Multer.File[][] = []) { - return this.service.insertMany(createDtos, filesArray); - } - - - @ApiBearerAuth("jwt") - @Put(':id') - @UseInterceptors(AnyFilesInterceptor()) - update(@Param('id') id: number, @Body() updateDto: UpdateAiInteractionDto, @UploadedFiles() files: Array) { - return this.service.update(id, updateDto, files); - } - - @ApiBearerAuth("jwt") - @Patch(':id') - @UseInterceptors(AnyFilesInterceptor()) - partialUpdate(@Param('id') id: number, @Body() updateDto: UpdateAiInteractionDto, @UploadedFiles() files: Array) { - return this.service.update(id, updateDto, files, true); - } - - @ApiBearerAuth("jwt") - @Post('/bulk-recover') - async recoverMany(@Body() ids: number[]) { - return this.service.recoverMany(ids); - } - - @ApiBearerAuth("jwt") - @Get('/recover/:id') - async recover(@Param('id') id: number) { - return this.service.recover(id); - } - - @ApiBearerAuth("jwt") - @ApiQuery({ name: 'showSoftDeleted', required: false, enum: ShowSoftDeleted }) - @ApiQuery({ name: 'limit', required: false, type: Number }) - @ApiQuery({ name: 'offset', required: false, type: Number }) - @ApiQuery({ name: 'fields', required: false, type: Array }) - @ApiQuery({ name: 'sort', required: false, type: Array }) - @ApiQuery({ name: 'groupBy', required: false, type: Array }) - @ApiQuery({ name: 'populate', required: false, type: Array }) - @ApiQuery({ name: 'populateMedia', required: false, type: Array }) - @ApiQuery({ name: 'filters', required: false, type: Array }) - @Get() - async findMany(@Query() query: any) { - return this.service.find(query); - } - - @ApiBearerAuth("jwt") - @Get(':id') - async findOne(@Param('id') id: string, @Query() query: any) { - return this.service.findOne(+id, query); - } - - @ApiBearerAuth("jwt") - @Delete('/bulk') - async deleteMany(@Body() ids: number[]) { - return this.service.deleteMany(ids); - } - - @ApiBearerAuth("jwt") - @Delete(':id') - async delete(@Param('id') id: number) { - return this.service.delete(id); - } - - @ApiBearerAuth("jwt") - @Post('/trigger-mcp-client-job') - async triggerMcpClientJob(@Body() dto: InvokeAiPromptDto, @ActiveUser() activeUser: ActiveUserData) { - return this.service.triggerMcpClientJob(dto, activeUser.sub); - } - - @ApiBearerAuth("jwt") - @Post(':id/apply-solid-ai-interaction') - async applySolidAiInteraction(@Param('id') id: number) { - return this.service.applySolidAiInteraction(+id); - } - - @ApiBearerAuth("jwt") - @Post('/run-mcp-prompt') - async runMcpPrompt(@Body() dto: InvokeAiPromptDto) { - return this.service.runMcpPrompt(dto.prompt); - } -} diff --git a/src/controllers/service.controller.ts b/src/controllers/service.controller.ts index 8e6bf045..3aa58666 100755 --- a/src/controllers/service.controller.ts +++ b/src/controllers/service.controller.ts @@ -4,8 +4,6 @@ import { ActiveUser } from 'src/decorators/active-user.decorator'; import { Public } from 'src/decorators/public.decorator'; import { ErrorMapperService } from 'src/helpers/error-mapper.service'; import { ActiveUserData } from 'src/interfaces/active-user-data.interface'; -import { AiInteractionService } from 'src/services/ai-interaction.service'; -import { IngestMetadataService } from 'src/services/genai/ingest-metadata.service'; import { MqMessageService } from 'src/services/mq-message.service'; import { SolidRegistry } from '../helpers/solid-registry'; @@ -22,10 +20,8 @@ export class ServiceController { constructor( private readonly solidRegistry: SolidRegistry, - private readonly aiInteractionService: AiInteractionService, private readonly mqMessageService: MqMessageService, private readonly errorMapper: ErrorMapperService, - private readonly ingestMetadataService: IngestMetadataService, ) { } @Public() diff --git a/src/controllers/test.controller.ts b/src/controllers/test.controller.ts index c3e45614..a1801168 100755 --- a/src/controllers/test.controller.ts +++ b/src/controllers/test.controller.ts @@ -4,7 +4,6 @@ import { ApiBearerAuth, ApiTags } from "@nestjs/swagger"; import { SolidRegistry } from "src/helpers/solid-registry"; import { Auth } from "src/decorators/auth.decorator"; import { AuthType } from "src/enums/auth-type.enum"; -import { IngestMetadataService } from "src/services/genai/ingest-metadata.service"; export class SeedData { seeder: string; @@ -16,7 +15,6 @@ export class TestController { private readonly logger = new Logger(TestController.name); constructor( private readonly solidRegistry: SolidRegistry, - private readonly ingestMetadataService: IngestMetadataService, ) { } @Auth(AuthType.None) @@ -37,11 +35,4 @@ export class TestController { return { filename: file.originalname }; } - @ApiBearerAuth("jwt") - @Post('mcp/ingest') - @UseInterceptors(FileInterceptor('file')) - async mcpIngest(@UploadedFile() file: Express.Multer.File, @Body() body: any) { - await this.ingestMetadataService.ingest(); - return { ok: true }; - } } diff --git a/src/dtos/create-ai-interaction.dto.ts b/src/dtos/create-ai-interaction.dto.ts deleted file mode 100644 index 2985bcb5..00000000 --- a/src/dtos/create-ai-interaction.dto.ts +++ /dev/null @@ -1,89 +0,0 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsInt } from 'class-validator'; -import { IsOptional } from 'class-validator'; -import { IsString, IsNotEmpty, IsJSON, IsBoolean } from 'class-validator'; - -export class CreateAiInteractionDto { - @IsOptional() - @IsInt() - @ApiProperty() - userId: number; - @IsString() - @IsOptional() - @ApiProperty() - userUserKey: string; - @IsNotEmpty() - @IsString() - @ApiProperty() - threadId: string; - @IsNotEmpty() - @IsString() - @ApiProperty() - role: string; - @IsNotEmpty() - @IsString() - @ApiProperty() - message: string; - @IsOptional() - @IsString() - @ApiProperty() - contentType: string; - @IsOptional() - @IsString() - @ApiProperty() - status: string; - @IsOptional() - @IsString() - @ApiProperty() - errorMessage: string; - @IsOptional() - @IsString() - @ApiProperty() - modelUsed: string; - @IsOptional() - @IsInt() - @ApiProperty() - responseTimeMs: number; - @IsOptional() - @IsJSON() - @ApiProperty() - metadata: any; - @IsOptional() - @IsBoolean() - @ApiProperty() - isApplied: boolean = false; - @IsOptional() - @IsInt() - @ApiProperty() - parentInteractionId: number; - @IsString() - @IsOptional() - @ApiProperty() - parentInteractionUserKey: string; - @IsOptional() - @IsBoolean() - @ApiProperty() - isAutoApply: boolean = false; - @IsOptional() - @IsInt() - @ApiProperty() - inputTokens: number; - @IsOptional() - @IsInt() - @ApiProperty() - outputTokens: number; - @IsOptional() - @IsInt() - @ApiProperty() - totalTokens: number; - -@IsOptional() -@IsString() -@ApiProperty() -originalMessage: string; - -@IsOptional() -@IsBoolean() -@ApiProperty() -isEdited: boolean = false; -} \ No newline at end of file diff --git a/src/dtos/invoke-ai-prompt.dto.ts b/src/dtos/invoke-ai-prompt.dto.ts deleted file mode 100644 index f106de73..00000000 --- a/src/dtos/invoke-ai-prompt.dto.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { IsOptional, IsString } from 'class-validator'; - -export class InvokeAiPromptDto { - @IsString() - prompt: string; - - @IsOptional() - @IsString() - moduleName: string; -} \ No newline at end of file diff --git a/src/dtos/update-ai-interaction.dto.ts b/src/dtos/update-ai-interaction.dto.ts deleted file mode 100644 index 1124742e..00000000 --- a/src/dtos/update-ai-interaction.dto.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { IsInt,IsOptional, IsString, IsNotEmpty, IsJSON, IsBoolean } from 'class-validator'; -import { ApiProperty } from '@nestjs/swagger'; - -export class UpdateAiInteractionDto { - @IsOptional() - @IsInt() - id: number; - @IsOptional() - @IsInt() - @ApiProperty() - userId: number; - @IsString() - @IsOptional() - @ApiProperty() - userUserKey: string; - @IsNotEmpty() - @IsOptional() - @IsString() - @ApiProperty() - threadId: string; - @IsNotEmpty() - @IsOptional() - @IsString() - @ApiProperty() - role: string; - @IsNotEmpty() - @IsOptional() - @IsString() - @ApiProperty() - message: string; - @IsOptional() - @IsString() - @ApiProperty() - contentType: string; - @IsOptional() - @IsString() - @ApiProperty() - status: string; - @IsOptional() - @IsString() - @ApiProperty() - errorMessage: string; - @IsOptional() - @IsString() - @ApiProperty() - modelUsed: string; - @IsOptional() - @IsInt() - @ApiProperty() - responseTimeMs: number; - @IsOptional() - @IsJSON() - @ApiProperty() - metadata: any; - @IsOptional() - @IsBoolean() - @ApiProperty() - isApplied: boolean; - @IsOptional() - @IsInt() - @ApiProperty() - parentInteractionId: number; - @IsString() - @IsOptional() - @ApiProperty() - parentInteractionUserKey: string; - @IsOptional() - @IsBoolean() - @ApiProperty() - isAutoApply: boolean; - @IsOptional() - @IsInt() - @ApiProperty() - inputTokens: number; - @IsOptional() - @IsInt() - @ApiProperty() - outputTokens: number; - @IsOptional() - @IsInt() - @ApiProperty() - totalTokens: number; - -@IsOptional() -@IsString() -@ApiProperty() -originalMessage: string; - -@IsOptional() -@IsBoolean() -@ApiProperty() -isEdited: boolean; -} \ No newline at end of file diff --git a/src/entities/ai-interaction.entity.ts b/src/entities/ai-interaction.entity.ts deleted file mode 100644 index 7728ad6d..00000000 --- a/src/entities/ai-interaction.entity.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { CommonEntity } from 'src/entities/common.entity' -import { Entity, JoinColumn, ManyToOne, Index, Column } from 'typeorm'; -import { User } from 'src/entities/user.entity' -import { getColumnType } from 'src/helpers/typeorm-db-helper'; - -@Entity("ss_ai_interactions") -export class AiInteraction extends CommonEntity { - @Index() - @ManyToOne(() => User, { nullable: true }) - @JoinColumn() - user: User; - - @Index() - @Column({ type: "varchar" }) - threadId: string; - - @Column({ type: "varchar" }) - role: string; - - @Column({ ...getColumnType('longText'), nullable: true }) - message: string; - - @Column({ type: "varchar", nullable: true }) - contentType: string; - - @Index() - @Column({ type: "varchar", nullable: true }) - status: string; - - @Column({ nullable: true, ...getColumnType('longText') }) - errorMessage: string; - - @Column({ type: "varchar", nullable: true }) - modelUsed: string; - - @Column({ type: "integer", nullable: true }) - responseTimeMs: number; - - @Column({ type: "simple-json", nullable: true, ...getColumnType('simpleJsonLargeText') }) - metadata: any; - - @Column({ nullable: true, default: false }) - isApplied: boolean = false; - - @Index() - @ManyToOne(() => AiInteraction, { nullable: true }) - @JoinColumn() - parentInteraction: AiInteraction; - - @Index({ unique: true }) - @Column({ type: "varchar" }) - externalId: string; - - @Column({ nullable: true, default: false }) - isAutoApply: boolean = false; - - @Column({ type: "integer", nullable: true }) - inputTokens: number; - - @Column({ type: "integer", nullable: true }) - outputTokens: number; - - @Column({ type: "integer", nullable: true }) - totalTokens: number; - - @Column({ nullable: true, ...getColumnType('longText') }) - originalMessage: string; - - @Column({ nullable: true, default: false }) - isEdited: boolean = false; -} diff --git a/src/index.ts b/src/index.ts index 2fd247e4..48176bb1 100755 --- a/src/index.ts +++ b/src/index.ts @@ -154,7 +154,6 @@ export * from './entities/import-transaction.entity' export * from './entities/import-transaction-error-log.entity' export * from './entities/locale.entity' export * from './entities/user-activity-history.entity' -export * from './entities/ai-interaction.entity' export * from './entities/model-sequence.entity' export * from './entities/dashboard-user-layout.entity' export * from './entities/user-api-key.entity' @@ -261,9 +260,6 @@ export * from './jobs/redis/test-queue-subscriber-redis.service' export * from './jobs/redis/three60-whatsapp-publisher-redis.service' export * from './jobs/redis/three60-whatsapp-queue-options-redis' export * from './jobs/redis/three60-whatsapp-subscriber-redis.service' -export * from './jobs/redis/trigger-mcp-client-publisher-redis.service' -export * from './jobs/redis/trigger-mcp-client-queue-options-redis' -export * from './jobs/redis/trigger-mcp-client-subscriber-redis.service' export * from './jobs/redis/twilio-sms-publisher-redis.service' export * from './jobs/redis/twilio-sms-queue-options-redis' export * from './jobs/redis/twilio-sms-subscriber-redis.service' @@ -355,7 +351,6 @@ export * from './services/dashboard-runtime.service' export * from './services/queues/publisher-factory.service' export * from './services/queues/database-publisher.service' export * from './services/queues/database-subscriber.service' -export * from './services/ai-interaction.service' export * from './services/dashboard-providers/mq-dashboard-total-messages-kpi-provider.service' export * from './services/dashboard-providers/mq-dashboard-succeeded-messages-kpi-provider.service' export * from './services/dashboard-providers/mq-dashboard-failed-messages-kpi-provider.service' diff --git a/src/interfaces.ts b/src/interfaces.ts index 9e86e103..b1705aae 100755 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -13,7 +13,6 @@ import { CreateSecurityRuleDto } from './dtos/create-security-rule.dto'; import { FieldMetadata } from './entities/field-metadata.entity'; import { Media } from './entities/media.entity'; import { ComputedFieldMetadata } from './helpers/solid-registry'; -import { AiInteraction } from './entities/ai-interaction.entity'; import { ActiveUserData } from './interfaces/active-user-data.interface'; import { SecurityRuleConfig } from './dtos/security-rule-config.dto'; import { SecurityRule } from './entities/security-rule.entity'; @@ -121,10 +120,6 @@ export interface CodeGenerationOptions { dryRun?: boolean; } -export interface TriggerMcpClientOptions { - aiInteractionId: number; - moduleName: string; -} export interface McpResponse { success: boolean; @@ -196,9 +191,6 @@ export interface IDashboardWidgetDataProvider< ): Promise | any>; } -export interface IMcpToolResponseHandler { - apply(aiInteraction: AiInteraction); -} /** * @deprecated Use `IEntityComputedFieldProvider` instead. diff --git a/src/jobs/database/trigger-mcp-client-publisher-database.service.ts b/src/jobs/database/trigger-mcp-client-publisher-database.service.ts deleted file mode 100644 index bb2e13d9..00000000 --- a/src/jobs/database/trigger-mcp-client-publisher-database.service.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Injectable } from "@nestjs/common"; -import { QueuesModuleOptions, TriggerMcpClientOptions } from "src/interfaces"; -import { MqMessageQueueService } from "src/services/mq-message-queue.service"; -import { MqMessageService } from "src/services/mq-message.service"; -import { DatabasePublisher } from "src/services/queues/database-publisher.service"; -import triggerMcpClientQueueOptions from "./trigger-mcp-client-queue-options"; - -@Injectable() -export class TriggerMcpClientPublisherDatabase extends DatabasePublisher { - constructor( - protected readonly mqMessageService: MqMessageService, - protected readonly mqMessageQueueService: MqMessageQueueService, - ) { - super(mqMessageService, mqMessageQueueService); - } - - options(): QueuesModuleOptions { - return { - ...triggerMcpClientQueueOptions - }; - } -} \ No newline at end of file diff --git a/src/jobs/database/trigger-mcp-client-queue-options.ts b/src/jobs/database/trigger-mcp-client-queue-options.ts deleted file mode 100644 index 62dacb6d..00000000 --- a/src/jobs/database/trigger-mcp-client-queue-options.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { BrokerType } from "src/interfaces"; - -const TRIGGER_MCP_CLIENT_QUEUE_NAME = 'solid_trigger_mcp_client_queue'; - -export default { - name: TRIGGER_MCP_CLIENT_QUEUE_NAME, - type: BrokerType.Database, - queueName: TRIGGER_MCP_CLIENT_QUEUE_NAME, -}; diff --git a/src/jobs/database/trigger-mcp-client-subscriber-database.service.ts b/src/jobs/database/trigger-mcp-client-subscriber-database.service.ts deleted file mode 100644 index 5fe4cdf5..00000000 --- a/src/jobs/database/trigger-mcp-client-subscriber-database.service.ts +++ /dev/null @@ -1,278 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import { ModelMetadataService } from '../../services/model-metadata.service'; -import { SolidRegistry } from 'src/helpers/solid-registry'; -import { QueueMessage } from 'src/interfaces/mq'; -import { AiInteractionService } from 'src/services/ai-interaction.service'; -import { ModuleMetadataService } from 'src/services/module-metadata.service'; -import { PollerService } from 'src/services/poller.service'; -import { DatabaseSubscriber } from 'src/services/queues/database-subscriber.service'; -import { McpResponse, QueuesModuleOptions, TriggerMcpClientOptions } from "../../interfaces"; -import { MqMessageQueueService } from '../../services/mq-message-queue.service'; -import { MqMessageService } from '../../services/mq-message.service'; -import triggerMcpClientQueueOptions from "./trigger-mcp-client-queue-options"; - -@Injectable() -export class TriggerMcpClientSubscriberDatabase extends DatabaseSubscriber { - private readonly triggerMcpClientSubscriberLogger = new Logger(TriggerMcpClientSubscriberDatabase.name); - - constructor( - readonly mqMessageService: MqMessageService, - readonly mqMessageQueueService: MqMessageQueueService, - readonly poller: PollerService, - readonly aiInteractionService: AiInteractionService, - readonly moduleMetadataService: ModuleMetadataService, - readonly modelMetadataService: ModelMetadataService, - private readonly solidRegistry: SolidRegistry - - ) { - super(mqMessageService, mqMessageQueueService, poller); - } - - options(): QueuesModuleOptions { - return { - ...triggerMcpClientQueueOptions - } - } - - cleanNestedResponse(aiResponse: McpResponse) { - let nestedResponse: any; - - try { - let raw = aiResponse.response; - - if (typeof raw === "string") { - raw = raw.trim(); - try { - // Try to parse as JSON - nestedResponse = JSON.parse(raw); - } catch { - // Not JSON, just keep as string - nestedResponse = raw; - } - } else if (typeof raw === "object" && raw !== null) { - // Already JSON - nestedResponse = raw; - } else { - // Fallback - nestedResponse = String(raw); - } - } catch (err: any) { - this.triggerMcpClientSubscriberLogger.error("Error processing AI response:", err); - nestedResponse = `Error handling response: ${err?.message || String(err)}`; - } - - return nestedResponse; - } - - async subscribe(message: QueueMessage) { - this.triggerMcpClientSubscriberLogger.debug(`Received message: ${JSON.stringify(message)}`); - - const codeGnerationOptions = message.payload; - - const aiInteraction = await this.aiInteractionService.findOne(codeGnerationOptions.aiInteractionId, { - populate: ['user'] - }); - if (!aiInteraction) { - const m = `Unable to identified the aiInteraction entry that triggered this job... using id: ${codeGnerationOptions.aiInteractionId}` - this.triggerMcpClientSubscriberLogger.log(m); - throw new Error(m); - } - - // The message contains the users prompt. - const prompt = aiInteraction.message; - - // Use this to invoke our mcp client - // TODO: try / catch ... - // Handle the rejection gracefully... - - // We create the aiInteraction entry first - const genAiInteraction = await this.aiInteractionService.create({ - userId: aiInteraction.user.id, - threadId: aiInteraction.threadId, - parentInteractionId: aiInteraction.id, - role: 'gen-ai', - message: '...', // Updated in the tool - contentType: '', // Updated in the tool - errorMessage: '', // Updated after we receive the response - modelUsed: '', // Updated after we receive the response - responseTimeMs: 0, // Updated after we receive the response - metadata: '', // Updated in the tool - isApplied: false, // Updated after we receive the response - status: 'pending' // Updated after we receive the response - }); - - const existingComputationProviders = this.solidRegistry.getComputedFieldProviders(); - - const { records: existingModules } = await this.moduleMetadataService.findMany({ - filters: { - name: { $ne: 'solid-core' } - }, - fields: ['id', 'name', 'displayName', 'description'], - limit: 1000 - }); - const { records: existingModels } = await this.modelMetadataService.findMany({ - filters: { - module: { - name: { - $ne: 'solid-core', - } - } - }, - limit: 1000, - populate: ['module'] - }); - - const existingControllerAndTheirMethods = this.solidRegistry.getControllers(); - - - - // Build markdown sections using template interpolation - const modulesSection = (existingModules ?? []) - .map(m => [ - `### ${m.displayName}`, - `- name: ${m.name}`, - `- description: ${m.description ?? ""}`, - ].join('\n')) - .join('\n\n'); - - const modelsSection = (existingModels ?? []) - .map(m => [ - `### ${m.displayName}`, - `- singularName: ${m.singularName}`, - `- description: ${m.description ?? ""}`, - `- moduleName: ${m.module.name}`, - ].join('\n')) - .join('\n\n'); - - const computationProvidersSection = (existingComputationProviders ?? []) - .map(m => [ - `### ${m.instance.name()}`, - `- name: ${m.instance.name()}`, - `- description: ${m.instance.help() ?? ""}`, - ].join('\n')) - .join('\n\n'); - - const controllersAndTheirMethods = (existingControllerAndTheirMethods ?? []) - .map(m => [ - `### ${m.name}`, - `- methods: ${m.methods.length ? m.methods.map(m => `- ${m}`).join('\n') : '- No methods found'}`, - ].join('\n')) - .join('\n\n'); - - - - const finalPrompt = ` -# User Prompt: -${prompt} - -# System Instructions: -- aiInteractionId: ${genAiInteraction.id} -- You will be invoking tools if needed, hence you will have to choose the applicable tools based on the tool context given to you. -- If a tool is invoked, you must return **exactly** the raw output from the tool, without any json envelopes, additional formatting, commentary, or text. -- Do not wrap the result in quotes, JSON, or markdown fences. -- Do not explain what the result means. - -# LISTS FOR REFERENCE AND VALIDATIONS - -## LIST OF EXISTING MODULES -Use the below list of modules to infer which module the user is referring to, . - -${modulesSection} - -## LIST OF EXISTING MODELS -Use the below list of models to infer which model the user is referring to. -You need to pull out the singularName for the model from the user prompt to match against the below list. - -${modelsSection} - -## LIST OF EXISTING COMPUTED FIELD PROVIDERS -Use the below list of computed field providers to infer which provider the user is referring to. - -${computationProvidersSection} -`.trim(); - - // ## LIST OF EXISTING CONTROLLERS AND THEIR METHODS - // Use the below list of controllers and their methods to infer whether the api call being made to a particular controller exists or not. - - // ${controllersAndTheirMethods} - - const aiResponse = await this.aiInteractionService.runMcpPrompt(finalPrompt); - this.triggerMcpClientSubscriberLogger.log(`aiResponse: `); - this.triggerMcpClientSubscriberLogger.log(JSON.stringify(aiResponse)); - - if (!aiResponse.success) { - this.triggerMcpClientSubscriberLogger.log(`Gen ai has returned with a false status code`); - - const errorsStr = aiResponse.errors?.join('\n '); - const errorTrace = aiResponse.error_trace?.join('\n'); - - // await this.aiInteractionService.create({ - // userId: aiInteraction.user.id, - // threadId: aiInteraction.threadId, - // parentInteractionId: aiInteraction.id, - // role: 'gen-ai', - // message: '-', - // contentType: aiResponse.content_type, - // errorMessage: `${errorsStr}\n\n${errorTrace}`, - // modelUsed: aiResponse.model, - // responseTimeMs: aiResponse.duration_ms, - // metadata: JSON.stringify(aiResponse, null, 2), - // isApplied: aiInteraction.isApplied, - // status: aiResponse.success ? 'succeeded' : 'failed' - // }); - - // TODO: Update the previously created genAiInteraction record with the respective error fields and save to DB - await this.aiInteractionService.update(genAiInteraction.id, { - contentType: aiResponse.content_type, - errorMessage: `${errorsStr}\n\n${errorTrace}`, - modelUsed: aiResponse.model, - responseTimeMs: aiResponse.duration_ms, - isApplied: aiInteraction.isApplied, - status: aiResponse.success ? 'succeeded' : 'failed' - }, [], true); - - // update the job entry with failure... raising an error will lead the job to be marked as failed... - throw new Error(errorsStr); - } - else { - let nestedResponse = this.cleanNestedResponse(aiResponse); - - // TODO: Update the previously created genAiInteraction record with the respective success fields and save to DB - const errorsStr = nestedResponse?.status == "error" && nestedResponse?.errors.join('\n '); - const errorTrace = nestedResponse?.status == "error" && nestedResponse?.error_trace?.join('\n'); - - // const genAiInteraction = await this.aiInteractionService.create({ - // userId: aiInteraction.user.id, - // threadId: aiInteraction.threadId, - // parentInteractionId: aiInteraction.id, - // role: 'gen-ai', - // message: nestedResponse, - // contentType: aiResponse.content_type, - // errorMessage: `${errorsStr}\n\n${errorTrace}`, - // modelUsed: aiResponse.model, - // responseTimeMs: aiResponse.duration_ms, - // metadata: JSON.stringify(aiResponse, null, 2), - // isApplied: aiInteraction.isApplied, - // status: aiResponse.success ? 'succeeded' : 'failed' - // }); - - await this.aiInteractionService.update(genAiInteraction.id, { - errorMessage: nestedResponse.status == "error" ? `${errorsStr}\n\n${errorTrace}` : "", - contentType: aiResponse.content_type, - message: JSON.stringify(nestedResponse), - modelUsed: aiResponse.model, - responseTimeMs: aiResponse.duration_ms, - metadata: JSON.stringify(aiResponse, null, 2), - isApplied: aiInteraction.isApplied, - status: aiResponse.success && nestedResponse.status == "success" ? 'succeeded' : 'failed' - }, [], true); - - // If the human interaction was with isAutoApply=true, then we can go ahead and autoApply. - if (aiInteraction.isAutoApply) { - this.aiInteractionService.applySolidAiInteraction(genAiInteraction.id); - } - } - - return aiResponse; - } -} diff --git a/src/jobs/rabbitmq/trigger-mcp-client-publisher.service.ts b/src/jobs/rabbitmq/trigger-mcp-client-publisher.service.ts deleted file mode 100644 index 350722ec..00000000 --- a/src/jobs/rabbitmq/trigger-mcp-client-publisher.service.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { Injectable } from "@nestjs/common"; -import { QueuesModuleOptions, TriggerMcpClientOptions } from "../../interfaces"; -import { MqMessageQueueService } from "src/services/mq-message-queue.service"; -import { MqMessageService } from "src/services/mq-message.service"; -import { RabbitMqPublisher } from "src/services/queues/rabbitmq-publisher.service"; -import triggerMcpClientQueueOptions from "./trigger-mcp-client-queue-options"; - -@Injectable() -export class TriggerMcpClientPublisherRabbitmq extends RabbitMqPublisher { - constructor( - protected readonly mqMessageService: MqMessageService, - protected readonly mqMessageQueueService: MqMessageQueueService, - ) { - super(mqMessageService, mqMessageQueueService); - } - - options(): QueuesModuleOptions { - return { - ...triggerMcpClientQueueOptions - }; - } -} diff --git a/src/jobs/rabbitmq/trigger-mcp-client-queue-options.ts b/src/jobs/rabbitmq/trigger-mcp-client-queue-options.ts deleted file mode 100644 index b1230a32..00000000 --- a/src/jobs/rabbitmq/trigger-mcp-client-queue-options.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { BrokerType } from "../../interfaces"; - -const TRIGGER_MCP_CLIENT_QUEUE_NAME = 'solid_trigger_mcp_client_queue_rabbitmq'; - -export default { - name: TRIGGER_MCP_CLIENT_QUEUE_NAME, - type: BrokerType.RabbitMQ, - queueName: TRIGGER_MCP_CLIENT_QUEUE_NAME, -}; diff --git a/src/jobs/rabbitmq/trigger-mcp-client-subscriber.service.ts b/src/jobs/rabbitmq/trigger-mcp-client-subscriber.service.ts deleted file mode 100644 index 0cbad5fd..00000000 --- a/src/jobs/rabbitmq/trigger-mcp-client-subscriber.service.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; - -import { QueueMessage } from 'src/interfaces/mq'; -import triggerMcpClientQueueOptions from "./trigger-mcp-client-queue-options"; -import { AiInteractionService } from 'src/services/ai-interaction.service'; -import { PollerService } from 'src/services/poller.service'; -import { TriggerMcpClientOptions, QueuesModuleOptions } from '../../interfaces'; -import { MqMessageQueueService } from 'src/services/mq-message-queue.service'; -import { MqMessageService } from 'src/services/mq-message.service'; -import { RabbitMqSubscriber } from 'src/services/queues/rabbitmq-subscriber.service'; - -@Injectable() -export class TriggerMcpClientSubscriberRabbitmq extends RabbitMqSubscriber { - private readonly triggerMcpClientSubscriberLogger = new Logger(TriggerMcpClientSubscriberRabbitmq.name); - - constructor( - readonly mqMessageService: MqMessageService, - readonly mqMessageQueueService: MqMessageQueueService, - readonly poller: PollerService, - readonly aiInteractionService: AiInteractionService, - ) { - super(mqMessageService, mqMessageQueueService); - } - - options(): QueuesModuleOptions { - return { - ...triggerMcpClientQueueOptions - } - } - - async subscribe(message: QueueMessage) { - this.triggerMcpClientSubscriberLogger.debug(`Received message: ${JSON.stringify(message)}`); - - const codeGnerationOptions = message.payload; - - const aiInteraction = await this.aiInteractionService.findOne(codeGnerationOptions.aiInteractionId, { - populate: ['user'] - }); - if (!aiInteraction) { - const m = `Unable to identified the aiInteraction entry that triggered this job... using id: ${codeGnerationOptions.aiInteractionId}` - this.triggerMcpClientSubscriberLogger.log(m); - throw new Error(m); - } - - // The message contains the users prompt. - const prompt = aiInteraction.message; - - // Use this to invoke our mcp client - // TODO: try / catch ... - // Handle the rejection gracefully... - const aiResponse = await this.aiInteractionService.runMcpPrompt(prompt); - this.triggerMcpClientSubscriberLogger.log(`aiResponse: `); - this.triggerMcpClientSubscriberLogger.log(JSON.stringify(aiResponse)); - - if (!aiResponse.success) { - this.triggerMcpClientSubscriberLogger.log(`Gen ai has returned with a false status code`); - - const errorsStr = aiResponse.errors.join('; '); - - await this.aiInteractionService.create({ - userId: aiInteraction.user.id, - threadId: aiInteraction.threadId, - parentInteractionId: aiInteraction.id, - role: 'gen-ai', - message: '-', - contentType: aiResponse.content_type, - errorMessage: errorsStr, - modelUsed: aiResponse.model, - responseTimeMs: aiResponse.duration_ms, - metadata: JSON.stringify(aiResponse), - isApplied: aiInteraction.isApplied, - status: aiResponse.success ? 'succeeded' : 'failed' - }); - - // update the job entry with failure... raising an error will lead the job to be marked as failed... - throw new Error(errorsStr); - } - else { - let nestedResponse = aiResponse.response.trim(); - - const genAiInteraction = await this.aiInteractionService.create({ - userId: aiInteraction.user.id, - threadId: aiInteraction.threadId, - parentInteractionId: aiInteraction.id, - role: 'gen-ai', - message: nestedResponse, - contentType: aiResponse.content_type, - errorMessage: '', - modelUsed: aiResponse.model, - responseTimeMs: aiResponse.duration_ms, - metadata: JSON.stringify(aiResponse), - isApplied: aiInteraction.isApplied, - status: aiResponse.success ? 'succeeded' : 'failed' - }); - - // If the human interaction was with isAutoApply=true, then we can go ahead and autoApply. - if (aiInteraction.isAutoApply) { - this.aiInteractionService.applySolidAiInteraction(genAiInteraction.id); - } - } - - return aiResponse; - } -} diff --git a/src/jobs/redis/trigger-mcp-client-publisher-redis.service.ts b/src/jobs/redis/trigger-mcp-client-publisher-redis.service.ts deleted file mode 100644 index 6857491b..00000000 --- a/src/jobs/redis/trigger-mcp-client-publisher-redis.service.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { RedisPublisher } from 'src/services/queues/redis-publisher.service'; -import triggerMcpClientQueueConfig from './trigger-mcp-client-queue-options-redis'; -import { MqMessageQueueService } from '../../services/mq-message-queue.service'; -import { MqMessageService } from '../../services/mq-message.service'; -import { TriggerMcpClientOptions, QueuesModuleOptions } from "../../interfaces"; - -@Injectable() -export class TriggerMcpClientPublisherRedis extends RedisPublisher { - constructor( - protected readonly mqMessageService: MqMessageService, - protected readonly mqMessageQueueService: MqMessageQueueService, - ) { - super(mqMessageService, mqMessageQueueService); - } - - options(): QueuesModuleOptions { - return { - ...triggerMcpClientQueueConfig - } - } -} diff --git a/src/jobs/redis/trigger-mcp-client-queue-options-redis.ts b/src/jobs/redis/trigger-mcp-client-queue-options-redis.ts deleted file mode 100644 index fc3de41f..00000000 --- a/src/jobs/redis/trigger-mcp-client-queue-options-redis.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { BrokerType } from "../../interfaces"; - -const QUEUE_NAME = 'solid_trigger_mcp_client_queue_redis'; - -export default { - name: QUEUE_NAME, - type: BrokerType.Redis, - queueName: QUEUE_NAME, -}; diff --git a/src/jobs/redis/trigger-mcp-client-subscriber-redis.service.ts b/src/jobs/redis/trigger-mcp-client-subscriber-redis.service.ts deleted file mode 100644 index b0b1c3c5..00000000 --- a/src/jobs/redis/trigger-mcp-client-subscriber-redis.service.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; - -import { RedisSubscriber } from 'src/services/queues/redis-subscriber.service'; -import { QueueMessage } from 'src/interfaces/mq'; -import triggerMcpClientQueueConfig from './trigger-mcp-client-queue-options-redis'; -import { MqMessageService } from '../../services/mq-message.service'; -import { MqMessageQueueService } from '../../services/mq-message-queue.service'; -import { TriggerMcpClientOptions, QueuesModuleOptions } from "../../interfaces"; -import { AiInteractionService } from '../../services/ai-interaction.service'; -import { PollerService } from '../../services/poller.service'; - -@Injectable() -export class TriggerMcpClientSubscriberRedis extends RedisSubscriber { - private readonly triggerMcpClientSubscriberLogger = new Logger(TriggerMcpClientSubscriberRedis.name); - - constructor( - readonly mqMessageService: MqMessageService, - readonly mqMessageQueueService: MqMessageQueueService, - readonly poller: PollerService, - readonly aiInteractionService: AiInteractionService, - ) { - super(mqMessageService, mqMessageQueueService); - } - - options(): QueuesModuleOptions { - return { - ...triggerMcpClientQueueConfig - } - } - - async subscribe(message: QueueMessage) { - this.triggerMcpClientSubscriberLogger.debug(`Received message: ${JSON.stringify(message)}`); - - const codeGnerationOptions = message.payload; - - const aiInteraction = await this.aiInteractionService.findOne(codeGnerationOptions.aiInteractionId, { - populate: ['user'] - }); - if (!aiInteraction) { - const m = `Unable to identified the aiInteraction entry that triggered this job... using id: ${codeGnerationOptions.aiInteractionId}` - this.triggerMcpClientSubscriberLogger.log(m); - throw new Error(m); - } - - const prompt = aiInteraction.message; - - const aiResponse = await this.aiInteractionService.runMcpPrompt(prompt); - this.triggerMcpClientSubscriberLogger.log(`aiResponse: `); - this.triggerMcpClientSubscriberLogger.log(JSON.stringify(aiResponse)); - - if (!aiResponse.success) { - this.triggerMcpClientSubscriberLogger.log(`Gen ai has returned with a false status code`); - - const errorsStr = aiResponse.errors.join('; '); - - await this.aiInteractionService.create({ - userId: aiInteraction.user.id, - threadId: aiInteraction.threadId, - parentInteractionId: aiInteraction.id, - role: 'gen-ai', - message: '-', - contentType: aiResponse.content_type, - errorMessage: errorsStr, - modelUsed: aiResponse.model, - responseTimeMs: aiResponse.duration_ms, - metadata: JSON.stringify(aiResponse), - isApplied: aiInteraction.isApplied, - status: aiResponse.success ? 'succeeded' : 'failed' - }); - - throw new Error(errorsStr); - } - else { - let nestedResponse = aiResponse.response.trim(); - - const genAiInteraction = await this.aiInteractionService.create({ - userId: aiInteraction.user.id, - threadId: aiInteraction.threadId, - parentInteractionId: aiInteraction.id, - role: 'gen-ai', - message: nestedResponse, - contentType: aiResponse.content_type, - errorMessage: '', - modelUsed: aiResponse.model, - responseTimeMs: aiResponse.duration_ms, - metadata: JSON.stringify(aiResponse), - isApplied: aiInteraction.isApplied, - status: aiResponse.success ? 'succeeded' : 'failed' - }); - - if (aiInteraction.isAutoApply) { - this.aiInteractionService.applySolidAiInteraction(genAiInteraction.id); - } - } - - return aiResponse; - } -} diff --git a/src/repository/ai-interaction.repository.ts b/src/repository/ai-interaction.repository.ts deleted file mode 100644 index ae8c224a..00000000 --- a/src/repository/ai-interaction.repository.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { AiInteraction } from 'src/entities/ai-interaction.entity'; -import { RequestContextService } from 'src/services/request-context.service'; -import { DataSource } from 'typeorm'; -import { SecurityRuleRepository } from './security-rule.repository'; -import { SolidBaseRepository } from './solid-base.repository'; - -@Injectable() -export class AiInteractionRepository extends SolidBaseRepository { - constructor( - readonly dataSource: DataSource, - readonly requestContextService: RequestContextService, - readonly securityRuleRepository: SecurityRuleRepository, - ) { - super(AiInteraction, dataSource, requestContextService, securityRuleRepository); - } -} \ No newline at end of file diff --git a/src/seeders/seed-data/solid-core-metadata.json b/src/seeders/seed-data/solid-core-metadata.json index e92ca711..3927f551 100755 --- a/src/seeders/seed-data/solid-core-metadata.json +++ b/src/seeders/seed-data/solid-core-metadata.json @@ -4538,263 +4538,6 @@ } ] }, - { - "singularName": "aiInteraction", - "pluralName": "aiInteractions", - "displayName": "AI Interaction", - "description": "Stores user and assistant messages in an AI chat session.", - "tableName": "ss_ai_interactions", - "dataSource": "default", - "dataSourceType": "postgres", - "userKeyFieldUserKey": "externalId", - "isSystem": false, - "fields": [ - { - "name": "user", - "displayName": "User", - "type": "relation", - "required": false, - "unique": false, - "index": true, - "private": false, - "encrypt": false, - "relationType": "many-to-one", - "relationCoModelSingularName": "user", - "relationCreateInverse": true, - "relationCascade": "cascade", - "relationModelModuleName": "solid-core", - "isSystem": true - }, - { - "name": "externalId", - "displayName": "External ID", - "description": "Used to track using a reference number of each ai interaction.", - "type": "computed", - "ormType": "varchar", - "isSystem": false, - "computedFieldValueType": "string", - "computedFieldTriggerConfig": [ - { - "modelName": "aiInteraction", - "moduleName": "solid-core", - "operations": [ - "before-insert" - ] - } - ], - "computedFieldValueProvider": "AlphaNumExternalIdComputationProvider", - "computedFieldValueProviderCtxt": "{\n \"prefix\": \"AI\",\n \"length\": \"10\"\n}", - "required": true, - "unique": true, - "index": true, - "private": false, - "encrypt": false, - "encryptionType": null, - "decryptWhen": null, - "columnName": null, - "isUserKey": true - }, - { - "name": "threadId", - "displayName": "Thread ID", - "type": "shortText", - "ormType": "varchar", - "length": 128, - "required": true, - "unique": false, - "index": true, - "private": false, - "encrypt": false - }, - { - "name": "parentInteraction", - "displayName": "Parent Interaction", - "type": "relation", - "required": false, - "unique": false, - "index": true, - "private": false, - "encrypt": false, - "relationType": "many-to-one", - "relationCoModelSingularName": "aiInteraction", - "relationCreateInverse": false, - "relationCascade": "set null", - "relationModelModuleName": "solid-core", - "isSystem": true - }, - { - "name": "role", - "displayName": "Role", - "type": "shortText", - "ormType": "varchar", - "length": 32, - "required": true, - "unique": false, - "index": false, - "private": false, - "encrypt": false - }, - { - "name": "message", - "displayName": "Message", - "type": "longText", - "ormType": "text", - "required": true, - "unique": false, - "index": false, - "private": false, - "encrypt": false - }, - { - "name": "originalMessage", - "displayName": "Original Message", - "type": "longText", - "ormType": "text", - "required": false, - "unique": false, - "index": false, - "private": false, - "encrypt": false - }, - { - "name": "contentType", - "displayName": "Content Type", - "type": "shortText", - "ormType": "varchar", - "length": 64, - "required": false, - "unique": false, - "index": false, - "private": false, - "encrypt": false - }, - { - "name": "status", - "displayName": "Status", - "type": "selectionStatic", - "ormType": "varchar", - "length": 64, - "required": false, - "unique": false, - "index": true, - "private": false, - "encrypt": false, - "selectionStaticValues": [ - "pending:Pending", - "failed:Failed", - "succeeded:Succeeded" - ] - }, - { - "name": "errorMessage", - "displayName": "Error Message", - "type": "longText", - "ormType": "text", - "required": false, - "unique": false, - "index": false, - "private": true, - "encrypt": false - }, - { - "name": "modelUsed", - "displayName": "Model Used", - "type": "shortText", - "ormType": "varchar", - "length": 128, - "required": false, - "unique": false, - "index": false, - "private": false, - "encrypt": false - }, - { - "name": "responseTimeMs", - "displayName": "Response Time (ms)", - "type": "int", - "ormType": "integer", - "required": false, - "unique": false, - "index": false, - "private": false, - "encrypt": false - }, - { - "name": "metadata", - "displayName": "Metadata", - "type": "json", - "ormType": "simple-json", - "required": false, - "unique": false, - "index": false, - "private": false, - "encrypt": false - }, - { - "name": "isApplied", - "displayName": "Is Applied", - "type": "boolean", - "required": false, - "unique": false, - "index": false, - "private": false, - "encrypt": false - }, - { - "name": "isEdited", - "displayName": "Is Edited", - "type": "boolean", - "required": false, - "unique": false, - "index": false, - "private": false, - "encrypt": false - }, - { - "name": "isAutoApply", - "displayName": "Is Auto Apply", - "type": "boolean", - "required": false, - "unique": false, - "index": false, - "private": false, - "encrypt": false - }, - { - "name": "inputTokens", - "displayName": "Input Tokens", - "type": "int", - "ormType": "integer", - "required": false, - "unique": false, - "index": false, - "private": false, - "encrypt": false - }, - { - "name": "outputTokens", - "displayName": "Output Tokens", - "type": "int", - "ormType": "integer", - "required": false, - "unique": false, - "index": false, - "private": false, - "encrypt": false - }, - { - "name": "totalTokens", - "displayName": "Total Tokens", - "type": "int", - "ormType": "integer", - "required": false, - "unique": false, - "index": false, - "private": false, - "encrypt": false - } - ] - }, { "singularName": "modelSequence", "tableName": "ss_model_sequence", @@ -6041,19 +5784,6 @@ "moduleUserKey": "solid-core", "modelUserKey": "setting" }, - { - "displayName": "AI Interactions", - "name": "aiInteraction-list-action", - "type": "solid", - "domain": "", - "context": "", - "customComponent": "", - "customIsModal": true, - "serverEndpoint": "", - "viewUserKey": "aiInteraction-list-view", - "moduleUserKey": "solid-core", - "modelUserKey": "aiInteraction" - }, { "displayName": "Model Sequence List Action", "name": "modelSequence-list-action", @@ -6564,14 +6294,6 @@ "moduleUserKey": "solid-core", "parentMenuItemUserKey": "other-menu-item" }, - { - "displayName": "AI Interactions", - "name": "aiInteraction-menu-item", - "sequenceNumber": 3, - "actionUserKey": "aiInteraction-list-action", - "moduleUserKey": "solid-core", - "parentMenuItemUserKey": "other-menu-item" - }, { "displayName": "Saved Filters", "name": "savedFilters-menu-item", @@ -12300,323 +12022,6 @@ ] } }, - { - "name": "aiInteraction-list-view", - "displayName": "AI Interaction", - "type": "list", - "context": "{}", - "moduleUserKey": "solid-core", - "modelUserKey": "aiInteraction", - "layout": { - "type": "list", - "attrs": { - "pagination": true, - "pageSizeOptions": [ - 10, - 25, - 50 - ], - "enableGlobalSearch": true, - "create": false, - "edit": false, - "delete": false - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "user" - } - }, - { - "type": "field", - "attrs": { - "name": "threadId" - } - }, - { - "type": "field", - "attrs": { - "name": "externalId" - } - }, - { - "type": "field", - "attrs": { - "name": "parentInteraction" - } - }, - { - "type": "field", - "attrs": { - "name": "role" - } - }, - { - "type": "field", - "attrs": { - "name": "message" - } - }, - { - "type": "field", - "attrs": { - "name": "status" - } - }, - { - "type": "field", - "attrs": { - "name": "contentType" - } - }, - { - "type": "field", - "attrs": { - "name": "responseTimeMs" - } - }, - { - "type": "field", - "attrs": { - "name": "inputTokens" - } - }, - { - "type": "field", - "attrs": { - "name": "outputTokens" - } - }, - { - "type": "field", - "attrs": { - "name": "totalTokens" - } - } - ] - } - }, - { - "name": "aiInteraction-form-view", - "displayName": "AI Interaction", - "type": "form", - "context": "{}", - "moduleUserKey": "solid-core", - "modelUserKey": "aiInteraction", - "layout": { - "type": "form", - "edit": false, - "attrs": { - "name": "form-1", - "label": "AI Interaction", - "className": "grid" - }, - "children": [ - { - "type": "sheet", - "attrs": { - "name": "sheet-1" - }, - "children": [ - { - "type": "notebook", - "attrs": { - "name": "notebook-1" - }, - "children": [ - { - "type": "page", - "attrs": { - "name": "general", - "label": "General Info" - }, - "children": [ - { - "type": "row", - "attrs": { - "name": "row-general" - }, - "children": [ - { - "type": "column", - "attrs": { - "name": "left-col", - "label": "Interaction Details", - "className": "col-12 sm:col-12 md:col-6 lg:col-6" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "user" - } - }, - { - "type": "field", - "attrs": { - "name": "externalId" - } - }, - { - "type": "field", - "attrs": { - "name": "threadId" - } - }, - { - "type": "field", - "attrs": { - "name": "role" - } - }, - { - "type": "field", - "attrs": { - "name": "message", - "viewWidget": "SolidAiInteractionMessageFieldFormWidget" - } - } - ] - }, - { - "type": "column", - "attrs": { - "name": "right-col", - "label": "Message Properties", - "className": "col-12 sm:col-12 md:col-6 lg:col-6" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "contentType" - } - }, - { - "type": "field", - "attrs": { - "name": "modelUsed" - } - }, - { - "type": "field", - "attrs": { - "name": "responseTimeMs" - } - }, - { - "type": "field", - "attrs": { - "name": "parentInteraction" - } - }, - { - "type": "field", - "attrs": { - "name": "inputTokens" - } - }, - { - "type": "field", - "attrs": { - "name": "outputTokens" - } - }, - { - "type": "field", - "attrs": { - "name": "totalTokens" - } - } - ] - } - ] - } - ] - }, - { - "type": "page", - "attrs": { - "name": "status", - "label": "Status & Debug" - }, - "children": [ - { - "type": "row", - "attrs": { - "name": "row-status" - }, - "children": [ - { - "type": "column", - "attrs": { - "name": "col-status", - "label": "Execution Info", - "className": "col-12" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "status" - } - }, - { - "type": "field", - "attrs": { - "name": "errorMessage", - "rows": 5 - } - } - ] - } - ] - } - ] - }, - { - "type": "page", - "attrs": { - "name": "meta", - "label": "System Metadata" - }, - "children": [ - { - "type": "row", - "attrs": { - "name": "row-meta" - }, - "children": [ - { - "type": "column", - "attrs": { - "name": "col-meta", - "label": "", - "className": "col-12" - }, - "children": [ - { - "type": "field", - "attrs": { - "name": "metadata", - "height": "80vh", - "viewWidget": "SolidAiInteractionMetadataFieldFormWidget" - } - } - ] - } - ] - } - ] - } - ] - } - ] - } - ] - } - }, { "name": "importTransactionErrorLog-list-view", "displayName": "Import Error Logs", diff --git a/src/services/ai-interaction.service.ts b/src/services/ai-interaction.service.ts deleted file mode 100644 index ff1c4b3c..00000000 --- a/src/services/ai-interaction.service.ts +++ /dev/null @@ -1,239 +0,0 @@ -import { BadRequestException, Injectable, Logger } from '@nestjs/common'; -import { ModuleRef } from "@nestjs/core"; -import { InjectEntityManager } from '@nestjs/typeorm'; -import { EntityManager } from 'typeorm'; -import type { SolidCoreSetting } from "src/services/settings/default-settings-provider.service"; - -import { spawn } from 'child_process'; -import * as fs from 'fs/promises'; -import { ERROR_MESSAGES } from 'src/constants/error-messages'; -import { InvokeAiPromptDto } from 'src/dtos/invoke-ai-prompt.dto'; -import { McpResponse, TriggerMcpClientOptions } from 'src/interfaces'; -import { AiInteractionRepository } from 'src/repository/ai-interaction.repository'; -import { CRUDService } from 'src/services/crud.service'; -import { AiInteraction } from '../entities/ai-interaction.entity'; -import { McpHandlerFactory } from './genai/mcp-handlers/mcp-handler-factory.service'; -import { PublisherFactory } from './queues/publisher-factory.service'; - -@Injectable() -export class AiInteractionService extends CRUDService { - private readonly logger = new Logger(AiInteractionService.name); - - constructor( - @InjectEntityManager() - readonly entityManager: EntityManager, - // @InjectRepository(AiInteraction, 'default') - // readonly repo: Repository, - readonly repo: AiInteractionRepository, - readonly moduleRef: ModuleRef, - readonly publisherFactory: PublisherFactory, - // readonly requestContextService: RequestContextService, - readonly mcpHandlerFactory: McpHandlerFactory, - - ) { - super(entityManager, repo, 'aiInteraction', 'solid-core', moduleRef); - } - - async triggerMcpClientJob(dto: InvokeAiPromptDto, userId: number, isAutoApply: boolean = false, threadId: string = null): Promise { - // const activeUser: ActiveUserData = this.requestContextService.getActiveUser(); - - const aiInteraction = await this.create({ - userId: userId, - threadId: threadId ? threadId : `thread-${userId}`, - role: 'human', - message: dto.prompt, - contentType: '', - errorMessage: '', - modelUsed: '', - responseTimeMs: 0, - metadata: '', - isAutoApply: isAutoApply - }); - const m = { - payload: { - aiInteractionId: aiInteraction.id, - moduleName: dto.moduleName - }, - parentEntity: 'aiInteraction', - parentEntityId: aiInteraction.id, - }; - - const queueMessageId = await this.publisherFactory.publish(m, 'TriggerMcpClientPublisher'); - - return { - queueMessageId: queueMessageId, - aiInteractionId: aiInteraction.id - } - } - - /** - * Runs the Python MCP client with a prompt and returns the parsed JSON embedded in the 'response'. - * @param prompt - The question or instruction to send to the MCP client. - * @returns The parsed object inside the 'response' field of the JSON output. - */ - async runMcpPrompt(prompt: string): Promise { - const pythonExecutable = this.settingService.getConfigValue('mcpPythonExecutable'); - const mcpClient = this.settingService.getConfigValue('mcpClient'); - - // TODO: We can return an error if the above env variables are not properly setup... - if (!pythonExecutable || !mcpClient) { - throw new BadRequestException(ERROR_MESSAGES.PYTHON_EXECUTABLE_NOT_CONFIGURED); - } - - // Check if both paths are valid and accessible - try { - const [pyStat, clientStat] = await Promise.all([ - fs.stat(pythonExecutable), - fs.stat(mcpClient), - ]); - - if (!pyStat.isFile()) { - throw new BadRequestException(`MCP_PYTHON_EXECUTABLE path is not a file: ${pythonExecutable}`); - } - - if (!clientStat.isFile()) { - throw new BadRequestException(`MCP_CLIENT path is not a file: ${mcpClient}`); - } - - } catch (err: any) { - throw new BadRequestException(`Invalid MCP executable or client path: ${err.message}`); - } - - // TODO: Refactor to use the command.service.ts instead... - return new Promise((resolve, reject) => { - this.logger.log(`Attempting to run command:`) - this.logger.log(`${pythonExecutable} ${mcpClient} "${prompt}"`); - - const python = spawn(pythonExecutable, [mcpClient, `"${prompt}"`]); - - let stdout = ''; - let stderr = ''; - - python.stdout.on('data', (data) => { - stdout += data.toString(); - }); - - python.stderr.on('data', (data) => { - stderr += data.toString(); - }); - - python.on('close', (code) => { - this.logger.log(`Python script exited with code ${code}`); - - if (code !== 0) { - this.logger.error(`Python script exited with a non-zero exit code: ${stderr}`); - return reject(new Error(`Python script exited with a non-zero exit code: ${stderr}`)); - } - - try { - this.logger.log(`Python script exited with zero exit code: ${stdout}`); - const raw: McpResponse = JSON.parse(stdout); - - // Sometimes the raw.response might not be a valid json - // TODO: examine the content type of the raw response.. - // if (raw.content_type==='json') { - // } - let parsedResponse = raw.response; - try { - parsedResponse = JSON.parse(raw.response); - } - catch (ex) { - this.logger.warn(`Attempting to parse mcp client response assuming it is JSON, however it is not: ${parsedResponse}`); - // raw.success = false - } - // Parse the response string into an object - // const parsedResponse = JSON.parse(raw.response); - - // Replace the string with the parsed object - const enrichedRaw = { - ...raw, - response: parsedResponse, - }; - // if (!raw.success) { - // return reject(new Error(`MCP error: ${raw.errors?.join(', ')}`)); - // } - // let cleaned = raw.response.trim(); - - // Don't need to re-parse this... - // const parsed = JSON.parse(cleaned); - // resolve(cleaned); - - resolve(enrichedRaw); - } catch (err: any) { - reject(new Error(`Mcp Invocation Failed: ${err.message}`)); - } - }); - }); - } - - cleanResponse(response: string) { - this.logger.log(`mcp server response is: ${response}`); - - // Remove markdown-style code block wrapper - if (response.startsWith('```json')) { - response = response.replace(/^```json/, '').trim(); - } - if (response.endsWith('```')) { - response = response.replace(/```$/, '').trim(); - } - this.logger.log(`mcp server response after removing doc tags is: ${response}`); - - return response; - } - - async applySolidAiInteraction(id: number) { - // Fetch the aiInteraction - const aiInteraction = await this.findOne(id, { - populate: ['user'] - }); - if (!aiInteraction) { - const m = `Unable to identified the aiInteraction entry that triggered this job... using id: ${id}` - - // TODO: RESPONSE SHAPE ALERT Check if we want to control the shape of the response.... - throw new Error(m); - } - - // TODO: Validation: Check if JSON.parse(metadata).tools_invoked starts with solid_ - let metadata: any = {}; - try { - if (typeof aiInteraction.metadata === "string") { - metadata = JSON.parse(aiInteraction.metadata); - } else if (typeof aiInteraction.metadata === "object" && aiInteraction.metadata !== null) { - metadata = aiInteraction.metadata; - } else { - // optional fallback - metadata = {}; - } - } catch (e) { - // TODO: RESPONSE SHAPE ALERT Check if we want to control the shape of the response.... - throw new Error(`Invalid metadata JSON: ${e}`); - } - - const toolsInvoked = metadata['tools_invoked']; - if (!toolsInvoked) { - // TODO: RESPONSE SHAPE ALERT Check if we want to control the shape of the response.... - throw new Error(ERROR_MESSAGES.UNABLE_TO_RESOLVE_SOLID_COMMAND); - } - - // TODO: OPTIMISATION for chained tool invocation, for now we are assuming only 1 tool was used. - const toolInvoked = toolsInvoked[0]; - - // TODO: use the toolInvoked to identify a service using some convention. - // TODO: Eg. if toolInvoked is solid_create_module <> SolidCreateModuleMcpToolHandler ... create a factory class to do this mapping and identify the relevant provider. - const mcpToolHandler = this.mcpHandlerFactory.getInstance(toolInvoked); - if (!mcpToolHandler) { - // TODO: RESPONSE SHAPE ALERT Check if we want to control the shape of the response.... - throw new Error(ERROR_MESSAGES.UNABLE_TO_RESOLVE_MCP_HANDLER); - } - - const handlerApplicationResponse = await mcpToolHandler.apply(aiInteraction); - - // TODO: This provider to implement an interface - IMcpToolResponseHandler ... apply(aiInteraction: AiInteraction) - // throw new Error('Method not implemented.'); - - // Mark the interaction as applied - await this.update(aiInteraction.id, { isApplied: true }, [], true); - - return handlerApplicationResponse; - } -} diff --git a/src/services/genai/ingest-metadata.service.ts b/src/services/genai/ingest-metadata.service.ts deleted file mode 100644 index 8f1b997c..00000000 --- a/src/services/genai/ingest-metadata.service.ts +++ /dev/null @@ -1,798 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import * as crypto from 'crypto'; -import * as fs from 'fs'; -import * as path from 'path'; - -import { getDynamicModuleNames, getDynamicModuleNamesBasedOnMetadata } from 'src/helpers/module.helper'; -import { CreateModuleMetadataDto } from 'src/dtos/create-module-metadata.dto'; -import { R2RHelperService } from './r2r-helper.service'; -import { r2rClient } from 'r2r-js'; -import { CreateModelMetadataDto } from 'src/dtos/create-model-metadata.dto'; -import { CreateFieldMetadataDto } from 'src/dtos/create-field-metadata.dto'; - -export type FieldIngestionInfo = { - fieldName: string; - fieldChunkId?: string; - fieldHash?: string; -}; - -export type ModelIngestionInfo = { - modelName: string; - modelChunkId?: string; - modelHash?: string; - fields: FieldIngestionInfo[]; -}; - -export type ModuleRAGIngestionInfo = { - moduleName?: string; - collectionId?: string; - - // Full json document is also uploaded so we track references... - documentId?: string; - documentHash?: string; - - // module references - moduleChunkId?: string; - // track a hash of module metadata to skip unchanged - moduleHash?: string; - - // model references... - models: ModelIngestionInfo[]; -}; - -@Injectable() -export class IngestMetadataService { - private readonly logger = new Logger(IngestMetadataService.name); - private ragClient: r2rClient; - - constructor( - private readonly r2rService: R2RHelperService, - ) { } - - // Stable stringify so hashes/ids don't flap - // private stableStringify(obj: any): string { - // return JSON.stringify(obj, Object.keys(obj).sort(), 2); - // } - // private sha256(input: string): string { - // return crypto.createHash('sha256').update(input).digest('hex'); - // } - // private hashSchema(obj: any): string { - // return this.sha256(this.stableStringify(obj)); - // } - private _sha256OfJson(obj: any): string { - const s = JSON.stringify(obj); - return crypto.createHash('sha256').update(s).digest('hex'); - } - // // Small natural-language one-liners for relations - // private relationSig(model: any): string[] { - // const rels: string[] = []; - // for (const f of model.fields ?? []) { - // if (f.relation?.targetModel) { - // rels.push(`${model.singularName}.${f.name} -> ${f.relation.targetModel}(${f.relation.targetField ?? 'id'})`); - // } - // } - // return rels; - // } - - private _oneLineBool(b?: boolean): 'yes' | 'no' { - return b ? 'yes' : 'no'; - } - - private _shortList(arr?: string[] | null, max = 10): string { - if (!Array.isArray(arr) || arr.length === 0) return 'none'; - const a = arr.slice(0, max); - return `${a.join(', ')}${arr.length > max ? ', …' : ''}`; - } - - async ingest() { - // Create a new ragClient... - this.ragClient = await this.r2rService.getClient(); - - // const allModuleMetadataJson = []; - this.logger.debug(`getting dynamics modules`); - // const enabledModules = getDynamicModuleNames(); - const enabledModules = getDynamicModuleNamesBasedOnMetadata(); - this.logger.log(`ingesting metadata`); - - for (let i = 0; i < enabledModules.length; i++) { - const enabledModule = enabledModules[i]; - const fileName = `${enabledModule}-metadata.json`; - const enabledModuleSeedFile = `src/${enabledModule}/metadata/${fileName}`; - const fullPath = path.join(process.cwd(), enabledModuleSeedFile); - const overallMetadata: any = JSON.parse(fs.readFileSync(fullPath, 'utf-8').toString()); - - const moduleMetadata: CreateModuleMetadataDto = overallMetadata.moduleMetadata; - - // Manage all the ingestion info file paths... - const enabledModulIngestionInfoFile = `src/${enabledModule}/metadata/genai/${enabledModule}-ingested-info.json`; - const enabledModulIngestionInfoFullPath = path.join(process.cwd(), enabledModulIngestionInfoFile); - const ingestionInfo: ModuleRAGIngestionInfo = fs.existsSync(enabledModulIngestionInfoFullPath) ? JSON.parse(fs.readFileSync(enabledModulIngestionInfoFullPath, 'utf-8').toString()) : {}; - const enabledModulIngestionInfoDir = path.dirname(enabledModulIngestionInfoFullPath); - if (!fs.existsSync(enabledModulIngestionInfoDir)) { - fs.mkdirSync(enabledModulIngestionInfoDir, { recursive: true }); - } - - ingestionInfo.moduleName = enabledModule - - // Process module metadata first. - this.logger.log(`[Start] Processing module metadata for ${moduleMetadata.name}`) - - // Create or use an existing collection... - const collectionId = await this.resolveRagCollectionForModule(ingestionInfo, enabledModule) - ingestionInfo.collectionId = collectionId; - - // Delete and re-insert a document representing the full json... - // await this.deleteInsertRagDocumentForModuleMetadataJsonFile(ingestionInfo, fullPath, fileName) - - // Delete all metadata chunks. - // const collectionName = `${enabledModule}-rag-collection`; - // const deleteResult = await this.ragClient.documents.deleteByFilter({ - // filters: { - // // AND semantics across fields: - // "$and": [ - // { 'metadata.kind': { $eq: 'solidx-metadata' } }, - // { 'metadata.collectionName': { $eq: collectionName } }, - // { 'title': { $like: '%Ingested Chunks%' } }, - // ] - // }, - // }); - // this.logger.log(`Deleted existing documents for collectionName: ${collectionName}. Got response:`, deleteResult); - - // Delete and re-insert a chunk representing the module. - await this.deleteInsertRagChunkForModule(ingestionInfo, moduleMetadata); - - // Delete and re-insert chunks representing each model. - for (const model of moduleMetadata.models) { - await this.deleteInsertRagChunkForModel(ingestionInfo, enabledModule, model); - - // Disabling this for now... - // for (const field of model.fields) { - // await this.deleteInsertRagChunkForField(ingestionInfo, enabledModule, model.singularName, field); - // } - } - - // TODO: Delete and re-insert chunks representing roles - - // TODO: Delete and re-insert chunks representing menus - - // TODO: Delete and re-insert chunks representing actions - - // TODO: Delete and re-insert chunks representing list views - - // TODO: Delete and re-insert chunks representing kanban views - - // TODO: Delete and re-insert chunks representing form views - - // TODO: Delete and re-insert chunks representing security rules - - // TODO: Delete and re-insert chunks representing scheduled jobs - - // Save ingestion info to disk... - fs.writeFileSync(enabledModulIngestionInfoFullPath, JSON.stringify({ ...ingestionInfo }, null, 2), 'utf8'); - } - } - - private async resolveRagCollectionForModule(ingestionInfo: ModuleRAGIngestionInfo, moduleName: string): Promise { - this.logger.debug(`Resolving RAG collection for module: ${moduleName}`); - - const collectionName = `${moduleName}-rag-collection`; - - // delete and recreate - // if (alwaysRecreate === true) { - const existingCollection = await this.ragClient.collections.retrieveByName({ - name: collectionName - }); - - if (existingCollection) { - try { - await this.ragClient.collections.delete({ id: existingCollection.results.id }); - } catch (e) { - this.logger.warn(`[Warn] Failed deleteByFilter for collection': ${String(e)}`); - } - } - // } - - // let existingCollection: CollectionResponse = null; - // if (ingestionInfo.collectionId) { - // // See if collection already exists... - // const r = await this.ragClient.collections.list({ - // ids: [ - // ingestionInfo.collectionId - // ] - // }); - - // if (r) { - // if (r.results.length === 1) { - // existingCollection = r.results[0]; - // } - // if (r.results.length > 1) { - // // TODO: do something that will print a meaningful error on the console... - // } - // } - // } - - // if (!existingCollection) { - const r = await this.ragClient.collections.create({ - name: collectionName, - description: `Collection created to group all documents, chunks related to module: ${moduleName}` - }); - - // TODO: for some reason if we are unable to create a collection then fail with a visible error message in the console... - return r.results.id; - // } - // return existingCollection.id; - } - - private async deleteInsertRagDocumentForModuleMetadataJsonFile(ingestionInfo: ModuleRAGIngestionInfo, fullPath: string, fileName: string): Promise { - this.logger.debug(`Ingesting file: ${fullPath}`); - - // 1) Compute hash of the entire JSON string (as-is) - const jsonStr = fs.readFileSync(fullPath, 'utf-8'); - const contentHash = this._sha256OfJson(JSON.parse(jsonStr)); - - // 2) Short-circuit if unchanged and we still have a documentId - if (ingestionInfo.documentHash === contentHash && ingestionInfo.documentId) { - this.logger.log(`[Skip] Unchanged: ${fileName} (hash=${contentHash.slice(0, 8)}…)`); - return; - // return ingestionInfo.documentId; - } - - // 3) Delete the previous doc if present - if (ingestionInfo.documentId) { - try { - await this.ragClient.documents.delete({ id: ingestionInfo.documentId }); - } catch (e) { - this.logger.warn( - `[Warn] Failed deleting prior document ${ingestionInfo.documentId}: ${String(e)}` - ); - } - } - - // 4) Create a fresh document; attach the hash into metadata for traceability - const ingestResult = await this.ragClient.documents.create({ - file: { - path: fullPath, - name: fileName - }, - collectionIds: [ingestionInfo.collectionId], - metadata: { - contentHash, - fileName - }, - }); - // console.log("file ingest result:", JSON.stringify(ingestResult, null, 2)); - - const newId = ingestResult?.results?.documentId; - if (!newId) { - throw new Error(`R2R did not return a documentId for ${fileName}`); - } - - // 5) Persist identifiers + hash on our side - ingestionInfo.documentId = newId; - ingestionInfo.documentHash = contentHash; - - this.logger.log(`[OK] Ingested ${fileName} → id=${newId}, hash=${contentHash.slice(0, 8)}…`); - // return newId; - - } - - private async deleteInsertRagChunkForModule(ingestionInfo: ModuleRAGIngestionInfo, moduleMetadata: CreateModuleMetadataDto): Promise { - const moduleName: string = moduleMetadata?.name; - - // Hash the meaningful parts of the module to detect changes and skip re-ingest. - const schemaHash = this._sha256OfJson({ - name: moduleMetadata?.name ?? null, - description: moduleMetadata?.description ?? null, - - // Keep model names + brief shape so module-level hash changes when models change. - models: (moduleMetadata?.models ?? []).map((m: any) => ({ - singularName: m?.singularName ?? null, - description: m?.description ?? null, - - // Include field names to detect field-level changes at module granularity - maybe remove this later? - // fields: Array.isArray(m?.fields) ? m.fields.map((f: any) => f?.name ?? null) : [], - })), - }); - - // Skip unchanged module - if (ingestionInfo.moduleHash === schemaHash && ingestionInfo.moduleChunkId) { - this.logger.log(`[Skip] Module unchanged: ${moduleName}`); - return; - // return ingestionInfo.moduleChunkId; - } - - const models: any[] = moduleMetadata?.models ?? []; - const modelLines = models.map((m) => { - const name = m?.singularName; - const desc = m?.description ? `: ${m.description}` : ''; - return `- ${name}${desc}`; - }); - - const text = `SolidX Module: ${moduleName} -Purpose: ${moduleMetadata?.description ?? 'N/A'} - -Models (${models.length}): -${modelLines.join('\n')} - -Usage: Use this chunk to choose the correct model/field chunks for code generation or metadata edits.`; - - // metadata has to be concise and queryable - const metadata = { - collectionName: `${moduleName}-rag-collection`, - kind: 'solidx-metadata', - type: 'module', - moduleName, - modelCount: models.length, - schemaHash, - models: models.map((m) => m?.singularName).filter(Boolean), - }; - - // Delete previous chunk if we have one - // if (ingestionInfo.moduleChunkId) { - // try { - // await this.ragClient.documents.delete({ id: ingestionInfo.moduleChunkId }); - // } catch (e) { - // this.logger.warn(`[Warn] Failed deleting old module chunk (${ingestionInfo.moduleChunkId}): ${String(e)}`); - // } - // } - - // We changed the approach to delete and re-create using metadata. - // delete any existing module records by metadata (idempotent) - try { - const deleteResult = await this.ragClient.documents.deleteByFilter({ - filters: { - // AND semantics across fields: - "$and": [ - { 'metadata.kind': { $eq: 'solidx-metadata' } }, - { 'metadata.type': { $eq: 'module' } }, - { 'metadata.moduleName': { $eq: moduleName } }, - // (optional but nice to constrain tightly if you always set it) - { 'metadata.collectionName': { $eq: `${moduleName}-rag-collection` } }, - ] - }, - }); - - this.logger.log(`Deleted existing module document for moduleName: ${moduleName}. Got response:`, deleteResult); - - } catch (e) { - this.logger.warn(`[Warn] Failed deleteByFilter for module '${moduleName}': ${String(e)}`); - // Non-fatal: we can still proceed to create; caller’s auth will limit scope anyway - } - - const r = await this.ragClient.documents.create({ - chunks: [text], - // raw_text: text, - metadata: metadata, - collectionIds: [ingestionInfo.collectionId], - }); - - const newId = r?.results?.documentId; - if (!newId) { - throw new Error(`R2R did not return a documentId while creating module chunk for module name ${moduleName}`); - } - - // Update ingestion info for persistence by the caller - ingestionInfo.moduleChunkId = r.results?.documentId; - ingestionInfo.moduleHash = schemaHash; - - this.logger.log(`[OK] Ingested module ${moduleName} → id=${newId}, hash=${schemaHash.slice(0, 8)}…`); - - } - - private async deleteInsertRagChunkForModel(ingestionInfo: ModuleRAGIngestionInfo, moduleName: string, model: CreateModelMetadataDto): Promise { - const modelName: string = model?.singularName; - - // 1) Hash full JSON (as-is) to detect changes and skip re-ingest - const schemaHash = this._sha256OfJson(model); - - // Ensure ingestionInfo.models[] has an entry for this model - const modelsArr = ingestionInfo.models ?? (ingestionInfo.models = []); - let modelEntry = modelsArr.find(m => m.modelName === modelName); - if (!modelEntry) { - modelEntry = { modelName, fields: [] }; - modelsArr.push(modelEntry); - } - - // 2) Short-circuit if unchanged - if (modelEntry.modelHash === schemaHash && modelEntry.modelChunkId) { - this.logger.log(`[Skip] Model unchanged: ${moduleName}.${modelName}`); - return; - // return modelEntry.modelChunkId; - } - - // 3) Build retrieval-friendly text (concise) - const fields: CreateFieldMetadataDto[] = Array.isArray(model?.fields) ? model.fields : []; - const userkey = fields.find((f: CreateFieldMetadataDto) => f?.isUserKey)?.name ?? 'id'; - const uniques = fields.filter((f: CreateFieldMetadataDto) => f?.unique).map((f: any) => f.name); - const required = fields.filter((f: CreateFieldMetadataDto) => f?.required).map((f: any) => f.name); - const rels = fields - .filter((f: CreateFieldMetadataDto) => f.type === 'relation') - .map((f: CreateFieldMetadataDto) => `${modelName}.${f.name} -> ${f.relationCoModelSingularName}.${f.relationCoModelColumnName ?? 'id'}`); - - const fieldSummaryLines = fields.slice(0, 30).map((f: CreateFieldMetadataDto) => { - const bits = [ - `${f.name}:${f.type}`, - f.required ? 'req' : '', - f.unique ? 'unique' : '', - f.isUserKey ? 'userkey' : '', - f.relationCoModelSingularName ? `rel->${f.relationCoModelSingularName}` : '', - ].filter(Boolean).join('|'); - return `- ${bits}`; - }); - - const text = - `SolidX Model: ${modelName} -Module: ${moduleName} -Purpose: ${model?.description ?? 'N/A'} - -Signature: -- Primary: ${userkey} -- Unique: ${uniques.length ? uniques.join(', ') : 'none'} -- Required (${required.length}): ${required.slice(0, 12).join(', ')}${required.length > 12 ? '…' : ''} - -Relations (${rels.length}): -${rels.length ? `- ${rels.join('\n- ')}` : 'None'} - -Fields (${fields.length}) [name:type|flags]: -${fieldSummaryLines.join('\n')} - -Usage: Use this chunk to generate DTOs, subscribers, custom service methods, and CRUD handlers for ${modelName}. - -Full model metadata json: -${JSON.stringify(model)} - -`; - - // 4) Metadata (concise & queryable) - const metadata = { - collectionName: `${moduleName}-rag-collection`, - kind: 'solidx-metadata', - type: 'model', - moduleName, - modelName, - fieldCount: fields.length, - requiredCount: required.length, - relationCount: rels.length, - userkey: userkey, - uniqueFields: uniques, - hasTimestamps: !!fields.find((f: CreateFieldMetadataDto) => ['time', 'date', 'datetime'].includes(f.type)), - schemaHash, - }; - - // 5) Delete previous chunk if present - // if (modelEntry.modelChunkId) { - // try { - // await this.ragClient.documents.delete({ id: modelEntry.modelChunkId }); - // } catch (e) { - // this.logger.warn(`[Warn] Failed deleting old model chunk (${modelEntry.modelChunkId}): ${String(e)}`); - // } - // } - - // We changed the approach to delete and re-create using metadata. - // delete any existing module records by metadata (idempotent) - try { - const deleteResult = await this.ragClient.documents.deleteByFilter({ - filters: { - // AND semantics across fields: - "$and": [ - { 'metadata.kind': { $eq: 'solidx-metadata' } }, - { 'metadata.type': { $eq: 'model' } }, - { 'metadata.moduleName': { $eq: moduleName } }, - { 'metadata.modelName': { $eq: modelName } }, - // (optional but nice to constrain tightly if you always set it) - { 'metadata.collectionName': { $eq: `${moduleName}-rag-collection` } }, - ] - }, - }); - - this.logger.log(`Deleted existing model document for modelName: ${modelName}. Got response:`, deleteResult); - - } catch (e) { - this.logger.warn(`[Warn] Failed deleteByFilter for module '${moduleName}': ${String(e)}`); - // Non-fatal: we can still proceed to create; caller’s auth will limit scope anyway - } - - // 6) Create new document (R2R auto-generates the ID) - const r = await this.ragClient.documents.create({ - chunks: [text], - metadata, - collectionIds: [ingestionInfo.collectionId], - }); - - const newId = r?.results?.documentId; - if (!newId) { - throw new Error(`R2R did not return a documentId while creating model chunk for ${moduleName}.${modelName}`); - } - - // 7) Update ingestionInfo for persistence - modelEntry.modelChunkId = newId; - modelEntry.modelHash = schemaHash; - - this.logger.log(`[OK] Ingested model ${moduleName}.${modelName} → id=${newId}, hash=${schemaHash.slice(0, 8)}…`); - // return newId; - } - - private _buildFieldTextAndMetadata(moduleName: string, modelName: string, f: CreateFieldMetadataDto) { - // Identity - const name = f?.name; - const displayName = f?.displayName ?? name; - const description = f?.description ?? 'N/A'; - - // Types - const type = f?.type; - const ormType = f?.ormType ?? null; - - // Constraints / validation - const required = !!f?.required; - const unique = !!f?.unique; - const index = !!f?.index; - const length = f?.length ?? null; - const min = f?.min ?? null; - const max = f?.max ?? null; - const defaultValue = f?.defaultValue ?? null; - const regex = f?.regexPattern ?? null; - const regexErr = f?.regexPatternNotMatchingErrorMsg ?? null; - - // Relation - const relationType = f?.relationType ?? null; // e.g., many-to-one, many-to-many, one-to-many - const relModule = f?.relationModelModuleName ?? null; - const relModel = f?.relationCoModelSingularName ?? null; - const relField = f?.relationCoModelFieldName ?? 'id'; - // const relOwner = f?.isRelationManyToManyOwner ?? null; - // const relJoinTable = f?.relationJoinTableName ?? null; - // const relCreateInverse = !!f?.relationCreateInverse; - const relCascade = f?.relationCascade ?? null; - const relFixedFilter = f?.relationFieldFixedFilter ?? null; - - // Selection (dropdowns) - const selectionDynProvider = f?.selectionDynamicProvider ?? null; - const selectionDynCtxt = f?.selectionDynamicProviderCtxt ?? null; - const selectionStatic = Array.isArray(f?.selectionStaticValues) ? f.selectionStaticValues : null; - const selectionValueType = f?.selectionValueType ?? null; - const isMultiSelect = !!f?.isMultiSelect; - - // Media (uploads) - const mediaTypes = Array.isArray(f?.mediaTypes) ? f.mediaTypes : null; - const mediaMaxSizeKb = f?.mediaMaxSizeKb ?? null; - const mediaStorageProvider = f?.mediaStorageProviderUserKey ?? null; // likely object/id in your JSON - - // Computed fields - const computedProvider = f?.computedFieldValueProvider ?? null; - const computedProviderCtxt = f?.computedFieldValueProviderCtxt ?? null; - const computedValueType = f?.computedFieldValueType ?? null; - const computedTriggerCfg = f?.computedFieldTriggerConfig ?? null; - - // Security / privacy / audit - // const encrypt = !!f?.encrypt; - // const encryptionType = f?.encryptionType ?? null; - // const decryptWhen = f?.decryptWhen ?? null; - const isPrivate = !!f?.private; - const enableAuditTracking = !!f?.enableAuditTracking; - - // Keys / system flags / mapping - const isUserKey = !!f?.isUserKey; - const isSystem = !!f?.isSystem; - const isMarkedForRemoval = !!f?.isMarkedForRemoval; - const columnName = f?.columnName ?? null; - const relCoModelColumn = f?.relationCoModelColumnName ?? null; - const uuid = f?.uuid ?? null; - - const relationSummary = (() => { - if (!relationType || !relModel) return 'none'; - const parts = [ - `type=${relationType}`, - relModule ? `module=${relModule}` : null, - `model=${relModel}`, - relField ? `field=${relField}` : null, - // relOwner !== null ? `m2mOwner=${relOwner}` : null, - // relCreateInverse ? 'createInverse=yes' : null, - relCascade ? `cascade=${relCascade}` : null, - // relJoinTable ? `joinTable=${relJoinTable}` : null, - relFixedFilter ? `fixedFilter=${relFixedFilter}` : null, - ].filter(Boolean); - return parts.join(', '); - })(); - - const selectionSummary = (() => { - const parts: string[] = []; - if (selectionDynProvider) parts.push(`dynamicProvider=${selectionDynProvider}`); - if (selectionDynCtxt) parts.push(`dynamicCtxt=${selectionDynCtxt}`); - if (selectionStatic?.length) parts.push(`static=[${this._shortList(selectionStatic, 12)}]`); - if (selectionValueType) parts.push(`valueType=${selectionValueType}`); - parts.push(`multiSelect=${this._oneLineBool(isMultiSelect)}`); - return parts.length ? parts.join(', ') : 'none'; - })(); - - const mediaSummary = (() => { - const parts: string[] = []; - if (mediaTypes?.length) parts.push(`types=[${this._shortList(mediaTypes, 12)}]`); - if (mediaMaxSizeKb) parts.push(`maxSizeKb=${mediaMaxSizeKb}`); - if (mediaStorageProvider) parts.push(`storageProvider=${typeof mediaStorageProvider === 'string' ? mediaStorageProvider : 'set'}`); - return parts.length ? parts.join(', ') : 'none'; - })(); - - const computedSummary = (() => { - const parts: string[] = []; - if (computedProvider) parts.push(`provider=${computedProvider}`); - if (computedProviderCtxt) parts.push(`providerCtxt=${computedProviderCtxt}`); - if (computedValueType) parts.push(`valueType=${computedValueType}`); - if (computedTriggerCfg?.length) parts.push(`triggers=${computedTriggerCfg.length}`); - return parts.length ? parts.join(', ') : 'none'; - })(); - - const securitySummary = [ - // `encrypt=${this.oneLineBool(encrypt)}`, - // encryptionType ? `encType=${encryptionType}` : null, - // decryptWhen ? `decryptWhen=${decryptWhen}` : null, - `private=${this._oneLineBool(isPrivate)}`, - `auditTracking=${this._oneLineBool(enableAuditTracking)}` - ].filter(Boolean).join(', '); - - const constraintSummary = [ - `required=${this._oneLineBool(required)}`, - `unique=${this._oneLineBool(unique)}`, - `index=${this._oneLineBool(index)}`, - length !== null ? `length=${length}` : null, - min !== null ? `min=${min}` : null, - max !== null ? `max=${max}` : null, - defaultValue !== null ? `default=${defaultValue}` : null, - regex ? `regex=${regex}${regexErr ? ` (${regexErr})` : ''}` : null - ].filter(Boolean).join(', '); - - const mappingSummary = [ - columnName ? `column=${columnName}` : null, - relCoModelColumn ? `relColumn=${relCoModelColumn}` : null, - uuid ? `uuid=${uuid}` : null, - `userKey=${this._oneLineBool(isUserKey)}`, - `system=${this._oneLineBool(isSystem)}`, - `markedForRemoval=${this._oneLineBool(isMarkedForRemoval)}` - ].filter(Boolean).join(', '); - - const text = [ - `SolidX Field: ${name} (${displayName})`, - `Model: ${modelName}`, - `Module: ${moduleName}`, - ``, - `Type: ${type}${ormType ? ` (orm=${ormType})` : ''}`, - `Description: ${description}`, - ``, - `Constraints: ${constraintSummary || 'none'}`, - `Relation: ${relationSummary}`, - `Selection: ${selectionSummary}`, - `Media: ${mediaSummary}`, - `Computed: ${computedSummary}`, - `Security/Privacy/Audit: ${securitySummary}`, - `Mapping/Flags: ${mappingSummary || 'none'}`, - ``, - `Usage: Use this chunk to generate exact field contracts (DTO, form control, DB column), `, - `validation rules, relation wiring, and UI widgets (selection/media/computed).`, - ].join('\n'); - - const metadata = { - kind: 'solidx-metadata', - type: 'field', - moduleName, - modelName, - fieldName: name, - displayName, - description, - dataType: type, - ormType, - required, - unique, - index, - defaultValue, - length, - min, - max, - regexPattern: regex, - regexPatternNotMatchingErrorMsg: regexErr, - - // relation - relationType, - relationModelModuleName: relModule, - relationCoModelSingularName: relModel, - relationCoModelFieldName: relField, - // isRelationManyToManyOwner: relOwner, - // relationJoinTableName: relJoinTable, - // relationCreateInverse: relCreateInverse, - relationCascade: relCascade, - relationFieldFixedFilter: relFixedFilter, - - // selection - selectionDynProvider, - selectionDynCtxt, - selectionStaticValues: selectionStatic, - selectionValueType, - isMultiSelect, - - // media - mediaTypes, - mediaMaxSizeKb, - mediaStorageProvider: mediaStorageProvider ? (typeof mediaStorageProvider === 'string' ? mediaStorageProvider : 'set') : null, - - // computed - computedFieldValueProvider: computedProvider, - computedFieldValueProviderCtxt: computedProviderCtxt, - computedFieldValueType: computedValueType, - computedFieldTriggerConfigCount: Array.isArray(computedTriggerCfg) ? computedTriggerCfg.length : 0, - - // security/privacy/audit - // encrypt, - // encryptionType, - // decryptWhen, - private: isPrivate, - enableAuditTracking, - - // mapping/flags - columnName, - relationCoModelColumnName: relCoModelColumn, - isUserKey, - isSystem, - isMarkedForRemoval, - }; - - return { text, metadata }; - } - - private async deleteInsertRagChunkForField(ingestionInfo: ModuleRAGIngestionInfo, moduleName: string, modelName: string, field: CreateFieldMetadataDto,): Promise { - const fieldName: string = field?.name ?? 'unknown_field'; - - // 1) Full JSON hash (as-is) - const schemaHash = this._sha256OfJson(field); - - // 2) Ensure ingestionInfo entry for the model & field - const modelsArr = ingestionInfo.models ?? (ingestionInfo.models = []); - let modelEntry = modelsArr.find(m => m.modelName === modelName); - if (!modelEntry) { - modelEntry = { modelName, fields: [] }; - modelsArr.push(modelEntry); - } - const fieldsArr = modelEntry.fields ?? (modelEntry.fields = []); - let fieldEntry = fieldsArr.find(f => f.fieldName === fieldName); - if (!fieldEntry) { - fieldEntry = { fieldName }; - fieldsArr.push(fieldEntry); - } - - // 3) Skip if unchanged - if (fieldEntry.fieldHash === schemaHash && fieldEntry.fieldChunkId) { - this.logger.log(`[Skip] Field unchanged: ${moduleName}.${modelName}.${fieldName}`); - return fieldEntry.fieldChunkId; - } - - // 4) Build text + metadata tailored to FieldMetadata - const { text, metadata } = this._buildFieldTextAndMetadata(moduleName, modelName, field); - - // also keep the hash in metadata for audit/debug - (metadata as any).schemaHash = schemaHash; - - // 5) Delete previous chunk if present - if (fieldEntry.fieldChunkId) { - try { - await this.ragClient.documents.delete({ id: fieldEntry.fieldChunkId }); - } catch (e) { - this.logger.warn(`[Warn] Failed deleting old field chunk (${fieldEntry.fieldChunkId}): ${String(e)}`); - } - } - - // 6) Create new document (R2R auto-generates the ID) - const r = await this.ragClient.documents.create({ - raw_text: text, - metadata, - collectionIds: [ingestionInfo.collectionId], - }); - - const newId = r?.results?.documentId; - if (!newId) { - throw new Error(`R2R did not return a documentId while creating field chunk for ${moduleName}.${modelName}.${fieldName}`); - } - - // 7) Update ingestion info - fieldEntry.fieldChunkId = newId; - fieldEntry.fieldHash = schemaHash; - - this.logger.log(`[OK] Ingested field ${moduleName}.${modelName}.${fieldName} → id=${newId}, hash=${schemaHash.slice(0, 8)}…`); - return newId; - } -} diff --git a/src/services/genai/mcp-handlers/mcp-handler-factory.service.ts b/src/services/genai/mcp-handlers/mcp-handler-factory.service.ts deleted file mode 100644 index 11a001f8..00000000 --- a/src/services/genai/mcp-handlers/mcp-handler-factory.service.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; - -import { classify } from '../../../helpers/string.helper'; -import { IMcpToolResponseHandler } from 'src/interfaces'; -import { SolidIntrospectService } from '../../solid-introspect.service'; - -@Injectable() -export class McpHandlerFactory { - private readonly logger = new Logger(McpHandlerFactory.name); - - constructor( - private readonly solidIntrospectionService: SolidIntrospectService - ) { - } - - getInstance(toolInvoked: string): IMcpToolResponseHandler { - toolInvoked = classify(toolInvoked); - - let resolvedHandlerName = `${toolInvoked}McpHandler`; - - // Get hold of the tool response handler instance using the tool name used. - let actualHandler = this.solidIntrospectionService.getProvider(resolvedHandlerName); - if (!actualHandler) { - throw new Error(`Unable to locate mcp tool handler with name ${resolvedHandlerName}`); - } - - // type safe - const actualHandlerInstance: IMcpToolResponseHandler = actualHandler.instance; - this.logger.error(`Resolved mcp tool response handler with name ${actualHandler.name}`); - - return actualHandlerInstance; - } -} diff --git a/src/services/genai/r2r-helper.service.ts b/src/services/genai/r2r-helper.service.ts deleted file mode 100644 index 4323bdad..00000000 --- a/src/services/genai/r2r-helper.service.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Injectable, Logger } from '@nestjs/common'; -import type { SolidCoreSetting } from "src/services/settings/default-settings-provider.service"; - -import { r2rClient } from 'r2r-js'; -import { SettingService } from '../setting.service'; - - - -@Injectable() -export class R2RHelperService { - private readonly logger = new Logger(R2RHelperService.name); - - constructor(private readonly settingService: SettingService) { } - - async getClient() { - const ragServerUrl = this.settingService.getConfigValue('ragServerUrl'); - this.logger.debug(`Attempting to create RAG client with url: ${ragServerUrl}`); - const client = new r2rClient(ragServerUrl); - - const ragServerLogin = this.settingService.getConfigValue('ragServerLogin'); - // @ts-ignore - this.logger.debug(`Attempting to login to our RAG server with user ${ragServerLogin}`) - await client.users.login({ - email: ragServerLogin, - password: this.settingService.getConfigValue('ragServerPassword') - }); - - return client; - } - - // async checkHealth() { - // const client = new r2rClient('http://localhost:7272'); - - // return await client.health(); - // } - -} \ No newline at end of file diff --git a/src/solid-core.module.ts b/src/solid-core.module.ts index 147583ab..15a4aa9e 100644 --- a/src/solid-core.module.ts +++ b/src/solid-core.module.ts @@ -140,8 +140,6 @@ import { SmtpEmailQueuePublisherRedis } from "./jobs/redis/smtp-email-publisher- import { SmtpEmailQueueSubscriberRedis } from "./jobs/redis/smtp-email-subscriber-redis.service"; import { Three60WhatsappQueuePublisherRedis } from "./jobs/redis/three60-whatsapp-publisher-redis.service"; import { Three60WhatsappQueueSubscriberRedis } from "./jobs/redis/three60-whatsapp-subscriber-redis.service"; -import { TriggerMcpClientPublisherRedis } from "./jobs/redis/trigger-mcp-client-publisher-redis.service"; -import { TriggerMcpClientSubscriberRedis } from "./jobs/redis/trigger-mcp-client-subscriber-redis.service"; import { TwilioSmsQueuePublisherRedis } from "./jobs/redis/twilio-sms-publisher-redis.service"; import { TwilioSmsQueueSubscriberRedis } from "./jobs/redis/twilio-sms-subscriber-redis.service"; import { UserRegistrationListener } from "./listeners/user-registration.listener"; @@ -184,7 +182,6 @@ import { PermissionMetadataService } from "./services/permission-metadata.servic import { ScheduleModule } from "@nestjs/schedule"; import { ClsModule } from "nestjs-cls"; -import { AiInteractionController } from "./controllers/ai-interaction.controller"; import { ChatterMessageDetailsController } from "./controllers/chatter-message-details.controller"; import { ChatterMessageController } from "./controllers/chatter-message.controller"; @@ -207,7 +204,6 @@ import { InfoService } from './services/info.service'; import { UserActivityHistoryController } from './controllers/user-activity-history.controller'; import { UserViewMetadataController } from './controllers/user-view-metadata.controller'; import { UserController } from './controllers/user.controller'; -import { AiInteraction } from './entities/ai-interaction.entity'; import { ChatterMessageDetails } from './entities/chatter-message-details.entity'; import { ChatterMessage } from './entities/chatter-message.entity'; @@ -248,7 +244,6 @@ import { TwilioSmsQueuePublisherDatabase } from "./jobs/database/twilio-sms-publ import { TwilioSmsQueueSubscriberDatabase } from "./jobs/database/twilio-sms-subscriber-database.service"; // import { ThrottlerModule } from '@nestjs/throttler'; -import { IngestCommand } from "./commands/ingest.command"; import { MailFactory } from "./factories/mail.factory"; import { ErrorMapperService } from "./helpers/error-mapper.service"; import { SolidCoreErrorCodesProvider } from "./helpers/solid-core-error-codes-provider.service"; @@ -258,19 +253,14 @@ import { Msg91WhatsappQueuePublisherDatabase } from "./jobs/database/msg91-whats import { Msg91WhatsappQueueSubscriberDatabase } from "./jobs/database/msg91-whatsapp-subscriber-database.service"; import { Three60WhatsappQueuePublisherDatabase } from "./jobs/database/three60-whatsapp-publisher-database.service"; import { Three60WhatsappQueueSubscriberDatabase } from "./jobs/database/three60-whatsapp-subscriber-database.service"; -import { TriggerMcpClientPublisherDatabase } from "./jobs/database/trigger-mcp-client-publisher-database.service"; -import { TriggerMcpClientSubscriberDatabase } from "./jobs/database/trigger-mcp-client-subscriber-database.service"; import { GenerateCodePublisherRabbitmq } from "./jobs/rabbitmq/generate-code-publisher.service"; import { GenerateCodeSubscriberRabbitmq } from "./jobs/rabbitmq/generate-code-subscriber.service"; import { Three60WhatsappQueuePublisher } from "./jobs/rabbitmq/three60-whatsapp-publisher.service"; import { Three60WhatsappQueueSubscriber } from "./jobs/rabbitmq/three60-whatsapp-subscriber.service"; -import { TriggerMcpClientPublisherRabbitmq } from "./jobs/rabbitmq/trigger-mcp-client-publisher.service"; -import { TriggerMcpClientSubscriberRabbitmq } from "./jobs/rabbitmq/trigger-mcp-client-subscriber.service"; import { TwilioSmsQueuePublisherRabbitmq } from "./jobs/rabbitmq/twilio-sms-publisher.service"; import { TwilioSmsQueueSubscriberRabbitmq } from "./jobs/rabbitmq/twilio-sms-subscriber.service"; import { ListOfValuesMapper } from "./mappers/list-of-values-mapper"; import { ActionMetadataRepository } from "./repository/action-metadata.repository"; -import { AiInteractionRepository } from "./repository/ai-interaction.repository"; import { ChatterMessageDetailsRepository } from "./repository/chatter-message-details.repository"; import { ChatterMessageRepository } from "./repository/chatter-message.repository"; @@ -305,7 +295,6 @@ import { UserRepository } from './repository/user.repository'; import { ViewMetadataRepository } from './repository/view-metadata.repository'; import { PermissionMetadataSeederService } from './seeders/permission-metadata-seeder.service'; import { SystemFieldsSeederService } from './seeders/system-fields-seeder.service'; -import { AiInteractionService } from './services/ai-interaction.service'; import { ChatterMessageDetailsService } from './services/chatter-message-details.service'; import { ChatterMessageService } from './services/chatter-message.service'; import { ConcatComputedFieldProvider } from './services/computed-fields/concat-computed-field-provider.service'; @@ -331,9 +320,6 @@ import { CsvService } from './services/csv.service'; import { ExcelService } from './services/excel.service'; import { ExportTemplateService } from './services/export-template.service'; import { ExportTransactionService } from './services/export-transaction.service'; -import { IngestMetadataService } from './services/genai/ingest-metadata.service'; -import { McpHandlerFactory } from './services/genai/mcp-handlers/mcp-handler-factory.service'; -import { R2RHelperService } from './services/genai/r2r-helper.service'; import { ImportTransactionErrorLogService } from './services/import-transaction-error-log.service'; import { ImportTransactionService } from './services/import-transaction.service'; import { LocaleService } from './services/locale.service'; @@ -397,7 +383,6 @@ import { DashboardUserLayoutRepository } from './repositories/dashboard-user-lay imports: [ TypeOrmModule.forFeature([ ActionMetadata, - AiInteraction, ChatterMessage, ChatterMessageDetails, EmailAttachment, @@ -468,7 +453,6 @@ import { DashboardUserLayoutRepository } from './repositories/dashboard-user-lay ], controllers: [ ActionMetadataController, - AiInteractionController, AuthenticationController, ChatterMessageController, ChatterMessageDetailsController, @@ -553,7 +537,6 @@ import { DashboardUserLayoutRepository } from './repositories/dashboard-user-lay InfoService, SolidIntrospectService, DiscoveryService, - R2RHelperService, CrudHelperService, CRUDService, Reflector, @@ -590,8 +573,6 @@ import { DashboardUserLayoutRepository } from './repositories/dashboard-user-lay TestDataCommand, TestRunCommand, McpCommand, - IngestCommand, - IngestMetadataService, SMTPEMailService, ElasticEmailService, Msg91SMSService, @@ -607,11 +588,6 @@ import { DashboardUserLayoutRepository } from './repositories/dashboard-user-lay ErrorMapperService, SolidCoreErrorCodesProvider, - TriggerMcpClientPublisherDatabase, - TriggerMcpClientSubscriberDatabase, - TriggerMcpClientPublisherRabbitmq, - TriggerMcpClientSubscriberRabbitmq, - SmtpEmailQueuePublisherRabbitmq, SmtpEmailQueueSubscriberRabbitmq, SmtpEmailQueuePublisherDatabase, @@ -692,8 +668,6 @@ import { DashboardUserLayoutRepository } from './repositories/dashboard-user-lay SmtpEmailQueueSubscriberRedis, Three60WhatsappQueuePublisherRedis, Three60WhatsappQueueSubscriberRedis, - TriggerMcpClientPublisherRedis, - TriggerMcpClientSubscriberRedis, TwilioSmsQueuePublisherRedis, TwilioSmsQueueSubscriberRedis, GenerateCodePublisherDatabase, @@ -758,11 +732,8 @@ import { DashboardUserLayoutRepository } from './repositories/dashboard-user-lay ComputedFieldEvaluationSubscriberRabbitmq, ConcatEntityComputedFieldProvider, UserActivityHistoryService, - AiInteractionService, NoopsEntityComputedFieldProviderService, - McpHandlerFactory, - SolidTsMorphService, ViewMetadataRepository, @@ -782,7 +753,6 @@ import { DashboardUserLayoutRepository } from './repositories/dashboard-user-lay SmsFactory, ChatterMessageRepository, ChatterMessageDetailsRepository, - AiInteractionRepository, EmailTemplateRepository, ExportTemplateRepository, ExportTransactionRepository, @@ -818,7 +788,6 @@ import { DashboardUserLayoutRepository } from './repositories/dashboard-user-lay DashboardUserLayoutRepository, ], exports: [ - AiInteractionService, AuthenticationService, ChatterMessageDetailsRepository, ChatterMessageDetailsService, From 60c74bf42e76832bf747e4a1b26262fb5138242d Mon Sep 17 00:00:00 2001 From: SaibazKhan Date: Wed, 17 Jun 2026 13:55:39 +0530 Subject: [PATCH 130/136] Add Date field in Instructions Response --- src/dtos/import-instructions.dto.ts | 12 +++++++++++- src/services/import-transaction.service.ts | 12 +++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/dtos/import-instructions.dto.ts b/src/dtos/import-instructions.dto.ts index 7da28351..13a37d63 100644 --- a/src/dtos/import-instructions.dto.ts +++ b/src/dtos/import-instructions.dto.ts @@ -20,11 +20,21 @@ export class StandardImportInstructionsResponseDto { description: 'List of date fields for import', }) dateFields: string[]; + @ApiProperty({ + type: String, + description: 'Expected format for date fields during import', + }) + dateFieldFormat: string; @ApiProperty({ type: [String], description: 'List of date-time fields for import', }) dateTimeFields: string[]; + @ApiProperty({ + type: String, + description: 'Expected format for date-time fields during import', + }) + dateTimeFieldFormat: string; @ApiProperty({ type: [String], description: 'List of number fields for import', @@ -63,4 +73,4 @@ export class RegexFieldInstructionResponseDto { description: 'Regex pattern for the field', }) regexPattern: string; -} \ No newline at end of file +} diff --git a/src/services/import-transaction.service.ts b/src/services/import-transaction.service.ts index e110b79b..0eaf3cd9 100644 --- a/src/services/import-transaction.service.ts +++ b/src/services/import-transaction.service.ts @@ -27,6 +27,7 @@ import { getUserExcludedFields } from 'src/helpers/user-helper'; import { ActiveUserData } from 'src/interfaces/active-user-data.interface'; import { upperFirst, camelCase } from 'lodash'; import { classify } from '../helpers/string.helper'; +import type { SolidCoreSetting } from './settings/default-settings-provider.service'; interface ImportTemplateFileInfo { stream: NodeJS.ReadableStream; @@ -180,11 +181,20 @@ export class ImportTransactionService extends CRUDService { // Replace modelMetadata.fields with combined (child + parent) fields // modelMetadata.fields = allFields; + const dateFieldFormat = + (this.settingService.getConfigValue('dateFormat') as string | null) ?? + 'YYYY-MM-DD'; + const dateTimeFieldFormat = + (this.settingService.getConfigValue('dateTimeFormat') as string | null) ?? + 'YYYY-MM-DD HH:mm:ss'; + // Create the standard import instructions const standardInstructions: StandardImportInstructionsResponseDto = { requiredFields: [], dateFields: [], + dateFieldFormat, dateTimeFields: [], + dateTimeFieldFormat, numberFields: [], emailFields: [], regexFields: [], @@ -792,4 +802,4 @@ export class ImportTransactionService extends CRUDService { const relatedRecordsIds = relatedRecordsResult.records.map(record => record.id); return relatedRecordsIds; } -} \ No newline at end of file +} From 6caaa69516122965971f4159f9163335b6499288 Mon Sep 17 00:00:00 2001 From: Jenendar Date: Sat, 20 Jun 2026 16:44:33 +0530 Subject: [PATCH 131/136] defaulted to modulemetadataseederservice if no seeder provided --- src/controllers/service.controller.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/controllers/service.controller.ts b/src/controllers/service.controller.ts index 3aa58666..fca3b735 100755 --- a/src/controllers/service.controller.ts +++ b/src/controllers/service.controller.ts @@ -90,18 +90,19 @@ export class ServiceController { @ApiBearerAuth("jwt") @Post('seed') async seedData(@Body() seedData: any) { + const seederName = seedData?.seeder ?? 'ModuleMetadataSeederService'; const seeder = this.solidRegistry .getSeeders() - .filter((seeder) => seeder.name === seedData.seeder) + .filter((seeder) => seeder.name === seederName) .map((seeder) => seeder.instance) .pop(); if (!seeder) { - this.logger.error(`Seeder service ${seedData.seeder} not found. Does your service have a seed() method?`); + this.logger.error(`Seeder service ${seederName} not found. Does your service have a seed() method?`); return; } this.logger.log(`Running the seed() method for seeder :${seeder.constructor.name}`); await seeder.seed(); - return { message: `seed data for ${seedData.seeder}` }; + return { message: `seed data for ${seederName}` }; } @ApiBearerAuth("jwt") From 7914a9a34dc803753645ecbc335702ab9c2f6fc5 Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Sat, 20 Jun 2026 17:01:12 +0530 Subject: [PATCH 132/136] changes to avoid extra module checks before dynamically loading it --- src/helpers/module.helper.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/helpers/module.helper.ts b/src/helpers/module.helper.ts index bc39103e..7fe6365d 100755 --- a/src/helpers/module.helper.ts +++ b/src/helpers/module.helper.ts @@ -54,11 +54,11 @@ export const getDynamicModuleNamesBasedOnMetadata = (): string[] => { if (!isValidDirectory) return false; - const moduleManifestPath = path.join(srcPath, dirent.name, `${dirent.name}.module.ts`); - const moduleManifestStats = fs.statSync(moduleManifestPath, { throwIfNoEntry: false }); - if (!moduleManifestStats || !moduleManifestStats.isFile()) { - return false; - } + // const moduleManifestPath = path.join(srcPath, dirent.name, `${dirent.name}.module.ts`); + // const moduleManifestStats = fs.statSync(moduleManifestPath, { throwIfNoEntry: false }); + // if (!moduleManifestStats || !moduleManifestStats.isFile()) { + // return false; + // } // Commenting this out, since we want to rely solely on the presence of metadata files and be able to load this as a dynamic module const fullPath = path.join(srcPath, dirent.name, 'metadata', `${dirent.name}-metadata.json`); From f62a441e34c612cd3a41e8998fb83d32187cf433 Mon Sep 17 00:00:00 2001 From: Pathik Date: Sun, 21 Jun 2026 19:09:07 +0530 Subject: [PATCH 133/136] =?UTF-8?q?subscribers=20now=20use=20the=20event?= =?UTF-8?q?=E2=80=99s=20own=20transaction=20connection=20instead=20of=20a?= =?UTF-8?q?=20second=20pooled=20one=20(helpful=20for=20embedded=20db)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/subscribers/scheduled-job.subscriber.ts | 2 +- src/subscribers/security-rule.subscriber.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/subscribers/scheduled-job.subscriber.ts b/src/subscribers/scheduled-job.subscriber.ts index 320f7dc0..c294d293 100644 --- a/src/subscribers/scheduled-job.subscriber.ts +++ b/src/subscribers/scheduled-job.subscriber.ts @@ -83,7 +83,7 @@ export class ScheduledJobSubscriber return; } - const moduleMetadataRepo = this.dataSource.getRepository(ModuleMetadata); + const moduleMetadataRepo = event.queryRunner.manager.getRepository(ModuleMetadata); const populatedModuleMetadata = await moduleMetadataRepo.findOne({ where: { id: moduleMetadata.id }, }); diff --git a/src/subscribers/security-rule.subscriber.ts b/src/subscribers/security-rule.subscriber.ts index a4c3d293..e780c37c 100644 --- a/src/subscribers/security-rule.subscriber.ts +++ b/src/subscribers/security-rule.subscriber.ts @@ -40,7 +40,7 @@ export class SecurityRuleSubscriber implements EntitySubscriberInterface Date: Mon, 22 Jun 2026 15:19:18 +0530 Subject: [PATCH 134/136] 0.1.10-beta.27 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3cb0285e..62abe41f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.26", + "version": "0.1.10-beta.27", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@solidxai/core", - "version": "0.1.10-beta.26", + "version": "0.1.10-beta.27", "license": "BUSL-1.1", "dependencies": { "@aws-sdk/client-s3": "^3.637.0", diff --git a/package.json b/package.json index cbb8bc39..262e47c5 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.26", + "version": "0.1.10-beta.27", "description": "This module is a NestJS module containing all the required core providers required by a Solid application", "main": "dist/index.js", "types": "dist/index.d.ts", From 18d0a4a256a3fa962a6840536de1c195b5a00d82 Mon Sep 17 00:00:00 2001 From: SaibazKhan Date: Fri, 12 Jun 2026 19:24:22 +0530 Subject: [PATCH 135/136] ad changes --- MICROSOFT_ACTIVE_DIRECTORY_OAUTH_FLOW.md | 616 ++++++++++++++++++ src/constants/error-messages.ts | 3 +- ...ive-directory-authentication.controller.ts | 104 +++ src/entities/user.entity.ts | 14 +- ...microsoft-active-directory-oauth.helper.ts | 83 +++ src/helpers/user-helper.ts | 6 +- src/index.ts | 1 + ...crosoft-active-directory-oauth.strategy.ts | 63 ++ src/services/authentication.service.ts | 78 +++ .../default-settings-provider.service.ts | 59 ++ src/services/user.service.ts | 61 ++ src/solid-core.module.ts | 4 + 12 files changed, 1089 insertions(+), 3 deletions(-) create mode 100644 MICROSOFT_ACTIVE_DIRECTORY_OAUTH_FLOW.md create mode 100644 src/controllers/microsoft-active-directory-authentication.controller.ts create mode 100644 src/helpers/microsoft-active-directory-oauth.helper.ts create mode 100644 src/passport-strategies/microsoft-active-directory-oauth.strategy.ts diff --git a/MICROSOFT_ACTIVE_DIRECTORY_OAUTH_FLOW.md b/MICROSOFT_ACTIVE_DIRECTORY_OAUTH_FLOW.md new file mode 100644 index 00000000..9b503ce8 --- /dev/null +++ b/MICROSOFT_ACTIVE_DIRECTORY_OAUTH_FLOW.md @@ -0,0 +1,616 @@ +# Microsoft Active Directory OAuth Flow + +This document explains how Microsoft Active Directory, also called Azure AD or Microsoft Entra ID, OAuth login works in Solid Core and Solid Core UI. + +## Important Short Answer + +On button click, frontend does not call the backend with `axios` or `fetch`. + +Frontend only builds this URL: + +```ts +const backendApiUrl = ( + env("NEXT_PUBLIC_BACKEND_API_URL") || env("API_URL") +).replace(/\/+$/, ""); + +const getOAuthConnectUrl = (provider: string) => + `${backendApiUrl}/api/iam/${provider}/connect`; +``` + +For Microsoft Active Directory, this becomes: + +```text +http://localhost:3000/api/iam/microsoft-active-directory/connect +``` + +Then frontend does: + +```ts +router.push(getOAuthConnectUrl("microsoft-active-directory")); +``` + +So frontend creates a URL and navigates the browser to that URL. Browser navigation itself makes the `GET /connect` request. + +This is correct for OAuth because OAuth needs full browser redirects: + +```text +Frontend page +-> Backend /connect +-> Microsoft login page +-> Backend /connect/callback +-> Frontend callback page +-> Backend /authenticate +``` + +## Why It Is Not A Normal API Call + +Normal login with username/password can use an API call: + +```text +frontend axios/fetch -> backend -> response JSON +``` + +OAuth login cannot work like that for the first step because the user must leave our app and open Microsoft login in the browser. + +So the first step is navigation: + +```text +browser location changes to backend /connect +``` + +After Microsoft login completes, the final token exchange uses an API call: + +```text +frontend -> backend /authenticate?accessCode=... +``` + +## Files Involved + +Backend files: + +```text +src/controllers/microsoft-active-directory-authentication.controller.ts +src/passport-strategies/microsoft-active-directory-oauth.strategy.ts +src/helpers/microsoft-active-directory-oauth.helper.ts +src/services/user.service.ts +src/services/authentication.service.ts +src/services/settings/default-settings-provider.service.ts +``` + +Frontend files: + +```text +src/components/common/SocialMediaLogin.tsx +src/routes/pages/auth/InitiateMicrosoftActiveDirectoryOauthPage.tsx +src/components/auth/MicrosoftActiveDirectoryAuthChecking.tsx +src/adapters/auth/signInWithOAuthAccessCode.ts +src/routes/solidRoutes.tsx +``` + +## Backend Endpoints + +### 1. Start Microsoft Active Directory OAuth + +```http +GET /api/iam/microsoft-active-directory/connect +``` + +Controller method: + +```ts +@Public() +@UseGuards(MicrosoftActiveDirectoryOauthGuard) +@Get("connect") +async connect() { + await this.validateConfiguration(); +} +``` + +This endpoint starts OAuth. The guard runs before the method body. + +The guard: + +```ts +export class MicrosoftActiveDirectoryOauthGuard + extends AuthGuard("microsoft-active-directory") {} +``` + +This tells Passport to use the strategy named: + +```text +microsoft-active-directory +``` + +### 2. Microsoft Callback + +```http +GET /api/iam/microsoft-active-directory/connect/callback +``` + +Controller method: + +```ts +@Public() +@Get("connect/callback") +@UseGuards(MicrosoftActiveDirectoryOauthGuard) +async microsoftActiveDirectoryAuthCallback(@Req() req, @Res() res) { + const config = await this.validateConfiguration(); + const user = req.user; + + return res.redirect( + this.buildFrontendRedirectUrl(config.redirectURL, user["accessCode"]), + ); +} +``` + +Microsoft redirects to this endpoint after successful login. + +This route also uses `MicrosoftActiveDirectoryOauthGuard`, but now Microsoft sends a `code` in the URL. Passport uses that code to get an access token and profile from Microsoft. + +### 3. Authenticate With accessCode + +```http +GET /api/iam/microsoft-active-directory/authenticate?accessCode= +``` + +Controller method: + +```ts +@Public() +@Get("authenticate") +async microsoftActiveDirectoryAuth(@Query("accessCode") accessCode: string) { + await this.validateConfiguration(); + return this.authService.signInUsingMicrosoftActiveDirectory(accessCode); +} +``` + +Frontend calls this after it receives `accessCode` from the callback redirect. + +This returns application JWT tokens. + +## Complete Request Flow + +### Step 1: User Clicks Button + +Frontend file: + +```text +src/components/common/SocialMediaLogin.tsx +``` + +Click handler: + +```ts +onClick={() => router.push(getOAuthConnectUrl("microsoft-active-directory"))} +``` + +This creates: + +```text +http://localhost:3000/api/iam/microsoft-active-directory/connect +``` + +Then browser opens that URL. + +Note: yahan frontend direct API call nahi kar raha. Sirf URL bana kar browser ko us URL par bhej raha hai. + +### Step 2: Backend /connect Runs Guard + +Backend route: + +```http +GET /api/iam/microsoft-active-directory/connect +``` + +Before `connect()` runs, Nest runs: + +```ts +MicrosoftActiveDirectoryOauthGuard +``` + +Guard calls Passport strategy: + +```ts +AuthGuard("microsoft-active-directory") +``` + +Strategy config: + +```ts +super({ + clientID, + clientSecret, + callbackURL, + tenant, + scope: MICROSOFT_ACTIVE_DIRECTORY_OAUTH_SCOPES, + addUPNAsEmail: true, +}); +``` + +Passport redirects the browser to Microsoft login page. + +### Step 3: Microsoft Login Page Opens + +Browser leaves our app and opens Microsoft login. + +Microsoft receives values like: + +```text +client_id +redirect_uri +scope +tenant +response_type=code +``` + +The `redirect_uri` comes from: + +```env +IAM_MICROSOFT_ACTIVE_DIRECTORY_OAUTH_CALLBACK_URL +``` + +Example: + +```env +IAM_MICROSOFT_ACTIVE_DIRECTORY_OAUTH_CALLBACK_URL=http://localhost:3000/api/iam/microsoft-active-directory/connect/callback +``` + +This exact URL must be registered in Azure App Registration. + +### Step 4: Microsoft Redirects Back To Backend + +After successful Microsoft login, Microsoft redirects browser to: + +```text +http://localhost:3000/api/iam/microsoft-active-directory/connect/callback?code=... +``` + +This is the callback route. + +### Step 5: Guard Runs Again On Callback + +The callback route also has: + +```ts +@UseGuards(MicrosoftActiveDirectoryOauthGuard) +``` + +Now the guard sees Microsoft `code`. + +Passport exchanges that `code` with Microsoft and gets: + +```text +access token +profile +``` + +Then Passport calls the strategy `validate()` method. + +### Step 6: Strategy validate() Creates accessCode + +Backend file: + +```text +src/passport-strategies/microsoft-active-directory-oauth.strategy.ts +``` + +Method: + +```ts +async validate(_accessToken, _refreshToken, profile, done) { + const loginAccessCode = uuid(); + + const user = { + provider: "microsoftActiveDirectory", + providerId, + email, + name, + picture, + accessCode: loginAccessCode, + }; + + await this.userService.resolveUserOnOauthMicrosoftActiveDirectory({ + ...user, + accessToken: _accessToken, + refreshToken: null, + }); + + done(null, user); +} +``` + +This method creates a temporary `accessCode`. This is not the JWT. It is only a one-time code used by frontend to complete login. + +### Step 7: User Is Created Or Updated + +Backend file: + +```text +src/services/user.service.ts +``` + +Method: + +```ts +resolveUserOnOauthMicrosoftActiveDirectory() +``` + +It checks user by email: + +```text +if user does not exist: + create user + save Microsoft Active Directory id/token/profile picture + save accessCode + initialize default role + +if user exists: + update Microsoft Active Directory id/token/profile picture + update accessCode +``` + +Important fields saved: + +```text +accessCode +microsoftActiveDirectoryId +microsoftActiveDirectoryAccessToken +microsoftActiveDirectoryProfilePicture +lastLoginProvider +``` + +### Step 8: Backend Redirects To Frontend + +After strategy validation, callback controller gets `req.user`. + +Then it redirects to frontend: + +```ts +this.buildFrontendRedirectUrl(config.redirectURL, user["accessCode"]) +``` + +`config.redirectURL` comes from: + +```env +IAM_MICROSOFT_ACTIVE_DIRECTORY_OAUTH_REDIRECT_URL +``` + +Example: + +```env +IAM_MICROSOFT_ACTIVE_DIRECTORY_OAUTH_REDIRECT_URL=http://localhost:3001/auth/initiate-microsoft-active-directory-oauth +``` + +Final redirect URL becomes: + +```text +http://localhost:3001/auth/initiate-microsoft-active-directory-oauth?accessCode= +``` + +This URL is frontend, not backend. + +### Step 9: Frontend Reads accessCode + +Frontend route: + +```text +/auth/initiate-microsoft-active-directory-oauth +``` + +Frontend file: + +```text +src/routes/pages/auth/InitiateMicrosoftActiveDirectoryOauthPage.tsx +``` + +It renders: + +```ts + +``` + +That component reads: + +```ts +const accessCode = searchParams.get("accessCode"); +``` + +### Step 10: Frontend Calls Backend /authenticate + +Frontend file: + +```text +src/adapters/auth/signInWithOAuthAccessCode.ts +``` + +It calls: + +```ts +solidGet( + `${apiUrl}/api/iam/${provider}/authenticate?accessCode=${encodeURIComponent(accessCode)}` +) +``` + +For Microsoft Active Directory: + +```text +GET /api/iam/microsoft-active-directory/authenticate?accessCode= +``` + +This is the real frontend API call. + +### Step 11: Backend Validates accessCode And Returns JWT + +Backend file: + +```text +src/services/authentication.service.ts +``` + +Method: + +```ts +signInUsingMicrosoftActiveDirectory(accessCode) +``` + +It does: + +```text +1. Find user by accessCode +2. Check account is not blocked +3. Validate saved Microsoft access token using Microsoft Graph /me +4. Verify Microsoft profile id/email matches the saved user +5. Generate application JWT accessToken and refreshToken +6. Return user and tokens +``` + +After frontend receives tokens, it stores session the same way as existing OAuth login. + +## callbackURL vs redirectURL + +These two are different and both are required. + +### callbackURL + +Used by Microsoft to come back to backend: + +```env +IAM_MICROSOFT_ACTIVE_DIRECTORY_OAUTH_CALLBACK_URL=http://localhost:3000/api/iam/microsoft-active-directory/connect/callback +``` + +This must be registered in Azure App Registration. + +### redirectURL + +Used by backend to send the user back to frontend: + +```env +IAM_MICROSOFT_ACTIVE_DIRECTORY_OAUTH_REDIRECT_URL=http://localhost:3001/auth/initiate-microsoft-active-directory-oauth +``` + +This does not go in Azure redirect URI list unless your Azure app directly redirects to frontend, which this backend flow does not do. + +## Required Environment Variables + +Backend: + +```env +IAM_MICROSOFT_ACTIVE_DIRECTORY_OAUTH_CLIENT_ID= +IAM_MICROSOFT_ACTIVE_DIRECTORY_OAUTH_CLIENT_SECRET= +IAM_MICROSOFT_ACTIVE_DIRECTORY_OAUTH_TENANT_ID= +IAM_MICROSOFT_ACTIVE_DIRECTORY_OAUTH_CALLBACK_URL=http://localhost:3000/api/iam/microsoft-active-directory/connect/callback +IAM_MICROSOFT_ACTIVE_DIRECTORY_OAUTH_REDIRECT_URL=http://localhost:3001/auth/initiate-microsoft-active-directory-oauth +``` + +Frontend: + +```env +VITE_BACKEND_API_URL=http://localhost:3000 +VITE_API_URL=http://localhost:3000 +VITE_BASE_URL=http://localhost:3001 +``` + +## Azure App Registration Setup + +In Azure Portal: + +```text +Microsoft Entra ID +-> App registrations +-> Your app +-> Authentication +-> Add platform +-> Web +-> Redirect URIs +``` + +Add exactly: + +```text +http://localhost:3000/api/iam/microsoft-active-directory/connect/callback +``` + +The value must exactly match `IAM_MICROSOFT_ACTIVE_DIRECTORY_OAUTH_CALLBACK_URL`. + +Exact means same: + +```text +protocol: http vs https +host: localhost +port: 3000 +path: /api/iam/microsoft-active-directory/connect/callback +trailing slash: no extra slash +``` + +## Common Mistakes + +### redirect_uri is not valid + +Reason: + +```text +IAM_MICROSOFT_ACTIVE_DIRECTORY_OAUTH_CALLBACK_URL does not match Azure redirect URI. +``` + +Fix: + +```text +Register the exact callback URL in Azure. +``` + +### Using frontend URL as callbackURL + +Wrong: + +```env +IAM_MICROSOFT_ACTIVE_DIRECTORY_OAUTH_CALLBACK_URL=http://localhost:3001/auth/initiate-microsoft-active-directory-oauth +``` + +Correct: + +```env +IAM_MICROSOFT_ACTIVE_DIRECTORY_OAUTH_CALLBACK_URL=http://localhost:3000/api/iam/microsoft-active-directory/connect/callback +``` + +### Using backend API URL as redirectURL + +Wrong: + +```env +IAM_MICROSOFT_ACTIVE_DIRECTORY_OAUTH_REDIRECT_URL=http://localhost:3000/api/auth/initiate-microsoft-active-directory-oauth +``` + +Correct: + +```env +IAM_MICROSOFT_ACTIVE_DIRECTORY_OAUTH_REDIRECT_URL=http://localhost:3001/auth/initiate-microsoft-active-directory-oauth +``` + +### Empty client id crashes strategy + +Wrong: + +```env +IAM_MICROSOFT_ACTIVE_DIRECTORY_OAUTH_CLIENT_ID= +``` + +If OAuth is not configured, leave it absent or let the strategy use dummy startup values. If OAuth should work, set the real Azure client id. + +## Final Flow Summary + +```text +1. User clicks Microsoft Active Directory button +2. Frontend builds backend /connect URL +3. Frontend navigates browser to backend /connect +4. Backend Passport guard redirects browser to Microsoft +5. User logs in on Microsoft +6. Microsoft redirects browser to backend /connect/callback with code +7. Backend exchanges code for Microsoft token/profile +8. Backend creates/updates user and stores temporary accessCode +9. Backend redirects browser to frontend redirectURL with accessCode +10. Frontend reads accessCode +11. Frontend calls backend /authenticate?accessCode=... +12. Backend verifies user/token and returns application JWT +13. Frontend stores JWT session +``` + diff --git a/src/constants/error-messages.ts b/src/constants/error-messages.ts index a64e7f04..8f888d0e 100644 --- a/src/constants/error-messages.ts +++ b/src/constants/error-messages.ts @@ -28,6 +28,7 @@ export const ERROR_MESSAGES = { ACCESS_DENIED: 'Access denied', INVALID_USER_PROFILE: 'Invalid user profile', GOOGLE_OAUTH_PROFILE_FETCH_FAILED: 'Failed to fetch user profile from Google OAuth service', + MICROSOFT_ACTIVE_DIRECTORY_OAUTH_PROFILE_FETCH_FAILED: 'Failed to fetch user profile from Microsoft Active Directory OAuth service', LOGOUT_FAILED: 'Logout failed due to an unexpected error.', INVALID_CREDENTIALS: 'Invalid credentials', @@ -130,4 +131,4 @@ export const ERROR_MESSAGES = { `You do not have permission to import ${model ?? 'these'} records.`, } as Record string>, -}; \ No newline at end of file +}; diff --git a/src/controllers/microsoft-active-directory-authentication.controller.ts b/src/controllers/microsoft-active-directory-authentication.controller.ts new file mode 100644 index 00000000..a742d404 --- /dev/null +++ b/src/controllers/microsoft-active-directory-authentication.controller.ts @@ -0,0 +1,104 @@ +import { + Controller, + Get, + InternalServerErrorException, + Query, + Req, + Res, + UseGuards, +} from "@nestjs/common"; +import { ApiQuery, ApiTags } from "@nestjs/swagger"; +import { Request, Response } from "express"; +import { + DEFAULT_MICROSOFT_ACTIVE_DIRECTORY_OAUTH_TENANT, + MicrosoftActiveDirectoryAuthConfiguration, + isMicrosoftActiveDirectoryOAuthConfigured, +} from "../helpers/microsoft-active-directory-oauth.helper"; +import { AuthenticationService } from "../services/authentication.service"; +import { SettingService } from "../services/setting.service"; +import { Public } from "src/decorators/public.decorator"; +import { Auth } from "../decorators/auth.decorator"; +import { AuthType } from "../enums/auth-type.enum"; +import { MicrosoftActiveDirectoryOauthGuard } from "../passport-strategies/microsoft-active-directory-oauth.strategy"; +import { UserService } from "../services/user.service"; +import type { SolidCoreSetting } from "../services/settings/default-settings-provider.service"; + +@Auth(AuthType.None) +@ApiTags("Solid Core") +@Controller("iam/microsoft-active-directory") +export class MicrosoftActiveDirectoryAuthenticationController { + constructor( + private readonly userService: UserService, + private readonly authService: AuthenticationService, + private readonly settingService: SettingService, + ) {} + + private async getConfiguration(): Promise { + return { + clientID: this.settingService.getConfigValue("MICROSOFT_ACTIVE_DIRECTORY_CLIENT_ID"), + clientSecret: this.settingService.getConfigValue("MICROSOFT_ACTIVE_DIRECTORY_CLIENT_SECRET"), + tenant: this.settingService.getConfigValue("MICROSOFT_ACTIVE_DIRECTORY_TENANT_ID" ) ?? DEFAULT_MICROSOFT_ACTIVE_DIRECTORY_OAUTH_TENANT, + callbackURL: this.settingService.getConfigValue("MICROSOFT_ACTIVE_DIRECTORY_CALLBACK_URL"), + redirectURL: this.settingService.getConfigValue("MICROSOFT_ACTIVE_DIRECTORY_REDIRECT_URL"), + }; + } + + private buildFrontendRedirectUrl(redirectURL: string, accessCode: string) { + const separator = redirectURL.includes("?") ? "&" : "?"; + return `${redirectURL}${separator}accessCode=${encodeURIComponent(accessCode)}`; + } + + private async validateConfiguration() { + const config = await this.getConfiguration(); + if (!isMicrosoftActiveDirectoryOAuthConfigured(config)) { + throw new InternalServerErrorException("Microsoft Active Directory OAuth is not configured"); + } + return config; + } + + @Public() + @UseGuards(MicrosoftActiveDirectoryOauthGuard) + @Get("connect") + async connect() { + await this.validateConfiguration(); + } + + @Public() + @Get("connect/callback") + @UseGuards(MicrosoftActiveDirectoryOauthGuard) + async microsoftActiveDirectoryAuthCallback( + @Req() req: Request, + @Res() res: Response, + ) { + const config = await this.validateConfiguration(); + const user = req.user; + return res.redirect( + this.buildFrontendRedirectUrl(config.redirectURL, user["accessCode"]), + ); + } + + @Public() + @Get("dummy-redirect") + async dummyMicrosoftActiveDirectoryAuthRedirect( + @Query("accessCode") accessCode, + ) { + await this.validateConfiguration(); + const user = await this.userService.findOneByAccessCode(accessCode); + + if (user) { + delete user["password"]; + } + + return user; + } + + @Public() + @Get("authenticate") + @ApiQuery({ name: "accessCode", required: true, type: String }) + async microsoftActiveDirectoryAuth( + @Query("accessCode") accessCode: string, + ) { + await this.validateConfiguration(); + return this.authService.signInUsingMicrosoftActiveDirectory(accessCode); + } +} diff --git a/src/entities/user.entity.ts b/src/entities/user.entity.ts index 26df82e9..e05a2278 100755 --- a/src/entities/user.entity.ts +++ b/src/entities/user.entity.ts @@ -89,6 +89,18 @@ export class User extends CommonEntity { // don't send to client microsoftProfilePicture: string; + @Column({ type: "varchar", nullable: true }) + // don't send to client + microsoftActiveDirectoryId: string; + + @Column({ type: "varchar", nullable: true }) + // don't send to client + microsoftActiveDirectoryAccessToken: string; + + @Column({ type: "varchar", nullable: true }) + // don't send to client + microsoftActiveDirectoryProfilePicture: string; + @Index() @Column({ default: true }) @Expose() @@ -194,4 +206,4 @@ export class User extends CommonEntity { @Expose() apiKeys: UserApiKey[]; -} \ No newline at end of file +} diff --git a/src/helpers/microsoft-active-directory-oauth.helper.ts b/src/helpers/microsoft-active-directory-oauth.helper.ts new file mode 100644 index 00000000..e941b352 --- /dev/null +++ b/src/helpers/microsoft-active-directory-oauth.helper.ts @@ -0,0 +1,83 @@ +export type MicrosoftActiveDirectoryAuthConfiguration = { + clientID: string; + clientSecret: string; + tenant: string; + callbackURL: string; + redirectURL: string; +}; + +export const DEFAULT_MICROSOFT_ACTIVE_DIRECTORY_OAUTH_TENANT = "common"; +export const MICROSOFT_ACTIVE_DIRECTORY_OAUTH_SCOPES = [ + "openid", + "profile", + "email", + "User.Read", +]; + +type MicrosoftActiveDirectoryOauthProfileValue = { + value?: string; +}; + +export type MicrosoftActiveDirectoryOauthProfile = { + id?: string; + displayName?: string; + emails?: MicrosoftActiveDirectoryOauthProfileValue[]; + photos?: MicrosoftActiveDirectoryOauthProfileValue[]; + _json?: { + id?: string; + displayName?: string; + mail?: string; + userPrincipalName?: string; + email?: string; + preferred_username?: string; + picture?: string; + }; +}; + +export const isMicrosoftActiveDirectoryOAuthConfigured = ( + config: MicrosoftActiveDirectoryAuthConfiguration, +): boolean => { + return !!( + config.clientID && + config.clientSecret && + config.tenant && + config.callbackURL && + config.redirectURL + ); +}; + +export const getMicrosoftActiveDirectoryOAuthProfileId = ( + profile: MicrosoftActiveDirectoryOauthProfile, +): string | null => { + return profile.id || profile._json?.id || null; +}; + +export const getMicrosoftActiveDirectoryOAuthEmail = ( + profile: MicrosoftActiveDirectoryOauthProfile, +): string | null => { + const email = + profile.emails?.find((item) => !!item.value)?.value || + profile._json?.mail || + profile._json?.userPrincipalName || + profile._json?.email || + profile._json?.preferred_username || + null; + + return email?.trim().toLowerCase() || null; +}; + +export const getMicrosoftActiveDirectoryOAuthDisplayName = ( + profile: MicrosoftActiveDirectoryOauthProfile, +): string | null => { + return profile.displayName || profile._json?.displayName || null; +}; + +export const getMicrosoftActiveDirectoryOAuthPicture = ( + profile: MicrosoftActiveDirectoryOauthProfile, +): string | null => { + return ( + profile.photos?.find((item) => !!item.value)?.value || + profile._json?.picture || + null + ); +}; diff --git a/src/helpers/user-helper.ts b/src/helpers/user-helper.ts index 001d8726..52a2d737 100644 --- a/src/helpers/user-helper.ts +++ b/src/helpers/user-helper.ts @@ -15,6 +15,10 @@ export function getUserExcludedFields(): string[] { "facebookId", "microsoftAccessToken", "microsoftId", + "microsoftProfilePicture", + "microsoftActiveDirectoryAccessToken", + "microsoftActiveDirectoryId", + "microsoftActiveDirectoryProfilePicture", "forgotPasswordConfirmedAt", "verificationTokenOnForgotPassword", "verificationTokenOnForgotPasswordExpiresAt", @@ -38,4 +42,4 @@ export function getUserExcludedFields(): string[] { "userViewMetadataIds", "userViewMetadataCommand", ]; -} \ No newline at end of file +} diff --git a/src/index.ts b/src/index.ts index 48176bb1..95a4a82c 100755 --- a/src/index.ts +++ b/src/index.ts @@ -269,6 +269,7 @@ export * from './listeners/user-registration.listener' export * from './passport-strategies/google-oauth.strategy' export * from './passport-strategies/facebook-oauth.strategy' export * from './passport-strategies/microsoft-oauth.strategy' +export * from './passport-strategies/microsoft-active-directory-oauth.strategy' export * from './services/selection-providers/list-of-values-selection-providers.service' diff --git a/src/passport-strategies/microsoft-active-directory-oauth.strategy.ts b/src/passport-strategies/microsoft-active-directory-oauth.strategy.ts new file mode 100644 index 00000000..239e43bc --- /dev/null +++ b/src/passport-strategies/microsoft-active-directory-oauth.strategy.ts @@ -0,0 +1,63 @@ +import { Injectable, Logger, UnauthorizedException } from '@nestjs/common'; +import { AuthGuard, PassportStrategy } from '@nestjs/passport'; +import { Strategy } from 'passport-microsoft'; +import { DEFAULT_MICROSOFT_ACTIVE_DIRECTORY_OAUTH_TENANT, MICROSOFT_ACTIVE_DIRECTORY_OAUTH_SCOPES, MicrosoftActiveDirectoryAuthConfiguration, getMicrosoftActiveDirectoryOAuthDisplayName, getMicrosoftActiveDirectoryOAuthEmail, getMicrosoftActiveDirectoryOAuthPicture, getMicrosoftActiveDirectoryOAuthProfileId, isMicrosoftActiveDirectoryOAuthConfigured } from 'src/helpers/microsoft-active-directory-oauth.helper'; +import { v4 as uuid } from 'uuid'; +import { UserService } from '../services/user.service'; + +const DUMMY_CLIENT_ID = 'DUMMY_CLIENT_ID'; +const DUMMY_CLIENT_SECRET = 'DUMMY_CLIENT_SECRET'; +const DUMMY_CALLBACK_URL = 'DUMMY_CALLBACK_URL'; + +@Injectable() +export class MicrosoftActiveDirectoryOauthGuard extends AuthGuard('microsoft-active-directory') { } + + +@Injectable() +export class MicrosoftActiveDirectoryOAuthStrategy extends PassportStrategy(Strategy, 'microsoft-active-directory') { + private readonly logger = new Logger(MicrosoftActiveDirectoryOAuthStrategy.name); + constructor(private readonly userService: UserService) { + // TODO: Have added default dummy values for the configuration, since the configuration is not mandatory. + // Perhaps a cleaner way needs to be figured out + const clientID = process.env.IAM_MICROSOFT_ACTIVE_DIRECTORY_OAUTH_CLIENT_ID || DUMMY_CLIENT_ID; + const clientSecret = process.env.IAM_MICROSOFT_ACTIVE_DIRECTORY_OAUTH_CLIENT_SECRET || DUMMY_CLIENT_SECRET; + const tenant = process.env.IAM_MICROSOFT_ACTIVE_DIRECTORY_OAUTH_TENANT_ID || DEFAULT_MICROSOFT_ACTIVE_DIRECTORY_OAUTH_TENANT; + const callbackURL = process.env.IAM_MICROSOFT_ACTIVE_DIRECTORY_OAUTH_CALLBACK_URL || DUMMY_CALLBACK_URL; + const redirectURL = process.env.IAM_MICROSOFT_ACTIVE_DIRECTORY_OAUTH_REDIRECT_URL; + + super({ clientID, clientSecret, callbackURL, tenant, scope: MICROSOFT_ACTIVE_DIRECTORY_OAUTH_SCOPES, addUPNAsEmail: true }); + + const microsoftActiveDirectoryOauth: MicrosoftActiveDirectoryAuthConfiguration = { clientID, clientSecret, tenant, callbackURL, redirectURL } + if (!isMicrosoftActiveDirectoryOAuthConfigured(microsoftActiveDirectoryOauth)) { + this.logger.debug('Microsoft Active Directory OAuth strategy is not configured'); + } + } + + async validate(_accessToken: string, _refreshToken: string, profile: any, done: any): Promise { + const providerId = getMicrosoftActiveDirectoryOAuthProfileId(profile); + + if (!providerId) { + return done(new UnauthorizedException('Microsoft Active Directory OAuth profile is missing an id'), false); + } + + // generate a unique access code. + const loginAccessCode: string = uuid(); + + const user = { + provider: 'microsoftActiveDirectory', + providerId, + email: getMicrosoftActiveDirectoryOAuthEmail(profile), + name: getMicrosoftActiveDirectoryOAuthDisplayName(profile) || getMicrosoftActiveDirectoryOAuthEmail(profile) || 'Microsoft Active Directory User', + picture: getMicrosoftActiveDirectoryOAuthPicture(profile), + accessCode: loginAccessCode, + }; + + // store the access code and the access token in the database. + // while doing this we also check if the user exists in the database if not we create one. + // if exists then we update the user and store the specified access code & token. + await this.userService.resolveUserOnOauthMicrosoftActiveDirectory({ ...user, accessToken: _accessToken, refreshToken: null }); + + done(null, user); + } + +} diff --git a/src/services/authentication.service.ts b/src/services/authentication.service.ts index 4d46daa2..d9a2ddec 100755 --- a/src/services/authentication.service.ts +++ b/src/services/authentication.service.ts @@ -1997,6 +1997,84 @@ export class AuthenticationService { }; } + async validateUserUsingMicrosoftActiveDirectory(user: User) { + if ( + !user.microsoftActiveDirectoryAccessToken || + !user.microsoftActiveDirectoryId + ) { + throw new UnauthorizedException(ERROR_MESSAGES.USER_NOT_FOUND); + } + + try { + const response = await this.httpService.axiosRef.get( + `https://graph.microsoft.com/v1.0/me?$select=id,displayName,mail,userPrincipalName`, + { + headers: { + Authorization: `Bearer ${user.microsoftActiveDirectoryAccessToken}`, + }, + }, + ); + const userProfile = response.data; + const profileEmail = (userProfile.mail || userProfile.userPrincipalName) + ?.trim() + .toLowerCase(); + const userEmail = user.email?.trim().toLowerCase(); + + if ( + userProfile.id === user.microsoftActiveDirectoryId && + (!userEmail || profileEmail === userEmail) + ) { + return userProfile; + } else { + throw new UnauthorizedException(ERROR_MESSAGES.INVALID_USER_PROFILE); + } + } catch (error: any) { + if (error instanceof UnauthorizedException) { + throw error; + } + + throw new UnauthorizedException( + ERROR_MESSAGES.MICROSOFT_ACTIVE_DIRECTORY_OAUTH_PROFILE_FETCH_FAILED, + ); + } + } + + async signInUsingMicrosoftActiveDirectory(accessCode: string) { + const user = await this.userRepository.findOne({ + where: { + accessCode: accessCode, + }, + relations: { + roles: true, + }, + }); + + if (!user) { + throw new UnauthorizedException(ERROR_MESSAGES.USER_NOT_FOUND); + } + this.checkAccountBlocked(user); + + try { + await this.validateUserUsingMicrosoftActiveDirectory(user); + } catch (e) { + await this.incrementFailedAttempts(user); + throw e; + } + + await this.resetFailedAttempts(user); + const tokens = await this.generateTokens(user); + return { + user: { + email: user.email, + mobile: user.mobile, + username: user.username, + id: user.id, + roles: user.roles.map((role) => role.name), + }, + ...tokens, + }; + } + async signInUsingApple(accessCode: string) { const user = await this.userRepository.findOne({ where: { diff --git a/src/services/settings/default-settings-provider.service.ts b/src/services/settings/default-settings-provider.service.ts index 99728e7e..728bfe6f 100644 --- a/src/services/settings/default-settings-provider.service.ts +++ b/src/services/settings/default-settings-provider.service.ts @@ -43,6 +43,16 @@ const getSolidCoreSettings = (isProd: boolean) => sortOrder: 50, controlType: "boolean", }, + { + moduleName: "solid-core", + key: "iamMicrosoftActiveDirectoryOAuthEnabled", + value: false, + level: SettingLevel.SystemAdminEditable, + label: "Allow Login / Signup With Microsoft Active Directory", + group: "authentication-settings", + sortOrder: 50, + controlType: "boolean", + }, { moduleName: "solid-core", key: "authPagesLayout", @@ -703,6 +713,55 @@ const getSolidCoreSettings = (isProd: boolean) => level: SettingLevel.SystemAdminReadonly, }, + // microsoft-active-directory-oauth-settings-provider.service.ts + { + moduleName: "solid-core", + key: "MICROSOFT_ACTIVE_DIRECTORY_CLIENT_ID", + value: process.env.IAM_MICROSOFT_ACTIVE_DIRECTORY_OAUTH_CLIENT_ID, + level: SettingLevel.SystemAdminReadonly, + label: "Microsoft Active Directory OAuth Client ID", + group: "oauth-settings", + sortOrder: 70, + controlType: "shortText", + }, + { + moduleName: "solid-core", + key: "MICROSOFT_ACTIVE_DIRECTORY_CLIENT_SECRET", + value: process.env.IAM_MICROSOFT_ACTIVE_DIRECTORY_OAUTH_CLIENT_SECRET, + level: SettingLevel.SystemEnv, + }, + { + moduleName: "solid-core", + key: "MICROSOFT_ACTIVE_DIRECTORY_TENANT_ID", + value: + process.env.IAM_MICROSOFT_ACTIVE_DIRECTORY_OAUTH_TENANT_ID || "common", + level: SettingLevel.SystemAdminReadonly, + label: "Microsoft Active Directory OAuth Tenant ID", + group: "oauth-settings", + sortOrder: 80, + controlType: "shortText", + }, + { + moduleName: "solid-core", + key: "MICROSOFT_ACTIVE_DIRECTORY_CALLBACK_URL", + value: process.env.IAM_MICROSOFT_ACTIVE_DIRECTORY_OAUTH_CALLBACK_URL, + level: SettingLevel.SystemAdminReadonly, + label: "Microsoft Active Directory OAuth Callback URL", + group: "oauth-settings", + sortOrder: 90, + controlType: "shortText", + }, + { + moduleName: "solid-core", + key: "MICROSOFT_ACTIVE_DIRECTORY_REDIRECT_URL", + value: process.env.IAM_MICROSOFT_ACTIVE_DIRECTORY_OAUTH_REDIRECT_URL, + level: SettingLevel.SystemAdminReadonly, + label: "Microsoft Active Directory OAuth Redirect URL", + group: "oauth-settings", + sortOrder: 100, + controlType: "shortText", + }, + // iam-settings-provider.service.ts { moduleName: "solid-core", diff --git a/src/services/user.service.ts b/src/services/user.service.ts index b527ef68..e1ca9c07 100755 --- a/src/services/user.service.ts +++ b/src/services/user.service.ts @@ -30,6 +30,23 @@ export class UserService extends CRUDService { return normalized || "facebook_user"; } + private buildMicrosoftActiveDirectoryUsernameBase( + email?: string, + providerId?: string, + name?: string, + ): string { + const source = + email || + name || + (providerId ? `microsoft_active_directory_${providerId}` : ""); + const normalized = source + .trim() + .toLowerCase() + .replace(/[^a-z0-9@._-]+/g, "_") + .replace(/^_+|_+$/g, ""); + return normalized || "microsoft_active_directory_user"; + } + private async resolveUniqueUsername( preferredUsername: string, // fallbackUsername: string, @@ -397,6 +414,50 @@ export class UserService extends CRUDService { return user; } + async resolveUserOnOauthMicrosoftActiveDirectory( + oauthUserDto: OauthUserDto, + ): Promise { + const user = await this.repo.findOne({ + where: { + email: oauthUserDto.email, + }, + relations: { + roles: true, + }, + }); + + if (!user) { + const newUser = new User(); + newUser.username = oauthUserDto.email; + newUser.email = oauthUserDto.email; + newUser.fullName = oauthUserDto.name; + newUser.lastLoginProvider = oauthUserDto.provider; + newUser.accessCode = oauthUserDto.accessCode; + newUser.microsoftActiveDirectoryAccessToken = oauthUserDto.accessToken; + newUser.microsoftActiveDirectoryId = oauthUserDto.providerId; + newUser.microsoftActiveDirectoryProfilePicture = oauthUserDto.picture; + + const savedUser = await this.repo.save(newUser); + + await this.initializeRolesForNewUser( + [this.settingService.getConfigValue("defaultRole")], + savedUser, + ); + } else { + const entity = await this.repo.preload({ + id: user.id, + lastLoginProvider: oauthUserDto.provider, + accessCode: oauthUserDto.accessCode, + microsoftActiveDirectoryAccessToken: oauthUserDto.accessToken, + microsoftActiveDirectoryId: oauthUserDto.providerId, + microsoftActiveDirectoryProfilePicture: oauthUserDto.picture, + }); + + await this.repo.save(entity); + } + return user; + } + async findUsersByRole( roleName: string, relations: any = {}, diff --git a/src/solid-core.module.ts b/src/solid-core.module.ts index 15a4aa9e..a3c94f53 100644 --- a/src/solid-core.module.ts +++ b/src/solid-core.module.ts @@ -66,8 +66,10 @@ import { ActionMetadataService } from "./services/action-metadata.service"; import { FacebookAuthenticationController } from "./controllers/facebook-authentication.controller"; import { MicrosoftAuthenticationController } from "./controllers/microsoft-authentication.controller"; +import { MicrosoftActiveDirectoryAuthenticationController } from "./controllers/microsoft-active-directory-authentication.controller"; import { FacebookOAuthStrategy } from "./passport-strategies/facebook-oauth.strategy"; import { MicrosoftOAuthStrategy } from "./passport-strategies/microsoft-oauth.strategy"; +import { MicrosoftActiveDirectoryOAuthStrategy } from "./passport-strategies/microsoft-active-directory-oauth.strategy"; import { GupshupOtpWhatsappService } from "./services/whatsapp/GupshupOtpWhatsappService"; import { MetaCloudWhatsappService } from "./services/whatsapp/MetaCloudWhatsappService"; @@ -464,6 +466,7 @@ import { DashboardUserLayoutRepository } from './repositories/dashboard-user-lay GoogleAuthenticationController, FacebookAuthenticationController, MicrosoftAuthenticationController, + MicrosoftActiveDirectoryAuthenticationController, ImportTransactionController, ImportTransactionErrorLogController, ListOfValuesController, @@ -637,6 +640,7 @@ import { DashboardUserLayoutRepository } from './repositories/dashboard-user-lay GoogleOauthStrategy, FacebookOAuthStrategy, MicrosoftOAuthStrategy, + MicrosoftActiveDirectoryOAuthStrategy, UserRegistrationListener, TestQueuePublisher, TestQueueSubscriber, From c47be9fa79160a255b5348803be8049b22a7ff46 Mon Sep 17 00:00:00 2001 From: Oswald Rodrigues Date: Tue, 23 Jun 2026 08:23:21 +0530 Subject: [PATCH 136/136] 0.1.10-beta.28 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 62abe41f..9831fb0c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.27", + "version": "0.1.10-beta.28", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@solidxai/core", - "version": "0.1.10-beta.27", + "version": "0.1.10-beta.28", "license": "BUSL-1.1", "dependencies": { "@aws-sdk/client-s3": "^3.637.0", diff --git a/package.json b/package.json index 262e47c5..6ffc30ce 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@solidxai/core", - "version": "0.1.10-beta.27", + "version": "0.1.10-beta.28", "description": "This module is a NestJS module containing all the required core providers required by a Solid application", "main": "dist/index.js", "types": "dist/index.d.ts",