Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions backend/src/common/data-injection.tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ export enum UseCaseType {
AGENTS_GET_AI_TABLE_STRUCTURE = 'AGENTS_GET_AI_TABLE_STRUCTURE',
AGENTS_EXECUTE_AI_RAW_QUERY = 'AGENTS_EXECUTE_AI_RAW_QUERY',
AGENTS_EXECUTE_AI_AGGREGATION_PIPELINE = 'AGENTS_EXECUTE_AI_AGGREGATION_PIPELINE',
AGENTS_GET_AI_SAMPLE_ROWS = 'AGENTS_GET_AI_SAMPLE_ROWS',
AGENTS_SCAN_AND_CREATE_SETTINGS = 'AGENTS_SCAN_AND_CREATE_SETTINGS',
AGENTS_GET_COMPANY_SUBSCRIPTION_INFO = 'AGENTS_GET_COMPANY_SUBSCRIPTION_INFO',

Expand Down
26 changes: 26 additions & 0 deletions backend/src/microservices/agents-microservice/agents.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
AiConnectionContextRO,
AiConnectionTablesRO,
AiQueryResultRO,
AiSampleRowsRO,
CompanySubscriptionInfoRO,
PermissionAllowedRO,
ValidatedUserTokenRO,
Expand All @@ -20,6 +21,7 @@ import {
AiDataRequestBaseDto,
ExecuteAiAggregationPipelineDto,
ExecuteAiRawQueryDto,
GetAiSampleRowsDto,
GetAiTableStructureDto,
} from './dto/agents-ai-data.dtos.js';
import { ValidateConnectionEditDto, ValidateTableAiRequestDto, ValidateUserTokenDto } from './dto/agents-auth.dtos.js';
Expand All @@ -29,6 +31,7 @@ import {
IExecuteAiRawQuery,
IGetAiConnectionContext,
IGetAiConnectionTables,
IGetAiSampleRows,
IGetAiTableStructure,
IGetCompanySubscriptionInfo,
IScanAndCreateSettings,
Expand Down Expand Up @@ -61,6 +64,8 @@ export class AgentsController {
private readonly executeAiRawQueryUseCase: IExecuteAiRawQuery,
@Inject(UseCaseType.AGENTS_EXECUTE_AI_AGGREGATION_PIPELINE)
private readonly executeAiAggregationPipelineUseCase: IExecuteAiAggregationPipeline,
@Inject(UseCaseType.AGENTS_GET_AI_SAMPLE_ROWS)
private readonly getAiSampleRowsUseCase: IGetAiSampleRows,
@Inject(UseCaseType.AGENTS_SCAN_AND_CREATE_SETTINGS)
private readonly scanAndCreateSettingsUseCase: IScanAndCreateSettings,
@Inject(UseCaseType.AGENTS_GET_COMPANY_SUBSCRIPTION_INFO)
Expand Down Expand Up @@ -167,6 +172,27 @@ export class AgentsController {
);
}

@ApiOperation({ summary: 'Fetch permission-filtered sample rows and a row count (grounds website generation)' })
@ApiResponse({ status: 201, type: AiSampleRowsRO })
@ApiBody({ type: GetAiSampleRowsDto })
@Timeout(!isTest() ? TimeoutDefaults.EXTENDED : TimeoutDefaults.EXTENDED_TEST)
@Post('/ai/data/:connectionId/sample-rows')
public async getAiSampleRows(
@SlugUuid('connectionId') connectionId: string,
@Body() body: GetAiSampleRowsDto,
): Promise<AiSampleRowsRO> {
return await this.getAiSampleRowsUseCase.execute(
{
connectionId,
userId: body.userId,
masterPassword: body.masterPassword ?? null,
tableName: body.tableName,
limit: body.limit ?? null,
},
InTransactionEnum.OFF,
);
}

@ApiOperation({ summary: 'Validate and execute a read-only MongoDB aggregation pipeline for the AI tool loop' })
@ApiResponse({ status: 201, type: AiQueryResultRO })
@ApiBody({ type: ExecuteAiAggregationPipelineDto })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ExecuteAiAggregationPipelineUseCase } from './use-cases/execute-ai-aggr
import { ExecuteAiRawQueryUseCase } from './use-cases/execute-ai-raw-query.use.case.js';
import { GetAiConnectionContextUseCase } from './use-cases/get-ai-connection-context.use.case.js';
import { GetAiConnectionTablesUseCase } from './use-cases/get-ai-connection-tables.use.case.js';
import { GetAiSampleRowsUseCase } from './use-cases/get-ai-sample-rows.use.case.js';
import { GetAiTableStructureUseCase } from './use-cases/get-ai-table-structure.use.case.js';
import { GetCompanySubscriptionInfoUseCase } from './use-cases/get-company-subscription-info.use.case.js';
import { ScanAndCreateSettingsUseCase } from './use-cases/scan-and-create-settings.use.case.js';
Expand Down Expand Up @@ -54,6 +55,10 @@ import { ValidateUserTokenUseCase } from './use-cases/validate-user-token.use.ca
provide: UseCaseType.AGENTS_EXECUTE_AI_AGGREGATION_PIPELINE,
useClass: ExecuteAiAggregationPipelineUseCase,
},
{
provide: UseCaseType.AGENTS_GET_AI_SAMPLE_ROWS,
useClass: GetAiSampleRowsUseCase,
},
{
provide: UseCaseType.AGENTS_SCAN_AND_CREATE_SETTINGS,
useClass: ScanAndCreateSettingsUseCase,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,21 @@ export class AiQueryResultRO {
result: unknown;
}

export class AiSampleRowsRO {
@ApiProperty({
type: 'array',
items: { type: 'object', additionalProperties: true },
description: 'Sample rows, filtered to the columns the user may read.',
})
rows: Array<Record<string, unknown>>;

@ApiProperty({ description: 'Total row count in the table (may be an estimate for large datasets).' })
rowCount: number;

@ApiProperty({ description: 'When true, rowCount is an estimate rather than an exact COUNT.' })
largeDataset: boolean;
}

export class AiConnectionTablesRO {
@ApiProperty({ type: [String], description: 'Table names the user is permitted to read on the connection.' })
tables: Array<string>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ export class ExecuteAiRawQueryDs extends AiDataRequestDs {
query: string;
}

export class GetAiSampleRowsDs extends AiDataRequestDs {
tableName: string;
limit: number | null;
}

export class ExecuteAiAggregationPipelineDs extends AiDataRequestDs {
tableName: string;
pipeline: string;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator';
import { IsInt, IsNotEmpty, IsOptional, IsString, IsUUID, Min } from 'class-validator';

export class AiDataRequestBaseDto {
@ApiProperty()
Expand Down Expand Up @@ -33,6 +33,19 @@ export class ExecuteAiRawQueryDto extends AiDataRequestBaseDto {
query: string;
}

export class GetAiSampleRowsDto extends AiDataRequestBaseDto {
@ApiProperty()
@IsString()
@IsNotEmpty()
tableName: string;

@ApiPropertyOptional({ description: 'Max sample rows to return. Clamped server-side to 5.' })
@IsOptional()
@IsInt()
@Min(1)
limit?: number;
}

export class ExecuteAiAggregationPipelineDto extends AiDataRequestBaseDto {
@ApiProperty()
@IsString()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
AiDataRequestDs,
ExecuteAiAggregationPipelineDs,
ExecuteAiRawQueryDs,
GetAiSampleRowsDs,
GetAiTableStructureDs,
GetCompanySubscriptionInfoDs,
ScanAndCreateSettingsDs,
Expand All @@ -13,6 +14,7 @@ import {
AiConnectionContextRO,
AiConnectionTablesRO,
AiQueryResultRO,
AiSampleRowsRO,
CompanySubscriptionInfoRO,
PermissionAllowedRO,
ValidatedUserTokenRO,
Expand Down Expand Up @@ -42,6 +44,10 @@ export interface IGetAiConnectionTables {
execute(inputData: AiDataRequestDs, inTransaction: InTransactionEnum): Promise<AiConnectionTablesRO>;
}

export interface IGetAiSampleRows {
execute(inputData: GetAiSampleRowsDs, inTransaction: InTransactionEnum): Promise<AiSampleRowsRO>;
}

export interface IExecuteAiRawQuery {
execute(inputData: ExecuteAiRawQueryDs, inTransaction: InTransactionEnum): Promise<AiQueryResultRO>;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { Inject, Injectable, Scope } from '@nestjs/common';
import { buildDAOsTableSettingsDs } from '@rocketadmin/shared-code/dist/src/helpers/data-structures-builders/table-settings.ds.builder.js';
import AbstractUseCase from '../../../common/abstract-use.case.js';
import { IGlobalDatabaseContext } from '../../../common/application/global-database-context.interface.js';
import { BaseType } from '../../../common/data-injection.tokens.js';
import { CedarPermissionsService } from '../../../entities/cedar-authorization/cedar-permissions.service.js';
import { buildCommonTableSettingsInput } from '../../../entities/table/utils/build-common-table-settings-input.util.js';
import { GetAiSampleRowsDs } from '../data-structures/agents.ds.js';
import { AiSampleRowsRO } from '../data-structures/agents-responses.ds.js';
import { assertUserCanReadTables, setupAiConnection } from '../utils/ai-data-access.helpers.js';
import { IGetAiSampleRows } from './agents-use-cases.interface.js';

export const AI_SAMPLE_ROWS_MAX_LIMIT = 5;

@Injectable({ scope: Scope.REQUEST })
export class GetAiSampleRowsUseCase
extends AbstractUseCase<GetAiSampleRowsDs, AiSampleRowsRO>
implements IGetAiSampleRows
{
constructor(
@Inject(BaseType.GLOBAL_DB_CONTEXT)
protected _dbContext: IGlobalDatabaseContext,
private readonly cedarPermissions: CedarPermissionsService,
) {
super();
}

protected async implementation(inputData: GetAiSampleRowsDs): Promise<AiSampleRowsRO> {
const { connectionId, userId, masterPassword, tableName, limit } = inputData;

const { foundConnection, dataAccessObject, userEmail } = await setupAiConnection(
this._dbContext,
connectionId,
masterPassword,
userId,
);

await assertUserCanReadTables(this.cedarPermissions, [tableName], userId, foundConnection.id);

const tableStructure = await dataAccessObject.getTableStructure(tableName, userEmail);
const readableColumns = await this.cedarPermissions.getReadableColumns(
userId,
foundConnection.id,
tableName,
tableStructure.map((column) => column.column_name),
);

const perPage = Math.min(limit ?? AI_SAMPLE_ROWS_MAX_LIMIT, AI_SAMPLE_ROWS_MAX_LIMIT);
const settings = buildDAOsTableSettingsDs(buildCommonTableSettingsInput(null), null);
const foundRows = await dataAccessObject.getRowsFromTable(
tableName,
settings,
1,
perPage,
'',
[],
{ fields: [], value: '' },
tableStructure,
userEmail,
);

const rows = foundRows.data.map((row) =>
Object.fromEntries(Object.entries(row).filter(([columnName]) => readableColumns.has(columnName))),
);

return {
rows,
rowCount: foundRows.pagination.total,
largeDataset: foundRows.large_dataset,
};
}
}
Loading
Loading