diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index 6e9a95d1b5a..3b8729b82a3 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -120,6 +120,11 @@ export type AgentHandoff = { toAgent: Agent; }; +export type AgentIdInput = { + /** The id of the agent. */ + id: Scalars['UUID']; +}; + export type AggregateChartConfiguration = { __typename?: 'AggregateChartConfiguration'; aggregateFieldMetadataId: Scalars['UUID']; @@ -749,6 +754,24 @@ export type CoreViewSort = { workspaceId: Scalars['UUID']; }; +export type CreateAgentHandoffInput = { + description?: InputMaybe; + fromAgentId: Scalars['UUID']; + toAgentId: Scalars['UUID']; +}; + +export type CreateAgentInput = { + description?: InputMaybe; + icon?: InputMaybe; + label: Scalars['String']; + modelConfiguration?: InputMaybe; + modelId: Scalars['String']; + name?: InputMaybe; + prompt: Scalars['String']; + responseFormat?: InputMaybe; + roleId?: InputMaybe; +}; + export type CreateApiKeyInput = { expiresAt: Scalars['String']; name: Scalars['String']; @@ -1705,8 +1728,10 @@ export type Mutation = { checkPublicDomainValidRecords?: Maybe; checkoutSession: BillingSessionOutput; computeStepOutputSchema: Scalars['JSON']; + createAgentHandoff: Scalars['Boolean']; createApiKey: ApiKey; createApprovedAccessDomain: ApprovedAccessDomain; + createChatThread: AgentChatThread; createCoreView: CoreView; createCoreViewField: CoreViewField; createCoreViewFilter: CoreViewFilter; @@ -1721,6 +1746,7 @@ export type Mutation = { createManyCoreViewGroups: Array; createOIDCIdentityProvider: SetupSsoOutput; createObjectEvent: Analytics; + createOneAgent: Agent; createOneAppToken: AppToken; createOneCronTrigger: CronTrigger; createOneDatabaseEventTrigger: DatabaseEventTrigger; @@ -1752,6 +1778,7 @@ export type Mutation = { deleteEmailingDomain: Scalars['Boolean']; deleteFile: File; deleteJobs: DeleteJobsResponse; + deleteOneAgent: Agent; deleteOneCronTrigger: CronTrigger; deleteOneDatabaseEventTrigger: DatabaseEventTrigger; deleteOneField: Field; @@ -1798,6 +1825,7 @@ export type Mutation = { initiateOTPProvisioning: InitiateTwoFactorAuthenticationProvisioningOutput; initiateOTPProvisioningForAuthenticatedUser: InitiateTwoFactorAuthenticationProvisioningOutput; publishServerlessFunction: ServerlessFunction; + removeAgentHandoff: Scalars['Boolean']; removeRoleFromAgent: Scalars['Boolean']; renewToken: AuthTokens; resendEmailVerificationToken: ResendEmailVerificationTokenOutput; @@ -1834,6 +1862,7 @@ export type Mutation = { updateCoreViewSort: CoreViewSort; updateDatabaseConfigVariable: Scalars['Boolean']; updateLabPublicFeatureFlag: FeatureFlagDto; + updateOneAgent: Agent; updateOneApplicationVariable: Scalars['Boolean']; updateOneCronTrigger: CronTrigger; updateOneDatabaseEventTrigger: DatabaseEventTrigger; @@ -1918,6 +1947,11 @@ export type MutationComputeStepOutputSchemaArgs = { }; +export type MutationCreateAgentHandoffArgs = { + input: CreateAgentHandoffInput; +}; + + export type MutationCreateApiKeyArgs = { input: CreateApiKeyInput; }; @@ -2003,6 +2037,11 @@ export type MutationCreateObjectEventArgs = { }; +export type MutationCreateOneAgentArgs = { + input: CreateAgentInput; +}; + + export type MutationCreateOneCronTriggerArgs = { input: CreateCronTriggerInput; }; @@ -2150,6 +2189,11 @@ export type MutationDeleteJobsArgs = { }; +export type MutationDeleteOneAgentArgs = { + input: AgentIdInput; +}; + + export type MutationDeleteOneCronTriggerArgs = { input: CronTriggerIdInput; }; @@ -2363,6 +2407,11 @@ export type MutationPublishServerlessFunctionArgs = { }; +export type MutationRemoveAgentHandoffArgs = { + input: RemoveAgentHandoffInput; +}; + + export type MutationRemoveRoleFromAgentArgs = { agentId: Scalars['UUID']; }; @@ -2547,6 +2596,11 @@ export type MutationUpdateLabPublicFeatureFlagArgs = { }; +export type MutationUpdateOneAgentArgs = { + input: UpdateAgentInput; +}; + + export type MutationUpdateOneApplicationVariableArgs = { applicationId: Scalars['UUID']; key: Scalars['String']; @@ -3070,18 +3124,25 @@ export type Query = { apiKey?: Maybe; apiKeys: Array; billingPortalSession: BillingSessionOutput; + chatMessages: Array; + chatThread: AgentChatThread; + chatThreads: Array; checkUserExists: CheckUserExistOutput; checkWorkspaceInviteHashIsValid: WorkspaceInviteHashValidOutput; currentUser: User; currentWorkspace: Workspace; field: Field; fields: FieldConnection; + findAgentHandoffTargets: Array; + findAgentHandoffs: Array; + findManyAgents: Array; findManyApplications: Array; findManyCronTriggers: Array; findManyDatabaseEventTriggers: Array; findManyPublicDomains: Array; findManyRouteTriggers: Array; findManyServerlessFunctions: Array; + findOneAgent: Agent; findOneApplication: Application; findOneCronTrigger: CronTrigger; findOneDatabaseEventTrigger: DatabaseEventTrigger; @@ -3154,6 +3215,16 @@ export type QueryBillingPortalSessionArgs = { }; +export type QueryChatMessagesArgs = { + threadId: Scalars['UUID']; +}; + + +export type QueryChatThreadArgs = { + id: Scalars['UUID']; +}; + + export type QueryCheckUserExistsArgs = { captchaToken?: InputMaybe; email: Scalars['String']; @@ -3165,6 +3236,21 @@ export type QueryCheckWorkspaceInviteHashIsValidArgs = { }; +export type QueryFindAgentHandoffTargetsArgs = { + input: AgentIdInput; +}; + + +export type QueryFindAgentHandoffsArgs = { + input: AgentIdInput; +}; + + +export type QueryFindOneAgentArgs = { + input: AgentIdInput; +}; + + export type QueryFindOneApplicationArgs = { id: Scalars['UUID']; }; @@ -3509,6 +3595,11 @@ export enum RemoteTableStatus { SYNCED = 'SYNCED' } +export type RemoveAgentHandoffInput = { + fromAgentId: Scalars['UUID']; + toAgentId: Scalars['UUID']; +}; + export type ResendEmailVerificationTokenOutput = { __typename?: 'ResendEmailVerificationTokenOutput'; success: Scalars['Boolean']; @@ -3932,6 +4023,19 @@ export type UuidFilterComparison = { notLike?: InputMaybe; }; +export type UpdateAgentInput = { + description?: InputMaybe; + icon?: InputMaybe; + id: Scalars['UUID']; + label: Scalars['String']; + modelConfiguration?: InputMaybe; + modelId: Scalars['String']; + name: Scalars['String']; + prompt: Scalars['String']; + responseFormat?: InputMaybe; + roleId?: InputMaybe; +}; + export type UpdateApiKeyInput = { expiresAt?: InputMaybe; id: Scalars['UUID']; diff --git a/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts b/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts index 80915d37d55..ceddd577307 100644 --- a/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/common/common-query-runners/common-base-query-runner.service.ts @@ -101,7 +101,7 @@ export abstract class CommonBaseQueryRunnerService< if (!isWorkspaceAuthContext(authContext)) { throw new CommonQueryRunnerException( - `Invalid auth context: ${JSON.stringify(authContext)}`, + 'Invalid auth context', CommonQueryRunnerExceptionCode.INVALID_AUTH_CONTEXT, ); } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module.ts index 4cc34c2be07..aacccc3418b 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module.ts @@ -6,6 +6,7 @@ import { workspaceQueryRunnerFactories } from 'src/engine/api/graphql/workspace- import { TelemetryListener } from 'src/engine/api/graphql/workspace-query-runner/listeners/telemetry.listener'; import { WorkspaceQueryHookModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.module'; import { AuditModule } from 'src/engine/core-modules/audit/audit.module'; +import { AuthModule } from 'src/engine/core-modules/auth/auth.module'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { FileModule } from 'src/engine/core-modules/file/file.module'; import { RecordPositionModule } from 'src/engine/core-modules/record-position/record-position.module'; @@ -18,6 +19,7 @@ import { EntityEventsToDbListener } from './listeners/entity-events-to-db.listen @Module({ imports: [ + AuthModule, WorkspaceQueryBuilderModule, WorkspaceDataSourceModule, WorkspaceQueryHookModule, diff --git a/packages/twenty-server/src/engine/core-modules/actor/services/created-by-from-auth-context.service.ts b/packages/twenty-server/src/engine/core-modules/actor/services/created-by-from-auth-context.service.ts index 79c6e0c0a0d..01ac1c171b2 100644 --- a/packages/twenty-server/src/engine/core-modules/actor/services/created-by-from-auth-context.service.ts +++ b/packages/twenty-server/src/engine/core-modules/actor/services/created-by-from-auth-context.service.ts @@ -68,19 +68,6 @@ export class CreatedByFromAuthContextService { const clonedRecords = structuredClone(records); - // Check if all records already have createdBy with name populated - // If so, skip building from auth context (e.g., workflows provide explicit createdBy) - const recordsArray = Array.isArray(clonedRecords) - ? clonedRecords - : [clonedRecords]; - const allRecordsHaveCreatedBy = recordsArray.every( - (record) => record.createdBy?.name, - ); - - if (allRecordsHaveCreatedBy) { - return clonedRecords; - } - const createdBy = await this.buildCreatedBy(authContext); if (Array.isArray(clonedRecords)) { diff --git a/packages/twenty-server/src/engine/core-modules/ai/services/tool.service.ts b/packages/twenty-server/src/engine/core-modules/ai/services/tool.service.ts index fff714d638c..09f889f5f42 100644 --- a/packages/twenty-server/src/engine/core-modules/ai/services/tool.service.ts +++ b/packages/twenty-server/src/engine/core-modules/ai/services/tool.service.ts @@ -12,6 +12,7 @@ import { generateUpdateRecordInputSchema } from 'src/engine/core-modules/record- import { BulkDeleteToolInputSchema } from 'src/engine/core-modules/record-crud/zod-schemas/bulk-delete-tool.zod-schema'; import { generateFindToolInputSchema } from 'src/engine/core-modules/record-crud/zod-schemas/find-tool.zod-schema'; import { SoftDeleteToolInputSchema } from 'src/engine/core-modules/record-crud/zod-schemas/soft-delete-tool.zod-schema'; +import { FindOneToolInputSchema } from 'src/engine/core-modules/record-crud/zod-schemas/find-one-tool.zod-schema'; import { isWorkflowRelatedObject } from 'src/engine/metadata-modules/agent/utils/is-workflow-related-object.util'; import { type ToolHints, @@ -44,7 +45,6 @@ export class ToolService { rolePermissionConfig: RolePermissionConfig, workspaceId: string, actorContext?: ActorMetadata, - userWorkspaceId?: string, toolHints?: ToolHints, ): Promise { const tools: ToolSet = {}; @@ -150,7 +150,20 @@ export class ToolService { offset, workspaceId, rolePermissionConfig, - userWorkspaceId, + }); + }, + }; + + tools[`find_one_${objectMetadata.nameSingular}`] = { + description: `Retrieve a single ${objectMetadata.labelSingular} record by its unique ID. Use this when you know the exact record ID and need the complete record data. Returns the full record or an error if not found.`, + inputSchema: FindOneToolInputSchema, + execute: async (parameters) => { + return this.findRecordsService.execute({ + objectName: objectMetadata.nameSingular, + filter: { id: { eq: parameters.input.id } }, + limit: 1, + workspaceId, + rolePermissionConfig, }); }, }; @@ -171,7 +184,6 @@ export class ToolService { workspaceId, rolePermissionConfig, createdBy: actorContext, - userWorkspaceId, }); }, }; @@ -199,7 +211,6 @@ export class ToolService { objectRecord, workspaceId, rolePermissionConfig, - userWorkspaceId, }); }, }; @@ -217,7 +228,6 @@ export class ToolService { workspaceId, rolePermissionConfig, soft: true, - userWorkspaceId, }); }, }; diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/record-crud.module.ts b/packages/twenty-server/src/engine/core-modules/record-crud/record-crud.module.ts index 69704e70685..56d4c8b3f77 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/record-crud.module.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/record-crud.module.ts @@ -1,21 +1,23 @@ -import { forwardRef, Module } from '@nestjs/common'; +import { Module } from '@nestjs/common'; -import { CoreCommonApiModule } from 'src/engine/api/common/core-common-api.module'; import { CreateRecordService } from 'src/engine/core-modules/record-crud/services/create-record.service'; import { DeleteRecordService } from 'src/engine/core-modules/record-crud/services/delete-record.service'; import { FindRecordsService } from 'src/engine/core-modules/record-crud/services/find-records.service'; import { UpdateRecordService } from 'src/engine/core-modules/record-crud/services/update-record.service'; import { UpsertRecordService } from 'src/engine/core-modules/record-crud/services/upsert-record.service'; -import { CommonApiContextBuilder } from 'src/engine/core-modules/record-crud/utils/common-api-context-builder.util'; -import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module'; +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 { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module'; +import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module'; @Module({ imports: [ - forwardRef(() => CoreCommonApiModule), - WorkspaceMetadataCacheModule, + TwentyORMModule, + RecordPositionModule, + RecordTransformerModule, + WorkflowCommonModule, ], providers: [ - CommonApiContextBuilder, CreateRecordService, UpdateRecordService, DeleteRecordService, diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/services/create-record.service.ts b/packages/twenty-server/src/engine/core-modules/record-crud/services/create-record.service.ts index 2a20ff09b96..e4a00c6c643 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/services/create-record.service.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/services/create-record.service.ts @@ -1,60 +1,134 @@ import { Injectable, Logger } from '@nestjs/common'; -import { CommonCreateOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-create-one-query-runner.service'; +import { isDefined } from 'class-validator'; +import { canObjectBeManagedByWorkflow } from 'twenty-shared/workflow'; +import { FieldActorSource } from 'twenty-shared/types'; + +import { + RecordCrudException, + RecordCrudExceptionCode, +} from 'src/engine/core-modules/record-crud/exceptions/record-crud.exception'; import { type CreateRecordParams } from 'src/engine/core-modules/record-crud/types/create-record-params.type'; -import { CommonApiContextBuilder } from 'src/engine/core-modules/record-crud/utils/common-api-context-builder.util'; +import { getSelectedColumnsFromRestrictedFields } from 'src/engine/core-modules/record-crud/utils/get-selected-columns-from-restricted-fields.util'; +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 { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; @Injectable() +// eslint-disable-next-line @nx/workspace-inject-workspace-repository export class CreateRecordService { private readonly logger = new Logger(CreateRecordService.name); constructor( - private readonly commonCreateOneRunner: CommonCreateOneQueryRunnerService, - private readonly commonApiContextBuilder: CommonApiContextBuilder, + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly recordPositionService: RecordPositionService, + private readonly recordInputTransformerService: RecordInputTransformerService, + private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService, ) {} async execute(params: CreateRecordParams): Promise { - const { - objectName, - objectRecord, - workspaceId, - rolePermissionConfig, - createdBy, - userWorkspaceId, - apiKey, - } = params; + const { objectName, objectRecord, workspaceId, rolePermissionConfig } = + params; + + if (!workspaceId) { + return { + success: false, + message: 'Failed to create record: Workspace ID is required', + error: 'Workspace ID not found', + }; + } try { - const { queryRunnerContext, selectedFields } = - await this.commonApiContextBuilder.build({ + const repository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + objectName, + rolePermissionConfig, + ); + + const { objectMetadataItemWithFieldsMaps } = + await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps( objectName, workspaceId, - rolePermissionConfig, - userWorkspaceId, - apiKey, - actorContext: createdBy, + ); + + if ( + !canObjectBeManagedByWorkflow({ + nameSingular: objectMetadataItemWithFieldsMaps.nameSingular, + isSystem: objectMetadataItemWithFieldsMaps.isSystem, + }) + ) { + throw new RecordCrudException( + 'Failed to create: Object cannot be created by workflow', + RecordCrudExceptionCode.INVALID_REQUEST, + ); + } + + const position = await this.recordPositionService.buildRecordPosition({ + value: 'first', + objectMetadata: objectMetadataItemWithFieldsMaps, + workspaceId, + }); + + const validObjectRecord = Object.fromEntries( + Object.entries(objectRecord).filter( + ([key]) => + isDefined(objectMetadataItemWithFieldsMaps.fieldIdByName[key]) || + isDefined( + objectMetadataItemWithFieldsMaps.fieldIdByJoinColumnName[key], + ), + ), + ); + + const transformedObjectRecord = + await this.recordInputTransformerService.process({ + recordInput: validObjectRecord, + objectMetadataMapItem: objectMetadataItemWithFieldsMaps, }); - // Pass createdBy explicitly if provided (for workflows) - // Common API hook will also inject createdBy from authContext if available - const dataWithActor = createdBy - ? { ...objectRecord, createdBy } - : objectRecord; + const restrictedFields = + repository.objectRecordsPermissions?.[ + objectMetadataItemWithFieldsMaps.id + ]?.restrictedFields; - const result = await this.commonCreateOneRunner.execute( - { data: dataWithActor, selectedFields }, - queryRunnerContext, + const selectedColumns = getSelectedColumnsFromRestrictedFields( + restrictedFields, + objectMetadataItemWithFieldsMaps, ); + const insertResult = await repository.insert( + { + ...transformedObjectRecord, + position, + createdBy: params.createdBy ?? { + source: FieldActorSource.WORKFLOW, + name: 'Workflow', + }, + }, + undefined, + selectedColumns, + ); + + const [createdRecord] = insertResult.generatedMaps; + this.logger.log(`Record created successfully in ${objectName}`); return { success: true, message: `Record created successfully in ${objectName}`, - result, + result: createdRecord, }; } catch (error) { + if (error instanceof RecordCrudException) { + return { + success: false, + message: `Failed to create record in ${objectName}`, + error: error.message, + }; + } + this.logger.error(`Failed to create record: ${error}`); return { diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/services/delete-record.service.ts b/packages/twenty-server/src/engine/core-modules/record-crud/services/delete-record.service.ts index 344985b0098..a7a1102ab2d 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/services/delete-record.service.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/services/delete-record.service.ts @@ -1,17 +1,25 @@ import { Injectable, Logger } from '@nestjs/common'; -import { CommonDeleteOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-delete-one-query-runner.service'; +import { isDefined, isValidUuid } from 'twenty-shared/utils'; +import { canObjectBeManagedByWorkflow } from 'twenty-shared/workflow'; + +import { + RecordCrudException, + RecordCrudExceptionCode, +} from 'src/engine/core-modules/record-crud/exceptions/record-crud.exception'; import { type DeleteRecordParams } from 'src/engine/core-modules/record-crud/types/delete-record-params.type'; -import { CommonApiContextBuilder } from 'src/engine/core-modules/record-crud/utils/common-api-context-builder.util'; import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; @Injectable() +// eslint-disable-next-line @nx/workspace-inject-workspace-repository export class DeleteRecordService { private readonly logger = new Logger(DeleteRecordService.name); constructor( - private readonly commonDeleteOneRunner: CommonDeleteOneQueryRunnerService, - private readonly commonApiContextBuilder: CommonApiContextBuilder, + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService, ) {} async execute(params: DeleteRecordParams): Promise { @@ -20,40 +28,107 @@ export class DeleteRecordService { objectRecordId, workspaceId, rolePermissionConfig, - userWorkspaceId, - apiKey, - createdBy, + soft = true, } = params; + if (!workspaceId) { + return { + success: false, + message: 'Failed to delete record: Workspace ID is required', + error: 'Workspace ID not found', + }; + } + + if (!isDefined(objectRecordId) || !isValidUuid(objectRecordId)) { + return { + success: false, + message: 'Failed to delete: Object record ID must be a valid UUID', + error: 'Invalid object record ID', + }; + } + try { - const { queryRunnerContext, selectedFields } = - await this.commonApiContextBuilder.build({ + const repository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + objectName, + rolePermissionConfig, + ); + + const { objectMetadataItemWithFieldsMaps } = + await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps( objectName, workspaceId, - rolePermissionConfig, - userWorkspaceId, - apiKey, - actorContext: createdBy, - }); + ); - const result = await this.commonDeleteOneRunner.execute( - { id: objectRecordId, selectedFields }, - queryRunnerContext, - ); + if ( + !canObjectBeManagedByWorkflow({ + nameSingular: objectMetadataItemWithFieldsMaps.nameSingular, + isSystem: objectMetadataItemWithFieldsMaps.isSystem, + }) + ) { + throw new RecordCrudException( + 'Failed to delete: Object cannot be deleted by workflow', + RecordCrudExceptionCode.INVALID_REQUEST, + ); + } - this.logger.log(`Record deleted successfully in ${objectName}`); + const objectRecord = await repository.findOne({ + where: { + id: objectRecordId, + }, + }); - return { - success: true, - message: `Record deleted successfully in ${objectName}`, - result, - }; + if (!objectRecord) { + throw new RecordCrudException( + `Failed to delete: Record ${objectName} with id ${objectRecordId} not found`, + RecordCrudExceptionCode.RECORD_NOT_FOUND, + ); + } + + if (soft) { + const columnsToReturnForSoftDelete: string[] = []; + + await repository.softDelete( + objectRecordId, + undefined, + columnsToReturnForSoftDelete, + ); + + this.logger.log(`Record soft deleted successfully from ${objectName}`); + + return { + success: true, + message: `Record soft deleted successfully from ${objectName}`, + result: objectRecord, + }; + } else { + await repository.remove(objectRecord); + + this.logger.log( + `Record permanently deleted successfully from ${objectName}`, + ); + + return { + success: true, + message: `Record permanently deleted successfully from ${objectName}`, + result: { id: objectRecordId }, + }; + } } catch (error) { + if (error instanceof RecordCrudException) { + return { + success: false, + message: `Failed to delete record from ${objectName}`, + error: error.message, + }; + } + this.logger.error(`Failed to delete record: ${error}`); return { success: false, - message: `Failed to delete record in ${objectName}`, + message: `Failed to delete record from ${objectName}`, error: error instanceof Error ? error.message : 'Failed to delete record', }; diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/services/find-records.service.ts b/packages/twenty-server/src/engine/core-modules/record-crud/services/find-records.service.ts index f913707d22b..b112990860f 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/services/find-records.service.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/services/find-records.service.ts @@ -1,63 +1,103 @@ import { Injectable, Logger } from '@nestjs/common'; -import { CommonFindManyQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-find-many-query-runner.service'; +import isEmpty from 'lodash.isempty'; +import { QUERY_MAX_RECORDS } from 'twenty-shared/constants'; +import { OrderByDirection } from 'twenty-shared/types'; +import { type ObjectLiteral } from 'typeorm'; + +import { + type ObjectRecordFilter, + type ObjectRecordOrderBy, +} from 'src/engine/api/graphql/workspace-query-builder/interfaces/object-record.interface'; + +import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser'; +import { getAllSelectableColumnNames } from 'src/engine/api/utils/get-all-selectable-column-names.utils'; import { type FindRecordsParams } from 'src/engine/core-modules/record-crud/types/find-records-params.type'; -import { CommonApiContextBuilder } from 'src/engine/core-modules/record-crud/utils/common-api-context-builder.util'; +import { FindRecordsResult } from 'src/engine/core-modules/record-crud/types/find-records-result.type'; import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type'; +import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; +import { type WorkspaceSelectQueryBuilder } from 'src/engine/twenty-orm/repository/workspace-select-query-builder'; +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'; @Injectable() +// eslint-disable-next-line @nx/workspace-inject-workspace-repository export class FindRecordsService { private readonly logger = new Logger(FindRecordsService.name); constructor( - private readonly commonFindManyRunner: CommonFindManyQueryRunnerService, - private readonly commonApiContextBuilder: CommonApiContextBuilder, + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService, ) {} async execute( params: FindRecordsParams, - ): Promise> { + ): Promise> { const { objectName, filter, orderBy, limit, - offset, + offset = 0, workspaceId, rolePermissionConfig, - userWorkspaceId, - apiKey, - createdBy, } = params; + if (!workspaceId) { + return { + success: false, + message: 'Failed to find records: Workspace ID is required', + error: 'Workspace ID not found', + }; + } + try { - const { queryRunnerContext, selectedFields } = - await this.commonApiContextBuilder.build({ + const repository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + objectName, + rolePermissionConfig, + ); + + const { objectMetadataItemWithFieldsMaps, objectMetadataMaps } = + await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps( objectName, workspaceId, - rolePermissionConfig, - userWorkspaceId, - apiKey, - actorContext: createdBy, - }); + ); - const result = await this.commonFindManyRunner.execute( - { - filter: filter || {}, - orderBy, - first: limit ?? 50, - offset: offset ?? 0, - selectedFields, - }, - queryRunnerContext, + const graphqlQueryParser = new GraphqlQueryParser( + objectMetadataItemWithFieldsMaps, + objectMetadataMaps, ); + const records = await this.getObjectRecords({ + objectName, + filter, + orderBy, + limit, + offset, + repository, + graphqlQueryParser, + objectMetadataItemWithFieldsMaps, + }); + + const totalCount = await this.getTotalCount({ + objectName, + filter, + repository, + graphqlQueryParser, + objectMetadataItemWithFieldsMaps, + }); + + this.logger.log(`Found ${records.length} records in ${objectName}`); + return { success: true, - message: `Found ${result.records.length} records in ${objectName}`, + message: `Found ${records.length} ${objectName} records`, result: { - records: result.records, - totalCount: result.totalCount, + records, + count: totalCount, }, }; } catch (error) { @@ -65,10 +105,129 @@ export class FindRecordsService { return { success: false, - message: `Failed to find records in ${objectName}`, + message: `Failed to find ${objectName} records`, error: error instanceof Error ? error.message : 'Failed to find records', }; } } + + private applyRestrictedFieldsToQueryBuilder( + queryBuilder: WorkspaceSelectQueryBuilder, + repository: WorkspaceRepository, + objectMetadataItemWithFieldsMaps: ObjectMetadataItemWithFieldMaps, + ): WorkspaceSelectQueryBuilder { + const restrictedFields = + repository.objectRecordsPermissions?.[objectMetadataItemWithFieldsMaps.id] + ?.restrictedFields; + + if (!restrictedFields || isEmpty(restrictedFields)) { + return queryBuilder; + } + + const selectableFields = getAllSelectableColumnNames({ + restrictedFields, + objectMetadata: { + objectMetadataMapItem: objectMetadataItemWithFieldsMaps, + }, + }); + + return queryBuilder.setFindOptions({ + // @ts-expect-error - TypeORM typing limitation with dynamic select fields + select: selectableFields, + }); + } + + private async getObjectRecords({ + objectName, + filter, + orderBy, + limit, + offset, + repository, + graphqlQueryParser, + objectMetadataItemWithFieldsMaps, + }: { + objectName: string; + filter: + | Partial + | Partial[] + | undefined; + orderBy: Partial | undefined; + limit: number | undefined; + offset: number; + repository: WorkspaceRepository; + graphqlQueryParser: GraphqlQueryParser; + objectMetadataItemWithFieldsMaps: ObjectMetadataItemWithFieldMaps; + }): Promise { + const queryBuilder = repository.createQueryBuilder(objectName); + + const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder( + queryBuilder, + objectName, + filter ?? {}, + ); + + const orderByWithIdCondition: ObjectRecordOrderBy = [ + ...(orderBy ?? []).filter((item) => item !== undefined), + { id: OrderByDirection.AscNullsFirst }, + ]; + + const withOrderByQueryBuilder = graphqlQueryParser.applyOrderToBuilder( + withFilterQueryBuilder, + orderByWithIdCondition, + objectName, + true, + ); + + const queryBuilderWithSelect = this.applyRestrictedFieldsToQueryBuilder( + withOrderByQueryBuilder, + repository, + objectMetadataItemWithFieldsMaps, + ); + + return queryBuilderWithSelect + .skip(offset) + .take(limit ? Math.min(limit, QUERY_MAX_RECORDS) : QUERY_MAX_RECORDS) + .getMany(); + } + + private async getTotalCount({ + objectName, + filter, + repository, + graphqlQueryParser, + objectMetadataItemWithFieldsMaps, + }: { + objectName: string; + filter: + | Partial + | Partial[] + | undefined; + repository: WorkspaceRepository; + graphqlQueryParser: GraphqlQueryParser; + objectMetadataItemWithFieldsMaps: ObjectMetadataItemWithFieldMaps; + }): Promise { + const countQueryBuilder = repository.createQueryBuilder(objectName); + + const withFilterCountQueryBuilder = graphqlQueryParser.applyFilterToBuilder( + countQueryBuilder, + objectName, + filter ?? {}, + ); + + const withDeletedCountQueryBuilder = + graphqlQueryParser.applyDeletedAtToBuilder( + withFilterCountQueryBuilder, + filter ?? {}, + ); + + const queryBuilderWithSelect = this.applyRestrictedFieldsToQueryBuilder( + withDeletedCountQueryBuilder, + repository, + objectMetadataItemWithFieldsMaps, + ); + + return queryBuilderWithSelect.getCount(); + } } diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/services/update-record.service.ts b/packages/twenty-server/src/engine/core-modules/record-crud/services/update-record.service.ts index cd15439cf58..8d974af88ef 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/services/update-record.service.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/services/update-record.service.ts @@ -1,17 +1,29 @@ import { Injectable, Logger } from '@nestjs/common'; -import { CommonUpdateOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-update-one-query-runner.service'; +import deepEqual from 'deep-equal'; +import { isDefined, isValidUuid } from 'twenty-shared/utils'; +import { canObjectBeManagedByWorkflow } from 'twenty-shared/workflow'; + +import { + RecordCrudException, + RecordCrudExceptionCode, +} from 'src/engine/core-modules/record-crud/exceptions/record-crud.exception'; import { type UpdateRecordParams } from 'src/engine/core-modules/record-crud/types/update-record-params.type'; -import { CommonApiContextBuilder } from 'src/engine/core-modules/record-crud/utils/common-api-context-builder.util'; +import { getSelectedColumnsFromRestrictedFields } from 'src/engine/core-modules/record-crud/utils/get-selected-columns-from-restricted-fields.util'; +import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service'; import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; @Injectable() +// eslint-disable-next-line @nx/workspace-inject-workspace-repository export class UpdateRecordService { private readonly logger = new Logger(UpdateRecordService.name); constructor( - private readonly commonUpdateOneRunner: CommonUpdateOneQueryRunnerService, - private readonly commonApiContextBuilder: CommonApiContextBuilder, + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly recordInputTransformerService: RecordInputTransformerService, + private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService, ) {} async execute(params: UpdateRecordParams): Promise { @@ -19,37 +31,139 @@ export class UpdateRecordService { objectName, objectRecordId, objectRecord, + fieldsToUpdate, workspaceId, rolePermissionConfig, - userWorkspaceId, - apiKey, - createdBy, } = params; + if (!workspaceId) { + return { + success: false, + message: 'Failed to update record: Workspace ID is required', + error: 'Workspace ID not found', + }; + } + + if (!isDefined(objectRecordId) || !isValidUuid(objectRecordId)) { + return { + success: false, + message: 'Failed to update: Object record ID must be a valid UUID', + error: 'Invalid object record ID', + }; + } + try { - const { queryRunnerContext, selectedFields } = - await this.commonApiContextBuilder.build({ + const repository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + objectName, + rolePermissionConfig, + ); + + const { objectMetadataItemWithFieldsMaps } = + await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps( objectName, workspaceId, - rolePermissionConfig, - userWorkspaceId, - apiKey, - actorContext: createdBy, + ); + + const restrictedFields = + repository.objectRecordsPermissions?.[ + objectMetadataItemWithFieldsMaps.id + ]?.restrictedFields; + + const selectedColumns = getSelectedColumnsFromRestrictedFields( + restrictedFields, + objectMetadataItemWithFieldsMaps, + ); + + const previousObjectRecord = await repository.findOne({ + where: { + id: objectRecordId, + }, + select: selectedColumns, + }); + + if (!previousObjectRecord) { + throw new RecordCrudException( + `Failed to update: Record ${objectName} with id ${objectRecordId} not found`, + RecordCrudExceptionCode.RECORD_NOT_FOUND, + ); + } + + const fieldsToUpdateArray = fieldsToUpdate || Object.keys(objectRecord); + + if (fieldsToUpdateArray.length === 0) { + return { + success: true, + message: 'No fields to update', + result: previousObjectRecord, + }; + } + + if ( + !canObjectBeManagedByWorkflow({ + nameSingular: objectMetadataItemWithFieldsMaps.nameSingular, + isSystem: objectMetadataItemWithFieldsMaps.isSystem, + }) + ) { + throw new RecordCrudException( + 'Failed to update: Object cannot be updated by workflow', + RecordCrudExceptionCode.INVALID_REQUEST, + ); + } + + const objectRecordWithFilteredFields = Object.keys(objectRecord).reduce( + (acc, key) => { + if (fieldsToUpdateArray.includes(key)) { + return { + ...acc, + [key]: objectRecord[key], + }; + } + + return acc; + }, + {}, + ); + + const transformedObjectRecord = + await this.recordInputTransformerService.process({ + recordInput: objectRecordWithFilteredFields, + objectMetadataMapItem: objectMetadataItemWithFieldsMaps, }); - const result = await this.commonUpdateOneRunner.execute( - { id: objectRecordId, data: objectRecord, selectedFields }, - queryRunnerContext, - ); + const updatedObjectRecord = { + ...previousObjectRecord, + ...objectRecordWithFilteredFields, + }; + + if (!deepEqual(updatedObjectRecord, previousObjectRecord)) { + await repository.update( + objectRecordId, + { + ...transformedObjectRecord, + }, + undefined, + selectedColumns, + ); + } this.logger.log(`Record updated successfully in ${objectName}`); return { success: true, message: `Record updated successfully in ${objectName}`, - result, + result: updatedObjectRecord, }; } catch (error) { + if (error instanceof RecordCrudException) { + return { + success: false, + message: `Failed to update record in ${objectName}`, + error: error.message, + }; + } + this.logger.error(`Failed to update record: ${error}`); return { diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/services/upsert-record.service.ts b/packages/twenty-server/src/engine/core-modules/record-crud/services/upsert-record.service.ts index 8f099c660dd..4b385cd5347 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/services/upsert-record.service.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/services/upsert-record.service.ts @@ -1,56 +1,204 @@ import { Injectable, Logger } from '@nestjs/common'; -import { CommonCreateOneQueryRunnerService } from 'src/engine/api/common/common-query-runners/common-create-one-query-runner.service'; -import { type UpsertRecordParams } from 'src/engine/core-modules/record-crud/types/upsert-record-params.type'; -import { CommonApiContextBuilder } from 'src/engine/core-modules/record-crud/utils/common-api-context-builder.util'; +import { isDefined } from 'twenty-shared/utils'; +import { canObjectBeManagedByWorkflow } from 'twenty-shared/workflow'; + +import { + RecordCrudException, + RecordCrudExceptionCode, +} from 'src/engine/core-modules/record-crud/exceptions/record-crud.exception'; +import { UpsertRecordParams } from 'src/engine/core-modules/record-crud/types/upsert-record-params.type'; +import { getSelectedColumnsFromRestrictedFields } from 'src/engine/core-modules/record-crud/utils/get-selected-columns-from-restricted-fields.util'; +import { RecordInputTransformerService } from 'src/engine/core-modules/record-transformer/services/record-input-transformer.service'; import { type ToolOutput } from 'src/engine/core-modules/tool/types/tool-output.type'; +import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; +import { getCompositeTypeOrThrow } from 'src/engine/metadata-modules/field-metadata/utils/get-composite-type-or-throw.util'; +import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; +import { computeUniqueIndexWhereClause } from 'src/engine/metadata-modules/index-metadata/utils/compute-unique-index-where-clause.util'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; @Injectable() +// eslint-disable-next-line @nx/workspace-inject-workspace-repository export class UpsertRecordService { private readonly logger = new Logger(UpsertRecordService.name); constructor( - private readonly commonCreateOneRunner: CommonCreateOneQueryRunnerService, - private readonly commonApiContextBuilder: CommonApiContextBuilder, + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly recordInputTransformerService: RecordInputTransformerService, + private readonly workflowCommonWorkspaceService: WorkflowCommonWorkspaceService, ) {} async execute(params: UpsertRecordParams): Promise { - const { - objectName, - objectRecord, - workspaceId, - rolePermissionConfig, - userWorkspaceId, - apiKey, - createdBy, - } = params; + const { objectName, objectRecord, workspaceId, rolePermissionConfig } = + params; + + if (!workspaceId) { + return { + success: false, + message: 'Failed to upsert record: Workspace ID is required', + error: 'Workspace ID not found', + }; + } try { - const { queryRunnerContext, selectedFields } = - await this.commonApiContextBuilder.build({ + const repository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + objectName, + rolePermissionConfig, + ); + + const fieldsToUpdateArray = Object.keys(objectRecord).filter((field) => + isDefined(objectRecord[field]), + ); + + const { objectMetadataItemWithFieldsMaps } = + await this.workflowCommonWorkspaceService.getObjectMetadataItemWithFieldsMaps( objectName, workspaceId, - rolePermissionConfig, - userWorkspaceId, - apiKey, - actorContext: createdBy, + ); + + if ( + !canObjectBeManagedByWorkflow({ + nameSingular: objectMetadataItemWithFieldsMaps.nameSingular, + isSystem: objectMetadataItemWithFieldsMaps.isSystem, + }) + ) { + throw new RecordCrudException( + 'Failed to update: Object cannot be updated by workflow', + RecordCrudExceptionCode.INVALID_REQUEST, + ); + } + + const objectRecordWithFilteredFields = Object.keys(objectRecord).reduce( + (acc, key) => { + if (fieldsToUpdateArray.includes(key)) { + return { + ...acc, + [key]: objectRecord[key], + }; + } + + return acc; + }, + {}, + ); + + const transformedObjectRecord = + await this.recordInputTransformerService.process({ + recordInput: objectRecordWithFilteredFields, + objectMetadataMapItem: objectMetadataItemWithFieldsMaps, }); - // Use Common API's built-in upsert functionality - // This handles finding existing records by unique fields and updating or inserting - const result = await this.commonCreateOneRunner.execute( - { data: objectRecord, selectedFields, upsert: true }, - queryRunnerContext, + const uniqueFieldsToUpdate = fieldsToUpdateArray + .map( + (field) => + objectMetadataItemWithFieldsMaps.fieldIdByName[field] || + objectMetadataItemWithFieldsMaps.fieldIdByJoinColumnName[field], + ) + .map((fieldId) => objectMetadataItemWithFieldsMaps.fieldsById[fieldId]) + .filter((field) => field && (field.isUnique || field.name === 'id')); + + const conflictPathsUniqueFieldsToUpdate = uniqueFieldsToUpdate.flatMap( + (field) => { + if (isCompositeFieldMetadataType(field.type)) { + const compositeType = getCompositeTypeOrThrow(field.type); + + const uniqueProperties = compositeType.properties.filter( + (prop) => prop.isIncludedInUniqueConstraint, + ); + + const propertiesToUse = + uniqueProperties.length > 0 + ? uniqueProperties + : [compositeType.properties[0]]; + + return propertiesToUse.map((prop) => + computeCompositeColumnName(field, prop), + ); + } + + return [field.name]; + }, ); + const conflictPaths = + conflictPathsUniqueFieldsToUpdate.length > 0 + ? conflictPathsUniqueFieldsToUpdate + : ['id']; + + const indexPredicate = uniqueFieldsToUpdate + .map((field) => + computeUniqueIndexWhereClause({ + type: field.type, + name: field.name, + }), + ) + .filter(isDefined); + + const restrictedFields = + repository.objectRecordsPermissions?.[ + objectMetadataItemWithFieldsMaps.id + ]?.restrictedFields; + + const selectedColumns = getSelectedColumnsFromRestrictedFields( + restrictedFields, + objectMetadataItemWithFieldsMaps, + ); + + const upsertResult = await repository.upsert( + transformedObjectRecord, + { + conflictPaths: conflictPaths, + indexPredicate: + indexPredicate.length > 0 + ? `${indexPredicate.join(' AND ')}` + : undefined, + }, + undefined, + selectedColumns, + ); + + const upsertedRecordId = upsertResult.identifiers?.[0].id; + + if (!isDefined(upsertedRecordId)) { + throw new RecordCrudException( + `Failed to upsert record in ${objectName}`, + RecordCrudExceptionCode.RECORD_UPSERT_FAILED, + ); + } + + const upsertedRecord = await repository.findOne({ + where: { + id: upsertedRecordId, + }, + select: selectedColumns, + }); + + if (!upsertedRecord) { + throw new RecordCrudException( + `Record not found after upsert with id ${upsertedRecordId} in ${objectName}`, + RecordCrudExceptionCode.RECORD_UPSERT_FAILED, + ); + } + this.logger.log(`Record upserted successfully in ${objectName}`); return { success: true, message: `Record upserted successfully in ${objectName}`, - result, + result: upsertedRecord, }; } catch (error) { + if (error instanceof RecordCrudException) { + return { + success: false, + message: `Failed to upsert record in ${objectName}`, + error: error.message, + }; + } + this.logger.error(`Failed to upsert record: ${error}`); return { diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/types/execution-context.type.ts b/packages/twenty-server/src/engine/core-modules/record-crud/types/execution-context.type.ts index 35257323557..90207c5d5ae 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/types/execution-context.type.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/types/execution-context.type.ts @@ -1,14 +1,10 @@ import { type ActorMetadata } from 'twenty-shared/types'; -import { type ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity'; import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; export type RecordCrudExecutionContext = { workspaceId: string; rolePermissionConfig?: RolePermissionConfig; - userWorkspaceId?: string; - apiKey?: ApiKeyEntity; - createdBy?: ActorMetadata; }; export type CreateRecordExecutionContext = RecordCrudExecutionContext & { diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/types/find-records-params.type.ts b/packages/twenty-server/src/engine/core-modules/record-crud/types/find-records-params.type.ts index 384786b0d04..a314dee03e5 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/types/find-records-params.type.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/types/find-records-params.type.ts @@ -13,6 +13,6 @@ export type FindRecordsParams = FindRecordsInput & | Record[] | Partial | Partial[]; - orderBy?: ObjectRecordOrderBy; + orderBy?: Partial; offset?: number; }; diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/types/record-crud-input.type.ts b/packages/twenty-server/src/engine/core-modules/record-crud/types/record-crud-input.type.ts index 92f6ff503b8..2c8c395cf97 100644 --- a/packages/twenty-server/src/engine/core-modules/record-crud/types/record-crud-input.type.ts +++ b/packages/twenty-server/src/engine/core-modules/record-crud/types/record-crud-input.type.ts @@ -35,7 +35,7 @@ export type FindRecordsInput = { orderBy?: { // eslint-disable-next-line @typescript-eslint/no-explicit-any recordSorts?: any; - gqlOperationOrderBy?: ObjectRecordOrderBy; + gqlOperationOrderBy?: Partial; }; limit?: number; }; diff --git a/packages/twenty-server/src/engine/core-modules/record-crud/utils/common-api-context-builder.util.ts b/packages/twenty-server/src/engine/core-modules/record-crud/utils/common-api-context-builder.util.ts deleted file mode 100644 index fbed16c69d1..00000000000 --- a/packages/twenty-server/src/engine/core-modules/record-crud/utils/common-api-context-builder.util.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { type ActorMetadata } from 'twenty-shared/types'; - -import { type ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity'; -import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; -import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/workspace-metadata-cache/services/workspace-metadata-cache.service'; -import { type RolePermissionConfig } from 'src/engine/twenty-orm/types/role-permission-config'; -import { type CommonBaseQueryRunnerContext } from 'src/engine/api/common/types/common-base-query-runner-context.type'; -import { type AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; - -@Injectable() -export class CommonApiContextBuilder { - constructor( - private readonly workspaceMetadataCache: WorkspaceMetadataCacheService, - ) {} - - async build(params: { - objectName: string; - workspaceId: string; - userWorkspaceId?: string; - apiKey?: ApiKeyEntity; - rolePermissionConfig?: RolePermissionConfig; - actorContext?: ActorMetadata; - }): Promise<{ - queryRunnerContext: CommonBaseQueryRunnerContext; - selectedFields: Record; - }> { - if (!params.userWorkspaceId && !params.apiKey) { - throw new Error( - 'Either userWorkspaceId or apiKey is required for Common API operations', - ); - } - - const { objectMetadataMaps } = - await this.workspaceMetadataCache.getExistingOrRecomputeMetadataMaps({ - workspaceId: params.workspaceId, - }); - - const objectMetadata = this.getObjectMetadataOrThrow( - params.objectName, - objectMetadataMaps, - ); - - const authContext = this.buildAuthContext( - params.workspaceId, - params.userWorkspaceId, - params.apiKey, - params.actorContext, - ); - - const selectedFields = this.buildSelectedFields(objectMetadata); - - return { - queryRunnerContext: { - authContext, - objectMetadataItemWithFieldMaps: objectMetadata, - objectMetadataMaps, - }, - selectedFields, - }; - } - - private getObjectMetadataOrThrow( - objectName: string, - objectMetadataMaps: { - byId: Partial>; - idByNameSingular: Partial>; - }, - ): ObjectMetadataItemWithFieldMaps { - const objectMetadataId = objectMetadataMaps.idByNameSingular[objectName]; - - if (!objectMetadataId) { - throw new Error(`Object ${objectName} not found in workspace`); - } - - const objectMetadata = objectMetadataMaps.byId[objectMetadataId]; - - if (!objectMetadata) { - throw new Error(`Object metadata not found for ${objectName}`); - } - - return objectMetadata; - } - - private buildAuthContext( - workspaceId: string, - userWorkspaceId?: string, - apiKey?: ApiKeyEntity, - actorContext?: ActorMetadata, - ): AuthContext { - // Workspace object is intentionally minimal - the Common API validates - // and enriches the auth context internally - return { - workspace: { id: workspaceId } as unknown as AuthContext['workspace'], - workspaceMemberId: actorContext?.workspaceMemberId ?? undefined, - userWorkspaceId, - apiKey, - user: null, - }; - } - - private buildSelectedFields( - objectMetadata: ObjectMetadataItemWithFieldMaps, - ): Record { - const selectedFields: Record = { id: true }; - - for (const fieldName of Object.keys(objectMetadata.fieldIdByName)) { - selectedFields[fieldName] = true; - } - - return selectedFields; - } -} diff --git a/packages/twenty-server/src/engine/core-modules/workspace/workspace.module.ts b/packages/twenty-server/src/engine/core-modules/workspace/workspace.module.ts index 7281e0445cb..090f0a537e7 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/workspace.module.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/workspace.module.ts @@ -28,6 +28,7 @@ import { WorkspaceWorkspaceMemberListener } from 'src/engine/core-modules/worksp import { workspaceAutoResolverOpts } from 'src/engine/core-modules/workspace/workspace.auto-resolver-opts'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { WorkspaceResolver } from 'src/engine/core-modules/workspace/workspace.resolver'; +import { AgentModule } from 'src/engine/metadata-modules/agent/agent.module'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module'; import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; @@ -64,6 +65,7 @@ import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-m PermissionsModule, WorkspaceCacheStorageModule, RoleModule, + AgentModule, DnsManagerModule, WorkspaceDomainsModule, SubdomainManagerModule, diff --git a/packages/twenty-server/src/engine/metadata-modules/agent/agent-execution.service.ts b/packages/twenty-server/src/engine/metadata-modules/agent/agent-execution.service.ts index b919f8fe9ec..f5f8d3a11c9 100644 --- a/packages/twenty-server/src/engine/metadata-modules/agent/agent-execution.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/agent/agent-execution.service.ts @@ -78,7 +78,6 @@ export class AgentExecutionService implements AgentExecutionContext { actorContext, roleIds, excludeHandoffTools = false, - userWorkspaceId, toolHints, }: { system: string; @@ -87,7 +86,6 @@ export class AgentExecutionService implements AgentExecutionContext { actorContext?: ActorMetadata; roleIds?: string[]; excludeHandoffTools?: boolean; - userWorkspaceId?: string; toolHints?: ToolHints; }) { try { @@ -110,7 +108,6 @@ export class AgentExecutionService implements AgentExecutionContext { agent.workspaceId, actorContext, roleIds, - userWorkspaceId, toolHints, ); @@ -353,7 +350,6 @@ export class AgentExecutionService implements AgentExecutionContext { messages, actorContext, roleIds: [roleId, ...(agent?.roleId ? [agent?.roleId] : [])], - userWorkspaceId, toolHints, }); diff --git a/packages/twenty-server/src/engine/metadata-modules/agent/agent-tool-generator.service.ts b/packages/twenty-server/src/engine/metadata-modules/agent/agent-tool-generator.service.ts index 5820966c4f0..75ab21a5dca 100644 --- a/packages/twenty-server/src/engine/metadata-modules/agent/agent-tool-generator.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/agent/agent-tool-generator.service.ts @@ -3,26 +3,24 @@ import { InjectRepository } from '@nestjs/typeorm'; import { type ToolSet } from 'ai'; import { Repository } from 'typeorm'; -import { type ActorMetadata } from 'twenty-shared/types'; + +import type { ActorMetadata } from 'twenty-shared/types'; import { ToolAdapterService } from 'src/engine/core-modules/ai/services/tool-adapter.service'; import { ToolService } from 'src/engine/core-modules/ai/services/tool.service'; import { SearchArticlesTool } from 'src/engine/core-modules/tool/tools/search-articles-tool/search-articles-tool'; import { AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity'; -import { type ToolHints } from 'src/engine/metadata-modules/ai-router/types/tool-hints.interface'; import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service'; -import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; import { HELPER_AGENT } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-agents/agents/helper-agent'; import { WorkflowToolWorkspaceService as WorkflowToolService } from 'src/modules/workflow/workflow-tools/services/workflow-tool.workspace-service'; +import type { ToolHints } from 'src/engine/metadata-modules/ai-router/types/tool-hints.interface'; @Injectable() export class AgentToolGeneratorService { private readonly logger = new Logger(AgentToolGeneratorService.name); constructor( - @InjectRepository(RoleEntity) - private readonly roleRepository: Repository, @InjectRepository(AgentEntity) private readonly agentRepository: Repository, private readonly toolAdapterService: ToolAdapterService, @@ -37,7 +35,6 @@ export class AgentToolGeneratorService { workspaceId: string, actorContext?: ActorMetadata, roleIds?: string[], - userWorkspaceId?: string, toolHints?: ToolHints, ): Promise { let tools: ToolSet = {}; @@ -79,7 +76,6 @@ export class AgentToolGeneratorService { { intersectionOf: roleIds }, workspaceId, actorContext, - userWorkspaceId, toolHints, ); diff --git a/packages/twenty-server/src/engine/metadata-modules/agent/agent.module.ts b/packages/twenty-server/src/engine/metadata-modules/agent/agent.module.ts index 4f15346dba2..17a180fce38 100644 --- a/packages/twenty-server/src/engine/metadata-modules/agent/agent.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/agent/agent.module.ts @@ -1,4 +1,4 @@ -import { Module } from '@nestjs/common'; +import { Module, forwardRef } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { AiModule } from 'src/engine/core-modules/ai/ai.module'; @@ -10,6 +10,7 @@ import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity'; import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module'; import { FileModule } from 'src/engine/core-modules/file/file.module'; import { ThrottlerModule } from 'src/engine/core-modules/throttler/throttler.module'; +import { UserModule } from 'src/engine/core-modules/user/user.module'; import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module'; import { AgentRoleModule } from 'src/engine/metadata-modules/agent-role/agent-role.module'; @@ -72,6 +73,7 @@ import { AgentActorContextService } from './services/agent-actor-context.service TokenModule, WorkspaceDomainsModule, WorkflowToolsModule, + forwardRef(() => UserModule), UserWorkspaceModule, UserRoleModule, ], diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-manager.module.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-manager.module.ts index 50ff0eafe04..460075ffdef 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-manager.module.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-manager.module.ts @@ -5,6 +5,7 @@ import { ApplicationModule } from 'src/engine/core-modules/application/applicati import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; +import { AgentModule } from 'src/engine/metadata-modules/agent/agent.module'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module'; @@ -34,6 +35,7 @@ import { WorkspaceManagerService } from './workspace-manager.service'; WorkspaceHealthModule, FeatureFlagModule, PermissionsModule, + AgentModule, TypeOrmModule.forFeature([UserWorkspaceEntity, WorkspaceEntity]), RoleModule, UserRoleModule, diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/services/workflow-execution-context.service.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/services/workflow-execution-context.service.ts index c7768dc39eb..73dabc2f9c2 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/services/workflow-execution-context.service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/services/workflow-execution-context.service.ts @@ -1,25 +1,20 @@ -import { Injectable, Logger } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { isDefined } from 'twenty-shared/utils'; import { FieldActorSource } from 'twenty-shared/types'; import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service'; import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; -import { type WorkflowWorkspaceEntity } from 'src/modules/workflow/common/standard-objects/workflow.workspace-entity'; import { type WorkflowExecutionContext } from 'src/modules/workflow/workflow-executor/types/workflow-execution-context.type'; import { WorkflowRunWorkspaceService as WorkflowRunService } from 'src/modules/workflow/workflow-runner/workflow-run/workflow-run.workspace-service'; @Injectable() // eslint-disable-next-line @nx/workspace-inject-workspace-repository export class WorkflowExecutionContextService { - private readonly logger = new Logger(WorkflowExecutionContextService.name); - constructor( private readonly workflowRunService: WorkflowRunService, private readonly userWorkspaceService: UserWorkspaceService, private readonly userRoleService: UserRoleService, - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, ) {} async getExecutionContext(runInfo: { @@ -31,26 +26,29 @@ export class WorkflowExecutionContextService { workspaceId: runInfo.workspaceId, }); - if (!workflowRun.createdBy) { - throw new Error( - 'WorkflowRun createdBy field is missing - cannot determine execution context', - ); - } - const isActingOnBehalfOfUser = workflowRun.createdBy.source === FieldActorSource.MANUAL && isDefined(workflowRun.createdBy.workspaceMemberId); - const { userWorkspaceId, roleId } = await this.resolveUserContext({ - workflowRun, - isActingOnBehalfOfUser, - runInfo, - }); + let roleId: string | undefined; - if (!userWorkspaceId) { - throw new Error( - `userWorkspaceId is required but could not be determined for workflow run ${runInfo.workflowRunId}`, - ); + if (isActingOnBehalfOfUser) { + const workspaceMember = + await this.userWorkspaceService.getWorkspaceMemberOrThrow({ + workspaceMemberId: workflowRun.createdBy.workspaceMemberId!, + workspaceId: runInfo.workspaceId, + }); + + const userWorkspace = + await this.userWorkspaceService.getUserWorkspaceForUserOrThrow({ + userId: workspaceMember.userId, + workspaceId: runInfo.workspaceId, + }); + + roleId = await this.userRoleService.getRoleIdForUserWorkspace({ + userWorkspaceId: userWorkspace.id, + workspaceId: runInfo.workspaceId, + }); } const rolePermissionConfig = roleId @@ -61,87 +59,6 @@ export class WorkflowExecutionContextService { isActingOnBehalfOfUser, initiator: workflowRun.createdBy, rolePermissionConfig, - userWorkspaceId, }; } - - private async resolveUserContext({ - workflowRun, - isActingOnBehalfOfUser, - runInfo, - }: { - workflowRun: { - createdBy: { workspaceMemberId?: string | null }; - workflowId: string; - }; - isActingOnBehalfOfUser: boolean; - runInfo: { workflowRunId: string; workspaceId: string }; - }): Promise<{ userWorkspaceId?: string; roleId?: string }> { - // Determine which workspace member to use for context - let workspaceMemberId = workflowRun.createdBy.workspaceMemberId; - - // If workflow run was triggered automatically (no user initiator), - // use the workflow creator's workspace member - if (!isDefined(workspaceMemberId)) { - const workflow = await this.getWorkflow( - workflowRun.workflowId, - runInfo.workspaceId, - ); - - if (!workflow.createdBy?.workspaceMemberId) { - this.logger.error( - `Workflow ${workflowRun.workflowId} has no creator workspaceMemberId - cannot determine execution context`, - ); - - return { userWorkspaceId: undefined, roleId: undefined }; - } - - workspaceMemberId = workflow.createdBy.workspaceMemberId; - } - - const workspaceMember = - await this.userWorkspaceService.getWorkspaceMemberOrThrow({ - workspaceMemberId, - workspaceId: runInfo.workspaceId, - }); - - const userWorkspace = - await this.userWorkspaceService.getUserWorkspaceForUserOrThrow({ - userId: workspaceMember.userId, - workspaceId: runInfo.workspaceId, - }); - - if (!isActingOnBehalfOfUser) { - return { userWorkspaceId: userWorkspace.id, roleId: undefined }; - } - - const roleId = await this.userRoleService.getRoleIdForUserWorkspace({ - userWorkspaceId: userWorkspace.id, - workspaceId: runInfo.workspaceId, - }); - - return { userWorkspaceId: userWorkspace.id, roleId }; - } - - private async getWorkflow( - workflowId: string, - workspaceId: string, - ): Promise { - const workflowRepository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( - workspaceId, - 'workflow', - { shouldBypassPermissionChecks: true }, - ); - - const workflow = await workflowRepository.findOne({ - where: { id: workflowId }, - }); - - if (!workflow) { - throw new Error(`Workflow ${workflowId} not found`); - } - - return workflow; - } } diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/types/workflow-execution-context.type.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/types/workflow-execution-context.type.ts index 0b7ef24a39f..8714f55fb88 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/types/workflow-execution-context.type.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/types/workflow-execution-context.type.ts @@ -6,5 +6,4 @@ export type WorkflowExecutionContext = { isActingOnBehalfOfUser: boolean; initiator: ActorMetadata; rolePermissionConfig: RolePermissionConfig; - userWorkspaceId?: string; }; diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/ai-agent/ai-agent.workflow-action.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/ai-agent/ai-agent.workflow-action.ts index 62b700ae77a..3df983e3b00 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/ai-agent/ai-agent.workflow-action.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/ai-agent/ai-agent.workflow-action.ts @@ -86,7 +86,6 @@ export class AiAgentWorkflowAction implements WorkflowAction { ? executionContext.initiator : undefined, rolePermissionConfig: executionContext.rolePermissionConfig, - userWorkspaceId: executionContext.userWorkspaceId, }, ); diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/ai-agent/services/ai-agent-executor.service.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/ai-agent/services/ai-agent-executor.service.ts index d3119a623cc..721f3add8f2 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/ai-agent/services/ai-agent-executor.service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/ai-agent/services/ai-agent-executor.service.ts @@ -38,7 +38,6 @@ export class AiAgentExecutorService { workspaceId: string, actorContext?: ActorMetadata, rolePermissionConfig?: RolePermissionConfig, - userWorkspaceId?: string, ): Promise { const roleTarget = await this.roleTargetsRepository.findOne({ where: { @@ -77,7 +76,6 @@ export class AiAgentExecutorService { effectiveRoleContext, workspaceId, actorContext, - userWorkspaceId, ); return { @@ -92,14 +90,12 @@ export class AiAgentExecutorService { userPrompt, actorContext, rolePermissionConfig, - userWorkspaceId, }: { agent: AgentEntity | null; schema: OutputSchema; userPrompt: string; actorContext?: ActorMetadata; rolePermissionConfig?: RolePermissionConfig; - userWorkspaceId?: string; }): Promise { try { const registeredModel = @@ -111,7 +107,6 @@ export class AiAgentExecutorService { agent.workspaceId, actorContext, rolePermissionConfig, - userWorkspaceId, ) : {}; diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/create-record.workflow-action.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/create-record.workflow-action.ts index 7f89017529c..767961bebc4 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/create-record.workflow-action.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/create-record.workflow-action.ts @@ -62,7 +62,6 @@ export class CreateRecordWorkflowAction implements WorkflowAction { workspaceId, createdBy, rolePermissionConfig: executionContext.rolePermissionConfig, - userWorkspaceId: executionContext.userWorkspaceId, }); if (!toolOutput.success) { diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/delete-record.workflow-action.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/delete-record.workflow-action.ts index 01346ecd802..3d5d1b2d283 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/delete-record.workflow-action.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/delete-record.workflow-action.ts @@ -80,8 +80,6 @@ export class DeleteRecordWorkflowAction implements WorkflowAction { objectRecordId: workflowActionInput.objectRecordId, workspaceId, rolePermissionConfig: executionContext.rolePermissionConfig, - userWorkspaceId: executionContext.userWorkspaceId, - createdBy: executionContext.initiator, soft: true, }); diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/find-records.workflow-action.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/find-records.workflow-action.ts index db788edd589..d7fbbce49d3 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/find-records.workflow-action.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/find-records.workflow-action.ts @@ -71,22 +71,23 @@ export class FindRecordsWorkflowAction implements WorkflowAction { limit: workflowActionInput.limit, workspaceId, rolePermissionConfig: executionContext.rolePermissionConfig, - userWorkspaceId: executionContext.userWorkspaceId, - createdBy: executionContext.initiator, }); - if (!toolOutput.success || !toolOutput.result) { + if (!toolOutput.success) { throw new RecordCrudException( toolOutput.error || toolOutput.message, RecordCrudExceptionCode.QUERY_FAILED, ); } + const records = toolOutput.result?.records ?? []; + const totalCount = toolOutput.result?.count ?? 0; + return { result: { - first: toolOutput.result.records[0], - all: toolOutput.result.records, - totalCount: toolOutput.result.totalCount, + first: records[0], + all: records, + totalCount, }, }; } diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/update-record.workflow-action.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/update-record.workflow-action.ts index ce4eada7d4f..62ec017610d 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/update-record.workflow-action.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/update-record.workflow-action.ts @@ -82,8 +82,6 @@ export class UpdateRecordWorkflowAction implements WorkflowAction { fieldsToUpdate: workflowActionInput.fieldsToUpdate, workspaceId, rolePermissionConfig: executionContext.rolePermissionConfig, - userWorkspaceId: executionContext.userWorkspaceId, - createdBy: executionContext.initiator, }); if (!toolOutput.success) { diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/upsert-record.workflow-action.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/upsert-record.workflow-action.ts index 3937047beb8..7ca7d475684 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/upsert-record.workflow-action.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-actions/record-crud/upsert-record.workflow-action.ts @@ -76,8 +76,6 @@ export class UpsertRecordWorkflowAction implements WorkflowAction { objectRecord: workflowActionInput.objectRecord, workspaceId, rolePermissionConfig: executionContext.rolePermissionConfig, - userWorkspaceId: executionContext.userWorkspaceId, - createdBy: executionContext.initiator, }); if (!toolOutput.success) { diff --git a/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-query-hook.module.ts b/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-query-hook.module.ts index d7ef75765a3..0063b200077 100644 --- a/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-query-hook.module.ts +++ b/packages/twenty-server/src/modules/workspace-member/query-hooks/workspace-member-query-hook.module.ts @@ -1,4 +1,4 @@ -import { forwardRef, Module } from '@nestjs/common'; +import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; @@ -36,7 +36,7 @@ import { WorkspaceMemberUpdateOnePreQueryHook } from 'src/modules/workspace-memb imports: [ FeatureFlagModule, PermissionsModule, - forwardRef(() => UserWorkspaceModule), + UserWorkspaceModule, TypeOrmModule.forFeature([UserWorkspaceEntity]), ], }) diff --git a/packages/twenty-server/test/integration/metadata/suites/agent/agent-tool.service.integration-spec.ts b/packages/twenty-server/test/integration/metadata/suites/agent/agent-tool.service.integration-spec.ts index 11625c37db1..97982663519 100644 --- a/packages/twenty-server/test/integration/metadata/suites/agent/agent-tool.service.integration-spec.ts +++ b/packages/twenty-server/test/integration/metadata/suites/agent/agent-tool.service.integration-spec.ts @@ -55,10 +55,11 @@ describe('AgentToolGeneratorService Integration', () => { ); expect(tools).toBeDefined(); - expect(Object.keys(tools)).toHaveLength(6); + expect(Object.keys(tools)).toHaveLength(7); expect(Object.keys(tools)).toContain('create_testObject'); expect(Object.keys(tools)).toContain('update_testObject'); expect(Object.keys(tools)).toContain('find_testObject'); + expect(Object.keys(tools)).toContain('find_one_testObject'); expect(Object.keys(tools)).toContain('soft_delete_testObject'); expect(Object.keys(tools)).toContain('soft_delete_many_testObject'); expect(Object.keys(tools)).toContain('http_request'); @@ -99,8 +100,9 @@ describe('AgentToolGeneratorService Integration', () => { ); expect(tools).toBeDefined(); - expect(Object.keys(tools)).toHaveLength(2); + expect(Object.keys(tools)).toHaveLength(3); expect(Object.keys(tools)).toContain('find_testObject'); + expect(Object.keys(tools)).toContain('find_one_testObject'); expect(Object.keys(tools)).toContain('http_request'); expect(Object.keys(tools)).not.toContain('create_testObject'); expect(Object.keys(tools)).not.toContain('update_testObject'); @@ -156,7 +158,7 @@ describe('AgentToolGeneratorService Integration', () => { [context.testRoleId], ); - expect(Object.keys(tools)).toHaveLength(6); + expect(Object.keys(tools)).toHaveLength(7); }); }); }); diff --git a/packages/twenty-server/test/integration/metadata/suites/agent/utils/agent-tool-test-utils.ts b/packages/twenty-server/test/integration/metadata/suites/agent/utils/agent-tool-test-utils.ts index 4af26c3e768..4479b8bcf57 100644 --- a/packages/twenty-server/test/integration/metadata/suites/agent/utils/agent-tool-test-utils.ts +++ b/packages/twenty-server/test/integration/metadata/suites/agent/utils/agent-tool-test-utils.ts @@ -338,94 +338,3 @@ export const createAgentToolTestModule = testRoleId, }; }; - -export const createMockRepository = () => ({ - find: jest.fn(), - findOne: jest.fn(), - save: jest.fn(), - update: jest.fn(), - softDelete: jest.fn(), - remove: jest.fn(), - delete: jest.fn(), -}); - -export const setupBasicPermissions = (context: AgentToolTestContext) => { - jest - .spyOn(context.agentService, 'findOneAgent') - .mockResolvedValue(context.testAgent); - jest - .spyOn(context.roleRepository, 'findOne') - .mockResolvedValue(context.testRole); - jest - .spyOn( - context.workspacePermissionsCacheService, - 'getRolesPermissionsFromCache', - ) - .mockResolvedValue({ - data: { - [context.testRoleId]: { - [context.testObjectMetadata.id]: { - canReadObjectRecords: true, - canUpdateObjectRecords: true, - canSoftDeleteObjectRecords: true, - canDestroyObjectRecords: false, - restrictedFields: {}, - }, - }, - }, - version: '1.0', - }); - jest - .spyOn(context.objectMetadataService, 'findManyWithinWorkspace') - .mockResolvedValue([context.testObjectMetadata]); -}; - -export const setupRepositoryMock = ( - context: AgentToolTestContext, - mockRepository: any, -) => { - jest - .spyOn(context.twentyORMGlobalManager, 'getRepositoryForWorkspace') - .mockResolvedValue(mockRepository); -}; - -export const createTestRecord = ( - id: string, - data: Record = {}, -) => ({ - id, - name: `Test Record ${id}`, - createdAt: new Date(), - updatedAt: new Date(), - ...data, -}); - -export const createTestRecords = ( - count: number, - baseData: Record = {}, -) => { - return Array.from({ length: count }, (_, i) => - createTestRecord(`record-${i + 1}`, baseData), - ); -}; - -export const expectSuccessResult = (result: any, expectedMessage?: string) => { - expect(result.success).toBe(true); - if (expectedMessage) { - expect(result.message).toContain(expectedMessage); - } -}; - -export const expectErrorResult = ( - result: any, - expectedError?: string, - expectedMessage?: string, -) => { - expect(result.success).toBe(false); - if (expectedError) { - expect(result.error).toBe(expectedError); - } - if (expectedMessage) { - expect(result.message).toContain(expectedMessage); - } -};