Logic function refactorization (#17861)

As title
This commit is contained in:
martmull
2026-02-12 11:40:49 +01:00
committed by GitHub
parent b456f79167
commit a4ed043d43
122 changed files with 1441 additions and 1897 deletions
@@ -12,4 +12,5 @@ export const FLAT_LOGIC_FUNCTION_EDITABLE_PROPERTIES = [
'cronTriggerSettings',
'databaseEventTriggerSettings',
'httpRouteTriggerSettings',
] as const satisfies (MetadataEntityPropertyName<'logicFunction'> | 'code')[];
'isBuildUpToDate',
] as const satisfies MetadataEntityPropertyName<'logicFunction'>[];
@@ -0,0 +1,3 @@
export const DEFAULT_BUILT_HANDLER_PATH = 'src/index.mjs';
export const DEFAULT_SOURCE_HANDLER_PATH = 'src/index.ts';
export const DEFAULT_HANDLER_NAME = 'main';
@@ -1,52 +0,0 @@
import { Field, HideField, InputType } from '@nestjs/graphql';
import {
IsBoolean,
IsNotEmpty,
IsNumber,
IsObject,
IsOptional,
IsString,
Max,
Min,
} from 'class-validator';
import graphqlTypeJson from 'graphql-type-json';
@InputType()
export class CreateDefaultLogicFunctionInput {
@IsString()
@IsNotEmpty()
@Field()
name: string;
@IsString()
@IsOptional()
@Field({ nullable: true })
description?: string;
@IsNumber()
@Field({ nullable: true })
@Min(1)
@Max(900)
@IsOptional()
timeoutSeconds?: number;
@HideField()
applicationId?: string;
@HideField()
universalIdentifier?: string;
@HideField()
id?: string;
@Field(() => graphqlTypeJson, { nullable: true })
@IsObject()
@IsOptional()
toolInputSchema?: object;
@IsBoolean()
@Field({ nullable: true })
@IsOptional()
isTool?: boolean;
}
@@ -0,0 +1,83 @@
import { Field, InputType } from '@nestjs/graphql';
import {
IsBoolean,
IsNotEmpty,
IsNumber,
IsObject,
IsOptional,
IsString,
IsUUID,
Max,
Min,
} from 'class-validator';
import graphqlTypeJson from 'graphql-type-json';
import {
CronTriggerSettings,
DatabaseEventTriggerSettings,
HttpRouteTriggerSettings,
} from 'twenty-shared/application';
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';
import { LogicFunctionSourceInput } from 'src/engine/metadata-modules/logic-function/dtos/logic-function-source.input';
@InputType()
export class CreateLogicFunctionFromSourceInput {
@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: true })
@IsObject()
@IsOptional()
toolInputSchema?: object;
@IsBoolean()
@Field({ nullable: true })
@IsOptional()
isTool?: boolean;
@IsObject()
@Field(() => graphqlTypeJson, { nullable: true })
@IsOptional()
source?: JsonbProperty<LogicFunctionSourceInput>;
@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>;
}
@@ -1,6 +1,16 @@
import { Field, HideField, InputType } from '@nestjs/graphql';
import { Field, InputType } from '@nestjs/graphql';
import { IsObject, IsOptional, IsString } from 'class-validator';
import {
IsBoolean,
IsNotEmpty,
IsNumber,
IsObject,
IsOptional,
IsString,
IsUUID,
Max,
Min,
} from 'class-validator';
import graphqlTypeJson from 'graphql-type-json';
import {
CronTriggerSettings,
@@ -9,12 +19,54 @@ import {
} from 'twenty-shared/application';
import type { JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type';
import { CreateDefaultLogicFunctionInput } from 'src/engine/metadata-modules/logic-function/dtos/create-default-logic-function.input';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@InputType()
export class CreateLogicFunctionInput extends CreateDefaultLogicFunctionInput {
@HideField()
checksum: string;
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: object;
@IsBoolean()
@Field({ nullable: true })
@IsOptional()
isTool?: boolean;
@IsBoolean()
@Field({ nullable: false })
isBuildUpToDate: boolean;
@IsString()
@Field({ nullable: true })
@IsOptional()
checksum?: string;
@IsString()
@Field({ nullable: false })
@@ -1,12 +1,6 @@
import { Field, InputType } from '@nestjs/graphql';
import {
IsBoolean,
IsNotEmpty,
IsObject,
IsOptional,
IsUUID,
} from 'class-validator';
import { IsNotEmpty, IsObject, IsUUID } from 'class-validator';
import graphqlTypeJson from 'graphql-type-json';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@@ -25,12 +19,4 @@ export class ExecuteOneLogicFunctionInput {
})
@IsObject()
payload: JSON;
@Field(() => Boolean, {
description: 'Force rebuild from source before executing',
nullable: true,
})
@IsOptional()
@IsBoolean()
forceRebuild?: boolean;
}
@@ -0,0 +1,19 @@
import { Field, InputType } from '@nestjs/graphql';
import { IsObject, IsString } from 'class-validator';
import graphqlTypeJson from 'graphql-type-json';
@InputType()
export class LogicFunctionSourceInput {
@IsString()
@Field({ nullable: false })
sourceHandlerCode: string;
@Field(() => graphqlTypeJson, { nullable: false })
@IsObject()
toolInputSchema: object;
@IsString()
@Field({ nullable: false })
handlerName: string;
}
@@ -62,10 +62,6 @@ export class LogicFunctionDTO {
@Field()
sourceHandlerPath: string;
@IsString()
@Field()
builtHandlerPath: string;
@IsString()
@Field()
handlerName: string;
@@ -24,9 +24,9 @@ import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/
import type { JsonbProperty } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/jsonb-property.type';
@InputType()
class UpdateLogicFunctionInputUpdates {
class UpdateLogicFunctionFromSourceInputUpdates {
@IsString()
@Field()
@Field({ nullable: true })
@IsOptional()
name?: string;
@@ -42,6 +42,16 @@ class UpdateLogicFunctionInputUpdates {
@IsOptional()
timeoutSeconds?: number;
@IsString()
@Field({ nullable: true })
@IsOptional()
sourceHandlerCode?: string;
@Field(() => graphqlTypeJson, { nullable: true })
@IsObject()
@IsOptional()
toolInputSchema?: object;
@IsString()
@Field({ nullable: true })
@IsOptional()
@@ -52,21 +62,16 @@ class UpdateLogicFunctionInputUpdates {
@IsOptional()
sourceHandlerPath?: string;
@IsString()
@Field({ nullable: true })
@IsOptional()
builtHandlerPath?: string;
@Field(() => graphqlTypeJson, { nullable: true })
@IsObject()
@IsOptional()
toolInputSchema?: object;
@IsBoolean()
@Field({ nullable: true })
@IsOptional()
isTool?: boolean;
@IsBoolean()
@Field({ nullable: true })
@IsOptional()
isBuildUpToDate?: boolean;
@IsString()
@Field({ nullable: true })
@IsOptional()
@@ -89,7 +94,7 @@ class UpdateLogicFunctionInputUpdates {
}
@InputType()
export class UpdateLogicFunctionSourceInput {
export class UpdateLogicFunctionFromSourceInput {
@Field(() => UUIDScalarType, {
description: 'Id of the logic function to update',
})
@@ -97,10 +102,10 @@ export class UpdateLogicFunctionSourceInput {
@IsUUID()
id: string;
@Type(() => UpdateLogicFunctionInputUpdates)
@Type(() => UpdateLogicFunctionFromSourceInputUpdates)
@ValidateNested()
@Field(() => UpdateLogicFunctionInputUpdates, {
@Field(() => UpdateLogicFunctionFromSourceInputUpdates, {
description: 'The logic function updates',
})
update: UpdateLogicFunctionInputUpdates;
update: UpdateLogicFunctionFromSourceInputUpdates;
}
@@ -1,25 +0,0 @@
import { Field, InputType } from '@nestjs/graphql';
import { IsNotEmpty, IsObject, IsUUID } from 'class-validator';
import graphqlTypeJson from 'graphql-type-json';
import type { Sources } from 'twenty-shared/types';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@InputType()
export class UpdateLogicFunctionSourceInput {
@Field(() => UUIDScalarType, {
description: 'The id of the logic function.',
})
@IsNotEmpty()
@IsUUID()
id!: string;
@Field(() => graphqlTypeJson, {
description:
'The source code (Sources) to write. Only updates source files.',
})
@IsObject()
code!: Sources;
}
@@ -24,10 +24,6 @@ export enum LogicFunctionRuntime {
NODE22 = 'nodejs22.x',
}
export const DEFAULT_SOURCE_HANDLER_PATH = 'src/index.ts';
export const DEFAULT_BUILT_HANDLER_PATH = 'src/index.mjs';
export const DEFAULT_HANDLER_NAME = 'main';
@Entity('logicFunction')
@Index('IDX_LOGIC_FUNCTION_ID_DELETED_AT', ['id', 'deletedAt'])
export class LogicFunctionEntity
@@ -40,13 +36,13 @@ export class LogicFunctionEntity
@Column({ nullable: false })
name: string;
@Column({ nullable: false, default: DEFAULT_SOURCE_HANDLER_PATH })
@Column({ nullable: false })
sourceHandlerPath: string;
@Column({ nullable: false, default: DEFAULT_BUILT_HANDLER_PATH })
@Column({ nullable: false })
builtHandlerPath: string;
@Column({ nullable: false, default: DEFAULT_HANDLER_NAME })
@Column({ nullable: false })
handlerName: string;
@Column({ nullable: true, type: 'varchar' })
@@ -68,6 +64,9 @@ export class LogicFunctionEntity
@Column({ nullable: false, default: false })
isTool: boolean;
@Column({ nullable: false, type: 'boolean', default: true })
isBuildUpToDate: boolean;
@Column({ nullable: true, type: 'jsonb' })
cronTriggerSettings: JsonbProperty<CronTriggerSettings> | null;
@@ -3,7 +3,6 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { ApplicationLayerModule } from 'src/engine/core-modules/application-layer/application-layer.module';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
import { AuditModule } from 'src/engine/core-modules/audit/audit.module';
@@ -19,7 +18,7 @@ import { LogicFunctionLayerModule } from 'src/engine/metadata-modules/logic-func
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 { LogicFunctionService } from 'src/engine/metadata-modules/logic-function/services/logic-function.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';
import { SubscriptionsModule } from 'src/engine/subscriptions/subscriptions.module';
@@ -32,7 +31,6 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
TypeOrmModule.forFeature([ApplicationEntity, FeatureFlagEntity]),
ThrottlerModule,
ApplicationModule,
ApplicationLayerModule,
AuditModule,
FeatureFlagModule,
PermissionsModule,
@@ -46,10 +44,10 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
],
providers: [
LogicFunctionMetadataService,
LogicFunctionService,
LogicFunctionFromSourceService,
LogicFunctionResolver,
WorkspaceFlatLogicFunctionMapCacheService,
],
exports: [LogicFunctionMetadataService, LogicFunctionService],
exports: [LogicFunctionMetadataService, LogicFunctionFromSourceService],
})
export class LogicFunctionModule {}
@@ -14,18 +14,19 @@ 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 { CreateDefaultLogicFunctionInput } from 'src/engine/metadata-modules/logic-function/dtos/create-default-logic-function.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 { UpdateLogicFunctionSourceInput } from 'src/engine/metadata-modules/logic-function/dtos/update-logic-function-source.input';
import { LogicFunctionService } from 'src/engine/metadata-modules/logic-function/services/logic-function.service';
import { LogicFunctionFromSourceService } from 'src/engine/metadata-modules/logic-function/services/logic-function-from-source.service';
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()
@@ -33,7 +34,8 @@ import { SubscriptionService } from 'src/engine/subscriptions/subscription.servi
@UseFilters(PreventNestToAutoLogGraphqlErrorsFilter)
export class LogicFunctionResolver {
constructor(
private readonly logicFunctionService: LogicFunctionService,
private readonly logicFunctionFromSourceService: LogicFunctionFromSourceService,
private readonly logicFunctionMetadataService: LogicFunctionMetadataService,
private readonly subscriptionService: SubscriptionService,
) {}
@@ -43,7 +45,10 @@ export class LogicFunctionResolver {
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<LogicFunctionDTO> {
try {
return await this.logicFunctionService.findOne({ id, workspaceId });
return await this.logicFunctionMetadataService.findOne({
id,
workspaceId,
});
} catch (error) {
return logicFunctionGraphQLApiExceptionHandler(error);
}
@@ -54,7 +59,9 @@ export class LogicFunctionResolver {
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<LogicFunctionDTO[]> {
try {
return await this.logicFunctionService.findMany({ workspaceId });
return await this.logicFunctionMetadataService.findMany({
workspaceId,
});
} catch (error) {
return logicFunctionGraphQLApiExceptionHandler(error);
}
@@ -67,8 +74,8 @@ export class LogicFunctionResolver {
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
) {
try {
return await this.logicFunctionService.getAvailablePackages({
id,
return await this.logicFunctionMetadataService.getAvailablePackages({
logicFunctionId: id,
workspaceId,
});
} catch (error) {
@@ -83,7 +90,10 @@ export class LogicFunctionResolver {
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<LogicFunctionDTO> {
try {
return await this.logicFunctionService.deleteOne({ id, workspaceId });
return await this.logicFunctionFromSourceService.deleteOne({
id,
workspaceId,
});
} catch (error) {
return logicFunctionGraphQLApiExceptionHandler(error);
}
@@ -91,12 +101,12 @@ export class LogicFunctionResolver {
@Mutation(() => LogicFunctionDTO)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.WORKFLOWS))
async createDefaultLogicFunction(
@Args('input') input: CreateDefaultLogicFunctionInput,
async createOneLogicFunction(
@Args('input') input: CreateLogicFunctionFromSourceInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<LogicFunctionDTO> {
try {
return await this.logicFunctionService.createDefault({
return await this.logicFunctionFromSourceService.createOne({
input,
workspaceId,
});
@@ -108,14 +118,13 @@ export class LogicFunctionResolver {
@Mutation(() => LogicFunctionExecutionResultDTO)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.WORKFLOWS))
async executeOneLogicFunction(
@Args('input') { id, payload, forceRebuild }: ExecuteOneLogicFunctionInput,
@Args('input') { id, payload }: ExecuteOneLogicFunctionInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<LogicFunctionExecutionResultDTO> {
try {
return await this.logicFunctionService.executeOne({
return await this.logicFunctionFromSourceService.executeOne({
id,
payload,
forceRebuild,
workspaceId,
});
} catch (error) {
@@ -123,14 +132,14 @@ export class LogicFunctionResolver {
}
}
@Query(() => graphqlTypeJson, { nullable: true })
@Query(() => String, { nullable: true })
@UseGuards(SettingsPermissionGuard(PermissionFlagType.WORKFLOWS))
async getLogicFunctionSourceCode(
@Args('input') { id }: LogicFunctionIdInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
) {
try {
return await this.logicFunctionService.getSourceCode({
return await this.logicFunctionFromSourceService.getSourceCode({
id,
workspaceId,
});
@@ -141,14 +150,14 @@ export class LogicFunctionResolver {
@Mutation(() => Boolean)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.WORKFLOWS))
async updateLogicFunctionSource(
@Args('input') { id, code }: UpdateLogicFunctionSourceInput,
async updateOneLogicFunction(
@Args('input') { id, update }: UpdateLogicFunctionFromSourceInput,
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
): Promise<boolean> {
try {
await this.logicFunctionService.updateLogicFunctionSource({
await this.logicFunctionFromSourceService.updateOne({
id,
code,
update,
workspaceId,
});
@@ -1,93 +1,37 @@
import { Injectable } from '@nestjs/common';
import { Sources } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { join } from 'path';
import { v4 } from 'uuid';
import { ApplicationLayerService } from 'src/engine/core-modules/application-layer/application-layer.service';
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
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 { CreateDefaultLogicFunctionInput } from 'src/engine/metadata-modules/logic-function/dtos/create-default-logic-function.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 { 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 { CreateLogicFunctionFromSourceInput } from 'src/engine/metadata-modules/logic-function/dtos/create-logic-function-from-source.input';
import { SEED_LOGIC_FUNCTION_INPUT_SCHEMA } from 'src/engine/core-modules/logic-function/logic-function-resource/constants/seed-logic-function-input-schema';
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';
@Injectable()
export class LogicFunctionService {
export class LogicFunctionFromSourceService {
constructor(
private readonly logicFunctionExecutorService: LogicFunctionExecutorService,
private readonly logicFunctionMetadataService: LogicFunctionMetadataService,
private readonly logicFunctionResourceService: LogicFunctionResourceService,
private readonly applicationService: ApplicationService,
private readonly applicationLayerService: ApplicationLayerService,
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
) {}
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 getAvailablePackages({
id,
workspaceId,
}: {
id: string;
workspaceId: string;
}) {
return this.applicationLayerService.getAvailablePackages({
logicFunctionId: id,
workspaceId,
});
}
async deleteOne({
id,
workspaceId,
@@ -104,11 +48,27 @@ export class LogicFunctionService {
return fromFlatLogicFunctionToLogicFunctionDto({ flatLogicFunction });
}
async createDefault({
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({
input,
workspaceId,
}: {
input: CreateDefaultLogicFunctionInput;
input: CreateLogicFunctionFromSourceInput;
workspaceId: string;
}): Promise<LogicFunctionDTO> {
const { workspaceCustomFlatApplication } =
@@ -118,9 +78,40 @@ export class LogicFunctionService {
const logicFunctionId = input.id ?? v4();
const { sourceHandlerPath, builtHandlerPath, handlerName, checksum } =
const { sourceHandlerPath, builtHandlerPath } =
this.getHandlerPaths(logicFunctionId);
if (input.source) {
await this.logicFunctionResourceService.uploadSourceFile({
sourceHandlerPath,
sourceHandlerCode: input.source.sourceHandlerCode,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
workspaceId,
});
const flatLogicFunction =
await this.logicFunctionMetadataService.createOne({
input: {
...input,
handlerName: input.source.handlerName,
toolInputSchema: input.source.toolInputSchema,
sourceHandlerPath,
builtHandlerPath,
id: logicFunctionId,
isBuildUpToDate: false,
},
workspaceId,
ownerFlatApplication: workspaceCustomFlatApplication,
});
return fromFlatLogicFunctionToLogicFunctionDto({ flatLogicFunction });
}
const { handlerName, checksum } =
await this.logicFunctionResourceService.seedSourceFiles({
sourceSubfolder: logicFunctionId,
sourceHandlerPath,
builtHandlerPath,
workspaceId,
applicationUniversalIdentifier:
workspaceCustomFlatApplication.universalIdentifier,
@@ -135,6 +126,8 @@ export class LogicFunctionService {
builtHandlerPath,
handlerName,
checksum,
toolInputSchema: SEED_LOGIC_FUNCTION_INPUT_SCHEMA,
isBuildUpToDate: true,
},
workspaceId,
ownerFlatApplication: workspaceCustomFlatApplication,
@@ -144,35 +137,84 @@ export class LogicFunctionService {
return fromFlatLogicFunctionToLogicFunctionDto({ flatLogicFunction });
}
// TODO: remove forceRebuild parameter and add a column called shouldRebuild or isBuiltUpToDate
async updateOne({
id,
update,
workspaceId,
}: {
id: string;
update: UpdateLogicFunctionFromSourceInput['update'];
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,
workspaceId,
});
formattedUpdate = {
...formattedUpdate,
isBuildUpToDate: false,
};
}
await this.logicFunctionMetadataService.updateOne({
id,
update: formattedUpdate,
workspaceId,
});
}
async buildOneFromSource({
id,
workspaceId,
}: {
id: string;
workspaceId: string;
}): Promise<void> {
const { flatLogicFunction, applicationUniversalIdentifier } =
await this.getLogicFunctionContext({ id, workspaceId });
const { checksum } =
await this.logicFunctionResourceService.buildFromSourceFile({
workspaceId,
applicationUniversalIdentifier,
sourceHandlerPath: flatLogicFunction.sourceHandlerPath,
builtHandlerPath: flatLogicFunction.builtHandlerPath,
});
await this.logicFunctionMetadataService.updateOne({
id,
update: { checksum, isBuildUpToDate: true },
workspaceId,
});
}
async executeOne({
id,
payload,
forceRebuild,
workspaceId,
}: {
id: string;
payload: object;
forceRebuild?: boolean;
workspaceId: string;
}): Promise<LogicFunctionExecutionResultDTO> {
if (forceRebuild) {
const { flatLogicFunction, applicationUniversalIdentifier } =
await this.getLogicFunctionContext({ id, workspaceId });
const { flatLogicFunction } = await this.getLogicFunctionContext({
id,
workspaceId,
});
const { checksum } =
await this.logicFunctionResourceService.buildFromSource({
workspaceId,
applicationUniversalIdentifier,
sourceHandlerPath: flatLogicFunction.sourceHandlerPath,
builtHandlerPath: flatLogicFunction.builtHandlerPath,
});
await this.logicFunctionMetadataService.updateChecksum({
id,
checksum,
workspaceId,
});
if (!flatLogicFunction.isBuildUpToDate) {
await this.buildOneFromSource({ workspaceId, id });
}
const result = await this.logicFunctionExecutorService.execute({
@@ -204,37 +246,17 @@ export class LogicFunctionService {
}: {
id: string;
workspaceId: string;
}): Promise<Sources | null> {
}): Promise<string | null> {
const { flatLogicFunction, applicationUniversalIdentifier } =
await this.getLogicFunctionContext({ id, workspaceId });
return this.logicFunctionResourceService.getSourceCode({
return this.logicFunctionResourceService.getSourceFile({
workspaceId,
applicationUniversalIdentifier,
sourceHandlerPath: flatLogicFunction.sourceHandlerPath,
});
}
async updateLogicFunctionSource({
id,
code,
workspaceId,
}: {
id: string;
code: Sources;
workspaceId: string;
}): Promise<void> {
const { flatLogicFunction, applicationUniversalIdentifier } =
await this.getLogicFunctionContext({ id, workspaceId });
await this.logicFunctionResourceService.updateSourceFiles({
workspaceId,
applicationUniversalIdentifier,
sourceHandlerPath: flatLogicFunction.sourceHandlerPath,
code,
});
}
private async getLogicFunctionContext({
id,
workspaceId,
@@ -7,18 +7,20 @@ import { type FlatApplication } from 'src/engine/core-modules/application/types/
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 type { CreateLogicFunctionInput } from 'src/engine/metadata-modules/logic-function/dtos/create-logic-function.input';
import type { UpdateLogicFunctionSourceInput } from 'src/engine/metadata-modules/logic-function/dtos/update-logic-function.input';
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-input-to-flat-logic-function.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 {
@@ -28,12 +30,59 @@ export class LogicFunctionMetadataService {
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: Omit<CreateLogicFunctionInput, 'applicationId'>;
input: CreateLogicFunction;
ownerFlatApplication: FlatApplication;
workspaceId: string;
}) {
@@ -86,7 +135,7 @@ export class LogicFunctionMetadataService {
ownerFlatApplication,
}: {
id: string;
update: UpdateLogicFunctionSourceInput['update'];
update: Omit<UpdateLogicFunctionFromSourceInput['update'], 'source'>;
workspaceId: string;
ownerFlatApplication?: FlatApplication;
}) {
@@ -142,64 +191,6 @@ export class LogicFunctionMetadataService {
);
}
async updateChecksum({
id,
checksum,
workspaceId,
}: {
id: string;
checksum: string;
workspaceId: string;
}) {
const resolvedOwnerFlatApplication = (
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspaceId },
)
).workspaceCustomFlatApplication;
const { flatLogicFunctionMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
{
workspaceId,
flatMapsKeys: ['flatLogicFunctionMaps'],
},
);
const flatLogicFunction = findFlatLogicFunctionOrThrow({
id,
flatLogicFunctionMaps,
});
const optimisticallyUpdatedFlatLogicFunction = {
...flatLogicFunction,
checksum,
};
const validateAndBuildResult =
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
{
allFlatEntityOperationByMetadataName: {
logicFunction: {
flatEntityToCreate: [],
flatEntityToDelete: [],
flatEntityToUpdate: [optimisticallyUpdatedFlatLogicFunction],
},
},
workspaceId,
isSystemBuild: false,
applicationUniversalIdentifier:
resolvedOwnerFlatApplication.universalIdentifier,
},
);
if (isDefined(validateAndBuildResult)) {
throw new WorkspaceMigrationBuilderException(
validateAndBuildResult,
'Multiple validation errors occurred while updating logic function checksum',
);
}
}
private async getFlatLogicFunctionAfterUpdate(
id: string,
workspaceId: string,
@@ -285,4 +276,40 @@ export class LogicFunctionMetadataService {
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 ?? {};
}
}
@@ -1,8 +1,4 @@
import { type Sources } from 'twenty-shared/types';
import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
import { type LogicFunctionEntity } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
export type FlatLogicFunction = FlatEntityFrom<LogicFunctionEntity> & {
code?: Sources;
};
export type FlatLogicFunction = FlatEntityFrom<LogicFunctionEntity>;
@@ -1,15 +1,12 @@
import { v4 } from 'uuid';
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
import { type CreateLogicFunctionInput } from 'src/engine/metadata-modules/logic-function/dtos/create-logic-function.input';
import {
DEFAULT_HANDLER_NAME,
LogicFunctionRuntime,
} from 'src/engine/metadata-modules/logic-function/logic-function.entity';
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: Omit<CreateLogicFunctionInput, 'applicationId'>;
createLogicFunctionInput: CreateLogicFunction;
workspaceId: string;
ownerFlatApplication: FlatApplication;
};
@@ -22,14 +19,9 @@ export const fromCreateLogicFunctionInputToFlatLogicFunction = ({
const id = rawCreateLogicFunctionInput.id ?? v4();
const currentDate = new Date();
const sourceHandlerPath = rawCreateLogicFunctionInput.sourceHandlerPath;
const builtHandlerPath = rawCreateLogicFunctionInput.builtHandlerPath;
const universalIdentifier =
rawCreateLogicFunctionInput.universalIdentifier ?? v4();
const checksum = rawCreateLogicFunctionInput.checksum;
return {
id,
cronTriggerSettings:
@@ -40,10 +32,9 @@ export const fromCreateLogicFunctionInputToFlatLogicFunction = ({
rawCreateLogicFunctionInput.httpRouteTriggerSettings ?? null,
name: rawCreateLogicFunctionInput.name,
description: rawCreateLogicFunctionInput.description ?? null,
sourceHandlerPath,
handlerName:
rawCreateLogicFunctionInput.handlerName ?? DEFAULT_HANDLER_NAME,
builtHandlerPath,
sourceHandlerPath: rawCreateLogicFunctionInput.sourceHandlerPath,
handlerName: rawCreateLogicFunctionInput.handlerName,
builtHandlerPath: rawCreateLogicFunctionInput.builtHandlerPath,
universalIdentifier,
createdAt: currentDate.toISOString(),
updatedAt: currentDate.toISOString(),
@@ -52,9 +43,10 @@ export const fromCreateLogicFunctionInputToFlatLogicFunction = ({
runtime: LogicFunctionRuntime.NODE22,
timeoutSeconds: rawCreateLogicFunctionInput.timeoutSeconds ?? 300,
workspaceId,
checksum,
checksum: rawCreateLogicFunctionInput.checksum ?? null,
toolInputSchema: rawCreateLogicFunctionInput.toolInputSchema ?? null,
isTool: rawCreateLogicFunctionInput?.isTool ?? false,
isBuildUpToDate: rawCreateLogicFunctionInput.isBuildUpToDate,
applicationUniversalIdentifier: ownerFlatApplication.universalIdentifier,
};
};
@@ -14,7 +14,6 @@ export const fromFlatLogicFunctionToLogicFunctionDto = ({
runtime: flatLogicFunction.runtime,
timeoutSeconds: flatLogicFunction.timeoutSeconds,
sourceHandlerPath: flatLogicFunction.sourceHandlerPath,
builtHandlerPath: flatLogicFunction.builtHandlerPath,
handlerName: flatLogicFunction.handlerName,
toolInputSchema: flatLogicFunction.toolInputSchema ?? undefined,
isTool: flatLogicFunction.isTool,
@@ -2,16 +2,16 @@ import { trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties } from 'tw
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 UpdateLogicFunctionSourceInput } from 'src/engine/metadata-modules/logic-function/dtos/update-logic-function.input';
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: UpdateLogicFunctionSourceInput;
updateLogicFunctionInput: UpdateLogicFunctionFromSourceInput;
flatLogicFunctionMaps: MetadataFlatEntityMaps<'logicFunction'>;
}): FlatLogicFunction => {
const { id: logicFunctionToUpdateId } =
@@ -0,0 +1,5 @@
export const getLogicFunctionSubfolderForFromSource = (
logicFunctionId: string,
) => {
return logicFunctionId;
};