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
Original file line number Diff line number Diff line change
Expand Up @@ -13,93 +13,100 @@ import { TableActionActivationService } from '../../table-actions-module/table-a

@Injectable()
export class ActivateActionsInEventUseCase
extends AbstractUseCase<ActivateEventActionsDS, ActivatedTableActionsDTO>
implements IActivateTableActionsInRule
extends AbstractUseCase<ActivateEventActionsDS, ActivatedTableActionsDTO>
implements IActivateTableActionsInRule
{
constructor(
@Inject(BaseType.GLOBAL_DB_CONTEXT)
protected _dbContext: IGlobalDatabaseContext,
private tableLogsService: TableLogsService,
private tableActionActivationService: TableActionActivationService,
) {
super();
}
constructor(
@Inject(BaseType.GLOBAL_DB_CONTEXT)
protected _dbContext: IGlobalDatabaseContext,
private tableLogsService: TableLogsService,
private tableActionActivationService: TableActionActivationService,
) {
super();
}

public async implementation(inputData: ActivateEventActionsDS): Promise<ActivatedTableActionsDTO> {
const { connection_data, request_body, event_id } = inputData;
const { connectionId, masterPwd, userId } = connection_data;
let operationResult = OperationResultStatusEnum.unknown;
const foundActionsWithCustomEvents =
await this._dbContext.tableActionRepository.findActionsWithCustomEventsByEventIdConnectionId(
event_id,
connectionId,
);
public async implementation(inputData: ActivateEventActionsDS): Promise<ActivatedTableActionsDTO> {
const { connection_data, request_body, event_id } = inputData;
const { connectionId, masterPwd, userId } = connection_data;
let operationResult = OperationResultStatusEnum.unknown;
const foundActionsWithCustomEvents =
await this._dbContext.tableActionRepository.findActionsWithCustomEventsByEventIdConnectionId(
event_id,
connectionId,
);

if (!foundActionsWithCustomEvents.length) {
throw new HttpException(
{
message: Messages.NO_CUSTOM_ACTIONS_FOUND_FOR_THIS_RULE,
},
HttpStatus.BAD_REQUEST,
);
}
const tableName = foundActionsWithCustomEvents[0].action_rule.table_name;
const canUserReadTable = await this._dbContext.userAccessRepository.checkTableRead(userId, connectionId, tableName, masterPwd);
if (!canUserReadTable) {
throw new ForbiddenException(Messages.DONT_HAVE_PERMISSIONS);
}
if (!foundActionsWithCustomEvents.length) {
throw new HttpException(
{
message: Messages.NO_CUSTOM_ACTIONS_FOUND_FOR_THIS_RULE,
},
HttpStatus.BAD_REQUEST,
);
}
const tableName = foundActionsWithCustomEvents[0].action_rule.table_name;
const canUserReadTable = await this._dbContext.userAccessRepository.checkTableRead(
userId,
connectionId,
tableName,
masterPwd,
);
if (!canUserReadTable) {
throw new ForbiddenException(Messages.DONT_HAVE_PERMISSIONS);
}

const foundConnection = await this._dbContext.connectionRepository.findAndDecryptConnection(
connectionId,
masterPwd,
);
const foundConnection = await this._dbContext.connectionRepository.findAndDecryptConnection(
connectionId,
masterPwd,
);

let locationFromResult: string = null;
const activationResults: Array<{ actionId: string; result: OperationResultStatusEnum }> = [];
let locationFromResult: string = null;
const activationResults: Array<{ actionId: string; result: OperationResultStatusEnum }> = [];

for (const action of foundActionsWithCustomEvents) {
let primaryKeyValuesArray: Array<Record<string, unknown>> = [];
try {
const { receivedOperationResult, receivedPrimaryKeysObj, location } =
await this.tableActionActivationService.activateTableAction(
action,
foundConnection,
request_body,
userId,
tableName,
null,
);
operationResult = receivedOperationResult;
primaryKeyValuesArray = receivedPrimaryKeysObj;
if (location) {
locationFromResult = location;
}
activationResults.push({ actionId: action.id, result: operationResult });
} catch (e) {
operationResult = OperationResultStatusEnum.unsuccessfully;
activationResults.push({ actionId: action.id, result: operationResult });
throw new HttpException(
{
message: e.message,
},
e.response?.status || HttpStatus.BAD_REQUEST,
);
} finally {
for (const primaryKey of primaryKeyValuesArray) {
const logRecord = {
table_name: tableName,
userId: userId,
connection: foundConnection,
operationType: LogOperationTypeEnum.actionActivated,
operationStatusResult: operationResult,
row: primaryKey,
old_data: null,
table_primary_key: primaryKey,
};
await this.tableLogsService.crateAndSaveNewLogUtil(logRecord);
}
}
}
return { location: locationFromResult, activationResults };
}
for (const action of foundActionsWithCustomEvents) {
let primaryKeyValuesArray: Array<Record<string, unknown>> = [];
try {
const { receivedOperationResult, receivedPrimaryKeysObj, location } =
await this.tableActionActivationService.activateTableAction(
action,
foundConnection,
request_body,
userId,
tableName,
null,
);
operationResult = receivedOperationResult;
primaryKeyValuesArray = receivedPrimaryKeysObj;
if (location) {
locationFromResult = location;
}
activationResults.push({ actionId: action.id, result: operationResult });
} catch (e) {
operationResult = OperationResultStatusEnum.unsuccessfully;
activationResults.push({ actionId: action.id, result: operationResult });
throw new HttpException(
{
message: e.message,
},
e.response?.status || HttpStatus.BAD_REQUEST,
);
} finally {
const eventTitle = action.action_rule.action_events?.find((e) => e.id === event_id)?.title ?? null;
for (const primaryKey of primaryKeyValuesArray) {
const logRecord = {
table_name: tableName,
userId: userId,
connection: foundConnection,
operationType: LogOperationTypeEnum.actionActivated,
operationStatusResult: operationResult,
row: primaryKey,
old_data: null,
table_primary_key: primaryKey,

Copilot AI Feb 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This log record uses table_primary_key, but CreateLogRecordDs/TableLogsEntity persist the primary key under affected_primary_key. As written, action activation logs will have affected_primary_key unset, and the extra table_primary_key field is ignored. Pass the primary key via affected_primary_key here (and consider removing table_primary_key if it's not used anywhere).

Suggested change
table_primary_key: primaryKey,
affected_primary_key: primaryKey,

Copilot uses AI. Check for mistakes.
operation_custom_action_name: eventTitle,
};
await this.tableLogsService.crateAndSaveNewLogUtil(logRecord);
}
}
}
return { location: locationFromResult, activationResults };
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import { ConnectionEntity } from '../../../connection/connection.entity.js';
import { LogOperationTypeEnum, OperationResultStatusEnum } from '../../../../enums/index.js';

export class CreateLogRecordDs {
connection: ConnectionEntity;
old_data?: unknown;
operationStatusResult: OperationResultStatusEnum;
operationType: LogOperationTypeEnum;
row?: string | Record<string, unknown>;
table_name: string;
userId: string;
affected_primary_key?: string | Record<string, unknown>;
connection: ConnectionEntity;
old_data?: unknown;
operationStatusResult: OperationResultStatusEnum;
operationType: LogOperationTypeEnum;
row?: string | Record<string, unknown>;
table_name: string;
userId: string;
affected_primary_key?: string | Record<string, unknown>;
operation_custom_action_name?: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ export class FoundLogRecordDs {

@ApiProperty()
table_name: string;

@ApiProperty()
operation_custom_action_name: string | null;
}

export class FoundLogsDs {
Expand Down
3 changes: 3 additions & 0 deletions backend/src/entities/table-logs/table-logs.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ export class TableLogsEntity {
@Column({ default: null })
email: string;

@Column({ default: null })
operation_custom_action_name: string | null;

@Column('enum', {
nullable: false,
enum: LogOperationTypeEnum,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,6 @@ export function buildFoundLogRecordDs(log: TableLogsEntity): FoundLogRecordDs {
createdAt: createdAt,
connection_id: connection_id.id,
affected_primary_key: affected_primary_key,
operation_custom_action_name: log.operation_custom_action_name || null,

Copilot AI Feb 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

operation_custom_action_name: log.operation_custom_action_name || null will coerce empty strings to null. If an event title can be an empty string, this will lose data; prefer using nullish coalescing (?? null) so only undefined/null become null.

Suggested change
operation_custom_action_name: log.operation_custom_action_name || null,
operation_custom_action_name: log.operation_custom_action_name ?? null,

Copilot uses AI. Check for mistakes.
};
}
38 changes: 24 additions & 14 deletions backend/src/entities/table-logs/utils/build-table-logs-entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,28 @@ import { CreateLogRecordDs } from '../application/data-structures/create-log-rec
import { TableLogsEntity } from '../table-logs.entity.js';

export function buildTableLogsEntity(logData: CreateLogRecordDs, userEmail: string): TableLogsEntity {
const { connection, old_data, operationStatusResult, operationType, row, table_name, userId, affected_primary_key } =
logData;
const newLogs = new TableLogsEntity();
newLogs.received_data = row as string;
newLogs.table_name = table_name;
newLogs.cognitoUserName = userId;
newLogs.email = userEmail.toLowerCase();
newLogs.createdAt = new Date();
newLogs.operationType = operationType;
newLogs.operationStatusResult = operationStatusResult;
newLogs.connection_id = connection;
newLogs.old_data = old_data as string;
newLogs.affected_primary_key = affected_primary_key as unknown as string;
return newLogs;
const {
connection,
old_data,
operationStatusResult,
operationType,
row,
table_name,
userId,
affected_primary_key,
operation_custom_action_name,
} = logData;
const newLogs = new TableLogsEntity();
newLogs.received_data = row as string;
newLogs.table_name = table_name;
newLogs.cognitoUserName = userId;
newLogs.email = userEmail.toLowerCase();
newLogs.createdAt = new Date();
newLogs.operationType = operationType;
newLogs.operationStatusResult = operationStatusResult;
newLogs.connection_id = connection;
newLogs.old_data = old_data as string;
newLogs.affected_primary_key = affected_primary_key as unknown as string;
newLogs.operation_custom_action_name = operation_custom_action_name ?? null;
return newLogs;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class AddOperationCustomActionNameToTableLogsEntity1770626855789 implements MigrationInterface {
name = 'AddOperationCustomActionNameToTableLogsEntity1770626855789';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "tableLogs" ADD "operation_custom_action_name" character varying`);
}
Comment on lines +6 to +8

Copilot AI Feb 9, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new column is added without an explicit DEFAULT null, while the entity metadata uses @Column({ default: null }). Consider adding DEFAULT null in the migration to keep the schema consistent with the entity definition and avoid future schema drift/migration diffs.

Copilot uses AI. Check for mistakes.

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "tableLogs" DROP COLUMN "operation_custom_action_name"`);
}
}
88 changes: 88 additions & 0 deletions backend/test/ava-tests/saas-tests/action-rules-e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1328,3 +1328,91 @@ test.serial(`${currentTest} should create trigger and activate http table action
t.truthy(userUpdatedRowSlackMessageRequestBody.text.includes(`[{"id":"1"}]`));
scope.done();
});

currentTest = 'POST /event/actions/activate/:eventId/:connectionId - logs operation_custom_action_name';

test.serial(`${currentTest} should log custom action event title in operation_custom_action_name field`, async (t) => {
const { token } = await registerUserAndReturnUserInfo(app);
const createConnectionResult = await request(app.getHttpServer())
.post('/connection')
.send(newConnection)
.set('Cookie', token)
.set('Content-Type', 'application/json')
.set('Accept', 'application/json');

const createConnectionRO = JSON.parse(createConnectionResult.text);
t.is(createConnectionResult.status, 201);

await resetPostgresTestDB();

const fakeUrl = 'http://www.example.com';
const customEventTitle = 'Test Custom Action Title';
const tableRuleDTO: CreateTableActionRuleBodyDTO = {
title: 'Test rule for logging',
table_name: testTableName,
events: [
{
type: TableActionTypeEnum.single,
event: TableActionEventEnum.CUSTOM,
title: customEventTitle,
icon: 'test-icon',
require_confirmation: false,
},
],
table_actions: [
{
url: fakeUrl,
method: TableActionMethodEnum.URL,
slack_url: undefined,
emails: [],
},
],
};

const createTableRuleResult = await request(app.getHttpServer())
.post(`/action/rule/${createConnectionRO.id}`)
.send(tableRuleDTO)
.set('Cookie', token)
.set('Content-Type', 'application/json')
.set('Accept', 'application/json');

const createTableRuleRO: FoundActionRulesWithActionsAndEventsDTO = JSON.parse(createTableRuleResult.text);
t.is(createTableRuleResult.status, 201);

const scope = nock(fakeUrl)
.post('/')
.reply(201, () => {
return {
status: 201,
message: 'Table action was triggered',
};
});

const activateTableRuleResult = await request(app.getHttpServer())
.post(`/event/actions/activate/${createTableRuleRO.events[0].id}/${createConnectionRO.id}`)
.set('Cookie', token)
.send([{ id: 1 }])
.set('Content-Type', 'application/json')
.set('Accept', 'application/json');
const activateTableRuleRO: ActivatedTableActionsDTO = JSON.parse(activateTableRuleResult.text);
t.is(activateTableRuleResult.status, 201);
t.is(Object.hasOwn(activateTableRuleRO, 'activationResults'), true);

// Check that the custom action activation was logged with the correct event title
const getLogsResponse = await request(app.getHttpServer())
.get(`/logs/${createConnectionRO.id}?tableName=${testTableName}`)
.set('Cookie', token)
.set('Content-Type', 'application/json')
.set('Accept', 'application/json');

t.is(getLogsResponse.status, 200);
const getLogsRO = JSON.parse(getLogsResponse.text);
t.is(Object.hasOwn(getLogsRO, 'logs'), true);
t.is(getLogsRO.logs.length > 0, true);

const actionActivatedLog = getLogsRO.logs.find((log) => log.operationType === 'actionActivated');
t.truthy(actionActivatedLog);
t.is(actionActivatedLog.operation_custom_action_name, customEventTitle);

scope.done();
});
Loading