From a68e05682baace05ea44c9589aeadcf2f65c02cd Mon Sep 17 00:00:00 2001 From: martmull Date: Thu, 22 Jan 2026 16:04:29 +0100 Subject: [PATCH] Serverless built updates (#17351) follow up of https://github.com/twentyhq/twenty/pull/17317 --- .../src/generated-metadata/graphql.ts | 27 ++--- .../twenty-front/src/generated/graphql.ts | 6 +- .../fragments/serverlessFunctionFragment.ts | 3 +- .../useServerlessFunctionUpdateFormState.ts | 3 +- .../SettingsServerlessFunctionDetail.tsx | 6 +- ...00-renameHandlerPathToSourceHandlerPath.ts | 19 +++ .../application/application-sync.service.ts | 4 +- .../build-and-upload-serverless-function.ts | 86 -------------- .../utils/is-serverless-function-built.ts | 23 ---- .../function-build/function-build.module.ts | 9 ++ .../function-build/function-build.service.ts | 109 ++++++++++++++++++ ...s-function-editable-properties.constant.ts | 2 +- .../dtos/create-serverless-function.input.ts | 2 +- .../dtos/serverless-function.dto.ts | 2 +- .../dtos/update-serverless-function.input.ts | 2 +- .../serverless-function.entity.ts | 6 +- .../serverless-function.module.ts | 2 + .../serverless-function.service.ts | 10 +- ...-input-to-flat-serverless-function.util.ts | 7 +- ...unction-to-serverless-function-dto.util.ts | 2 +- ...verless-function-action-handler.service.ts | 10 +- ...verless-function-action-handler.service.ts | 6 +- ...migration-runner-action-handlers.module.ts | 3 +- ...-step-operations.workspace-service.spec.ts | 4 +- 24 files changed, 194 insertions(+), 159 deletions(-) create mode 100644 packages/twenty-server/src/database/typeorm/core/migrations/common/1769091641000-renameHandlerPathToSourceHandlerPath.ts delete mode 100644 packages/twenty-server/src/engine/core-modules/serverless/drivers/utils/build-and-upload-serverless-function.ts delete mode 100644 packages/twenty-server/src/engine/core-modules/serverless/drivers/utils/is-serverless-function-built.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/function-build/function-build.module.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/function-build/function-build.service.ts diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 547d6d2078e..af8b0d10182 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -1060,9 +1060,9 @@ export type CreateServerlessFunctionInput = { code?: InputMaybe; description?: InputMaybe; handlerName?: InputMaybe; - handlerPath?: InputMaybe; isTool?: InputMaybe; name: Scalars['String']; + sourceHandlerPath?: InputMaybe; timeoutSeconds?: InputMaybe; toolInputSchema?: InputMaybe; }; @@ -4364,7 +4364,6 @@ export type ServerlessFunction = { databaseEventTriggers?: Maybe>; description?: Maybe; handlerName: Scalars['String']; - handlerPath: Scalars['String']; id: Scalars['UUID']; isTool: Scalars['Boolean']; latestVersion?: Maybe; @@ -4372,6 +4371,7 @@ export type ServerlessFunction = { publishedVersions: Array; routeTriggers?: Maybe>; runtime: Scalars['String']; + sourceHandlerPath: Scalars['String']; timeoutSeconds: Scalars['Float']; toolInputSchema?: Maybe; universalIdentifier?: Maybe; @@ -4949,9 +4949,9 @@ export type UpdateServerlessFunctionInputUpdates = { code: Scalars['JSON']; description?: InputMaybe; handlerName?: InputMaybe; - handlerPath?: InputMaybe; isTool?: InputMaybe; name: Scalars['String']; + sourceHandlerPath?: InputMaybe; timeoutSeconds?: InputMaybe; toolInputSchema?: InputMaybe; }; @@ -5762,7 +5762,7 @@ export type UpdateOneApplicationVariableMutationVariables = Exact<{ export type UpdateOneApplicationVariableMutation = { __typename?: 'Mutation', updateOneApplicationVariable: boolean }; -export type ApplicationFieldsFragment = { __typename?: 'Application', id: string, name: string, description: string, version: string, universalIdentifier: string, canBeUninstalled: boolean, applicationVariables: Array<{ __typename?: 'ApplicationVariable', id: string, key: string, value: string, description: string, isSecret: boolean }>, agents: Array<{ __typename?: 'Agent', id: string, name: string, label: string, description?: string | null, icon?: string | null, prompt: string, modelId: string, responseFormat?: any | null, roleId?: string | null, isCustom: boolean, modelConfiguration?: any | null, evaluationInputs: Array, applicationId?: string | null, createdAt: string, updatedAt: string }>, objects: Array<{ __typename?: 'Object', id: string, nameSingular: string, namePlural: string, labelSingular: string, labelPlural: string, description?: string | null, icon?: string | null, isCustom: boolean, isRemote: boolean, isActive: boolean, isSystem: boolean, isUIReadOnly: boolean, createdAt: string, updatedAt: string, labelIdentifierFieldMetadataId?: string | null, imageIdentifierFieldMetadataId?: string | null, applicationId: string, shortcut?: string | null, isLabelSyncedWithName: boolean, isSearchable: boolean, duplicateCriteria?: Array> | null, indexMetadataList: Array<{ __typename?: 'Index', id: string, createdAt: string, updatedAt: string, name: string, indexWhereClause?: string | null, indexType: IndexType, isUnique: boolean, isCustom?: boolean | null, indexFieldMetadataList: Array<{ __typename?: 'IndexField', id: string, fieldMetadataId: string, createdAt: string, updatedAt: string, order: number }> }>, fieldsList: Array<{ __typename?: 'Field', id: string, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isSystem?: boolean | null, isUIReadOnly?: boolean | null, isNullable?: boolean | null, isUnique?: boolean | null, createdAt: string, updatedAt: string, defaultValue?: any | null, options?: any | null, settings?: any | null, isLabelSyncedWithName?: boolean | null, morphId?: string | null, applicationId: string, relation?: { __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } } | null, morphRelations?: Array<{ __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } }> | null }> }>, serverlessFunctions: Array<{ __typename?: 'ServerlessFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, publishedVersions: Array, handlerPath: string, handlerName: string, toolInputSchema?: any | null, isTool: boolean, applicationId?: string | null, createdAt: string, updatedAt: string, cronTriggers?: Array<{ __typename?: 'CronTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, databaseEventTriggers?: Array<{ __typename?: 'DatabaseEventTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, routeTriggers?: Array<{ __typename?: 'RouteTrigger', id: string, path: string, isAuthRequired: boolean, httpMethod: HttpMethod, forwardedRequestHeaders: Array, createdAt: string, updatedAt: string }> | null }> }; +export type ApplicationFieldsFragment = { __typename?: 'Application', id: string, name: string, description: string, version: string, universalIdentifier: string, canBeUninstalled: boolean, applicationVariables: Array<{ __typename?: 'ApplicationVariable', id: string, key: string, value: string, description: string, isSecret: boolean }>, agents: Array<{ __typename?: 'Agent', id: string, name: string, label: string, description?: string | null, icon?: string | null, prompt: string, modelId: string, responseFormat?: any | null, roleId?: string | null, isCustom: boolean, modelConfiguration?: any | null, evaluationInputs: Array, applicationId?: string | null, createdAt: string, updatedAt: string }>, objects: Array<{ __typename?: 'Object', id: string, nameSingular: string, namePlural: string, labelSingular: string, labelPlural: string, description?: string | null, icon?: string | null, isCustom: boolean, isRemote: boolean, isActive: boolean, isSystem: boolean, isUIReadOnly: boolean, createdAt: string, updatedAt: string, labelIdentifierFieldMetadataId?: string | null, imageIdentifierFieldMetadataId?: string | null, applicationId: string, shortcut?: string | null, isLabelSyncedWithName: boolean, isSearchable: boolean, duplicateCriteria?: Array> | null, indexMetadataList: Array<{ __typename?: 'Index', id: string, createdAt: string, updatedAt: string, name: string, indexWhereClause?: string | null, indexType: IndexType, isUnique: boolean, isCustom?: boolean | null, indexFieldMetadataList: Array<{ __typename?: 'IndexField', id: string, fieldMetadataId: string, createdAt: string, updatedAt: string, order: number }> }>, fieldsList: Array<{ __typename?: 'Field', id: string, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isSystem?: boolean | null, isUIReadOnly?: boolean | null, isNullable?: boolean | null, isUnique?: boolean | null, createdAt: string, updatedAt: string, defaultValue?: any | null, options?: any | null, settings?: any | null, isLabelSyncedWithName?: boolean | null, morphId?: string | null, applicationId: string, relation?: { __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } } | null, morphRelations?: Array<{ __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } }> | null }> }>, serverlessFunctions: Array<{ __typename?: 'ServerlessFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, publishedVersions: Array, sourceHandlerPath: string, builtHandlerPath: string, handlerName: string, toolInputSchema?: any | null, isTool: boolean, applicationId?: string | null, createdAt: string, updatedAt: string, cronTriggers?: Array<{ __typename?: 'CronTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, databaseEventTriggers?: Array<{ __typename?: 'DatabaseEventTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, routeTriggers?: Array<{ __typename?: 'RouteTrigger', id: string, path: string, isAuthRequired: boolean, httpMethod: HttpMethod, forwardedRequestHeaders: Array, createdAt: string, updatedAt: string }> | null }> }; export type FindManyApplicationsQueryVariables = Exact<{ [key: string]: never; }>; @@ -5774,7 +5774,7 @@ export type FindOneApplicationQueryVariables = Exact<{ }>; -export type FindOneApplicationQuery = { __typename?: 'Query', findOneApplication: { __typename?: 'Application', id: string, name: string, description: string, version: string, universalIdentifier: string, canBeUninstalled: boolean, applicationVariables: Array<{ __typename?: 'ApplicationVariable', id: string, key: string, value: string, description: string, isSecret: boolean }>, agents: Array<{ __typename?: 'Agent', id: string, name: string, label: string, description?: string | null, icon?: string | null, prompt: string, modelId: string, responseFormat?: any | null, roleId?: string | null, isCustom: boolean, modelConfiguration?: any | null, evaluationInputs: Array, applicationId?: string | null, createdAt: string, updatedAt: string }>, objects: Array<{ __typename?: 'Object', id: string, nameSingular: string, namePlural: string, labelSingular: string, labelPlural: string, description?: string | null, icon?: string | null, isCustom: boolean, isRemote: boolean, isActive: boolean, isSystem: boolean, isUIReadOnly: boolean, createdAt: string, updatedAt: string, labelIdentifierFieldMetadataId?: string | null, imageIdentifierFieldMetadataId?: string | null, applicationId: string, shortcut?: string | null, isLabelSyncedWithName: boolean, isSearchable: boolean, duplicateCriteria?: Array> | null, indexMetadataList: Array<{ __typename?: 'Index', id: string, createdAt: string, updatedAt: string, name: string, indexWhereClause?: string | null, indexType: IndexType, isUnique: boolean, isCustom?: boolean | null, indexFieldMetadataList: Array<{ __typename?: 'IndexField', id: string, fieldMetadataId: string, createdAt: string, updatedAt: string, order: number }> }>, fieldsList: Array<{ __typename?: 'Field', id: string, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isSystem?: boolean | null, isUIReadOnly?: boolean | null, isNullable?: boolean | null, isUnique?: boolean | null, createdAt: string, updatedAt: string, defaultValue?: any | null, options?: any | null, settings?: any | null, isLabelSyncedWithName?: boolean | null, morphId?: string | null, applicationId: string, relation?: { __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } } | null, morphRelations?: Array<{ __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } }> | null }> }>, serverlessFunctions: Array<{ __typename?: 'ServerlessFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, publishedVersions: Array, handlerPath: string, handlerName: string, toolInputSchema?: any | null, isTool: boolean, applicationId?: string | null, createdAt: string, updatedAt: string, cronTriggers?: Array<{ __typename?: 'CronTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, databaseEventTriggers?: Array<{ __typename?: 'DatabaseEventTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, routeTriggers?: Array<{ __typename?: 'RouteTrigger', id: string, path: string, isAuthRequired: boolean, httpMethod: HttpMethod, forwardedRequestHeaders: Array, createdAt: string, updatedAt: string }> | null }> } }; +export type FindOneApplicationQuery = { __typename?: 'Query', findOneApplication: { __typename?: 'Application', id: string, name: string, description: string, version: string, universalIdentifier: string, canBeUninstalled: boolean, applicationVariables: Array<{ __typename?: 'ApplicationVariable', id: string, key: string, value: string, description: string, isSecret: boolean }>, agents: Array<{ __typename?: 'Agent', id: string, name: string, label: string, description?: string | null, icon?: string | null, prompt: string, modelId: string, responseFormat?: any | null, roleId?: string | null, isCustom: boolean, modelConfiguration?: any | null, evaluationInputs: Array, applicationId?: string | null, createdAt: string, updatedAt: string }>, objects: Array<{ __typename?: 'Object', id: string, nameSingular: string, namePlural: string, labelSingular: string, labelPlural: string, description?: string | null, icon?: string | null, isCustom: boolean, isRemote: boolean, isActive: boolean, isSystem: boolean, isUIReadOnly: boolean, createdAt: string, updatedAt: string, labelIdentifierFieldMetadataId?: string | null, imageIdentifierFieldMetadataId?: string | null, applicationId: string, shortcut?: string | null, isLabelSyncedWithName: boolean, isSearchable: boolean, duplicateCriteria?: Array> | null, indexMetadataList: Array<{ __typename?: 'Index', id: string, createdAt: string, updatedAt: string, name: string, indexWhereClause?: string | null, indexType: IndexType, isUnique: boolean, isCustom?: boolean | null, indexFieldMetadataList: Array<{ __typename?: 'IndexField', id: string, fieldMetadataId: string, createdAt: string, updatedAt: string, order: number }> }>, fieldsList: Array<{ __typename?: 'Field', id: string, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isSystem?: boolean | null, isUIReadOnly?: boolean | null, isNullable?: boolean | null, isUnique?: boolean | null, createdAt: string, updatedAt: string, defaultValue?: any | null, options?: any | null, settings?: any | null, isLabelSyncedWithName?: boolean | null, morphId?: string | null, applicationId: string, relation?: { __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } } | null, morphRelations?: Array<{ __typename?: 'Relation', type: RelationType, sourceObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, targetObjectMetadata: { __typename?: 'Object', id: string, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'Field', id: string, name: string }, targetFieldMetadata: { __typename?: 'Field', id: string, name: string } }> | null }> }>, serverlessFunctions: Array<{ __typename?: 'ServerlessFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, publishedVersions: Array, sourceHandlerPath: string, builtHandlerPath: string, handlerName: string, toolInputSchema?: any | null, isTool: boolean, applicationId?: string | null, createdAt: string, updatedAt: string, cronTriggers?: Array<{ __typename?: 'CronTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, databaseEventTriggers?: Array<{ __typename?: 'DatabaseEventTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, routeTriggers?: Array<{ __typename?: 'RouteTrigger', id: string, path: string, isAuthRequired: boolean, httpMethod: HttpMethod, forwardedRequestHeaders: Array, createdAt: string, updatedAt: string }> | null }> } }; export type UploadFileMutationVariables = Exact<{ file: Scalars['Upload']; @@ -6566,21 +6566,21 @@ export type GetSsoIdentityProvidersQueryVariables = Exact<{ [key: string]: never export type GetSsoIdentityProvidersQuery = { __typename?: 'Query', getSSOIdentityProviders: Array<{ __typename?: 'FindAvailableSSOIDPOutput', type: IdentityProviderType, id: string, name: string, issuer: string, status: SsoIdentityProviderStatus }> }; -export type ServerlessFunctionFieldsFragment = { __typename?: 'ServerlessFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, publishedVersions: Array, handlerPath: string, handlerName: string, toolInputSchema?: any | null, isTool: boolean, applicationId?: string | null, createdAt: string, updatedAt: string, cronTriggers?: Array<{ __typename?: 'CronTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, databaseEventTriggers?: Array<{ __typename?: 'DatabaseEventTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, routeTriggers?: Array<{ __typename?: 'RouteTrigger', id: string, path: string, isAuthRequired: boolean, httpMethod: HttpMethod, forwardedRequestHeaders: Array, createdAt: string, updatedAt: string }> | null }; +export type ServerlessFunctionFieldsFragment = { __typename?: 'ServerlessFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, publishedVersions: Array, sourceHandlerPath: string, builtHandlerPath: string, handlerName: string, toolInputSchema?: any | null, isTool: boolean, applicationId?: string | null, createdAt: string, updatedAt: string, cronTriggers?: Array<{ __typename?: 'CronTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, databaseEventTriggers?: Array<{ __typename?: 'DatabaseEventTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, routeTriggers?: Array<{ __typename?: 'RouteTrigger', id: string, path: string, isAuthRequired: boolean, httpMethod: HttpMethod, forwardedRequestHeaders: Array, createdAt: string, updatedAt: string }> | null }; export type CreateOneServerlessFunctionItemMutationVariables = Exact<{ input: CreateServerlessFunctionInput; }>; -export type CreateOneServerlessFunctionItemMutation = { __typename?: 'Mutation', createOneServerlessFunction: { __typename?: 'ServerlessFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, publishedVersions: Array, handlerPath: string, handlerName: string, toolInputSchema?: any | null, isTool: boolean, applicationId?: string | null, createdAt: string, updatedAt: string, cronTriggers?: Array<{ __typename?: 'CronTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, databaseEventTriggers?: Array<{ __typename?: 'DatabaseEventTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, routeTriggers?: Array<{ __typename?: 'RouteTrigger', id: string, path: string, isAuthRequired: boolean, httpMethod: HttpMethod, forwardedRequestHeaders: Array, createdAt: string, updatedAt: string }> | null } }; +export type CreateOneServerlessFunctionItemMutation = { __typename?: 'Mutation', createOneServerlessFunction: { __typename?: 'ServerlessFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, publishedVersions: Array, sourceHandlerPath: string, builtHandlerPath: string, handlerName: string, toolInputSchema?: any | null, isTool: boolean, applicationId?: string | null, createdAt: string, updatedAt: string, cronTriggers?: Array<{ __typename?: 'CronTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, databaseEventTriggers?: Array<{ __typename?: 'DatabaseEventTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, routeTriggers?: Array<{ __typename?: 'RouteTrigger', id: string, path: string, isAuthRequired: boolean, httpMethod: HttpMethod, forwardedRequestHeaders: Array, createdAt: string, updatedAt: string }> | null } }; export type DeleteOneServerlessFunctionMutationVariables = Exact<{ input: ServerlessFunctionIdInput; }>; -export type DeleteOneServerlessFunctionMutation = { __typename?: 'Mutation', deleteOneServerlessFunction: { __typename?: 'ServerlessFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, publishedVersions: Array, handlerPath: string, handlerName: string, toolInputSchema?: any | null, isTool: boolean, applicationId?: string | null, createdAt: string, updatedAt: string, cronTriggers?: Array<{ __typename?: 'CronTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, databaseEventTriggers?: Array<{ __typename?: 'DatabaseEventTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, routeTriggers?: Array<{ __typename?: 'RouteTrigger', id: string, path: string, isAuthRequired: boolean, httpMethod: HttpMethod, forwardedRequestHeaders: Array, createdAt: string, updatedAt: string }> | null } }; +export type DeleteOneServerlessFunctionMutation = { __typename?: 'Mutation', deleteOneServerlessFunction: { __typename?: 'ServerlessFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, publishedVersions: Array, sourceHandlerPath: string, builtHandlerPath: string, handlerName: string, toolInputSchema?: any | null, isTool: boolean, applicationId?: string | null, createdAt: string, updatedAt: string, cronTriggers?: Array<{ __typename?: 'CronTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, databaseEventTriggers?: Array<{ __typename?: 'DatabaseEventTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, routeTriggers?: Array<{ __typename?: 'RouteTrigger', id: string, path: string, isAuthRequired: boolean, httpMethod: HttpMethod, forwardedRequestHeaders: Array, createdAt: string, updatedAt: string }> | null } }; export type ExecuteOneServerlessFunctionMutationVariables = Exact<{ input: ExecuteServerlessFunctionInput; @@ -6594,14 +6594,14 @@ export type PublishOneServerlessFunctionMutationVariables = Exact<{ }>; -export type PublishOneServerlessFunctionMutation = { __typename?: 'Mutation', publishServerlessFunction: { __typename?: 'ServerlessFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, publishedVersions: Array, handlerPath: string, handlerName: string, toolInputSchema?: any | null, isTool: boolean, applicationId?: string | null, createdAt: string, updatedAt: string, cronTriggers?: Array<{ __typename?: 'CronTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, databaseEventTriggers?: Array<{ __typename?: 'DatabaseEventTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, routeTriggers?: Array<{ __typename?: 'RouteTrigger', id: string, path: string, isAuthRequired: boolean, httpMethod: HttpMethod, forwardedRequestHeaders: Array, createdAt: string, updatedAt: string }> | null } }; +export type PublishOneServerlessFunctionMutation = { __typename?: 'Mutation', publishServerlessFunction: { __typename?: 'ServerlessFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, publishedVersions: Array, sourceHandlerPath: string, builtHandlerPath: string, handlerName: string, toolInputSchema?: any | null, isTool: boolean, applicationId?: string | null, createdAt: string, updatedAt: string, cronTriggers?: Array<{ __typename?: 'CronTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, databaseEventTriggers?: Array<{ __typename?: 'DatabaseEventTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, routeTriggers?: Array<{ __typename?: 'RouteTrigger', id: string, path: string, isAuthRequired: boolean, httpMethod: HttpMethod, forwardedRequestHeaders: Array, createdAt: string, updatedAt: string }> | null } }; export type UpdateOneServerlessFunctionMutationVariables = Exact<{ input: UpdateServerlessFunctionInput; }>; -export type UpdateOneServerlessFunctionMutation = { __typename?: 'Mutation', updateOneServerlessFunction: { __typename?: 'ServerlessFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, publishedVersions: Array, handlerPath: string, handlerName: string, toolInputSchema?: any | null, isTool: boolean, applicationId?: string | null, createdAt: string, updatedAt: string, cronTriggers?: Array<{ __typename?: 'CronTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, databaseEventTriggers?: Array<{ __typename?: 'DatabaseEventTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, routeTriggers?: Array<{ __typename?: 'RouteTrigger', id: string, path: string, isAuthRequired: boolean, httpMethod: HttpMethod, forwardedRequestHeaders: Array, createdAt: string, updatedAt: string }> | null } }; +export type UpdateOneServerlessFunctionMutation = { __typename?: 'Mutation', updateOneServerlessFunction: { __typename?: 'ServerlessFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, publishedVersions: Array, sourceHandlerPath: string, builtHandlerPath: string, handlerName: string, toolInputSchema?: any | null, isTool: boolean, applicationId?: string | null, createdAt: string, updatedAt: string, cronTriggers?: Array<{ __typename?: 'CronTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, databaseEventTriggers?: Array<{ __typename?: 'DatabaseEventTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, routeTriggers?: Array<{ __typename?: 'RouteTrigger', id: string, path: string, isAuthRequired: boolean, httpMethod: HttpMethod, forwardedRequestHeaders: Array, createdAt: string, updatedAt: string }> | null } }; export type FindManyAvailablePackagesQueryVariables = Exact<{ input: ServerlessFunctionIdInput; @@ -6613,14 +6613,14 @@ export type FindManyAvailablePackagesQuery = { __typename?: 'Query', getAvailabl export type GetManyServerlessFunctionsQueryVariables = Exact<{ [key: string]: never; }>; -export type GetManyServerlessFunctionsQuery = { __typename?: 'Query', findManyServerlessFunctions: Array<{ __typename?: 'ServerlessFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, publishedVersions: Array, handlerPath: string, handlerName: string, toolInputSchema?: any | null, isTool: boolean, applicationId?: string | null, createdAt: string, updatedAt: string, cronTriggers?: Array<{ __typename?: 'CronTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, databaseEventTriggers?: Array<{ __typename?: 'DatabaseEventTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, routeTriggers?: Array<{ __typename?: 'RouteTrigger', id: string, path: string, isAuthRequired: boolean, httpMethod: HttpMethod, forwardedRequestHeaders: Array, createdAt: string, updatedAt: string }> | null }> }; +export type GetManyServerlessFunctionsQuery = { __typename?: 'Query', findManyServerlessFunctions: Array<{ __typename?: 'ServerlessFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, publishedVersions: Array, sourceHandlerPath: string, builtHandlerPath: string, handlerName: string, toolInputSchema?: any | null, isTool: boolean, applicationId?: string | null, createdAt: string, updatedAt: string, cronTriggers?: Array<{ __typename?: 'CronTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, databaseEventTriggers?: Array<{ __typename?: 'DatabaseEventTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, routeTriggers?: Array<{ __typename?: 'RouteTrigger', id: string, path: string, isAuthRequired: boolean, httpMethod: HttpMethod, forwardedRequestHeaders: Array, createdAt: string, updatedAt: string }> | null }> }; export type GetOneServerlessFunctionQueryVariables = Exact<{ input: ServerlessFunctionIdInput; }>; -export type GetOneServerlessFunctionQuery = { __typename?: 'Query', findOneServerlessFunction: { __typename?: 'ServerlessFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, publishedVersions: Array, handlerPath: string, handlerName: string, toolInputSchema?: any | null, isTool: boolean, applicationId?: string | null, createdAt: string, updatedAt: string, cronTriggers?: Array<{ __typename?: 'CronTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, databaseEventTriggers?: Array<{ __typename?: 'DatabaseEventTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, routeTriggers?: Array<{ __typename?: 'RouteTrigger', id: string, path: string, isAuthRequired: boolean, httpMethod: HttpMethod, forwardedRequestHeaders: Array, createdAt: string, updatedAt: string }> | null } }; +export type GetOneServerlessFunctionQuery = { __typename?: 'Query', findOneServerlessFunction: { __typename?: 'ServerlessFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, latestVersion?: string | null, publishedVersions: Array, sourceHandlerPath: string, builtHandlerPath: string, handlerName: string, toolInputSchema?: any | null, isTool: boolean, applicationId?: string | null, createdAt: string, updatedAt: string, cronTriggers?: Array<{ __typename?: 'CronTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, databaseEventTriggers?: Array<{ __typename?: 'DatabaseEventTrigger', id: string, settings: any, createdAt: string, updatedAt: string }> | null, routeTriggers?: Array<{ __typename?: 'RouteTrigger', id: string, path: string, isAuthRequired: boolean, httpMethod: HttpMethod, forwardedRequestHeaders: Array, createdAt: string, updatedAt: string }> | null } }; export type FindOneServerlessFunctionSourceCodeQueryVariables = Exact<{ input: GetServerlessFunctionSourceCodeInput; @@ -7286,7 +7286,8 @@ export const ServerlessFunctionFieldsFragmentDoc = gql` timeoutSeconds latestVersion publishedVersions - handlerPath + sourceHandlerPath + builtHandlerPath handlerName cronTriggers { id diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index 8999a29ba98..47b6981124d 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -1036,9 +1036,9 @@ export type CreateServerlessFunctionInput = { code?: InputMaybe; description?: InputMaybe; handlerName?: InputMaybe; - handlerPath?: InputMaybe; isTool?: InputMaybe; name: Scalars['String']; + sourceHandlerPath?: InputMaybe; timeoutSeconds?: InputMaybe; toolInputSchema?: InputMaybe; }; @@ -4173,7 +4173,6 @@ export type ServerlessFunction = { databaseEventTriggers?: Maybe>; description?: Maybe; handlerName: Scalars['String']; - handlerPath: Scalars['String']; id: Scalars['UUID']; isTool: Scalars['Boolean']; latestVersion?: Maybe; @@ -4181,6 +4180,7 @@ export type ServerlessFunction = { publishedVersions: Array; routeTriggers?: Maybe>; runtime: Scalars['String']; + sourceHandlerPath: Scalars['String']; timeoutSeconds: Scalars['Float']; toolInputSchema?: Maybe; universalIdentifier?: Maybe; @@ -4734,9 +4734,9 @@ export type UpdateServerlessFunctionInputUpdates = { code: Scalars['JSON']; description?: InputMaybe; handlerName?: InputMaybe; - handlerPath?: InputMaybe; isTool?: InputMaybe; name: Scalars['String']; + sourceHandlerPath?: InputMaybe; timeoutSeconds?: InputMaybe; toolInputSchema?: InputMaybe; }; diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/graphql/fragments/serverlessFunctionFragment.ts b/packages/twenty-front/src/modules/settings/serverless-functions/graphql/fragments/serverlessFunctionFragment.ts index 6d518df7bce..3855dbe294f 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/graphql/fragments/serverlessFunctionFragment.ts +++ b/packages/twenty-front/src/modules/settings/serverless-functions/graphql/fragments/serverlessFunctionFragment.ts @@ -9,7 +9,8 @@ export const SERVERLESS_FUNCTION_FRAGMENT = gql` timeoutSeconds latestVersion publishedVersions - handlerPath + sourceHandlerPath + builtHandlerPath handlerName cronTriggers { id diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState.ts b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState.ts index 70eef3ba93c..58c0e4f79b3 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState.ts +++ b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState.ts @@ -79,7 +79,8 @@ export const useServerlessFunctionUpdateFormState = ({ const flattenedCode = flattenSources(code); const sourceCode = flattenedCode.find( - (flatCode) => flatCode.path === serverlessFunction?.handlerPath, + (flatCode) => + flatCode.path === serverlessFunction?.sourceHandlerPath, ); if (isDefined(sourceCode)) { diff --git a/packages/twenty-front/src/pages/settings/serverless-functions/SettingsServerlessFunctionDetail.tsx b/packages/twenty-front/src/pages/settings/serverless-functions/SettingsServerlessFunctionDetail.tsx index 9f97c71e7b3..684d5b71987 100644 --- a/packages/twenty-front/src/pages/settings/serverless-functions/SettingsServerlessFunctionDetail.tsx +++ b/packages/twenty-front/src/pages/settings/serverless-functions/SettingsServerlessFunctionDetail.tsx @@ -110,7 +110,7 @@ export const SettingsServerlessFunctionDetail = () => { // Parse and save schema if editing the handler file let toolInputSchema: object | null | undefined; - if (filePath === serverlessFunction?.handlerPath) { + if (filePath === serverlessFunction?.sourceHandlerPath) { toolInputSchema = await getToolInputSchemaFromSourceCode(value); } @@ -138,9 +138,9 @@ export const SettingsServerlessFunctionDetail = () => { content: file.content, })) .sort((a, b) => - a.path === serverlessFunction?.handlerPath + a.path === serverlessFunction?.sourceHandlerPath ? -1 - : b.path === serverlessFunction?.handlerPath + : b.path === serverlessFunction?.sourceHandlerPath ? 1 : 0, ); diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/common/1769091641000-renameHandlerPathToSourceHandlerPath.ts b/packages/twenty-server/src/database/typeorm/core/migrations/common/1769091641000-renameHandlerPathToSourceHandlerPath.ts new file mode 100644 index 00000000000..a01cf64db56 --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/common/1769091641000-renameHandlerPathToSourceHandlerPath.ts @@ -0,0 +1,19 @@ +import { type MigrationInterface, type QueryRunner } from 'typeorm'; + +export class RenameHandlerPathToSourceHandlerPath1769091641000 + implements MigrationInterface +{ + name = 'RenameHandlerPathToSourceHandlerPath1769091641000'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."serverlessFunction" RENAME COLUMN "handlerPath" TO "sourceHandlerPath"`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."serverlessFunction" RENAME COLUMN "sourceHandlerPath" TO "handlerPath"`, + ); + } +} 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 baf2703ab66..e45f2afc413 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 @@ -947,7 +947,7 @@ export class ApplicationSyncService { name, code, timeoutSeconds: serverlessFunctionToSync.timeoutSeconds, - handlerPath: serverlessFunctionToSync.sourceHandlerPath, + sourceHandlerPath: serverlessFunctionToSync.sourceHandlerPath, handlerName: serverlessFunctionToSync.handlerName, toolInputSchema: serverlessFunctionToSync.toolInputSchema, isTool: serverlessFunctionToSync.isTool, @@ -991,7 +991,7 @@ export class ApplicationSyncService { code, universalIdentifier: serverlessFunctionToCreate.universalIdentifier, timeoutSeconds: serverlessFunctionToCreate.timeoutSeconds, - handlerPath: serverlessFunctionToCreate.sourceHandlerPath, + sourceHandlerPath: serverlessFunctionToCreate.sourceHandlerPath, handlerName: serverlessFunctionToCreate.handlerName, applicationId, serverlessFunctionLayerId, diff --git a/packages/twenty-server/src/engine/core-modules/serverless/drivers/utils/build-and-upload-serverless-function.ts b/packages/twenty-server/src/engine/core-modules/serverless/drivers/utils/build-and-upload-serverless-function.ts deleted file mode 100644 index d78697262ce..00000000000 --- a/packages/twenty-server/src/engine/core-modules/serverless/drivers/utils/build-and-upload-serverless-function.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { join } from 'path'; -import fs from 'fs/promises'; - -import { build } from 'esbuild'; -import { FileFolder } from 'twenty-shared/types'; - -import { getServerlessFolderOrThrow } from 'src/engine/core-modules/serverless/utils/get-serverless-folder-or-throw.utils'; -import { LambdaBuildDirectoryManager } from 'src/engine/core-modules/serverless/drivers/utils/lambda-build-directory-manager'; -import { type FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type'; -import { type FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; - -const buildServerlessFunctionInMemory = async ({ - sourceTemporaryDir, - handlerPath, - builtHandlerPath, -}: { - sourceTemporaryDir: string; - handlerPath: string; - builtHandlerPath: string; -}): Promise => { - const entryFilePath = join(sourceTemporaryDir, handlerPath); - const builtBundleFilePath = join(sourceTemporaryDir, builtHandlerPath); - - await build({ - entryPoints: [entryFilePath], - outfile: builtBundleFilePath, - platform: 'node', - format: 'esm', - target: 'es2017', - bundle: true, - sourcemap: true, - packages: 'external', - }); - - return builtBundleFilePath; -}; - -export const buildAndUploadServerlessFunction = async ({ - flatServerlessFunction, - version, - fileStorageService, -}: { - flatServerlessFunction: FlatServerlessFunction; - version: string; - fileStorageService: FileStorageService; -}): Promise => { - const sourceFolderPath = getServerlessFolderOrThrow({ - flatServerlessFunction, - version, - fileFolder: FileFolder.ServerlessFunction, - }); - - const builtFolderPath = getServerlessFolderOrThrow({ - flatServerlessFunction, - version, - fileFolder: FileFolder.BuiltFunction, - }); - - const lambdaBuildDirectoryManager = new LambdaBuildDirectoryManager(); - - try { - const { sourceTemporaryDir } = await lambdaBuildDirectoryManager.init(); - - await fileStorageService.download({ - from: { folderPath: sourceFolderPath }, - to: { folderPath: sourceTemporaryDir }, - }); - - const builtBundleFilePath = await buildServerlessFunctionInMemory({ - sourceTemporaryDir, - handlerPath: flatServerlessFunction.handlerPath, - builtHandlerPath: flatServerlessFunction.builtHandlerPath, - }); - - const builtFile = await fs.readFile(builtBundleFilePath, 'utf-8'); - - await fileStorageService.write({ - file: builtFile, - name: flatServerlessFunction.builtHandlerPath, - mimeType: 'application/javascript', - folder: builtFolderPath, - }); - } finally { - await lambdaBuildDirectoryManager.clean(); - } -}; diff --git a/packages/twenty-server/src/engine/core-modules/serverless/drivers/utils/is-serverless-function-built.ts b/packages/twenty-server/src/engine/core-modules/serverless/drivers/utils/is-serverless-function-built.ts deleted file mode 100644 index 4acb919813e..00000000000 --- a/packages/twenty-server/src/engine/core-modules/serverless/drivers/utils/is-serverless-function-built.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type'; -import type { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; -import { getServerlessFolderOrThrow } from 'src/engine/core-modules/serverless/utils/get-serverless-folder-or-throw.utils'; - -export const isServerlessFunctionBuilt = async ({ - flatServerlessFunction, - version, - fileStorageService, -}: { - flatServerlessFunction: FlatServerlessFunction; - version: string; - fileStorageService: FileStorageService; -}) => { - const folderPath = getServerlessFolderOrThrow({ - flatServerlessFunction, - version, - }); - - return await fileStorageService.checkFileExists({ - folderPath, - filename: flatServerlessFunction.builtHandlerPath, - }); -}; diff --git a/packages/twenty-server/src/engine/metadata-modules/function-build/function-build.module.ts b/packages/twenty-server/src/engine/metadata-modules/function-build/function-build.module.ts new file mode 100644 index 00000000000..75374f01ae6 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/function-build/function-build.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; + +import { FunctionBuildService } from 'src/engine/metadata-modules/function-build/function-build.service'; + +@Module({ + providers: [FunctionBuildService], + exports: [FunctionBuildService], +}) +export class FunctionBuildModule {} diff --git a/packages/twenty-server/src/engine/metadata-modules/function-build/function-build.service.ts b/packages/twenty-server/src/engine/metadata-modules/function-build/function-build.service.ts new file mode 100644 index 00000000000..a71b3f3a961 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/function-build/function-build.service.ts @@ -0,0 +1,109 @@ +import { Injectable } from '@nestjs/common'; + +import { join } from 'path'; +import fs from 'fs/promises'; + +import { build } from 'esbuild'; +import { FileFolder } from 'twenty-shared/types'; + +import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; +import { LambdaBuildDirectoryManager } from 'src/engine/core-modules/serverless/drivers/utils/lambda-build-directory-manager'; +import { getServerlessFolderOrThrow } from 'src/engine/core-modules/serverless/utils/get-serverless-folder-or-throw.utils'; +import { type FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type'; + +@Injectable() +export class FunctionBuildService { + constructor(private readonly fileStorageService: FileStorageService) {} + + async isBuilt({ + flatServerlessFunction, + version, + }: { + flatServerlessFunction: FlatServerlessFunction; + version: string; + }): Promise { + const folderPath = getServerlessFolderOrThrow({ + flatServerlessFunction, + version, + }); + + return await this.fileStorageService.checkFileExists({ + folderPath, + filename: flatServerlessFunction.builtHandlerPath, + }); + } + + async buildAndUpload({ + flatServerlessFunction, + version, + }: { + flatServerlessFunction: FlatServerlessFunction; + version: string; + }): Promise { + const sourceFolderPath = getServerlessFolderOrThrow({ + flatServerlessFunction, + version, + fileFolder: FileFolder.ServerlessFunction, + }); + + const builtFolderPath = getServerlessFolderOrThrow({ + flatServerlessFunction, + version, + fileFolder: FileFolder.BuiltFunction, + }); + + const lambdaBuildDirectoryManager = new LambdaBuildDirectoryManager(); + + try { + const { sourceTemporaryDir } = await lambdaBuildDirectoryManager.init(); + + await this.fileStorageService.download({ + from: { folderPath: sourceFolderPath }, + to: { folderPath: sourceTemporaryDir }, + }); + + const builtBundleFilePath = await this.buildInMemory({ + sourceTemporaryDir, + sourceHandlerPath: flatServerlessFunction.sourceHandlerPath, + builtHandlerPath: flatServerlessFunction.builtHandlerPath, + }); + + const builtFile = await fs.readFile(builtBundleFilePath, 'utf-8'); + + await this.fileStorageService.write({ + file: builtFile, + name: flatServerlessFunction.builtHandlerPath, + mimeType: 'application/javascript', + folder: builtFolderPath, + }); + } finally { + await lambdaBuildDirectoryManager.clean(); + } + } + + private async buildInMemory({ + sourceTemporaryDir, + sourceHandlerPath, + builtHandlerPath, + }: { + sourceTemporaryDir: string; + sourceHandlerPath: string; + builtHandlerPath: string; + }): Promise { + const entryFilePath = join(sourceTemporaryDir, sourceHandlerPath); + const builtBundleFilePath = join(sourceTemporaryDir, builtHandlerPath); + + await build({ + entryPoints: [entryFilePath], + outfile: builtBundleFilePath, + platform: 'node', + format: 'esm', + target: 'es2017', + bundle: true, + sourcemap: true, + packages: 'external', + }); + + return builtBundleFilePath; + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/constants/flat-serverless-function-editable-properties.constant.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/constants/flat-serverless-function-editable-properties.constant.ts index 8b2a95584ea..085ec170c81 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/constants/flat-serverless-function-editable-properties.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/constants/flat-serverless-function-editable-properties.constant.ts @@ -6,7 +6,7 @@ export const FLAT_SERVERLESS_FUNCTION_EDITABLE_PROPERTIES = [ 'timeoutSeconds', 'checksum', 'code', - 'handlerPath', + 'sourceHandlerPath', 'handlerName', 'toolInputSchema', 'isTool', diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input.ts index f107a22a159..a954cc01450 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input.ts @@ -54,7 +54,7 @@ export class CreateServerlessFunctionInput { @IsString() @Field({ nullable: true }) @IsOptional() - handlerPath?: string; + sourceHandlerPath?: string; @IsString() @Field({ nullable: true }) diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto.ts index 6c54be479cc..831ace2ea64 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto.ts @@ -63,7 +63,7 @@ export class ServerlessFunctionDTO { @IsString() @Field() - handlerPath: string; + sourceHandlerPath: string; @IsString() @Field() diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input.ts index aabb6c6130f..a16e769dbf9 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input.ts @@ -50,7 +50,7 @@ class UpdateServerlessFunctionInputUpdates { @IsString() @Field({ nullable: true }) @IsOptional() - handlerPath?: string; + sourceHandlerPath?: string; @Field(() => graphqlTypeJson, { nullable: true }) @IsObject() diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.entity.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.entity.ts index dc228fc25ec..05db212ec04 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.entity.ts @@ -26,7 +26,7 @@ export enum ServerlessFunctionRuntime { NODE22 = 'nodejs22.x', } -export const DEFAULT_HANDLER_PATH = 'src/index.ts'; +export const DEFAULT_SOURCE_HANDLER_PATH = 'src/index.ts'; export const DEFAULT_BUILT_HANDLER_PATH = 'index.mjs'; export const DEFAULT_HANDLER_NAME = 'main'; @@ -43,8 +43,8 @@ export class ServerlessFunctionEntity @Column({ nullable: false }) name: string; - @Column({ nullable: false, default: DEFAULT_HANDLER_PATH }) - handlerPath: string; + @Column({ nullable: false, default: DEFAULT_SOURCE_HANDLER_PATH }) + sourceHandlerPath: string; @Column({ nullable: false, default: DEFAULT_BUILT_HANDLER_PATH }) builtHandlerPath: string; diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.module.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.module.ts index 77f1f7a735b..912a992bcef 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.module.ts @@ -26,6 +26,7 @@ import { WorkspaceFlatServerlessFunctionMapCacheService } from 'src/engine/metad import { SubscriptionsModule } from 'src/engine/subscriptions/subscriptions.module'; import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module'; +import { FunctionBuildModule } from 'src/engine/metadata-modules/function-build/function-build.module'; @Module({ imports: [ @@ -45,6 +46,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace PermissionsModule, WorkspaceManyOrAllFlatEntityMapsCacheModule, WorkspaceMigrationModule, + FunctionBuildModule, ServerlessFunctionLayerModule, SubscriptionsModule, WorkspaceCacheModule, diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.service.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.service.ts index 4d6fc09325e..301358dd153 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.service.ts @@ -48,8 +48,7 @@ import { WorkflowVersionStepExceptionCode, } from 'src/modules/workflow/common/exceptions/workflow-version-step.exception'; import { cleanServerUrl } from 'src/utils/clean-server-url'; -import { isServerlessFunctionBuilt } from 'src/engine/core-modules/serverless/drivers/utils/is-serverless-function-built'; -import { buildAndUploadServerlessFunction } from 'src/engine/core-modules/serverless/drivers/utils/build-and-upload-serverless-function'; +import { FunctionBuildService } from 'src/engine/metadata-modules/function-build/function-build.service'; const MIN_TOKEN_EXPIRATION_IN_SECONDS = 5; @@ -58,6 +57,7 @@ export class ServerlessFunctionService { constructor( private readonly fileStorageService: FileStorageService, private readonly serverlessService: ServerlessService, + private readonly functionBuildService: FunctionBuildService, private readonly serverlessFunctionLayerService: ServerlessFunctionLayerService, @InjectRepository(ServerlessFunctionEntity) private readonly serverlessFunctionRepository: Repository, @@ -210,16 +210,14 @@ export class ServerlessFunctionService { // We keep that check to build functions if ( - !(await isServerlessFunctionBuilt({ + !(await this.functionBuildService.isBuilt({ flatServerlessFunction, version, - fileStorageService: this.fileStorageService, })) ) { - await buildAndUploadServerlessFunction({ + await this.functionBuildService.buildAndUpload({ flatServerlessFunction, version, - fileStorageService: this.fileStorageService, }); } diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/from-create-serverless-function-input-to-flat-serverless-function.util.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/from-create-serverless-function-input-to-flat-serverless-function.util.ts index aba4338a060..162cba6c1c8 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/from-create-serverless-function-input-to-flat-serverless-function.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/from-create-serverless-function-input-to-flat-serverless-function.util.ts @@ -6,7 +6,7 @@ import { type CreateServerlessFunctionInput } from 'src/engine/metadata-modules/ import { DEFAULT_BUILT_HANDLER_PATH, DEFAULT_HANDLER_NAME, - DEFAULT_HANDLER_PATH, + DEFAULT_SOURCE_HANDLER_PATH, ServerlessFunctionRuntime, } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; import { type FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type'; @@ -35,8 +35,9 @@ export const fromCreateServerlessFunctionInputToFlatServerlessFunction = ({ id, name: rawCreateServerlessFunctionInput.name, description: rawCreateServerlessFunctionInput.description ?? null, - handlerPath: - rawCreateServerlessFunctionInput.handlerPath ?? DEFAULT_HANDLER_PATH, + sourceHandlerPath: + rawCreateServerlessFunctionInput.sourceHandlerPath ?? + DEFAULT_SOURCE_HANDLER_PATH, handlerName: rawCreateServerlessFunctionInput.handlerName ?? DEFAULT_HANDLER_NAME, builtHandlerPath: diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/from-flat-serverless-function-to-serverless-function-dto.util.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/from-flat-serverless-function-to-serverless-function-dto.util.ts index 8c5f9a93250..30f358b5468 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/from-flat-serverless-function-to-serverless-function-dto.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/from-flat-serverless-function-to-serverless-function-dto.util.ts @@ -43,7 +43,7 @@ export const fromFlatServerlessFunctionToServerlessFunctionDto = ({ runtime: flatServerlessFunction.runtime, timeoutSeconds: flatServerlessFunction.timeoutSeconds, latestVersion: flatServerlessFunction.latestVersion ?? undefined, - handlerPath: flatServerlessFunction.handlerPath, + sourceHandlerPath: flatServerlessFunction.sourceHandlerPath, builtHandlerPath: flatServerlessFunction.builtHandlerPath, handlerName: flatServerlessFunction.handlerName, publishedVersions: flatServerlessFunction.publishedVersions, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/serverless-function/services/create-serverless-function-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/serverless-function/services/create-serverless-function-action-handler.service.ts index 9ab4b81bc57..8cf643c1029 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/serverless-function/services/create-serverless-function-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/serverless-function/services/create-serverless-function-action-handler.service.ts @@ -13,14 +13,17 @@ import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless import { CreateServerlessFunctionAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/serverless-function/types/workspace-migration-serverless-function-action.type'; import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type'; import { FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type'; -import { buildAndUploadServerlessFunction } from 'src/engine/core-modules/serverless/drivers/utils/build-and-upload-serverless-function'; +import { FunctionBuildService } from 'src/engine/metadata-modules/function-build/function-build.service'; @Injectable() export class CreateServerlessFunctionActionHandlerService extends WorkspaceMigrationRunnerActionHandler( 'create', 'serverlessFunction', ) { - constructor(private readonly fileStorageService: FileStorageService) { + constructor( + private readonly fileStorageService: FileStorageService, + private readonly functionBuildService: FunctionBuildService, + ) { super(); } @@ -66,10 +69,9 @@ export class CreateServerlessFunctionActionHandlerService extends WorkspaceMigra }); } } - await buildAndUploadServerlessFunction({ + await this.functionBuildService.buildAndUpload({ flatServerlessFunction: serverlessFunction, version: 'draft', - fileStorageService: this.fileStorageService, }); } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/serverless-function/services/update-serverless-function-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/serverless-function/services/update-serverless-function-action-handler.service.ts index e8decfdd366..e1c20fa27fb 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/serverless-function/services/update-serverless-function-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/serverless-function/services/update-serverless-function-action-handler.service.ts @@ -14,7 +14,7 @@ import { FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-f import { UpdateServerlessFunctionAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/serverless-function/types/workspace-migration-serverless-function-action.type'; import { WorkspaceMigrationActionRunnerArgs } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/types/workspace-migration-action-runner-args.type'; import { fromFlatEntityPropertiesUpdatesToPartialFlatEntity } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/from-flat-entity-properties-updates-to-partial-flat-entity'; -import { buildAndUploadServerlessFunction } from 'src/engine/core-modules/serverless/drivers/utils/build-and-upload-serverless-function'; +import { FunctionBuildService } from 'src/engine/metadata-modules/function-build/function-build.service'; @Injectable() export class UpdateServerlessFunctionActionHandlerService extends WorkspaceMigrationRunnerActionHandler( @@ -24,6 +24,7 @@ export class UpdateServerlessFunctionActionHandlerService extends WorkspaceMigra constructor( private readonly fileStorageService: FileStorageService, private readonly serverlessService: ServerlessService, + private readonly functionBuildService: FunctionBuildService, ) { super(); } @@ -86,10 +87,9 @@ export class UpdateServerlessFunctionActionHandlerService extends WorkspaceMigra await this.fileStorageService.writeFolder(code, fileFolder); - await buildAndUploadServerlessFunction({ + await this.functionBuildService.buildAndUpload({ flatServerlessFunction, version: 'draft', - fileStorageService: this.fileStorageService, }); } } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/workspace-schema-migration-runner-action-handlers.module.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/workspace-schema-migration-runner-action-handlers.module.ts index 8dcf22b7f58..64d8f18865c 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/workspace-schema-migration-runner-action-handlers.module.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/workspace-schema-migration-runner-action-handlers.module.ts @@ -73,9 +73,10 @@ import { UpdateViewGroupActionHandlerService } from 'src/engine/workspace-manage import { CreateViewActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view/services/create-view-action-handler.service'; import { DeleteViewActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view/services/delete-view-action-handler.service'; import { UpdateViewActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/view/services/update-view-action-handler.service'; +import { FunctionBuildModule } from 'src/engine/metadata-modules/function-build/function-build.module'; @Module({ - imports: [WorkspaceSchemaManagerModule], + imports: [WorkspaceSchemaManagerModule, FunctionBuildModule], providers: [ CreateFieldActionHandlerService, UpdateFieldActionHandlerService, diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/__tests__/workflow-version-step-operations.workspace-service.spec.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/__tests__/workflow-version-step-operations.workspace-service.spec.ts index f8e96217663..915d7d107c9 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/__tests__/workflow-version-step-operations.workspace-service.spec.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/__tests__/workflow-version-step-operations.workspace-service.spec.ts @@ -251,7 +251,7 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => { deletedAt: null, runtime: ServerlessFunctionRuntime.NODE22, timeoutSeconds: 30, - handlerPath: 'src/index.ts', + sourceHandlerPath: 'src/index.ts', builtHandlerPath: 'index.mjs', handlerName: 'main', checksum: null, @@ -331,7 +331,7 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => { deletedAt: null, runtime: ServerlessFunctionRuntime.NODE22, timeoutSeconds: 30, - handlerPath: 'src/index.ts', + sourceHandlerPath: 'src/index.ts', builtHandlerPath: 'index.mjs', handlerName: 'main', checksum: null,