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
@@ -4,7 +4,6 @@ import { isDefined } from 'twenty-shared/utils';
import {
AgentManifest,
AppManifest,
ObjectManifest,
} from 'src/engine/core-modules/application/types/application.types';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
@@ -17,6 +16,8 @@ import {
ApplicationException,
ApplicationExceptionCode,
} from 'src/engine/core-modules/application/application.exception';
import { ServerlessFunctionLayerService } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.service';
import { ApplicationInput } from 'src/engine/core-modules/application/dtos/application.input';
@Injectable()
export class ApplicationSyncService {
@@ -24,17 +25,27 @@ export class ApplicationSyncService {
constructor(
private readonly applicationService: ApplicationService,
private readonly serverlessFunctionLayerService: ServerlessFunctionLayerService,
private readonly objectMetadataServiceV2: ObjectMetadataServiceV2,
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
private readonly dataSourceService: DataSourceService,
private readonly agentService: AgentService,
) {}
public async synchronizeFromManifest(
workspaceId: string,
manifest: AppManifest,
) {
const applicationId = await this.syncApplication(manifest, workspaceId);
public async synchronizeFromManifest({
workspaceId,
manifest,
packageJson,
yarnLock,
}: ApplicationInput & {
workspaceId: string;
}) {
const applicationId = await this.syncApplication({
workspaceId,
manifest,
packageJson,
yarnLock,
});
await this.syncAgents({
agentsToSync: manifest.agents,
@@ -51,32 +62,53 @@ export class ApplicationSyncService {
this.logger.log('✅ Application sync from manifest completed');
}
private async syncApplication(
applicationToSync: AppManifest,
workspaceId: string,
): Promise<string> {
private async syncApplication({
workspaceId,
manifest,
packageJson,
yarnLock,
}: ApplicationInput & {
workspaceId: string;
}): Promise<string> {
const application = await this.applicationService.findByStandardId(
applicationToSync.standardId,
manifest.standardId,
workspaceId,
);
if (!isDefined(application)) {
const serverlessFunctionLayer =
await this.serverlessFunctionLayerService.create(
{
packageJson,
yarnLock,
},
workspaceId,
);
const createdApplication = await this.applicationService.create({
standardId: applicationToSync.standardId,
label: applicationToSync.label,
description: applicationToSync.description,
version: applicationToSync.version,
standardId: manifest.standardId,
label: manifest.label,
description: manifest.description,
version: manifest.version,
sourcePath: 'cli-sync', // Placeholder for CLI-synced apps
serverlessFunctionLayerId: serverlessFunctionLayer.id,
workspaceId,
});
return createdApplication.id;
}
await this.serverlessFunctionLayerService.update(
application.serverlessFunctionLayerId,
{
packageJson,
yarnLock,
},
);
await this.applicationService.update(application.id, {
label: applicationToSync.label,
description: applicationToSync.description,
version: applicationToSync.version,
label: manifest.label,
description: manifest.description,
version: manifest.version,
});
return application.id;
@@ -6,12 +6,18 @@ import {
Index,
JoinColumn,
ManyToOne,
OneToMany,
OneToOne,
PrimaryGeneratedColumn,
Relation,
UpdateDateColumn,
} from 'typeorm';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity';
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
import { ServerlessFunctionLayerEntity } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.entity';
@Entity({ name: 'application', schema: 'core' })
@Index('IDX_APPLICATION_WORKSPACE_ID', ['workspaceId'])
@@ -48,6 +54,38 @@ export class ApplicationEntity {
@Column({ nullable: false, type: 'uuid' })
workspaceId: string;
@Column({ nullable: false, type: 'uuid' })
serverlessFunctionLayerId: string;
@OneToOne(
() => ServerlessFunctionLayerEntity,
(serverlessFunctionLayer) => serverlessFunctionLayer.application,
{
onDelete: 'CASCADE',
},
)
@JoinColumn({ name: 'serverlessFunctionLayerId' })
serverlessFunctionLayer: Relation<ServerlessFunctionLayerEntity>;
@OneToMany(() => AgentEntity, (agent) => agent.application, {
onDelete: 'CASCADE',
})
agents: Relation<AgentEntity[]>;
@OneToMany(
() => ServerlessFunctionEntity,
(serverlessFunction) => serverlessFunction.application,
{
onDelete: 'CASCADE',
},
)
serverlessFunctions: Relation<ServerlessFunctionEntity[]>;
@OneToMany(() => ObjectMetadataEntity, (object) => object.application, {
onDelete: 'CASCADE',
})
objects: Relation<ObjectMetadataEntity[]>;
@ManyToOne(() => Workspace, {
onDelete: 'CASCADE',
})
@@ -11,6 +11,7 @@ import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/core-mod
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { AgentModule } from 'src/engine/metadata-modules/agent/agent.module';
import { ServerlessFunctionLayerModule } from 'src/engine/metadata-modules/serverless-function-layer/serverless-function-layer.module';
@Module({
imports: [
@@ -19,6 +20,7 @@ import { AgentModule } from 'src/engine/metadata-modules/agent/agent.module';
ObjectMetadataModule,
DataSourceModule,
AgentModule,
ServerlessFunctionLayerModule,
],
providers: [ApplicationResolver, ApplicationService, ApplicationSyncService],
})
@@ -1,13 +1,11 @@
import { UseGuards } from '@nestjs/common';
import { Args, Mutation, Resolver } from '@nestjs/graphql';
import GraphQLJSON from 'graphql-type-json';
import { AppManifest } from 'src/engine/core-modules/application/types/application.types';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { ApplicationSyncService } from 'src/engine/core-modules/application/application-sync.service';
import { ApplicationInput } from 'src/engine/core-modules/application/dtos/application.input';
@UseGuards(WorkspaceAuthGuard)
@Resolver()
@@ -18,14 +16,15 @@ export class ApplicationResolver {
@Mutation(() => Boolean)
async syncApplication(
@Args('manifest', { type: () => GraphQLJSON })
manifest: AppManifest,
@Args() { manifest, packageJson, yarnLock }: ApplicationInput,
@AuthWorkspace() { id: workspaceId }: Workspace,
) {
await this.applicationSyncService.synchronizeFromManifest(
await this.applicationSyncService.synchronizeFromManifest({
workspaceId,
manifest,
);
yarnLock,
packageJson,
});
return true;
}
@@ -4,6 +4,7 @@ import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
import { PackageJson } from 'src/engine/core-modules/application/types/application.types';
@Injectable()
export class ApplicationService {
@@ -32,6 +33,7 @@ export class ApplicationService {
label: string;
description?: string;
version?: string;
serverlessFunctionLayerId: string;
sourcePath: string;
workspaceId: string;
}): Promise<ApplicationEntity> {
@@ -50,6 +52,9 @@ export class ApplicationService {
description?: string;
version?: string;
sourcePath?: string;
packageJson?: PackageJson;
yarnLock?: string;
packageChecksum?: string;
},
): Promise<ApplicationEntity> {
await this.applicationRepository.update({ id }, data);
@@ -0,0 +1,20 @@
import { ArgsType, Field } from '@nestjs/graphql';
import GraphQLJSON from 'graphql-type-json';
import {
AppManifest,
PackageJson,
} from 'src/engine/core-modules/application/types/application.types';
@ArgsType()
export class ApplicationInput {
@Field(() => GraphQLJSON, { nullable: false })
manifest: AppManifest;
@Field(() => GraphQLJSON, { nullable: false })
packageJson: PackageJson;
@Field(() => String, { nullable: false })
yarnLock: string;
}
@@ -3,6 +3,11 @@ export type PackageJson = {
standardId: string;
label: string;
description?: string;
engines: {
node: string;
npm: string;
yarn: string;
};
icon?: string;
version: string;
dependencies?: object;