Serverless function UI (#6388)

https://www.figma.com/design/xt8O9mFeLl46C5InWwoMrN/Twenty?node-id=36235-120877

Did not do the file manager part. A Function is defined using one unique
file at the moment

Feature protected by featureFlag `IS_FUNCTION_SETTINGS_ENABLED`

## Demo


https://github.com/user-attachments/assets/0acb8291-47b4-4521-a6fa-a88b9198609b
This commit is contained in:
martmull
2024-07-29 13:03:09 +02:00
committed by GitHub
parent 936279f895
commit 00fea17920
100 changed files with 2283 additions and 121 deletions

View File

@ -1,6 +1,7 @@
import { Readable } from 'stream';
export interface StorageDriver {
delete(params: { folderPath: string; filename?: string }): Promise<void>;
read(params: { folderPath: string; filename: string }): Promise<Readable>;
write(params: {
file: Buffer | Uint8Array | string;

View File

@ -42,6 +42,19 @@ export class LocalDriver implements StorageDriver {
await fs.writeFile(filePath, params.file);
}
async delete(params: {
folderPath: string;
filename?: string;
}): Promise<void> {
const filePath = join(
`${this.options.storagePath}/`,
params.folderPath,
params.filename || '',
);
await fs.rm(filePath, { recursive: true });
}
async read(params: {
folderPath: string;
filename: string;

View File

@ -2,8 +2,11 @@ import { Readable } from 'stream';
import {
CreateBucketCommandInput,
DeleteObjectCommand,
DeleteObjectsCommand,
GetObjectCommand,
HeadBucketCommandInput,
ListObjectsV2Command,
NotFound,
PutObjectCommand,
S3,
@ -53,6 +56,57 @@ export class S3Driver implements StorageDriver {
await this.s3Client.send(command);
}
private async emptyS3Directory(folderPath) {
const listParams = {
Bucket: this.bucketName,
Prefix: folderPath,
};
const listObjectsCommand = new ListObjectsV2Command(listParams);
const listedObjects = await this.s3Client.send(listObjectsCommand);
if (listedObjects.Contents?.length === 0) return;
const deleteParams = {
Bucket: this.bucketName,
Delete: {
Objects: listedObjects.Contents?.map(({ Key }) => {
return { Key };
}),
},
};
const deleteObjectCommand = new DeleteObjectsCommand(deleteParams);
await this.s3Client.send(deleteObjectCommand);
if (listedObjects.IsTruncated) {
await this.emptyS3Directory(folderPath);
}
}
async delete(params: {
folderPath: string;
filename?: string;
}): Promise<void> {
if (params.filename) {
const deleteCommand = new DeleteObjectCommand({
Key: `${params.folderPath}/${params.filename}`,
Bucket: this.bucketName,
});
await this.s3Client.send(deleteCommand);
} else {
await this.emptyS3Directory(params.folderPath);
const deleteEmptyFolderCommand = new DeleteObjectCommand({
Key: `${params.folderPath}`,
Bucket: this.bucketName,
});
await this.s3Client.send(deleteEmptyFolderCommand);
}
}
async read(params: {
folderPath: string;
filename: string;

View File

@ -10,6 +10,10 @@ import { StorageDriver } from './drivers/interfaces/storage-driver.interface';
export class FileStorageService implements StorageDriver {
constructor(@Inject(STORAGE_DRIVER) private driver: StorageDriver) {}
delete(params: { folderPath: string; filename?: string }): Promise<void> {
return this.driver.delete(params);
}
write(params: {
file: string | Buffer | Uint8Array;
name: string;

View File

@ -1,6 +1,7 @@
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
export interface ServerlessDriver {
delete(serverlessFunction: ServerlessFunctionEntity): Promise<void>;
build(serverlessFunction: ServerlessFunctionEntity): Promise<void>;
execute(
serverlessFunction: ServerlessFunctionEntity,

View File

@ -5,8 +5,12 @@ import {
Lambda,
LambdaClientConfig,
InvokeCommand,
GetFunctionCommand,
UpdateFunctionCodeCommand,
DeleteFunctionCommand,
} from '@aws-sdk/client-lambda';
import { CreateFunctionCommandInput } from '@aws-sdk/client-lambda/dist-types/commands/CreateFunctionCommand';
import { UpdateFunctionCodeCommandInput } from '@aws-sdk/client-lambda/dist-types/commands/UpdateFunctionCodeCommand';
import { ServerlessDriver } from 'src/engine/integrations/serverless/drivers/interfaces/serverless-driver.interface';
@ -42,6 +46,18 @@ export class LambdaDriver
this.buildDirectoryManagerService = options.buildDirectoryManagerService;
}
async delete(serverlessFunction: ServerlessFunctionEntity) {
try {
const deleteFunctionCommand = new DeleteFunctionCommand({
FunctionName: serverlessFunction.id,
});
await this.lambdaClient.send(deleteFunctionCommand);
} catch {
return;
}
}
async build(serverlessFunction: ServerlessFunctionEntity) {
const javascriptCode = await this.getCompiledCode(
serverlessFunction,
@ -59,21 +75,44 @@ export class LambdaDriver
await createZipFile(sourceTemporaryDir, lambdaZipPath);
const params: CreateFunctionCommandInput = {
Code: {
let existingFunction = true;
try {
const getFunctionCommand = new GetFunctionCommand({
FunctionName: serverlessFunction.id,
});
await this.lambdaClient.send(getFunctionCommand);
} catch {
existingFunction = false;
}
if (!existingFunction) {
const params: CreateFunctionCommandInput = {
Code: {
ZipFile: await fs.promises.readFile(lambdaZipPath),
},
FunctionName: serverlessFunction.id,
Handler: lambdaHandler,
Role: this.lambdaRole,
Runtime: serverlessFunction.runtime,
Description: 'Lambda function to run user script',
Timeout: 900,
};
const command = new CreateFunctionCommand(params);
await this.lambdaClient.send(command);
} else {
const params: UpdateFunctionCodeCommandInput = {
ZipFile: await fs.promises.readFile(lambdaZipPath),
},
FunctionName: serverlessFunction.id,
Handler: lambdaHandler,
Role: this.lambdaRole,
Runtime: 'nodejs18.x',
Description: 'Lambda function to run user script',
Timeout: 900,
};
FunctionName: serverlessFunction.id,
};
const command = new CreateFunctionCommand(params);
const command = new UpdateFunctionCodeCommand(params);
await this.lambdaClient.send(command);
await this.lambdaClient.send(command);
}
await this.buildDirectoryManagerService.clean();
}

View File

@ -28,6 +28,12 @@ export class LocalDriver
this.fileStorageService = options.fileStorageService;
}
async delete(serverlessFunction: ServerlessFunctionEntity) {
await this.fileStorageService.delete({
folderPath: this.getFolderPath(serverlessFunction),
});
}
async build(serverlessFunction: ServerlessFunctionEntity) {
const javascriptCode = await this.getCompiledCode(
serverlessFunction,
@ -57,8 +63,16 @@ export class LocalDriver
const modifiedContent = `
process.on('message', async (message) => {
const { event, context } = message;
const result = await handler(event, context);
process.send(result);
try {
const result = await handler(event, context);
process.send(result);
} catch (error) {
process.send({
errorType: error.name,
errorMessage: error.message,
stackTrace: error.stack.split('\\n').filter((line) => line.trim() !== ''),
});
}
});
${fileContent}
@ -67,7 +81,7 @@ export class LocalDriver
await fs.writeFile(tmpFilePath, modifiedContent);
return await new Promise((resolve, reject) => {
const child = fork(tmpFilePath);
const child = fork(tmpFilePath, { silent: true });
child.on('message', (message: object) => {
resolve(message);
@ -75,6 +89,32 @@ export class LocalDriver
fs.unlink(tmpFilePath);
});
child.stderr?.on('data', (data) => {
const stackTrace = data
.toString()
.split('\n')
.filter((line) => line.trim() !== '');
const errorTrace = stackTrace.filter((line) =>
line.includes('Error: '),
)?.[0];
let errorType = 'Unknown';
let errorMessage = '';
if (errorTrace) {
errorType = errorTrace.split(':')[0];
errorMessage = errorTrace.split(': ')[1];
}
resolve({
errorType,
errorMessage,
stackTrace: stackTrace,
});
child.kill();
fs.unlink(tmpFilePath);
});
child.on('error', (error) => {
reject(error);
child.kill();

View File

@ -9,6 +9,10 @@ import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless
export class ServerlessService implements ServerlessDriver {
constructor(@Inject(SERVERLESS_DRIVER) private driver: ServerlessDriver) {}
async delete(serverlessFunction: ServerlessFunctionEntity): Promise<void> {
return this.driver.delete(serverlessFunction);
}
async build(serverlessFunction: ServerlessFunctionEntity): Promise<void> {
return this.driver.build(serverlessFunction);
}