twenty / packages /twenty-server /src /engine /metadata-modules /logic-function /services /logic-function-from-source.service.ts
Jules
Initial clean CRM deployment with prebuilt assets
d9494a5
Raw
History Blame Contribute Delete
15.5 kB
import { Injectable } from '@nestjs/common';
import crypto from 'crypto';
import { v4 } from 'uuid';
import { isDefined } from 'twenty-shared/utils';
import { ApplicationService } from 'src/engine/core-modules/application/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 { LogicFunctionExecutionMode } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
import {
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 logicFunctionResourceService: LogicFunctionResourceService,
private readonly applicationService: ApplicationService,
private readonly helperService: LogicFunctionFromSourceHelperService,
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
) {}
async createOneFromSource({
input,
workspaceId,
}: {
input: CreateLogicFunctionFromSourceInput;
workspaceId: string;
}): Promise<LogicFunctionDTO> {
const { workspaceCustomFlatApplication: ownerFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
);
const logicFunctionId = input.id ?? v4();
const { sourceHandlerPath, builtHandlerPath } =
this.helperService.buildHandlerPaths(logicFunctionId);
if (input.source) {
await this.logicFunctionResourceService.uploadSourceFile({
sourceHandlerPath,
sourceHandlerCode: input.source.sourceHandlerCode,
applicationUniversalIdentifier:
ownerFlatApplication.universalIdentifier,
workspaceId,
});
const universalFlatLogicFunctionToCreate =
fromCreateLogicFunctionFromSourceInputToUniversalFlatLogicFunctionToCreate(
{
createLogicFunctionFromSourceInput: {
...input,
id: logicFunctionId,
},
sourceHandlerPath,
builtHandlerPath,
handlerName: input.source.handlerName,
checksum: null,
isBuildUpToDate: false,
applicationUniversalIdentifier:
ownerFlatApplication.universalIdentifier,
},
);
await this.helperService.createOneFromMetadata({
universalFlatLogicFunctionToCreate,
workspaceId,
});
return this.findFlatLogicFunctionByIdAndConvertToDto({
id: universalFlatLogicFunctionToCreate.id,
workspaceId,
});
}
const { handlerName, checksum } =
await this.logicFunctionResourceService.seedSourceFiles({
sourceHandlerPath,
builtHandlerPath,
workspaceId,
applicationUniversalIdentifier:
ownerFlatApplication.universalIdentifier,
});
const universalFlatLogicFunctionToCreate =
fromCreateLogicFunctionFromSourceInputToUniversalFlatLogicFunctionToCreate(
{
createLogicFunctionFromSourceInput: { ...input, id: logicFunctionId },
sourceHandlerPath,
builtHandlerPath,
handlerName,
checksum,
isBuildUpToDate: true,
applicationUniversalIdentifier:
ownerFlatApplication.universalIdentifier,
},
);
await this.helperService.createOneFromMetadata({
universalFlatLogicFunctionToCreate,
workspaceId,
});
return this.findFlatLogicFunctionByIdAndConvertToDto({
id: universalFlatLogicFunctionToCreate.id,
workspaceId,
});
}
async duplicateOneWithSource({
existingLogicFunctionId,
workspaceId,
}: {
existingLogicFunctionId: string;
workspaceId: string;
}): Promise<{ id: string }> {
const { flatLogicFunction: existingLogicFunction, ownerFlatApplication } =
await this.helperService.findLogicFunctionAndApplicationOrThrow({
id: existingLogicFunctionId,
workspaceId,
});
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,
isBuildUpToDate: existingLogicFunction.isBuildUpToDate,
checksum: existingLogicFunction.checksum,
executionMode: LogicFunctionExecutionMode.LIVE,
handlerName: existingLogicFunction.handlerName,
sourceHandlerPath: toSourceHandlerPath,
builtHandlerPath: toBuiltHandlerPath,
cronTriggerSettings: existingLogicFunction.cronTriggerSettings,
databaseEventTriggerSettings:
existingLogicFunction.databaseEventTriggerSettings,
httpRouteTriggerSettings:
existingLogicFunction.httpRouteTriggerSettings,
toolTriggerSettings: existingLogicFunction.toolTriggerSettings,
workflowActionTriggerSettings:
existingLogicFunction.workflowActionTriggerSettings,
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,
);
}
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,
});
}
async buildOneFromSource({
id,
workspaceId,
}: {
id: string;
workspaceId: string;
}): Promise<void> {
const { flatLogicFunction, ownerFlatApplication } =
await this.helperService.findLogicFunctionAndApplicationOrThrow({
id,
workspaceId,
});
const sourceCode = await this.logicFunctionResourceService.getSourceFile({
workspaceId,
applicationUniversalIdentifier: ownerFlatApplication.universalIdentifier,
sourceHandlerPath: flatLogicFunction.sourceHandlerPath,
});
if (!sourceCode) {
throw new LogicFunctionException(
'Source file not found',
LogicFunctionExceptionCode.LOGIC_FUNCTION_NOT_FOUND,
);
}
const { builtCode } = await this.logicFunctionExecutorService.transpile({
sourceCode,
sourceFileName: flatLogicFunction.sourceHandlerPath,
builtFileName: flatLogicFunction.builtHandlerPath,
});
await this.logicFunctionResourceService.uploadBuiltFile({
workspaceId,
applicationUniversalIdentifier: ownerFlatApplication.universalIdentifier,
builtHandlerPath: flatLogicFunction.builtHandlerPath,
builtCode,
});
const checksum = crypto.createHash('md5').update(builtCode).digest('hex');
await this.helperService.updateOneFromMetadata({
flatLogicFunctionToUpdate: {
...flatLogicFunction,
checksum,
isBuildUpToDate: true,
updatedAt: new Date().toISOString(),
},
workspaceId,
applicationUniversalIdentifier: ownerFlatApplication.universalIdentifier,
});
}
async executeOneFromSource({
id,
payload,
workspaceId,
}: {
id: string;
payload: object;
workspaceId: string;
}): Promise<LogicFunctionExecutionResultDTO> {
const { flatLogicFunction } =
await this.helperService.findLogicFunctionAndApplicationOrThrow({
id,
workspaceId,
});
if (!flatLogicFunction.isBuildUpToDate) {
await this.buildOneFromSource({ workspaceId, id });
}
const result = await this.logicFunctionExecutorService.execute({
logicFunctionId: id,
workspaceId,
payload,
executionMode: LogicFunctionExecutionMode.LIVE,
});
return {
data: result.data as LogicFunctionExecutionResultDTO['data'],
logs: result.logs,
duration: result.duration,
status: result.status,
error: result.error
? {
errorType: result.error.errorType,
errorMessage: result.error.errorMessage,
stackTrace: Array.isArray(result.error.stackTrace)
? result.error.stackTrace.join('\n')
: result.error.stackTrace,
}
: undefined,
};
}
async getSourceCode({
id,
workspaceId,
}: {
id: string;
workspaceId: string;
}): Promise<string | null> {
const { flatLogicFunction, ownerFlatApplication } =
await this.helperService.findLogicFunctionAndApplicationOrThrow({
id,
workspaceId,
});
return this.logicFunctionResourceService.getSourceFile({
workspaceId,
applicationUniversalIdentifier: ownerFlatApplication.universalIdentifier,
sourceHandlerPath: flatLogicFunction.sourceHandlerPath,
});
}
private async findFlatLogicFunctionByIdAndConvertToDto({
id,
workspaceId,
}: {
id: string;
workspaceId: string;
}): Promise<LogicFunctionDTO> {
const { flatLogicFunctionMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatLogicFunctionMaps'],
},
);
return fromFlatLogicFunctionToLogicFunctionDto({
flatLogicFunction: findFlatEntityByIdInFlatEntityMapsOrThrow({
flatEntityId: id,
flatEntityMaps: flatLogicFunctionMaps,
}),
});
}
}