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 8bd046ca72..136a356672 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 @@ -177,6 +177,7 @@ export class ApplicationSyncService { packageJson, yarnLock, }, + workspaceId, ); } @@ -184,6 +185,7 @@ export class ApplicationSyncService { { applicationVariables: manifest.application.applicationVariables, applicationId: application.id, + workspaceId, }, ); diff --git a/packages/twenty-server/src/engine/core-modules/applicationVariable/application-variable.module.ts b/packages/twenty-server/src/engine/core-modules/applicationVariable/application-variable.module.ts index e00580d845..979d050c28 100644 --- a/packages/twenty-server/src/engine/core-modules/applicationVariable/application-variable.module.ts +++ b/packages/twenty-server/src/engine/core-modules/applicationVariable/application-variable.module.ts @@ -1,21 +1,30 @@ import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm'; import { ApplicationVariableEntity } from 'src/engine/core-modules/applicationVariable/application-variable.entity'; -import { ApplicationVariableEntityService } from 'src/engine/core-modules/applicationVariable/application-variable.service'; import { ApplicationVariableEntityResolver } from 'src/engine/core-modules/applicationVariable/application-variable.resolver'; +import { ApplicationVariableEntityService } from 'src/engine/core-modules/applicationVariable/application-variable.service'; +import { WorkspaceApplicationVariableMapCacheService } from 'src/engine/core-modules/applicationVariable/services/workspace-application-variable-map-cache.service'; import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; +import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; @Module({ imports: [ NestjsQueryTypeOrmModule.forFeature([ApplicationVariableEntity]), + TypeOrmModule.forFeature([ApplicationVariableEntity]), PermissionsModule, + WorkspaceCacheModule, ], providers: [ ApplicationVariableEntityService, ApplicationVariableEntityResolver, + WorkspaceApplicationVariableMapCacheService, + ], + exports: [ + ApplicationVariableEntityService, + WorkspaceApplicationVariableMapCacheService, ], - exports: [ApplicationVariableEntityService], }) export class ApplicationVariableEntityModule {} diff --git a/packages/twenty-server/src/engine/core-modules/applicationVariable/application-variable.resolver.ts b/packages/twenty-server/src/engine/core-modules/applicationVariable/application-variable.resolver.ts index 211a27cb2e..84514b7872 100644 --- a/packages/twenty-server/src/engine/core-modules/applicationVariable/application-variable.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/applicationVariable/application-variable.resolver.ts @@ -6,6 +6,8 @@ import { PermissionFlagType } from 'twenty-shared/constants'; import { ApplicationVariableEntityExceptionFilter } from 'src/engine/core-modules/applicationVariable/application-variable-exception-filter'; import { ApplicationVariableEntityService } from 'src/engine/core-modules/applicationVariable/application-variable.service'; import { UpdateApplicationVariableEntityInput } from 'src/engine/core-modules/applicationVariable/dtos/update-application-variable.input'; +import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; +import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity'; import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; @@ -23,8 +25,14 @@ export class ApplicationVariableEntityResolver { @Mutation(() => Boolean) async updateOneApplicationVariable( @Args() { key, value, applicationId }: UpdateApplicationVariableEntityInput, + @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, ) { - await this.applicationVariableService.update({ key, value, applicationId }); + await this.applicationVariableService.update({ + key, + value, + applicationId, + workspaceId, + }); return true; } diff --git a/packages/twenty-server/src/engine/core-modules/applicationVariable/application-variable.service.ts b/packages/twenty-server/src/engine/core-modules/applicationVariable/application-variable.service.ts index f16096a585..addec34de0 100644 --- a/packages/twenty-server/src/engine/core-modules/applicationVariable/application-variable.service.ts +++ b/packages/twenty-server/src/engine/core-modules/applicationVariable/application-variable.service.ts @@ -1,3 +1,4 @@ +import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { isDefined } from 'twenty-shared/utils'; @@ -5,19 +6,24 @@ import { In, Not, Repository } from 'typeorm'; import { ApplicationVariables } from 'twenty-shared/application'; import { ApplicationVariableEntity } from 'src/engine/core-modules/applicationVariable/application-variable.entity'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; +@Injectable() export class ApplicationVariableEntityService { constructor( @InjectRepository(ApplicationVariableEntity) private readonly applicationVariableRepository: Repository, + private readonly workspaceCacheService: WorkspaceCacheService, ) {} async update({ key, value, applicationId, + workspaceId, }: Pick & { applicationId: string; + workspaceId: string; }) { await this.applicationVariableRepository.update( { key, applicationId }, @@ -25,14 +31,20 @@ export class ApplicationVariableEntityService { value, }, ); + + await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [ + 'applicationVariableMaps', + ]); } async upsertManyApplicationVariableEntities({ applicationVariables, applicationId, + workspaceId, }: { applicationVariables?: ApplicationVariables; applicationId: string; + workspaceId: string; }) { if (!isDefined(applicationVariables)) { return; @@ -74,5 +86,9 @@ export class ApplicationVariableEntityService { applicationId, key: Not(In(Object.keys(applicationVariables))), }); + + await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [ + 'applicationVariableMaps', + ]); } } diff --git a/packages/twenty-server/src/engine/core-modules/applicationVariable/services/workspace-application-variable-map-cache.service.ts b/packages/twenty-server/src/engine/core-modules/applicationVariable/services/workspace-application-variable-map-cache.service.ts new file mode 100644 index 0000000000..0e84bbb9d9 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/applicationVariable/services/workspace-application-variable-map-cache.service.ts @@ -0,0 +1,68 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { Repository } from 'typeorm'; +import { isDefined } from 'twenty-shared/utils'; + +import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; + +import { ApplicationVariableEntity } from 'src/engine/core-modules/applicationVariable/application-variable.entity'; +import { type ApplicationVariableCacheMaps } from 'src/engine/core-modules/applicationVariable/types/application-variable-cache-maps.type'; +import { fromApplicationVariableEntityToFlatApplicationVariable } from 'src/engine/core-modules/applicationVariable/utils/from-application-variable-entity-to-flat-application-variable.util'; +import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; + +@Injectable() +@WorkspaceCache('applicationVariableMaps') +export class WorkspaceApplicationVariableMapCacheService extends WorkspaceCacheProvider { + constructor( + @InjectRepository(ApplicationVariableEntity) + private readonly applicationVariableRepository: Repository, + ) { + super(); + } + + async computeForCache( + workspaceId: string, + ): Promise { + const applicationVariableEntities = await this.applicationVariableRepository + .createQueryBuilder('applicationVariable') + .innerJoin('applicationVariable.application', 'application') + .where('application.workspaceId = :workspaceId', { workspaceId }) + .getMany(); + + const applicationVariableMaps: ApplicationVariableCacheMaps = { + byId: {}, + byApplicationId: {}, + }; + + for (const entity of applicationVariableEntities) { + const flatApplicationVariable = + fromApplicationVariableEntityToFlatApplicationVariable(entity); + + applicationVariableMaps.byId[flatApplicationVariable.id] = + flatApplicationVariable; + + if (!isDefined(flatApplicationVariable.applicationId)) { + continue; + } + if ( + !isDefined( + applicationVariableMaps.byApplicationId[ + flatApplicationVariable.applicationId + ], + ) + ) { + applicationVariableMaps.byApplicationId[ + flatApplicationVariable.applicationId + ] = [flatApplicationVariable]; + continue; + } + + applicationVariableMaps.byApplicationId[ + flatApplicationVariable.applicationId + ]?.push(flatApplicationVariable); + } + + return applicationVariableMaps; + } +} diff --git a/packages/twenty-server/src/engine/core-modules/applicationVariable/types/application-variable-cache-maps.type.ts b/packages/twenty-server/src/engine/core-modules/applicationVariable/types/application-variable-cache-maps.type.ts new file mode 100644 index 0000000000..0a25616135 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/applicationVariable/types/application-variable-cache-maps.type.ts @@ -0,0 +1,6 @@ +import { type FlatApplicationVariable } from 'src/engine/core-modules/applicationVariable/types/flat-application-variable.type'; + +export type ApplicationVariableCacheMaps = { + byId: Partial>; + byApplicationId: Partial>; +}; diff --git a/packages/twenty-server/src/engine/core-modules/applicationVariable/types/flat-application-variable.type.ts b/packages/twenty-server/src/engine/core-modules/applicationVariable/types/flat-application-variable.type.ts new file mode 100644 index 0000000000..e01f18d6db --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/applicationVariable/types/flat-application-variable.type.ts @@ -0,0 +1,4 @@ +import { type ApplicationVariableEntity } from 'src/engine/core-modules/applicationVariable/application-variable.entity'; +import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/types/flat-entity.type'; + +export type FlatApplicationVariable = FlatEntityFrom; diff --git a/packages/twenty-server/src/engine/core-modules/applicationVariable/utils/from-application-variable-entity-to-flat-application-variable.util.ts b/packages/twenty-server/src/engine/core-modules/applicationVariable/utils/from-application-variable-entity-to-flat-application-variable.util.ts new file mode 100644 index 0000000000..ca37dacc64 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/applicationVariable/utils/from-application-variable-entity-to-flat-application-variable.util.ts @@ -0,0 +1,15 @@ +import { type ApplicationVariableEntity } from 'src/engine/core-modules/applicationVariable/application-variable.entity'; +import { type FlatApplicationVariable } from 'src/engine/core-modules/applicationVariable/types/flat-application-variable.type'; + +export const fromApplicationVariableEntityToFlatApplicationVariable = ( + entity: ApplicationVariableEntity, +): FlatApplicationVariable => ({ + id: entity.id, + key: entity.key, + value: entity.value, + description: entity.description, + isSecret: entity.isSecret, + applicationId: entity.applicationId, + createdAt: entity.createdAt.toISOString(), + updatedAt: entity.updatedAt.toISOString(), +}); diff --git a/packages/twenty-server/src/engine/core-modules/serverless/drivers/interfaces/serverless-driver.interface.ts b/packages/twenty-server/src/engine/core-modules/serverless/drivers/interfaces/serverless-driver.interface.ts index 9fe554b450..58525855d4 100644 --- a/packages/twenty-server/src/engine/core-modules/serverless/drivers/interfaces/serverless-driver.interface.ts +++ b/packages/twenty-server/src/engine/core-modules/serverless/drivers/interfaces/serverless-driver.interface.ts @@ -1,5 +1,6 @@ +import { type FlatServerlessFunctionLayer } from 'src/engine/metadata-modules/serverless-function-layer/types/flat-serverless-function-layer.type'; import { type ServerlessFunctionExecutionStatus } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-execution-result.dto'; -import { type ServerlessFunctionEntity } 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'; export type ServerlessExecuteError = { errorType: string; @@ -15,16 +16,17 @@ export type ServerlessExecuteResult = { error?: ServerlessExecuteError; }; -// TODO refactor to be using FlatServerlessFunction export interface ServerlessDriver { - delete(serverlessFunction: ServerlessFunctionEntity): Promise; + delete(flatServerlessFunction: FlatServerlessFunction): Promise; execute({ - serverlessFunction, + flatServerlessFunction, + flatServerlessFunctionLayer, payload, version, env, }: { - serverlessFunction: ServerlessFunctionEntity; + flatServerlessFunction: FlatServerlessFunction; + flatServerlessFunctionLayer: FlatServerlessFunctionLayer; payload: object; version: string; env?: Record; diff --git a/packages/twenty-server/src/engine/core-modules/serverless/drivers/lambda.driver.ts b/packages/twenty-server/src/engine/core-modules/serverless/drivers/lambda.driver.ts index 15e803f323..6a1b173b3d 100644 --- a/packages/twenty-server/src/engine/core-modules/serverless/drivers/lambda.driver.ts +++ b/packages/twenty-server/src/engine/core-modules/serverless/drivers/lambda.driver.ts @@ -36,16 +36,15 @@ import { LambdaBuildDirectoryManager, NODE_LAYER_SUBFOLDER, } from 'src/engine/core-modules/serverless/drivers/utils/lambda-build-directory-manager'; -import { getServerlessFolder } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils'; +import { getServerlessFolderOrThrow } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils'; +import { type FlatServerlessFunctionLayer } from 'src/engine/metadata-modules/serverless-function-layer/types/flat-serverless-function-layer.type'; import { ServerlessFunctionExecutionStatus } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-execution-result.dto'; -import { - type ServerlessFunctionEntity, - ServerlessFunctionRuntime, -} from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; +import { ServerlessFunctionRuntime } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; import { ServerlessFunctionException, 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'; const UPDATE_FUNCTION_DURATION_TIMEOUT_IN_SECONDS = 60; const CREDENTIALS_DURATION_IN_SECONDS = 60 * 60; // 1h @@ -126,11 +125,11 @@ export class LambdaDriver implements ServerlessDriver { } private async waitFunctionUpdates( - serverlessFunction: ServerlessFunctionEntity, + flatServerlessFunction: FlatServerlessFunction, maxWaitTime: number = UPDATE_FUNCTION_DURATION_TIMEOUT_IN_SECONDS, ) { const waitParams = { - FunctionName: serverlessFunction.id, + FunctionName: flatServerlessFunction.id, }; await waitUntilFunctionUpdatedV2( @@ -139,14 +138,16 @@ export class LambdaDriver implements ServerlessDriver { ); } - private getLayerName(serverlessFunction: ServerlessFunctionEntity) { - return serverlessFunction.serverlessFunctionLayer.checksum; + private getLayerName( + flatServerlessFunctionLayer: FlatServerlessFunctionLayer, + ) { + return flatServerlessFunctionLayer.checksum; } private async createLayerIfNotExists( - serverlessFunction: ServerlessFunctionEntity, + flatServerlessFunctionLayer: FlatServerlessFunctionLayer, ): Promise { - const layerName = this.getLayerName(serverlessFunction); + const layerName = this.getLayerName(flatServerlessFunctionLayer); const listLayerParams: ListLayerVersionsCommandInput = { LayerName: layerName, @@ -172,7 +173,10 @@ export class LambdaDriver implements ServerlessDriver { NODE_LAYER_SUBFOLDER, ); - await copyAndBuildDependencies(nodeDependenciesFolder, serverlessFunction); + await copyAndBuildDependencies( + nodeDependenciesFolder, + flatServerlessFunctionLayer, + ); await createZipFile(sourceTemporaryDir, lambdaZipPath); @@ -201,11 +205,11 @@ export class LambdaDriver implements ServerlessDriver { } private async getLambdaExecutor( - serverlessFunction: ServerlessFunctionEntity, + flatServerlessFunction: FlatServerlessFunction, ) { try { const getFunctionCommand: GetFunctionCommand = new GetFunctionCommand({ - FunctionName: serverlessFunction.id, + FunctionName: flatServerlessFunction.id, }); return await (await this.getLambdaClient()).send(getFunctionCommand); @@ -216,20 +220,23 @@ export class LambdaDriver implements ServerlessDriver { } } - async delete(serverlessFunction: ServerlessFunctionEntity) { - const lambdaExecutor = await this.getLambdaExecutor(serverlessFunction); + async delete(flatServerlessFunction: FlatServerlessFunction) { + const lambdaExecutor = await this.getLambdaExecutor(flatServerlessFunction); if (isDefined(lambdaExecutor)) { const deleteFunctionCommand = new DeleteFunctionCommand({ - FunctionName: serverlessFunction.id, + FunctionName: flatServerlessFunction.id, }); await (await this.getLambdaClient()).send(deleteFunctionCommand); } } - private async isAlreadyBuilt(serverlessFunction: ServerlessFunctionEntity) { - const lambdaExecutor = await this.getLambdaExecutor(serverlessFunction); + private async isAlreadyBuilt( + flatServerlessFunction: FlatServerlessFunction, + flatServerlessFunctionLayer: FlatServerlessFunctionLayer, + ) { + const lambdaExecutor = await this.getLambdaExecutor(flatServerlessFunction); if (!isDefined(lambdaExecutor)) { return false; @@ -238,28 +245,38 @@ export class LambdaDriver implements ServerlessDriver { const layers = lambdaExecutor.Configuration?.Layers; if (!isDefined(layers) || layers.length !== 1) { - await this.delete(serverlessFunction); + await this.delete(flatServerlessFunction); return false; } - const layerName = this.getLayerName(serverlessFunction); + const layerName = this.getLayerName(flatServerlessFunctionLayer); if (layers[0].Arn?.includes(layerName)) { return true; } - await this.delete(serverlessFunction); + await this.delete(flatServerlessFunction); return false; } - private async build(serverlessFunction: ServerlessFunctionEntity) { - if (await this.isAlreadyBuilt(serverlessFunction)) { + private async build( + flatServerlessFunction: FlatServerlessFunction, + flatServerlessFunctionLayer: FlatServerlessFunctionLayer, + ) { + if ( + await this.isAlreadyBuilt( + flatServerlessFunction, + flatServerlessFunctionLayer, + ) + ) { return; } - const layerArn = await this.createLayerIfNotExists(serverlessFunction); + const layerArn = await this.createLayerIfNotExists( + flatServerlessFunctionLayer, + ); const lambdaBuildDirectoryManager = new LambdaBuildDirectoryManager(); @@ -274,11 +291,11 @@ export class LambdaDriver implements ServerlessDriver { Code: { ZipFile: await fs.readFile(lambdaZipPath), }, - FunctionName: serverlessFunction.id, + FunctionName: flatServerlessFunction.id, Layers: [layerArn], Handler: 'index.handler', Role: this.options.lambdaRole, - Runtime: serverlessFunction.runtime, + Runtime: flatServerlessFunction.runtime, Timeout: 900, // timeout is handled by the serverless function service }; @@ -305,23 +322,25 @@ export class LambdaDriver implements ServerlessDriver { } async execute({ - serverlessFunction, + flatServerlessFunction, + flatServerlessFunctionLayer, payload, version, env, }: { - serverlessFunction: ServerlessFunctionEntity; + flatServerlessFunction: FlatServerlessFunction; + flatServerlessFunctionLayer: FlatServerlessFunctionLayer; payload: object; version: string; env?: Record; }): Promise { - await this.build(serverlessFunction); - await this.waitFunctionUpdates(serverlessFunction); + await this.build(flatServerlessFunction, flatServerlessFunctionLayer); + await this.waitFunctionUpdates(flatServerlessFunction); const startTime = Date.now(); - const folderPath = getServerlessFolder({ - serverlessFunction, + const folderPath = getServerlessFolderOrThrow({ + flatServerlessFunction, version, }); @@ -340,7 +359,7 @@ export class LambdaDriver implements ServerlessDriver { try { builtBundleFilePath = await buildServerlessFunctionInMemory({ sourceTemporaryDir, - handlerPath: serverlessFunction.handlerPath, + handlerPath: flatServerlessFunction.handlerPath, }); } catch (error) { return formatBuildError(error, startTime); @@ -354,11 +373,11 @@ export class LambdaDriver implements ServerlessDriver { params: payload, code: compiledCode, env: env ?? {}, - handlerName: serverlessFunction.handlerName, + handlerName: flatServerlessFunction.handlerName, }; const params: InvokeCommandInput = { - FunctionName: serverlessFunction.id, + FunctionName: flatServerlessFunction.id, Payload: JSON.stringify(executorPayload), LogType: LogType.Tail, }; diff --git a/packages/twenty-server/src/engine/core-modules/serverless/drivers/local.driver.ts b/packages/twenty-server/src/engine/core-modules/serverless/drivers/local.driver.ts index c333e953c3..1a7887045d 100644 --- a/packages/twenty-server/src/engine/core-modules/serverless/drivers/local.driver.ts +++ b/packages/twenty-server/src/engine/core-modules/serverless/drivers/local.driver.ts @@ -14,9 +14,10 @@ import { copyAndBuildDependencies } from 'src/engine/core-modules/serverless/dri import { formatBuildError } from 'src/engine/core-modules/serverless/drivers/utils/format-build-error'; import { ConsoleListener } from 'src/engine/core-modules/serverless/drivers/utils/intercept-console'; import { LambdaBuildDirectoryManager } from 'src/engine/core-modules/serverless/drivers/utils/lambda-build-directory-manager'; -import { getServerlessFolder } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils'; +import { getServerlessFolderOrThrow } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils'; +import { type FlatServerlessFunctionLayer } from 'src/engine/metadata-modules/serverless-function-layer/types/flat-serverless-function-layer.type'; import { ServerlessFunctionExecutionStatus } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-execution-result.dto'; -import { type ServerlessFunctionEntity } 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'; export interface LocalDriverOptions { fileStorageService: FileStorageService; @@ -30,53 +31,55 @@ export class LocalDriver implements ServerlessDriver { } private getInMemoryLayerFolderPath = ( - serverlessFunction: ServerlessFunctionEntity, + flatServerlessFunctionLayer: FlatServerlessFunctionLayer, ) => { - return join( - SERVERLESS_TMPDIR_FOLDER, - serverlessFunction.serverlessFunctionLayer.checksum, - ); + return join(SERVERLESS_TMPDIR_FOLDER, flatServerlessFunctionLayer.checksum); }; private async createLayerIfNotExists( - serverlessFunction: ServerlessFunctionEntity, + flatServerlessFunctionLayer: FlatServerlessFunctionLayer, ) { - const inMemoryLayerFolderPath = - this.getInMemoryLayerFolderPath(serverlessFunction); + const inMemoryLayerFolderPath = this.getInMemoryLayerFolderPath( + flatServerlessFunctionLayer, + ); try { await fs.access(inMemoryLayerFolderPath); } catch { await copyAndBuildDependencies( inMemoryLayerFolderPath, - serverlessFunction, + flatServerlessFunctionLayer, ); } } async delete() {} - private async build(serverlessFunction: ServerlessFunctionEntity) { - await this.createLayerIfNotExists(serverlessFunction); + private async build( + flatServerlessFunctionLayer: FlatServerlessFunctionLayer, + ) { + await this.createLayerIfNotExists(flatServerlessFunctionLayer); } async execute({ - serverlessFunction, + flatServerlessFunction, + flatServerlessFunctionLayer, payload, version, env, }: { - serverlessFunction: ServerlessFunctionEntity; + flatServerlessFunction: FlatServerlessFunction; + flatServerlessFunctionLayer: FlatServerlessFunctionLayer; payload: object; version: string; env?: Record; }): Promise { - await this.build(serverlessFunction); + await this.build(flatServerlessFunctionLayer); const startTime = Date.now(); - const folderPath = getServerlessFolder({ - serverlessFunction, + const folderPath = getServerlessFolderOrThrow({ + flatServerlessFunction, version, }); @@ -95,7 +98,7 @@ export class LocalDriver implements ServerlessDriver { try { builtBundleFilePath = await buildServerlessFunctionInMemory({ sourceTemporaryDir, - handlerPath: serverlessFunction.handlerPath, + handlerPath: flatServerlessFunction.handlerPath, }); } catch (error) { return formatBuildError(error, startTime); @@ -104,7 +107,7 @@ export class LocalDriver implements ServerlessDriver { try { await fs.symlink( join( - this.getInMemoryLayerFolderPath(serverlessFunction), + this.getInMemoryLayerFolderPath(flatServerlessFunctionLayer), 'node_modules', ), join(sourceTemporaryDir, 'node_modules'), @@ -153,7 +156,7 @@ export class LocalDriver implements ServerlessDriver { const runnerPath = await this.writeBootstrapRunner({ dir: sourceTemporaryDir, builtFileAbsPath: builtBundleFilePath, - handlerName: serverlessFunction.handlerName, + handlerName: flatServerlessFunction.handlerName, }); const { ok, result, error, stack, stdout, stderr } = diff --git a/packages/twenty-server/src/engine/core-modules/serverless/drivers/utils/build-env-var.ts b/packages/twenty-server/src/engine/core-modules/serverless/drivers/utils/build-env-var.ts index 09bab79dbd..f9fb7c3443 100644 --- a/packages/twenty-server/src/engine/core-modules/serverless/drivers/utils/build-env-var.ts +++ b/packages/twenty-server/src/engine/core-modules/serverless/drivers/utils/build-env-var.ts @@ -1,12 +1,14 @@ -import { type ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; +import { type FlatApplicationVariable } from 'src/engine/core-modules/applicationVariable/types/flat-application-variable.type'; -export const buildEnvVar = (serverlessFunction: ServerlessFunctionEntity) => { - return (serverlessFunction.application?.applicationVariables ?? []).reduce( - (acc, v) => { - acc[v.key] = String(v.value ?? ''); +export const buildEnvVar = ( + flatApplicationVariables: FlatApplicationVariable[], +): Record => { + return flatApplicationVariables.reduce>( + (acc, flatApplicationVariable) => { + acc[flatApplicationVariable.key] = flatApplicationVariable.value; return acc; }, - {} as Record, + {}, ); }; diff --git a/packages/twenty-server/src/engine/core-modules/serverless/drivers/utils/copy-and-build-dependencies.ts b/packages/twenty-server/src/engine/core-modules/serverless/drivers/utils/copy-and-build-dependencies.ts index 424bd82381..44ca71522a 100644 --- a/packages/twenty-server/src/engine/core-modules/serverless/drivers/utils/copy-and-build-dependencies.ts +++ b/packages/twenty-server/src/engine/core-modules/serverless/drivers/utils/copy-and-build-dependencies.ts @@ -4,21 +4,21 @@ import { join } from 'path'; import { promisify } from 'util'; import { getLayerDependenciesDirName } from 'src/engine/core-modules/serverless/drivers/utils/get-layer-dependencies-dir-name'; -import type { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; +import { type FlatServerlessFunctionLayer } from 'src/engine/metadata-modules/serverless-function-layer/types/flat-serverless-function-layer.type'; const execFilePromise = promisify(execFile); export const copyAndBuildDependencies = async ( buildDirectory: string, - serverlessFunction: ServerlessFunctionEntity, + flatServerlessFunctionLayer: FlatServerlessFunctionLayer, ) => { await fs.mkdir(buildDirectory, { recursive: true, }); - const packageJson = serverlessFunction.serverlessFunctionLayer.packageJson; + const packageJson = flatServerlessFunctionLayer.packageJson; - const yarnLock = serverlessFunction.serverlessFunctionLayer.yarnLock; + const yarnLock = flatServerlessFunctionLayer.yarnLock; await fs.writeFile( join(buildDirectory, 'package.json'), diff --git a/packages/twenty-server/src/engine/core-modules/serverless/serverless.service.ts b/packages/twenty-server/src/engine/core-modules/serverless/serverless.service.ts index a586c6bc3b..d1e0ab1355 100644 --- a/packages/twenty-server/src/engine/core-modules/serverless/serverless.service.ts +++ b/packages/twenty-server/src/engine/core-modules/serverless/serverless.service.ts @@ -6,27 +6,36 @@ import { } from 'src/engine/core-modules/serverless/drivers/interfaces/serverless-driver.interface'; import { SERVERLESS_DRIVER } from 'src/engine/core-modules/serverless/serverless.constants'; -import { type ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; +import { type FlatServerlessFunctionLayer } from 'src/engine/metadata-modules/serverless-function-layer/types/flat-serverless-function-layer.type'; +import { type FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type'; @Injectable() export class ServerlessService implements ServerlessDriver { constructor(@Inject(SERVERLESS_DRIVER) private driver: ServerlessDriver) {} - async delete(serverlessFunction: ServerlessFunctionEntity): Promise { - return this.driver.delete(serverlessFunction); + async delete(flatServerlessFunction: FlatServerlessFunction): Promise { + return this.driver.delete(flatServerlessFunction); } async execute({ - serverlessFunction, + flatServerlessFunction, + flatServerlessFunctionLayer, payload, version, env, }: { - serverlessFunction: ServerlessFunctionEntity; + flatServerlessFunction: FlatServerlessFunction; + flatServerlessFunctionLayer: FlatServerlessFunctionLayer; payload: object; version: string; env?: Record; }): Promise { - return this.driver.execute({ serverlessFunction, payload, version, env }); + return this.driver.execute({ + flatServerlessFunction, + flatServerlessFunctionLayer, + payload, + version, + env, + }); } } diff --git a/packages/twenty-server/src/engine/core-modules/serverless/utils/serverless-get-folder.utils.ts b/packages/twenty-server/src/engine/core-modules/serverless/utils/serverless-get-folder.utils.ts index 6e78863d17..25243ec8ff 100644 --- a/packages/twenty-server/src/engine/core-modules/serverless/utils/serverless-get-folder.utils.ts +++ b/packages/twenty-server/src/engine/core-modules/serverless/utils/serverless-get-folder.utils.ts @@ -3,23 +3,25 @@ import { join } from 'path'; import { isDefined } from 'twenty-shared/utils'; import { FileFolder } from 'twenty-shared/types'; -import { type ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; import { ServerlessFunctionException, 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'; -export const getServerlessFolder = ({ - serverlessFunction, +export const getServerlessFolderOrThrow = ({ + flatServerlessFunction, version, toDelete = false, }: { - serverlessFunction: ServerlessFunctionEntity | FlatServerlessFunction; + flatServerlessFunction: FlatServerlessFunction; version?: 'draft' | 'latest' | (string & NonNullable); toDelete?: boolean; }) => { - if (version === 'latest' && !isDefined(serverlessFunction.latestVersion)) { + if ( + version === 'latest' && + !isDefined(flatServerlessFunction.latestVersion) + ) { throw new ServerlessFunctionException( "Can't get 'latest' version when serverlessFunction 'latestVersion' is undefined", ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_VERSION_NOT_FOUND, @@ -27,14 +29,14 @@ export const getServerlessFolder = ({ } const computedVersion = - version === 'latest' ? serverlessFunction.latestVersion : version; + version === 'latest' ? flatServerlessFunction.latestVersion : version; return join( - 'workspace-' + serverlessFunction.workspaceId, + 'workspace-' + flatServerlessFunction.workspaceId, toDelete ? FileFolder.ServerlessFunctionToDelete : FileFolder.ServerlessFunction, - serverlessFunction.id, + flatServerlessFunction.id, computedVersion || '', ); }; diff --git a/packages/twenty-server/src/engine/metadata-modules/cron-trigger/resolvers/cron-trigger.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/cron-trigger/resolvers/cron-trigger.resolver.ts index 93b47b62c6..632256f4fb 100644 --- a/packages/twenty-server/src/engine/metadata-modules/cron-trigger/resolvers/cron-trigger.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/cron-trigger/resolvers/cron-trigger.resolver.ts @@ -1,9 +1,8 @@ import { UseFilters, UseGuards, UsePipes } from '@nestjs/common'; import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; import { PermissionFlagType } from 'twenty-shared/constants'; +import { isDefined } from 'twenty-shared/utils'; import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter'; import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe'; @@ -15,9 +14,15 @@ import { CreateCronTriggerInput } from 'src/engine/metadata-modules/cron-trigger import { CronTriggerIdInput } from 'src/engine/metadata-modules/cron-trigger/dtos/cron-trigger-id.input'; import { CronTriggerDTO } from 'src/engine/metadata-modules/cron-trigger/dtos/cron-trigger.dto'; import { UpdateCronTriggerInput } from 'src/engine/metadata-modules/cron-trigger/dtos/update-cron-trigger.input'; -import { CronTriggerEntity } from 'src/engine/metadata-modules/cron-trigger/entities/cron-trigger.entity'; +import { + CronTriggerException, + CronTriggerExceptionCode, +} from 'src/engine/metadata-modules/cron-trigger/exceptions/cron-trigger.exception'; import { CronTriggerV2Service } from 'src/engine/metadata-modules/cron-trigger/services/cron-trigger-v2.service'; import { cronTriggerGraphQLApiExceptionHandler } from 'src/engine/metadata-modules/cron-trigger/utils/cron-trigger-graphql-api-exception-handler.util'; +import { fromFlatCronTriggerToCronTriggerDto } from 'src/engine/metadata-modules/cron-trigger/utils/from-flat-cron-trigger-to-cron-trigger-dto.util'; +import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; +import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; @UseGuards( WorkspaceAuthGuard, @@ -29,37 +34,59 @@ import { cronTriggerGraphQLApiExceptionHandler } from 'src/engine/metadata-modul export class CronTriggerResolver { constructor( private readonly cronTriggerV2Service: CronTriggerV2Service, - @InjectRepository(CronTriggerEntity) - private readonly cronTriggerRepository: Repository, + private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, ) {} @Query(() => CronTriggerDTO) async findOneCronTrigger( @Args('input') { id }: CronTriggerIdInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, - ) { + ): Promise { try { - return await this.cronTriggerRepository.findOneOrFail({ - where: { - id, - workspaceId, - }, + const { flatCronTriggerMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatCronTriggerMaps'], + }, + ); + + const flatCronTrigger = findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: id, + flatEntityMaps: flatCronTriggerMaps, }); + + if (!isDefined(flatCronTrigger)) { + throw new CronTriggerException( + `Cron trigger with id ${id} not found`, + CronTriggerExceptionCode.CRON_TRIGGER_NOT_FOUND, + ); + } + + return fromFlatCronTriggerToCronTriggerDto(flatCronTrigger); } catch (error) { - cronTriggerGraphQLApiExceptionHandler(error); + return cronTriggerGraphQLApiExceptionHandler(error); } } @Query(() => [CronTriggerDTO]) async findManyCronTriggers( @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, - ) { + ): Promise { try { - return await this.cronTriggerRepository.find({ - where: { workspaceId }, - }); + const { flatCronTriggerMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatCronTriggerMaps'], + }, + ); + + return Object.values(flatCronTriggerMaps.byId) + .filter(isDefined) + .map(fromFlatCronTriggerToCronTriggerDto); } catch (error) { - cronTriggerGraphQLApiExceptionHandler(error); + return cronTriggerGraphQLApiExceptionHandler(error); } } @@ -67,14 +94,16 @@ export class CronTriggerResolver { async deleteOneCronTrigger( @Args('input') input: CronTriggerIdInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, - ) { + ): Promise { try { - return await this.cronTriggerV2Service.destroyOne({ + const flatCronTrigger = await this.cronTriggerV2Service.destroyOne({ destroyCronTriggerInput: input, workspaceId, }); + + return fromFlatCronTriggerToCronTriggerDto(flatCronTrigger); } catch (error) { - cronTriggerGraphQLApiExceptionHandler(error); + return cronTriggerGraphQLApiExceptionHandler(error); } } @@ -83,11 +112,16 @@ export class CronTriggerResolver { @Args('input') input: UpdateCronTriggerInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, - ) { + ): Promise { try { - return await this.cronTriggerV2Service.updateOne(input, workspaceId); + const flatCronTrigger = await this.cronTriggerV2Service.updateOne( + input, + workspaceId, + ); + + return fromFlatCronTriggerToCronTriggerDto(flatCronTrigger); } catch (error) { - cronTriggerGraphQLApiExceptionHandler(error); + return cronTriggerGraphQLApiExceptionHandler(error); } } @@ -96,11 +130,16 @@ export class CronTriggerResolver { @Args('input') input: CreateCronTriggerInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, - ) { + ): Promise { try { - return await this.cronTriggerV2Service.createOne(input, workspaceId); + const flatCronTrigger = await this.cronTriggerV2Service.createOne( + input, + workspaceId, + ); + + return fromFlatCronTriggerToCronTriggerDto(flatCronTrigger); } catch (error) { - cronTriggerGraphQLApiExceptionHandler(error); + return cronTriggerGraphQLApiExceptionHandler(error); } } } diff --git a/packages/twenty-server/src/engine/metadata-modules/cron-trigger/services/cron-trigger-v2.service.ts b/packages/twenty-server/src/engine/metadata-modules/cron-trigger/services/cron-trigger-v2.service.ts index 440d82ac21..a71d57bf0f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/cron-trigger/services/cron-trigger-v2.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/cron-trigger/services/cron-trigger-v2.service.ts @@ -10,7 +10,7 @@ import { CronTriggerException, CronTriggerExceptionCode, } from 'src/engine/metadata-modules/cron-trigger/exceptions/cron-trigger.exception'; -import { FlatCronTrigger } from 'src/engine/metadata-modules/cron-trigger/types/flat-cron-trigger.type'; +import { type FlatCronTrigger } from 'src/engine/metadata-modules/cron-trigger/types/flat-cron-trigger.type'; import { fromCreateCronTriggerInputToFlatCronTrigger } from 'src/engine/metadata-modules/cron-trigger/utils/from-create-cron-trigger-input-to-flat-cron-trigger.util'; import { fromUpdateCronTriggerInputToFlatCronTriggerToUpdateOrThrow } from 'src/engine/metadata-modules/cron-trigger/utils/from-update-cron-trigger-input-to-flat-cron-trigger-to-update-or-throw.util'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; @@ -34,7 +34,7 @@ export class CronTriggerV2Service { * when interacting with another application than workspace custom one * */ applicationId?: string, - ) { + ): Promise { const { workspaceCustomFlatApplication } = await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( { @@ -90,7 +90,7 @@ export class CronTriggerV2Service { async updateOne( cronTriggerInput: UpdateCronTriggerInput, workspaceId: string, - ) { + ): Promise { const { flatCronTriggerMaps } = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { diff --git a/packages/twenty-server/src/engine/metadata-modules/cron-trigger/utils/cron-trigger-graphql-api-exception-handler.util.ts b/packages/twenty-server/src/engine/metadata-modules/cron-trigger/utils/cron-trigger-graphql-api-exception-handler.util.ts index 083c3ed37c..707a7b5c5e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/cron-trigger/utils/cron-trigger-graphql-api-exception-handler.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/cron-trigger/utils/cron-trigger-graphql-api-exception-handler.util.ts @@ -7,7 +7,7 @@ import { CronTriggerExceptionCode, } from 'src/engine/metadata-modules/cron-trigger/exceptions/cron-trigger.exception'; -export const cronTriggerGraphQLApiExceptionHandler = (error: Error): void => { +export const cronTriggerGraphQLApiExceptionHandler = (error: Error): never => { if (error instanceof CronTriggerException) { switch (error.code) { case CronTriggerExceptionCode.CRON_TRIGGER_NOT_FOUND: diff --git a/packages/twenty-server/src/engine/metadata-modules/cron-trigger/utils/from-flat-cron-trigger-to-cron-trigger-dto.util.ts b/packages/twenty-server/src/engine/metadata-modules/cron-trigger/utils/from-flat-cron-trigger-to-cron-trigger-dto.util.ts new file mode 100644 index 0000000000..c98120c3a6 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/cron-trigger/utils/from-flat-cron-trigger-to-cron-trigger-dto.util.ts @@ -0,0 +1,11 @@ +import { type CronTriggerDTO } from 'src/engine/metadata-modules/cron-trigger/dtos/cron-trigger.dto'; +import { type FlatCronTrigger } from 'src/engine/metadata-modules/cron-trigger/types/flat-cron-trigger.type'; + +export const fromFlatCronTriggerToCronTriggerDto = ( + flatCronTrigger: FlatCronTrigger, +): CronTriggerDTO => ({ + id: flatCronTrigger.id, + settings: flatCronTrigger.settings, + createdAt: new Date(flatCronTrigger.createdAt), + updatedAt: new Date(flatCronTrigger.updatedAt), +}); diff --git a/packages/twenty-server/src/engine/metadata-modules/database-event-trigger/resolvers/database-event-trigger.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/database-event-trigger/resolvers/database-event-trigger.resolver.ts index 5e0f77096f..1f216cecd0 100644 --- a/packages/twenty-server/src/engine/metadata-modules/database-event-trigger/resolvers/database-event-trigger.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/database-event-trigger/resolvers/database-event-trigger.resolver.ts @@ -1,9 +1,8 @@ import { UseFilters, UseGuards, UsePipes } from '@nestjs/common'; import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; import { PermissionFlagType } from 'twenty-shared/constants'; +import { isDefined } from 'twenty-shared/utils'; import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter'; import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe'; @@ -15,9 +14,15 @@ import { CreateDatabaseEventTriggerInput } from 'src/engine/metadata-modules/dat import { DatabaseEventTriggerIdInput } from 'src/engine/metadata-modules/database-event-trigger/dtos/database-event-trigger-id.input'; import { DatabaseEventTriggerDTO } from 'src/engine/metadata-modules/database-event-trigger/dtos/database-event-trigger.dto'; import { UpdateDatabaseEventTriggerInput } from 'src/engine/metadata-modules/database-event-trigger/dtos/update-database-event-trigger.input'; -import { DatabaseEventTriggerEntity } from 'src/engine/metadata-modules/database-event-trigger/entities/database-event-trigger.entity'; +import { + DatabaseEventTriggerException, + DatabaseEventTriggerExceptionCode, +} from 'src/engine/metadata-modules/database-event-trigger/exceptions/database-event-trigger.exception'; import { DatabaseEventTriggerV2Service } from 'src/engine/metadata-modules/database-event-trigger/services/database-event-trigger-v2.service'; import { databaseEventTriggerGraphQLApiExceptionHandler } from 'src/engine/metadata-modules/database-event-trigger/utils/database-event-trigger-graphql-api-exception-handler.utils'; +import { fromFlatDatabaseEventTriggerToDatabaseEventTriggerDto } from 'src/engine/metadata-modules/database-event-trigger/utils/from-flat-database-event-trigger-to-database-event-trigger-dto.util'; +import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; +import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; @UseGuards( WorkspaceAuthGuard, @@ -29,37 +34,61 @@ import { databaseEventTriggerGraphQLApiExceptionHandler } from 'src/engine/metad export class DatabaseEventTriggerResolver { constructor( private readonly databaseEventTriggerV2Service: DatabaseEventTriggerV2Service, - @InjectRepository(DatabaseEventTriggerEntity) - private readonly databaseEventTriggerRepository: Repository, + private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, ) {} @Query(() => DatabaseEventTriggerDTO) async findOneDatabaseEventTrigger( @Args('input') { id }: DatabaseEventTriggerIdInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, - ) { + ): Promise { try { - return await this.databaseEventTriggerRepository.findOneOrFail({ - where: { - id, - workspaceId, - }, + const { flatDatabaseEventTriggerMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatDatabaseEventTriggerMaps'], + }, + ); + + const flatDatabaseEventTrigger = findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: id, + flatEntityMaps: flatDatabaseEventTriggerMaps, }); + + if (!isDefined(flatDatabaseEventTrigger)) { + throw new DatabaseEventTriggerException( + `Database event trigger with id ${id} not found`, + DatabaseEventTriggerExceptionCode.DATABASE_EVENT_TRIGGER_NOT_FOUND, + ); + } + + return fromFlatDatabaseEventTriggerToDatabaseEventTriggerDto( + flatDatabaseEventTrigger, + ); } catch (error) { - databaseEventTriggerGraphQLApiExceptionHandler(error); + return databaseEventTriggerGraphQLApiExceptionHandler(error); } } @Query(() => [DatabaseEventTriggerDTO]) async findManyDatabaseEventTriggers( @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, - ) { + ): Promise { try { - return await this.databaseEventTriggerRepository.find({ - where: { workspaceId }, - }); + const { flatDatabaseEventTriggerMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatDatabaseEventTriggerMaps'], + }, + ); + + return Object.values(flatDatabaseEventTriggerMaps.byId) + .filter(isDefined) + .map(fromFlatDatabaseEventTriggerToDatabaseEventTriggerDto); } catch (error) { - databaseEventTriggerGraphQLApiExceptionHandler(error); + return databaseEventTriggerGraphQLApiExceptionHandler(error); } } @@ -67,14 +96,19 @@ export class DatabaseEventTriggerResolver { async deleteOneDatabaseEventTrigger( @Args('input') input: DatabaseEventTriggerIdInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, - ) { + ): Promise { try { - return await this.databaseEventTriggerV2Service.destroyOne({ - destroyDatabaseEventTriggerInput: input, - workspaceId, - }); + const flatDatabaseEventTrigger = + await this.databaseEventTriggerV2Service.destroyOne({ + destroyDatabaseEventTriggerInput: input, + workspaceId, + }); + + return fromFlatDatabaseEventTriggerToDatabaseEventTriggerDto( + flatDatabaseEventTrigger, + ); } catch (error) { - databaseEventTriggerGraphQLApiExceptionHandler(error); + return databaseEventTriggerGraphQLApiExceptionHandler(error); } } @@ -83,14 +117,16 @@ export class DatabaseEventTriggerResolver { @Args('input') input: UpdateDatabaseEventTriggerInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, - ) { + ): Promise { try { - return await this.databaseEventTriggerV2Service.updateOne( - input, - workspaceId, + const flatDatabaseEventTrigger = + await this.databaseEventTriggerV2Service.updateOne(input, workspaceId); + + return fromFlatDatabaseEventTriggerToDatabaseEventTriggerDto( + flatDatabaseEventTrigger, ); } catch (error) { - databaseEventTriggerGraphQLApiExceptionHandler(error); + return databaseEventTriggerGraphQLApiExceptionHandler(error); } } @@ -99,14 +135,16 @@ export class DatabaseEventTriggerResolver { @Args('input') input: CreateDatabaseEventTriggerInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, - ) { + ): Promise { try { - return await this.databaseEventTriggerV2Service.createOne( - input, - workspaceId, + const flatDatabaseEventTrigger = + await this.databaseEventTriggerV2Service.createOne(input, workspaceId); + + return fromFlatDatabaseEventTriggerToDatabaseEventTriggerDto( + flatDatabaseEventTrigger, ); } catch (error) { - databaseEventTriggerGraphQLApiExceptionHandler(error); + return databaseEventTriggerGraphQLApiExceptionHandler(error); } } } diff --git a/packages/twenty-server/src/engine/metadata-modules/database-event-trigger/services/database-event-trigger-v2.service.ts b/packages/twenty-server/src/engine/metadata-modules/database-event-trigger/services/database-event-trigger-v2.service.ts index 4be493b08a..40909c5c8b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/database-event-trigger/services/database-event-trigger-v2.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/database-event-trigger/services/database-event-trigger-v2.service.ts @@ -10,7 +10,7 @@ import { DatabaseEventTriggerException, DatabaseEventTriggerExceptionCode, } from 'src/engine/metadata-modules/database-event-trigger/exceptions/database-event-trigger.exception'; -import { FlatDatabaseEventTrigger } from 'src/engine/metadata-modules/database-event-trigger/types/flat-database-event-trigger.type'; +import { type FlatDatabaseEventTrigger } from 'src/engine/metadata-modules/database-event-trigger/types/flat-database-event-trigger.type'; import { fromCreateDatabaseEventTriggerInputToFlatDatabaseEventTrigger } from 'src/engine/metadata-modules/database-event-trigger/utils/from-create-database-event-trigger-input-to-flat-database-event-trigger.util'; import { fromUpdateDatabaseEventTriggerInputToFlatDatabaseEventTriggerToUpdateOrThrow } from 'src/engine/metadata-modules/database-event-trigger/utils/from-update-database-event-trigger-input-to-flat-database-event-trigger-to-update-or-throw.util'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; @@ -34,7 +34,7 @@ export class DatabaseEventTriggerV2Service { * when interacting with another application than workspace custom one * */ applicationId?: string, - ) { + ): Promise { const { workspaceCustomFlatApplication } = await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( { @@ -92,7 +92,7 @@ export class DatabaseEventTriggerV2Service { async updateOne( databaseEventTriggerInput: UpdateDatabaseEventTriggerInput, workspaceId: string, - ) { + ): Promise { const { flatDatabaseEventTriggerMaps } = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { diff --git a/packages/twenty-server/src/engine/metadata-modules/database-event-trigger/utils/database-event-trigger-graphql-api-exception-handler.utils.ts b/packages/twenty-server/src/engine/metadata-modules/database-event-trigger/utils/database-event-trigger-graphql-api-exception-handler.utils.ts index 069f072720..e7d7408c3a 100644 --- a/packages/twenty-server/src/engine/metadata-modules/database-event-trigger/utils/database-event-trigger-graphql-api-exception-handler.utils.ts +++ b/packages/twenty-server/src/engine/metadata-modules/database-event-trigger/utils/database-event-trigger-graphql-api-exception-handler.utils.ts @@ -9,7 +9,7 @@ import { export const databaseEventTriggerGraphQLApiExceptionHandler = ( error: Error, -): void => { +): never => { if (error instanceof DatabaseEventTriggerException) { switch (error.code) { case DatabaseEventTriggerExceptionCode.DATABASE_EVENT_TRIGGER_NOT_FOUND: diff --git a/packages/twenty-server/src/engine/metadata-modules/database-event-trigger/utils/from-flat-database-event-trigger-to-database-event-trigger-dto.util.ts b/packages/twenty-server/src/engine/metadata-modules/database-event-trigger/utils/from-flat-database-event-trigger-to-database-event-trigger-dto.util.ts new file mode 100644 index 0000000000..446e18037b --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/database-event-trigger/utils/from-flat-database-event-trigger-to-database-event-trigger-dto.util.ts @@ -0,0 +1,11 @@ +import { type DatabaseEventTriggerDTO } from 'src/engine/metadata-modules/database-event-trigger/dtos/database-event-trigger.dto'; +import { type FlatDatabaseEventTrigger } from 'src/engine/metadata-modules/database-event-trigger/types/flat-database-event-trigger.type'; + +export const fromFlatDatabaseEventTriggerToDatabaseEventTriggerDto = ( + flatDatabaseEventTrigger: FlatDatabaseEventTrigger, +): DatabaseEventTriggerDTO => ({ + id: flatDatabaseEventTrigger.id, + settings: flatDatabaseEventTrigger.settings, + createdAt: new Date(flatDatabaseEventTrigger.createdAt), + updatedAt: new Date(flatDatabaseEventTrigger.updatedAt), +}); diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-flat-entity-properties-to-compare-and-stringify.constant.ts b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-flat-entity-properties-to-compare-and-stringify.constant.ts index c5210b5e5e..6975cbd0ac 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-flat-entity-properties-to-compare-and-stringify.constant.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-entity/constant/all-flat-entity-properties-to-compare-and-stringify.constant.ts @@ -3,6 +3,7 @@ import { type AllMetadataName } from 'twenty-shared/metadata'; import { FLAT_CRON_TRIGGER_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/cron-trigger/constants/flat-cron-trigger-editable-properties.constant'; import { FLAT_DATABASE_EVENT_TRIGGER_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/database-event-trigger/constants/flat-database-event-trigger-editable-properties.constant'; import { FLAT_AGENT_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-agent/constants/flat-agent-editable-properties.constant'; +import { FLAT_COMMAND_MENU_ITEM_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-command-menu-item/constants/flat-command-menu-item-editable-properties.constant'; import { type MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity.type'; import { FLAT_FIELD_METADATA_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-field-metadata/constants/flat-field-metadata-editable-properties.constant'; import { FLAT_FRONT_COMPONENT_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-front-component/constants/flat-front-component-editable-properties.constant'; @@ -14,7 +15,6 @@ import { FLAT_ROLE_TARGET_EDITABLE_PROPERTIES } from 'src/engine/metadata-module import { FLAT_ROLE_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-role/constants/flat-role-editable-properties.constant'; import { FLAT_ROW_LEVEL_PERMISSION_PREDICATE_GROUP_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-row-level-permission-predicate-group/constants/flat-row-level-permission-predicate-group-editable-properties.constant'; import { FLAT_ROW_LEVEL_PERMISSION_PREDICATE_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-row-level-permission-predicate/constants/flat-row-level-permission-predicate-editable-properties.constant'; -import { FLAT_COMMAND_MENU_ITEM_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-command-menu-item/constants/flat-command-menu-item-editable-properties.constant'; import { FLAT_SKILL_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-skill/constants/flat-skill-editable-properties.constant'; import { FLAT_VIEW_FIELD_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-view-field/constants/flat-view-field-editable-properties.constant'; import { FLAT_VIEW_FILTER_GROUP_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-view-filter-group/constants/flat-view-filter-group-editable-properties.constant'; @@ -81,8 +81,10 @@ export const ALL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY = { (property) => property !== 'code', ), 'deletedAt', + 'latestVersion', + 'publishedVersions', ], - propertiesToStringify: ['toolInputSchema'], + propertiesToStringify: ['toolInputSchema', 'publishedVersions'], }, cronTrigger: { propertiesToCompare: [...FLAT_CRON_TRIGGER_EDITABLE_PROPERTIES], diff --git a/packages/twenty-server/src/engine/metadata-modules/route-trigger/resolvers/route-trigger.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/route-trigger/resolvers/route-trigger.resolver.ts index 2ea1cf41c6..7868f77ca2 100644 --- a/packages/twenty-server/src/engine/metadata-modules/route-trigger/resolvers/route-trigger.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/route-trigger/resolvers/route-trigger.resolver.ts @@ -1,9 +1,8 @@ import { UseFilters, UseGuards, UsePipes } from '@nestjs/common'; import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; -import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; import { PermissionFlagType } from 'twenty-shared/constants'; +import { isDefined } from 'twenty-shared/utils'; import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter'; import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe'; @@ -11,13 +10,19 @@ import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.ent import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; +import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; +import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; import { CreateRouteTriggerInput } from 'src/engine/metadata-modules/route-trigger/dtos/create-route-trigger.input'; import { RouteTriggerIdInput } from 'src/engine/metadata-modules/route-trigger/dtos/route-trigger-id.input'; import { RouteTriggerDTO } from 'src/engine/metadata-modules/route-trigger/dtos/route-trigger.dto'; import { UpdateRouteTriggerInput } from 'src/engine/metadata-modules/route-trigger/dtos/update-route-trigger.input'; -import { RouteTriggerEntity } from 'src/engine/metadata-modules/route-trigger/route-trigger.entity'; +import { + RouteTriggerException, + RouteTriggerExceptionCode, +} from 'src/engine/metadata-modules/route-trigger/exceptions/route-trigger.exception'; import { RouteTriggerV2Service } from 'src/engine/metadata-modules/route-trigger/services/route-trigger-v2.service'; +import { fromFlatRouteTriggerToRouteTriggerDto } from 'src/engine/metadata-modules/route-trigger/utils/from-flat-route-trigger-to-route-trigger-dto.util'; import { routeTriggerGraphQLApiExceptionHandler } from 'src/engine/metadata-modules/route-trigger/utils/route-trigger-graphql-api-exception-handler.utils'; @UseGuards( @@ -33,37 +38,59 @@ import { routeTriggerGraphQLApiExceptionHandler } from 'src/engine/metadata-modu export class RouteTriggerResolver { constructor( private readonly routeV2Service: RouteTriggerV2Service, - @InjectRepository(RouteTriggerEntity) - private readonly routeTriggerRepository: Repository, + private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, ) {} @Query(() => RouteTriggerDTO) async findOneRouteTrigger( @Args('input') { id }: RouteTriggerIdInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, - ) { + ): Promise { try { - return await this.routeTriggerRepository.findOneOrFail({ - where: { - id, - workspaceId, - }, + const { flatRouteTriggerMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatRouteTriggerMaps'], + }, + ); + + const flatRouteTrigger = findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: id, + flatEntityMaps: flatRouteTriggerMaps, }); + + if (!isDefined(flatRouteTrigger)) { + throw new RouteTriggerException( + `Route trigger with id ${id} not found`, + RouteTriggerExceptionCode.ROUTE_NOT_FOUND, + ); + } + + return fromFlatRouteTriggerToRouteTriggerDto(flatRouteTrigger); } catch (error) { - routeTriggerGraphQLApiExceptionHandler(error); + return routeTriggerGraphQLApiExceptionHandler(error); } } @Query(() => [RouteTriggerDTO]) async findManyRouteTriggers( @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, - ) { + ): Promise { try { - return await this.routeTriggerRepository.find({ - where: { workspaceId }, - }); + const { flatRouteTriggerMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatRouteTriggerMaps'], + }, + ); + + return Object.values(flatRouteTriggerMaps.byId) + .filter(isDefined) + .map(fromFlatRouteTriggerToRouteTriggerDto); } catch (error) { - routeTriggerGraphQLApiExceptionHandler(error); + return routeTriggerGraphQLApiExceptionHandler(error); } } @@ -71,14 +98,16 @@ export class RouteTriggerResolver { async deleteOneRouteTrigger( @Args('input') input: RouteTriggerIdInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, - ) { + ): Promise { try { - return await this.routeV2Service.destroyOne({ + const flatRouteTrigger = await this.routeV2Service.destroyOne({ destroyRouteTriggerInput: input, workspaceId, }); + + return fromFlatRouteTriggerToRouteTriggerDto(flatRouteTrigger); } catch (error) { - routeTriggerGraphQLApiExceptionHandler(error); + return routeTriggerGraphQLApiExceptionHandler(error); } } @@ -87,11 +116,16 @@ export class RouteTriggerResolver { @Args('input') input: UpdateRouteTriggerInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, - ) { + ): Promise { try { - return await this.routeV2Service.updateOne(input, workspaceId); + const flatRouteTrigger = await this.routeV2Service.updateOne( + input, + workspaceId, + ); + + return fromFlatRouteTriggerToRouteTriggerDto(flatRouteTrigger); } catch (error) { - routeTriggerGraphQLApiExceptionHandler(error); + return routeTriggerGraphQLApiExceptionHandler(error); } } @@ -100,11 +134,16 @@ export class RouteTriggerResolver { @Args('input') input: CreateRouteTriggerInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, - ) { + ): Promise { try { - return await this.routeV2Service.createOne(input, workspaceId); + const flatRouteTrigger = await this.routeV2Service.createOne( + input, + workspaceId, + ); + + return fromFlatRouteTriggerToRouteTriggerDto(flatRouteTrigger); } catch (error) { - routeTriggerGraphQLApiExceptionHandler(error); + return routeTriggerGraphQLApiExceptionHandler(error); } } } diff --git a/packages/twenty-server/src/engine/metadata-modules/route-trigger/services/route-trigger-v2.service.ts b/packages/twenty-server/src/engine/metadata-modules/route-trigger/services/route-trigger-v2.service.ts index c4b9852ba5..17a75c2b6f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/route-trigger/services/route-trigger-v2.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/route-trigger/services/route-trigger-v2.service.ts @@ -12,7 +12,7 @@ import { RouteTriggerException, RouteTriggerExceptionCode, } from 'src/engine/metadata-modules/route-trigger/exceptions/route-trigger.exception'; -import { FlatRouteTrigger } from 'src/engine/metadata-modules/route-trigger/types/flat-route-trigger.type'; +import { type FlatRouteTrigger } from 'src/engine/metadata-modules/route-trigger/types/flat-route-trigger.type'; import { fromCreateRouteTriggerInputToFlatRouteTrigger } from 'src/engine/metadata-modules/route-trigger/utils/from-create-route-trigger-input-to-flat-route-trigger.util'; import { fromUpdateRouteTriggerInputToFlatRouteTriggerToUpdateOrThrow } from 'src/engine/metadata-modules/route-trigger/utils/from-update-route-trigger-input-to-flat-route-trigger-to-update-or-throw.util'; import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception'; @@ -34,7 +34,7 @@ export class RouteTriggerV2Service { * when interacting with another application than workspace custom one * */ applicationId?: string, - ) { + ): Promise { const { workspaceCustomFlatApplication } = await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( { @@ -89,7 +89,7 @@ export class RouteTriggerV2Service { async updateOne( routeTriggerInput: UpdateRouteTriggerInput, workspaceId: string, - ) { + ): Promise { const { flatRouteTriggerMaps } = await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { diff --git a/packages/twenty-server/src/engine/metadata-modules/route-trigger/utils/from-flat-route-trigger-to-route-trigger-dto.util.ts b/packages/twenty-server/src/engine/metadata-modules/route-trigger/utils/from-flat-route-trigger-to-route-trigger-dto.util.ts new file mode 100644 index 0000000000..f925a3d146 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/route-trigger/utils/from-flat-route-trigger-to-route-trigger-dto.util.ts @@ -0,0 +1,14 @@ +import { type RouteTriggerDTO } from 'src/engine/metadata-modules/route-trigger/dtos/route-trigger.dto'; +import { type FlatRouteTrigger } from 'src/engine/metadata-modules/route-trigger/types/flat-route-trigger.type'; + +export const fromFlatRouteTriggerToRouteTriggerDto = ( + flatRouteTrigger: FlatRouteTrigger, +): RouteTriggerDTO => ({ + id: flatRouteTrigger.id, + path: flatRouteTrigger.path, + isAuthRequired: flatRouteTrigger.isAuthRequired, + httpMethod: flatRouteTrigger.httpMethod, + forwardedRequestHeaders: flatRouteTrigger.forwardedRequestHeaders, + createdAt: new Date(flatRouteTrigger.createdAt), + updatedAt: new Date(flatRouteTrigger.updatedAt), +}); diff --git a/packages/twenty-server/src/engine/metadata-modules/route-trigger/utils/route-trigger-graphql-api-exception-handler.utils.ts b/packages/twenty-server/src/engine/metadata-modules/route-trigger/utils/route-trigger-graphql-api-exception-handler.utils.ts index 078f6dcb78..fd82c2e35b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/route-trigger/utils/route-trigger-graphql-api-exception-handler.utils.ts +++ b/packages/twenty-server/src/engine/metadata-modules/route-trigger/utils/route-trigger-graphql-api-exception-handler.utils.ts @@ -7,7 +7,7 @@ import { RouteTriggerExceptionCode, } from 'src/engine/metadata-modules/route-trigger/exceptions/route-trigger.exception'; -export const routeTriggerGraphQLApiExceptionHandler = (error: Error): void => { +export const routeTriggerGraphQLApiExceptionHandler = (error: Error): never => { if (error instanceof RouteTriggerException) { switch (error.code) { case RouteTriggerExceptionCode.ROUTE_NOT_FOUND: diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.module.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.module.ts index d21f7d058e..44da77e49d 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.module.ts @@ -5,13 +5,23 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi import { ServerlessFunctionLayerEntity } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.entity'; import { ServerlessFunctionLayerResolver } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.resolver'; import { ServerlessFunctionLayerService } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.service'; +import { WorkspaceServerlessFunctionLayerMapCacheService } from 'src/engine/metadata-modules/serverless-function-layer/services/workspace-serverless-function-layer-map-cache.service'; +import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module'; @Module({ imports: [ PermissionsModule, TypeOrmModule.forFeature([ServerlessFunctionLayerEntity]), + WorkspaceCacheModule, + ], + providers: [ + ServerlessFunctionLayerService, + ServerlessFunctionLayerResolver, + WorkspaceServerlessFunctionLayerMapCacheService, + ], + exports: [ + ServerlessFunctionLayerService, + WorkspaceServerlessFunctionLayerMapCacheService, ], - providers: [ServerlessFunctionLayerService, ServerlessFunctionLayerResolver], - exports: [ServerlessFunctionLayerService], }) export class ServerlessFunctionLayerModule {} diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.service.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.service.ts index 79eeb8b2f9..67fe628537 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.service.ts @@ -10,12 +10,14 @@ import { ServerlessFunctionLayerEntity } from 'src/engine/metadata-modules/serve import { CreateServerlessFunctionLayerInput } from 'src/engine/metadata-modules/serverless-function-layer/dtos/create-serverless-function-layer.input'; import { getLastCommonLayerDependencies } from 'src/engine/core-modules/serverless/drivers/utils/get-last-common-layer-dependencies'; import { serverlessFunctionCreateHash } from 'src/engine/metadata-modules/serverless-function/utils/serverless-function-create-hash.utils'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; @Injectable() export class ServerlessFunctionLayerService { constructor( @InjectRepository(ServerlessFunctionLayerEntity) private readonly serverlessFunctionLayerRepository: Repository, + private readonly workspaceCacheService: WorkspaceCacheService, ) {} async create( @@ -32,12 +34,21 @@ export class ServerlessFunctionLayerService { workspaceId, }); - return this.serverlessFunctionLayerRepository.save(serverlessFunctionLayer); + const savedLayer = await this.serverlessFunctionLayerRepository.save( + serverlessFunctionLayer, + ); + + await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [ + 'serverlessFunctionLayerMaps', + ]); + + return savedLayer; } async update( id: string, data: QueryDeepPartialEntity, + workspaceId: string, ) { const checksum = data.yarnLock ? serverlessFunctionCreateHash(data.yarnLock as string) @@ -45,7 +56,16 @@ export class ServerlessFunctionLayerService { const updateData = { ...data, ...(checksum && { checksum }) }; - return this.serverlessFunctionLayerRepository.update(id, updateData); + const result = await this.serverlessFunctionLayerRepository.update( + id, + updateData, + ); + + await this.workspaceCacheService.invalidateAndRecompute(workspaceId, [ + 'serverlessFunctionLayerMaps', + ]); + + return result; } async createCommonLayerIfNotExist(workspaceId: string) { diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function-layer/services/workspace-serverless-function-layer-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function-layer/services/workspace-serverless-function-layer-map-cache.service.ts new file mode 100644 index 0000000000..1deeed4421 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function-layer/services/workspace-serverless-function-layer-map-cache.service.ts @@ -0,0 +1,47 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { Repository } from 'typeorm'; + +import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; + +import { ServerlessFunctionLayerEntity } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.entity'; +import { type ServerlessFunctionLayerCacheMaps } from 'src/engine/metadata-modules/serverless-function-layer/types/serverless-function-layer-cache-maps.type'; +import { fromServerlessFunctionLayerEntityToFlatServerlessFunctionLayer } from 'src/engine/metadata-modules/serverless-function-layer/utils/from-serverless-function-layer-entity-to-flat-serverless-function-layer.util'; +import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; + +@Injectable() +@WorkspaceCache('serverlessFunctionLayerMaps') +export class WorkspaceServerlessFunctionLayerMapCacheService extends WorkspaceCacheProvider { + constructor( + @InjectRepository(ServerlessFunctionLayerEntity) + private readonly serverlessFunctionLayerRepository: Repository, + ) { + super(); + } + + async computeForCache( + workspaceId: string, + ): Promise { + const serverlessFunctionLayerEntities = + await this.serverlessFunctionLayerRepository.find({ + where: { + workspaceId, + }, + }); + + const serverlessFunctionLayerMaps: ServerlessFunctionLayerCacheMaps = { + byId: {}, + }; + + for (const entity of serverlessFunctionLayerEntities) { + const flatServerlessFunctionLayer = + fromServerlessFunctionLayerEntityToFlatServerlessFunctionLayer(entity); + + serverlessFunctionLayerMaps.byId[flatServerlessFunctionLayer.id] = + flatServerlessFunctionLayer; + } + + return serverlessFunctionLayerMaps; + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function-layer/types/flat-serverless-function-layer.type.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function-layer/types/flat-serverless-function-layer.type.ts new file mode 100644 index 0000000000..2cb2b0003c --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function-layer/types/flat-serverless-function-layer.type.ts @@ -0,0 +1,5 @@ +import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/types/flat-entity.type'; +import { type ServerlessFunctionLayerEntity } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.entity'; + +export type FlatServerlessFunctionLayer = + FlatEntityFrom; diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function-layer/types/serverless-function-layer-cache-maps.type.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function-layer/types/serverless-function-layer-cache-maps.type.ts new file mode 100644 index 0000000000..f3c5171c8d --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function-layer/types/serverless-function-layer-cache-maps.type.ts @@ -0,0 +1,5 @@ +import { type FlatServerlessFunctionLayer } from 'src/engine/metadata-modules/serverless-function-layer/types/flat-serverless-function-layer.type'; + +export type ServerlessFunctionLayerCacheMaps = { + byId: Partial>; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function-layer/utils/from-serverless-function-layer-entity-to-flat-serverless-function-layer.util.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function-layer/utils/from-serverless-function-layer-entity-to-flat-serverless-function-layer.util.ts new file mode 100644 index 0000000000..612cf5d93a --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function-layer/utils/from-serverless-function-layer-entity-to-flat-serverless-function-layer.util.ts @@ -0,0 +1,15 @@ +import { type ServerlessFunctionLayerEntity } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.entity'; +import { type FlatServerlessFunctionLayer } from 'src/engine/metadata-modules/serverless-function-layer/types/flat-serverless-function-layer.type'; + +export const fromServerlessFunctionLayerEntityToFlatServerlessFunctionLayer = ( + entity: ServerlessFunctionLayerEntity, +): FlatServerlessFunctionLayer => ({ + id: entity.id, + packageJson: entity.packageJson, + yarnLock: entity.yarnLock, + checksum: entity.checksum, + workspaceId: entity.workspaceId, + createdAt: entity.createdAt.toISOString(), + updatedAt: entity.updatedAt.toISOString(), + serverlessFunctionIds: entity.serverlessFunctions?.map((sf) => sf.id) ?? [], +}); 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 92b86da941..77f1f7a735 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 @@ -24,6 +24,7 @@ import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverles import { ServerlessFunctionV2Service } from 'src/engine/metadata-modules/serverless-function/services/serverless-function-v2.service'; import { WorkspaceFlatServerlessFunctionMapCacheService } from 'src/engine/metadata-modules/serverless-function/services/workspace-flat-serverless-function-map-cache.service'; 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'; @Module({ @@ -46,6 +47,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace WorkspaceMigrationModule, ServerlessFunctionLayerModule, SubscriptionsModule, + WorkspaceCacheModule, TokenModule, ], providers: [ diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.resolver.ts index 2f9d4772a7..092b7fbe85 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.resolver.ts @@ -1,11 +1,9 @@ import { UseFilters, UseGuards, UsePipes } from '@nestjs/common'; import { Args, Mutation, Query, Resolver, Subscription } from '@nestjs/graphql'; -import { InjectRepository } from '@nestjs/typeorm'; import graphqlTypeJson from 'graphql-type-json'; -import { Repository } from 'typeorm'; -import { isDefined } from 'twenty-shared/utils'; import { PermissionFlagType } from 'twenty-shared/constants'; +import { isDefined } from 'twenty-shared/utils'; import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter'; import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe'; @@ -14,19 +12,22 @@ import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorat import { FeatureFlagGuard } from 'src/engine/guards/feature-flag.guard'; import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; +import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { CreateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input'; import { ExecuteServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/execute-serverless-function.input'; import { GetServerlessFunctionSourceCodeInput } from 'src/engine/metadata-modules/serverless-function/dtos/get-serverless-function-source-code.input'; import { PublishServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/publish-serverless-function.input'; import { ServerlessFunctionExecutionResultDTO } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-execution-result.dto'; import { ServerlessFunctionIdInput } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-id.input'; -import { ServerlessFunctionDTO } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto'; -import { UpdateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input'; -import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; -import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service'; -import { serverlessFunctionGraphQLApiExceptionHandler } from 'src/engine/metadata-modules/serverless-function/utils/serverless-function-graphql-api-exception-handler.utils'; import { ServerlessFunctionLogsDTO } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-logs.dto'; import { ServerlessFunctionLogsInput } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-logs.input'; +import { ServerlessFunctionDTO } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto'; +import { UpdateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input'; +import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service'; +import { FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type'; +import { findFlatServerlessFunctionOrThrow } from 'src/engine/metadata-modules/serverless-function/utils/find-flat-serverless-function-or-throw.util'; +import { fromFlatServerlessFunctionToServerlessFunctionDto } from 'src/engine/metadata-modules/serverless-function/utils/from-flat-serverless-function-to-serverless-function-dto.util'; +import { serverlessFunctionGraphQLApiExceptionHandler } from 'src/engine/metadata-modules/serverless-function/utils/serverless-function-graphql-api-exception-handler.utils'; import { SubscriptionChannel } from 'src/engine/subscriptions/enums/subscription-channel.enum'; import { SubscriptionService } from 'src/engine/subscriptions/subscription.service'; @@ -41,40 +42,91 @@ import { SubscriptionService } from 'src/engine/subscriptions/subscription.servi export class ServerlessFunctionResolver { constructor( private readonly serverlessFunctionService: ServerlessFunctionService, - @InjectRepository(ServerlessFunctionEntity) - private readonly serverlessFunctionRepository: Repository, private readonly subscriptionService: SubscriptionService, + private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, ) {} @Query(() => ServerlessFunctionDTO) async findOneServerlessFunction( @Args('input') { id }: ServerlessFunctionIdInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, - ) { + ): Promise { try { - return await this.serverlessFunctionRepository.findOneOrFail({ - where: { - id, - workspaceId, - }, - relations: ['cronTriggers', 'databaseEventTriggers', 'routeTriggers'], + const { + flatServerlessFunctionMaps, + flatCronTriggerMaps, + flatDatabaseEventTriggerMaps, + flatRouteTriggerMaps, + } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: [ + 'flatServerlessFunctionMaps', + 'flatCronTriggerMaps', + 'flatDatabaseEventTriggerMaps', + 'flatRouteTriggerMaps', + ], + }, + ); + + const flatServerlessFunction = findFlatServerlessFunctionOrThrow({ + id, + flatServerlessFunctionMaps, + }); + + return fromFlatServerlessFunctionToServerlessFunctionDto({ + flatServerlessFunction, + flatCronTriggerMaps, + flatDatabaseEventTriggerMaps, + flatRouteTriggerMaps, }); } catch (error) { - serverlessFunctionGraphQLApiExceptionHandler(error); + return serverlessFunctionGraphQLApiExceptionHandler(error); } } @Query(() => [ServerlessFunctionDTO]) async findManyServerlessFunctions( @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, - ) { + ): Promise { try { - return this.serverlessFunctionRepository.find({ - where: { workspaceId }, - relations: ['cronTriggers', 'databaseEventTriggers', 'routeTriggers'], - }); + const { + flatServerlessFunctionMaps, + flatCronTriggerMaps, + flatDatabaseEventTriggerMaps, + flatRouteTriggerMaps, + } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: [ + 'flatServerlessFunctionMaps', + 'flatCronTriggerMaps', + 'flatDatabaseEventTriggerMaps', + 'flatRouteTriggerMaps', + ], + }, + ); + + return Object.values(flatServerlessFunctionMaps.byId) + .filter( + ( + flatServerlessFunction, + ): flatServerlessFunction is FlatServerlessFunction => + isDefined(flatServerlessFunction) && + !isDefined(flatServerlessFunction.deletedAt), + ) + .map((flatServerlessFunction) => + fromFlatServerlessFunctionToServerlessFunctionDto({ + flatServerlessFunction, + flatCronTriggerMaps, + flatDatabaseEventTriggerMaps, + flatRouteTriggerMaps, + }), + ); } catch (error) { - serverlessFunctionGraphQLApiExceptionHandler(error); + return serverlessFunctionGraphQLApiExceptionHandler(error); } } @@ -83,7 +135,7 @@ export class ServerlessFunctionResolver { try { return await this.serverlessFunctionService.getAvailablePackages(id); } catch (error) { - serverlessFunctionGraphQLApiExceptionHandler(error); + return serverlessFunctionGraphQLApiExceptionHandler(error); } } @@ -99,7 +151,7 @@ export class ServerlessFunctionResolver { input.version, ); } catch (error) { - serverlessFunctionGraphQLApiExceptionHandler(error); + return serverlessFunctionGraphQLApiExceptionHandler(error); } } @@ -108,14 +160,38 @@ export class ServerlessFunctionResolver { async deleteOneServerlessFunction( @Args('input') input: ServerlessFunctionIdInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, - ) { + ): Promise { try { - return await this.serverlessFunctionService.deleteOneServerlessFunction({ - id: input.id, - workspaceId, + const flatServerlessFunction = + await this.serverlessFunctionService.deleteOneServerlessFunction({ + id: input.id, + workspaceId, + }); + + const { + flatCronTriggerMaps, + flatDatabaseEventTriggerMaps, + flatRouteTriggerMaps, + } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: [ + 'flatCronTriggerMaps', + 'flatDatabaseEventTriggerMaps', + 'flatRouteTriggerMaps', + ], + }, + ); + + return fromFlatServerlessFunctionToServerlessFunctionDto({ + flatServerlessFunction, + flatCronTriggerMaps, + flatDatabaseEventTriggerMaps, + flatRouteTriggerMaps, }); } catch (error) { - serverlessFunctionGraphQLApiExceptionHandler(error); + return serverlessFunctionGraphQLApiExceptionHandler(error); } } @@ -125,14 +201,38 @@ export class ServerlessFunctionResolver { @Args('input') input: UpdateServerlessFunctionInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, - ) { + ): Promise { try { - return await this.serverlessFunctionService.updateOneServerlessFunction( - input, - workspaceId, - ); + const flatServerlessFunction = + await this.serverlessFunctionService.updateOneServerlessFunction( + input, + workspaceId, + ); + + const { + flatCronTriggerMaps, + flatDatabaseEventTriggerMaps, + flatRouteTriggerMaps, + } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: [ + 'flatCronTriggerMaps', + 'flatDatabaseEventTriggerMaps', + 'flatRouteTriggerMaps', + ], + }, + ); + + return fromFlatServerlessFunctionToServerlessFunctionDto({ + flatServerlessFunction, + flatCronTriggerMaps, + flatDatabaseEventTriggerMaps, + flatRouteTriggerMaps, + }); } catch (error) { - serverlessFunctionGraphQLApiExceptionHandler(error); + return serverlessFunctionGraphQLApiExceptionHandler(error); } } @@ -142,14 +242,38 @@ export class ServerlessFunctionResolver { @Args('input') input: CreateServerlessFunctionInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, - ) { + ): Promise { try { - return await this.serverlessFunctionService.createOneServerlessFunction( - input, - workspaceId, - ); + const flatServerlessFunction = + await this.serverlessFunctionService.createOneServerlessFunction( + input, + workspaceId, + ); + + const { + flatCronTriggerMaps, + flatDatabaseEventTriggerMaps, + flatRouteTriggerMaps, + } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: [ + 'flatCronTriggerMaps', + 'flatDatabaseEventTriggerMaps', + 'flatRouteTriggerMaps', + ], + }, + ); + + return fromFlatServerlessFunctionToServerlessFunctionDto({ + flatServerlessFunction, + flatCronTriggerMaps, + flatDatabaseEventTriggerMaps, + flatRouteTriggerMaps, + }); } catch (error) { - serverlessFunctionGraphQLApiExceptionHandler(error); + return serverlessFunctionGraphQLApiExceptionHandler(error); } } @@ -169,7 +293,7 @@ export class ServerlessFunctionResolver { version, }); } catch (error) { - serverlessFunctionGraphQLApiExceptionHandler(error); + return serverlessFunctionGraphQLApiExceptionHandler(error); } } @@ -178,16 +302,40 @@ export class ServerlessFunctionResolver { async publishServerlessFunction( @Args('input') input: PublishServerlessFunctionInput, @AuthWorkspace() { id: workspaceId }: WorkspaceEntity, - ) { + ): Promise { try { const { id } = input; - return await this.serverlessFunctionService.publishOneServerlessFunctionOrFail( - id, - workspaceId, - ); + const flatServerlessFunction = + await this.serverlessFunctionService.publishOneServerlessFunctionOrFail( + id, + workspaceId, + ); + + const { + flatCronTriggerMaps, + flatDatabaseEventTriggerMaps, + flatRouteTriggerMaps, + } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: [ + 'flatCronTriggerMaps', + 'flatDatabaseEventTriggerMaps', + 'flatRouteTriggerMaps', + ], + }, + ); + + return fromFlatServerlessFunctionToServerlessFunctionDto({ + flatServerlessFunction, + flatCronTriggerMaps, + flatDatabaseEventTriggerMaps, + flatRouteTriggerMaps, + }); } catch (error) { - serverlessFunctionGraphQLApiExceptionHandler(error); + return serverlessFunctionGraphQLApiExceptionHandler(error); } } 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 ac35213d6d..eb89cf423a 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 @@ -1,32 +1,31 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { join } from 'path'; - import deepEqual from 'deep-equal'; import { DEFAULT_API_KEY_NAME, DEFAULT_API_URL_NAME, } from 'twenty-shared/application'; -import { Sources } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; -import { IsNull, Not, Repository } from 'typeorm'; +import { Repository } from 'typeorm'; import { FileStorageExceptionCode } from 'src/engine/core-modules/file-storage/interfaces/file-storage-exception'; import { type ServerlessExecuteResult } from 'src/engine/core-modules/serverless/drivers/interfaces/serverless-driver.interface'; +import { ApplicationService } from 'src/engine/core-modules/application/application.service'; import { AuditService } from 'src/engine/core-modules/audit/services/audit.service'; import { SERVERLESS_FUNCTION_EXECUTED_EVENT } from 'src/engine/core-modules/audit/utils/events/workspace-event/serverless-function/serverless-function-executed'; import { ApplicationTokenService } from 'src/engine/core-modules/auth/token/services/application-token.service'; import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; import { buildEnvVar } from 'src/engine/core-modules/serverless/drivers/utils/build-env-var'; -import { getBaseTypescriptProjectFiles } from 'src/engine/core-modules/serverless/drivers/utils/get-base-typescript-project-files'; import { ServerlessService } from 'src/engine/core-modules/serverless/serverless.service'; -import { getServerlessFolder } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils'; +import { getServerlessFolderOrThrow } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils'; import { ThrottlerService } from 'src/engine/core-modules/throttler/throttler.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; +import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; +import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; +import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; import { ServerlessFunctionLayerService } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.service'; -import { DEFAULT_TOOL_INPUT_SCHEMA } from 'src/engine/metadata-modules/serverless-function/constants/default-tool-input-schema.constant'; import { CreateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input'; import { type UpdateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input'; import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; @@ -34,8 +33,15 @@ import { ServerlessFunctionException, 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 { findFlatServerlessFunctionOrThrow } from 'src/engine/metadata-modules/serverless-function/utils/find-flat-serverless-function-or-throw.util'; +import { fromCreateServerlessFunctionInputToFlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/utils/from-create-serverless-function-input-to-flat-serverless-function.util'; +import { fromUpdateServerlessFunctionInputToFlatServerlessFunctionToUpdateOrThrow } from 'src/engine/metadata-modules/serverless-function/utils/from-update-serverless-function-input-to-flat-serverless-function-to-update-or-throw.util'; import { SubscriptionChannel } from 'src/engine/subscriptions/enums/subscription-channel.enum'; import { SubscriptionService } from 'src/engine/subscriptions/subscription.service'; +import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; +import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception'; +import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service'; import { WorkflowVersionStepException, WorkflowVersionStepExceptionCode, @@ -57,33 +63,57 @@ export class ServerlessFunctionService { private readonly auditService: AuditService, private readonly applicationTokenService: ApplicationTokenService, private readonly subscriptionService: SubscriptionService, + private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, + private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService, + private readonly applicationService: ApplicationService, + private readonly workspaceCacheService: WorkspaceCacheService, ) {} - async hasServerlessFunctionPublishedVersion(serverlessFunctionId: string) { - return await this.serverlessFunctionRepository.exists({ - where: { - id: serverlessFunctionId, - latestVersion: Not(IsNull()), - }, + async hasServerlessFunctionPublishedVersion( + serverlessFunctionId: string, + workspaceId: string, + ) { + const { flatServerlessFunctionMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatServerlessFunctionMaps'], + }, + ); + + const flatServerlessFunction = findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: serverlessFunctionId, + flatEntityMaps: flatServerlessFunctionMaps, }); + + return ( + isDefined(flatServerlessFunction) && + !isDefined(flatServerlessFunction.deletedAt) && + isDefined(flatServerlessFunction.latestVersion) + ); } async getServerlessFunctionSourceCode( workspaceId: string, id: string, version: string, - ): Promise { - const serverlessFunction = - await this.serverlessFunctionRepository.findOneOrFail({ - where: { - id, + ) { + const { flatServerlessFunctionMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { workspaceId, + flatMapsKeys: ['flatServerlessFunctionMaps'], }, - }); + ); + + const flatServerlessFunction = findFlatServerlessFunctionOrThrow({ + id, + flatServerlessFunctionMaps, + }); try { - const folderPath = getServerlessFolder({ - serverlessFunction, + const folderPath = getServerlessFolderOrThrow({ + flatServerlessFunction, version, }); @@ -109,24 +139,43 @@ export class ServerlessFunctionService { }): Promise { await this.throttleExecution(workspaceId); - const functionToExecute = - await this.serverlessFunctionRepository.findOneOrFail({ - where: { - id, - workspaceId, - }, - relations: [ - 'serverlessFunctionLayer', - 'application.applicationVariables', - ], - }); + const { + flatServerlessFunctionMaps, + flatApplicationMaps, + applicationVariableMaps, + serverlessFunctionLayerMaps, + } = await this.workspaceCacheService.getOrRecompute(workspaceId, [ + 'flatServerlessFunctionMaps', + 'flatApplicationMaps', + 'applicationVariableMaps', + 'serverlessFunctionLayerMaps', + ]); - const applicationAccessToken = isDefined(functionToExecute.applicationId) + const flatServerlessFunction = findFlatServerlessFunctionOrThrow({ + id, + flatServerlessFunctionMaps, + }); + + const flatServerlessFunctionLayer = + serverlessFunctionLayerMaps.byId[ + flatServerlessFunction.serverlessFunctionLayerId + ]; + + if (!isDefined(flatServerlessFunctionLayer)) { + throw new ServerlessFunctionException( + `Serverless function layer with id ${flatServerlessFunction.serverlessFunctionLayerId} not found`, + ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND, + ); + } + + const applicationAccessToken = isDefined( + flatServerlessFunction.applicationId, + ) ? await this.applicationTokenService.generateApplicationToken({ workspaceId, - applicationId: functionToExecute.applicationId, + applicationId: flatServerlessFunction.applicationId, expiresInSeconds: Math.max( - functionToExecute.timeoutSeconds, + flatServerlessFunction.timeoutSeconds, MIN_TOKEN_EXPIRATION_IN_SECONDS, ), }) @@ -134,6 +183,14 @@ export class ServerlessFunctionService { const baseUrl = cleanServerUrl(this.twentyConfigService.get('SERVER_URL')); + const flatApplicationVariables = isDefined( + flatServerlessFunction.applicationId, + ) + ? (applicationVariableMaps.byApplicationId[ + flatServerlessFunction.applicationId + ] ?? []) + : []; + const envVariables = { ...(isDefined(baseUrl) ? { @@ -145,18 +202,19 @@ export class ServerlessFunctionService { [DEFAULT_API_KEY_NAME]: applicationAccessToken.token, } : {}), - ...buildEnvVar(functionToExecute), + ...buildEnvVar(flatApplicationVariables), }; const resultServerlessFunction = await this.callWithTimeout({ callback: () => this.serverlessService.execute({ - serverlessFunction: functionToExecute, + flatServerlessFunction, + flatServerlessFunctionLayer, payload, version, env: envVariables, }), - timeoutMs: functionToExecute.timeoutSeconds * 1000, + timeoutMs: flatServerlessFunction.timeoutSeconds * 1000, }); if (this.twentyConfigService.get('SERVERLESS_LOGS_ENABLED')) { @@ -164,18 +222,24 @@ export class ServerlessFunctionService { console.log(resultServerlessFunction.logs); } + const applicationUniversalIdentifier = isDefined( + flatServerlessFunction.applicationId, + ) + ? flatApplicationMaps.byId[flatServerlessFunction.applicationId] + ?.universalIdentifier + : undefined; + await this.subscriptionService.publish({ channel: SubscriptionChannel.SERVERLESS_FUNCTION_LOGS_CHANNEL, workspaceId, payload: { serverlessFunctionLogs: { logs: resultServerlessFunction.logs, - id: functionToExecute.id, - name: functionToExecute.name, - universalIdentifier: functionToExecute.universalIdentifier, - applicationId: functionToExecute.applicationId, - applicationUniversalIdentifier: - functionToExecute.application?.universalIdentifier, + id: flatServerlessFunction.id, + name: flatServerlessFunction.name, + universalIdentifier: flatServerlessFunction.universalIdentifier, + applicationId: flatServerlessFunction.applicationId, + applicationUniversalIdentifier, }, }, }); @@ -190,23 +254,31 @@ export class ServerlessFunctionService { ...(resultServerlessFunction.error && { errorType: resultServerlessFunction.error.errorType, }), - functionId: functionToExecute.id, - functionName: functionToExecute.name, + functionId: flatServerlessFunction.id, + functionName: flatServerlessFunction.name, }); return resultServerlessFunction; } - async publishOneServerlessFunctionOrFail(id: string, workspaceId: string) { - const existingServerlessFunction = - await this.serverlessFunctionRepository.findOneOrFail({ - where: { - id, + async publishOneServerlessFunctionOrFail( + id: string, + workspaceId: string, + ): Promise { + const { flatServerlessFunctionMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { workspaceId, + flatMapsKeys: ['flatServerlessFunctionMaps'], }, - }); + ); - if (isDefined(existingServerlessFunction.latestVersion)) { + const existingFlatServerlessFunction = findFlatServerlessFunctionOrThrow({ + id, + flatServerlessFunctionMaps, + }); + + if (isDefined(existingFlatServerlessFunction.latestVersion)) { const latestCode = await this.getServerlessFunctionSourceCode( workspaceId, id, @@ -219,21 +291,21 @@ export class ServerlessFunctionService { ); if (deepEqual(latestCode, draftCode)) { - return existingServerlessFunction; + return existingFlatServerlessFunction; } } - const newVersion = existingServerlessFunction.latestVersion - ? `${parseInt(existingServerlessFunction.latestVersion, 10) + 1}` + const newVersion = existingFlatServerlessFunction.latestVersion + ? `${parseInt(existingFlatServerlessFunction.latestVersion, 10) + 1}` : '1'; - const draftFolderPath = getServerlessFolder({ - serverlessFunction: existingServerlessFunction, + const draftFolderPath = getServerlessFolderOrThrow({ + flatServerlessFunction: existingFlatServerlessFunction, version: 'draft', }); - const newFolderPath = getServerlessFolder({ - serverlessFunction: existingServerlessFunction, + const newFolderPath = getServerlessFolderOrThrow({ + flatServerlessFunction: existingFlatServerlessFunction, version: newVersion, }); @@ -243,38 +315,60 @@ export class ServerlessFunctionService { }); const newPublishedVersions = [ - ...existingServerlessFunction.publishedVersions, + ...existingFlatServerlessFunction.publishedVersions, newVersion, ]; - await this.serverlessFunctionRepository.update( - existingServerlessFunction.id, - { - latestVersion: newVersion, - publishedVersions: newPublishedVersions, - }, - ); + const updatedFlatServerlessFunction: FlatServerlessFunction = { + ...existingFlatServerlessFunction, + latestVersion: newVersion, + publishedVersions: newPublishedVersions, + }; - const publishedServerlessFunction = - await this.serverlessFunctionRepository.findOneOrFail({ - where: { - id, + const validateAndBuildResult = + await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( + { + allFlatEntityOperationByMetadataName: { + serverlessFunction: { + flatEntityToCreate: [], + flatEntityToDelete: [], + flatEntityToUpdate: [updatedFlatServerlessFunction], + }, + }, workspaceId, + isSystemBuild: false, }, + ); + + if (isDefined(validateAndBuildResult)) { + throw new WorkspaceMigrationBuilderException( + validateAndBuildResult, + 'Multiple validation errors occurred while publishing serverless function', + ); + } + + const { flatServerlessFunctionMaps: recomputedFlatServerlessFunctionMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatServerlessFunctionMaps'], + }, + ); + + const publishedFlatServerlessFunction = + findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityId: id, + flatEntityMaps: recomputedFlatServerlessFunctionMaps, }); - // This check should never be thrown, but we encounter some issue with - // publishing serverless function in self hosted instances - // See https://github.com/twentyhq/twenty/issues/13058 - // TODO: remove this check when issue solved - if (!isDefined(publishedServerlessFunction.latestVersion)) { + if (!isDefined(publishedFlatServerlessFunction.latestVersion)) { throw new WorkflowVersionStepException( - `Fail to publish serverlessFunction ${publishedServerlessFunction.id}.Received latest version ${publishedServerlessFunction.latestVersion}`, + `Fail to publish serverlessFunction ${publishedFlatServerlessFunction.id}.Received latest version ${publishedFlatServerlessFunction.latestVersion}`, WorkflowVersionStepExceptionCode.CODE_STEP_FAILURE, ); } - return publishedServerlessFunction; + return publishedFlatServerlessFunction; } async deleteOneServerlessFunction({ @@ -285,73 +379,196 @@ export class ServerlessFunctionService { id: string; workspaceId: string; softDelete?: boolean; - }) { - const existingServerlessFunction = - await this.serverlessFunctionRepository.findOneOrFail({ - where: { - id, + }): Promise { + const { flatServerlessFunctionMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { workspaceId, + flatMapsKeys: ['flatServerlessFunctionMaps'], }, - withDeleted: true, - }); + ); - if (softDelete) { - await this.serverlessFunctionRepository.softDelete({ id }); - } else { - await this.serverlessFunctionRepository.delete({ id }); - // We don't need to await this - this.fileStorageService.delete({ - folderPath: getServerlessFolder({ - serverlessFunction: existingServerlessFunction, - }), - }); + const existingFlatServerlessFunction = flatServerlessFunctionMaps.byId[id]; + + if (!isDefined(existingFlatServerlessFunction)) { + throw new ServerlessFunctionException( + 'Serverless function to delete not found', + ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND, + ); } - // We don't need to await this - this.serverlessService.delete(existingServerlessFunction); + if (softDelete) { + const updatedFlatServerlessFunctionWithDeletedAt: FlatServerlessFunction = + { + ...existingFlatServerlessFunction, + deletedAt: new Date().toISOString(), + }; - return existingServerlessFunction; + const validateAndBuildResult = + await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( + { + allFlatEntityOperationByMetadataName: { + serverlessFunction: { + flatEntityToCreate: [], + flatEntityToDelete: [], + flatEntityToUpdate: [ + updatedFlatServerlessFunctionWithDeletedAt, + ], + }, + }, + workspaceId, + isSystemBuild: false, + }, + ); + + if (isDefined(validateAndBuildResult)) { + throw new WorkspaceMigrationBuilderException( + validateAndBuildResult, + 'Multiple validation errors occurred while deleting serverless function', + ); + } + + return updatedFlatServerlessFunctionWithDeletedAt; + } else { + const validateAndBuildResult = + await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( + { + allFlatEntityOperationByMetadataName: { + serverlessFunction: { + flatEntityToCreate: [], + flatEntityToDelete: [existingFlatServerlessFunction], + flatEntityToUpdate: [], + }, + }, + workspaceId, + isSystemBuild: false, + }, + ); + + if (isDefined(validateAndBuildResult)) { + throw new WorkspaceMigrationBuilderException( + validateAndBuildResult, + 'Multiple validation errors occurred while destroying serverless function', + ); + } + } + + return existingFlatServerlessFunction; } - async restoreOneServerlessFunction(id: string) { - await this.serverlessFunctionRepository.restore({ id }); + async restoreOneServerlessFunction( + id: string, + workspaceId: string, + ): Promise { + const { flatServerlessFunctionMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatServerlessFunctionMaps'], + }, + ); + + const existingFlatServerlessFunction = flatServerlessFunctionMaps.byId[id]; + + if (!isDefined(existingFlatServerlessFunction)) { + throw new ServerlessFunctionException( + 'Serverless function to restore not found', + ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND, + ); + } + + const restoredFlatServerlessFunction: FlatServerlessFunction = { + ...existingFlatServerlessFunction, + deletedAt: null, + }; + + const validateAndBuildResult = + await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( + { + allFlatEntityOperationByMetadataName: { + serverlessFunction: { + flatEntityToCreate: [], + flatEntityToDelete: [], + flatEntityToUpdate: [restoredFlatServerlessFunction], + }, + }, + workspaceId, + isSystemBuild: false, + }, + ); + + if (isDefined(validateAndBuildResult)) { + throw new WorkspaceMigrationBuilderException( + validateAndBuildResult, + 'Multiple validation errors occurred while restoring serverless function', + ); + } + + const { flatServerlessFunctionMaps: recomputedFlatServerlessFunctionMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatServerlessFunctionMaps'], + }, + ); + + return findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityId: id, + flatEntityMaps: recomputedFlatServerlessFunctionMaps, + }); } async updateOneServerlessFunction( serverlessFunctionInput: UpdateServerlessFunctionInput, workspaceId: string, - ) { - const existingServerlessFunction = - await this.serverlessFunctionRepository.findOneOrFail({ - where: { - id: serverlessFunctionInput.id, + ): Promise { + const { flatServerlessFunctionMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { workspaceId, + flatMapsKeys: ['flatServerlessFunctionMaps'], }, + ); + + const updatedFlatServerlessFunction = + fromUpdateServerlessFunctionInputToFlatServerlessFunctionToUpdateOrThrow({ + flatServerlessFunctionMaps, + updateServerlessFunctionInput: serverlessFunctionInput, }); - await this.serverlessFunctionRepository.update( - existingServerlessFunction.id, - { - name: serverlessFunctionInput.update.name, - description: serverlessFunctionInput.update.description, - timeoutSeconds: serverlessFunctionInput.update.timeoutSeconds, - toolInputSchema: serverlessFunctionInput.update.toolInputSchema, - isTool: serverlessFunctionInput.update.isTool, - }, - ); + const validateAndBuildResult = + await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( + { + allFlatEntityOperationByMetadataName: { + serverlessFunction: { + flatEntityToCreate: [], + flatEntityToDelete: [], + flatEntityToUpdate: [updatedFlatServerlessFunction], + }, + }, + workspaceId, + isSystemBuild: false, + }, + ); - const fileFolder = getServerlessFolder({ - serverlessFunction: existingServerlessFunction, - version: 'draft', - }); + if (isDefined(validateAndBuildResult)) { + throw new WorkspaceMigrationBuilderException( + validateAndBuildResult, + 'Multiple validation errors occurred while updating serverless function', + ); + } - await this.fileStorageService.writeFolder( - serverlessFunctionInput.update.code, - fileFolder, - ); + const { flatServerlessFunctionMaps: recomputedFlatServerlessFunctionMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatServerlessFunctionMaps'], + }, + ); - return this.serverlessFunctionRepository.findOneBy({ - id: existingServerlessFunction.id, + return findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityId: updatedFlatServerlessFunction.id, + flatEntityMaps: recomputedFlatServerlessFunctionMaps, }); } @@ -390,7 +607,7 @@ export class ServerlessFunctionService { serverlessFunctionLayerId?: string; }, workspaceId: string, - ) { + ): Promise { let serverlessFunctionToCreateLayerId = serverlessFunctionInput.serverlessFunctionLayerId; @@ -403,40 +620,58 @@ export class ServerlessFunctionService { serverlessFunctionToCreateLayerId = commonServerlessFunctionLayerId; } - const createServerlessFunctionInput: CreateServerlessFunctionInput = { - ...serverlessFunctionInput, - serverlessFunctionLayerId: serverlessFunctionToCreateLayerId, - }; + const { workspaceCustomFlatApplication } = + await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( + { + workspaceId, + }, + ); - // If no toolInputSchema is provided, use the default schema - // (because the default template will be used for the code) - const toolInputSchema = isDefined(serverlessFunctionInput.toolInputSchema) - ? serverlessFunctionInput.toolInputSchema - : DEFAULT_TOOL_INPUT_SCHEMA; - - const serverlessFunctionToCreate = this.serverlessFunctionRepository.create( - { ...createServerlessFunctionInput, workspaceId, toolInputSchema }, - ); - - const createdServerlessFunction = - await this.serverlessFunctionRepository.save(serverlessFunctionToCreate); - - const draftFileFolder = getServerlessFolder({ - serverlessFunction: createdServerlessFunction, - version: 'draft', - }); - - for (const file of await getBaseTypescriptProjectFiles) { - await this.fileStorageService.write({ - file: file.content, - name: file.name, - mimeType: undefined, - folder: join(draftFileFolder, file.path), + const flatServerlessFunctionToCreate = + fromCreateServerlessFunctionInputToFlatServerlessFunction({ + createServerlessFunctionInput: { + ...serverlessFunctionInput, + serverlessFunctionLayerId: serverlessFunctionToCreateLayerId, + }, + workspaceId, + workspaceCustomApplicationId: + serverlessFunctionInput.applicationId ?? + workspaceCustomFlatApplication.id, }); + + const validateAndBuildResult = + await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( + { + allFlatEntityOperationByMetadataName: { + serverlessFunction: { + flatEntityToCreate: [flatServerlessFunctionToCreate], + flatEntityToDelete: [], + flatEntityToUpdate: [], + }, + }, + workspaceId, + isSystemBuild: false, + }, + ); + + if (isDefined(validateAndBuildResult)) { + throw new WorkspaceMigrationBuilderException( + validateAndBuildResult, + 'Multiple validation errors occurred while creating serverless function', + ); } - return this.serverlessFunctionRepository.findOneBy({ - id: createdServerlessFunction.id, + const { flatServerlessFunctionMaps: recomputedFlatServerlessFunctionMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { + workspaceId, + flatMapsKeys: ['flatServerlessFunctionMaps'], + }, + ); + + return findFlatEntityByIdInFlatEntityMapsOrThrow({ + flatEntityId: flatServerlessFunctionToCreate.id, + flatEntityMaps: recomputedFlatServerlessFunctionMaps, }); } @@ -453,24 +688,29 @@ export class ServerlessFunctionService { return; } - const serverlessFunction = - await this.serverlessFunctionRepository.findOneOrFail({ - where: { - id, + const { flatServerlessFunctionMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { workspaceId, + flatMapsKeys: ['flatServerlessFunctionMaps'], }, - }); + ); + + const flatServerlessFunction = findFlatServerlessFunctionOrThrow({ + id, + flatServerlessFunctionMaps, + }); await this.fileStorageService.copy({ from: { - folderPath: getServerlessFolder({ - serverlessFunction: serverlessFunction, + folderPath: getServerlessFolderOrThrow({ + flatServerlessFunction, version, }), }, to: { - folderPath: getServerlessFolder({ - serverlessFunction: serverlessFunction, + folderPath: getServerlessFolderOrThrow({ + flatServerlessFunction, version: 'draft', }), }, @@ -485,50 +725,51 @@ export class ServerlessFunctionService { id: string; version: string; workspaceId: string; - }) { - const serverlessFunctionToDuplicate = - await this.serverlessFunctionRepository.findOneOrFail({ - where: { - id, + }): Promise { + const { flatServerlessFunctionMaps } = + await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( + { workspaceId, + flatMapsKeys: ['flatServerlessFunctionMaps'], }, - }); + ); - const newServerlessFunction = await this.createOneServerlessFunction( + const flatServerlessFunctionToDuplicate = findFlatServerlessFunctionOrThrow( { - name: serverlessFunctionToDuplicate.name, - description: serverlessFunctionToDuplicate.description ?? undefined, - timeoutSeconds: serverlessFunctionToDuplicate.timeoutSeconds, - applicationId: serverlessFunctionToDuplicate.applicationId ?? undefined, + id, + flatServerlessFunctionMaps, + }, + ); + + const newFlatServerlessFunction = await this.createOneServerlessFunction( + { + name: flatServerlessFunctionToDuplicate.name, + description: flatServerlessFunctionToDuplicate.description ?? undefined, + timeoutSeconds: flatServerlessFunctionToDuplicate.timeoutSeconds, + applicationId: + flatServerlessFunctionToDuplicate.applicationId ?? undefined, serverlessFunctionLayerId: - serverlessFunctionToDuplicate.serverlessFunctionLayerId, + flatServerlessFunctionToDuplicate.serverlessFunctionLayerId, }, workspaceId, ); - if (!isDefined(newServerlessFunction)) { - throw new ServerlessFunctionException( - 'Failed to create new serverless function', - ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_CREATE_FAILED, - ); - } - await this.fileStorageService.copy({ from: { - folderPath: getServerlessFolder({ - serverlessFunction: serverlessFunctionToDuplicate, + folderPath: getServerlessFolderOrThrow({ + flatServerlessFunction: flatServerlessFunctionToDuplicate, version, }), }, to: { - folderPath: getServerlessFolder({ - serverlessFunction: newServerlessFunction, + folderPath: getServerlessFolderOrThrow({ + flatServerlessFunction: newFlatServerlessFunction, version: 'draft', }), }, }); - return newServerlessFunction; + return newFlatServerlessFunction; } private async throttleExecution(workspaceId: string) { diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/services/workspace-flat-serverless-function-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/services/workspace-flat-serverless-function-map-cache.service.ts index 0f94bffcc2..e1eb992f26 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/services/workspace-flat-serverless-function-map-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/services/workspace-flat-serverless-function-map-cache.service.ts @@ -12,7 +12,7 @@ import { FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/fl import { RouteTriggerEntity } from 'src/engine/metadata-modules/route-trigger/route-trigger.entity'; import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; import { FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type'; -import { fromServerlessFunctionEntityToFlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/utils/from-serverless-function-entity-to-flat-serverless-function.type'; +import { fromServerlessFunctionEntityToFlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/utils/from-serverless-function-entity-to-flat-serverless-function.util'; import { WorkspaceCache } from 'src/engine/workspace-cache/decorators/workspace-cache.decorator'; import { regroupEntitiesByRelatedEntityId } from 'src/engine/workspace-cache/utils/regroup-entities-by-related-entity-id'; import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/workspace-manager/workspace-migration/utils/add-flat-entity-to-flat-entity-maps-through-mutation-or-throw.util'; diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/find-flat-serverless-function-or-throw.util.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/find-flat-serverless-function-or-throw.util.ts new file mode 100644 index 0000000000..5d5f8333c0 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/find-flat-serverless-function-or-throw.util.ts @@ -0,0 +1,33 @@ +import { isDefined } from 'twenty-shared/utils'; + +import { type MetadataFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity-maps.type'; +import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; +import { + ServerlessFunctionException, + ServerlessFunctionExceptionCode, +} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception'; + +export const findFlatServerlessFunctionOrThrow = ({ + flatServerlessFunctionMaps, + id, +}: { + flatServerlessFunctionMaps: MetadataFlatEntityMaps<'serverlessFunction'>; + id: string; +}) => { + const flatServerlessFunction = findFlatEntityByIdInFlatEntityMaps({ + flatEntityId: id, + flatEntityMaps: flatServerlessFunctionMaps, + }); + + if ( + !isDefined(flatServerlessFunction) || + isDefined(flatServerlessFunction.deletedAt) + ) { + throw new ServerlessFunctionException( + `Serverless function with id ${id} not found`, + ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND, + ); + } + + return flatServerlessFunction; +}; 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 60414cb32e..f3dd0a28cf 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 @@ -1,5 +1,5 @@ -import { v4 } from 'uuid'; import { isDefined } from 'twenty-shared/utils'; +import { v4 } from 'uuid'; import { DEFAULT_TOOL_INPUT_SCHEMA } from 'src/engine/metadata-modules/serverless-function/constants/default-tool-input-schema.constant'; import { type CreateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input'; @@ -39,7 +39,7 @@ export const fromCreateServerlessFunctionInputToFlatServerlessFunction = ({ handlerName: rawCreateServerlessFunctionInput.handlerName ?? DEFAULT_HANDLER_NAME, universalIdentifier: - rawCreateServerlessFunctionInput.universalIdentifier ?? id, + rawCreateServerlessFunctionInput.universalIdentifier ?? v4(), createdAt: currentDate.toISOString(), updatedAt: currentDate.toISOString(), deletedAt: null, 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 new file mode 100644 index 0000000000..81b2c4c1fa --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/from-flat-serverless-function-to-serverless-function-dto.util.ts @@ -0,0 +1,59 @@ +import { isDefined } from 'twenty-shared/utils'; + +import { type FlatCronTrigger } from 'src/engine/metadata-modules/cron-trigger/types/flat-cron-trigger.type'; +import { fromFlatCronTriggerToCronTriggerDto } from 'src/engine/metadata-modules/cron-trigger/utils/from-flat-cron-trigger-to-cron-trigger-dto.util'; +import { type FlatDatabaseEventTrigger } from 'src/engine/metadata-modules/database-event-trigger/types/flat-database-event-trigger.type'; +import { fromFlatDatabaseEventTriggerToDatabaseEventTriggerDto } from 'src/engine/metadata-modules/database-event-trigger/utils/from-flat-database-event-trigger-to-database-event-trigger-dto.util'; +import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type FlatRouteTrigger } from 'src/engine/metadata-modules/route-trigger/types/flat-route-trigger.type'; +import { fromFlatRouteTriggerToRouteTriggerDto } from 'src/engine/metadata-modules/route-trigger/utils/from-flat-route-trigger-to-route-trigger-dto.util'; +import { type ServerlessFunctionDTO } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto'; +import { type FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type'; + +export const fromFlatServerlessFunctionToServerlessFunctionDto = ({ + flatServerlessFunction, + flatCronTriggerMaps, + flatDatabaseEventTriggerMaps, + flatRouteTriggerMaps, +}: { + flatServerlessFunction: FlatServerlessFunction; + flatCronTriggerMaps: FlatEntityMaps; + flatDatabaseEventTriggerMaps: FlatEntityMaps; + flatRouteTriggerMaps: FlatEntityMaps; +}): ServerlessFunctionDTO => { + const cronTriggers = flatServerlessFunction.cronTriggerIds + .map((id) => flatCronTriggerMaps.byId[id]) + .filter(isDefined) + .map(fromFlatCronTriggerToCronTriggerDto); + + const databaseEventTriggers = flatServerlessFunction.databaseEventTriggerIds + .map((id) => flatDatabaseEventTriggerMaps.byId[id]) + .filter(isDefined) + .map(fromFlatDatabaseEventTriggerToDatabaseEventTriggerDto); + + const routeTriggers = flatServerlessFunction.routeTriggerIds + .map((id) => flatRouteTriggerMaps.byId[id]) + .filter(isDefined) + .map(fromFlatRouteTriggerToRouteTriggerDto); + + return { + id: flatServerlessFunction.id, + name: flatServerlessFunction.name, + description: flatServerlessFunction.description ?? undefined, + runtime: flatServerlessFunction.runtime, + timeoutSeconds: flatServerlessFunction.timeoutSeconds, + latestVersion: flatServerlessFunction.latestVersion ?? undefined, + handlerPath: flatServerlessFunction.handlerPath, + handlerName: flatServerlessFunction.handlerName, + publishedVersions: flatServerlessFunction.publishedVersions, + toolInputSchema: flatServerlessFunction.toolInputSchema ?? undefined, + isTool: flatServerlessFunction.isTool, + applicationId: flatServerlessFunction.applicationId ?? undefined, + workspaceId: flatServerlessFunction.workspaceId, + createdAt: new Date(flatServerlessFunction.createdAt), + updatedAt: new Date(flatServerlessFunction.updatedAt), + cronTriggers, + databaseEventTriggers, + routeTriggers, + }; +}; diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/from-serverless-function-entity-to-flat-serverless-function.type.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/from-serverless-function-entity-to-flat-serverless-function.util.ts similarity index 100% rename from packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/from-serverless-function-entity-to-flat-serverless-function.type.ts rename to packages/twenty-server/src/engine/metadata-modules/serverless-function/utils/from-serverless-function-entity-to-flat-serverless-function.util.ts 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 511372aa18..dd8dc1b644 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 @@ -1,18 +1,13 @@ -import { t } from '@lingui/core/macro'; import { extractAndSanitizeObjectStringFields, - isDefined, trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties, } from 'twenty-shared/utils'; -import { type FlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-maps.type'; +import { type MetadataFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity-maps.type'; import { FLAT_SERVERLESS_FUNCTION_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/serverless-function/constants/flat-serverless-function-editable-properties.constant'; import { type UpdateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input'; -import { - ServerlessFunctionException, - 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 { findFlatServerlessFunctionOrThrow } from 'src/engine/metadata-modules/serverless-function/utils/find-flat-serverless-function-or-throw.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'; @@ -22,7 +17,7 @@ export const fromUpdateServerlessFunctionInputToFlatServerlessFunctionToUpdateOr flatServerlessFunctionMaps, }: { updateServerlessFunctionInput: UpdateServerlessFunctionInput; - flatServerlessFunctionMaps: FlatEntityMaps; + flatServerlessFunctionMaps: MetadataFlatEntityMaps<'serverlessFunction'>; }): FlatServerlessFunction => { const { id: serverlessFunctionToUpdateId } = trimAndRemoveDuplicatedWhitespacesFromObjectStringProperties( @@ -31,14 +26,10 @@ export const fromUpdateServerlessFunctionInputToFlatServerlessFunctionToUpdateOr ); const existingFlatServerlessFunctionToUpdate = - flatServerlessFunctionMaps.byId[serverlessFunctionToUpdateId]; - - if (!isDefined(existingFlatServerlessFunctionToUpdate)) { - throw new ServerlessFunctionException( - t`Serverless function to update not found`, - ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND, - ); - } + findFlatServerlessFunctionOrThrow({ + id: serverlessFunctionToUpdateId, + flatServerlessFunctionMaps, + }); const updatedEditableFieldProperties = { ...extractAndSanitizeObjectStringFields( { diff --git a/packages/twenty-server/src/engine/workspace-cache/types/workspace-cache-key.type.ts b/packages/twenty-server/src/engine/workspace-cache/types/workspace-cache-key.type.ts index 1cbf4d7b3e..b4d8b8c6bc 100644 --- a/packages/twenty-server/src/engine/workspace-cache/types/workspace-cache-key.type.ts +++ b/packages/twenty-server/src/engine/workspace-cache/types/workspace-cache-key.type.ts @@ -2,6 +2,7 @@ import { type ObjectsPermissionsByRoleId } from 'twenty-shared/types'; import { type EntityMetadata } from 'typeorm'; import { type FlatApplicationCacheMaps } from 'src/engine/core-modules/application/types/flat-application-cache-maps.type'; +import { type ApplicationVariableCacheMaps } from 'src/engine/core-modules/applicationVariable/types/application-variable-cache-maps.type'; import { type FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; import { type FlatWorkspaceMemberMaps } from 'src/engine/core-modules/user/types/flat-workspace-member-maps.type'; import { type FlatRoleTargetByAgentIdMaps } from 'src/engine/metadata-modules/flat-agent/types/flat-role-target-by-agent-id-maps.type'; @@ -9,6 +10,7 @@ import { type AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/ import { type UserWorkspaceRoleMap } from 'src/engine/metadata-modules/role-target/services/workspace-user-workspace-role-map-cache.service'; import { type FlatRowLevelPermissionPredicateGroupMaps } from 'src/engine/metadata-modules/row-level-permission-predicate/types/flat-row-level-permission-predicate-group-maps.type'; import { type FlatRowLevelPermissionPredicateMaps } from 'src/engine/metadata-modules/row-level-permission-predicate/types/flat-row-level-permission-predicate-maps.type'; +import { type ServerlessFunctionLayerCacheMaps } from 'src/engine/metadata-modules/serverless-function-layer/types/serverless-function-layer-cache-maps.type'; export const WORKSPACE_CACHE_KEYS_V2 = { flatObjectMetadataMaps: 'flat-maps:object-metadata', @@ -44,6 +46,8 @@ export const WORKSPACE_CACHE_KEYS_V2 = { 'flat-maps:row-level-permission-predicate-group', flatFrontComponentMaps: 'flat-maps:front-component', flatWorkspaceMemberMaps: 'flat-maps:workspace-member', + serverlessFunctionLayerMaps: 'cache:serverless-function-layer', + applicationVariableMaps: 'cache:application-variable', } as const satisfies Record; export type AdditionalCacheDataMaps = { @@ -57,6 +61,8 @@ export type AdditionalCacheDataMaps = { flatRowLevelPermissionPredicateMaps: FlatRowLevelPermissionPredicateMaps; flatRowLevelPermissionPredicateGroupMaps: FlatRowLevelPermissionPredicateGroupMaps; flatWorkspaceMemberMaps: FlatWorkspaceMemberMaps; + serverlessFunctionLayerMaps: ServerlessFunctionLayerCacheMaps; + applicationVariableMaps: ApplicationVariableCacheMaps; }; export type WorkspaceCacheDataMap = AllFlatEntityMaps & AdditionalCacheDataMaps; 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 a0c310845f..1e866faf6a 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 @@ -8,7 +8,7 @@ import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-mana import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; import { getBaseTypescriptProjectFiles } from 'src/engine/core-modules/serverless/drivers/utils/get-base-typescript-project-files'; -import { getServerlessFolder } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils'; +import { getServerlessFolderOrThrow } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils'; import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; 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'; @@ -38,8 +38,8 @@ export class CreateServerlessFunctionActionHandlerService extends WorkspaceMigra workspaceId, }); - const draftFileFolder = getServerlessFolder({ - serverlessFunction, + const draftFileFolder = getServerlessFolderOrThrow({ + flatServerlessFunction: serverlessFunction, version: 'draft', }); @@ -66,8 +66,8 @@ export class CreateServerlessFunctionActionHandlerService extends WorkspaceMigra const { action } = context; await this.fileStorageService.delete({ - folderPath: getServerlessFolder({ - serverlessFunction: action.flatEntity, + folderPath: getServerlessFolderOrThrow({ + flatServerlessFunction: action.flatEntity, }), }); } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/serverless-function/services/delete-serverless-function-action-handler.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/serverless-function/services/delete-serverless-function-action-handler.service.ts index 61bc655b95..23aa9755b5 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/serverless-function/services/delete-serverless-function-action-handler.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/serverless-function/services/delete-serverless-function-action-handler.service.ts @@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common'; import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/interfaces/workspace-migration-runner-action-handler-service.interface'; import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; -import { getServerlessFolder } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils'; +import { getServerlessFolderOrThrow } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils'; import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; import { DeleteServerlessFunctionAction } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/serverless-function/types/workspace-migration-serverless-function-action.type'; @@ -43,13 +43,13 @@ export class DeleteServerlessFunctionActionHandlerService extends WorkspaceMigra // TODO: Should implement a cron task or a job to delete the files after a certain period of time await this.fileStorageService.move({ from: { - folderPath: getServerlessFolder({ - serverlessFunction: existingServerlessFunction, + folderPath: getServerlessFolderOrThrow({ + flatServerlessFunction: existingServerlessFunction, }), }, to: { - folderPath: getServerlessFolder({ - serverlessFunction: existingServerlessFunction, + folderPath: getServerlessFolderOrThrow({ + flatServerlessFunction: existingServerlessFunction, toDelete: true, }), }, @@ -70,14 +70,14 @@ export class DeleteServerlessFunctionActionHandlerService extends WorkspaceMigra await this.fileStorageService.move({ from: { - folderPath: getServerlessFolder({ - serverlessFunction: existingServerlessFunction, + folderPath: getServerlessFolderOrThrow({ + flatServerlessFunction: existingServerlessFunction, toDelete: true, }), }, to: { - folderPath: getServerlessFolder({ - serverlessFunction: existingServerlessFunction, + folderPath: getServerlessFolderOrThrow({ + flatServerlessFunction: existingServerlessFunction, toDelete: false, }), }, 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 df15270459..68236fe55c 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 @@ -7,7 +7,7 @@ import { WorkspaceMigrationRunnerActionHandler } from 'src/engine/workspace-mana import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; import { ServerlessService } from 'src/engine/core-modules/serverless/serverless.service'; -import { getServerlessFolder } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils'; +import { getServerlessFolderOrThrow } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils'; import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; import { FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type'; @@ -43,7 +43,7 @@ export class UpdateServerlessFunctionActionHandlerService extends WorkspaceMigra fromFlatEntityPropertiesUpdatesToPartialFlatEntity(action), ); - const serverlessFunction = findFlatEntityByIdInFlatEntityMapsOrThrow({ + const flatServerlessFunction = findFlatEntityByIdInFlatEntityMapsOrThrow({ flatEntityId: entityId, flatEntityMaps: context.allFlatEntityMaps.flatServerlessFunctionMaps, }); @@ -51,37 +51,35 @@ export class UpdateServerlessFunctionActionHandlerService extends WorkspaceMigra for (const update of action.updates) { if (update.property === 'checksum' && isDefined(code)) { await this.handleChecksumUpdate({ - serverlessFunction, + flatServerlessFunction, code, }); } if (update.property === 'deletedAt' && isDefined(update.to)) { await this.handleDeletedAtUpdate({ - serverlessFunction, + flatServerlessFunction, }); } } } async handleDeletedAtUpdate({ - serverlessFunction, + flatServerlessFunction, }: { - serverlessFunction: FlatServerlessFunction; + flatServerlessFunction: FlatServerlessFunction; }) { - this.serverlessService.delete( - serverlessFunction as unknown as ServerlessFunctionEntity, - ); + this.serverlessService.delete(flatServerlessFunction); } async handleChecksumUpdate({ - serverlessFunction, + flatServerlessFunction, code, }: { - serverlessFunction: FlatServerlessFunction; + flatServerlessFunction: FlatServerlessFunction; code: Sources; }) { - const fileFolder = getServerlessFolder({ - serverlessFunction, + const fileFolder = getServerlessFolderOrThrow({ + flatServerlessFunction, version: 'draft', }); diff --git a/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-common.workspace-service.ts b/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-common.workspace-service.ts index ed44369c08..82fa308abd 100644 --- a/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-common.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/common/workspace-services/workflow-common.workspace-service.ts @@ -327,6 +327,7 @@ export class WorkflowCommonWorkspaceService { case 'restore': await this.serverlessFunctionService.restoreOneServerlessFunction( step.settings.input.serverlessFunctionId, + workspaceId, ); break; case 'destroy': 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 ec14c30d53..1c0ac4f3fc 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 @@ -6,8 +6,9 @@ import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/ag import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { RoleTargetEntity } from 'src/engine/metadata-modules/role-target/role-target.entity'; import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity'; -import { type ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; +import { ServerlessFunctionRuntime } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service'; +import { type FlatServerlessFunction } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type'; import { GlobalWorkspaceOrmManager } from 'src/engine/twenty-orm/global-workspace-datasource/global-workspace-orm.manager'; import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service'; import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; @@ -238,29 +239,33 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => { describe('runStepCreationSideEffectsAndBuildStep', () => { it('should create code step with serverless function', async () => { - const mockServerlessFunction = { + const mockFlatServerlessFunction: FlatServerlessFunction = { id: 'new-function-id', name: 'Test Function', description: 'Test Description', latestVersion: 'v1', publishedVersions: [], workspaceId: mockWorkspaceId, - createdAt: new Date(), - updatedAt: new Date(), + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), deletedAt: null, - isActive: true, - isSystem: false, - isCustom: true, - isPublic: false, - runtime: 'nodejs', + runtime: ServerlessFunctionRuntime.NODE22, timeoutSeconds: 30, - layerArn: '', - layerName: '', - layerSize: 0, - } as unknown as ServerlessFunctionEntity; + handlerPath: 'src/index.ts', + handlerName: 'main', + checksum: null, + toolInputSchema: null, + isTool: false, + serverlessFunctionLayerId: 'layer-id', + universalIdentifier: 'universal-id', + applicationId: null, + cronTriggerIds: [], + databaseEventTriggerIds: [], + routeTriggerIds: [], + }; serverlessFunctionService.createOneServerlessFunction.mockResolvedValue( - mockServerlessFunction, + mockFlatServerlessFunction, ); const result = await service.runStepCreationSideEffectsAndBuildStep({ @@ -313,29 +318,33 @@ describe('WorkflowVersionStepOperationsWorkspaceService', () => { nextStepIds: ['next-step'], } as unknown as WorkflowAction; - const mockNewServerlessFunction = { + const mockNewFlatServerlessFunction: FlatServerlessFunction = { id: 'new-function-id', name: 'Test Function', description: 'Test Description', latestVersion: 'v1', publishedVersions: [], workspaceId: mockWorkspaceId, - createdAt: new Date(), - updatedAt: new Date(), + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), deletedAt: null, - isActive: true, - isSystem: false, - isCustom: true, - isPublic: false, - runtime: 'nodejs', + runtime: ServerlessFunctionRuntime.NODE22, timeoutSeconds: 30, - layerArn: '', - layerName: '', - layerSize: 0, - } as unknown as ServerlessFunctionEntity; + handlerPath: 'src/index.ts', + handlerName: 'main', + checksum: null, + toolInputSchema: null, + isTool: false, + serverlessFunctionLayerId: 'layer-id', + universalIdentifier: 'universal-id', + applicationId: null, + cronTriggerIds: [], + databaseEventTriggerIds: [], + routeTriggerIds: [], + }; serverlessFunctionService.duplicateServerlessFunction.mockResolvedValue( - mockNewServerlessFunction, + mockNewFlatServerlessFunction, ); const clonedStep = await service.cloneStep({ diff --git a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-operations.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-operations.workspace-service.ts index f5f49d9fdf..0748536f2f 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-operations.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-builder/workflow-version-step/workflow-version-step-operations.workspace-service.ts @@ -86,6 +86,7 @@ export class WorkflowVersionStepOperationsWorkspaceService { if ( !(await this.serverlessFunctionService.hasServerlessFunctionPublishedVersion( step.settings.input.serverlessFunctionId, + workspaceId, )) ) { await this.serverlessFunctionService.deleteOneServerlessFunction({