6658 workflows add a first twenty piece email sender (#6965)

This commit is contained in:
martmull
2024-09-12 11:00:25 +02:00
committed by GitHub
parent f8e5b333d9
commit 3190f4a87b
397 changed files with 1143 additions and 1037 deletions

View File

@ -0,0 +1,25 @@
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { readFileContent } from 'src/engine/core-modules/file-storage/utils/read-file-content';
import { SOURCE_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/source-file-name';
import { compileTypescript } from 'src/engine/core-modules/serverless/drivers/utils/compile-typescript';
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { getServerlessFolder } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils';
export class BaseServerlessDriver {
async getCompiledCode(
serverlessFunction: ServerlessFunctionEntity,
fileStorageService: FileStorageService,
) {
const folderPath = getServerlessFolder({
serverlessFunction,
version: 'draft',
});
const fileStream = await fileStorageService.read({
folderPath,
filename: SOURCE_FILE_NAME,
});
const typescriptCode = await readFileContent(fileStream);
return compileTypescript(typescriptCode);
}
}

View File

@ -0,0 +1 @@
export const BUILD_FILE_NAME = 'build.js';

View File

@ -0,0 +1 @@
export const COMMON_LAYER_NAME = 'common-layer';

View File

@ -0,0 +1,4 @@
import { join } from 'path';
import { tmpdir } from 'os';
export const SERVERLESS_TMPDIR_FOLDER = join(tmpdir(), 'serverless-tmpdir');

View File

@ -0,0 +1 @@
export const SOURCE_FILE_NAME = 'source.ts';

View File

@ -0,0 +1,29 @@
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { ServerlessFunctionExecutionStatus } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-execution-result.dto';
export type ServerlessExecuteError = {
errorType: string;
errorMessage: string;
stackTrace: string;
};
export type ServerlessExecuteResult = {
data: object | null;
duration: number;
status: ServerlessFunctionExecutionStatus;
error?: ServerlessExecuteError;
};
export interface ServerlessDriver {
delete(serverlessFunction: ServerlessFunctionEntity): Promise<void>;
build(
serverlessFunction: ServerlessFunctionEntity,
version: string,
): Promise<void>;
publish(serverlessFunction: ServerlessFunctionEntity): Promise<string>;
execute(
serverlessFunction: ServerlessFunctionEntity,
payload: object,
version: string,
): Promise<ServerlessExecuteResult>;
}

View File

@ -0,0 +1,301 @@
import * as fs from 'fs/promises';
import { join } from 'path';
import {
CreateFunctionCommand,
DeleteFunctionCommand,
GetFunctionCommand,
InvokeCommand,
InvokeCommandInput,
Lambda,
LambdaClientConfig,
PublishLayerVersionCommand,
PublishLayerVersionCommandInput,
PublishVersionCommand,
PublishVersionCommandInput,
ResourceNotFoundException,
UpdateFunctionCodeCommand,
waitUntilFunctionUpdatedV2,
ListLayerVersionsCommandInput,
ListLayerVersionsCommand,
} 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,
ServerlessExecuteResult,
} from 'src/engine/core-modules/serverless/drivers/interfaces/serverless-driver.interface';
import {
ServerlessFunctionEntity,
ServerlessFunctionRuntime,
} from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import {
LambdaBuildDirectoryManager,
NODE_LAYER_SUBFOLDER,
} from 'src/engine/core-modules/serverless/drivers/utils/lambda-build-directory-manager';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { BaseServerlessDriver } from 'src/engine/core-modules/serverless/drivers/base-serverless.driver';
import { createZipFile } from 'src/engine/core-modules/serverless/drivers/utils/create-zip-file';
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 { isDefined } from 'src/utils/is-defined';
import { COMMON_LAYER_NAME } from 'src/engine/core-modules/serverless/drivers/constants/common-layer-name';
import { copyAndBuildDependencies } from 'src/engine/core-modules/serverless/drivers/utils/copy-and-build-dependencies';
export interface LambdaDriverOptions extends LambdaClientConfig {
fileStorageService: FileStorageService;
region: string;
role: string;
}
export class LambdaDriver
extends BaseServerlessDriver
implements ServerlessDriver
{
private readonly lambdaClient: Lambda;
private readonly lambdaRole: string;
private readonly fileStorageService: FileStorageService;
constructor(options: LambdaDriverOptions) {
super();
const { region, role, ...lambdaOptions } = options;
this.lambdaClient = new Lambda({ ...lambdaOptions, region });
this.lambdaRole = role;
this.fileStorageService = options.fileStorageService;
}
private async waitFunctionUpdates(
serverlessFunctionId: string,
maxWaitTime: number,
) {
const waitParams = { FunctionName: serverlessFunctionId };
await waitUntilFunctionUpdatedV2(
{ client: this.lambdaClient, maxWaitTime },
waitParams,
);
}
private async createLayerIfNotExists(version: number): Promise<string> {
const listLayerParams: ListLayerVersionsCommandInput = {
LayerName: COMMON_LAYER_NAME,
MaxItems: 1,
};
const listLayerCommand = new ListLayerVersionsCommand(listLayerParams);
const listLayerResult = await this.lambdaClient.send(listLayerCommand);
if (
isDefined(listLayerResult.LayerVersions) &&
listLayerResult.LayerVersions?.[0].Description === `${version}` &&
isDefined(listLayerResult.LayerVersions[0].LayerVersionArn)
) {
return listLayerResult.LayerVersions[0].LayerVersionArn;
}
const lambdaBuildDirectoryManager = new LambdaBuildDirectoryManager();
const { sourceTemporaryDir, lambdaZipPath } =
await lambdaBuildDirectoryManager.init();
const nodeDependenciesFolder = join(
sourceTemporaryDir,
NODE_LAYER_SUBFOLDER,
);
await copyAndBuildDependencies(nodeDependenciesFolder);
await createZipFile(sourceTemporaryDir, lambdaZipPath);
const params: PublishLayerVersionCommandInput = {
LayerName: COMMON_LAYER_NAME,
Content: {
ZipFile: await fs.readFile(lambdaZipPath),
},
CompatibleRuntimes: [ServerlessFunctionRuntime.NODE18],
Description: `${version}`,
};
const command = new PublishLayerVersionCommand(params);
const result = await this.lambdaClient.send(command);
await lambdaBuildDirectoryManager.clean();
if (!isDefined(result.LayerVersionArn)) {
throw new Error('new layer version arn si undefined');
}
return result.LayerVersionArn;
}
private async checkFunctionExists(functionName: string): Promise<boolean> {
try {
const getFunctionCommand = new GetFunctionCommand({
FunctionName: functionName,
});
await this.lambdaClient.send(getFunctionCommand);
return true;
} catch (error) {
if (error instanceof ResourceNotFoundException) {
return false;
}
throw error;
}
}
async delete(serverlessFunction: ServerlessFunctionEntity) {
const functionExists = await this.checkFunctionExists(
serverlessFunction.id,
);
if (functionExists) {
const deleteFunctionCommand = new DeleteFunctionCommand({
FunctionName: serverlessFunction.id,
});
await this.lambdaClient.send(deleteFunctionCommand);
}
}
async build(serverlessFunction: ServerlessFunctionEntity) {
const javascriptCode = await this.getCompiledCode(
serverlessFunction,
this.fileStorageService,
);
const lambdaBuildDirectoryManager = new LambdaBuildDirectoryManager();
const {
sourceTemporaryDir,
lambdaZipPath,
javascriptFilePath,
lambdaHandler,
} = await lambdaBuildDirectoryManager.init();
await fs.writeFile(javascriptFilePath, javascriptCode);
await createZipFile(sourceTemporaryDir, lambdaZipPath);
const functionExists = await this.checkFunctionExists(
serverlessFunction.id,
);
if (!functionExists) {
const layerArn = await this.createLayerIfNotExists(
serverlessFunction.layerVersion,
);
const params: CreateFunctionCommandInput = {
Code: {
ZipFile: await fs.readFile(lambdaZipPath),
},
FunctionName: serverlessFunction.id,
Handler: lambdaHandler,
Layers: [layerArn],
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.readFile(lambdaZipPath),
FunctionName: serverlessFunction.id,
};
const command = new UpdateFunctionCodeCommand(params);
await this.lambdaClient.send(command);
}
await this.waitFunctionUpdates(serverlessFunction.id, 10);
await lambdaBuildDirectoryManager.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,
version: string,
): Promise<ServerlessExecuteResult> {
const computedVersion =
version === 'latest' ? functionToExecute.latestVersion : version;
const functionName =
computedVersion === 'draft'
? functionToExecute.id
: `${functionToExecute.id}:${computedVersion}`;
await this.waitFunctionUpdates(functionToExecute.id, 10);
const startTime = Date.now();
const params: InvokeCommandInput = {
FunctionName: functionName,
Payload: JSON.stringify(payload),
};
const command = new InvokeCommand(params);
try {
const result = await this.lambdaClient.send(command);
const parsedResult = result.Payload
? JSON.parse(result.Payload.transformToString())
: {};
const duration = Date.now() - startTime;
if (result.FunctionError) {
return {
data: null,
duration,
status: ServerlessFunctionExecutionStatus.ERROR,
error: parsedResult,
};
}
return {
data: parsedResult,
duration,
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;
}
}
}

View File

@ -0,0 +1,48 @@
{
"dependencies": {
"@types/bcrypt": "^5.0.2",
"@types/deep-equal": "^1.0.4",
"@types/lodash.camelcase": "^4.3.9",
"@types/lodash.compact": "^3.0.9",
"@types/lodash.debounce": "^4.0.9",
"@types/lodash.groupby": "^4.6.9",
"@types/lodash.identity": "^3.0.9",
"@types/lodash.isempty": "^4.4.9",
"@types/lodash.isequal": "^4.5.8",
"@types/lodash.isobject": "^3.0.9",
"@types/lodash.kebabcase": "^4.1.9",
"@types/lodash.mapvalues": "^4.6.9",
"@types/lodash.omit": "^4.5.9",
"@types/lodash.pickby": "^4.6.9",
"@types/lodash.snakecase": "^4.1.9",
"@types/lodash.upperfirst": "^4.3.9",
"@types/uuid": "^10.0.0",
"archiver": "^7.0.1",
"axios": "^1.7.5",
"bcrypt": "^5.1.1",
"body-parser": "^1.20.2",
"deep-equal": "^2.2.3",
"jsonwebtoken": "^9.0.2",
"lodash.camelcase": "^4.3.0",
"lodash.chunk": "^4.2.0",
"lodash.compact": "^3.0.1",
"lodash.debounce": "^4.0.8",
"lodash.groupby": "^4.6.0",
"lodash.identity": "^3.0.0",
"lodash.isempty": "^4.4.0",
"lodash.isequal": "^4.5.0",
"lodash.isobject": "^3.0.2",
"lodash.kebabcase": "^4.1.1",
"lodash.mapvalues": "^4.6.0",
"lodash.merge": "^4.6.2",
"lodash.omit": "^4.5.0",
"lodash.pick": "^4.4.0",
"lodash.pickby": "^4.6.0",
"lodash.snakecase": "^4.1.1",
"lodash.upperfirst": "^4.3.1",
"nodemailer": "^6.9.14",
"sharp": "^0.33.5",
"uuid": "^10.0.0",
"winston": "^3.14.2"
}
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,5 @@
enableInlineHunks: true
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.4.0.cjs

View File

@ -0,0 +1 @@
export const LAST_LAYER_VERSION = 1;

View File

@ -0,0 +1,221 @@
import { fork } from 'child_process';
import { promises as fs, existsSync } from 'fs';
import { join } from 'path';
import { v4 } from 'uuid';
import { FileStorageExceptionCode } from 'src/engine/core-modules/file-storage/interfaces/file-storage-exception';
import {
ServerlessDriver,
ServerlessExecuteError,
ServerlessExecuteResult,
} from 'src/engine/core-modules/serverless/drivers/interfaces/serverless-driver.interface';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { readFileContent } from 'src/engine/core-modules/file-storage/utils/read-file-content';
import { BaseServerlessDriver } from 'src/engine/core-modules/serverless/drivers/base-serverless.driver';
import { BUILD_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/build-file-name';
import { getServerlessFolder } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils';
import { ServerlessFunctionExecutionStatus } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-execution-result.dto';
import { 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 { COMMON_LAYER_NAME } from 'src/engine/core-modules/serverless/drivers/constants/common-layer-name';
import { copyAndBuildDependencies } from 'src/engine/core-modules/serverless/drivers/utils/copy-and-build-dependencies';
import { SERVERLESS_TMPDIR_FOLDER } from 'src/engine/core-modules/serverless/drivers/constants/serverless-tmpdir-folder';
export interface LocalDriverOptions {
fileStorageService: FileStorageService;
}
export class LocalDriver
extends BaseServerlessDriver
implements ServerlessDriver
{
private readonly fileStorageService: FileStorageService;
constructor(options: LocalDriverOptions) {
super();
this.fileStorageService = options.fileStorageService;
}
private getInMemoryLayerFolderPath = (version: number) => {
return join(SERVERLESS_TMPDIR_FOLDER, COMMON_LAYER_NAME, `${version}`);
};
private async createLayerIfNotExists(version: number) {
const inMemoryLastVersionLayerFolderPath =
this.getInMemoryLayerFolderPath(version);
if (existsSync(inMemoryLastVersionLayerFolderPath)) {
return;
}
await copyAndBuildDependencies(inMemoryLastVersionLayerFolderPath);
}
async delete() {}
async build(serverlessFunction: ServerlessFunctionEntity) {
await this.createLayerIfNotExists(serverlessFunction.layerVersion);
const javascriptCode = await this.getCompiledCode(
serverlessFunction,
this.fileStorageService,
);
const draftFolderPath = getServerlessFolder({
serverlessFunction,
version: 'draft',
});
await this.fileStorageService.write({
file: javascriptCode,
name: BUILD_FILE_NAME,
mimeType: undefined,
folder: draftFolderPath,
});
}
async publish(serverlessFunction: ServerlessFunctionEntity) {
await this.build(serverlessFunction);
return serverlessFunction.latestVersion
? `${parseInt(serverlessFunction.latestVersion, 10) + 1}`
: '1';
}
async execute(
serverlessFunction: ServerlessFunctionEntity,
payload: object,
version: string,
): Promise<ServerlessExecuteResult> {
await this.createLayerIfNotExists(serverlessFunction.layerVersion);
const startTime = Date.now();
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 tmpFolderPath = join(SERVERLESS_TMPDIR_FOLDER, v4());
const tmpFilePath = join(tmpFolderPath, 'index.js');
await fs.symlink(
this.getInMemoryLayerFolderPath(serverlessFunction.layerVersion),
tmpFolderPath,
'dir',
);
const modifiedContent = `
process.on('message', async (message) => {
const { event, context } = message;
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}
`;
await fs.writeFile(tmpFilePath, modifiedContent);
return await new Promise((resolve, reject) => {
const child = fork(tmpFilePath, { silent: true });
child.on('message', (message: object | ServerlessExecuteError) => {
const duration = Date.now() - startTime;
if ('errorType' in message) {
resolve({
data: null,
duration,
error: message,
status: ServerlessFunctionExecutionStatus.ERROR,
});
} else {
resolve({
data: message,
duration,
status: ServerlessFunctionExecutionStatus.SUCCESS,
});
}
child.kill();
fs.unlink(tmpFilePath).catch(console.error);
});
child.stderr?.on('data', (data) => {
const stackTrace = data
.toString()
.split('\n')
.filter((line: string) => line.trim() !== '');
const errorTrace = stackTrace.filter((line: string) =>
line.includes('Error: '),
)?.[0];
let errorType = 'Unknown';
let errorMessage = '';
if (errorTrace) {
errorType = errorTrace.split(':')[0];
errorMessage = errorTrace.split(': ')[1];
}
const duration = Date.now() - startTime;
resolve({
data: null,
duration,
status: ServerlessFunctionExecutionStatus.ERROR,
error: {
errorType,
errorMessage,
stackTrace: stackTrace,
},
});
child.kill();
fs.unlink(tmpFilePath).catch(console.error);
});
child.on('error', (error) => {
reject(error);
child.kill();
fs.unlink(tmpFilePath).catch(console.error);
});
child.on('exit', (code) => {
if (code && code !== 0) {
reject(new Error(`Child process exited with code ${code}`));
fs.unlink(tmpFilePath).catch(console.error);
}
});
child.send({ event: payload });
});
}
}

View File

@ -0,0 +1,19 @@
import ts from 'typescript';
export const compileTypescript = (typescriptCode: string): string => {
const options: ts.CompilerOptions = {
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES2017,
moduleResolution: ts.ModuleResolutionKind.Node10,
esModuleInterop: true,
resolveJsonModule: true,
allowSyntheticDefaultImports: true,
types: ['node'],
};
const result = ts.transpileModule(typescriptCode, {
compilerOptions: options,
});
return result.outputText;
};

View File

@ -0,0 +1,40 @@
import { statSync, promises as fs } from 'fs';
import { promisify } from 'util';
import { exec } from 'child_process';
import { join } from 'path';
import { getLayerDependenciesDirName } from 'src/engine/core-modules/serverless/drivers/utils/get-layer-dependencies-dir-name';
const execPromise = promisify(exec);
export const copyAndBuildDependencies = async (buildDirectory: string) => {
await fs.mkdir(buildDirectory, {
recursive: true,
});
await fs.cp(getLayerDependenciesDirName('latest'), buildDirectory, {
recursive: true,
});
await fs.cp(getLayerDependenciesDirName('engine'), buildDirectory, {
recursive: true,
});
try {
await execPromise('yarn', { cwd: buildDirectory });
} catch (error: any) {
throw new Error(error.stdout);
}
const objects = await fs.readdir(buildDirectory);
objects.forEach((object) => {
const fullPath = join(buildDirectory, object);
if (object === 'node_modules') return;
if (statSync(fullPath).isDirectory()) {
fs.rm(fullPath, { recursive: true, force: true });
} else {
fs.rm(fullPath);
}
});
};

View File

@ -0,0 +1,21 @@
import fs from 'fs';
import { pipeline } from 'stream/promises';
import archiver from 'archiver';
export const createZipFile = async (
sourceDir: string,
outPath: string,
): Promise<void> => {
const output = fs.createWriteStream(outPath);
const archive = archiver('zip', {
zlib: { level: 9 }, // Compression level
});
const p = pipeline(archive, output);
archive.directory(sourceDir, false);
archive.finalize();
return p;
};

View File

@ -0,0 +1,24 @@
import fs from 'fs/promises';
import { join } from 'path';
import { getLayerDependenciesDirName } from 'src/engine/core-modules/serverless/drivers/utils/get-layer-dependencies-dir-name';
export type LayerDependencies = {
packageJson: { dependencies: object };
yarnLock: string;
};
export const getLastLayerDependencies =
async (): Promise<LayerDependencies> => {
const lastVersionLayerDirName = getLayerDependenciesDirName('latest');
const packageJson = await fs.readFile(
join(lastVersionLayerDirName, 'package.json'),
'utf8',
);
const yarnLock = await fs.readFile(
join(lastVersionLayerDirName, 'yarn.lock'),
'utf8',
);
return { packageJson: JSON.parse(packageJson), yarnLock };
};

View File

@ -0,0 +1,12 @@
import path from 'path';
import { LAST_LAYER_VERSION } from 'src/engine/core-modules/serverless/drivers/layers/last-layer-version';
// Can only be used in src/engine/integrations/serverless/drivers/utils folder
export const getLayerDependenciesDirName = (
version: 'latest' | 'engine' | number,
): string => {
const formattedVersion = version === 'latest' ? LAST_LAYER_VERSION : version;
return path.resolve(__dirname, `../layers/${formattedVersion}`);
};

View File

@ -0,0 +1,43 @@
import { join } from 'path';
import * as fs from 'fs/promises';
import { v4 } from 'uuid';
import { SERVERLESS_TMPDIR_FOLDER } from 'src/engine/core-modules/serverless/drivers/constants/serverless-tmpdir-folder';
export const NODE_LAYER_SUBFOLDER = 'nodejs';
const TEMPORARY_LAMBDA_FOLDER = 'lambda-build';
const TEMPORARY_LAMBDA_SOURCE_FOLDER = 'src';
const LAMBDA_ZIP_FILE_NAME = 'lambda.zip';
const LAMBDA_ENTRY_FILE_NAME = 'index.js';
export class LambdaBuildDirectoryManager {
private temporaryDir = join(
SERVERLESS_TMPDIR_FOLDER,
`${TEMPORARY_LAMBDA_FOLDER}-${v4()}`,
);
private lambdaHandler = `${LAMBDA_ENTRY_FILE_NAME.split('.')[0]}.handler`;
async init() {
const sourceTemporaryDir = join(
this.temporaryDir,
TEMPORARY_LAMBDA_SOURCE_FOLDER,
);
const lambdaZipPath = join(this.temporaryDir, LAMBDA_ZIP_FILE_NAME);
const javascriptFilePath = join(sourceTemporaryDir, LAMBDA_ENTRY_FILE_NAME);
await fs.mkdir(sourceTemporaryDir, { recursive: true });
return {
sourceTemporaryDir,
lambdaZipPath,
javascriptFilePath,
lambdaHandler: this.lambdaHandler,
};
}
async clean() {
await fs.rm(this.temporaryDir, { recursive: true, force: true });
}
}

View File

@ -0,0 +1,56 @@
import { fromNodeProviderChain } from '@aws-sdk/credential-providers';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import {
ServerlessDriverType,
ServerlessModuleOptions,
} from 'src/engine/core-modules/serverless/serverless.interface';
export const serverlessModuleFactory = async (
environmentService: EnvironmentService,
fileStorageService: FileStorageService,
): Promise<ServerlessModuleOptions> => {
const driverType = environmentService.get('SERVERLESS_TYPE');
const options = { fileStorageService };
switch (driverType) {
case ServerlessDriverType.Local: {
return {
type: ServerlessDriverType.Local,
options,
};
}
case ServerlessDriverType.Lambda: {
const region = environmentService.get('SERVERLESS_LAMBDA_REGION');
const accessKeyId = environmentService.get(
'SERVERLESS_LAMBDA_ACCESS_KEY_ID',
);
const secretAccessKey = environmentService.get(
'SERVERLESS_LAMBDA_SECRET_ACCESS_KEY',
);
const role = environmentService.get('SERVERLESS_LAMBDA_ROLE');
return {
type: ServerlessDriverType.Lambda,
options: {
...options,
credentials: accessKeyId
? {
accessKeyId,
secretAccessKey,
}
: fromNodeProviderChain({
clientConfig: { region },
}),
region: region ?? '',
role: role ?? '',
},
};
}
default:
throw new Error(
`Invalid serverless driver type (${driverType}), check your .env file`,
);
}
};

View File

@ -0,0 +1 @@
export const SERVERLESS_DRIVER = Symbol('SERVERLESS_DRIVER');

View File

@ -0,0 +1,30 @@
import { FactoryProvider, ModuleMetadata } from '@nestjs/common';
import { LocalDriverOptions } from 'src/engine/core-modules/serverless/drivers/local.driver';
import { LambdaDriverOptions } from 'src/engine/core-modules/serverless/drivers/lambda.driver';
export enum ServerlessDriverType {
Lambda = 'lambda',
Local = 'local',
}
export interface LocalDriverFactoryOptions {
type: ServerlessDriverType.Local;
options: LocalDriverOptions;
}
export interface LambdaDriverFactoryOptions {
type: ServerlessDriverType.Lambda;
options: LambdaDriverOptions;
}
export type ServerlessModuleOptions =
| LocalDriverFactoryOptions
| LambdaDriverFactoryOptions;
export type ServerlessModuleAsyncOptions = {
useFactory: (
...args: any[]
) => ServerlessModuleOptions | Promise<ServerlessModuleOptions>;
} & Pick<ModuleMetadata, 'imports'> &
Pick<FactoryProvider, 'inject'>;

View File

@ -0,0 +1,34 @@
import { DynamicModule, Global } from '@nestjs/common';
import { LambdaDriver } from 'src/engine/core-modules/serverless/drivers/lambda.driver';
import { LocalDriver } from 'src/engine/core-modules/serverless/drivers/local.driver';
import { SERVERLESS_DRIVER } from 'src/engine/core-modules/serverless/serverless.constants';
import {
ServerlessDriverType,
ServerlessModuleAsyncOptions,
} from 'src/engine/core-modules/serverless/serverless.interface';
import { ServerlessService } from 'src/engine/core-modules/serverless/serverless.service';
@Global()
export class ServerlessModule {
static forRootAsync(options: ServerlessModuleAsyncOptions): DynamicModule {
const provider = {
provide: SERVERLESS_DRIVER,
useFactory: async (...args: any[]) => {
const config = await options.useFactory(...args);
return config?.type === ServerlessDriverType.Local
? new LocalDriver(config.options)
: new LambdaDriver(config.options);
},
inject: options.inject || [],
};
return {
module: ServerlessModule,
imports: options.imports || [],
providers: [ServerlessService, provider],
exports: [ServerlessService],
};
}
}

View File

@ -0,0 +1,37 @@
import { Inject, Injectable } from '@nestjs/common';
import {
ServerlessDriver,
ServerlessExecuteResult,
} from 'src/engine/core-modules/serverless/drivers/interfaces/serverless-driver.interface';
import { SERVERLESS_DRIVER } from 'src/engine/core-modules/serverless/serverless.constants';
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
@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 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,
version: string,
): Promise<ServerlessExecuteResult> {
return this.driver.execute(serverlessFunction, payload, version);
}
}

View File

@ -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 || '',
);
};