Add endpoint to upload application file (#17270)

As title
This commit is contained in:
martmull
2026-01-20 14:10:34 +00:00
committed by GitHub
parent 801878cb2b
commit 02a181a49f
24 changed files with 302 additions and 39 deletions
@@ -1,4 +1,5 @@
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ApplicationSyncService } from 'src/engine/core-modules/application/application-sync.service';
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
@@ -20,9 +21,12 @@ import { ServerlessFunctionModule } from 'src/engine/metadata-modules/serverless
import { WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-builder-graphql-api-exception.interceptor';
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-common.module';
import { FileStorageModule } from 'src/engine/core-modules/file-storage/file-storage.module';
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
@Module({
imports: [
TypeOrmModule.forFeature([FileEntity]),
ApplicationModule,
ApplicationVariableEntityModule,
WorkspaceManyOrAllFlatEntityMapsCacheModule,
@@ -40,6 +44,7 @@ import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-commo
ObjectPermissionModule,
PermissionFlagModule,
WorkflowCommonModule,
FileStorageModule,
],
providers: [
ApplicationResolver,
@@ -1,7 +1,15 @@
import { UseFilters, UseGuards, UseInterceptors } from '@nestjs/common';
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { InjectRepository } from '@nestjs/typeorm';
import path, { join } from 'path';
import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs';
import { PermissionFlagType } from 'twenty-shared/constants';
import { type Repository } from 'typeorm';
import { FileFolder } from 'twenty-shared/types';
import type { FileUpload } from 'graphql-upload/processRequest.mjs';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { ApplicationExceptionFilter } from 'src/engine/core-modules/application/application-exception-filter';
@@ -11,12 +19,21 @@ import { ApplicationDTO } from 'src/engine/core-modules/application/dtos/applica
import { ApplicationInput } from 'src/engine/core-modules/application/dtos/application.input';
import { UninstallApplicationInput } from 'src/engine/core-modules/application/dtos/uninstallApplicationInput';
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { RequireFeatureFlag } from 'src/engine/guards/feature-flag.guard';
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { WorkspaceMigrationBuilderGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-builder-graphql-api-exception.interceptor';
import { streamToBuffer } from 'src/utils/stream-to-buffer';
import { UploadApplicationFileInput } from 'src/engine/core-modules/application/dtos/uploadApplicationFileInput';
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
import {
ApplicationException,
ApplicationExceptionCode,
} from 'src/engine/core-modules/application/application.exception';
import { FileDTO } from 'src/engine/core-modules/file/dtos/file.dto';
@UseGuards(
WorkspaceAuthGuard,
@@ -29,6 +46,9 @@ export class ApplicationResolver {
constructor(
private readonly applicationSyncService: ApplicationSyncService,
private readonly applicationService: ApplicationService,
private readonly fileStorageService: FileStorageService,
@InjectRepository(FileEntity)
private readonly fileRepository: Repository<FileEntity>,
) {}
@Query(() => [ApplicationDTO])
@@ -75,4 +95,61 @@ export class ApplicationResolver {
return true;
}
@Mutation(() => FileDTO)
@UseGuards(SettingsPermissionGuard(PermissionFlagType.UPLOAD_FILE))
async uploadApplicationFile(
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
@Args({ name: 'file', type: () => GraphQLUpload })
{ createReadStream, mimetype }: FileUpload,
@Args()
{
applicationUniversalIdentifier,
fileFolder,
filePath,
}: UploadApplicationFileInput,
): Promise<FileDTO> {
const allowedApplicationFileFolders: FileFolder[] = [
FileFolder.Functions,
FileFolder.FrontComponents,
FileFolder.Assets,
FileFolder.SourceCode,
];
if (!allowedApplicationFileFolders.includes(fileFolder)) {
throw new ApplicationException(
`Invalid fileFolder for application file upload. Allowed values: ${allowedApplicationFileFolders.join(', ')}`,
ApplicationExceptionCode.INVALID_INPUT,
);
}
const stream = createReadStream();
const buffer = await streamToBuffer(stream);
const dirname = path.dirname(filePath);
const filename = path.basename(filePath);
const folderPath = join(
`workspace-${workspaceId}`,
applicationUniversalIdentifier,
fileFolder,
dirname,
);
await this.fileStorageService.write({
file: buffer,
name: filename,
folder: folderPath,
mimeType: mimetype,
});
const createdFile = this.fileRepository.create({
path: join(folderPath, filename),
size: buffer.length,
workspaceId,
});
return await this.fileRepository.save(createdFile);
}
}
@@ -0,0 +1,15 @@
import { ArgsType, Field } from '@nestjs/graphql';
import { FileFolder } from 'twenty-shared/types';
@ArgsType()
export class UploadApplicationFileInput {
@Field(() => String)
applicationUniversalIdentifier: string;
@Field(() => FileFolder)
fileFolder: FileFolder;
@Field(() => String)
filePath: string;
}