diff --git a/packages/twenty-cli/src/commands/app-add.command.ts b/packages/twenty-cli/src/commands/app-add.command.ts index 9961d0488d..591999916c 100644 --- a/packages/twenty-cli/src/commands/app-add.command.ts +++ b/packages/twenty-cli/src/commands/app-add.command.ts @@ -4,13 +4,14 @@ import inquirer from 'inquirer'; import path from 'path'; import { v4 } from 'uuid'; import { resolveAppPath } from '../utils/app-path-resolver'; -import { writeJsoncFile } from '../utils/jsonc-parser'; +import { parseJsoncFile, writeJsoncFile } from '../utils/jsonc-parser'; import { getSchemaUrls } from '../utils/schema-validator'; enum SyncableEntity { AGENT = 'agent', OBJECT = 'object', SERVERLESS_FUNCTION = 'serverlessFunction', + TRIGGER = 'trigger', } const getFolderName = (entity: SyncableEntity) => { @@ -40,6 +41,11 @@ export class AppAddCommand { process.exit(1); } + if (entity === SyncableEntity.TRIGGER) { + await this.addTriggerToServerlessFunction(appPath); + return; + } + const entityName = await this.getEntityName(entity); const entityData = await this.getEntityToCreateData(entity, entityName); @@ -97,6 +103,7 @@ export class AppAddCommand { SyncableEntity.AGENT, SyncableEntity.OBJECT, SyncableEntity.SERVERLESS_FUNCTION, + SyncableEntity.TRIGGER, ], }, ]); @@ -184,4 +191,123 @@ export class AppAddCommand { return entityToCreateData; } + + private async addTriggerToServerlessFunction(appPath: string) { + const serverlessFunctionsDir = path.join(appPath, 'serverlessFunctions'); + + if (!(await fs.pathExists(serverlessFunctionsDir))) { + console.error(chalk.red('No serverless functions found in this app')); + process.exit(1); + } + + const serverlessFunctions = await fs.readdir(serverlessFunctionsDir); + + if (serverlessFunctions.length === 0) { + console.error(chalk.red('No serverless functions found in this app')); + process.exit(1); + } + + const { serverlessFunctionName } = await inquirer.prompt([ + { + type: 'list', + name: 'serverlessFunctionName', + message: 'Select a serverless function to add a trigger to:', + choices: serverlessFunctions, + }, + ]); + + const { triggerType } = await inquirer.prompt([ + { + type: 'list', + name: 'triggerType', + message: 'Select the type of trigger:', + choices: ['databaseEvent', 'cron'], + }, + ]); + + let triggerData: any; + + if (triggerType === 'databaseEvent') { + triggerData = await this.createDatabaseEventTrigger(); + } else if (triggerType === 'cron') { + triggerData = await this.createCronTrigger(); + } + + const manifestPath = path.join( + serverlessFunctionsDir, + serverlessFunctionName, + 'serverlessFunction.manifest.jsonc', + ); + + const manifest = await parseJsoncFile(manifestPath); + + if (!manifest.triggers) { + manifest.triggers = []; + } + + manifest.triggers.push(triggerData); + + await writeJsoncFile(manifestPath, manifest); + + console.log( + chalk.green(`✅ Trigger added successfully to ${serverlessFunctionName}`), + ); + } + + private async createDatabaseEventTrigger() { + const uuid = v4(); + + const { eventName } = await inquirer.prompt([ + { + type: 'input', + name: 'eventName', + message: 'Enter the database event name (e.g., company.created):', + validate: (input) => { + if (input.length === 0) { + return 'Event name is required'; + } + if (!/^[a-zA-Z]+\.(created|updated|deleted)$/.test(input)) { + return 'Event name must be in format: objectName.(created|updated|deleted)'; + } + return true; + }, + }, + ]); + + return { + universalIdentifier: uuid, + type: 'databaseEvent', + eventName, + }; + } + + private async createCronTrigger() { + const uuid = v4(); + + const { schedule } = await inquirer.prompt([ + { + type: 'input', + name: 'schedule', + message: 'Enter the cron schedule (e.g., 0 9 * * * for daily at 9 AM):', + validate: (input) => { + if (input.length === 0) { + return 'Schedule is required'; + } + + const parts = input.trim().split(/\s+/); + + if (parts.length < 5 || parts.length > 6) { + return 'Cron schedule must have 5 or 6 fields (e.g., 0 9 * * *)'; + } + return true; + }, + }, + ]); + + return { + universalIdentifier: uuid, + type: 'cron', + schedule, + }; + } } diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index 8926a366a0..2df2f4e39a 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -719,6 +719,16 @@ export type CreateApprovedAccessDomainInput = { email: Scalars['String']; }; +export type CreateCronTriggerInput = { + serverlessFunctionId: Scalars['String']; + settings: Scalars['JSON']; +}; + +export type CreateDatabaseEventTriggerInput = { + serverlessFunctionId: Scalars['String']; + settings: Scalars['JSON']; +}; + export type CreateDraftFromWorkflowVersionInput = { /** Workflow ID */ workflowId: Scalars['UUID']; @@ -903,6 +913,10 @@ export type CronTrigger = { updatedAt: Scalars['DateTime']; }; +export type CronTriggerIdInput = { + id: Scalars['String']; +}; + export type CursorPaging = { /** Paginate after opaque cursor */ after?: InputMaybe; @@ -933,6 +947,10 @@ export type DatabaseEventTrigger = { updatedAt: Scalars['DateTime']; }; +export type DatabaseEventTriggerIdInput = { + id: Scalars['String']; +}; + export type DateFilter = { eq?: InputMaybe; gt?: InputMaybe; @@ -1577,6 +1595,8 @@ export type Mutation = { createObjectEvent: Analytics; createOneAgent: Agent; createOneAppToken: AppToken; + createOneCronTrigger: CronTrigger; + createOneDatabaseEventTrigger: DatabaseEventTrigger; createOneField: Field; createOneObject: Object; createOneRole: Role; @@ -1603,6 +1623,8 @@ export type Mutation = { deleteEmailingDomain: Scalars['Boolean']; deleteFile: File; deleteOneAgent: Agent; + deleteOneCronTrigger: CronTrigger; + deleteOneDatabaseEventTrigger: DatabaseEventTrigger; deleteOneField: Field; deleteOneObject: Object; deleteOneRole: Scalars['String']; @@ -1680,6 +1702,8 @@ export type Mutation = { updateDatabaseConfigVariable: Scalars['Boolean']; updateLabPublicFeatureFlag: FeatureFlagDto; updateOneAgent: Agent; + updateOneCronTrigger: CronTrigger; + updateOneDatabaseEventTrigger: DatabaseEventTrigger; updateOneField: Field; updateOneObject: Object; updateOneRole: Role; @@ -1847,6 +1871,16 @@ export type MutationCreateOneAgentArgs = { }; +export type MutationCreateOneCronTriggerArgs = { + input: CreateCronTriggerInput; +}; + + +export type MutationCreateOneDatabaseEventTriggerArgs = { + input: CreateDatabaseEventTriggerInput; +}; + + export type MutationCreateOneFieldArgs = { input: CreateOneFieldMetadataInput; }; @@ -1968,6 +2002,16 @@ export type MutationDeleteOneAgentArgs = { }; +export type MutationDeleteOneCronTriggerArgs = { + input: CronTriggerIdInput; +}; + + +export type MutationDeleteOneDatabaseEventTriggerArgs = { + input: DatabaseEventTriggerIdInput; +}; + + export type MutationDeleteOneFieldArgs = { input: DeleteOneFieldInput; }; @@ -2346,6 +2390,16 @@ export type MutationUpdateOneAgentArgs = { }; +export type MutationUpdateOneCronTriggerArgs = { + input: UpdateCronTriggerInput; +}; + + +export type MutationUpdateOneDatabaseEventTriggerArgs = { + input: UpdateDatabaseEventTriggerInput; +}; + + export type MutationUpdateOneFieldArgs = { input: UpdateOneFieldMetadataInput; }; @@ -2821,9 +2875,13 @@ export type Query = { findAgentHandoffTargets: Array; findAgentHandoffs: Array; findManyAgents: Array; + findManyCronTriggers: Array; + findManyDatabaseEventTriggers: Array; findManyPublicDomains: Array; findManyServerlessFunctions: Array; findOneAgent: Agent; + findOneCronTrigger: CronTrigger; + findOneDatabaseEventTrigger: DatabaseEventTrigger; findOneServerlessFunction: ServerlessFunction; findWorkspaceFromInviteHash: Workspace; findWorkspaceInvitations: Array; @@ -2932,6 +2990,16 @@ export type QueryFindOneAgentArgs = { }; +export type QueryFindOneCronTriggerArgs = { + input: CronTriggerIdInput; +}; + + +export type QueryFindOneDatabaseEventTriggerArgs = { + input: DatabaseEventTriggerIdInput; +}; + + export type QueryFindOneServerlessFunctionArgs = { input: ServerlessFunctionIdInput; }; @@ -3605,6 +3673,16 @@ export type UpdateApiKeyDto = { revokedAt?: InputMaybe; }; +export type UpdateCronTriggerInput = { + id: Scalars['String']; + settings: Scalars['JSON']; +}; + +export type UpdateDatabaseEventTriggerInput = { + id: Scalars['String']; + settings: Scalars['JSON']; +}; + export type UpdateFieldInput = { defaultValue?: InputMaybe; description?: InputMaybe; diff --git a/packages/twenty-server/src/engine/core-modules/application/application-sync.service.ts b/packages/twenty-server/src/engine/core-modules/application/application-sync.service.ts index d078ddbd92..13f2a3e49d 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application-sync.service.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application-sync.service.ts @@ -2,26 +2,31 @@ import { Injectable, Logger } from '@nestjs/common'; import { isDefined } from 'twenty-shared/utils'; -import { - AgentManifest, - ObjectManifest, - ServerlessFunctionManifest, -} from 'src/engine/core-modules/application/types/application.types'; -import { ApplicationService } from 'src/engine/core-modules/application/application.service'; -import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/core-modules/common/services/workspace-many-or-all-flat-entity-maps-cache.service.'; -import type { FlatObjectMetadataWithFlatFieldMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-with-flat-field-metadata-maps.type'; -import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service'; -import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; -import { AgentService } from 'src/engine/metadata-modules/agent/agent.service'; +import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; import { ApplicationException, ApplicationExceptionCode, } from 'src/engine/core-modules/application/application.exception'; +import { ApplicationService } from 'src/engine/core-modules/application/application.service'; import { ApplicationInput } from 'src/engine/core-modules/application/dtos/application.input'; +import { + AgentManifest, + ObjectManifest, + ServerlessFunctionManifest, + ServerlessFunctionTriggerManifest, +} from 'src/engine/core-modules/application/types/application.types'; +import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/core-modules/common/services/workspace-many-or-all-flat-entity-maps-cache.service.'; +import { AgentService } from 'src/engine/metadata-modules/agent/agent.service'; +import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; +import { DatabaseEventTriggerV2Service } from 'src/engine/metadata-modules/database-event-trigger/services/database-event-trigger-v2.service'; +import { FlatDatabaseEventTrigger } from 'src/engine/metadata-modules/database-event-trigger/types/flat-database-event-trigger.type'; +import { CronTriggerV2Service } from 'src/engine/metadata-modules/cron-trigger/services/cron-trigger-v2.service'; +import { FlatCronTrigger } from 'src/engine/metadata-modules/cron-trigger/types/flat-cron-trigger.type'; +import type { FlatObjectMetadataWithFlatFieldMaps } from 'src/engine/metadata-modules/flat-object-metadata-maps/types/flat-object-metadata-with-flat-field-metadata-maps.type'; +import { ObjectMetadataServiceV2 } from 'src/engine/metadata-modules/object-metadata/object-metadata-v2.service'; import { ServerlessFunctionLayerService } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.service'; -import { FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type'; import { ServerlessFunctionV2Service } from 'src/engine/metadata-modules/serverless-function/services/serverless-function-v2.service'; -import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity'; +import { FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type'; @Injectable() export class ApplicationSyncService { @@ -35,6 +40,8 @@ export class ApplicationSyncService { private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, private readonly dataSourceService: DataSourceService, private readonly agentService: AgentService, + private readonly databaseEventTriggerV2Service: DatabaseEventTriggerV2Service, + private readonly cronTriggerV2Service: CronTriggerV2Service, ) {} public async synchronizeFromManifest({ @@ -370,6 +377,18 @@ export class ApplicationSyncService { updateServerlessFunctionInput, workspaceId, ); + + await this.syncDatabaseEventTriggersForServerlessFunction({ + serverlessFunctionId: serverlessFunctionToUpdate.id, + triggersToSync: serverlessFunctionToSync.triggers || [], + workspaceId, + }); + + await this.syncCronTriggersForServerlessFunction({ + serverlessFunctionId: serverlessFunctionToUpdate.id, + triggersToSync: serverlessFunctionToSync.triggers || [], + workspaceId, + }); } for (const serverlessFunctionToCreate of serverlessFunctionsToCreate) { @@ -382,8 +401,244 @@ export class ApplicationSyncService { serverlessFunctionLayerId, }; - await this.serverlessFunctionV2Service.createOne( - createServerlessFunctionInput, + const createdServerlessFunction = + await this.serverlessFunctionV2Service.createOne( + createServerlessFunctionInput, + workspaceId, + ); + + await this.syncDatabaseEventTriggersForServerlessFunction({ + serverlessFunctionId: createdServerlessFunction.id, + triggersToSync: serverlessFunctionToCreate.triggers || [], + workspaceId, + }); + + await this.syncCronTriggersForServerlessFunction({ + serverlessFunctionId: createdServerlessFunction.id, + triggersToSync: serverlessFunctionToCreate.triggers || [], + workspaceId, + }); + } + } + + private async syncDatabaseEventTriggersForServerlessFunction({ + serverlessFunctionId, + triggersToSync, + workspaceId, + }: { + serverlessFunctionId: string; + triggersToSync: ServerlessFunctionTriggerManifest[]; + workspaceId: string; + }) { + const databaseEventTriggersToSync = triggersToSync.filter( + (trigger) => trigger.type === 'databaseEvent', + ); + + const { flatDatabaseEventTriggerMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatEntities: ['flatDatabaseEventTriggerMaps'], + }, + ); + + const existingDatabaseEventTriggers = Object.values( + flatDatabaseEventTriggerMaps.byId, + ).filter( + (trigger) => + isDefined(trigger) && + trigger.serverlessFunctionId === serverlessFunctionId, + ) as FlatDatabaseEventTrigger[]; + + const triggersToSyncUniversalIdentifiers = databaseEventTriggersToSync.map( + (trigger) => trigger.universalIdentifier, + ); + + const existingTriggersUniversalIdentifiers = + existingDatabaseEventTriggers.map( + (trigger) => trigger.universalIdentifier, + ); + + const triggersToDelete = existingDatabaseEventTriggers.filter( + (trigger) => + isDefined(trigger.universalIdentifier) && + !triggersToSyncUniversalIdentifiers.includes( + trigger.universalIdentifier, + ), + ); + + const triggersToUpdate = existingDatabaseEventTriggers.filter( + (trigger) => + isDefined(trigger.universalIdentifier) && + triggersToSyncUniversalIdentifiers.includes( + trigger.universalIdentifier, + ), + ); + + const triggersToCreate = databaseEventTriggersToSync.filter( + (triggerToSync) => + !existingTriggersUniversalIdentifiers.includes( + triggerToSync.universalIdentifier, + ), + ); + + for (const triggerToDelete of triggersToDelete) { + await this.databaseEventTriggerV2Service.destroyOne({ + destroyDatabaseEventTriggerInput: { id: triggerToDelete.id }, + workspaceId, + }); + } + + for (const triggerToUpdate of triggersToUpdate) { + const triggerToSync = databaseEventTriggersToSync.find( + (trigger) => + trigger.universalIdentifier === triggerToUpdate.universalIdentifier, + ); + + if (!triggerToSync || triggerToSync.type !== 'databaseEvent') { + throw new ApplicationException( + `Failed to find database event trigger to sync with universalIdentifier ${triggerToUpdate.universalIdentifier}`, + ApplicationExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND, + ); + } + + const updateDatabaseEventTriggerInput = { + id: triggerToUpdate.id, + settings: { + eventName: triggerToSync.eventName, + }, + }; + + await this.databaseEventTriggerV2Service.updateOne( + updateDatabaseEventTriggerInput, + workspaceId, + ); + } + + for (const triggerToCreate of triggersToCreate) { + if (triggerToCreate.type !== 'databaseEvent') { + continue; + } + + const createDatabaseEventTriggerInput = { + settings: { + eventName: triggerToCreate.eventName, + }, + serverlessFunctionId, + }; + + await this.databaseEventTriggerV2Service.createOne( + createDatabaseEventTriggerInput, + workspaceId, + ); + } + } + + private async syncCronTriggersForServerlessFunction({ + serverlessFunctionId, + triggersToSync, + workspaceId, + }: { + serverlessFunctionId: string; + triggersToSync: ServerlessFunctionTriggerManifest[]; + workspaceId: string; + }) { + const cronTriggersToSync = triggersToSync.filter( + (trigger) => trigger.type === 'cron', + ); + + const { flatCronTriggerMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatEntities: ['flatCronTriggerMaps'], + }, + ); + + const existingCronTriggers = Object.values(flatCronTriggerMaps.byId).filter( + (trigger) => + isDefined(trigger) && + trigger.serverlessFunctionId === serverlessFunctionId, + ) as FlatCronTrigger[]; + + const triggersToSyncUniversalIdentifiers = cronTriggersToSync.map( + (trigger) => trigger.universalIdentifier, + ); + + const existingTriggersUniversalIdentifiers = existingCronTriggers.map( + (trigger) => trigger.universalIdentifier, + ); + + const triggersToDelete = existingCronTriggers.filter( + (trigger) => + isDefined(trigger.universalIdentifier) && + !triggersToSyncUniversalIdentifiers.includes( + trigger.universalIdentifier, + ), + ); + + const triggersToUpdate = existingCronTriggers.filter( + (trigger) => + isDefined(trigger.universalIdentifier) && + triggersToSyncUniversalIdentifiers.includes( + trigger.universalIdentifier, + ), + ); + + const triggersToCreate = cronTriggersToSync.filter( + (triggerToSync) => + !existingTriggersUniversalIdentifiers.includes( + triggerToSync.universalIdentifier, + ), + ); + + for (const triggerToDelete of triggersToDelete) { + await this.cronTriggerV2Service.destroyOne({ + destroyCronTriggerInput: { id: triggerToDelete.id }, + workspaceId, + }); + } + + for (const triggerToUpdate of triggersToUpdate) { + const triggerToSync = cronTriggersToSync.find( + (trigger) => + trigger.universalIdentifier === triggerToUpdate.universalIdentifier, + ); + + if (!triggerToSync || triggerToSync.type !== 'cron') { + throw new ApplicationException( + `Failed to find cron trigger to sync with universalIdentifier ${triggerToUpdate.universalIdentifier}`, + ApplicationExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND, + ); + } + + const updateCronTriggerInput = { + id: triggerToUpdate.id, + settings: { + pattern: triggerToSync.schedule, + }, + }; + + await this.cronTriggerV2Service.updateOne( + updateCronTriggerInput, + workspaceId, + ); + } + + for (const triggerToCreate of triggersToCreate) { + if (triggerToCreate.type !== 'cron') { + continue; + } + + const createCronTriggerInput = { + settings: { + pattern: triggerToCreate.schedule, + }, + serverlessFunctionId, + }; + + await this.cronTriggerV2Service.createOne( + createCronTriggerInput, workspaceId, ); } diff --git a/packages/twenty-server/src/engine/core-modules/application/application.module.ts b/packages/twenty-server/src/engine/core-modules/application/application.module.ts index ccab295c8d..f5eeba6d8a 100644 --- a/packages/twenty-server/src/engine/core-modules/application/application.module.ts +++ b/packages/twenty-server/src/engine/core-modules/application/application.module.ts @@ -13,6 +13,8 @@ import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-s import { AgentModule } from 'src/engine/metadata-modules/agent/agent.module'; import { ServerlessFunctionLayerModule } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.module'; import { ServerlessFunctionModule } from 'src/engine/metadata-modules/serverless-function/serverless-function.module'; +import { DatabaseEventTriggerModule } from 'src/engine/metadata-modules/database-event-trigger/database-event-trigger.module'; +import { CronTriggerModule } from 'src/engine/metadata-modules/cron-trigger/cron-trigger.module'; @Module({ imports: [ @@ -23,6 +25,8 @@ import { ServerlessFunctionModule } from 'src/engine/metadata-modules/serverless AgentModule, ServerlessFunctionLayerModule, ServerlessFunctionModule, + DatabaseEventTriggerModule, + CronTriggerModule, ], providers: [ApplicationResolver, ApplicationService, ApplicationSyncService], }) diff --git a/packages/twenty-server/src/engine/core-modules/application/types/application.types.ts b/packages/twenty-server/src/engine/core-modules/application/types/application.types.ts index 4b1eb9b54c..79132322ea 100644 --- a/packages/twenty-server/src/engine/core-modules/application/types/application.types.ts +++ b/packages/twenty-server/src/engine/core-modules/application/types/application.types.ts @@ -32,7 +32,7 @@ export type ServerlessFunctionManifest = { code: ServerlessFunctionCode; }; -export type ServerlessFunctionTriggerManifest = +export type ServerlessFunctionTriggerManifest = ( | { type: 'cron'; schedule: string; @@ -46,7 +46,10 @@ export type ServerlessFunctionTriggerManifest = path: string; method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; isAuthRequired: boolean; - }; + } +) & { + universalIdentifier: string; +}; export type ObjectManifest = { $schema?: string; diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/jobs/serverless-function-trigger.job.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/jobs/serverless-function-trigger.job.ts index f92a788a54..e5b947ce46 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/jobs/serverless-function-trigger.job.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/jobs/serverless-function-trigger.job.ts @@ -1,8 +1,8 @@ -import { Scope } from '@nestjs/common'; +import { Logger, Scope } from '@nestjs/common'; +import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator'; import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; -import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator'; import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service'; export type ServerlessFunctionTriggerJobData = { @@ -16,17 +16,22 @@ export type ServerlessFunctionTriggerJobData = { scope: Scope.REQUEST, }) export class ServerlessFunctionTriggerJob { + private readonly logger = new Logger(ServerlessFunctionTriggerJob.name); + constructor( private readonly serverlessFunctionService: ServerlessFunctionService, ) {} @Process(ServerlessFunctionTriggerJob.name) async handle(data: ServerlessFunctionTriggerJobData) { - await this.serverlessFunctionService.executeOneServerlessFunction( - data.serverlessFunctionId, - data.workspaceId, - data.payload || {}, - 'draft', - ); + const result = + await this.serverlessFunctionService.executeOneServerlessFunction( + data.serverlessFunctionId, + data.workspaceId, + data.payload || {}, + 'draft', + ); + + this.logger.log(result.logs); } } diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/from-update-serverless-function-input-to-flat-serverless-function-to-update-or-throw.util.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/from-update-serverless-function-input-to-flat-serverless-function-to-update-or-throw.util.ts index ff03171921..e0542612b2 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/from-update-serverless-function-input-to-flat-serverless-function-to-update-or-throw.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/from-update-serverless-function-input-to-flat-serverless-function-to-update-or-throw.util.ts @@ -13,8 +13,8 @@ import { ServerlessFunctionExceptionCode, } from 'src/engine/metadata-modules/serverless-function/serverless-function.exception'; import { type FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type'; -import { mergeUpdateInExistingRecord } from 'src/utils/merge-update-in-existing-record.util'; import { serverlessFunctionCreateHash } from 'src/engine/metadata-modules/serverless-function/utils/serverless-function-create-hash.utils'; +import { mergeUpdateInExistingRecord } from 'src/utils/merge-update-in-existing-record.util'; export const fromUpdateServerlessFunctionInputToFlatServerlessFunctionToUpdateOrThrow = ({ @@ -39,15 +39,18 @@ export const fromUpdateServerlessFunctionInputToFlatServerlessFunctionToUpdateOr ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND, ); } - const updatedEditableFieldProperties = extractAndSanitizeObjectStringFields( - { - ...rawUpdateServerlessFunctionInput, - checksum: serverlessFunctionCreateHash( - JSON.stringify(rawUpdateServerlessFunctionInput.code), - ), - }, - FLAT_SERVERLESS_FUNCTION_EDITABLE_PROPERTIES, - ); + const updatedEditableFieldProperties = { + ...extractAndSanitizeObjectStringFields( + { + ...rawUpdateServerlessFunctionInput, + checksum: serverlessFunctionCreateHash( + JSON.stringify(rawUpdateServerlessFunctionInput.code), + ), + }, + FLAT_SERVERLESS_FUNCTION_EDITABLE_PROPERTIES, + ), + code: rawUpdateServerlessFunctionInput.code, + }; return mergeUpdateInExistingRecord({ existing: existingFlatServerlessFunctionToUpdate,