Migrate serverless function service to v2 (#17285)
# Introduction In this PR we're migrating the serverless function service that was using the SF repo directly to the v2 build and runner. The whole serverless engine now deals with flat entities only ## Resolvers Refactored the resolvers ( serverlessFunction, route, database and cron trigger) : - return types to `dto` - Standardized the flat to dto transpilation within the resolvers - Find and findMany passing by the cached data ## Services Refactored the services ( serverlessFunction, route, database and cron trigger) : - return type to be `flat` - always calling v2 and computing cache ## New additional caches - application variables ( cf https://github.com/twentyhq/core-team-issues/issues/2116 ) - serverless function layer ## What to test: - CRUD ( database trigger ✅ , route trigger, cron trigger, serverless function through workflows ✅ ) - Duplicating a workflow with a serverless function code node ✅ ## Concerns We need to implement the cron that will hard delete soft deleted s3 serverless functions, not in this PR though ( cf https://github.com/twentyhq/twenty/pull/17285#discussion_r2709168570 and https://github.com/twentyhq/core-team-issues/issues/2118 )
This commit is contained in:
@@ -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,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
+11
-2
@@ -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 {}
|
||||
|
||||
+9
-1
@@ -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;
|
||||
}
|
||||
|
||||
+16
@@ -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<ApplicationVariableEntity>,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
) {}
|
||||
|
||||
async update({
|
||||
key,
|
||||
value,
|
||||
applicationId,
|
||||
workspaceId,
|
||||
}: Pick<ApplicationVariableEntity, 'key' | 'value'> & {
|
||||
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',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
+68
@@ -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<ApplicationVariableCacheMaps> {
|
||||
constructor(
|
||||
@InjectRepository(ApplicationVariableEntity)
|
||||
private readonly applicationVariableRepository: Repository<ApplicationVariableEntity>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async computeForCache(
|
||||
workspaceId: string,
|
||||
): Promise<ApplicationVariableCacheMaps> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
import { type FlatApplicationVariable } from 'src/engine/core-modules/applicationVariable/types/flat-application-variable.type';
|
||||
|
||||
export type ApplicationVariableCacheMaps = {
|
||||
byId: Partial<Record<string, FlatApplicationVariable>>;
|
||||
byApplicationId: Partial<Record<string, FlatApplicationVariable[]>>;
|
||||
};
|
||||
+4
@@ -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<ApplicationVariableEntity>;
|
||||
+15
@@ -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(),
|
||||
});
|
||||
+7
-5
@@ -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<void>;
|
||||
delete(flatServerlessFunction: FlatServerlessFunction): Promise<void>;
|
||||
execute({
|
||||
serverlessFunction,
|
||||
flatServerlessFunction,
|
||||
flatServerlessFunctionLayer,
|
||||
payload,
|
||||
version,
|
||||
env,
|
||||
}: {
|
||||
serverlessFunction: ServerlessFunctionEntity;
|
||||
flatServerlessFunction: FlatServerlessFunction;
|
||||
flatServerlessFunctionLayer: FlatServerlessFunctionLayer;
|
||||
payload: object;
|
||||
version: string;
|
||||
env?: Record<string, string>;
|
||||
|
||||
+55
-36
@@ -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<string> {
|
||||
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<string, string>;
|
||||
}): Promise<ServerlessExecuteResult> {
|
||||
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,
|
||||
};
|
||||
|
||||
@@ -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<string, string>;
|
||||
}): Promise<ServerlessExecuteResult> {
|
||||
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 } =
|
||||
|
||||
+8
-6
@@ -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<string, string> => {
|
||||
return flatApplicationVariables.reduce<Record<string, string>>(
|
||||
(acc, flatApplicationVariable) => {
|
||||
acc[flatApplicationVariable.key] = flatApplicationVariable.value;
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string>,
|
||||
{},
|
||||
);
|
||||
};
|
||||
|
||||
+4
-4
@@ -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'),
|
||||
|
||||
@@ -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<void> {
|
||||
return this.driver.delete(serverlessFunction);
|
||||
async delete(flatServerlessFunction: FlatServerlessFunction): Promise<void> {
|
||||
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<string, string>;
|
||||
}): Promise<ServerlessExecuteResult> {
|
||||
return this.driver.execute({ serverlessFunction, payload, version, env });
|
||||
return this.driver.execute({
|
||||
flatServerlessFunction,
|
||||
flatServerlessFunctionLayer,
|
||||
payload,
|
||||
version,
|
||||
env,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+10
-8
@@ -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<unknown>);
|
||||
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 || '',
|
||||
);
|
||||
};
|
||||
|
||||
+65
-26
@@ -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<CronTriggerEntity>,
|
||||
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
) {}
|
||||
|
||||
@Query(() => CronTriggerDTO)
|
||||
async findOneCronTrigger(
|
||||
@Args('input') { id }: CronTriggerIdInput,
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
) {
|
||||
): Promise<CronTriggerDTO> {
|
||||
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<CronTriggerDTO[]> {
|
||||
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<CronTriggerDTO> {
|
||||
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<CronTriggerDTO> {
|
||||
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<CronTriggerDTO> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -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<FlatCronTrigger> {
|
||||
const { workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{
|
||||
@@ -90,7 +90,7 @@ export class CronTriggerV2Service {
|
||||
async updateOne(
|
||||
cronTriggerInput: UpdateCronTriggerInput,
|
||||
workspaceId: string,
|
||||
) {
|
||||
): Promise<FlatCronTrigger> {
|
||||
const { flatCronTriggerMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
|
||||
+1
-1
@@ -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:
|
||||
|
||||
+11
@@ -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),
|
||||
});
|
||||
+71
-33
@@ -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<DatabaseEventTriggerEntity>,
|
||||
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
) {}
|
||||
|
||||
@Query(() => DatabaseEventTriggerDTO)
|
||||
async findOneDatabaseEventTrigger(
|
||||
@Args('input') { id }: DatabaseEventTriggerIdInput,
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
) {
|
||||
): Promise<DatabaseEventTriggerDTO> {
|
||||
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<DatabaseEventTriggerDTO[]> {
|
||||
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<DatabaseEventTriggerDTO> {
|
||||
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<DatabaseEventTriggerDTO> {
|
||||
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<DatabaseEventTriggerDTO> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -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<FlatDatabaseEventTrigger> {
|
||||
const { workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{
|
||||
@@ -92,7 +92,7 @@ export class DatabaseEventTriggerV2Service {
|
||||
async updateOne(
|
||||
databaseEventTriggerInput: UpdateDatabaseEventTriggerInput,
|
||||
workspaceId: string,
|
||||
) {
|
||||
): Promise<FlatDatabaseEventTrigger> {
|
||||
const { flatDatabaseEventTriggerMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
|
||||
+1
-1
@@ -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:
|
||||
|
||||
+11
@@ -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),
|
||||
});
|
||||
+4
-2
@@ -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],
|
||||
|
||||
+65
-26
@@ -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<RouteTriggerEntity>,
|
||||
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
) {}
|
||||
|
||||
@Query(() => RouteTriggerDTO)
|
||||
async findOneRouteTrigger(
|
||||
@Args('input') { id }: RouteTriggerIdInput,
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
) {
|
||||
): Promise<RouteTriggerDTO> {
|
||||
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<RouteTriggerDTO[]> {
|
||||
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<RouteTriggerDTO> {
|
||||
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<RouteTriggerDTO> {
|
||||
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<RouteTriggerDTO> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -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<FlatRouteTrigger> {
|
||||
const { workspaceCustomFlatApplication } =
|
||||
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
|
||||
{
|
||||
@@ -89,7 +89,7 @@ export class RouteTriggerV2Service {
|
||||
async updateOne(
|
||||
routeTriggerInput: UpdateRouteTriggerInput,
|
||||
workspaceId: string,
|
||||
) {
|
||||
): Promise<FlatRouteTrigger> {
|
||||
const { flatRouteTriggerMaps } =
|
||||
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
|
||||
+14
@@ -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),
|
||||
});
|
||||
+1
-1
@@ -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:
|
||||
|
||||
+12
-2
@@ -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 {}
|
||||
|
||||
+22
-2
@@ -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<ServerlessFunctionLayerEntity>,
|
||||
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<ServerlessFunctionLayerEntity>,
|
||||
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) {
|
||||
|
||||
+47
@@ -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<ServerlessFunctionLayerCacheMaps> {
|
||||
constructor(
|
||||
@InjectRepository(ServerlessFunctionLayerEntity)
|
||||
private readonly serverlessFunctionLayerRepository: Repository<ServerlessFunctionLayerEntity>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async computeForCache(
|
||||
workspaceId: string,
|
||||
): Promise<ServerlessFunctionLayerCacheMaps> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
+5
@@ -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<ServerlessFunctionLayerEntity>;
|
||||
+5
@@ -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<Record<string, FlatServerlessFunctionLayer>>;
|
||||
};
|
||||
+15
@@ -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) ?? [],
|
||||
});
|
||||
+2
@@ -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: [
|
||||
|
||||
+198
-50
@@ -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<ServerlessFunctionEntity>,
|
||||
private readonly subscriptionService: SubscriptionService,
|
||||
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
) {}
|
||||
|
||||
@Query(() => ServerlessFunctionDTO)
|
||||
async findOneServerlessFunction(
|
||||
@Args('input') { id }: ServerlessFunctionIdInput,
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
) {
|
||||
): Promise<ServerlessFunctionDTO> {
|
||||
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<ServerlessFunctionDTO[]> {
|
||||
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<ServerlessFunctionDTO> {
|
||||
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<ServerlessFunctionDTO> {
|
||||
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<ServerlessFunctionDTO> {
|
||||
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<ServerlessFunctionDTO> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+431
-190
@@ -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<Sources | undefined> {
|
||||
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<ServerlessExecuteResult> {
|
||||
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<FlatServerlessFunction> {
|
||||
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<FlatServerlessFunction> {
|
||||
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<FlatServerlessFunction> {
|
||||
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<FlatServerlessFunction> {
|
||||
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<FlatServerlessFunction> {
|
||||
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<FlatServerlessFunction> {
|
||||
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) {
|
||||
|
||||
+1
-1
@@ -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';
|
||||
|
||||
+33
@@ -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;
|
||||
};
|
||||
+2
-2
@@ -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,
|
||||
|
||||
+59
@@ -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<FlatCronTrigger>;
|
||||
flatDatabaseEventTriggerMaps: FlatEntityMaps<FlatDatabaseEventTrigger>;
|
||||
flatRouteTriggerMaps: FlatEntityMaps<FlatRouteTrigger>;
|
||||
}): 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,
|
||||
};
|
||||
};
|
||||
+7
-16
@@ -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<FlatServerlessFunction>;
|
||||
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(
|
||||
{
|
||||
|
||||
@@ -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<WorkspaceCacheKeyName, string>;
|
||||
|
||||
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;
|
||||
|
||||
+5
-5
@@ -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,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
+9
-9
@@ -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,
|
||||
}),
|
||||
},
|
||||
|
||||
+11
-13
@@ -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',
|
||||
});
|
||||
|
||||
|
||||
+1
@@ -327,6 +327,7 @@ export class WorkflowCommonWorkspaceService {
|
||||
case 'restore':
|
||||
await this.serverlessFunctionService.restoreOneServerlessFunction(
|
||||
step.settings.input.serverlessFunctionId,
|
||||
workspaceId,
|
||||
);
|
||||
break;
|
||||
case 'destroy':
|
||||
|
||||
+36
-27
@@ -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({
|
||||
|
||||
+1
@@ -86,6 +86,7 @@ export class WorkflowVersionStepOperationsWorkspaceService {
|
||||
if (
|
||||
!(await this.serverlessFunctionService.hasServerlessFunctionPublishedVersion(
|
||||
step.settings.input.serverlessFunctionId,
|
||||
workspaceId,
|
||||
))
|
||||
) {
|
||||
await this.serverlessFunctionService.deleteOneServerlessFunction({
|
||||
|
||||
Reference in New Issue
Block a user