Rework types for logic function (#18074)

## Summary

- **Consolidate logic function services**: Remove
`LogicFunctionMetadataService` and consolidate all logic function CRUD
operations into `LogicFunctionFromSourceService`, with a new
`LogicFunctionFromSourceHelperService` for shared validation/migration
logic
- **Introduce typed conversion utils following the skill pattern**: Add
`fromCreateLogicFunctionFromSourceInputToUniversalFlatLogicFunctionToCreate`
and `fromUpdateLogicFunctionFromSourceInputToFlatLogicFunctionToUpdate`
that convert DTO inputs directly to flat entities
(`UniversalFlatLogicFunction` / `FlatLogicFunction`), replacing the
previous intermediate `UpdateLogicFunctionMetadataParams` indirection
- **Simplify `CodeStepBuildService`**: Remove ~100 lines of manual
duplication logic by delegating to
`LogicFunctionFromSourceService.duplicateOneWithSource`
- **Remove completed 1-17 migration**: Delete
`MigrateWorkflowCodeStepsCommand` and associated utils that migrated
workflow code steps from serverless functions to logic functions
This commit is contained in:
Charles Bochet
2026-02-19 17:25:08 +01:00
committed by GitHub
parent 0e25aeb5be
commit 10bd005021
22 changed files with 654 additions and 1343 deletions
@@ -4570,13 +4570,11 @@ export type UpdateLogicFunctionFromSourceInput = {
};
export type UpdateLogicFunctionFromSourceInputUpdates = {
checksum?: InputMaybe<Scalars['String']>;
cronTriggerSettings?: InputMaybe<Scalars['JSON']>;
databaseEventTriggerSettings?: InputMaybe<Scalars['JSON']>;
description?: InputMaybe<Scalars['String']>;
handlerName?: InputMaybe<Scalars['String']>;
httpRouteTriggerSettings?: InputMaybe<Scalars['JSON']>;
isBuildUpToDate?: InputMaybe<Scalars['Boolean']>;
isTool?: InputMaybe<Scalars['Boolean']>;
name?: InputMaybe<Scalars['String']>;
sourceHandlerCode?: InputMaybe<Scalars['String']>;
@@ -1,301 +0,0 @@
import { Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import crypto from 'crypto';
import { Command } from 'nest-commander';
import { FileFolder, type Sources } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { In, Repository } from 'typeorm';
import { v4 } from 'uuid';
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
import {
type CodeStepMigrationTarget,
collectCodeStepMigrationTargets,
migrateWorkflowCodeStepsWithMapping,
type ServerlessToLogicFunctionMapping,
} from 'src/database/commands/upgrade-version-command/1-17/utils/migrate-workflow-code-step.util';
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { LogicFunctionEntity } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
import { LogicFunctionMetadataService } from 'src/engine/metadata-modules/logic-function/services/logic-function-metadata.service';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { WorkflowVersionStatus } from 'src/modules/workflow/common/standard-objects/workflow-version.workspace-entity';
const OLD_BUILT_FOLDER = 'built-function';
const OLD_SOURCE_FOLDER = 'serverless-function';
const NEW_WORKFLOW_RESOURCE_PREFIX = 'workflow';
@Command({
name: 'upgrade:1-17:migrate-workflow-code-steps',
description:
'Migrate workflow code steps from v1.16 (serverless) to v1.17 (logic function): create new logic function per (serverlessFunctionId, version), move files, update steps, delete old logic function.',
})
export class MigrateWorkflowCodeStepsCommand extends ActiveOrSuspendedWorkspacesMigrationCommandRunner {
protected readonly logger = new Logger(MigrateWorkflowCodeStepsCommand.name);
constructor(
@InjectRepository(WorkspaceEntity)
protected readonly workspaceRepository: Repository<WorkspaceEntity>,
@InjectRepository(LogicFunctionEntity)
private readonly logicFunctionRepository: Repository<LogicFunctionEntity>,
protected readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
protected readonly dataSourceService: DataSourceService,
private readonly fileStorageService: FileStorageService,
private readonly applicationService: ApplicationService,
private readonly logicFunctionMetadataService: LogicFunctionMetadataService,
) {
super(workspaceRepository, globalWorkspaceOrmManager, dataSourceService);
}
override async runOnWorkspace({
workspaceId,
options,
}: RunOnWorkspaceArgs): Promise<void> {
const isDryRun = options.dryRun ?? false;
this.logger.log(
`Running MigrateWorkflowCodeStepsCommand for workspace ${workspaceId}`,
);
const workflowVersionRepository =
await this.globalWorkspaceOrmManager.getRepository(
workspaceId,
'workflowVersion',
{ shouldBypassPermissionChecks: true },
);
const workflowVersions = await workflowVersionRepository.find({
select: ['id', 'steps', 'status'],
where: {
status: In([WorkflowVersionStatus.DRAFT, WorkflowVersionStatus.ACTIVE]),
},
});
const allTargets = new Map<string, CodeStepMigrationTarget>();
for (const version of workflowVersions) {
const targets = collectCodeStepMigrationTargets(version.steps);
for (const target of targets) {
const key = `${target.serverlessFunctionId}:${target.serverlessFunctionVersion}`;
if (!allTargets.has(key)) {
allTargets.set(key, target);
}
}
}
const targetsList = Array.from(allTargets.values());
if (targetsList.length === 0) {
this.logger.log(`No code steps to migrate in workspace ${workspaceId}`);
return;
}
const mapping = new Map<string, string>();
if (!isDryRun) {
for (const target of targetsList) {
const newLogicFunctionId =
await this.createLogicFunctionAndMigrateFiles(workspaceId, target);
if (isDefined(newLogicFunctionId)) {
const key = `${target.serverlessFunctionId}:${target.serverlessFunctionVersion}`;
mapping.set(key, newLogicFunctionId);
}
}
const serverlessToLogicMapping: ServerlessToLogicFunctionMapping = (
serverlessFunctionId,
serverlessFunctionVersion,
) => {
const key = `${serverlessFunctionId}:${serverlessFunctionVersion}`;
const newId = mapping.get(key);
if (!isDefined(newId)) {
throw new Error(
`Missing mapping for ${serverlessFunctionId}:${serverlessFunctionVersion}`,
);
}
return newId;
};
for (const version of workflowVersions) {
const { migratedSteps, hasChanges } =
migrateWorkflowCodeStepsWithMapping(
version.steps,
serverlessToLogicMapping,
);
if (!hasChanges) {
continue;
}
await workflowVersionRepository.update(
{ id: version.id },
{ steps: migratedSteps },
);
this.logger.log(
`Migrated workflow version ${version.id} in workspace ${workspaceId}`,
);
}
} else {
this.logger.log(
`[DRY RUN] Would create ${targetsList.length} new logic function(s), migrate files, update workflow steps, and delete ${new Set(targetsList.map((t) => t.serverlessFunctionId)).size} old logic function(s) in workspace ${workspaceId}`,
);
}
}
private async getApplicationUniversalIdentifier(
applicationId: string,
): Promise<string | null> {
const application = await this.applicationService.findById(applicationId);
return application?.universalIdentifier ?? null;
}
private async createLogicFunctionAndMigrateFiles(
workspaceId: string,
target: CodeStepMigrationTarget,
): Promise<string | null> {
const { serverlessFunctionId, serverlessFunctionVersion } = target;
const version = serverlessFunctionVersion ?? 'draft';
const oldLogicFunction = await this.logicFunctionRepository.findOne({
where: { id: serverlessFunctionId, workspaceId },
});
if (!isDefined(oldLogicFunction)) {
this.logger.warn(
`Logic function ${serverlessFunctionId} not found in workspace ${workspaceId}, skipping`,
);
return null;
}
const newLogicFunctionId = v4();
const applicationUniversalIdentifier =
await this.getApplicationUniversalIdentifier(
oldLogicFunction.applicationId,
);
const { builtContent, sourceContent } = await this.readOldFunctionFiles(
workspaceId,
serverlessFunctionId,
version,
);
const checksum = crypto
.createHash('md5')
.update(builtContent)
.digest('hex');
if (isDefined(applicationUniversalIdentifier)) {
await this.uploadFunctionFiles(
workspaceId,
applicationUniversalIdentifier,
newLogicFunctionId,
{ builtContent, sourceContent },
);
}
await this.logicFunctionMetadataService.createOne({
input: {
id: newLogicFunctionId,
name: oldLogicFunction.name,
description: oldLogicFunction.description ?? undefined,
timeoutSeconds: oldLogicFunction.timeoutSeconds ?? 300,
toolInputSchema: oldLogicFunction.toolInputSchema ?? {},
isTool: oldLogicFunction.isTool ?? false,
handlerName: oldLogicFunction.handlerName,
sourceHandlerPath: `${NEW_WORKFLOW_RESOURCE_PREFIX}/${newLogicFunctionId}/src/index.ts`,
builtHandlerPath: `${NEW_WORKFLOW_RESOURCE_PREFIX}/${newLogicFunctionId}/src/index.mjs`,
checksum,
isBuildUpToDate: true,
},
workspaceId,
ownerFlatApplication: oldLogicFunction.application,
});
this.logger.log(
`Created logic function ${newLogicFunctionId} (from ${serverlessFunctionId}/${version}) and migrated files in workspace ${workspaceId}`,
);
return newLogicFunctionId;
}
private async readOldFunctionFiles(
workspaceId: string,
serverlessFunctionId: string,
version: string,
): Promise<{ builtContent: string; sourceContent: string }> {
const workspacePrefix = `workspace-${workspaceId}`;
const builtSources = await this.fileStorageService.readFolderLegacy(
`${workspacePrefix}/${OLD_BUILT_FOLDER}/${serverlessFunctionId}/${version}`,
);
const sourceSources = await this.fileStorageService.readFolderLegacy(
`${workspacePrefix}/${OLD_SOURCE_FOLDER}/${serverlessFunctionId}/${version}`,
);
// Old source layout may nest files under a `src/` key
const sourceRoot =
(sourceSources.src as Sources) ?? (sourceSources as Sources);
const builtContent = builtSources['index.mjs'] as string;
const sourceContent = sourceRoot['index.ts'] as string;
if (!isDefined(builtContent) || !isDefined(sourceContent)) {
throw new Error(
`Missing index.mjs or index.ts for serverless function ${serverlessFunctionId}/${version} in workspace ${workspaceId}`,
);
}
return { builtContent, sourceContent };
}
private async uploadFunctionFiles(
workspaceId: string,
applicationUniversalIdentifier: string,
newLogicFunctionId: string,
files: { builtContent: string; sourceContent: string },
): Promise<void> {
const resourcePath = `${NEW_WORKFLOW_RESOURCE_PREFIX}/${newLogicFunctionId}/src`;
await this.fileStorageService.writeFile({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.BuiltLogicFunction,
resourcePath: `${resourcePath}/index.mjs`,
sourceFile: Buffer.from(files.builtContent),
mimeType: 'application/javascript',
settings: {
isTemporaryFile: false,
toDelete: false,
},
});
await this.fileStorageService.writeFile({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Source,
resourcePath: `${resourcePath}/index.ts`,
sourceFile: Buffer.from(files.sourceContent),
mimeType: 'application/typescript',
settings: {
isTemporaryFile: false,
toDelete: false,
},
});
}
}
@@ -11,7 +11,6 @@ import { MigrateDateTimeIsFilterValuesCommand } from 'src/database/commands/upgr
import { MigrateNoteTargetToMorphRelationsCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-migrate-note-target-to-morph-relations.command';
import { MigrateSendEmailRecipientsCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-migrate-send-email-recipients.command';
import { MigrateTaskTargetToMorphRelationsCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-migrate-task-target-to-morph-relations.command';
import { MigrateWorkflowCodeStepsCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-migrate-workflow-code-steps.command';
import { SeedWorkflowV1_16Command } from 'src/database/commands/upgrade-version-command/1-17/1-17-seed-workflow-v1-16.command';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
@@ -25,7 +24,6 @@ import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-s
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module';
import { LogicFunctionEntity } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
import { LogicFunctionModule } from 'src/engine/metadata-modules/logic-function/logic-function.module';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
import { ViewFilterEntity } from 'src/engine/metadata-modules/view-filter/entities/view-filter.entity';
@@ -65,7 +63,6 @@ import { TaskTargetWorkspaceEntity } from 'src/modules/task/standard-objects/tas
ApplicationModule,
UserWorkspaceModule,
WorkspaceMigrationModule,
LogicFunctionModule,
RecordPositionModule,
GlobalWorkspaceDataSourceModule,
],
@@ -79,7 +76,6 @@ import { TaskTargetWorkspaceEntity } from 'src/modules/task/standard-objects/tas
DeleteFileRecordsAndUpdateTableCommand,
MigrateSendEmailRecipientsCommand,
MigrateDateTimeIsFilterValuesCommand,
MigrateWorkflowCodeStepsCommand,
SeedWorkflowV1_16Command,
BackfillApplicationPackageFilesCommand,
],
@@ -93,7 +89,6 @@ import { TaskTargetWorkspaceEntity } from 'src/modules/task/standard-objects/tas
MigrateSendEmailRecipientsCommand,
MigrateDateTimeIsFilterValuesCommand,
DeleteFileRecordsAndUpdateTableCommand,
MigrateWorkflowCodeStepsCommand,
SeedWorkflowV1_16Command,
BackfillApplicationPackageFilesCommand,
],
@@ -1,125 +0,0 @@
import { isDefined } from 'twenty-shared/utils';
import { WorkflowActionType } from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
type LegacyCodeStepInput = {
serverlessFunctionId: string;
serverlessFunctionVersion?: string;
serverlessFunctionInput?: Record<string, unknown>;
};
type MigratedCodeStepInput = {
logicFunctionId: string;
logicFunctionInput?: Record<string, unknown>;
};
type WorkflowStep = {
id: string;
type: string;
settings: {
input: LegacyCodeStepInput | MigratedCodeStepInput;
outputSchema?: unknown;
};
};
export const needsCodeStepMigration = (
input: LegacyCodeStepInput | MigratedCodeStepInput,
): input is LegacyCodeStepInput => {
return (
'serverlessFunctionId' in input && isDefined(input.serverlessFunctionId)
);
};
export type CodeStepMigrationTarget = {
serverlessFunctionId: string;
serverlessFunctionVersion: string;
};
export type ServerlessToLogicFunctionMapping = (
serverlessFunctionId: string,
serverlessFunctionVersion: string,
) => string;
export const collectCodeStepMigrationTargets = (
steps: unknown,
): CodeStepMigrationTarget[] => {
if (!isDefined(steps) || !Array.isArray(steps) || steps.length === 0) {
return [];
}
const typedSteps = steps as WorkflowStep[];
const seen = new Set<string>();
const targets: CodeStepMigrationTarget[] = [];
for (const step of typedSteps) {
if (step.type !== WorkflowActionType.CODE) {
continue;
}
const input = step.settings.input as
| LegacyCodeStepInput
| MigratedCodeStepInput;
if (!needsCodeStepMigration(input)) {
continue;
}
const version = input.serverlessFunctionVersion ?? 'draft';
const key = `${input.serverlessFunctionId}:${version}`;
if (seen.has(key)) {
continue;
}
seen.add(key);
targets.push({
serverlessFunctionId: input.serverlessFunctionId,
serverlessFunctionVersion: version,
});
}
return targets;
};
export const migrateWorkflowCodeStepsWithMapping = (
steps: unknown,
mapping: ServerlessToLogicFunctionMapping,
): { migratedSteps: WorkflowStep[]; hasChanges: boolean } => {
if (!isDefined(steps) || !Array.isArray(steps) || steps.length === 0) {
return { migratedSteps: [], hasChanges: false };
}
const typedSteps = steps as WorkflowStep[];
let hasChanges = false;
const migratedSteps = typedSteps.map((step) => {
if (step.type !== WorkflowActionType.CODE) {
return step;
}
const input = step.settings.input as
| LegacyCodeStepInput
| MigratedCodeStepInput;
if (!needsCodeStepMigration(input)) {
return step;
}
hasChanges = true;
const version = input.serverlessFunctionVersion ?? 'draft';
const newLogicFunctionId = mapping(input.serverlessFunctionId, version);
return {
...step,
settings: {
...step.settings,
input: {
logicFunctionId: newLogicFunctionId,
logicFunctionInput: input.serverlessFunctionInput ?? {},
} as MigratedCodeStepInput,
},
};
});
return { migratedSteps, hasChanges };
};
@@ -17,7 +17,6 @@ import { MakeWebhookUniversalIdentifierAndApplicationIdNotNullableMigrationComma
import { MigrateAttachmentToMorphRelationsCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-migrate-attachment-to-morph-relations.command';
import { MigrateNoteTargetToMorphRelationsCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-migrate-note-target-to-morph-relations.command';
import { MigrateTaskTargetToMorphRelationsCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-migrate-task-target-to-morph-relations.command';
import { MigrateWorkflowCodeStepsCommand } from 'src/database/commands/upgrade-version-command/1-17/1-17-migrate-workflow-code-steps.command';
import { BackfillFileSizeAndMimeTypeCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-backfill-file-size-and-mime-type.command';
import { BackfillMessageChannelThrottleRetryAfterCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-backfill-message-channel-throttle-retry-after.command';
import { BackfillStandardViewsAndFieldMetadataCommand } from 'src/database/commands/upgrade-version-command/1-18/1-18-backfill-standard-views-and-field-metadata.command';
@@ -57,7 +56,6 @@ export class UpgradeCommand extends UpgradeCommandRunner {
protected readonly migrateTaskTargetToMorphRelationsCommand: MigrateTaskTargetToMorphRelationsCommand,
protected readonly identifyWebhookMetadataCommand: IdentifyWebhookMetadataCommand,
protected readonly makeWebhookUniversalIdentifierAndApplicationIdNotNullableMigrationCommand: MakeWebhookUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
protected readonly migrateWorkflowCodeStepsCommand: MigrateWorkflowCodeStepsCommand,
protected readonly fixMorphRelationFieldNamesCommand: FixMorphRelationFieldNamesCommand,
// 1.18 Commands
@@ -94,7 +92,6 @@ export class UpgradeCommand extends UpgradeCommandRunner {
this
.makeWebhookUniversalIdentifierAndApplicationIdNotNullableMigrationCommand,
this.deleteFileRecordsAndUpdateTableCommand,
this.migrateWorkflowCodeStepsCommand,
this.backfillApplicationPackageFilesCommand,
this.fixMorphRelationFieldNamesCommand,
];
@@ -1,103 +0,0 @@
import { Field, InputType } from '@nestjs/graphql';
import {
IsBoolean,
IsNotEmpty,
IsNumber,
IsObject,
IsOptional,
IsString,
IsUUID,
Matches,
Max,
Min,
} from 'class-validator';
import graphqlTypeJson from 'graphql-type-json';
import {
CronTriggerSettings,
DatabaseEventTriggerSettings,
HttpRouteTriggerSettings,
} from 'twenty-shared/application';
import type { InputJsonSchema } from 'twenty-shared/logic-function';
import type { JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@InputType()
export class CreateLogicFunction {
@IsUUID()
@IsOptional()
@Field(() => UUIDScalarType, { nullable: true })
id?: string;
@IsUUID()
@IsOptional()
@Field(() => UUIDScalarType, { nullable: true })
universalIdentifier?: string;
@IsString()
@IsNotEmpty()
@Field()
name: string;
@IsString()
@IsOptional()
@Field({ nullable: true })
description?: string;
@IsNumber()
@Field({ nullable: true })
@Min(1)
@Max(900)
@IsOptional()
timeoutSeconds?: number;
@Field(() => graphqlTypeJson, { nullable: false })
@IsObject()
toolInputSchema: InputJsonSchema;
@IsBoolean()
@Field({ nullable: true })
@IsOptional()
isTool?: boolean;
@IsBoolean()
@Field({ nullable: false })
isBuildUpToDate: boolean;
@IsString()
@Field({ nullable: true })
@IsOptional()
checksum?: string;
@IsString()
@Matches(/^[a-zA-Z_$][a-zA-Z0-9_$]*$/, {
message: 'handlerName must be a valid JavaScript identifier',
})
@Field({ nullable: false })
handlerName: string;
@IsString()
@Field({ nullable: false })
sourceHandlerPath: string;
@IsString()
@Field({ nullable: false })
builtHandlerPath: string;
@IsObject()
@Field(() => graphqlTypeJson, { nullable: true })
@IsOptional()
cronTriggerSettings?: JsonbProperty<CronTriggerSettings>;
@IsObject()
@Field(() => graphqlTypeJson, { nullable: true })
@IsOptional()
databaseEventTriggerSettings?: JsonbProperty<DatabaseEventTriggerSettings>;
@IsObject()
@Field(() => graphqlTypeJson, { nullable: true })
@IsOptional()
httpRouteTriggerSettings?: JsonbProperty<HttpRouteTriggerSettings>;
}
@@ -71,16 +71,6 @@ class UpdateLogicFunctionFromSourceInputUpdates {
@IsOptional()
isTool?: boolean;
@IsBoolean()
@Field({ nullable: true })
@IsOptional()
isBuildUpToDate?: boolean;
@IsString()
@Field({ nullable: true })
@IsOptional()
checksum?: string;
@IsObject()
@Field(() => graphqlTypeJson, { nullable: true })
@IsOptional()
@@ -17,7 +17,7 @@ import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata
import { LogicFunctionLayerModule } from 'src/engine/metadata-modules/logic-function-layer/logic-function-layer.module';
import { LogicFunctionEntity } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
import { LogicFunctionResolver } from 'src/engine/metadata-modules/logic-function/logic-function.resolver';
import { LogicFunctionMetadataService } from 'src/engine/metadata-modules/logic-function/services/logic-function-metadata.service';
import { LogicFunctionFromSourceHelperService } from 'src/engine/metadata-modules/logic-function/services/logic-function-from-source-helper.service';
import { LogicFunctionFromSourceService } from 'src/engine/metadata-modules/logic-function/services/logic-function-from-source.service';
import { WorkspaceFlatLogicFunctionMapCacheService } from 'src/engine/metadata-modules/logic-function/services/workspace-flat-logic-function-map-cache.service';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
@@ -43,11 +43,11 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
SecretEncryptionModule,
],
providers: [
LogicFunctionMetadataService,
LogicFunctionFromSourceHelperService,
LogicFunctionFromSourceService,
LogicFunctionResolver,
WorkspaceFlatLogicFunctionMapCacheService,
],
exports: [LogicFunctionMetadataService, LogicFunctionFromSourceService],
exports: [LogicFunctionFromSourceService],
})
export class LogicFunctionModule {}
@@ -14,19 +14,22 @@ import { FeatureFlagGuard } from 'src/engine/guards/feature-flag.guard';
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { CreateLogicFunctionFromSourceInput } from 'src/engine/metadata-modules/logic-function/dtos/create-logic-function-from-source.input';
import { ExecuteOneLogicFunctionInput } from 'src/engine/metadata-modules/logic-function/dtos/execute-logic-function.input';
import { LogicFunctionExecutionResultDTO } from 'src/engine/metadata-modules/logic-function/dtos/logic-function-execution-result.dto';
import { LogicFunctionIdInput } from 'src/engine/metadata-modules/logic-function/dtos/logic-function-id.input';
import { LogicFunctionLogsDTO } from 'src/engine/metadata-modules/logic-function/dtos/logic-function-logs.dto';
import { LogicFunctionLogsInput } from 'src/engine/metadata-modules/logic-function/dtos/logic-function-logs.input';
import { LogicFunctionDTO } from 'src/engine/metadata-modules/logic-function/dtos/logic-function.dto';
import { UpdateLogicFunctionFromSourceInput } from 'src/engine/metadata-modules/logic-function/dtos/update-logic-function-from-source.input';
import { LogicFunctionFromSourceService } from 'src/engine/metadata-modules/logic-function/services/logic-function-from-source.service';
import { type FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
import { findFlatLogicFunctionOrThrow } from 'src/engine/metadata-modules/logic-function/utils/find-flat-logic-function-or-throw.util';
import { fromFlatLogicFunctionToLogicFunctionDto } from 'src/engine/metadata-modules/logic-function/utils/from-flat-logic-function-to-logic-function-dto.util';
import { logicFunctionGraphQLApiExceptionHandler } from 'src/engine/metadata-modules/logic-function/utils/logic-function-graphql-api-exception-handler.utils';
import { SubscriptionChannel } from 'src/engine/subscriptions/enums/subscription-channel.enum';
import { SubscriptionService } from 'src/engine/subscriptions/subscription.service';
import { CreateLogicFunctionFromSourceInput } from 'src/engine/metadata-modules/logic-function/dtos/create-logic-function-from-source.input';
import { UpdateLogicFunctionFromSourceInput } from 'src/engine/metadata-modules/logic-function/dtos/update-logic-function-from-source.input';
import { LogicFunctionMetadataService } from 'src/engine/metadata-modules/logic-function/services/logic-function-metadata.service';
@UseGuards(WorkspaceAuthGuard, FeatureFlagGuard, NoPermissionGuard)
@MetadataResolver()
@@ -35,7 +38,7 @@ import { LogicFunctionMetadataService } from 'src/engine/metadata-modules/logic-
export class LogicFunctionResolver {
constructor(
private readonly logicFunctionFromSourceService: LogicFunctionFromSourceService,
private readonly logicFunctionMetadataService: LogicFunctionMetadataService,
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly subscriptionService: SubscriptionService,
) {}
@@ -45,10 +48,20 @@ export class LogicFunctionResolver {
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<LogicFunctionDTO> {
try {
return await this.logicFunctionMetadataService.findOne({
const { flatLogicFunctionMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatLogicFunctionMaps'],
},
);
const flatLogicFunction = findFlatLogicFunctionOrThrow({
id,
workspaceId,
flatLogicFunctionMaps,
});
return fromFlatLogicFunctionToLogicFunctionDto({ flatLogicFunction });
} catch (error) {
return logicFunctionGraphQLApiExceptionHandler(error);
}
@@ -59,9 +72,23 @@ export class LogicFunctionResolver {
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<LogicFunctionDTO[]> {
try {
return await this.logicFunctionMetadataService.findMany({
workspaceId,
});
const { flatLogicFunctionMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatLogicFunctionMaps'],
},
);
return Object.values(flatLogicFunctionMaps.byUniversalIdentifier)
.filter(
(flatLogicFunction): flatLogicFunction is FlatLogicFunction =>
isDefined(flatLogicFunction) &&
!isDefined(flatLogicFunction.deletedAt),
)
.map((flatLogicFunction) =>
fromFlatLogicFunctionToLogicFunctionDto({ flatLogicFunction }),
);
} catch (error) {
return logicFunctionGraphQLApiExceptionHandler(error);
}
@@ -74,10 +101,33 @@ export class LogicFunctionResolver {
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
) {
try {
return await this.logicFunctionMetadataService.getAvailablePackages({
logicFunctionId: id,
workspaceId,
});
const { flatLogicFunctionMaps, flatApplicationMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatLogicFunctionMaps', 'flatApplicationMaps'],
},
);
const logicFunctionUniversalIdentifier =
flatLogicFunctionMaps.universalIdentifierById[id];
if (!logicFunctionUniversalIdentifier) {
return {};
}
const logicFunction =
flatLogicFunctionMaps.byUniversalIdentifier[
logicFunctionUniversalIdentifier
];
if (!logicFunction) {
return {};
}
const application = flatApplicationMaps.byId[logicFunction.applicationId];
return application?.availablePackages ?? {};
} catch (error) {
return logicFunctionGraphQLApiExceptionHandler(error);
}
@@ -90,7 +140,7 @@ export class LogicFunctionResolver {
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<LogicFunctionDTO> {
try {
return await this.logicFunctionFromSourceService.deleteOne({
return await this.logicFunctionFromSourceService.deleteOneWithSource({
id,
workspaceId,
});
@@ -106,7 +156,7 @@ export class LogicFunctionResolver {
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<LogicFunctionDTO> {
try {
return await this.logicFunctionFromSourceService.createOne({
return await this.logicFunctionFromSourceService.createOneFromSource({
input,
workspaceId,
});
@@ -122,7 +172,7 @@ export class LogicFunctionResolver {
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<LogicFunctionExecutionResultDTO> {
try {
return await this.logicFunctionFromSourceService.executeOne({
return await this.logicFunctionFromSourceService.executeOneFromSource({
id,
payload,
workspaceId,
@@ -151,13 +201,13 @@ export class LogicFunctionResolver {
@Mutation(() => Boolean)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.WORKFLOWS))
async updateOneLogicFunction(
@Args('input') { id, update }: UpdateLogicFunctionFromSourceInput,
@Args('input')
updateLogicFunctionFromSourceInput: UpdateLogicFunctionFromSourceInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<boolean> {
try {
await this.logicFunctionFromSourceService.updateOne({
id,
update,
await this.logicFunctionFromSourceService.updateOneFromSource({
updateLogicFunctionFromSourceInput,
workspaceId,
});
@@ -0,0 +1,153 @@
import { Injectable } from '@nestjs/common';
import { join } from 'path';
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import {
DEFAULT_BUILT_HANDLER_PATH,
DEFAULT_SOURCE_HANDLER_PATH,
} from 'src/engine/metadata-modules/logic-function/constants/handler.contant';
import { type FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
import { findFlatLogicFunctionOrThrow } from 'src/engine/metadata-modules/logic-function/utils/find-flat-logic-function-or-throw.util';
import { getLogicFunctionSubfolderForFromSource } from 'src/engine/metadata-modules/logic-function/utils/get-logic-function-subfolder-for-from-source';
import { type UniversalFlatLogicFunction } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-logic-function.type';
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
@Injectable()
export class LogicFunctionFromSourceHelperService {
constructor(
private readonly applicationService: ApplicationService,
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
) {}
async findLogicFunctionAndApplicationOrThrow({
id,
workspaceId,
}: {
id: string;
workspaceId: string;
}) {
const [{ flatLogicFunctionMaps }, { workspaceCustomFlatApplication }] =
await Promise.all([
this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps({
workspaceId,
flatMapsKeys: ['flatLogicFunctionMaps'],
}),
this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
),
]);
const flatLogicFunction = findFlatLogicFunctionOrThrow({
id,
flatLogicFunctionMaps,
});
return {
flatLogicFunction,
ownerFlatApplication: workspaceCustomFlatApplication,
};
}
buildHandlerPaths(logicFunctionId: string) {
const logicFunctionSubfolder =
getLogicFunctionSubfolderForFromSource(logicFunctionId);
return {
sourceHandlerPath: join(
logicFunctionSubfolder,
DEFAULT_SOURCE_HANDLER_PATH,
),
builtHandlerPath: join(
logicFunctionSubfolder,
DEFAULT_BUILT_HANDLER_PATH,
),
};
}
async createOneFromMetadata({
universalFlatLogicFunctionToCreate,
workspaceId,
}: {
universalFlatLogicFunctionToCreate: UniversalFlatLogicFunction & {
id: string;
};
workspaceId: string;
}): Promise<UniversalFlatLogicFunction & { id: string }> {
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
logicFunction: {
flatEntityToCreate: [universalFlatLogicFunctionToCreate],
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
},
workspaceId,
isSystemBuild: false,
applicationUniversalIdentifier:
universalFlatLogicFunctionToCreate.applicationUniversalIdentifier,
},
);
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while creating logic function',
);
}
return universalFlatLogicFunctionToCreate;
}
async updateOneFromMetadata({
flatLogicFunctionToUpdate,
workspaceId,
applicationUniversalIdentifier,
}: {
flatLogicFunctionToUpdate: FlatLogicFunction;
workspaceId: string;
applicationUniversalIdentifier: string;
}): Promise<FlatLogicFunction> {
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
logicFunction: {
flatEntityToCreate: [],
flatEntityToDelete: [],
flatEntityToUpdate: [flatLogicFunctionToUpdate],
},
},
workspaceId,
isSystemBuild: false,
applicationUniversalIdentifier,
},
);
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while updating logic function',
);
}
const { flatLogicFunctionMaps: recomputedFlatLogicFunctionMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatLogicFunctionMaps'],
},
);
return findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: flatLogicFunctionToUpdate.id,
flatEntityMaps: recomputedFlatLogicFunctionMaps,
});
}
}
@@ -1,77 +1,50 @@
import { Injectable } from '@nestjs/common';
import { join } from 'path';
import { v4 } from 'uuid';
import { isDefined } from 'twenty-shared/utils';
import { SEED_LOGIC_FUNCTION_INPUT_SCHEMA } from 'twenty-shared/logic-function';
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { LogicFunctionExecutorService } from 'src/engine/core-modules/logic-function/logic-function-executor/logic-function-executor.service';
import { LogicFunctionResourceService } from 'src/engine/core-modules/logic-function/logic-function-resource/logic-function-resource.service';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { CreateLogicFunctionFromSourceInput } from 'src/engine/metadata-modules/logic-function/dtos/create-logic-function-from-source.input';
import { LogicFunctionExecutionResultDTO } from 'src/engine/metadata-modules/logic-function/dtos/logic-function-execution-result.dto';
import { LogicFunctionDTO } from 'src/engine/metadata-modules/logic-function/dtos/logic-function.dto';
import { LogicFunctionMetadataService } from 'src/engine/metadata-modules/logic-function/services/logic-function-metadata.service';
import { findFlatLogicFunctionOrThrow } from 'src/engine/metadata-modules/logic-function/utils/find-flat-logic-function-or-throw.util';
import { fromFlatLogicFunctionToLogicFunctionDto } from 'src/engine/metadata-modules/logic-function/utils/from-flat-logic-function-to-logic-function-dto.util';
import { CreateLogicFunctionFromSourceInput } from 'src/engine/metadata-modules/logic-function/dtos/create-logic-function-from-source.input';
import { UpdateLogicFunctionFromSourceInput } from 'src/engine/metadata-modules/logic-function/dtos/update-logic-function-from-source.input';
import { getLogicFunctionSubfolderForFromSource } from 'src/engine/metadata-modules/logic-function/utils/get-logic-function-subfolder-for-from-source';
import {
DEFAULT_BUILT_HANDLER_PATH,
DEFAULT_SOURCE_HANDLER_PATH,
} from 'src/engine/metadata-modules/logic-function/constants/handler.contant';
LogicFunctionException,
LogicFunctionExceptionCode,
} from 'src/engine/metadata-modules/logic-function/logic-function.exception';
import { LogicFunctionFromSourceHelperService } from 'src/engine/metadata-modules/logic-function/services/logic-function-from-source-helper.service';
import { type UpdateLogicFunctionFromSourceInput } from 'src/engine/metadata-modules/logic-function/dtos/update-logic-function-from-source.input';
import { buildUniversalFlatLogicFunctionToCreate } from 'src/engine/metadata-modules/logic-function/utils/build-universal-flat-logic-function-to-create.util';
import { fromCreateLogicFunctionFromSourceInputToUniversalFlatLogicFunctionToCreate } from 'src/engine/metadata-modules/logic-function/utils/from-create-logic-function-from-source-input-to-universal-flat-logic-function-to-create.util';
import { fromFlatLogicFunctionToLogicFunctionDto } from 'src/engine/metadata-modules/logic-function/utils/from-flat-logic-function-to-logic-function-dto.util';
import { fromUpdateLogicFunctionFromSourceInputToFlatLogicFunctionToUpdate } from 'src/engine/metadata-modules/logic-function/utils/from-update-logic-function-from-source-input-to-flat-logic-function-to-update.util';
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
@Injectable()
export class LogicFunctionFromSourceService {
constructor(
private readonly logicFunctionExecutorService: LogicFunctionExecutorService,
private readonly logicFunctionMetadataService: LogicFunctionMetadataService,
private readonly logicFunctionResourceService: LogicFunctionResourceService,
private readonly applicationService: ApplicationService,
private readonly helperService: LogicFunctionFromSourceHelperService,
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
) {}
async deleteOne({
id,
workspaceId,
}: {
id: string;
workspaceId: string;
}): Promise<LogicFunctionDTO> {
const flatLogicFunction =
await this.logicFunctionMetadataService.destroyOne({
id,
workspaceId,
});
return fromFlatLogicFunctionToLogicFunctionDto({ flatLogicFunction });
}
private getHandlerPaths(logicFunctionId: string) {
const logicFunctionSubfolder =
getLogicFunctionSubfolderForFromSource(logicFunctionId);
return {
sourceHandlerPath: join(
logicFunctionSubfolder,
DEFAULT_SOURCE_HANDLER_PATH,
),
builtHandlerPath: join(
logicFunctionSubfolder,
DEFAULT_BUILT_HANDLER_PATH,
),
};
}
async createOne({
async createOneFromSource({
input,
workspaceId,
}: {
input: CreateLogicFunctionFromSourceInput;
workspaceId: string;
}): Promise<LogicFunctionDTO> {
const { workspaceCustomFlatApplication } =
const { workspaceCustomFlatApplication: ownerFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
@@ -79,33 +52,44 @@ export class LogicFunctionFromSourceService {
const logicFunctionId = input.id ?? v4();
const { sourceHandlerPath, builtHandlerPath } =
this.getHandlerPaths(logicFunctionId);
this.helperService.buildHandlerPaths(logicFunctionId);
if (input.source) {
await this.logicFunctionResourceService.uploadSourceFile({
sourceHandlerPath,
sourceHandlerCode: input.source.sourceHandlerCode,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
ownerFlatApplication.universalIdentifier,
workspaceId,
});
const flatLogicFunction =
await this.logicFunctionMetadataService.createOne({
input: {
...input,
handlerName: input.source.handlerName,
toolInputSchema: input.source.toolInputSchema,
const universalFlatLogicFunctionToCreate =
fromCreateLogicFunctionFromSourceInputToUniversalFlatLogicFunctionToCreate(
{
createLogicFunctionFromSourceInput: {
...input,
id: logicFunctionId,
},
sourceHandlerPath,
builtHandlerPath,
id: logicFunctionId,
handlerName: input.source.handlerName,
checksum: null,
toolInputSchema: input.source.toolInputSchema,
isBuildUpToDate: false,
applicationUniversalIdentifier:
ownerFlatApplication.universalIdentifier,
},
workspaceId,
ownerFlatApplication: workspaceCustomFlatApplication,
});
);
return fromFlatLogicFunctionToLogicFunctionDto({ flatLogicFunction });
await this.helperService.createOneFromMetadata({
universalFlatLogicFunctionToCreate,
workspaceId,
});
return this.findFlatLogicFunctionByIdAndConvertToDto({
id: universalFlatLogicFunctionToCreate.id,
workspaceId,
});
}
const { handlerName, checksum } =
@@ -114,63 +98,194 @@ export class LogicFunctionFromSourceService {
builtHandlerPath,
workspaceId,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
ownerFlatApplication.universalIdentifier,
});
const flatLogicFunction = await this.logicFunctionMetadataService.createOne(
{
input: {
...input,
id: logicFunctionId,
const universalFlatLogicFunctionToCreate =
fromCreateLogicFunctionFromSourceInputToUniversalFlatLogicFunctionToCreate(
{
createLogicFunctionFromSourceInput: { ...input, id: logicFunctionId },
sourceHandlerPath,
builtHandlerPath,
handlerName,
checksum,
toolInputSchema: SEED_LOGIC_FUNCTION_INPUT_SCHEMA,
isBuildUpToDate: true,
applicationUniversalIdentifier:
ownerFlatApplication.universalIdentifier,
},
workspaceId,
ownerFlatApplication: workspaceCustomFlatApplication,
},
);
);
return fromFlatLogicFunctionToLogicFunctionDto({ flatLogicFunction });
await this.helperService.createOneFromMetadata({
universalFlatLogicFunctionToCreate,
workspaceId,
});
return this.findFlatLogicFunctionByIdAndConvertToDto({
id: universalFlatLogicFunctionToCreate.id,
workspaceId,
});
}
async updateOne({
id,
update,
async duplicateOneWithSource({
existingLogicFunctionId,
workspaceId,
}: {
id: string;
update: UpdateLogicFunctionFromSourceInput['update'];
existingLogicFunctionId: string;
workspaceId: string;
}): Promise<void> {
const { applicationUniversalIdentifier, flatLogicFunction } =
await this.getLogicFunctionContext({ id, workspaceId });
let formattedUpdate: UpdateLogicFunctionFromSourceInput['update'] = {
...update,
};
if (update.sourceHandlerCode) {
await this.logicFunctionResourceService.uploadSourceFile({
sourceHandlerPath: flatLogicFunction.sourceHandlerPath,
sourceHandlerCode: update.sourceHandlerCode,
applicationUniversalIdentifier,
}): Promise<{ id: string }> {
const { flatLogicFunction: existingLogicFunction, ownerFlatApplication } =
await this.helperService.findLogicFunctionAndApplicationOrThrow({
id: existingLogicFunctionId,
workspaceId,
});
formattedUpdate = {
...formattedUpdate,
isBuildUpToDate: false,
};
const newId = v4();
const { sourceHandlerPath, builtHandlerPath } = existingLogicFunction;
const toSourceHandlerPath = sourceHandlerPath.replace(
existingLogicFunction.id,
newId,
);
const toBuiltHandlerPath = builtHandlerPath.replace(
existingLogicFunction.id,
newId,
);
await this.logicFunctionResourceService.copyResources({
fromSourceHandlerPath: sourceHandlerPath,
toSourceHandlerPath,
fromBuiltHandlerPath: builtHandlerPath,
toBuiltHandlerPath,
workspaceId,
applicationUniversalIdentifier: ownerFlatApplication.universalIdentifier,
});
const universalFlatLogicFunctionToCreate =
buildUniversalFlatLogicFunctionToCreate({
id: newId,
name: existingLogicFunction.name,
description: existingLogicFunction.description,
timeoutSeconds: existingLogicFunction.timeoutSeconds,
toolInputSchema: existingLogicFunction.toolInputSchema,
isTool: existingLogicFunction.isTool,
isBuildUpToDate: existingLogicFunction.isBuildUpToDate,
checksum: existingLogicFunction.checksum,
handlerName: existingLogicFunction.handlerName,
sourceHandlerPath: toSourceHandlerPath,
builtHandlerPath: toBuiltHandlerPath,
cronTriggerSettings: existingLogicFunction.cronTriggerSettings,
databaseEventTriggerSettings:
existingLogicFunction.databaseEventTriggerSettings,
httpRouteTriggerSettings:
existingLogicFunction.httpRouteTriggerSettings,
applicationUniversalIdentifier:
ownerFlatApplication.universalIdentifier,
});
const created = await this.helperService.createOneFromMetadata({
universalFlatLogicFunctionToCreate,
workspaceId,
});
if (!isDefined(created)) {
throw new LogicFunctionException(
'Failed to duplicate logic function',
LogicFunctionExceptionCode.LOGIC_FUNCTION_NOT_FOUND,
);
}
await this.logicFunctionMetadataService.updateOne({
id,
update: formattedUpdate,
return { id: created.id };
}
async updateOneFromSource({
updateLogicFunctionFromSourceInput,
workspaceId,
}: {
updateLogicFunctionFromSourceInput: UpdateLogicFunctionFromSourceInput;
workspaceId: string;
}): Promise<void> {
const { flatLogicFunction, ownerFlatApplication } =
await this.helperService.findLogicFunctionAndApplicationOrThrow({
id: updateLogicFunctionFromSourceInput.id,
workspaceId,
});
const sourceHandlerCode =
updateLogicFunctionFromSourceInput.update.sourceHandlerCode;
if (sourceHandlerCode) {
await this.logicFunctionResourceService.uploadSourceFile({
sourceHandlerPath: flatLogicFunction.sourceHandlerPath,
sourceHandlerCode,
applicationUniversalIdentifier:
ownerFlatApplication.universalIdentifier,
workspaceId,
});
}
const flatLogicFunctionToUpdate =
fromUpdateLogicFunctionFromSourceInputToFlatLogicFunctionToUpdate({
updateLogicFunctionFromSourceInput,
existingFlatLogicFunction: flatLogicFunction,
});
await this.helperService.updateOneFromMetadata({
flatLogicFunctionToUpdate,
workspaceId,
applicationUniversalIdentifier: ownerFlatApplication.universalIdentifier,
});
}
async deleteOneWithSource({
id,
workspaceId,
isSystemBuild = false,
ownerFlatApplication,
}: {
id: string;
workspaceId: string;
isSystemBuild?: boolean;
ownerFlatApplication?: FlatApplication;
}): Promise<LogicFunctionDTO> {
const {
flatLogicFunction: existingFlatLogicFunction,
ownerFlatApplication: resolvedOwnerFlatApplication,
} = await this.helperService.findLogicFunctionAndApplicationOrThrow({
id,
workspaceId,
});
const effectiveOwnerFlatApplication =
ownerFlatApplication ?? resolvedOwnerFlatApplication;
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
logicFunction: {
flatEntityToCreate: [],
flatEntityToDelete: [existingFlatLogicFunction],
flatEntityToUpdate: [],
},
},
workspaceId,
isSystemBuild,
applicationUniversalIdentifier:
effectiveOwnerFlatApplication.universalIdentifier,
},
);
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while destroying logic function',
);
}
return fromFlatLogicFunctionToLogicFunctionDto({
flatLogicFunction: existingFlatLogicFunction,
});
}
@@ -181,25 +296,34 @@ export class LogicFunctionFromSourceService {
id: string;
workspaceId: string;
}): Promise<void> {
const { flatLogicFunction, applicationUniversalIdentifier } =
await this.getLogicFunctionContext({ id, workspaceId });
const { flatLogicFunction, ownerFlatApplication } =
await this.helperService.findLogicFunctionAndApplicationOrThrow({
id,
workspaceId,
});
const { checksum } =
await this.logicFunctionResourceService.buildFromSourceFile({
workspaceId,
applicationUniversalIdentifier,
applicationUniversalIdentifier:
ownerFlatApplication.universalIdentifier,
sourceHandlerPath: flatLogicFunction.sourceHandlerPath,
builtHandlerPath: flatLogicFunction.builtHandlerPath,
});
await this.logicFunctionMetadataService.updateOne({
id,
update: { checksum, isBuildUpToDate: true },
await this.helperService.updateOneFromMetadata({
flatLogicFunctionToUpdate: {
...flatLogicFunction,
checksum,
isBuildUpToDate: true,
updatedAt: new Date().toISOString(),
},
workspaceId,
applicationUniversalIdentifier: ownerFlatApplication.universalIdentifier,
});
}
async executeOne({
async executeOneFromSource({
id,
payload,
workspaceId,
@@ -208,10 +332,11 @@ export class LogicFunctionFromSourceService {
payload: object;
workspaceId: string;
}): Promise<LogicFunctionExecutionResultDTO> {
const { flatLogicFunction } = await this.getLogicFunctionContext({
id,
workspaceId,
});
const { flatLogicFunction } =
await this.helperService.findLogicFunctionAndApplicationOrThrow({
id,
workspaceId,
});
if (!flatLogicFunction.isBuildUpToDate) {
await this.buildOneFromSource({ workspaceId, id });
@@ -247,43 +372,39 @@ export class LogicFunctionFromSourceService {
id: string;
workspaceId: string;
}): Promise<string | null> {
const { flatLogicFunction, applicationUniversalIdentifier } =
await this.getLogicFunctionContext({ id, workspaceId });
const { flatLogicFunction, ownerFlatApplication } =
await this.helperService.findLogicFunctionAndApplicationOrThrow({
id,
workspaceId,
});
return this.logicFunctionResourceService.getSourceFile({
workspaceId,
applicationUniversalIdentifier,
applicationUniversalIdentifier: ownerFlatApplication.universalIdentifier,
sourceHandlerPath: flatLogicFunction.sourceHandlerPath,
});
}
private async getLogicFunctionContext({
private async findFlatLogicFunctionByIdAndConvertToDto({
id,
workspaceId,
}: {
id: string;
workspaceId: string;
}) {
const [{ flatLogicFunctionMaps }, { workspaceCustomFlatApplication }] =
await Promise.all([
this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps({
}): Promise<LogicFunctionDTO> {
const { flatLogicFunctionMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatLogicFunctionMaps'],
}),
this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
),
]);
},
);
const flatLogicFunction = findFlatLogicFunctionOrThrow({
id,
flatLogicFunctionMaps,
return fromFlatLogicFunctionToLogicFunctionDto({
flatLogicFunction: findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: id,
flatEntityMaps: flatLogicFunctionMaps,
}),
});
return {
flatLogicFunction,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
};
}
}
@@ -1,315 +0,0 @@
import { Injectable } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import {
LogicFunctionException,
LogicFunctionExceptionCode,
} from 'src/engine/metadata-modules/logic-function/logic-function.exception';
import { FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
import { findFlatLogicFunctionOrThrow } from 'src/engine/metadata-modules/logic-function/utils/find-flat-logic-function-or-throw.util';
import { fromCreateLogicFunctionInputToFlatLogicFunction } from 'src/engine/metadata-modules/logic-function/utils/from-create-logic-function-from-source-input-to-flat-logic-function.util';
import { fromUpdateLogicFunctionInputToFlatLogicFunctionToUpdateOrThrow } from 'src/engine/metadata-modules/logic-function/utils/from-update-logic-function-input-to-flat-logic-function-to-update-or-throw.util';
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
import { LogicFunctionDTO } from 'src/engine/metadata-modules/logic-function/dtos/logic-function.dto';
import { fromFlatLogicFunctionToLogicFunctionDto } from 'src/engine/metadata-modules/logic-function/utils/from-flat-logic-function-to-logic-function-dto.util';
import { CreateLogicFunction } from 'src/engine/metadata-modules/logic-function/dtos/create-logic-function.input';
import { UpdateLogicFunctionFromSourceInput } from 'src/engine/metadata-modules/logic-function/dtos/update-logic-function-from-source.input';
@Injectable()
export class LogicFunctionMetadataService {
constructor(
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
private readonly applicationService: ApplicationService,
) {}
async findOne({
id,
workspaceId,
}: {
id: string;
workspaceId: string;
}): Promise<LogicFunctionDTO> {
const { flatLogicFunctionMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatLogicFunctionMaps'],
},
);
const flatLogicFunction = findFlatLogicFunctionOrThrow({
id,
flatLogicFunctionMaps,
});
return fromFlatLogicFunctionToLogicFunctionDto({ flatLogicFunction });
}
async findMany({
workspaceId,
}: {
workspaceId: string;
}): Promise<LogicFunctionDTO[]> {
const { flatLogicFunctionMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatLogicFunctionMaps'],
},
);
return Object.values(flatLogicFunctionMaps.byUniversalIdentifier)
.filter(
(flatLogicFunction): flatLogicFunction is FlatLogicFunction =>
isDefined(flatLogicFunction) &&
!isDefined(flatLogicFunction.deletedAt),
)
.map((flatLogicFunction) =>
fromFlatLogicFunctionToLogicFunctionDto({ flatLogicFunction }),
);
}
async createOne({
input,
workspaceId,
ownerFlatApplication,
}: {
input: CreateLogicFunction;
ownerFlatApplication: FlatApplication;
workspaceId: string;
}) {
const resolvedOwnerFlatApplication =
ownerFlatApplication ??
(
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
)
).workspaceCustomFlatApplication;
const flatLogicFunctionToCreate =
fromCreateLogicFunctionInputToFlatLogicFunction({
createLogicFunctionInput: input,
workspaceId,
ownerFlatApplication: resolvedOwnerFlatApplication,
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
logicFunction: {
flatEntityToCreate: [flatLogicFunctionToCreate],
flatEntityToDelete: [],
flatEntityToUpdate: [],
},
},
workspaceId,
isSystemBuild: false,
applicationUniversalIdentifier:
resolvedOwnerFlatApplication.universalIdentifier,
},
);
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while creating logic function',
);
}
return flatLogicFunctionToCreate;
}
async updateOne({
id,
update,
workspaceId,
ownerFlatApplication,
}: {
id: string;
update: Omit<UpdateLogicFunctionFromSourceInput['update'], 'source'>;
workspaceId: string;
ownerFlatApplication?: FlatApplication;
}) {
const resolvedOwnerFlatApplication =
ownerFlatApplication ??
(
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
)
).workspaceCustomFlatApplication;
const { flatLogicFunctionMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatLogicFunctionMaps'],
},
);
const optimisticallyUpdatedFlatLogicFunction =
fromUpdateLogicFunctionInputToFlatLogicFunctionToUpdateOrThrow({
flatLogicFunctionMaps,
updateLogicFunctionInput: { id, update },
});
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
logicFunction: {
flatEntityToCreate: [],
flatEntityToDelete: [],
flatEntityToUpdate: [optimisticallyUpdatedFlatLogicFunction],
},
},
workspaceId,
isSystemBuild: false,
applicationUniversalIdentifier:
resolvedOwnerFlatApplication.universalIdentifier,
},
);
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while updating logic function',
);
}
return this.getFlatLogicFunctionAfterUpdate(
optimisticallyUpdatedFlatLogicFunction.id,
workspaceId,
);
}
private async getFlatLogicFunctionAfterUpdate(
id: string,
workspaceId: string,
) {
const { flatLogicFunctionMaps: recomputedExistingFlatLogicFunctionMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatLogicFunctionMaps'],
},
);
return findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: id,
flatEntityMaps: recomputedExistingFlatLogicFunctionMaps,
});
}
async destroyOne({
id,
workspaceId,
applicationId: _applicationId,
isSystemBuild = false,
ownerFlatApplication,
}: {
id: string;
workspaceId: string;
applicationId?: string;
isSystemBuild?: boolean;
ownerFlatApplication?: FlatApplication;
}): Promise<FlatLogicFunction> {
const resolvedOwnerFlatApplication =
ownerFlatApplication ??
(
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
)
).workspaceCustomFlatApplication;
const { flatLogicFunctionMaps: existingFlatLogicFunctionMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatLogicFunctionMaps'],
},
);
const existingFlatLogicFunction = findFlatEntityByIdInFlatEntityMaps({
flatEntityId: id,
flatEntityMaps: existingFlatLogicFunctionMaps,
});
if (!isDefined(existingFlatLogicFunction)) {
throw new LogicFunctionException(
'Logic function to destroy not found',
LogicFunctionExceptionCode.LOGIC_FUNCTION_NOT_FOUND,
);
}
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
logicFunction: {
flatEntityToCreate: [],
flatEntityToDelete: [existingFlatLogicFunction],
flatEntityToUpdate: [],
},
},
workspaceId,
isSystemBuild,
applicationUniversalIdentifier:
resolvedOwnerFlatApplication.universalIdentifier,
},
);
if (validateAndBuildResult.status === 'fail') {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while destroying logic function',
);
}
return existingFlatLogicFunction;
}
async getAvailablePackages({
logicFunctionId,
workspaceId,
}: {
logicFunctionId: string;
workspaceId: string;
}) {
const { flatLogicFunctionMaps, flatApplicationMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatLogicFunctionMaps', 'flatApplicationMaps'],
},
);
const logicFunctionUniversalIdentifier =
flatLogicFunctionMaps.universalIdentifierById[logicFunctionId];
if (!logicFunctionUniversalIdentifier) {
return {};
}
const logicFunction =
flatLogicFunctionMaps.byUniversalIdentifier[
logicFunctionUniversalIdentifier
];
if (!logicFunction) {
return {};
}
const application = flatApplicationMaps.byId[logicFunction.applicationId];
return application?.availablePackages ?? {};
}
}
@@ -0,0 +1,56 @@
import { v4 } from 'uuid';
import { LogicFunctionRuntime } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
import { type UniversalFlatLogicFunction } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-logic-function.type';
type AutoGeneratedFields = 'createdAt' | 'updatedAt' | 'deletedAt' | 'runtime';
type RequiredFields =
| 'name'
| 'handlerName'
| 'sourceHandlerPath'
| 'builtHandlerPath'
| 'isBuildUpToDate'
| 'applicationUniversalIdentifier';
type OptionalFields = Exclude<
keyof UniversalFlatLogicFunction,
AutoGeneratedFields | RequiredFields
>;
export type BuildUniversalFlatLogicFunctionToCreateInput = Pick<
UniversalFlatLogicFunction,
RequiredFields
> &
Partial<Pick<UniversalFlatLogicFunction, OptionalFields>> & {
id?: string;
};
export const buildUniversalFlatLogicFunctionToCreate = (
input: BuildUniversalFlatLogicFunctionToCreateInput,
): UniversalFlatLogicFunction & { id: string } => {
const now = new Date().toISOString();
return {
id: input.id ?? v4(),
universalIdentifier: input.universalIdentifier ?? v4(),
name: input.name,
description: input.description ?? null,
runtime: LogicFunctionRuntime.NODE22,
timeoutSeconds: input.timeoutSeconds ?? 300,
checksum: input.checksum ?? null,
toolInputSchema: input.toolInputSchema ?? null,
isTool: input.isTool ?? false,
isBuildUpToDate: input.isBuildUpToDate,
handlerName: input.handlerName,
sourceHandlerPath: input.sourceHandlerPath,
builtHandlerPath: input.builtHandlerPath,
cronTriggerSettings: input.cronTriggerSettings ?? null,
databaseEventTriggerSettings: input.databaseEventTriggerSettings ?? null,
httpRouteTriggerSettings: input.httpRouteTriggerSettings ?? null,
createdAt: now,
updatedAt: now,
deletedAt: null,
applicationUniversalIdentifier: input.applicationUniversalIdentifier,
};
};
@@ -1,52 +0,0 @@
import { v4 } from 'uuid';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { LogicFunctionRuntime } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
import { type FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
import { type CreateLogicFunction } from 'src/engine/metadata-modules/logic-function/dtos/create-logic-function.input';
export type FromCreateLogicFunctionInputToFlatLogicFunctionArgs = {
createLogicFunctionInput: CreateLogicFunction;
workspaceId: string;
ownerFlatApplication: FlatApplication;
};
export const fromCreateLogicFunctionInputToFlatLogicFunction = ({
createLogicFunctionInput: rawCreateLogicFunctionInput,
workspaceId,
ownerFlatApplication,
}: FromCreateLogicFunctionInputToFlatLogicFunctionArgs): FlatLogicFunction => {
const id = rawCreateLogicFunctionInput.id ?? v4();
const currentDate = new Date();
const universalIdentifier =
rawCreateLogicFunctionInput.universalIdentifier ?? v4();
return {
id,
cronTriggerSettings:
rawCreateLogicFunctionInput.cronTriggerSettings ?? null,
databaseEventTriggerSettings:
rawCreateLogicFunctionInput.databaseEventTriggerSettings ?? null,
httpRouteTriggerSettings:
rawCreateLogicFunctionInput.httpRouteTriggerSettings ?? null,
name: rawCreateLogicFunctionInput.name,
description: rawCreateLogicFunctionInput.description ?? null,
sourceHandlerPath: rawCreateLogicFunctionInput.sourceHandlerPath,
handlerName: rawCreateLogicFunctionInput.handlerName,
builtHandlerPath: rawCreateLogicFunctionInput.builtHandlerPath,
universalIdentifier,
createdAt: currentDate.toISOString(),
updatedAt: currentDate.toISOString(),
deletedAt: null,
applicationId: ownerFlatApplication.id,
runtime: LogicFunctionRuntime.NODE22,
timeoutSeconds: rawCreateLogicFunctionInput.timeoutSeconds ?? 300,
workspaceId,
checksum: rawCreateLogicFunctionInput.checksum ?? null,
toolInputSchema: rawCreateLogicFunctionInput.toolInputSchema ?? null,
isTool: rawCreateLogicFunctionInput?.isTool ?? false,
isBuildUpToDate: rawCreateLogicFunctionInput.isBuildUpToDate,
applicationUniversalIdentifier: ownerFlatApplication.universalIdentifier,
};
};
@@ -0,0 +1,64 @@
import { trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import { LogicFunctionRuntime } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
import { type CreateLogicFunctionFromSourceInput } from 'src/engine/metadata-modules/logic-function/dtos/create-logic-function-from-source.input';
import { type UniversalFlatLogicFunction } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-logic-function.type';
export const fromCreateLogicFunctionFromSourceInputToUniversalFlatLogicFunctionToCreate =
({
createLogicFunctionFromSourceInput,
sourceHandlerPath,
builtHandlerPath,
handlerName,
checksum,
toolInputSchema,
isBuildUpToDate,
applicationUniversalIdentifier,
}: {
createLogicFunctionFromSourceInput: CreateLogicFunctionFromSourceInput;
sourceHandlerPath: string;
builtHandlerPath: string;
handlerName: string;
checksum: string | null;
toolInputSchema: object | null;
isBuildUpToDate: boolean;
applicationUniversalIdentifier: string;
}): UniversalFlatLogicFunction & { id: string } => {
const now = new Date().toISOString();
const { name, description } =
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
createLogicFunctionFromSourceInput,
['name', 'description'],
);
const id = createLogicFunctionFromSourceInput.id ?? v4();
return {
id,
universalIdentifier:
createLogicFunctionFromSourceInput.universalIdentifier ?? v4(),
name,
description: description ?? null,
runtime: LogicFunctionRuntime.NODE22,
timeoutSeconds: createLogicFunctionFromSourceInput.timeoutSeconds ?? 300,
checksum,
toolInputSchema,
isTool: createLogicFunctionFromSourceInput.isTool ?? false,
isBuildUpToDate,
handlerName,
sourceHandlerPath,
builtHandlerPath,
cronTriggerSettings:
createLogicFunctionFromSourceInput.cronTriggerSettings ?? null,
databaseEventTriggerSettings:
createLogicFunctionFromSourceInput.databaseEventTriggerSettings ?? null,
httpRouteTriggerSettings:
createLogicFunctionFromSourceInput.httpRouteTriggerSettings ?? null,
createdAt: now,
updatedAt: now,
deletedAt: null,
applicationUniversalIdentifier,
};
};
@@ -0,0 +1,30 @@
import { isDefined } from 'twenty-shared/utils';
import { FLAT_LOGIC_FUNCTION_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/logic-function/constants/flat-logic-function-editable-properties.constant';
import { type UpdateLogicFunctionFromSourceInput } from 'src/engine/metadata-modules/logic-function/dtos/update-logic-function-from-source.input';
import { type FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
import { mergeUpdateInExistingRecord } from 'src/utils/merge-update-in-existing-record.util';
export const fromUpdateLogicFunctionFromSourceInputToFlatLogicFunctionToUpdate =
({
updateLogicFunctionFromSourceInput,
existingFlatLogicFunction,
}: {
updateLogicFunctionFromSourceInput: UpdateLogicFunctionFromSourceInput;
existingFlatLogicFunction: FlatLogicFunction;
}): FlatLogicFunction => {
const { sourceHandlerCode, ...metadataUpdates } =
updateLogicFunctionFromSourceInput.update;
return {
...mergeUpdateInExistingRecord({
existing: existingFlatLogicFunction,
properties: [...FLAT_LOGIC_FUNCTION_EDITABLE_PROPERTIES],
update: {
...metadataUpdates,
...(isDefined(sourceHandlerCode) ? { isBuildUpToDate: false } : {}),
},
}),
updatedAt: new Date().toISOString(),
};
};
@@ -1,33 +0,0 @@
import { trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties } from 'twenty-shared/utils';
import { type MetadataFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity-maps.type';
import { FLAT_LOGIC_FUNCTION_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/logic-function/constants/flat-logic-function-editable-properties.constant';
import { type FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
import { findFlatLogicFunctionOrThrow } from 'src/engine/metadata-modules/logic-function/utils/find-flat-logic-function-or-throw.util';
import { mergeUpdateInExistingRecord } from 'src/utils/merge-update-in-existing-record.util';
import { type UpdateLogicFunctionFromSourceInput } from 'src/engine/metadata-modules/logic-function/dtos/update-logic-function-from-source.input';
export const fromUpdateLogicFunctionInputToFlatLogicFunctionToUpdateOrThrow = ({
updateLogicFunctionInput: rawUpdateLogicFunctionInput,
flatLogicFunctionMaps,
}: {
updateLogicFunctionInput: UpdateLogicFunctionFromSourceInput;
flatLogicFunctionMaps: MetadataFlatEntityMaps<'logicFunction'>;
}): FlatLogicFunction => {
const { id: logicFunctionToUpdateId } =
trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties(
rawUpdateLogicFunctionInput,
['id'],
);
const existingFlatLogicFunctionToUpdate = findFlatLogicFunctionOrThrow({
id: logicFunctionToUpdateId,
flatLogicFunctionMaps,
});
return mergeUpdateInExistingRecord({
existing: existingFlatLogicFunctionToUpdate,
properties: FLAT_LOGIC_FUNCTION_EDITABLE_PROPERTIES,
update: rawUpdateLogicFunctionInput.update,
});
};
@@ -8,7 +8,7 @@ import { type FlatFieldMetadata } from 'src/engine/metadata-modules/flat-field-m
import { type FlatObjectMetadata } from 'src/engine/metadata-modules/flat-object-metadata/types/flat-object-metadata.type';
import { buildObjectIdByNameMaps } from 'src/engine/metadata-modules/flat-object-metadata/utils/build-object-id-by-name-maps.util';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { LogicFunctionMetadataService } from 'src/engine/metadata-modules/logic-function/services/logic-function-metadata.service';
import { LogicFunctionFromSourceService } from 'src/engine/metadata-modules/logic-function/services/logic-function-from-source.service';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
import { type WorkspaceRepository } from 'src/engine/twenty-orm/repository/workspace.repository';
import { buildSystemAuthContext } from 'src/engine/twenty-orm/utils/build-system-auth-context.util';
@@ -42,7 +42,7 @@ export type ObjectMetadataInfo = {
export class WorkflowCommonWorkspaceService {
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly logicFunctionMetadataService: LogicFunctionMetadataService,
private readonly logicFunctionFromSourceService: LogicFunctionFromSourceService,
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
) {}
@@ -322,7 +322,7 @@ export class WorkflowCommonWorkspaceService {
for (const workflowVersion of workflowVersions) {
for (const step of workflowVersion.steps ?? []) {
if (step.type === WorkflowActionType.CODE) {
await this.logicFunctionMetadataService.destroyOne({
await this.logicFunctionFromSourceService.deleteOneWithSource({
id: step.settings.input.logicFunctionId,
workspaceId,
});
@@ -6,8 +6,7 @@ import { AgentService } from 'src/engine/metadata-modules/ai/ai-agent/agent.serv
import { createEmptyAllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-all-flat-entity-maps.constant';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { LogicFunctionRuntime } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
import { LogicFunctionMetadataService } from 'src/engine/metadata-modules/logic-function/services/logic-function-metadata.service';
import { type FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
import { LogicFunctionFromSourceService } from 'src/engine/metadata-modules/logic-function/services/logic-function-from-source.service';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager';
@@ -25,7 +24,7 @@ const mockWorkspaceId = 'workspace-id';
describe('WorkflowVersionStepOperationsWorkspaceService', () => {
let service: WorkflowVersionStepOperationsWorkspaceService;
let globalWorkspaceOrmManager: jest.Mocked<GlobalWorkspaceOrmManager>;
let logicFunctionMetadataService: jest.Mocked<LogicFunctionMetadataService>;
let logicFunctionFromSourceService: jest.Mocked<LogicFunctionFromSourceService>;
let codeStepBuildService: jest.Mocked<CodeStepBuildService>;
let agentService: jest.Mocked<AgentService>;
let roleTargetRepository: jest.Mocked<any>;
@@ -33,7 +32,6 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
let workflowCommonWorkspaceService: jest.Mocked<WorkflowCommonWorkspaceService>;
let aiAgentRoleService: jest.Mocked<AiAgentRoleService>;
let workspaceCacheService: jest.Mocked<WorkspaceCacheService>;
let flatEntityMapsCacheService: jest.Mocked<WorkspaceManyOrAllFlatEntityMapsCacheService>;
beforeEach(async () => {
codeStepBuildService = {
@@ -89,10 +87,9 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
}),
} as unknown as jest.Mocked<CodeStepBuildService>;
logicFunctionMetadataService = {
createOne: jest.fn(),
destroyOne: jest.fn(),
} as unknown as jest.Mocked<LogicFunctionMetadataService>;
logicFunctionFromSourceService = {
deleteOneWithSource: jest.fn(),
} as unknown as jest.Mocked<LogicFunctionFromSourceService>;
agentService = {
deleteManyAgents: jest.fn().mockResolvedValue([]),
@@ -132,8 +129,8 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
useValue: globalWorkspaceOrmManager,
},
{
provide: LogicFunctionMetadataService,
useValue: logicFunctionMetadataService,
provide: LogicFunctionFromSourceService,
useValue: logicFunctionFromSourceService,
},
{
provide: CodeStepBuildService,
@@ -165,12 +162,12 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
},
{
provide: WorkspaceManyOrAllFlatEntityMapsCacheService,
useValue: (flatEntityMapsCacheService = {
useValue: {
flushFlatEntityMaps: jest.fn(),
getOrRecomputeManyOrAllFlatEntityMaps: jest
.fn()
.mockResolvedValue(createEmptyAllFlatEntityMaps()),
} as unknown as jest.Mocked<WorkspaceManyOrAllFlatEntityMapsCacheService>),
},
},
],
}).compile();
@@ -203,7 +200,9 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
workspaceId: mockWorkspaceId,
});
expect(logicFunctionMetadataService.destroyOne).toHaveBeenCalledWith({
expect(
logicFunctionFromSourceService.deleteOneWithSource,
).toHaveBeenCalledWith({
id: 'function-id',
workspaceId: mockWorkspaceId,
});
@@ -286,35 +285,6 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
describe('runStepCreationSideEffectsAndBuildStep', () => {
it('should create code step with logic function', async () => {
const mockFlatLogicFunction: FlatLogicFunction = {
id: 'new-function-id',
name: 'Test Function',
description: 'Test Description',
workspaceId: mockWorkspaceId,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
deletedAt: null,
runtime: LogicFunctionRuntime.NODE22,
timeoutSeconds: 30,
sourceHandlerPath: 'src/index.ts',
builtHandlerPath: 'src/index.mjs',
handlerName: 'main',
checksum: null,
toolInputSchema: null,
isTool: false,
universalIdentifier: 'universal-id',
applicationId: 'application-id',
applicationUniversalIdentifier: 'application-universal-id',
cronTriggerSettings: null,
databaseEventTriggerSettings: null,
httpRouteTriggerSettings: null,
isBuildUpToDate: false,
};
logicFunctionMetadataService.createOne.mockResolvedValue(
mockFlatLogicFunction,
);
const result = await service.runStepCreationSideEffectsAndBuildStep({
type: WorkflowActionType.CODE,
workspaceId: mockWorkspaceId,
@@ -360,78 +330,6 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => {
nextStepIds: ['next-step'],
} as unknown as WorkflowAction;
const mockExistingFlatLogicFunction: FlatLogicFunction = {
id: 'function-id',
name: 'Existing Function',
description: 'Existing Description',
workspaceId: mockWorkspaceId,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
deletedAt: null,
runtime: LogicFunctionRuntime.NODE22,
timeoutSeconds: 30,
sourceHandlerPath: 'workflow/function-id/src/index.ts',
builtHandlerPath: 'workflow/function-id/src/index.mjs',
handlerName: 'main',
checksum: 'existing-checksum',
toolInputSchema: null,
isTool: false,
universalIdentifier: 'existing-universal-id',
applicationId: 'application-id',
applicationUniversalIdentifier: 'application-universal-id',
cronTriggerSettings: null,
databaseEventTriggerSettings: null,
httpRouteTriggerSettings: null,
isBuildUpToDate: true,
};
const mockNewFlatLogicFunction: FlatLogicFunction = {
id: 'new-function-id',
name: 'Test Function',
description: 'Test Description',
workspaceId: mockWorkspaceId,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
deletedAt: null,
runtime: LogicFunctionRuntime.NODE22,
timeoutSeconds: 30,
sourceHandlerPath: 'src/index.ts',
builtHandlerPath: 'src/index.mjs',
handlerName: 'main',
checksum: null,
toolInputSchema: null,
isTool: false,
universalIdentifier: 'universal-id',
applicationId: 'application-id',
applicationUniversalIdentifier: 'application-universal-id',
cronTriggerSettings: null,
databaseEventTriggerSettings: null,
httpRouteTriggerSettings: null,
isBuildUpToDate: false,
};
const emptyMaps = createEmptyAllFlatEntityMaps();
const flatLogicFunctionMapsKey = 'flatLogicFunctionMaps' as const;
flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps.mockResolvedValueOnce(
{
...emptyMaps,
[flatLogicFunctionMapsKey]: {
byUniversalIdentifier: {
'existing-universal-id': mockExistingFlatLogicFunction,
},
universalIdentifierById: {
'function-id': 'existing-universal-id',
},
universalIdentifiersByApplicationId: {},
},
},
);
logicFunctionMetadataService.createOne.mockResolvedValue(
mockNewFlatLogicFunction,
);
const clonedStep = await service.cloneStep({
step: originalStep,
workspaceId: mockWorkspaceId,
@@ -1,36 +1,20 @@
import { Injectable } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { v4 } from 'uuid';
import {
CronTriggerSettings,
DatabaseEventTriggerSettings,
HttpRouteTriggerSettings,
} from 'twenty-shared/application';
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
import { LogicFunctionMetadataService } from 'src/engine/metadata-modules/logic-function/services/logic-function-metadata.service';
import { type FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
import { findFlatLogicFunctionOrThrow } from 'src/engine/metadata-modules/logic-function/utils/find-flat-logic-function-or-throw.util';
import { fromCreateLogicFunctionInputToFlatLogicFunction } from 'src/engine/metadata-modules/logic-function/utils/from-create-logic-function-from-source-input-to-flat-logic-function.util';
import { LogicFunctionFromSourceService } from 'src/engine/metadata-modules/logic-function/services/logic-function-from-source.service';
import {
WorkflowActionType,
type WorkflowAction,
} from 'src/modules/workflow/workflow-executor/workflow-actions/types/workflow-action.type';
import { LogicFunctionResourceService } from 'src/engine/core-modules/logic-function/logic-function-resource/logic-function-resource.service';
import type { JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type';
import { LogicFunctionFromSourceService } from 'src/engine/metadata-modules/logic-function/services/logic-function-from-source.service';
@Injectable()
export class CodeStepBuildService {
constructor(
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly logicFunctionMetadataService: LogicFunctionMetadataService,
private readonly logicFunctionFromSourceService: LogicFunctionFromSourceService,
private readonly applicationService: ApplicationService,
private readonly logicFunctionResourceService: LogicFunctionResourceService,
) {}
async createCodeStepLogicFunction({
@@ -40,7 +24,7 @@ export class CodeStepBuildService {
logicFunctionId: string;
workspaceId: string;
}) {
return await this.logicFunctionFromSourceService.createOne({
return await this.logicFunctionFromSourceService.createOneFromSource({
input: {
id: logicFunctionId,
name: 'A Code Step',
@@ -56,109 +40,11 @@ export class CodeStepBuildService {
}: {
existingLogicFunctionId: string;
workspaceId: string;
}): Promise<FlatLogicFunction> {
const { flatLogicFunctionMaps } =
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatLogicFunctionMaps'],
},
);
const existingLogicFunction = findFlatLogicFunctionOrThrow({
id: existingLogicFunctionId,
flatLogicFunctionMaps,
});
const resolvedOwnerFlatApplication = (
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
)
).workspaceCustomFlatApplication;
const applicationUniversalIdentifier =
resolvedOwnerFlatApplication.universalIdentifier;
const newId = v4();
const newUniversalIdentifier = v4();
const { sourceHandlerPath, builtHandlerPath } = existingLogicFunction;
const toSourceHandlerPath = sourceHandlerPath.replace(
existingLogicFunction.id,
newId,
);
const toBuiltHandlerPath = builtHandlerPath.replace(
existingLogicFunction.id,
newId,
);
await this.logicFunctionResourceService.copyResources({
fromSourceHandlerPath: sourceHandlerPath,
toSourceHandlerPath,
fromBuiltHandlerPath: builtHandlerPath,
toBuiltHandlerPath: toBuiltHandlerPath,
}): Promise<{ id: string }> {
return this.logicFunctionFromSourceService.duplicateOneWithSource({
existingLogicFunctionId,
workspaceId,
applicationUniversalIdentifier,
});
const newFlatLogicFunction =
fromCreateLogicFunctionInputToFlatLogicFunction({
createLogicFunctionInput: {
...existingLogicFunction,
id: newId,
universalIdentifier: newUniversalIdentifier,
description: existingLogicFunction.description ?? undefined,
sourceHandlerPath: toSourceHandlerPath,
builtHandlerPath: toBuiltHandlerPath,
toolInputSchema: existingLogicFunction.toolInputSchema ?? {},
checksum: existingLogicFunction.checksum ?? '[default-checksum]', // TODO: checksum should never be null, update column in logicFunction entity to set it non nullable
cronTriggerSettings: existingLogicFunction.cronTriggerSettings as
| JsonbProperty<CronTriggerSettings>
| undefined,
databaseEventTriggerSettings:
existingLogicFunction.databaseEventTriggerSettings as
| JsonbProperty<DatabaseEventTriggerSettings>
| undefined,
httpRouteTriggerSettings:
existingLogicFunction.httpRouteTriggerSettings as
| JsonbProperty<HttpRouteTriggerSettings>
| undefined,
},
workspaceId,
ownerFlatApplication: resolvedOwnerFlatApplication,
});
const created = await this.logicFunctionMetadataService.createOne({
input: {
...newFlatLogicFunction,
description: newFlatLogicFunction.description ?? undefined,
checksum: newFlatLogicFunction.checksum ?? '[default-checksum]',
toolInputSchema: newFlatLogicFunction.toolInputSchema ?? {},
cronTriggerSettings: newFlatLogicFunction.cronTriggerSettings as
| JsonbProperty<CronTriggerSettings>
| undefined,
databaseEventTriggerSettings:
newFlatLogicFunction.databaseEventTriggerSettings as
| JsonbProperty<DatabaseEventTriggerSettings>
| undefined,
httpRouteTriggerSettings:
newFlatLogicFunction.httpRouteTriggerSettings as
| JsonbProperty<HttpRouteTriggerSettings>
| undefined,
},
workspaceId,
ownerFlatApplication: resolvedOwnerFlatApplication,
});
if (!isDefined(created)) {
throw new Error(
'Failed to create logic function when duplicating code step',
);
}
return created;
}
async buildCodeStepsFromSourceForSteps({
@@ -21,7 +21,7 @@ import { AiAgentRoleService } from 'src/engine/metadata-modules/ai/ai-agent-role
import { AgentService } from 'src/engine/metadata-modules/ai/ai-agent/agent.service';
import { DEFAULT_SMART_MODEL } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-models.const';
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
import { LogicFunctionMetadataService } from 'src/engine/metadata-modules/logic-function/services/logic-function-metadata.service';
import { LogicFunctionFromSourceService } from 'src/engine/metadata-modules/logic-function/services/logic-function-from-source.service';
import { findFlatLogicFunctionOrThrow } from 'src/engine/metadata-modules/logic-function/utils/find-flat-logic-function-or-throw.util';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity';
@@ -65,7 +65,7 @@ const ITERATOR_EMPTY_STEP_POSITION_OFFSET = {
export class WorkflowVersionStepOperationsWorkspaceService {
constructor(
private readonly globalWorkspaceOrmManager: GlobalWorkspaceOrmManager,
private readonly logicFunctionMetadataService: LogicFunctionMetadataService,
private readonly logicFunctionFromSourceService: LogicFunctionFromSourceService,
private readonly codeStepBuildService: CodeStepBuildService,
private readonly agentService: AgentService,
@InjectRepository(RoleTargetEntity)
@@ -87,7 +87,7 @@ export class WorkflowVersionStepOperationsWorkspaceService {
}) {
switch (step.type) {
case WorkflowActionType.CODE: {
await this.logicFunctionMetadataService.destroyOne({
await this.logicFunctionFromSourceService.deleteOneWithSource({
id: step.settings.input.logicFunctionId,
workspaceId,
});
@@ -50,10 +50,12 @@ To find the logicFunctionId, look at the code step's settings.input.logicFunctio
const { logicFunctionId, code } = parameters;
const { workspaceId } = context;
await deps.logicFunctionFromSourceService.updateOne({
id: logicFunctionId,
update: {
sourceHandlerCode: code,
await deps.logicFunctionFromSourceService.updateOneFromSource({
updateLogicFunctionFromSourceInput: {
id: logicFunctionId,
update: {
sourceHandlerCode: code,
},
},
workspaceId,
});