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:
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user