6654 serverless functions add a deploy button disable deploy when autosave (#6715)
- improvements on serverless function behavior (autosave performances, deploy on execution only) - add versioning to serverless functions - add a publish endpoint to create a new version of a serverless function - add deploy and reset to lastVersion button in the settings section: <img width="736" alt="image" src="https://github.com/user-attachments/assets/2001f8d2-07a4-4f79-84dd-ec74b6f301d3">
This commit is contained in:
@ -13,4 +13,8 @@ export interface StorageDriver {
|
||||
from: { folderPath: string; filename: string };
|
||||
to: { folderPath: string; filename: string };
|
||||
}): Promise<void>;
|
||||
copy(params: {
|
||||
from: { folderPath: string; filename?: string };
|
||||
to: { folderPath: string; filename?: string };
|
||||
}): Promise<void>;
|
||||
}
|
||||
|
||||
@ -70,6 +70,13 @@ export class LocalDriver implements StorageDriver {
|
||||
params.filename,
|
||||
);
|
||||
|
||||
if (!existsSync(filePath)) {
|
||||
throw new FileStorageException(
|
||||
'File not found',
|
||||
FileStorageExceptionCode.FILE_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
return createReadStream(filePath);
|
||||
} catch (error) {
|
||||
@ -115,4 +122,34 @@ export class LocalDriver implements StorageDriver {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async copy(params: {
|
||||
from: { folderPath: string; filename?: string };
|
||||
to: { folderPath: string; filename?: string };
|
||||
}): Promise<void> {
|
||||
const fromPath = join(
|
||||
`${this.options.storagePath}/`,
|
||||
params.from.folderPath,
|
||||
params.from.filename || '',
|
||||
);
|
||||
|
||||
const toPath = join(
|
||||
`${this.options.storagePath}/`,
|
||||
params.to.folderPath,
|
||||
params.to.filename || '',
|
||||
);
|
||||
|
||||
try {
|
||||
await fs.cp(fromPath, toPath, { recursive: true });
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
throw new FileStorageException(
|
||||
'File not found',
|
||||
FileStorageExceptionCode.FILE_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -187,6 +187,42 @@ export class S3Driver implements StorageDriver {
|
||||
}
|
||||
}
|
||||
|
||||
async copy(params: {
|
||||
from: { folderPath: string; filename?: string };
|
||||
to: { folderPath: string; filename?: string };
|
||||
}): Promise<void> {
|
||||
const fromKey = `${params.from.folderPath}/${params.from.filename || ''}`;
|
||||
const toKey = `${params.to.folderPath}/${params.to.filename || ''}`;
|
||||
|
||||
try {
|
||||
// Check if the source file exists
|
||||
await this.s3Client.send(
|
||||
new HeadObjectCommand({
|
||||
Bucket: this.bucketName,
|
||||
Key: fromKey,
|
||||
}),
|
||||
);
|
||||
|
||||
// Copy the object to the new location
|
||||
await this.s3Client.send(
|
||||
new CopyObjectCommand({
|
||||
CopySource: `${this.bucketName}/${fromKey}`,
|
||||
Bucket: this.bucketName,
|
||||
Key: toKey,
|
||||
}),
|
||||
);
|
||||
} catch (error) {
|
||||
if (error.name === 'NotFound') {
|
||||
throw new FileStorageException(
|
||||
'File not found',
|
||||
FileStorageExceptionCode.FILE_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
// For other errors, throw the original error
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async checkBucketExists(args: HeadBucketCommandInput) {
|
||||
try {
|
||||
await this.s3Client.headBucket(args);
|
||||
|
||||
@ -33,4 +33,11 @@ export class FileStorageService implements StorageDriver {
|
||||
}): Promise<void> {
|
||||
return this.driver.move(params);
|
||||
}
|
||||
|
||||
copy(params: {
|
||||
from: { folderPath: string; filename?: string };
|
||||
to: { folderPath: string; filename?: string };
|
||||
}): Promise<void> {
|
||||
return this.driver.copy(params);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,27 +1,19 @@
|
||||
import { join } from 'path';
|
||||
|
||||
import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface';
|
||||
|
||||
import { FileStorageService } from 'src/engine/integrations/file-storage/file-storage.service';
|
||||
import { readFileContent } from 'src/engine/integrations/file-storage/utils/read-file-content';
|
||||
import { SOURCE_FILE_NAME } from 'src/engine/integrations/serverless/drivers/constants/source-file-name';
|
||||
import { compileTypescript } from 'src/engine/integrations/serverless/drivers/utils/compile-typescript';
|
||||
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
||||
import { getServerlessFolder } from 'src/engine/integrations/serverless/utils/serverless-get-folder.utils';
|
||||
|
||||
export class BaseServerlessDriver {
|
||||
getFolderPath(serverlessFunction: ServerlessFunctionEntity) {
|
||||
return join(
|
||||
'workspace-' + serverlessFunction.workspaceId,
|
||||
FileFolder.ServerlessFunction,
|
||||
serverlessFunction.id,
|
||||
);
|
||||
}
|
||||
|
||||
async getCompiledCode(
|
||||
serverlessFunction: ServerlessFunctionEntity,
|
||||
fileStorageService: FileStorageService,
|
||||
) {
|
||||
const folderPath = this.getFolderPath(serverlessFunction);
|
||||
const folderPath = getServerlessFolder({
|
||||
serverlessFunction,
|
||||
version: 'draft',
|
||||
});
|
||||
const fileStream = await fileStorageService.read({
|
||||
folderPath,
|
||||
filename: SOURCE_FILE_NAME,
|
||||
|
||||
@ -16,9 +16,14 @@ export type ServerlessExecuteResult = {
|
||||
|
||||
export interface ServerlessDriver {
|
||||
delete(serverlessFunction: ServerlessFunctionEntity): Promise<void>;
|
||||
build(serverlessFunction: ServerlessFunctionEntity): Promise<void>;
|
||||
build(
|
||||
serverlessFunction: ServerlessFunctionEntity,
|
||||
version: string,
|
||||
): Promise<void>;
|
||||
publish(serverlessFunction: ServerlessFunctionEntity): Promise<string>;
|
||||
execute(
|
||||
serverlessFunction: ServerlessFunctionEntity,
|
||||
payload: object | undefined,
|
||||
version: string,
|
||||
): Promise<ServerlessExecuteResult>;
|
||||
}
|
||||
|
||||
@ -9,6 +9,9 @@ import {
|
||||
UpdateFunctionCodeCommand,
|
||||
DeleteFunctionCommand,
|
||||
ResourceNotFoundException,
|
||||
waitUntilFunctionUpdatedV2,
|
||||
PublishVersionCommandInput,
|
||||
PublishVersionCommand,
|
||||
} 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';
|
||||
@ -24,6 +27,10 @@ import { FileStorageService } from 'src/engine/integrations/file-storage/file-st
|
||||
import { BaseServerlessDriver } from 'src/engine/integrations/serverless/drivers/base-serverless.driver';
|
||||
import { BuildDirectoryManagerService } from 'src/engine/integrations/serverless/drivers/services/build-directory-manager.service';
|
||||
import { ServerlessFunctionExecutionStatus } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-execution-result.dto';
|
||||
import {
|
||||
ServerlessFunctionException,
|
||||
ServerlessFunctionExceptionCode,
|
||||
} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception';
|
||||
|
||||
export interface LambdaDriverOptions extends LambdaClientConfig {
|
||||
fileStorageService: FileStorageService;
|
||||
@ -51,12 +58,10 @@ export class LambdaDriver
|
||||
this.buildDirectoryManagerService = options.buildDirectoryManagerService;
|
||||
}
|
||||
|
||||
private async checkFunctionExists(
|
||||
serverlessFunctionId: string,
|
||||
): Promise<boolean> {
|
||||
private async checkFunctionExists(functionName: string): Promise<boolean> {
|
||||
try {
|
||||
const getFunctionCommand = new GetFunctionCommand({
|
||||
FunctionName: serverlessFunctionId,
|
||||
FunctionName: functionName,
|
||||
});
|
||||
|
||||
await this.lambdaClient.send(getFunctionCommand);
|
||||
@ -132,42 +137,85 @@ export class LambdaDriver
|
||||
await this.lambdaClient.send(command);
|
||||
}
|
||||
|
||||
const waitParams = { FunctionName: serverlessFunction.id };
|
||||
|
||||
await waitUntilFunctionUpdatedV2(
|
||||
{ client: this.lambdaClient, maxWaitTime: 5 },
|
||||
waitParams,
|
||||
);
|
||||
|
||||
await this.buildDirectoryManagerService.clean();
|
||||
}
|
||||
|
||||
async publish(serverlessFunction: ServerlessFunctionEntity) {
|
||||
await this.build(serverlessFunction);
|
||||
const params: PublishVersionCommandInput = {
|
||||
FunctionName: serverlessFunction.id,
|
||||
};
|
||||
|
||||
const command = new PublishVersionCommand(params);
|
||||
|
||||
const result = await this.lambdaClient.send(command);
|
||||
const newVersion = result.Version;
|
||||
|
||||
if (!newVersion) {
|
||||
throw new Error('New published version is undefined');
|
||||
}
|
||||
|
||||
return newVersion;
|
||||
}
|
||||
|
||||
async execute(
|
||||
functionToExecute: ServerlessFunctionEntity,
|
||||
payload: object | undefined = undefined,
|
||||
version: string,
|
||||
): Promise<ServerlessExecuteResult> {
|
||||
const computedVersion =
|
||||
version === 'latest' ? functionToExecute.latestVersion : version;
|
||||
|
||||
const functionName =
|
||||
computedVersion === 'draft'
|
||||
? functionToExecute.id
|
||||
: `${functionToExecute.id}:${computedVersion}`;
|
||||
const startTime = Date.now();
|
||||
const params = {
|
||||
FunctionName: functionToExecute.id,
|
||||
FunctionName: functionName,
|
||||
Payload: JSON.stringify(payload),
|
||||
};
|
||||
|
||||
const command = new InvokeCommand(params);
|
||||
|
||||
const result = await this.lambdaClient.send(command);
|
||||
try {
|
||||
const result = await this.lambdaClient.send(command);
|
||||
|
||||
const parsedResult = result.Payload
|
||||
? JSON.parse(result.Payload.transformToString())
|
||||
: {};
|
||||
const parsedResult = result.Payload
|
||||
? JSON.parse(result.Payload.transformToString())
|
||||
: {};
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
if (result.FunctionError) {
|
||||
return {
|
||||
data: null,
|
||||
duration,
|
||||
status: ServerlessFunctionExecutionStatus.ERROR,
|
||||
error: parsedResult,
|
||||
};
|
||||
}
|
||||
|
||||
if (result.FunctionError) {
|
||||
return {
|
||||
data: null,
|
||||
data: parsedResult,
|
||||
duration,
|
||||
status: ServerlessFunctionExecutionStatus.ERROR,
|
||||
error: parsedResult,
|
||||
status: ServerlessFunctionExecutionStatus.SUCCESS,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof ResourceNotFoundException) {
|
||||
throw new ServerlessFunctionException(
|
||||
`Function Version '${version}' does not exist`,
|
||||
ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
return {
|
||||
data: parsedResult,
|
||||
duration,
|
||||
status: ServerlessFunctionExecutionStatus.SUCCESS,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
ServerlessExecuteError,
|
||||
ServerlessExecuteResult,
|
||||
} from 'src/engine/integrations/serverless/drivers/interfaces/serverless-driver.interface';
|
||||
import { FileStorageExceptionCode } from 'src/engine/integrations/file-storage/interfaces/file-storage-exception';
|
||||
|
||||
import { FileStorageService } from 'src/engine/integrations/file-storage/file-storage.service';
|
||||
import { readFileContent } from 'src/engine/integrations/file-storage/utils/read-file-content';
|
||||
@ -17,6 +18,11 @@ import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless
|
||||
import { BUILD_FILE_NAME } from 'src/engine/integrations/serverless/drivers/constants/build-file-name';
|
||||
import { BaseServerlessDriver } from 'src/engine/integrations/serverless/drivers/base-serverless.driver';
|
||||
import { ServerlessFunctionExecutionStatus } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-execution-result.dto';
|
||||
import {
|
||||
ServerlessFunctionException,
|
||||
ServerlessFunctionExceptionCode,
|
||||
} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception';
|
||||
import { getServerlessFolder } from 'src/engine/integrations/serverless/utils/serverless-get-folder.utils';
|
||||
|
||||
export interface LocalDriverOptions {
|
||||
fileStorageService: FileStorageService;
|
||||
@ -33,11 +39,7 @@ export class LocalDriver
|
||||
this.fileStorageService = options.fileStorageService;
|
||||
}
|
||||
|
||||
async delete(serverlessFunction: ServerlessFunctionEntity) {
|
||||
await this.fileStorageService.delete({
|
||||
folderPath: this.getFolderPath(serverlessFunction),
|
||||
});
|
||||
}
|
||||
async delete() {}
|
||||
|
||||
async build(serverlessFunction: ServerlessFunctionEntity) {
|
||||
const javascriptCode = await this.getCompiledCode(
|
||||
@ -49,20 +51,48 @@ export class LocalDriver
|
||||
file: javascriptCode,
|
||||
name: BUILD_FILE_NAME,
|
||||
mimeType: undefined,
|
||||
folder: this.getFolderPath(serverlessFunction),
|
||||
folder: getServerlessFolder({
|
||||
serverlessFunction,
|
||||
version: 'draft',
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
async publish(serverlessFunction: ServerlessFunctionEntity) {
|
||||
await this.build(serverlessFunction);
|
||||
|
||||
return serverlessFunction.latestVersion
|
||||
? `${parseInt(serverlessFunction.latestVersion, 10) + 1}`
|
||||
: '1';
|
||||
}
|
||||
|
||||
async execute(
|
||||
serverlessFunction: ServerlessFunctionEntity,
|
||||
payload: object | undefined = undefined,
|
||||
version: string,
|
||||
): Promise<ServerlessExecuteResult> {
|
||||
const startTime = Date.now();
|
||||
const fileStream = await this.fileStorageService.read({
|
||||
folderPath: this.getFolderPath(serverlessFunction),
|
||||
filename: BUILD_FILE_NAME,
|
||||
});
|
||||
const fileContent = await readFileContent(fileStream);
|
||||
let fileContent = '';
|
||||
|
||||
try {
|
||||
const fileStream = await this.fileStorageService.read({
|
||||
folderPath: getServerlessFolder({
|
||||
serverlessFunction,
|
||||
version,
|
||||
}),
|
||||
filename: BUILD_FILE_NAME,
|
||||
});
|
||||
|
||||
fileContent = await readFileContent(fileStream);
|
||||
} catch (error) {
|
||||
if (error.code === FileStorageExceptionCode.FILE_NOT_FOUND) {
|
||||
throw new ServerlessFunctionException(
|
||||
`Function Version '${version}' does not exist`,
|
||||
ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
const tmpFilePath = join(tmpdir(), `${v4()}.js`);
|
||||
|
||||
|
||||
@ -16,14 +16,22 @@ export class ServerlessService implements ServerlessDriver {
|
||||
return this.driver.delete(serverlessFunction);
|
||||
}
|
||||
|
||||
async build(serverlessFunction: ServerlessFunctionEntity): Promise<void> {
|
||||
return this.driver.build(serverlessFunction);
|
||||
async build(
|
||||
serverlessFunction: ServerlessFunctionEntity,
|
||||
version: string,
|
||||
): Promise<void> {
|
||||
return this.driver.build(serverlessFunction, version);
|
||||
}
|
||||
|
||||
async publish(serverlessFunction: ServerlessFunctionEntity): Promise<string> {
|
||||
return this.driver.publish(serverlessFunction);
|
||||
}
|
||||
|
||||
async execute(
|
||||
serverlessFunction: ServerlessFunctionEntity,
|
||||
payload: object | undefined = undefined,
|
||||
version: string,
|
||||
): Promise<ServerlessExecuteResult> {
|
||||
return this.driver.execute(serverlessFunction, payload);
|
||||
return this.driver.execute(serverlessFunction, payload, version);
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,23 @@
|
||||
import { join } from 'path';
|
||||
|
||||
import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface';
|
||||
|
||||
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
||||
|
||||
export const getServerlessFolder = ({
|
||||
serverlessFunction,
|
||||
version,
|
||||
}: {
|
||||
serverlessFunction: ServerlessFunctionEntity;
|
||||
version?: string;
|
||||
}) => {
|
||||
const computedVersion =
|
||||
version === 'latest' ? serverlessFunction.latestVersion : version;
|
||||
|
||||
return join(
|
||||
'workspace-' + serverlessFunction.workspaceId,
|
||||
FileFolder.ServerlessFunction,
|
||||
serverlessFunction.id,
|
||||
computedVersion || '',
|
||||
);
|
||||
};
|
||||
@ -1,11 +1,11 @@
|
||||
import { ArgsType, Field } from '@nestjs/graphql';
|
||||
import { ArgsType, Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import { IsNotEmpty, IsObject, IsOptional, IsUUID } from 'class-validator';
|
||||
import graphqlTypeJson from 'graphql-type-json';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@ArgsType()
|
||||
@InputType()
|
||||
export class ExecuteServerlessFunctionInput {
|
||||
@Field(() => UUIDScalarType, {
|
||||
description: 'Id of the serverless function to execute',
|
||||
@ -21,4 +21,11 @@ export class ExecuteServerlessFunctionInput {
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
payload?: JSON;
|
||||
|
||||
@Field(() => String, {
|
||||
nullable: false,
|
||||
description: 'Version of the serverless function to execute',
|
||||
defaultValue: 'latest',
|
||||
})
|
||||
version: string;
|
||||
}
|
||||
|
||||
@ -0,0 +1,16 @@
|
||||
import { Field, ID, InputType } from '@nestjs/graphql';
|
||||
|
||||
import { IDField } from '@ptc-org/nestjs-query-graphql';
|
||||
|
||||
@InputType()
|
||||
export class GetServerlessFunctionSourceCodeInput {
|
||||
@IDField(() => ID, { description: 'The id of the function.' })
|
||||
id!: string;
|
||||
|
||||
@Field(() => String, {
|
||||
nullable: false,
|
||||
description: 'The version of the function',
|
||||
defaultValue: 'draft',
|
||||
})
|
||||
version: string;
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
import { ID, InputType } from '@nestjs/graphql';
|
||||
|
||||
import { IDField } from '@ptc-org/nestjs-query-graphql';
|
||||
|
||||
@InputType()
|
||||
export class PublishServerlessFunctionInput {
|
||||
@IDField(() => ID, { description: 'The id of the function.' })
|
||||
id!: string;
|
||||
}
|
||||
@ -14,6 +14,7 @@ import {
|
||||
IsDateString,
|
||||
IsEnum,
|
||||
IsNotEmpty,
|
||||
IsNumber,
|
||||
IsString,
|
||||
IsUUID,
|
||||
} from 'class-validator';
|
||||
@ -59,12 +60,11 @@ export class ServerlessFunctionDTO {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
sourceCodeFullPath: string;
|
||||
runtime: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
runtime: string;
|
||||
@Field({ nullable: true })
|
||||
latestVersion: string;
|
||||
|
||||
@IsEnum(ServerlessFunctionSyncStatus)
|
||||
@IsNotEmpty()
|
||||
|
||||
@ -28,11 +28,11 @@ export class ServerlessFunctionEntity {
|
||||
@Column({ nullable: true })
|
||||
description: string;
|
||||
|
||||
@Column({ nullable: false })
|
||||
sourceCodeHash: string;
|
||||
@Column({ nullable: true })
|
||||
latestVersion: string;
|
||||
|
||||
@Column({ nullable: false })
|
||||
sourceCodeFullPath: string;
|
||||
sourceCodeHash: string;
|
||||
|
||||
@Column({ nullable: false, default: ServerlessFunctionRuntime.NODE18 })
|
||||
runtime: ServerlessFunctionRuntime;
|
||||
|
||||
@ -9,6 +9,7 @@ export class ServerlessFunctionException extends CustomException {
|
||||
|
||||
export enum ServerlessFunctionExceptionCode {
|
||||
SERVERLESS_FUNCTION_NOT_FOUND = 'SERVERLESS_FUNCTION_NOT_FOUND',
|
||||
SERVERLESS_FUNCTION_VERSION_NOT_FOUND = 'SERVERLESS_FUNCTION_VERSION_NOT_FOUND',
|
||||
FEATURE_FLAG_INVALID = 'FEATURE_FLAG_INVALID',
|
||||
SERVERLESS_FUNCTION_ALREADY_EXIST = 'SERVERLESS_FUNCTION_ALREADY_EXIST',
|
||||
SERVERLESS_FUNCTION_NOT_READY = 'SERVERLESS_FUNCTION_NOT_READY',
|
||||
|
||||
@ -1,57 +0,0 @@
|
||||
import {
|
||||
CallHandler,
|
||||
ExecutionContext,
|
||||
Injectable,
|
||||
NestInterceptor,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { Observable, map } from 'rxjs';
|
||||
|
||||
import { FileService } from 'src/engine/core-modules/file/services/file.service';
|
||||
|
||||
@Injectable()
|
||||
export class ServerlessFunctionInterceptor implements NestInterceptor {
|
||||
constructor(private readonly fileService: FileService) {}
|
||||
|
||||
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
||||
return next.handle().pipe(
|
||||
map(async (data) => {
|
||||
if (data.edges && Array.isArray(data.edges)) {
|
||||
return {
|
||||
...data,
|
||||
edges: Promise.all(
|
||||
data.edges.map((item) => ({
|
||||
...item,
|
||||
node: this.processItem(item.node),
|
||||
})),
|
||||
),
|
||||
};
|
||||
} else {
|
||||
return this.processItem(data);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private async processItem(item: any): Promise<any> {
|
||||
if (item && item.sourceCodeFullPath) {
|
||||
const workspaceId = item.workspace?.id || item.workspaceId;
|
||||
|
||||
if (!workspaceId) {
|
||||
return item;
|
||||
}
|
||||
|
||||
const signedPayload = await this.fileService.encodeFileToken({
|
||||
serverlessFunctionId: item.id,
|
||||
workspace_id: workspaceId,
|
||||
});
|
||||
|
||||
return {
|
||||
...item,
|
||||
sourceCodeFullPath: `${item.sourceCodeFullPath}?token=${signedPayload}`,
|
||||
};
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
}
|
||||
@ -15,7 +15,6 @@ import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard';
|
||||
import { ServerlessModule } from 'src/engine/integrations/serverless/serverless.module';
|
||||
import { ServerlessFunctionDTO } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto';
|
||||
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
||||
import { ServerlessFunctionInterceptor } from 'src/engine/metadata-modules/serverless-function/serverless-function.interceptor';
|
||||
import { ServerlessFunctionResolver } from 'src/engine/metadata-modules/serverless-function/serverless-function.resolver';
|
||||
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
|
||||
|
||||
@ -45,7 +44,6 @@ import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverles
|
||||
update: { disabled: true },
|
||||
delete: { disabled: true },
|
||||
guards: [JwtAuthGuard],
|
||||
interceptors: [ServerlessFunctionInterceptor],
|
||||
},
|
||||
],
|
||||
}),
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { UseGuards, UseInterceptors } from '@nestjs/common';
|
||||
import { Args, Mutation, Resolver } from '@nestjs/graphql';
|
||||
import { UseGuards } from '@nestjs/common';
|
||||
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { FileUpload, GraphQLUpload } from 'graphql-upload';
|
||||
@ -21,9 +21,10 @@ import {
|
||||
ServerlessFunctionException,
|
||||
ServerlessFunctionExceptionCode,
|
||||
} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception';
|
||||
import { ServerlessFunctionInterceptor } from 'src/engine/metadata-modules/serverless-function/serverless-function.interceptor';
|
||||
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 { 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';
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Resolver()
|
||||
@ -50,6 +51,24 @@ export class ServerlessFunctionResolver {
|
||||
}
|
||||
}
|
||||
|
||||
@Query(() => String)
|
||||
async getServerlessFunctionSourceCode(
|
||||
@Args('input') input: GetServerlessFunctionSourceCodeInput,
|
||||
@AuthWorkspace() { id: workspaceId }: Workspace,
|
||||
) {
|
||||
try {
|
||||
await this.checkFeatureFlag(workspaceId);
|
||||
|
||||
return await this.serverlessFunctionService.getServerlessFunctionSourceCode(
|
||||
workspaceId,
|
||||
input.id,
|
||||
input.version,
|
||||
);
|
||||
} catch (error) {
|
||||
serverlessFunctionGraphQLApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
@Mutation(() => ServerlessFunctionDTO)
|
||||
async deleteOneServerlessFunction(
|
||||
@Args('input') input: DeleteServerlessFunctionInput,
|
||||
@ -67,7 +86,6 @@ export class ServerlessFunctionResolver {
|
||||
}
|
||||
}
|
||||
|
||||
@UseInterceptors(ServerlessFunctionInterceptor)
|
||||
@Mutation(() => ServerlessFunctionDTO)
|
||||
async updateOneServerlessFunction(
|
||||
@Args('input')
|
||||
@ -86,7 +104,6 @@ export class ServerlessFunctionResolver {
|
||||
}
|
||||
}
|
||||
|
||||
@UseInterceptors(ServerlessFunctionInterceptor)
|
||||
@Mutation(() => ServerlessFunctionDTO)
|
||||
async createOneServerlessFunction(
|
||||
@Args('input')
|
||||
@ -109,7 +126,6 @@ export class ServerlessFunctionResolver {
|
||||
}
|
||||
}
|
||||
|
||||
@UseInterceptors(ServerlessFunctionInterceptor)
|
||||
@Mutation(() => ServerlessFunctionDTO)
|
||||
async createOneServerlessFunctionFromFile(
|
||||
@Args({ name: 'file', type: () => GraphQLUpload })
|
||||
@ -133,17 +149,36 @@ export class ServerlessFunctionResolver {
|
||||
|
||||
@Mutation(() => ServerlessFunctionExecutionResultDTO)
|
||||
async executeOneServerlessFunction(
|
||||
@Args() executeServerlessFunctionInput: ExecuteServerlessFunctionInput,
|
||||
@Args('input') input: ExecuteServerlessFunctionInput,
|
||||
@AuthWorkspace() { id: workspaceId }: Workspace,
|
||||
) {
|
||||
try {
|
||||
await this.checkFeatureFlag(workspaceId);
|
||||
const { id, payload } = executeServerlessFunctionInput;
|
||||
const { id, payload, version } = input;
|
||||
|
||||
return await this.serverlessFunctionService.executeOne(
|
||||
return await this.serverlessFunctionService.executeOneServerlessFunction(
|
||||
id,
|
||||
workspaceId,
|
||||
payload,
|
||||
version,
|
||||
);
|
||||
} catch (error) {
|
||||
serverlessFunctionGraphQLApiExceptionHandler(error);
|
||||
}
|
||||
}
|
||||
|
||||
@Mutation(() => ServerlessFunctionDTO)
|
||||
async publishServerlessFunction(
|
||||
@Args('input') input: PublishServerlessFunctionInput,
|
||||
@AuthWorkspace() { id: workspaceId }: Workspace,
|
||||
) {
|
||||
try {
|
||||
await this.checkFeatureFlag(workspaceId);
|
||||
const { id } = input;
|
||||
|
||||
return await this.serverlessFunctionService.publishOneServerlessFunction(
|
||||
id,
|
||||
workspaceId,
|
||||
);
|
||||
} catch (error) {
|
||||
serverlessFunctionGraphQLApiExceptionHandler(error);
|
||||
|
||||
@ -1,15 +1,12 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { join } from 'path';
|
||||
|
||||
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
|
||||
import { FileUpload } from 'graphql-upload';
|
||||
import { Repository } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface';
|
||||
import { ServerlessExecuteResult } from 'src/engine/integrations/serverless/drivers/interfaces/serverless-driver.interface';
|
||||
import { FileStorageExceptionCode } from 'src/engine/integrations/file-storage/interfaces/file-storage-exception';
|
||||
|
||||
import { FileStorageService } from 'src/engine/integrations/file-storage/file-storage.service';
|
||||
import { readFileContent } from 'src/engine/integrations/file-storage/utils/read-file-content';
|
||||
@ -26,6 +23,8 @@ import {
|
||||
ServerlessFunctionExceptionCode,
|
||||
} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception';
|
||||
import { serverlessFunctionCreateHash } from 'src/engine/metadata-modules/serverless-function/utils/serverless-function-create-hash.utils';
|
||||
import { isDefined } from 'src/utils/is-defined';
|
||||
import { getServerlessFolder } from 'src/engine/integrations/serverless/utils/serverless-get-folder.utils';
|
||||
|
||||
@Injectable()
|
||||
export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFunctionEntity> {
|
||||
@ -38,10 +37,54 @@ export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFun
|
||||
super(serverlessFunctionRepository);
|
||||
}
|
||||
|
||||
async executeOne(
|
||||
async getServerlessFunctionSourceCode(
|
||||
workspaceId: string,
|
||||
id: string,
|
||||
version: string,
|
||||
) {
|
||||
try {
|
||||
const serverlessFunction =
|
||||
await this.serverlessFunctionRepository.findOne({
|
||||
where: {
|
||||
id,
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!serverlessFunction) {
|
||||
throw new ServerlessFunctionException(
|
||||
`Function does not exist`,
|
||||
ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const folderPath = getServerlessFolder({
|
||||
serverlessFunction,
|
||||
version,
|
||||
});
|
||||
|
||||
const fileStream = await this.fileStorageService.read({
|
||||
folderPath,
|
||||
filename: SOURCE_FILE_NAME,
|
||||
});
|
||||
|
||||
return await readFileContent(fileStream);
|
||||
} catch (error) {
|
||||
if (error.code === FileStorageExceptionCode.FILE_NOT_FOUND) {
|
||||
throw new ServerlessFunctionException(
|
||||
`Function Version '${version}' does not exist`,
|
||||
ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async executeOneServerlessFunction(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
payload: object | undefined = undefined,
|
||||
version = 'latest',
|
||||
): Promise<ServerlessExecuteResult> {
|
||||
const functionToExecute = await this.serverlessFunctionRepository.findOne({
|
||||
where: {
|
||||
@ -60,13 +103,73 @@ export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFun
|
||||
if (
|
||||
functionToExecute.syncStatus === ServerlessFunctionSyncStatus.NOT_READY
|
||||
) {
|
||||
await this.serverlessService.build(functionToExecute, version);
|
||||
await super.updateOne(functionToExecute.id, {
|
||||
syncStatus: ServerlessFunctionSyncStatus.READY,
|
||||
});
|
||||
}
|
||||
|
||||
return this.serverlessService.execute(functionToExecute, payload, version);
|
||||
}
|
||||
|
||||
async publishOneServerlessFunction(id: string, workspaceId: string) {
|
||||
const existingServerlessFunction =
|
||||
await this.serverlessFunctionRepository.findOne({
|
||||
where: { id, workspaceId },
|
||||
});
|
||||
|
||||
if (!existingServerlessFunction) {
|
||||
throw new ServerlessFunctionException(
|
||||
`Function is not ready to be executed`,
|
||||
`Function does not exist`,
|
||||
ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
return this.serverlessService.execute(functionToExecute, payload);
|
||||
if (isDefined(existingServerlessFunction.latestVersion)) {
|
||||
const latestCode = await this.getServerlessFunctionSourceCode(
|
||||
workspaceId,
|
||||
id,
|
||||
'latest',
|
||||
);
|
||||
const draftCode = await this.getServerlessFunctionSourceCode(
|
||||
workspaceId,
|
||||
id,
|
||||
'draft',
|
||||
);
|
||||
|
||||
if (
|
||||
serverlessFunctionCreateHash(latestCode) ===
|
||||
serverlessFunctionCreateHash(draftCode)
|
||||
) {
|
||||
throw new Error(
|
||||
'Cannot publish a new version when code has not changed',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const newVersion = await this.serverlessService.publish(
|
||||
existingServerlessFunction,
|
||||
);
|
||||
|
||||
const draftFolderPath = getServerlessFolder({
|
||||
serverlessFunction: existingServerlessFunction,
|
||||
version: 'draft',
|
||||
});
|
||||
const newFolderPath = getServerlessFolder({
|
||||
serverlessFunction: existingServerlessFunction,
|
||||
version: newVersion,
|
||||
});
|
||||
|
||||
await this.fileStorageService.copy({
|
||||
from: { folderPath: draftFolderPath },
|
||||
to: { folderPath: newFolderPath },
|
||||
});
|
||||
|
||||
await super.updateOne(existingServerlessFunction.id, {
|
||||
latestVersion: newVersion,
|
||||
});
|
||||
|
||||
return await this.findById(existingServerlessFunction.id);
|
||||
}
|
||||
|
||||
async deleteOneServerlessFunction(id: string, workspaceId: string) {
|
||||
@ -86,6 +189,12 @@ export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFun
|
||||
|
||||
await this.serverlessService.delete(existingServerlessFunction);
|
||||
|
||||
await this.fileStorageService.delete({
|
||||
folderPath: getServerlessFolder({
|
||||
serverlessFunction: existingServerlessFunction,
|
||||
}),
|
||||
});
|
||||
|
||||
return existingServerlessFunction;
|
||||
}
|
||||
|
||||
@ -105,34 +214,23 @@ export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFun
|
||||
);
|
||||
}
|
||||
|
||||
const codeHasChanged =
|
||||
serverlessFunctionCreateHash(serverlessFunctionInput.code) !==
|
||||
existingServerlessFunction.sourceCodeHash;
|
||||
|
||||
await super.updateOne(existingServerlessFunction.id, {
|
||||
name: serverlessFunctionInput.name,
|
||||
description: serverlessFunctionInput.description,
|
||||
sourceCodeHash: serverlessFunctionCreateHash(
|
||||
serverlessFunctionInput.code,
|
||||
),
|
||||
syncStatus: ServerlessFunctionSyncStatus.NOT_READY,
|
||||
});
|
||||
|
||||
if (codeHasChanged) {
|
||||
const fileFolder = join(
|
||||
'workspace-' + workspaceId,
|
||||
FileFolder.ServerlessFunction,
|
||||
existingServerlessFunction.id,
|
||||
);
|
||||
const fileFolder = getServerlessFolder({
|
||||
serverlessFunction: existingServerlessFunction,
|
||||
version: 'draft',
|
||||
});
|
||||
|
||||
await this.fileStorageService.write({
|
||||
file: serverlessFunctionInput.code,
|
||||
name: SOURCE_FILE_NAME,
|
||||
mimeType: undefined,
|
||||
folder: fileFolder,
|
||||
});
|
||||
|
||||
await this.serverlessService.build(existingServerlessFunction);
|
||||
}
|
||||
await this.fileStorageService.write({
|
||||
file: serverlessFunctionInput.code,
|
||||
name: SOURCE_FILE_NAME,
|
||||
mimeType: undefined,
|
||||
folder: fileFolder,
|
||||
});
|
||||
|
||||
return await this.findById(existingServerlessFunction.id);
|
||||
}
|
||||
@ -162,41 +260,24 @@ export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFun
|
||||
typescriptCode = await readFileContent(code.createReadStream());
|
||||
}
|
||||
|
||||
const serverlessFunctionId = v4();
|
||||
|
||||
const fileFolderWithoutWorkspace = join(
|
||||
FileFolder.ServerlessFunction,
|
||||
serverlessFunctionId,
|
||||
);
|
||||
|
||||
const fileFolder = join(
|
||||
'workspace-' + workspaceId,
|
||||
fileFolderWithoutWorkspace,
|
||||
);
|
||||
|
||||
const sourceCodeFullPath =
|
||||
fileFolderWithoutWorkspace + '/' + SOURCE_FILE_NAME;
|
||||
|
||||
const serverlessFunction = await super.createOne({
|
||||
const createdServerlessFunction = await super.createOne({
|
||||
...serverlessFunctionInput,
|
||||
id: serverlessFunctionId,
|
||||
workspaceId,
|
||||
sourceCodeHash: serverlessFunctionCreateHash(typescriptCode),
|
||||
sourceCodeFullPath,
|
||||
});
|
||||
|
||||
const draftFileFolder = getServerlessFolder({
|
||||
serverlessFunction: createdServerlessFunction,
|
||||
version: 'draft',
|
||||
});
|
||||
|
||||
await this.fileStorageService.write({
|
||||
file: typescriptCode,
|
||||
name: SOURCE_FILE_NAME,
|
||||
mimeType: undefined,
|
||||
folder: fileFolder,
|
||||
folder: draftFileFolder,
|
||||
});
|
||||
|
||||
await this.serverlessService.build(serverlessFunction);
|
||||
await super.updateOne(serverlessFunctionId, {
|
||||
syncStatus: ServerlessFunctionSyncStatus.READY,
|
||||
});
|
||||
|
||||
return await this.findById(serverlessFunctionId);
|
||||
return await this.findById(createdServerlessFunction.id);
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ export const serverlessFunctionGraphQLApiExceptionHandler = (error: any) => {
|
||||
if (error instanceof ServerlessFunctionException) {
|
||||
switch (error.code) {
|
||||
case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND:
|
||||
case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_VERSION_NOT_FOUND:
|
||||
throw new NotFoundError(error.message);
|
||||
case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_ALREADY_EXIST:
|
||||
throw new ConflictError(error.message);
|
||||
|
||||
Reference in New Issue
Block a user