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
3 changes: 3 additions & 0 deletions backend/src/common/data-injection.tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,9 @@ export enum UseCaseType {
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',
AGENTS_INSERT_AI_ROWS = 'AGENTS_INSERT_AI_ROWS',
AGENTS_UPDATE_AI_ROWS = 'AGENTS_UPDATE_AI_ROWS',
AGENTS_DELETE_AI_ROWS = 'AGENTS_DELETE_AI_ROWS',

SITENOVA_EXECUTE_RAW_QUERY = 'SITENOVA_EXECUTE_RAW_QUERY',
SITENOVA_REGISTER_ENDUSER = 'SITENOVA_REGISTER_ENDUSER',
Expand Down
97 changes: 97 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,9 @@ import {
AiConnectionContextRO,
AiConnectionTablesRO,
AiQueryResultRO,
AiRowsDeletedRO,
AiRowsInsertedRO,
AiRowsUpdatedRO,
AiSampleRowsRO,
CompanySubscriptionInfoRO,
PermissionAllowedRO,
Expand All @@ -21,26 +24,32 @@ import {
} from './data-structures/agents-responses.ds.js';
import {
AiDataRequestBaseDto,
DeleteAiRowsDto,
ExecuteAiAggregationPipelineDto,
ExecuteAiRawQueryDto,
GetAiSampleRowsDto,
GetAiTableStructureDto,
InsertAiRowsDto,
UpdateAiRowsDto,
} from './dto/agents-ai-data.dtos.js';
import { ValidateConnectionEditDto, ValidateTableAiRequestDto, ValidateUserTokenDto } from './dto/agents-auth.dtos.js';
import { GetCompanySubscriptionInfoDto } from './dto/agents-company.dtos.js';
import { SetAgentsPublicPermissionsDto } from './dto/agents-public-permissions.dtos.js';
import { SetSiteRuntimePolicyDto } from './dto/agents-site-runtime-policy.dtos.js';
import {
IDeleteAiRows,
IExecuteAiAggregationPipeline,
IExecuteAiRawQuery,
IGetAiConnectionContext,
IGetAiConnectionTables,
IGetAiSampleRows,
IGetAiTableStructure,
IGetCompanySubscriptionInfo,
IInsertAiRows,
IScanAndCreateSettings,
ISetPublicPermissions,
ISetSiteRuntimePolicy,
IUpdateAiRows,
IValidateConnectionEdit,
IValidateTableAiRequest,
IValidateUserToken,
Expand Down Expand Up @@ -80,6 +89,12 @@ export class AgentsController {
private readonly scanAndCreateSettingsUseCase: IScanAndCreateSettings,
@Inject(UseCaseType.AGENTS_GET_COMPANY_SUBSCRIPTION_INFO)
private readonly getCompanySubscriptionInfoUseCase: IGetCompanySubscriptionInfo,
@Inject(UseCaseType.AGENTS_INSERT_AI_ROWS)
private readonly insertAiRowsUseCase: IInsertAiRows,
@Inject(UseCaseType.AGENTS_UPDATE_AI_ROWS)
private readonly updateAiRowsUseCase: IUpdateAiRows,
@Inject(UseCaseType.AGENTS_DELETE_AI_ROWS)
private readonly deleteAiRowsUseCase: IDeleteAiRows,
) {}

@ApiOperation({ summary: 'Validate an end-user JWT on behalf of the agents microservice' })
Expand Down Expand Up @@ -264,6 +279,88 @@ export class AgentsController {
);
}

@ApiOperation({
summary: 'Insert rows on behalf of a user — SiteNova chat data agent (sitenova plan 85)',
description:
'Re-checks Cedar table:add for the given user, validates each row against the table structure, inserts ' +
'the rows one by one (stops at the first failure and reports how many went in), writes the audit log and ' +
'fires the ADD_ROW table actions. At most 100 rows per call.',
})
@ApiResponse({ status: 201, type: AiRowsInsertedRO })
@ApiBody({ type: InsertAiRowsDto })
@Timeout(!isTest() ? TimeoutDefaults.EXTENDED : TimeoutDefaults.EXTENDED_TEST)
@Post('/ai/data/:connectionId/rows/insert')
public async insertAiRows(
@SlugUuid('connectionId') connectionId: string,
@Body() body: InsertAiRowsDto,
): Promise<AiRowsInsertedRO> {
return await this.insertAiRowsUseCase.execute(
{
connectionId,
userId: body.userId,
masterPassword: body.masterPassword ?? null,
tableName: body.tableName,
rows: body.rows,
},
InTransactionEnum.OFF,
);
}

@ApiOperation({
summary: 'Update rows by primary key on behalf of a user — SiteNova chat data agent (sitenova plan 85)',
description:
'Re-checks Cedar table:edit for the given user, applies one set of new values to every listed primary key ' +
'(a key that names no row refuses the whole call before any write), writes the audit log and fires the ' +
'UPDATE_ROW table actions. At most 100 keys per call.',
})
@ApiResponse({ status: 201, type: AiRowsUpdatedRO })
@ApiBody({ type: UpdateAiRowsDto })
@Timeout(!isTest() ? TimeoutDefaults.EXTENDED : TimeoutDefaults.EXTENDED_TEST)
@Post('/ai/data/:connectionId/rows/update')
public async updateAiRows(
@SlugUuid('connectionId') connectionId: string,
@Body() body: UpdateAiRowsDto,
): Promise<AiRowsUpdatedRO> {
return await this.updateAiRowsUseCase.execute(
{
connectionId,
userId: body.userId,
masterPassword: body.masterPassword ?? null,
tableName: body.tableName,
primaryKeys: body.primaryKeys,
newValues: body.newValues,
},
InTransactionEnum.OFF,
);
}

@ApiOperation({
summary: 'Delete rows by primary key on behalf of a user — SiteNova chat data agent (sitenova plan 85)',
description:
'Re-checks Cedar table:delete for the given user, deletes every listed primary key in one statement (a key ' +
'that names no row refuses the whole call before any write), writes the audit log and fires the DELETE_ROW ' +
'table actions. At most 100 keys per call.',
})
@ApiResponse({ status: 201, type: AiRowsDeletedRO })
@ApiBody({ type: DeleteAiRowsDto })
@Timeout(!isTest() ? TimeoutDefaults.EXTENDED : TimeoutDefaults.EXTENDED_TEST)
@Post('/ai/data/:connectionId/rows/delete')
public async deleteAiRows(
@SlugUuid('connectionId') connectionId: string,
@Body() body: DeleteAiRowsDto,
): Promise<AiRowsDeletedRO> {
return await this.deleteAiRowsUseCase.execute(
{
connectionId,
userId: body.userId,
masterPassword: body.masterPassword ?? null,
tableName: body.tableName,
primaryKeys: body.primaryKeys,
},
InTransactionEnum.OFF,
);
}

@ApiOperation({ summary: 'Run the AI settings/widgets scan, streaming progress chunks' })
@ApiResponse({ status: 201, description: 'Streams progress as newline-delimited JSON chunks.' })
@ApiBody({ type: AiDataRequestBaseDto })
Expand Down
15 changes: 15 additions & 0 deletions backend/src/microservices/agents-microservice/agents.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,19 @@ import { SaaSAuthMiddleware } from '../../authorization/saas-auth.middleware.js'
import { GlobalDatabaseContext } from '../../common/application/global-database-context.js';
import { BaseType, UseCaseType } from '../../common/data-injection.tokens.js';
import { AgentsController } from './agents.controller.js';
import { DeleteAiRowsUseCase } from './use-cases/delete-ai-rows.use.case.js';
import { ExecuteAiAggregationPipelineUseCase } from './use-cases/execute-ai-aggregation-pipeline.use.case.js';
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 { InsertAiRowsUseCase } from './use-cases/insert-ai-rows.use.case.js';
import { ScanAndCreateSettingsUseCase } from './use-cases/scan-and-create-settings.use.case.js';
import { SetPublicPermissionsUseCase } from './use-cases/set-public-permissions.use.case.js';
import { SetSiteRuntimePolicyUseCase } from './use-cases/set-site-runtime-policy.use.case.js';
import { UpdateAiRowsUseCase } from './use-cases/update-ai-rows.use.case.js';
import { ValidateConnectionEditUseCase } from './use-cases/validate-connection-edit.use.case.js';
import { ValidateTableAiRequestUseCase } from './use-cases/validate-table-ai-request.use.case.js';
import { ValidateUserTokenUseCase } from './use-cases/validate-user-token.use.case.js';
Expand Down Expand Up @@ -77,6 +80,18 @@ import { ValidateUserTokenUseCase } from './use-cases/validate-user-token.use.ca
provide: UseCaseType.AGENTS_GET_COMPANY_SUBSCRIPTION_INFO,
useClass: GetCompanySubscriptionInfoUseCase,
},
{
provide: UseCaseType.AGENTS_INSERT_AI_ROWS,
useClass: InsertAiRowsUseCase,
},
{
provide: UseCaseType.AGENTS_UPDATE_AI_ROWS,
useClass: UpdateAiRowsUseCase,
},
{
provide: UseCaseType.AGENTS_DELETE_AI_ROWS,
useClass: DeleteAiRowsUseCase,
},
],
controllers: [AgentsController],
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,39 @@ export class CompanySubscriptionInfoRO {
@ApiProperty()
isPaymentMethodAdded: boolean;
}

export class AiRowWriteFailureRO {
@ApiProperty({ description: 'Zero-based index of the first row that failed. The rows before it were inserted.' })
rowIndex: number;

@ApiProperty({ description: 'The database or validation message for that row.' })
message: string;
}

export class AiRowsInsertedRO {
@ApiProperty({ description: 'How many rows the call inserted.' })
insertedCount: number;

@ApiProperty({
type: 'array',
items: { type: 'object', additionalProperties: true },
description: 'The primary key of each inserted row, in request order.',
})
insertedPrimaryKeys: Array<Record<string, unknown>>;

@ApiPropertyOptional({
type: AiRowWriteFailureRO,
description: 'Present when a row failed. Inserts stop at the first failure.',
})
failure?: AiRowWriteFailureRO;
}

export class AiRowsUpdatedRO {
@ApiProperty({ description: 'How many rows the call updated.' })
updatedCount: number;
}

export class AiRowsDeletedRO {
@ApiProperty({ description: 'How many rows the call deleted.' })
deletedCount: number;
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,20 @@ export class ScanAndCreateSettingsDs extends AiDataRequestDs {
export class GetCompanySubscriptionInfoDs {
userId: string;
}

// Row writes for the SiteNova chat data agent (sitenova plan 85).
export class InsertAiRowsDs extends AiDataRequestDs {
tableName: string;
rows: Array<Record<string, unknown>>;
}

export class UpdateAiRowsDs extends AiDataRequestDs {
tableName: string;
primaryKeys: Array<Record<string, unknown>>;
newValues: Record<string, unknown>;
}

export class DeleteAiRowsDs extends AiDataRequestDs {
tableName: string;
primaryKeys: Array<Record<string, unknown>>;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsInt, IsNotEmpty, IsOptional, IsString, IsUUID, Min } from 'class-validator';
import {
ArrayMaxSize,
ArrayMinSize,
IsArray,
IsInt,
IsNotEmpty,
IsNotEmptyObject,
IsObject,
IsOptional,
IsString,
IsUUID,
Min,
} from 'class-validator';

export class AiDataRequestBaseDto {
@ApiProperty()
Expand Down Expand Up @@ -57,3 +69,70 @@ export class ExecuteAiAggregationPipelineDto extends AiDataRequestBaseDto {
@IsNotEmpty()
pipeline: string;
}

// Row writes for the SiteNova chat data agent (sitenova plan 85). Each call names one table and
// at most AI_ROW_WRITE_MAX_ROWS rows, so one model tool call has a bounded blast radius.
export const AI_ROW_WRITE_MAX_ROWS = 100;

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

@ApiProperty({
type: 'array',
items: { type: 'object', additionalProperties: true },
description: `Rows to insert, column name to value. 1 to ${AI_ROW_WRITE_MAX_ROWS} rows.`,
})
@IsArray()
@ArrayMinSize(1)
@ArrayMaxSize(AI_ROW_WRITE_MAX_ROWS)
@IsObject({ each: true })
rows: Array<Record<string, unknown>>;
}

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

@ApiProperty({
type: 'array',
items: { type: 'object', additionalProperties: true },
description: `Primary keys of the rows to update. 1 to ${AI_ROW_WRITE_MAX_ROWS} keys.`,
})
@IsArray()
@ArrayMinSize(1)
@ArrayMaxSize(AI_ROW_WRITE_MAX_ROWS)
@IsObject({ each: true })
primaryKeys: Array<Record<string, unknown>>;

@ApiProperty({
type: 'object',
additionalProperties: true,
description: 'Column name to new value, applied to every listed row.',
})
@IsObject()
@IsNotEmptyObject()
newValues: Record<string, unknown>;
}

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

@ApiProperty({
type: 'array',
items: { type: 'object', additionalProperties: true },
description: `Primary keys of the rows to delete. 1 to ${AI_ROW_WRITE_MAX_ROWS} keys.`,
})
@IsArray()
@ArrayMinSize(1)
@ArrayMaxSize(AI_ROW_WRITE_MAX_ROWS)
@IsObject({ each: true })
primaryKeys: Array<Record<string, unknown>>;
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import { InTransactionEnum } from '../../../enums/in-transaction.enum.js';
import {
AiDataRequestDs,
DeleteAiRowsDs,
ExecuteAiAggregationPipelineDs,
ExecuteAiRawQueryDs,
GetAiSampleRowsDs,
GetAiTableStructureDs,
GetCompanySubscriptionInfoDs,
InsertAiRowsDs,
ScanAndCreateSettingsDs,
SetPublicPermissionsDs,
SetSiteRuntimePolicyDs,
UpdateAiRowsDs,
ValidateConnectionEditDs,
ValidateTableAiRequestDs,
ValidateUserTokenDs,
Expand All @@ -17,6 +20,9 @@ import {
AiConnectionContextRO,
AiConnectionTablesRO,
AiQueryResultRO,
AiRowsDeletedRO,
AiRowsInsertedRO,
AiRowsUpdatedRO,
AiSampleRowsRO,
CompanySubscriptionInfoRO,
PermissionAllowedRO,
Expand Down Expand Up @@ -79,3 +85,16 @@ export interface IGetCompanySubscriptionInfo {
inTransaction: InTransactionEnum,
): Promise<CompanySubscriptionInfoRO>;
}

// Row writes for the SiteNova chat data agent (sitenova plan 85).
export interface IInsertAiRows {
execute(inputData: InsertAiRowsDs, inTransaction: InTransactionEnum): Promise<AiRowsInsertedRO>;
}

export interface IUpdateAiRows {
execute(inputData: UpdateAiRowsDs, inTransaction: InTransactionEnum): Promise<AiRowsUpdatedRO>;
}

export interface IDeleteAiRows {
execute(inputData: DeleteAiRowsDs, inTransaction: InTransactionEnum): Promise<AiRowsDeletedRO>;
}
Loading
Loading