feat: mutualize CRUD tools between workflows and AI (#14996)
Closes [#1662](https://github.com/twentyhq/core-team-issues/issues/1662)
This commit is contained in:
+19
-69
@@ -1,34 +1,25 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { isDefined } from 'class-validator';
|
||||
import { resolveInput } from 'twenty-shared/utils';
|
||||
import { canObjectBeManagedByWorkflow } from 'twenty-shared/workflow';
|
||||
|
||||
import { type WorkflowAction } from 'src/modules/workflow/workflow-executor/interfaces/workflow-action.interface';
|
||||
|
||||
import { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service';
|
||||
import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service';
|
||||
import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||
import {
|
||||
RecordCrudException,
|
||||
RecordCrudExceptionCode,
|
||||
} from 'src/engine/core-modules/record-crud/exceptions/record-crud.exception';
|
||||
import { CreateRecordService } from 'src/engine/core-modules/record-crud/services/create-record.service';
|
||||
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service';
|
||||
import { type WorkflowActionInput } from 'src/modules/workflow/workflow-executor/types/workflow-action-input';
|
||||
import { type WorkflowActionOutput } from 'src/modules/workflow/workflow-executor/types/workflow-action-output.type';
|
||||
import { findStepOrThrow } from 'src/modules/workflow/workflow-executor/utils/find-step-or-throw.util';
|
||||
import {
|
||||
RecordCRUDActionException,
|
||||
RecordCRUDActionExceptionCode,
|
||||
} from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/exceptions/record-crud-action.exception';
|
||||
import { type WorkflowCreateRecordActionInput } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/types/workflow-record-crud-action-input.type';
|
||||
|
||||
@Injectable()
|
||||
export class CreateRecordWorkflowAction implements WorkflowAction {
|
||||
constructor(
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
private readonly createRecordService: CreateRecordService,
|
||||
private readonly scopedWorkspaceContextFactory: ScopedWorkspaceContextFactory,
|
||||
private readonly recordPositionService: RecordPositionService,
|
||||
private readonly recordInputTransformerService: RecordInputTransformerService,
|
||||
private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService,
|
||||
) {}
|
||||
|
||||
async execute({
|
||||
@@ -41,12 +32,12 @@ export class CreateRecordWorkflowAction implements WorkflowAction {
|
||||
stepId: currentStepId,
|
||||
});
|
||||
|
||||
const workspaceId = this.scopedWorkspaceContextFactory.create().workspaceId;
|
||||
const { workspaceId } = this.scopedWorkspaceContextFactory.create();
|
||||
|
||||
if (!workspaceId) {
|
||||
throw new RecordCRUDActionException(
|
||||
throw new RecordCrudException(
|
||||
'Failed to create: Workspace ID is required',
|
||||
RecordCRUDActionExceptionCode.INVALID_REQUEST,
|
||||
RecordCrudExceptionCode.INVALID_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -55,62 +46,21 @@ export class CreateRecordWorkflowAction implements WorkflowAction {
|
||||
context,
|
||||
) as WorkflowCreateRecordActionInput;
|
||||
|
||||
const repository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
workflowActionInput.objectName,
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const { objectMetadataItemWithFieldsMaps } =
|
||||
await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps(
|
||||
workflowActionInput.objectName,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (
|
||||
!canObjectBeManagedByWorkflow({
|
||||
nameSingular: objectMetadataItemWithFieldsMaps.nameSingular,
|
||||
isSystem: objectMetadataItemWithFieldsMaps.isSystem,
|
||||
})
|
||||
) {
|
||||
throw new RecordCRUDActionException(
|
||||
'Failed to create: Object cannot be created by workflow',
|
||||
RecordCRUDActionExceptionCode.INVALID_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
const position = await this.recordPositionService.buildRecordPosition({
|
||||
value: 'first',
|
||||
objectMetadata: objectMetadataItemWithFieldsMaps,
|
||||
const toolOutput = await this.createRecordService.execute({
|
||||
objectName: workflowActionInput.objectName,
|
||||
objectRecord: workflowActionInput.objectRecord,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
const validObjectRecord = Object.fromEntries(
|
||||
Object.entries(workflowActionInput.objectRecord).filter(([key]) =>
|
||||
isDefined(objectMetadataItemWithFieldsMaps.fieldIdByName[key]),
|
||||
),
|
||||
);
|
||||
|
||||
const transformedObjectRecord =
|
||||
await this.recordInputTransformerService.process({
|
||||
recordInput: validObjectRecord,
|
||||
objectMetadataMapItem: objectMetadataItemWithFieldsMaps,
|
||||
});
|
||||
|
||||
const insertResult = await repository.insert({
|
||||
...transformedObjectRecord,
|
||||
position,
|
||||
createdBy: {
|
||||
source: FieldActorSource.WORKFLOW,
|
||||
name: 'Workflow',
|
||||
},
|
||||
});
|
||||
|
||||
const [createdRecord] = insertResult.generatedMaps;
|
||||
if (!toolOutput.success) {
|
||||
throw new RecordCrudException(
|
||||
toolOutput.error || toolOutput.message,
|
||||
RecordCrudExceptionCode.RECORD_CREATION_FAILED,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
result: createdRecord,
|
||||
result: toolOutput.result,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+23
-59
@@ -1,14 +1,15 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { isDefined } from 'class-validator';
|
||||
import { isValidUuid, resolveInput } from 'twenty-shared/utils';
|
||||
import { canObjectBeManagedByWorkflow } from 'twenty-shared/workflow';
|
||||
import { isDefined, isValidUuid, resolveInput } from 'twenty-shared/utils';
|
||||
|
||||
import { type WorkflowAction } from 'src/modules/workflow/workflow-executor/interfaces/workflow-action.interface';
|
||||
|
||||
import {
|
||||
RecordCrudException,
|
||||
RecordCrudExceptionCode,
|
||||
} from 'src/engine/core-modules/record-crud/exceptions/record-crud.exception';
|
||||
import { DeleteRecordService } from 'src/engine/core-modules/record-crud/services/delete-record.service';
|
||||
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service';
|
||||
import {
|
||||
WorkflowStepExecutorException,
|
||||
WorkflowStepExecutorExceptionCode,
|
||||
@@ -16,18 +17,13 @@ import {
|
||||
import { type WorkflowActionInput } from 'src/modules/workflow/workflow-executor/types/workflow-action-input';
|
||||
import { type WorkflowActionOutput } from 'src/modules/workflow/workflow-executor/types/workflow-action-output.type';
|
||||
import { findStepOrThrow } from 'src/modules/workflow/workflow-executor/utils/find-step-or-throw.util';
|
||||
import {
|
||||
RecordCRUDActionException,
|
||||
RecordCRUDActionExceptionCode,
|
||||
} from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/exceptions/record-crud-action.exception';
|
||||
import { isWorkflowDeleteRecordAction } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/guards/is-workflow-delete-record-action.guard';
|
||||
import { type WorkflowDeleteRecordActionInput } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/types/workflow-record-crud-action-input.type';
|
||||
|
||||
@Injectable()
|
||||
export class DeleteRecordWorkflowAction implements WorkflowAction {
|
||||
constructor(
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService,
|
||||
private readonly deleteRecordService: DeleteRecordService,
|
||||
private readonly scopedWorkspaceContextFactory: ScopedWorkspaceContextFactory,
|
||||
) {}
|
||||
|
||||
@@ -58,69 +54,37 @@ export class DeleteRecordWorkflowAction implements WorkflowAction {
|
||||
!isValidUuid(workflowActionInput.objectRecordId) ||
|
||||
!isDefined(workflowActionInput.objectName)
|
||||
) {
|
||||
throw new RecordCRUDActionException(
|
||||
'Failed to update: Object record ID and name are required',
|
||||
RecordCRUDActionExceptionCode.INVALID_REQUEST,
|
||||
throw new RecordCrudException(
|
||||
'Failed to delete: Object record ID and name are required',
|
||||
RecordCrudExceptionCode.INVALID_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
const workspaceId = this.scopedWorkspaceContextFactory.create().workspaceId;
|
||||
const { workspaceId } = this.scopedWorkspaceContextFactory.create();
|
||||
|
||||
if (!workspaceId) {
|
||||
throw new RecordCRUDActionException(
|
||||
throw new RecordCrudException(
|
||||
'Failed to delete: Workspace ID is required',
|
||||
RecordCRUDActionExceptionCode.INVALID_REQUEST,
|
||||
RecordCrudExceptionCode.INVALID_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
const repository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
workflowActionInput.objectName,
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const { objectMetadataItemWithFieldsMaps } =
|
||||
await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps(
|
||||
workflowActionInput.objectName,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (
|
||||
!canObjectBeManagedByWorkflow({
|
||||
nameSingular: objectMetadataItemWithFieldsMaps.nameSingular,
|
||||
isSystem: objectMetadataItemWithFieldsMaps.isSystem,
|
||||
})
|
||||
) {
|
||||
throw new RecordCRUDActionException(
|
||||
'Failed to delete: Object cannot be deleted by workflow',
|
||||
RecordCRUDActionExceptionCode.INVALID_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
const objectRecord = await repository.findOne({
|
||||
where: {
|
||||
id: workflowActionInput.objectRecordId,
|
||||
},
|
||||
const toolOutput = await this.deleteRecordService.execute({
|
||||
objectName: workflowActionInput.objectName,
|
||||
objectRecordId: workflowActionInput.objectRecordId,
|
||||
workspaceId,
|
||||
soft: true,
|
||||
});
|
||||
|
||||
if (!objectRecord) {
|
||||
throw new RecordCRUDActionException(
|
||||
`Failed to delete: Record ${workflowActionInput.objectName} with id ${workflowActionInput.objectRecordId} not found`,
|
||||
RecordCRUDActionExceptionCode.RECORD_NOT_FOUND,
|
||||
if (!toolOutput.success) {
|
||||
throw new RecordCrudException(
|
||||
toolOutput.error || toolOutput.message,
|
||||
RecordCrudExceptionCode.RECORD_DELETION_FAILED,
|
||||
);
|
||||
}
|
||||
|
||||
const columnsToReturnForSoftDelete: string[] = [];
|
||||
|
||||
await repository.softDelete(
|
||||
workflowActionInput.objectRecordId,
|
||||
undefined,
|
||||
columnsToReturnForSoftDelete,
|
||||
);
|
||||
|
||||
return {
|
||||
result: objectRecord,
|
||||
result: toolOutput.result,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
import { CustomException } from 'src/utils/custom-exception';
|
||||
|
||||
export class RecordCRUDActionException extends CustomException<RecordCRUDActionExceptionCode> {}
|
||||
|
||||
export enum RecordCRUDActionExceptionCode {
|
||||
INVALID_REQUEST = 'INVALID_REQUEST',
|
||||
RECORD_NOT_FOUND = 'RECORD_NOT_FOUND',
|
||||
}
|
||||
+26
-120
@@ -1,24 +1,15 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type Entity } from '@microsoft/microsoft-graph-types';
|
||||
import { QUERY_MAX_RECORDS } from 'twenty-shared/constants';
|
||||
import { resolveInput } from 'twenty-shared/utils';
|
||||
import { type ObjectLiteral } from 'typeorm';
|
||||
|
||||
import {
|
||||
type ObjectRecordFilter,
|
||||
type ObjectRecordOrderBy,
|
||||
OrderByDirection,
|
||||
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
import { type WorkflowAction } from 'src/modules/workflow/workflow-executor/interfaces/workflow-action.interface';
|
||||
|
||||
import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser';
|
||||
import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps';
|
||||
import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps';
|
||||
import {
|
||||
RecordCrudException,
|
||||
RecordCrudExceptionCode,
|
||||
} from 'src/engine/core-modules/record-crud/exceptions/record-crud.exception';
|
||||
import { FindRecordsService } from 'src/engine/core-modules/record-crud/services/find-records.service';
|
||||
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
|
||||
import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service';
|
||||
import {
|
||||
WorkflowStepExecutorException,
|
||||
WorkflowStepExecutorExceptionCode,
|
||||
@@ -26,19 +17,14 @@ import {
|
||||
import { type WorkflowActionInput } from 'src/modules/workflow/workflow-executor/types/workflow-action-input';
|
||||
import { type WorkflowActionOutput } from 'src/modules/workflow/workflow-executor/types/workflow-action-output.type';
|
||||
import { findStepOrThrow } from 'src/modules/workflow/workflow-executor/utils/find-step-or-throw.util';
|
||||
import {
|
||||
RecordCRUDActionException,
|
||||
RecordCRUDActionExceptionCode,
|
||||
} from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/exceptions/record-crud-action.exception';
|
||||
import { isWorkflowFindRecordsAction } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/guards/is-workflow-find-records-action.guard';
|
||||
import { type WorkflowFindRecordsActionInput } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/types/workflow-record-crud-action-input.type';
|
||||
|
||||
@Injectable()
|
||||
export class FindRecordsWorkflowAction implements WorkflowAction {
|
||||
constructor(
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
private readonly findRecordsService: FindRecordsService,
|
||||
private readonly scopedWorkspaceContextFactory: ScopedWorkspaceContextFactory,
|
||||
private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService,
|
||||
) {}
|
||||
|
||||
async execute({
|
||||
@@ -63,119 +49,39 @@ export class FindRecordsWorkflowAction implements WorkflowAction {
|
||||
context,
|
||||
) as WorkflowFindRecordsActionInput;
|
||||
|
||||
const workspaceId = this.scopedWorkspaceContextFactory.create().workspaceId;
|
||||
const { workspaceId } = this.scopedWorkspaceContextFactory.create();
|
||||
|
||||
if (!workspaceId) {
|
||||
throw new RecordCRUDActionException(
|
||||
throw new RecordCrudException(
|
||||
'Failed to read: Workspace ID is required',
|
||||
RecordCRUDActionExceptionCode.INVALID_REQUEST,
|
||||
RecordCrudExceptionCode.INVALID_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
const repository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
workflowActionInput.objectName,
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
const toolOutput = await this.findRecordsService.execute({
|
||||
objectName: workflowActionInput.objectName,
|
||||
filter: workflowActionInput.filter?.gqlOperationFilter,
|
||||
orderBy: workflowActionInput.orderBy,
|
||||
limit: workflowActionInput.limit,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
if (!toolOutput.success) {
|
||||
throw new RecordCrudException(
|
||||
toolOutput.error || toolOutput.message,
|
||||
RecordCrudExceptionCode.QUERY_FAILED,
|
||||
);
|
||||
}
|
||||
|
||||
const { objectMetadataItemWithFieldsMaps, objectMetadataMaps } =
|
||||
await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps(
|
||||
workflowActionInput.objectName,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
const graphqlQueryParser = new GraphqlQueryParser(
|
||||
objectMetadataItemWithFieldsMaps,
|
||||
objectMetadataMaps,
|
||||
);
|
||||
|
||||
const objectRecords = await this.getObjectRecords(
|
||||
workflowActionInput,
|
||||
objectMetadataItemWithFieldsMaps,
|
||||
objectMetadataMaps,
|
||||
repository,
|
||||
graphqlQueryParser,
|
||||
);
|
||||
|
||||
const totalCount = await this.getTotalCount(
|
||||
workflowActionInput,
|
||||
repository,
|
||||
graphqlQueryParser,
|
||||
);
|
||||
const records = toolOutput.result?.records ?? [];
|
||||
const totalCount = toolOutput.result?.count ?? 0;
|
||||
|
||||
return {
|
||||
result: {
|
||||
first: objectRecords[0],
|
||||
all: objectRecords,
|
||||
first: records[0],
|
||||
all: records,
|
||||
totalCount,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private async getObjectRecords<T extends ObjectLiteral>(
|
||||
workflowActionInput: WorkflowFindRecordsActionInput,
|
||||
_objectMetadataItemWithFieldsMaps: ObjectMetadataItemWithFieldMaps,
|
||||
_objectMetadataMaps: ObjectMetadataMaps,
|
||||
repository: WorkspaceRepository<T>,
|
||||
graphqlQueryParser: GraphqlQueryParser,
|
||||
) {
|
||||
const queryBuilder = repository.createQueryBuilder(
|
||||
workflowActionInput.objectName,
|
||||
);
|
||||
|
||||
const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder(
|
||||
queryBuilder,
|
||||
workflowActionInput.objectName,
|
||||
workflowActionInput.filter?.gqlOperationFilter ??
|
||||
({} as ObjectRecordFilter),
|
||||
);
|
||||
|
||||
const orderByWithIdCondition = [
|
||||
...(workflowActionInput.orderBy ?? []),
|
||||
{ id: OrderByDirection.AscNullsFirst },
|
||||
] as ObjectRecordOrderBy;
|
||||
|
||||
const withOrderByQueryBuilder = graphqlQueryParser.applyOrderToBuilder(
|
||||
withFilterQueryBuilder,
|
||||
orderByWithIdCondition,
|
||||
workflowActionInput.objectName,
|
||||
false,
|
||||
);
|
||||
|
||||
return withOrderByQueryBuilder
|
||||
.take(
|
||||
workflowActionInput.limit
|
||||
? Math.min(workflowActionInput.limit, QUERY_MAX_RECORDS)
|
||||
: QUERY_MAX_RECORDS,
|
||||
)
|
||||
.getMany();
|
||||
}
|
||||
|
||||
private async getTotalCount(
|
||||
workflowActionInput: WorkflowFindRecordsActionInput,
|
||||
repository: WorkspaceRepository<Entity>,
|
||||
graphqlQueryParser: GraphqlQueryParser,
|
||||
) {
|
||||
const countQueryBuilder = repository.createQueryBuilder(
|
||||
workflowActionInput.objectName,
|
||||
);
|
||||
|
||||
const withFilterCountQueryBuilder = graphqlQueryParser.applyFilterToBuilder(
|
||||
countQueryBuilder,
|
||||
workflowActionInput.objectName,
|
||||
workflowActionInput.filter?.gqlOperationFilter ??
|
||||
({} as ObjectRecordFilter),
|
||||
);
|
||||
|
||||
const withDeletedCountQueryBuilder =
|
||||
graphqlQueryParser.applyDeletedAtToBuilder(
|
||||
withFilterCountQueryBuilder,
|
||||
workflowActionInput.filter?.gqlOperationFilter
|
||||
? workflowActionInput.filter.gqlOperationFilter
|
||||
: ({} as ObjectRecordFilter),
|
||||
);
|
||||
|
||||
return withDeletedCountQueryBuilder.getCount();
|
||||
}
|
||||
}
|
||||
|
||||
+2
-14
@@ -1,26 +1,14 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
|
||||
|
||||
import { RecordPositionModule } from 'src/engine/core-modules/record-position/record-position.module';
|
||||
import { RecordTransformerModule } from 'src/engine/core-modules/record-transformer/record-transformer.module';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { RecordCrudModule } from 'src/engine/core-modules/record-crud/record-crud.module';
|
||||
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
|
||||
import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/workspace-cache-storage.module';
|
||||
import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module';
|
||||
import { CreateRecordWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/create-record.workflow-action';
|
||||
import { DeleteRecordWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/delete-record.workflow-action';
|
||||
import { FindRecordsWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/find-records.workflow-action';
|
||||
import { UpdateRecordWorkflowAction } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/update-record.workflow-action';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
WorkspaceCacheStorageModule,
|
||||
NestjsQueryTypeOrmModule.forFeature([ObjectMetadataEntity]),
|
||||
RecordPositionModule,
|
||||
RecordTransformerModule,
|
||||
WorkflowCommonModule,
|
||||
],
|
||||
imports: [RecordCrudModule],
|
||||
providers: [
|
||||
ScopedWorkspaceContextFactory,
|
||||
CreateRecordWorkflowAction,
|
||||
|
||||
+3
-4
@@ -3,17 +3,16 @@ import {
|
||||
type ObjectRecordOrderBy,
|
||||
} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type ObjectRecord = Record<string, any>;
|
||||
import { type ObjectRecordProperties } from 'src/engine/core-modules/record-crud/types/object-record-properties.type';
|
||||
|
||||
export type WorkflowCreateRecordActionInput = {
|
||||
objectName: string;
|
||||
objectRecord: ObjectRecord;
|
||||
objectRecord: ObjectRecordProperties;
|
||||
};
|
||||
|
||||
export type WorkflowUpdateRecordActionInput = {
|
||||
objectName: string;
|
||||
objectRecord: ObjectRecord;
|
||||
objectRecord: ObjectRecordProperties;
|
||||
objectRecordId: string;
|
||||
fieldsToUpdate: string[];
|
||||
};
|
||||
|
||||
+22
-87
@@ -1,15 +1,15 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import deepEqual from 'deep-equal';
|
||||
import { isDefined, isValidUuid, resolveInput } from 'twenty-shared/utils';
|
||||
import { canObjectBeManagedByWorkflow } from 'twenty-shared/workflow';
|
||||
|
||||
import { type WorkflowAction } from 'src/modules/workflow/workflow-executor/interfaces/workflow-action.interface';
|
||||
|
||||
import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service';
|
||||
import {
|
||||
RecordCrudException,
|
||||
RecordCrudExceptionCode,
|
||||
} from 'src/engine/core-modules/record-crud/exceptions/record-crud.exception';
|
||||
import { UpdateRecordService } from 'src/engine/core-modules/record-crud/services/update-record.service';
|
||||
import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service';
|
||||
import {
|
||||
WorkflowStepExecutorException,
|
||||
WorkflowStepExecutorExceptionCode,
|
||||
@@ -17,20 +17,14 @@ import {
|
||||
import { type WorkflowActionInput } from 'src/modules/workflow/workflow-executor/types/workflow-action-input';
|
||||
import { type WorkflowActionOutput } from 'src/modules/workflow/workflow-executor/types/workflow-action-output.type';
|
||||
import { findStepOrThrow } from 'src/modules/workflow/workflow-executor/utils/find-step-or-throw.util';
|
||||
import {
|
||||
RecordCRUDActionException,
|
||||
RecordCRUDActionExceptionCode,
|
||||
} from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/exceptions/record-crud-action.exception';
|
||||
import { isWorkflowUpdateRecordAction } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/guards/is-workflow-update-record-action.guard';
|
||||
import { type WorkflowUpdateRecordActionInput } from 'src/modules/workflow/workflow-executor/workflow-actions/record-crud/types/workflow-record-crud-action-input.type';
|
||||
|
||||
@Injectable()
|
||||
export class UpdateRecordWorkflowAction implements WorkflowAction {
|
||||
constructor(
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
private readonly updateRecordService: UpdateRecordService,
|
||||
private readonly scopedWorkspaceContextFactory: ScopedWorkspaceContextFactory,
|
||||
private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService,
|
||||
private readonly recordInputTransformerService: RecordInputTransformerService,
|
||||
) {}
|
||||
|
||||
async execute({
|
||||
@@ -60,97 +54,38 @@ export class UpdateRecordWorkflowAction implements WorkflowAction {
|
||||
!isValidUuid(workflowActionInput.objectRecordId) ||
|
||||
!isDefined(workflowActionInput.objectName)
|
||||
) {
|
||||
throw new RecordCRUDActionException(
|
||||
throw new RecordCrudException(
|
||||
'Failed to update: Object record ID and name are required',
|
||||
RecordCRUDActionExceptionCode.INVALID_REQUEST,
|
||||
RecordCrudExceptionCode.INVALID_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
const workspaceId = this.scopedWorkspaceContextFactory.create().workspaceId;
|
||||
const { workspaceId } = this.scopedWorkspaceContextFactory.create();
|
||||
|
||||
if (!workspaceId) {
|
||||
throw new RecordCRUDActionException(
|
||||
throw new RecordCrudException(
|
||||
'Failed to update: Workspace ID is required',
|
||||
RecordCRUDActionExceptionCode.INVALID_REQUEST,
|
||||
RecordCrudExceptionCode.INVALID_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
const repository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||
workspaceId,
|
||||
workflowActionInput.objectName,
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const previousObjectRecord = await repository.findOne({
|
||||
where: {
|
||||
id: workflowActionInput.objectRecordId,
|
||||
},
|
||||
const toolOutput = await this.updateRecordService.execute({
|
||||
objectName: workflowActionInput.objectName,
|
||||
objectRecordId: workflowActionInput.objectRecordId,
|
||||
objectRecord: workflowActionInput.objectRecord,
|
||||
fieldsToUpdate: workflowActionInput.fieldsToUpdate,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
if (!previousObjectRecord) {
|
||||
throw new RecordCRUDActionException(
|
||||
`Failed to update: Record ${workflowActionInput.objectName} with id ${workflowActionInput.objectRecordId} not found`,
|
||||
RecordCRUDActionExceptionCode.RECORD_NOT_FOUND,
|
||||
if (!toolOutput.success) {
|
||||
throw new RecordCrudException(
|
||||
toolOutput.error || toolOutput.message,
|
||||
RecordCrudExceptionCode.RECORD_UPDATE_FAILED,
|
||||
);
|
||||
}
|
||||
|
||||
if (workflowActionInput.fieldsToUpdate.length === 0) {
|
||||
return {
|
||||
result: previousObjectRecord,
|
||||
};
|
||||
}
|
||||
|
||||
const { objectMetadataItemWithFieldsMaps } =
|
||||
await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps(
|
||||
workflowActionInput.objectName,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
if (
|
||||
!canObjectBeManagedByWorkflow({
|
||||
nameSingular: objectMetadataItemWithFieldsMaps.nameSingular,
|
||||
isSystem: objectMetadataItemWithFieldsMaps.isSystem,
|
||||
})
|
||||
) {
|
||||
throw new RecordCRUDActionException(
|
||||
'Failed to update: Object cannot be updated by workflow',
|
||||
RecordCRUDActionExceptionCode.INVALID_REQUEST,
|
||||
);
|
||||
}
|
||||
|
||||
const objectRecordWithFilteredFields = Object.keys(
|
||||
workflowActionInput.objectRecord,
|
||||
).reduce((acc, key) => {
|
||||
if (workflowActionInput.fieldsToUpdate.includes(key)) {
|
||||
return {
|
||||
...acc,
|
||||
[key]: workflowActionInput.objectRecord[key],
|
||||
};
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const transformedObjectRecord =
|
||||
await this.recordInputTransformerService.process({
|
||||
recordInput: objectRecordWithFilteredFields,
|
||||
objectMetadataMapItem: objectMetadataItemWithFieldsMaps,
|
||||
});
|
||||
|
||||
const updatedObjectRecord = {
|
||||
...previousObjectRecord,
|
||||
...objectRecordWithFilteredFields,
|
||||
};
|
||||
|
||||
if (!deepEqual(updatedObjectRecord, previousObjectRecord)) {
|
||||
await repository.update(workflowActionInput.objectRecordId, {
|
||||
...transformedObjectRecord,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
result: updatedObjectRecord,
|
||||
result: toolOutput.result,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user