1573 extensibility twenty cli handle custom layers for serverless functions of applications (#14779)

- allow specific layers for serverless functions
- add a serverlessFunctionLayer table
- sync application layer
This commit is contained in:
martmull
2025-09-30 16:47:49 +02:00
committed by GitHub
parent 5560d823c3
commit 1938202780
42 changed files with 748 additions and 108 deletions
@@ -1,4 +1,4 @@
import { Field, InputType } from '@nestjs/graphql';
import { Field, HideField, InputType } from '@nestjs/graphql';
import {
IsNotEmpty,
@@ -27,4 +27,10 @@ export class CreateServerlessFunctionInput {
@Max(900)
@IsOptional()
timeoutSeconds?: number;
@HideField()
applicationId?: string;
@HideField()
serverlessFunctionLayerId?: string;
}
@@ -5,8 +5,11 @@ import {
DeleteDateColumn,
Entity,
Index,
JoinColumn,
ManyToOne,
OneToMany,
PrimaryGeneratedColumn,
Relation,
UpdateDateColumn,
} from 'typeorm';
@@ -17,6 +20,8 @@ import { DatabaseEventTrigger } from 'src/engine/metadata-modules/database-event
import { Route } from 'src/engine/metadata-modules/route/route.entity';
import { ServerlessFunctionEntityRelationProperties } from 'src/engine/metadata-modules/serverless-function/types/flat-serverless-function.type';
import { InputSchema } from 'src/modules/workflow/workflow-builder/workflow-schema/types/input-schema.type';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { ServerlessFunctionLayerEntity } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.entity';
const DEFAULT_SERVERLESS_TIMEOUT_SECONDS = 300; // 5 minutes
@@ -74,6 +79,28 @@ export class ServerlessFunctionEntity
@Column({ nullable: true, type: 'text' })
checksum: string | null;
@Column({ nullable: true, type: 'uuid' })
serverlessFunctionLayerId: string | null;
@ManyToOne(
() => ServerlessFunctionLayerEntity,
(serverlessFunctionLayer) => serverlessFunctionLayer.serverlessFunctions,
{ nullable: true },
)
@JoinColumn({ name: 'serverlessFunctionLayerId' })
serverlessFunctionLayer: Relation<ServerlessFunctionLayerEntity> | null;
@ManyToOne(
() => ApplicationEntity,
(application) => application.serverlessFunctions,
{
onDelete: 'CASCADE',
nullable: true,
},
)
@JoinColumn({ name: 'applicationId' })
application: Relation<ApplicationEntity> | null;
@OneToMany(
() => CronTrigger,
(cronTrigger) => cronTrigger.serverlessFunction,
@@ -17,6 +17,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 { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspace-migration-v2/workspace-migration-v2.module';
import { ServerlessFunctionLayerModule } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.module';
@Module({
imports: [
@@ -29,6 +30,7 @@ import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspa
FeatureFlagModule,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
WorkspaceMigrationV2Module,
ServerlessFunctionLayerModule,
],
providers: [
ServerlessFunctionService,
@@ -16,7 +16,6 @@ import { FileStorageService } from 'src/engine/core-modules/file-storage/file-st
import { readFileContent } from 'src/engine/core-modules/file-storage/utils/read-file-content';
import { ENV_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/env-file-name';
import { INDEX_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/index-file-name';
import { LAST_LAYER_VERSION } from 'src/engine/core-modules/serverless/drivers/layers/last-layer-version';
import { getBaseTypescriptProjectFiles } from 'src/engine/core-modules/serverless/drivers/utils/get-base-typescript-project-files';
import { getLayerDependencies } from 'src/engine/core-modules/serverless/drivers/utils/get-last-layer-dependencies';
import { ServerlessService } from 'src/engine/core-modules/serverless/serverless.service';
@@ -34,12 +33,14 @@ import {
WorkflowVersionStepException,
WorkflowVersionStepExceptionCode,
} from 'src/modules/workflow/common/exceptions/workflow-version-step.exception';
import { ServerlessFunctionLayerService } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.service';
@Injectable()
export class ServerlessFunctionService {
constructor(
private readonly fileStorageService: FileStorageService,
private readonly serverlessService: ServerlessService,
private readonly serverlessFunctionLayerService: ServerlessFunctionLayerService,
@InjectRepository(ServerlessFunctionEntity)
private readonly serverlessFunctionRepository: Repository<ServerlessFunctionEntity>,
private readonly throttlerService: ThrottlerService,
@@ -116,6 +117,7 @@ export class ServerlessFunctionService {
id,
workspaceId,
},
relations: ['serverlessFunctionLayer'],
});
const resultServerlessFunction = await this.serverlessService.execute(
@@ -304,14 +306,16 @@ export class ServerlessFunctionService {
async getAvailablePackages(serverlessFunctionId: string) {
const serverlessFunction =
await this.serverlessFunctionRepository.findOneBy({
id: serverlessFunctionId,
await this.serverlessFunctionRepository.findOneOrFail({
where: { id: serverlessFunctionId },
relations: ['serverlessFunctionLayer'],
});
const { packageJson, yarnLock } = await getLayerDependencies(
serverlessFunction?.layerVersion || 'latest',
);
const { packageJson, yarnLock } =
await getLayerDependencies(serverlessFunction);
const packageVersionRegex = /^"([^@]+)@.*?":\n\s+version: (.+)$/gm;
const versions: Record<string, string> = {};
let match: RegExpExecArray | null;
@@ -321,7 +325,7 @@ export class ServerlessFunctionService {
const version = match[2];
// @ts-expect-error legacy noImplicitAny
if (packageJson.dependencies[packageName]) {
if (packageJson.dependencies?.[packageName]) {
versions[packageName] = version;
}
}
@@ -333,11 +337,16 @@ export class ServerlessFunctionService {
serverlessFunctionInput: CreateServerlessFunctionInput,
workspaceId: string,
) {
const commonServerlessFunctionLayer =
await this.serverlessFunctionLayerService.createCommonLayerIfNotExist(
workspaceId,
);
const serverlessFunctionToCreate = this.serverlessFunctionRepository.create(
{
...serverlessFunctionInput,
workspaceId,
layerVersion: LAST_LAYER_VERSION,
serverlessFunctionLayerId: commonServerlessFunctionLayer.id,
},
);
@@ -422,6 +431,9 @@ export class ServerlessFunctionService {
name: serverlessFunctionToDuplicate.name,
description: serverlessFunctionToDuplicate.description ?? undefined,
timeoutSeconds: serverlessFunctionToDuplicate.timeoutSeconds,
applicationId: serverlessFunctionToDuplicate.applicationId ?? undefined,
serverlessFunctionLayerId:
serverlessFunctionToDuplicate.serverlessFunctionLayerId ?? undefined,
},
workspaceId,
);
@@ -4,11 +4,18 @@ import { type DatabaseEventTrigger } from 'src/engine/metadata-modules/database-
import { type Route } from 'src/engine/metadata-modules/route/route.entity';
import { type ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { type ExtractRecordTypeOrmRelationProperties } from 'src/engine/workspace-manager/workspace-migration-v2/types/extract-record-typeorm-relation-properties.type';
import { type ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { type ServerlessFunctionLayerEntity } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.entity';
export type ServerlessFunctionEntityRelationProperties =
ExtractRecordTypeOrmRelationProperties<
ServerlessFunctionEntity,
CronTrigger | DatabaseEventTrigger | Route | Workspace
| CronTrigger
| DatabaseEventTrigger
| Route
| Workspace
| ApplicationEntity
| ServerlessFunctionLayerEntity
>;
export type FlatServerlessFunction = Omit<
@@ -26,11 +26,13 @@ export const fromCreateServerlessFunctionInputToFlatServerlessFunction = ({
deletedAt: null,
latestVersion: null,
publishedVersions: [],
applicationId: null,
applicationId: createServerlessFunctionInput.applicationId ?? null,
latestVersionInputSchema: null,
runtime: ServerlessFunctionRuntime.NODE22,
timeoutSeconds: createServerlessFunctionInput.timeoutSeconds ?? 300,
layerVersion: LAST_LAYER_VERSION,
serverlessFunctionLayerId:
createServerlessFunctionInput.serverlessFunctionLayerId ?? null,
workspaceId,
checksum: null,
};