Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6fa6bbd2ef | |||
| 1c64c7f416 |
@@ -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<Scalars['String']>;
|
||||
fromAgentId: Scalars['UUID'];
|
||||
toAgentId: Scalars['UUID'];
|
||||
};
|
||||
|
||||
export type CreateAgentInput = {
|
||||
description?: InputMaybe<Scalars['String']>;
|
||||
icon?: InputMaybe<Scalars['String']>;
|
||||
label: Scalars['String'];
|
||||
modelConfiguration?: InputMaybe<Scalars['JSON']>;
|
||||
modelId: Scalars['String'];
|
||||
name?: InputMaybe<Scalars['String']>;
|
||||
prompt: Scalars['String'];
|
||||
responseFormat?: InputMaybe<Scalars['JSON']>;
|
||||
roleId?: InputMaybe<Scalars['UUID']>;
|
||||
};
|
||||
|
||||
export type CreateApiKeyInput = {
|
||||
expiresAt: Scalars['String'];
|
||||
name: Scalars['String'];
|
||||
@@ -1686,8 +1709,10 @@ export type Mutation = {
|
||||
checkPublicDomainValidRecords?: Maybe<DomainValidRecords>;
|
||||
checkoutSession: BillingSessionOutput;
|
||||
computeStepOutputSchema: Scalars['JSON'];
|
||||
createAgentHandoff: Scalars['Boolean'];
|
||||
createApiKey: ApiKey;
|
||||
createApprovedAccessDomain: ApprovedAccessDomain;
|
||||
createChatThread: AgentChatThread;
|
||||
createCoreView: CoreView;
|
||||
createCoreViewField: CoreViewField;
|
||||
createCoreViewFilter: CoreViewFilter;
|
||||
@@ -1702,6 +1727,7 @@ export type Mutation = {
|
||||
createManyCoreViewGroups: Array<CoreViewGroup>;
|
||||
createOIDCIdentityProvider: SetupSsoOutput;
|
||||
createObjectEvent: Analytics;
|
||||
createOneAgent: Agent;
|
||||
createOneAppToken: AppToken;
|
||||
createOneCronTrigger: CronTrigger;
|
||||
createOneDatabaseEventTrigger: DatabaseEventTrigger;
|
||||
@@ -1733,6 +1759,7 @@ export type Mutation = {
|
||||
deleteEmailingDomain: Scalars['Boolean'];
|
||||
deleteFile: File;
|
||||
deleteJobs: DeleteJobsResponse;
|
||||
deleteOneAgent: Agent;
|
||||
deleteOneCronTrigger: CronTrigger;
|
||||
deleteOneDatabaseEventTrigger: DatabaseEventTrigger;
|
||||
deleteOneField: Field;
|
||||
@@ -1779,6 +1806,7 @@ export type Mutation = {
|
||||
initiateOTPProvisioning: InitiateTwoFactorAuthenticationProvisioningOutput;
|
||||
initiateOTPProvisioningForAuthenticatedUser: InitiateTwoFactorAuthenticationProvisioningOutput;
|
||||
publishServerlessFunction: ServerlessFunction;
|
||||
removeAgentHandoff: Scalars['Boolean'];
|
||||
removeRoleFromAgent: Scalars['Boolean'];
|
||||
renewToken: AuthTokens;
|
||||
resendEmailVerificationToken: ResendEmailVerificationTokenOutput;
|
||||
@@ -1815,6 +1843,7 @@ export type Mutation = {
|
||||
updateCoreViewSort: CoreViewSort;
|
||||
updateDatabaseConfigVariable: Scalars['Boolean'];
|
||||
updateLabPublicFeatureFlag: FeatureFlagDto;
|
||||
updateOneAgent: Agent;
|
||||
updateOneApplicationVariable: Scalars['Boolean'];
|
||||
updateOneCronTrigger: CronTrigger;
|
||||
updateOneDatabaseEventTrigger: DatabaseEventTrigger;
|
||||
@@ -1899,6 +1928,11 @@ export type MutationComputeStepOutputSchemaArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateAgentHandoffArgs = {
|
||||
input: CreateAgentHandoffInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateApiKeyArgs = {
|
||||
input: CreateApiKeyInput;
|
||||
};
|
||||
@@ -1984,6 +2018,11 @@ export type MutationCreateObjectEventArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateOneAgentArgs = {
|
||||
input: CreateAgentInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateOneCronTriggerArgs = {
|
||||
input: CreateCronTriggerInput;
|
||||
};
|
||||
@@ -2126,6 +2165,11 @@ export type MutationDeleteJobsArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteOneAgentArgs = {
|
||||
input: AgentIdInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteOneCronTriggerArgs = {
|
||||
input: CronTriggerIdInput;
|
||||
};
|
||||
@@ -2339,6 +2383,11 @@ export type MutationPublishServerlessFunctionArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationRemoveAgentHandoffArgs = {
|
||||
input: RemoveAgentHandoffInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationRemoveRoleFromAgentArgs = {
|
||||
agentId: Scalars['UUID'];
|
||||
};
|
||||
@@ -2523,6 +2572,11 @@ export type MutationUpdateLabPublicFeatureFlagArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationUpdateOneAgentArgs = {
|
||||
input: UpdateAgentInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationUpdateOneApplicationVariableArgs = {
|
||||
applicationId: Scalars['UUID'];
|
||||
key: Scalars['String'];
|
||||
@@ -3046,18 +3100,25 @@ export type Query = {
|
||||
apiKey?: Maybe<ApiKey>;
|
||||
apiKeys: Array<ApiKey>;
|
||||
billingPortalSession: BillingSessionOutput;
|
||||
chatMessages: Array<AgentChatMessage>;
|
||||
chatThread: AgentChatThread;
|
||||
chatThreads: Array<AgentChatThread>;
|
||||
checkUserExists: CheckUserExistOutput;
|
||||
checkWorkspaceInviteHashIsValid: WorkspaceInviteHashValidOutput;
|
||||
currentUser: User;
|
||||
currentWorkspace: Workspace;
|
||||
field: Field;
|
||||
fields: FieldConnection;
|
||||
findAgentHandoffTargets: Array<Agent>;
|
||||
findAgentHandoffs: Array<AgentHandoff>;
|
||||
findManyAgents: Array<Agent>;
|
||||
findManyApplications: Array<Application>;
|
||||
findManyCronTriggers: Array<CronTrigger>;
|
||||
findManyDatabaseEventTriggers: Array<DatabaseEventTrigger>;
|
||||
findManyPublicDomains: Array<PublicDomain>;
|
||||
findManyRouteTriggers: Array<RouteTrigger>;
|
||||
findManyServerlessFunctions: Array<ServerlessFunction>;
|
||||
findOneAgent: Agent;
|
||||
findOneApplication: Application;
|
||||
findOneCronTrigger: CronTrigger;
|
||||
findOneDatabaseEventTrigger: DatabaseEventTrigger;
|
||||
@@ -3130,6 +3191,16 @@ export type QueryBillingPortalSessionArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type QueryChatMessagesArgs = {
|
||||
threadId: Scalars['UUID'];
|
||||
};
|
||||
|
||||
|
||||
export type QueryChatThreadArgs = {
|
||||
id: Scalars['UUID'];
|
||||
};
|
||||
|
||||
|
||||
export type QueryCheckUserExistsArgs = {
|
||||
captchaToken?: InputMaybe<Scalars['String']>;
|
||||
email: Scalars['String'];
|
||||
@@ -3141,6 +3212,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'];
|
||||
};
|
||||
@@ -3485,6 +3571,11 @@ export enum RemoteTableStatus {
|
||||
SYNCED = 'SYNCED'
|
||||
}
|
||||
|
||||
export type RemoveAgentHandoffInput = {
|
||||
fromAgentId: Scalars['UUID'];
|
||||
toAgentId: Scalars['UUID'];
|
||||
};
|
||||
|
||||
export type ResendEmailVerificationTokenOutput = {
|
||||
__typename?: 'ResendEmailVerificationTokenOutput';
|
||||
success: Scalars['Boolean'];
|
||||
@@ -3908,6 +3999,19 @@ export type UuidFilterComparison = {
|
||||
notLike?: InputMaybe<Scalars['UUID']>;
|
||||
};
|
||||
|
||||
export type UpdateAgentInput = {
|
||||
description?: InputMaybe<Scalars['String']>;
|
||||
icon?: InputMaybe<Scalars['String']>;
|
||||
id: Scalars['UUID'];
|
||||
label: Scalars['String'];
|
||||
modelConfiguration?: InputMaybe<Scalars['JSON']>;
|
||||
modelId: Scalars['String'];
|
||||
name: Scalars['String'];
|
||||
prompt: Scalars['String'];
|
||||
responseFormat?: InputMaybe<Scalars['JSON']>;
|
||||
roleId?: InputMaybe<Scalars['UUID']>;
|
||||
};
|
||||
|
||||
export type UpdateApiKeyInput = {
|
||||
expiresAt?: InputMaybe<Scalars['String']>;
|
||||
id: Scalars['UUID'];
|
||||
|
||||
+1
-1
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
+2
@@ -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,
|
||||
|
||||
-13
@@ -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)) {
|
||||
|
||||
@@ -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<ToolSet> {
|
||||
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,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
+102
-28
@@ -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<ToolOutput> {
|
||||
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 {
|
||||
|
||||
+100
-25
@@ -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<ToolOutput> {
|
||||
@@ -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',
|
||||
};
|
||||
|
||||
+188
-29
@@ -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<ToolOutput<{ records: unknown[]; totalCount: number }>> {
|
||||
): Promise<ToolOutput<FindRecordsResult>> {
|
||||
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<T extends ObjectLiteral>(
|
||||
queryBuilder: WorkspaceSelectQueryBuilder<T>,
|
||||
repository: WorkspaceRepository<T>,
|
||||
objectMetadataItemWithFieldsMaps: ObjectMetadataItemWithFieldMaps,
|
||||
): WorkspaceSelectQueryBuilder<T> {
|
||||
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<T extends ObjectLiteral>({
|
||||
objectName,
|
||||
filter,
|
||||
orderBy,
|
||||
limit,
|
||||
offset,
|
||||
repository,
|
||||
graphqlQueryParser,
|
||||
objectMetadataItemWithFieldsMaps,
|
||||
}: {
|
||||
objectName: string;
|
||||
filter:
|
||||
| Partial<ObjectRecordFilter>
|
||||
| Partial<ObjectRecordFilter>[]
|
||||
| undefined;
|
||||
orderBy: Partial<ObjectRecordOrderBy> | undefined;
|
||||
limit: number | undefined;
|
||||
offset: number;
|
||||
repository: WorkspaceRepository<T>;
|
||||
graphqlQueryParser: GraphqlQueryParser;
|
||||
objectMetadataItemWithFieldsMaps: ObjectMetadataItemWithFieldMaps;
|
||||
}): Promise<T[]> {
|
||||
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<ObjectRecordFilter>
|
||||
| Partial<ObjectRecordFilter>[]
|
||||
| undefined;
|
||||
repository: WorkspaceRepository<ObjectLiteral>;
|
||||
graphqlQueryParser: GraphqlQueryParser;
|
||||
objectMetadataItemWithFieldsMaps: ObjectMetadataItemWithFieldMaps;
|
||||
}): Promise<number> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
+132
-18
@@ -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<ToolOutput> {
|
||||
@@ -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 {
|
||||
|
||||
+174
-26
@@ -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<ToolOutput> {
|
||||
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 {
|
||||
|
||||
-4
@@ -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 & {
|
||||
|
||||
+1
-1
@@ -13,6 +13,6 @@ export type FindRecordsParams = FindRecordsInput &
|
||||
| Record<string, unknown>[]
|
||||
| Partial<ObjectRecordFilter>
|
||||
| Partial<ObjectRecordFilter>[];
|
||||
orderBy?: ObjectRecordOrderBy;
|
||||
orderBy?: Partial<ObjectRecordOrderBy>;
|
||||
offset?: number;
|
||||
};
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ export type FindRecordsInput = {
|
||||
orderBy?: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
recordSorts?: any;
|
||||
gqlOperationOrderBy?: ObjectRecordOrderBy;
|
||||
gqlOperationOrderBy?: Partial<ObjectRecordOrderBy>;
|
||||
};
|
||||
limit?: number;
|
||||
};
|
||||
|
||||
-114
@@ -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<string, boolean>;
|
||||
}> {
|
||||
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<Record<string, ObjectMetadataItemWithFieldMaps>>;
|
||||
idByNameSingular: Partial<Record<string, string>>;
|
||||
},
|
||||
): 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<string, boolean> {
|
||||
const selectedFields: Record<string, boolean> = { id: true };
|
||||
|
||||
for (const fieldName of Object.keys(objectMetadata.fieldIdByName)) {
|
||||
selectedFields[fieldName] = true;
|
||||
}
|
||||
|
||||
return selectedFields;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
+3
-7
@@ -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<RoleEntity>,
|
||||
@InjectRepository(AgentEntity)
|
||||
private readonly agentRepository: Repository<AgentEntity>,
|
||||
private readonly toolAdapterService: ToolAdapterService,
|
||||
@@ -37,7 +35,6 @@ export class AgentToolGeneratorService {
|
||||
workspaceId: string,
|
||||
actorContext?: ActorMetadata,
|
||||
roleIds?: string[],
|
||||
userWorkspaceId?: string,
|
||||
toolHints?: ToolHints,
|
||||
): Promise<ToolSet> {
|
||||
let tools: ToolSet = {};
|
||||
@@ -79,7 +76,6 @@ export class AgentToolGeneratorService {
|
||||
{ intersectionOf: roleIds },
|
||||
workspaceId,
|
||||
actorContext,
|
||||
userWorkspaceId,
|
||||
toolHints,
|
||||
);
|
||||
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
|
||||
@@ -4,6 +4,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
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,
|
||||
|
||||
+5
-2
@@ -2,7 +2,7 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { type GaxiosError } from 'gaxios';
|
||||
import { type calendar_v3 as calendarV3 } from 'googleapis';
|
||||
import { google, type calendar_v3 as calendarV3 } from 'googleapis';
|
||||
|
||||
import { formatGoogleCalendarEvents } from 'src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/utils/format-google-calendar-event.util';
|
||||
import { parseGaxiosError } from 'src/modules/calendar/calendar-event-import-manager/drivers/google-calendar/utils/parse-gaxios-error.util';
|
||||
@@ -31,7 +31,10 @@ export class GoogleCalendarGetEventsService {
|
||||
connectedAccount,
|
||||
);
|
||||
|
||||
const googleCalendarClient = oAuth2Client.calendar({ version: 'v3' });
|
||||
const googleCalendarClient = google.calendar({
|
||||
version: 'v3',
|
||||
auth: oAuth2Client,
|
||||
});
|
||||
|
||||
let nextSyncToken: string | null | undefined;
|
||||
let nextPageToken: string | undefined;
|
||||
|
||||
+5
-2
@@ -1,5 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { google } from 'googleapis';
|
||||
|
||||
import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service';
|
||||
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
|
||||
@@ -17,11 +19,12 @@ export class GoogleEmailAliasManagerService {
|
||||
connectedAccount,
|
||||
);
|
||||
|
||||
const people = oAuth2Client.people({
|
||||
const peopleClient = google.people({
|
||||
version: 'v1',
|
||||
auth: oAuth2Client,
|
||||
});
|
||||
|
||||
const emailsResponse = await people.people.get({
|
||||
const emailsResponse = await peopleClient.people.get({
|
||||
resourceName: 'people/me',
|
||||
personFields: 'emailAddresses',
|
||||
});
|
||||
|
||||
+5
-5
@@ -1,6 +1,6 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { google, GoogleApis } from 'googleapis';
|
||||
import { google, type Auth } from 'googleapis';
|
||||
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
|
||||
@@ -11,7 +11,9 @@ export class GoogleOAuth2ClientManagerService {
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
public async getOAuth2Client(refreshToken: string): Promise<GoogleApis> {
|
||||
public async getOAuth2Client(
|
||||
refreshToken: string,
|
||||
): Promise<Auth.OAuth2Client> {
|
||||
const gmailClientId = this.twentyConfigService.get('AUTH_GOOGLE_CLIENT_ID');
|
||||
const gmailClientSecret = this.twentyConfigService.get(
|
||||
'AUTH_GOOGLE_CLIENT_SECRET',
|
||||
@@ -27,9 +29,7 @@ export class GoogleOAuth2ClientManagerService {
|
||||
refresh_token: refreshToken,
|
||||
});
|
||||
|
||||
google.options({ auth: oAuth2Client });
|
||||
|
||||
return google;
|
||||
return oAuth2Client;
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Error in ${GoogleOAuth2ClientManagerService.name}`,
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type Client } from '@microsoft/microsoft-graph-client';
|
||||
import { GoogleApis } from 'googleapis';
|
||||
import { type Auth } from 'googleapis';
|
||||
|
||||
import { GoogleOAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/drivers/google/google-oauth2-client-manager.service';
|
||||
import { MicrosoftOAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/drivers/microsoft/microsoft-oauth2-client-manager.service';
|
||||
@@ -19,7 +19,7 @@ export class OAuth2ClientManagerService {
|
||||
ConnectedAccountWorkspaceEntity,
|
||||
'provider' | 'refreshToken'
|
||||
>,
|
||||
): Promise<GoogleApis> {
|
||||
): Promise<Auth.OAuth2Client> {
|
||||
return this.googleOAuth2ClientManagerService.getOAuth2Client(
|
||||
connectedAccount.refreshToken,
|
||||
);
|
||||
|
||||
+6
-1
@@ -1,5 +1,7 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { google } from 'googleapis';
|
||||
|
||||
import {
|
||||
MessageFolder,
|
||||
MessageFolderDriver,
|
||||
@@ -39,7 +41,10 @@ export class GmailGetAllFoldersService implements MessageFolderDriver {
|
||||
connectedAccount,
|
||||
);
|
||||
|
||||
const gmailClient = oAuth2Client.gmail({ version: 'v1' });
|
||||
const gmailClient = google.gmail({
|
||||
version: 'v1',
|
||||
auth: oAuth2Client,
|
||||
});
|
||||
|
||||
const response = await gmailClient.users.labels
|
||||
.list({ userId: 'me' })
|
||||
|
||||
+13
-16
@@ -1,5 +1,6 @@
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { google } from 'googleapis';
|
||||
import { ConnectedAccountProvider } from 'twenty-shared/types';
|
||||
|
||||
import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service';
|
||||
@@ -63,6 +64,10 @@ describe('GmailGetMessageListService', () => {
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('getMessageList', () => {
|
||||
it('should return 0 messageExternalIds when gmail returns 0 messages', async () => {
|
||||
const mockGmailClient = {
|
||||
@@ -78,13 +83,11 @@ describe('GmailGetMessageListService', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const mockOAuth2Client = {
|
||||
gmail: jest.fn().mockReturnValue(mockGmailClient),
|
||||
};
|
||||
jest.spyOn(google, 'gmail').mockReturnValue(mockGmailClient as never);
|
||||
|
||||
(
|
||||
oAuth2ClientManagerService.getGoogleOAuth2Client as jest.Mock
|
||||
).mockResolvedValue(mockOAuth2Client);
|
||||
).mockResolvedValue({});
|
||||
|
||||
const result = await service.getMessageLists({
|
||||
messageChannel: { syncCursor: '', id: 'my-id' },
|
||||
@@ -133,13 +136,11 @@ describe('GmailGetMessageListService', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const mockOAuth2Client = {
|
||||
gmail: jest.fn().mockReturnValue(mockGmailClient),
|
||||
};
|
||||
jest.spyOn(google, 'gmail').mockReturnValue(mockGmailClient as never);
|
||||
|
||||
(
|
||||
oAuth2ClientManagerService.getGoogleOAuth2Client as jest.Mock
|
||||
).mockResolvedValue(mockOAuth2Client);
|
||||
).mockResolvedValue({});
|
||||
|
||||
const result = await service.getMessageLists({
|
||||
messageChannel: { syncCursor: '', id: 'my-id' },
|
||||
@@ -188,13 +189,11 @@ describe('GmailGetMessageListService', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const mockOAuth2Client = {
|
||||
gmail: jest.fn().mockReturnValue(mockGmailClient),
|
||||
};
|
||||
jest.spyOn(google, 'gmail').mockReturnValue(mockGmailClient as never);
|
||||
|
||||
(
|
||||
oAuth2ClientManagerService.getGoogleOAuth2Client as jest.Mock
|
||||
).mockResolvedValue(mockOAuth2Client);
|
||||
).mockResolvedValue({});
|
||||
|
||||
const result = await service.getMessageLists({
|
||||
messageChannel: { syncCursor: '', id: 'my-id' },
|
||||
@@ -224,13 +223,11 @@ describe('GmailGetMessageListService', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const mockOAuth2Client = {
|
||||
gmail: jest.fn().mockReturnValue(mockGmailClient),
|
||||
};
|
||||
jest.spyOn(google, 'gmail').mockReturnValue(mockGmailClient as never);
|
||||
|
||||
(
|
||||
oAuth2ClientManagerService.getGoogleOAuth2Client as jest.Mock
|
||||
).mockResolvedValue(mockOAuth2Client);
|
||||
).mockResolvedValue({});
|
||||
|
||||
const result = await service.getMessageLists({
|
||||
messageChannel: { syncCursor: '', id: 'my-id' },
|
||||
|
||||
+9
-3
@@ -1,7 +1,7 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { type gmail_v1 as gmailV1 } from 'googleapis';
|
||||
import { google, type gmail_v1 as gmailV1 } from 'googleapis';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service';
|
||||
@@ -42,7 +42,10 @@ export class GmailGetMessageListService {
|
||||
await this.oAuth2ClientManagerService.getGoogleOAuth2Client(
|
||||
connectedAccount,
|
||||
);
|
||||
const gmailClient = oAuth2Client.gmail({ version: 'v1' });
|
||||
const gmailClient = google.gmail({
|
||||
version: 'v1',
|
||||
auth: oAuth2Client,
|
||||
});
|
||||
|
||||
let pageToken: string | undefined;
|
||||
let hasMoreMessages = true;
|
||||
@@ -143,7 +146,10 @@ export class GmailGetMessageListService {
|
||||
await this.oAuth2ClientManagerService.getGoogleOAuth2Client(
|
||||
connectedAccount,
|
||||
);
|
||||
const gmailClient = oAuth2Client.gmail({ version: 'v1' });
|
||||
const gmailClient = google.gmail({
|
||||
version: 'v1',
|
||||
auth: oAuth2Client,
|
||||
});
|
||||
|
||||
if (!isNonEmptyString(messageChannel.syncCursor)) {
|
||||
return this.getMessageListWithoutCursor(connectedAccount, messageFolders);
|
||||
|
||||
+8
-6
@@ -1,5 +1,6 @@
|
||||
import { Test, type TestingModule } from '@nestjs/testing';
|
||||
|
||||
import { google } from 'googleapis';
|
||||
import { ConnectedAccountProvider } from 'twenty-shared/types';
|
||||
|
||||
import { OAuth2ClientManagerService } from 'src/modules/connected-account/oauth2-client-manager/services/oauth2-client-manager.service';
|
||||
@@ -27,7 +28,7 @@ describe('MessagingSendMessageService - Gmail HTML Support', () => {
|
||||
},
|
||||
getProfile: jest
|
||||
.fn()
|
||||
.mockResolvedValue({ data: { emailAdress: 'test@example.com' } }),
|
||||
.mockResolvedValue({ data: { emailAddress: 'test@example.com' } }),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -45,12 +46,12 @@ describe('MessagingSendMessageService - Gmail HTML Support', () => {
|
||||
},
|
||||
};
|
||||
|
||||
const mockOAuth2Client = {
|
||||
gmail: jest.fn().mockReturnValue(mockGmailClient),
|
||||
people: jest.fn().mockReturnValue(mockPeopleClient),
|
||||
};
|
||||
const mockOAuth2Client = {};
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.spyOn(google, 'gmail').mockReturnValue(mockGmailClient as never);
|
||||
jest.spyOn(google, 'people').mockReturnValue(mockPeopleClient as never);
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
MessagingSendMessageService,
|
||||
@@ -79,7 +80,8 @@ describe('MessagingSendMessageService - Gmail HTML Support', () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockSend.mockClear();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should send multipart/alternative email with both text and HTML parts via Gmail', async () => {
|
||||
|
||||
+5
-2
@@ -1,5 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { google } from 'googleapis';
|
||||
import MailComposer from 'nodemailer/lib/mail-composer';
|
||||
import { ConnectedAccountProvider } from 'twenty-shared/types';
|
||||
import { assertUnreachable, isDefined } from 'twenty-shared/utils';
|
||||
@@ -47,12 +48,14 @@ export class MessagingSendMessageService {
|
||||
connectedAccount,
|
||||
);
|
||||
|
||||
const gmailClient = oAuth2Client.gmail({
|
||||
const gmailClient = google.gmail({
|
||||
version: 'v1',
|
||||
auth: oAuth2Client,
|
||||
});
|
||||
|
||||
const peopleClient = oAuth2Client.people({
|
||||
const peopleClient = google.people({
|
||||
version: 'v1',
|
||||
auth: oAuth2Client,
|
||||
});
|
||||
|
||||
const { data: gmailData } = await gmailClient.users.getProfile({
|
||||
|
||||
+19
-102
@@ -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<WorkflowWorkspaceEntity> {
|
||||
const workflowRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkflowWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workflow',
|
||||
{ shouldBypassPermissionChecks: true },
|
||||
);
|
||||
|
||||
const workflow = await workflowRepository.findOne({
|
||||
where: { id: workflowId },
|
||||
});
|
||||
|
||||
if (!workflow) {
|
||||
throw new Error(`Workflow ${workflowId} not found`);
|
||||
}
|
||||
|
||||
return workflow;
|
||||
}
|
||||
}
|
||||
|
||||
-1
@@ -6,5 +6,4 @@ export type WorkflowExecutionContext = {
|
||||
isActingOnBehalfOfUser: boolean;
|
||||
initiator: ActorMetadata;
|
||||
rolePermissionConfig: RolePermissionConfig;
|
||||
userWorkspaceId?: string;
|
||||
};
|
||||
|
||||
-1
@@ -86,7 +86,6 @@ export class AiAgentWorkflowAction implements WorkflowAction {
|
||||
? executionContext.initiator
|
||||
: undefined,
|
||||
rolePermissionConfig: executionContext.rolePermissionConfig,
|
||||
userWorkspaceId: executionContext.userWorkspaceId,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
-5
@@ -38,7 +38,6 @@ export class AiAgentExecutorService {
|
||||
workspaceId: string,
|
||||
actorContext?: ActorMetadata,
|
||||
rolePermissionConfig?: RolePermissionConfig,
|
||||
userWorkspaceId?: string,
|
||||
): Promise<ToolSet> {
|
||||
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<AgentExecutionResult> {
|
||||
try {
|
||||
const registeredModel =
|
||||
@@ -111,7 +107,6 @@ export class AiAgentExecutorService {
|
||||
agent.workspaceId,
|
||||
actorContext,
|
||||
rolePermissionConfig,
|
||||
userWorkspaceId,
|
||||
)
|
||||
: {};
|
||||
|
||||
|
||||
-1
@@ -62,7 +62,6 @@ export class CreateRecordWorkflowAction implements WorkflowAction {
|
||||
workspaceId,
|
||||
createdBy,
|
||||
rolePermissionConfig: executionContext.rolePermissionConfig,
|
||||
userWorkspaceId: executionContext.userWorkspaceId,
|
||||
});
|
||||
|
||||
if (!toolOutput.success) {
|
||||
|
||||
-2
@@ -80,8 +80,6 @@ export class DeleteRecordWorkflowAction implements WorkflowAction {
|
||||
objectRecordId: workflowActionInput.objectRecordId,
|
||||
workspaceId,
|
||||
rolePermissionConfig: executionContext.rolePermissionConfig,
|
||||
userWorkspaceId: executionContext.userWorkspaceId,
|
||||
createdBy: executionContext.initiator,
|
||||
soft: true,
|
||||
});
|
||||
|
||||
|
||||
+7
-6
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
-2
@@ -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) {
|
||||
|
||||
-2
@@ -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) {
|
||||
|
||||
+2
-2
@@ -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]),
|
||||
],
|
||||
})
|
||||
|
||||
+5
-3
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
-91
@@ -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<string, any> = {},
|
||||
) => ({
|
||||
id,
|
||||
name: `Test Record ${id}`,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
...data,
|
||||
});
|
||||
|
||||
export const createTestRecords = (
|
||||
count: number,
|
||||
baseData: Record<string, any> = {},
|
||||
) => {
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user