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
@@ -0,0 +1,16 @@
import { ArgsType, Field } from '@nestjs/graphql';
import { IsString } from 'class-validator';
import GraphQLJSON from 'graphql-type-json';
import { PackageJson } from 'src/engine/core-modules/application/types/application.types';
@ArgsType()
export class CreateServerlessFunctionLayerInput {
@Field(() => GraphQLJSON, { nullable: false })
packageJson: PackageJson;
@IsString()
@Field(() => String, { nullable: false })
yarnLock: string;
}
@@ -0,0 +1,23 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { IsDateString } from 'class-validator';
import { IDField } from '@ptc-org/nestjs-query-graphql';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@ObjectType('ServerlessFunctionLayer')
export class ServerlessFunctionLayerDTO {
@IDField(() => UUIDScalarType)
id: string;
@IDField(() => UUIDScalarType, { nullable: true })
applicationId?: string;
@IsDateString()
@Field()
createdAt: Date;
@IsDateString()
@Field()
updatedAt: Date;
}
@@ -0,0 +1,56 @@
import {
Column,
CreateDateColumn,
Entity,
OneToMany,
OneToOne,
PrimaryGeneratedColumn,
Relation,
UpdateDateColumn,
} from 'typeorm';
import { PackageJson } from 'src/engine/core-modules/application/types/application.types';
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
@Entity('serverlessFunctionLayer')
export class ServerlessFunctionLayerEntity {
@PrimaryGeneratedColumn('uuid')
id: string;
@Column({ type: 'jsonb', nullable: false })
packageJson: PackageJson;
@Column({ type: 'text', nullable: false })
yarnLock: string;
@Column({ type: 'text', nullable: false })
checksum: string;
@Column({ nullable: false, type: 'uuid' })
workspaceId: string;
@OneToMany(
() => ServerlessFunctionEntity,
(serverlessFunction) => serverlessFunction.serverlessFunctionLayer,
{
onDelete: 'RESTRICT',
},
)
serverlessFunctions: Relation<ServerlessFunctionEntity[]>;
@OneToOne(
() => ApplicationEntity,
(application) => application.serverlessFunctionLayer,
{
nullable: true,
},
)
application: Relation<ApplicationEntity> | null;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt: Date;
}
@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ServerlessFunctionLayerEntity } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.entity';
import { ServerlessFunctionLayerService } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.service';
import { ServerlessFunctionLayerResolver } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.resolver';
@Module({
imports: [TypeOrmModule.forFeature([ServerlessFunctionLayerEntity])],
providers: [ServerlessFunctionLayerService, ServerlessFunctionLayerResolver],
exports: [ServerlessFunctionLayerService],
})
export class ServerlessFunctionLayerModule {}
@@ -0,0 +1,29 @@
import { UseGuards } from '@nestjs/common';
import { Args, Mutation, Resolver } from '@nestjs/graphql';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { ServerlessFunctionLayerService } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.service';
import { CreateServerlessFunctionLayerInput } from 'src/engine/metadata-modules/serverless-function-layer/dtos/create-serverless-function-layer.input';
import { ServerlessFunctionLayerDTO } from 'src/engine/metadata-modules/serverless-function-layer/dtos/serverless-function-layer.dto';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@UseGuards(WorkspaceAuthGuard)
@Resolver()
export class ServerlessFunctionLayerResolver {
constructor(
private readonly serverlessFunctionLayerService: ServerlessFunctionLayerService,
) {}
@Mutation(() => ServerlessFunctionLayerDTO)
async createOneServerlessFunctionLayer(
@Args()
createServerlessFunctionLayerInput: CreateServerlessFunctionLayerInput,
@AuthWorkspace() { id: workspaceId }: Workspace,
) {
return this.serverlessFunctionLayerService.create(
createServerlessFunctionLayerInput,
workspaceId,
);
}
}
@@ -0,0 +1,67 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { isDefined } from 'twenty-shared/utils';
import type { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity';
import { ServerlessFunctionLayerEntity } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.entity';
import { CreateServerlessFunctionLayerInput } from 'src/engine/metadata-modules/serverless-function-layer/dtos/create-serverless-function-layer.input';
import { getLastCommonLayerDependencies } from 'src/engine/core-modules/serverless/drivers/utils/get-last-layer-dependencies';
import { serverlessFunctionCreateHash } from 'src/engine/metadata-modules/serverless-function/utils/serverless-function-create-hash.utils';
@Injectable()
export class ServerlessFunctionLayerService {
constructor(
@InjectRepository(ServerlessFunctionLayerEntity)
private readonly serverlessFunctionLayerRepository: Repository<ServerlessFunctionLayerEntity>,
) {}
async create(
{ packageJson, yarnLock }: CreateServerlessFunctionLayerInput,
workspaceId: string,
) {
const checksum = serverlessFunctionCreateHash(yarnLock);
const serverlessFunctionLayer =
this.serverlessFunctionLayerRepository.create({
packageJson,
yarnLock,
checksum,
workspaceId,
});
return this.serverlessFunctionLayerRepository.save(serverlessFunctionLayer);
}
async update(
id: string,
data: QueryDeepPartialEntity<ServerlessFunctionLayerEntity>,
) {
const checksum = data.yarnLock
? serverlessFunctionCreateHash(data.yarnLock as string)
: undefined;
const updateData = { ...data, ...(checksum && { checksum }) };
return this.serverlessFunctionLayerRepository.update(id, updateData);
}
async createCommonLayerIfNotExist(workspaceId: string) {
const { packageJson, yarnLock } = await getLastCommonLayerDependencies();
const checksum = serverlessFunctionCreateHash(yarnLock);
const commonLayer = await this.serverlessFunctionLayerRepository.findOne({
where: {
checksum,
workspaceId,
},
});
if (isDefined(commonLayer)) {
return commonLayer;
}
return this.create({ packageJson, yarnLock }, workspaceId);
}
}