import { Injectable } from '@nestjs/common'; import { FileFolder } from 'twenty-shared/types'; import { isDefined } from 'twenty-shared/utils'; import { ApplicationService } from 'src/engine/core-modules/application/application.service'; import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type'; import { FileStorageService } from 'src/engine/core-modules/file-storage/services/file-storage.service'; import { type FileResponse } from 'src/engine/core-modules/file/types/file-response.type'; import { getContentDisposition } from 'src/engine/core-modules/file/utils/get-content-disposition.utils'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service'; import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util'; import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util'; import { type FlatFrontComponent } from 'src/engine/metadata-modules/flat-front-component/types/flat-front-component.type'; import { fromCreateFrontComponentInputToFlatFrontComponentToCreate } from 'src/engine/metadata-modules/flat-front-component/utils/from-create-front-component-input-to-flat-front-component-to-create.util'; import { fromFlatFrontComponentToFrontComponentDto } from 'src/engine/metadata-modules/flat-front-component/utils/from-flat-front-component-to-front-component-dto.util'; import { fromUpdateFrontComponentInputToFlatFrontComponentToUpdateOrThrow } from 'src/engine/metadata-modules/flat-front-component/utils/from-update-front-component-input-to-flat-front-component-to-update-or-throw.util'; import { type CreateFrontComponentInput } from 'src/engine/metadata-modules/front-component/dtos/create-front-component.input'; import { type FrontComponentDTO } from 'src/engine/metadata-modules/front-component/dtos/front-component.dto'; import { type UpdateFrontComponentInput } from 'src/engine/metadata-modules/front-component/dtos/update-front-component.input'; import { FrontComponentException, FrontComponentExceptionCode, } from 'src/engine/metadata-modules/front-component/front-component.exception'; import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception'; import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service'; @Injectable() export class FrontComponentService { constructor( private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService, private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, private readonly applicationService: ApplicationService, private readonly fileStorageService: FileStorageService, private readonly twentyConfigService: TwentyConfigService, ) {} async findAll(workspaceId: string): Promise { const { flatFrontComponentMaps } = await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { workspaceId, flatMapsKeys: ['flatFrontComponentMaps'], }, ); return Object.values(flatFrontComponentMaps.byUniversalIdentifier) .filter(isDefined) .sort((a, b) => a.name.localeCompare(b.name)) .map(fromFlatFrontComponentToFrontComponentDto); } async findById( id: string, workspaceId: string, ): Promise { const { flatFrontComponentMaps } = await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { workspaceId, flatMapsKeys: ['flatFrontComponentMaps'], }, ); const flatFrontComponent = findFlatEntityByIdInFlatEntityMaps({ flatEntityId: id, flatEntityMaps: flatFrontComponentMaps, }); if (!isDefined(flatFrontComponent)) { return null; } return fromFlatFrontComponentToFrontComponentDto(flatFrontComponent); } async createOne({ input, workspaceId, ownerFlatApplication, }: { input: Omit; workspaceId: string; ownerFlatApplication?: FlatApplication; }): Promise { const resolvedOwnerFlatApplication = ownerFlatApplication ?? ( await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( { workspaceId }, ) ).workspaceCustomFlatApplication; const flatFrontComponentToCreate = fromCreateFrontComponentInputToFlatFrontComponentToCreate({ createFrontComponentInput: input, workspaceId, flatApplication: resolvedOwnerFlatApplication, }); const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { allFlatEntityOperationByMetadataName: { frontComponent: { flatEntityToCreate: [flatFrontComponentToCreate], flatEntityToDelete: [], flatEntityToUpdate: [], }, }, workspaceId, isSystemBuild: false, applicationUniversalIdentifier: resolvedOwnerFlatApplication.universalIdentifier, }, ); if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while creating front component', ); } const { flatFrontComponentMaps: recomputedFlatFrontComponentMaps } = await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { workspaceId, flatMapsKeys: ['flatFrontComponentMaps'], }, ); const createdFlatFrontComponent = findFlatEntityByIdInFlatEntityMapsOrThrow( { flatEntityId: flatFrontComponentToCreate.id, flatEntityMaps: recomputedFlatFrontComponentMaps, }, ); return createdFlatFrontComponent; } async updateOne({ id, update, workspaceId, ownerFlatApplication, }: { id: string; update: UpdateFrontComponentInput['update']; workspaceId: string; ownerFlatApplication?: FlatApplication; }): Promise { const resolvedOwnerFlatApplication = ownerFlatApplication ?? ( await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( { workspaceId }, ) ).workspaceCustomFlatApplication; const { flatFrontComponentMaps: existingFlatFrontComponentMaps } = await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { workspaceId, flatMapsKeys: ['flatFrontComponentMaps'], }, ); const flatFrontComponentToUpdate = fromUpdateFrontComponentInputToFlatFrontComponentToUpdateOrThrow({ flatFrontComponentMaps: existingFlatFrontComponentMaps, updateFrontComponentInput: { id, update }, }); const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { allFlatEntityOperationByMetadataName: { frontComponent: { flatEntityToCreate: [], flatEntityToDelete: [], flatEntityToUpdate: [flatFrontComponentToUpdate], }, }, workspaceId, isSystemBuild: false, applicationUniversalIdentifier: resolvedOwnerFlatApplication.universalIdentifier, }, ); if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while updating front component', ); } const { flatFrontComponentMaps: recomputedFlatFrontComponentMaps } = await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { workspaceId, flatMapsKeys: ['flatFrontComponentMaps'], }, ); const updatedFlatFrontComponent = findFlatEntityByIdInFlatEntityMapsOrThrow( { flatEntityId: id, flatEntityMaps: recomputedFlatFrontComponentMaps, }, ); return updatedFlatFrontComponent; } async destroyOne({ id, workspaceId, isSystemBuild = false, ownerFlatApplication, }: { id: string; workspaceId: string; isSystemBuild?: boolean; ownerFlatApplication?: FlatApplication; }): Promise { const resolvedOwnerFlatApplication = ownerFlatApplication ?? ( await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( { workspaceId }, ) ).workspaceCustomFlatApplication; const { flatFrontComponentMaps: existingFlatFrontComponentMaps } = await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps( { workspaceId, flatMapsKeys: ['flatFrontComponentMaps'], }, ); const existingFlatFrontComponent = findFlatEntityByIdInFlatEntityMaps({ flatEntityId: id, flatEntityMaps: existingFlatFrontComponentMaps, }); if (!isDefined(existingFlatFrontComponent)) { throw new FrontComponentException( 'Front component to destroy not found', FrontComponentExceptionCode.FRONT_COMPONENT_NOT_FOUND, ); } const validateAndBuildResult = await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration( { allFlatEntityOperationByMetadataName: { frontComponent: { flatEntityToCreate: [], flatEntityToDelete: [existingFlatFrontComponent], flatEntityToUpdate: [], }, }, workspaceId, isSystemBuild, applicationUniversalIdentifier: resolvedOwnerFlatApplication.universalIdentifier, }, ); if (validateAndBuildResult.status === 'fail') { throw new WorkspaceMigrationBuilderException( validateAndBuildResult, 'Multiple validation errors occurred while destroying front component', ); } return existingFlatFrontComponent; } async findByIdOrThrow( id: string, workspaceId: string, ): Promise { const frontComponent = await this.findById(id, workspaceId); if (!isDefined(frontComponent)) { throw new FrontComponentException( 'Front component not found', FrontComponentExceptionCode.FRONT_COMPONENT_NOT_FOUND, ); } return frontComponent; } async getBuiltComponentPresignedUrlOrStream({ frontComponentId, workspaceId, }: { frontComponentId: string; workspaceId: string; }): Promise { const frontComponent = await this.findByIdOrThrow( frontComponentId, workspaceId, ); const application = await this.applicationService.findOneApplicationOrThrow( { id: frontComponent.applicationId, workspaceId, }, ); const mimeType = 'application/javascript'; const resourceIdentifier = { workspaceId, applicationUniversalIdentifier: application.universalIdentifier, fileFolder: FileFolder.BuiltFrontComponent, resourcePath: frontComponent.builtComponentPath, }; const presignedUrl = await this.fileStorageService.getPresignedUrl({ ...resourceIdentifier, expiresInSeconds: this.twentyConfigService.get( 'STORAGE_S3_PRESIGNED_URL_EXPIRES_IN', ), responseContentType: mimeType, responseContentDisposition: getContentDisposition(mimeType), }); if (presignedUrl) { return { type: 'redirect', presignedUrl }; } const stream = await this.fileStorageService.readFile(resourceIdentifier); return { type: 'stream', stream, mimeType }; } }