Skip to content

Commit 8d22503

Browse files
authored
Merge pull request #1848 from rocket-admin/backend-frontend_agent
Backend frontend agent
2 parents 6c46258 + a0f985e commit 8d22503

12 files changed

Lines changed: 351 additions & 0 deletions

backend/src/common/data-injection.tokens.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,10 +181,12 @@ export enum UseCaseType {
181181
AGENTS_VALIDATE_TABLE_AI_REQUEST = 'AGENTS_VALIDATE_TABLE_AI_REQUEST',
182182
AGENTS_VALIDATE_CONNECTION_EDIT = 'AGENTS_VALIDATE_CONNECTION_EDIT',
183183
AGENTS_GET_AI_CONNECTION_CONTEXT = 'AGENTS_GET_AI_CONNECTION_CONTEXT',
184+
AGENTS_GET_AI_CONNECTION_TABLES = 'AGENTS_GET_AI_CONNECTION_TABLES',
184185
AGENTS_GET_AI_TABLE_STRUCTURE = 'AGENTS_GET_AI_TABLE_STRUCTURE',
185186
AGENTS_EXECUTE_AI_RAW_QUERY = 'AGENTS_EXECUTE_AI_RAW_QUERY',
186187
AGENTS_EXECUTE_AI_AGGREGATION_PIPELINE = 'AGENTS_EXECUTE_AI_AGGREGATION_PIPELINE',
187188
AGENTS_SCAN_AND_CREATE_SETTINGS = 'AGENTS_SCAN_AND_CREATE_SETTINGS',
189+
AGENTS_GET_COMPANY_SUBSCRIPTION_INFO = 'AGENTS_GET_COMPANY_SUBSCRIPTION_INFO',
188190

189191
CREATE_TABLE_FILTERS = 'CREATE_TABLE_FILTERS',
190192
FIND_TABLE_FILTERS = 'FIND_TABLE_FILTERS',

backend/src/main.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { WinstonLogger } from './entities/logging/winston-logger.js';
1212
import { AllExceptionsFilter } from './exceptions/all-exceptions.filter.js';
1313
import { ValidationException } from './exceptions/custom-exceptions/validation-exception.js';
1414
import { Constants } from './helpers/constants/constants.js';
15+
import { publicCrudCorsMiddleware } from './middlewares/public-crud-cors.middleware.js';
1516
import { appConfig } from './shared/config/app-config.js';
1617

1718
async function bootstrap() {
@@ -38,6 +39,10 @@ async function bootstrap() {
3839

3940
app.use(helmet());
4041

42+
// Wildcard CORS for the public table CRUD routes — registered before the global enableCors()
43+
// so it owns these routes (including the OPTIONS preflight) before the global allowlist runs.
44+
app.use(publicCrudCorsMiddleware);
45+
4146
app.enableCors({
4247
origin: [
4348
'https://app.autoadmin.org',

backend/src/microservices/agents-microservice/agents.controller.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import { isTest } from '../../helpers/app/is-test.js';
1010
import { SentryInterceptor } from '../../interceptors/sentry.interceptor.js';
1111
import {
1212
AiConnectionContextRO,
13+
AiConnectionTablesRO,
1314
AiQueryResultRO,
15+
CompanySubscriptionInfoRO,
1416
PermissionAllowedRO,
1517
ValidatedUserTokenRO,
1618
} from './data-structures/agents-responses.ds.js';
@@ -21,11 +23,14 @@ import {
2123
GetAiTableStructureDto,
2224
} from './dto/agents-ai-data.dtos.js';
2325
import { ValidateConnectionEditDto, ValidateTableAiRequestDto, ValidateUserTokenDto } from './dto/agents-auth.dtos.js';
26+
import { GetCompanySubscriptionInfoDto } from './dto/agents-company.dtos.js';
2427
import {
2528
IExecuteAiAggregationPipeline,
2629
IExecuteAiRawQuery,
2730
IGetAiConnectionContext,
31+
IGetAiConnectionTables,
2832
IGetAiTableStructure,
33+
IGetCompanySubscriptionInfo,
2934
IScanAndCreateSettings,
3035
IValidateConnectionEdit,
3136
IValidateTableAiRequest,
@@ -48,6 +53,8 @@ export class AgentsController {
4853
private readonly validateConnectionEditUseCase: IValidateConnectionEdit,
4954
@Inject(UseCaseType.AGENTS_GET_AI_CONNECTION_CONTEXT)
5055
private readonly getAiConnectionContextUseCase: IGetAiConnectionContext,
56+
@Inject(UseCaseType.AGENTS_GET_AI_CONNECTION_TABLES)
57+
private readonly getAiConnectionTablesUseCase: IGetAiConnectionTables,
5158
@Inject(UseCaseType.AGENTS_GET_AI_TABLE_STRUCTURE)
5259
private readonly getAiTableStructureUseCase: IGetAiTableStructure,
5360
@Inject(UseCaseType.AGENTS_EXECUTE_AI_RAW_QUERY)
@@ -56,6 +63,8 @@ export class AgentsController {
5663
private readonly executeAiAggregationPipelineUseCase: IExecuteAiAggregationPipeline,
5764
@Inject(UseCaseType.AGENTS_SCAN_AND_CREATE_SETTINGS)
5865
private readonly scanAndCreateSettingsUseCase: IScanAndCreateSettings,
66+
@Inject(UseCaseType.AGENTS_GET_COMPANY_SUBSCRIPTION_INFO)
67+
private readonly getCompanySubscriptionInfoUseCase: IGetCompanySubscriptionInfo,
5968
) {}
6069

6170
@ApiOperation({ summary: 'Validate an end-user JWT on behalf of the agents microservice' })
@@ -102,6 +111,21 @@ export class AgentsController {
102111
);
103112
}
104113

114+
@ApiOperation({ summary: 'List connection tables the user may read (grounds website feasibility)' })
115+
@ApiResponse({ status: 201, type: AiConnectionTablesRO })
116+
@ApiBody({ type: AiDataRequestBaseDto })
117+
@Timeout(!isTest() ? TimeoutDefaults.EXTENDED : TimeoutDefaults.EXTENDED_TEST)
118+
@Post('/ai/data/:connectionId/tables')
119+
public async getAiConnectionTables(
120+
@SlugUuid('connectionId') connectionId: string,
121+
@Body() body: AiDataRequestBaseDto,
122+
): Promise<AiConnectionTablesRO> {
123+
return await this.getAiConnectionTablesUseCase.execute(
124+
{ connectionId, userId: body.userId, masterPassword: body.masterPassword ?? null },
125+
InTransactionEnum.OFF,
126+
);
127+
}
128+
105129
@ApiOperation({ summary: 'Get permission-aware table structure for the AI tool loop' })
106130
@ApiResponse({ status: 201, description: 'Table structure with related tables.' })
107131
@ApiBody({ type: GetAiTableStructureDto })
@@ -184,4 +208,14 @@ export class AgentsController {
184208
InTransactionEnum.OFF,
185209
);
186210
}
211+
212+
@ApiOperation({ summary: "Read a user's company subscription metadata (agents-core owns all feature policy)" })
213+
@ApiResponse({ status: 201, type: CompanySubscriptionInfoRO })
214+
@ApiBody({ type: GetCompanySubscriptionInfoDto })
215+
@Post('/company/subscription-info')
216+
public async getCompanySubscriptionInfo(
217+
@Body() body: GetCompanySubscriptionInfoDto,
218+
): Promise<CompanySubscriptionInfoRO> {
219+
return await this.getCompanySubscriptionInfoUseCase.execute({ userId: body.userId }, InTransactionEnum.OFF);
220+
}
187221
}

backend/src/microservices/agents-microservice/agents.module.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import { AgentsController } from './agents.controller.js';
77
import { ExecuteAiAggregationPipelineUseCase } from './use-cases/execute-ai-aggregation-pipeline.use.case.js';
88
import { ExecuteAiRawQueryUseCase } from './use-cases/execute-ai-raw-query.use.case.js';
99
import { GetAiConnectionContextUseCase } from './use-cases/get-ai-connection-context.use.case.js';
10+
import { GetAiConnectionTablesUseCase } from './use-cases/get-ai-connection-tables.use.case.js';
1011
import { GetAiTableStructureUseCase } from './use-cases/get-ai-table-structure.use.case.js';
12+
import { GetCompanySubscriptionInfoUseCase } from './use-cases/get-company-subscription-info.use.case.js';
1113
import { ScanAndCreateSettingsUseCase } from './use-cases/scan-and-create-settings.use.case.js';
1214
import { ValidateConnectionEditUseCase } from './use-cases/validate-connection-edit.use.case.js';
1315
import { ValidateTableAiRequestUseCase } from './use-cases/validate-table-ai-request.use.case.js';
@@ -36,6 +38,10 @@ import { ValidateUserTokenUseCase } from './use-cases/validate-user-token.use.ca
3638
provide: UseCaseType.AGENTS_GET_AI_CONNECTION_CONTEXT,
3739
useClass: GetAiConnectionContextUseCase,
3840
},
41+
{
42+
provide: UseCaseType.AGENTS_GET_AI_CONNECTION_TABLES,
43+
useClass: GetAiConnectionTablesUseCase,
44+
},
3945
{
4046
provide: UseCaseType.AGENTS_GET_AI_TABLE_STRUCTURE,
4147
useClass: GetAiTableStructureUseCase,
@@ -52,6 +58,10 @@ import { ValidateUserTokenUseCase } from './use-cases/validate-user-token.use.ca
5258
provide: UseCaseType.AGENTS_SCAN_AND_CREATE_SETTINGS,
5359
useClass: ScanAndCreateSettingsUseCase,
5460
},
61+
{
62+
provide: UseCaseType.AGENTS_GET_COMPANY_SUBSCRIPTION_INFO,
63+
useClass: GetCompanySubscriptionInfoUseCase,
64+
},
5565
],
5666
controllers: [AgentsController],
5767
})

backend/src/microservices/agents-microservice/data-structures/agents-responses.ds.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,22 @@ export class AiQueryResultRO {
4040
@ApiProperty()
4141
result: unknown;
4242
}
43+
44+
export class AiConnectionTablesRO {
45+
@ApiProperty({ type: [String], description: 'Table names the user is permitted to read on the connection.' })
46+
tables: Array<string>;
47+
}
48+
49+
export class CompanySubscriptionInfoRO {
50+
@ApiProperty({ description: 'Whether the backend is running in SaaS mode. When false, no subscription applies.' })
51+
isSaaS: boolean;
52+
53+
@ApiPropertyOptional({ nullable: true })
54+
companyId: string | null;
55+
56+
@ApiPropertyOptional({ nullable: true, description: 'FREE_PLAN | TEAM_PLAN | ENTERPRISE_PLAN | ANNUAL_* | null' })
57+
subscriptionLevel: string | null;
58+
59+
@ApiProperty()
60+
isPaymentMethodAdded: boolean;
61+
}

backend/src/microservices/agents-microservice/data-structures/agents.ds.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,7 @@ export class ExecuteAiAggregationPipelineDs extends AiDataRequestDs {
3434
export class ScanAndCreateSettingsDs extends AiDataRequestDs {
3535
response: Response;
3636
}
37+
38+
export class GetCompanySubscriptionInfoDs {
39+
userId: string;
40+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { ApiProperty } from '@nestjs/swagger';
2+
import { IsNotEmpty, IsString } from 'class-validator';
3+
4+
export class GetCompanySubscriptionInfoDto {
5+
@ApiProperty()
6+
@IsString()
7+
@IsNotEmpty()
8+
userId: string;
9+
}

backend/src/microservices/agents-microservice/use-cases/agents-use-cases.interface.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,16 @@ import {
44
ExecuteAiAggregationPipelineDs,
55
ExecuteAiRawQueryDs,
66
GetAiTableStructureDs,
7+
GetCompanySubscriptionInfoDs,
78
ScanAndCreateSettingsDs,
89
ValidateConnectionEditDs,
910
ValidateTableAiRequestDs,
1011
} from '../data-structures/agents.ds.js';
1112
import {
1213
AiConnectionContextRO,
14+
AiConnectionTablesRO,
1315
AiQueryResultRO,
16+
CompanySubscriptionInfoRO,
1417
PermissionAllowedRO,
1518
ValidatedUserTokenRO,
1619
} from '../data-structures/agents-responses.ds.js';
@@ -35,6 +38,10 @@ export interface IGetAiTableStructure {
3538
execute(inputData: GetAiTableStructureDs, inTransaction: InTransactionEnum): Promise<Record<string, unknown>>;
3639
}
3740

41+
export interface IGetAiConnectionTables {
42+
execute(inputData: AiDataRequestDs, inTransaction: InTransactionEnum): Promise<AiConnectionTablesRO>;
43+
}
44+
3845
export interface IExecuteAiRawQuery {
3946
execute(inputData: ExecuteAiRawQueryDs, inTransaction: InTransactionEnum): Promise<AiQueryResultRO>;
4047
}
@@ -46,3 +53,10 @@ export interface IExecuteAiAggregationPipeline {
4653
export interface IScanAndCreateSettings {
4754
execute(inputData: ScanAndCreateSettingsDs, inTransaction: InTransactionEnum): Promise<void>;
4855
}
56+
57+
export interface IGetCompanySubscriptionInfo {
58+
execute(
59+
inputData: GetCompanySubscriptionInfoDs,
60+
inTransaction: InTransactionEnum,
61+
): Promise<CompanySubscriptionInfoRO>;
62+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { Inject, Injectable, Scope } from '@nestjs/common';
2+
import AbstractUseCase from '../../../common/abstract-use.case.js';
3+
import { IGlobalDatabaseContext } from '../../../common/application/global-database-context.interface.js';
4+
import { BaseType } from '../../../common/data-injection.tokens.js';
5+
import { CedarPermissionsService } from '../../../entities/cedar-authorization/cedar-permissions.service.js';
6+
import { AiDataRequestDs } from '../data-structures/agents.ds.js';
7+
import { AiConnectionTablesRO } from '../data-structures/agents-responses.ds.js';
8+
import { setupAiConnection } from '../utils/ai-data-access.helpers.js';
9+
import { IGetAiConnectionTables } from './agents-use-cases.interface.js';
10+
11+
@Injectable({ scope: Scope.REQUEST })
12+
export class GetAiConnectionTablesUseCase
13+
extends AbstractUseCase<AiDataRequestDs, AiConnectionTablesRO>
14+
implements IGetAiConnectionTables
15+
{
16+
constructor(
17+
@Inject(BaseType.GLOBAL_DB_CONTEXT)
18+
protected _dbContext: IGlobalDatabaseContext,
19+
private readonly cedarPermissions: CedarPermissionsService,
20+
) {
21+
super();
22+
}
23+
24+
protected async implementation(inputData: AiDataRequestDs): Promise<AiConnectionTablesRO> {
25+
const { connectionId, userId, masterPassword } = inputData;
26+
27+
const { foundConnection, dataAccessObject } = await setupAiConnection(
28+
this._dbContext,
29+
connectionId,
30+
masterPassword,
31+
userId,
32+
);
33+
34+
const tables = await dataAccessObject.getTablesFromDB();
35+
const tableNames = tables.map((table) => table.tableName?.trim()).filter((name): name is string => Boolean(name));
36+
37+
const readableFlags = await Promise.all(
38+
tableNames.map((tableName) =>
39+
this.cedarPermissions.improvedCheckTableRead(userId, foundConnection.id, tableName),
40+
),
41+
);
42+
const readableTableNames = tableNames.filter((_name, index) => readableFlags[index]);
43+
44+
return { tables: readableTableNames };
45+
}
46+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { Inject, Injectable, NotFoundException, Scope } from '@nestjs/common';
2+
import AbstractUseCase from '../../../common/abstract-use.case.js';
3+
import { IGlobalDatabaseContext } from '../../../common/application/global-database-context.interface.js';
4+
import { BaseType } from '../../../common/data-injection.tokens.js';
5+
import { SubscriptionLevelEnum } from '../../../enums/subscription-level.enum.js';
6+
import { Messages } from '../../../exceptions/text/messages.js';
7+
import { isSaaS } from '../../../helpers/app/is-saas.js';
8+
import { isTest } from '../../../helpers/app/is-test.js';
9+
import { SaasCompanyGatewayService } from '../../gateways/saas-gateway.ts/saas-company-gateway.service.js';
10+
import { GetCompanySubscriptionInfoDs } from '../data-structures/agents.ds.js';
11+
import { CompanySubscriptionInfoRO } from '../data-structures/agents-responses.ds.js';
12+
import { IGetCompanySubscriptionInfo } from './agents-use-cases.interface.js';
13+
14+
/**
15+
* Thin metadata provider for the agents microservice: resolves a user's company subscription level
16+
* via the saas gateway. The agents service (agents-core) owns all website-generation policy
17+
* (model tier, hosting caps, quota enforcement) and only reads this subscription metadata from here.
18+
*/
19+
@Injectable({ scope: Scope.REQUEST })
20+
export class GetCompanySubscriptionInfoUseCase
21+
extends AbstractUseCase<GetCompanySubscriptionInfoDs, CompanySubscriptionInfoRO>
22+
implements IGetCompanySubscriptionInfo
23+
{
24+
constructor(
25+
@Inject(BaseType.GLOBAL_DB_CONTEXT)
26+
protected _dbContext: IGlobalDatabaseContext,
27+
private readonly saasCompanyGatewayService: SaasCompanyGatewayService,
28+
) {
29+
super();
30+
}
31+
32+
protected async implementation(inputData: GetCompanySubscriptionInfoDs): Promise<CompanySubscriptionInfoRO> {
33+
const { userId } = inputData;
34+
35+
// Self-hosted / non-SaaS / test runs have no subscription concept.
36+
if (!isSaaS() || isTest()) {
37+
return { isSaaS: false, companyId: null, subscriptionLevel: null, isPaymentMethodAdded: false };
38+
}
39+
40+
const company = await this._dbContext.companyInfoRepository.findCompanyInfoByUserId(userId);
41+
if (!company) {
42+
throw new NotFoundException(Messages.COMPANY_NOT_FOUND);
43+
}
44+
45+
const companyInfo = await this.saasCompanyGatewayService.getCompanyInfo(company.id);
46+
return {
47+
isSaaS: true,
48+
companyId: company.id,
49+
subscriptionLevel: companyInfo?.subscriptionLevel ?? SubscriptionLevelEnum.FREE_PLAN,
50+
isPaymentMethodAdded: companyInfo?.is_payment_method_added ?? false,
51+
};
52+
}
53+
}

0 commit comments

Comments
 (0)