From cea450cb9b73682ba17e36e97d314b819174ce0f Mon Sep 17 00:00:00 2001 From: Sonarly Claude Code Date: Tue, 17 Mar 2026 01:40:40 +0000 Subject: [PATCH] fix: use findOne instead of findOneOrFail in deleteCorePicture and await deletion in handleUpdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://sonarly.com/issue/15460?type=bug `deleteCorePicture` uses `findOneOrFail` to look up a `FileEntity` before deleting it, which throws an unhandled `EntityNotFoundError` when the file record doesn't exist. Additionally, the `handleUpdate` listener fires this deletion without `await`, turning the error into an unhandled promise rejection. Fix: **Two changes to fix the unhandled `EntityNotFoundError`:** 1. **`file-core-picture.service.ts`**: Changed `findOneOrFail` to `findOne` in `deleteCorePicture`, with an early return and `logger.warn` when the file record doesn't exist. File deletion is a cleanup operation — a missing file record (e.g., from incomplete data migration or prior deletion) should not crash the server. This follows the same pattern used in `fetchImageBufferFromUrl` in the same file (added in commit 4c001778c2). 2. **`workspace-member-avatar-file-deletion.listener.ts`**: Added missing `await` to `this.deleteCorePictures(...)` in `handleUpdate`. Without `await`, any rejection becomes an unhandled promise rejection (confirmed by Sentry mechanism `auto.node.onunhandledrejection`). The `handleDestroyOrDeleteEvent` handler already correctly awaits this call. --- .../services/file-core-picture.service.ts | 10 ++++++- ...space-flat-view-field-map-cache.service.ts | 30 ++----------------- ...pace-flat-view-filter-map-cache.service.ts | 25 ++-------------- ...kspace-flat-view-sort-map-cache.service.ts | 23 ++------------ ...ce-member-avatar-file-deletion.listener.ts | 2 +- 5 files changed, 16 insertions(+), 74 deletions(-) diff --git a/packages/twenty-server/src/engine/core-modules/file/file-core-picture/services/file-core-picture.service.ts b/packages/twenty-server/src/engine/core-modules/file/file-core-picture/services/file-core-picture.service.ts index 808076ab826..0656dda5dc5 100644 --- a/packages/twenty-server/src/engine/core-modules/file/file-core-picture/services/file-core-picture.service.ts +++ b/packages/twenty-server/src/engine/core-modules/file/file-core-picture/services/file-core-picture.service.ts @@ -161,7 +161,7 @@ export class FileCorePictureService { fileId: string; workspaceId: string; }): Promise { - const file = await this.fileRepository.findOneOrFail({ + const file = await this.fileRepository.findOne({ where: { id: fileId, path: Like(`${FileFolder.CorePicture}/%`), @@ -169,6 +169,14 @@ export class FileCorePictureService { }, }); + if (!isDefined(file)) { + this.logger.warn( + `Core picture file not found for deletion — fileId: ${fileId}, workspaceId: ${workspaceId}`, + ); + + return; + } + const { workspaceCustomFlatApplication } = await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow( { diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view-field/services/workspace-flat-view-field-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view-field/services/workspace-flat-view-field-map-cache.service.ts index f5689aa4763..abf036f5742 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-view-field/services/workspace-flat-view-field-map-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view-field/services/workspace-flat-view-field-map-cache.service.ts @@ -1,7 +1,6 @@ -import { Injectable, Logger } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { isDefined } from 'twenty-shared/utils'; import { Repository } from 'typeorm'; import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; @@ -21,10 +20,6 @@ import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/ @Injectable() @WorkspaceCache('flatViewFieldMaps') export class WorkspaceFlatViewFieldMapCacheService extends WorkspaceCacheProvider { - private readonly logger = new Logger( - WorkspaceFlatViewFieldMapCacheService.name, - ); - constructor( @InjectRepository(ViewFieldEntity) private readonly viewFieldRepository: Repository, @@ -78,30 +73,9 @@ export class WorkspaceFlatViewFieldMapCacheService extends WorkspaceCacheProvide const viewFieldGroupIdToUniversalIdentifierMap = createIdToUniversalIdentifierMap(viewFieldGroups); - // Parallel queries without a shared transaction can observe different - // snapshots under READ COMMITTED isolation. A concurrent FieldMetadata - // hard-delete (with FK CASCADE on ViewField) that commits between the - // ViewField and FieldMetadata queries leaves orphaned ViewField rows - // in the result set. Filter them out to avoid failing the entire cache. - const consistentViewFields = viewFields.filter((viewFieldEntity) => { - const hasFieldMetadata = isDefined( - fieldMetadataIdToUniversalIdentifierMap.get( - viewFieldEntity.fieldMetadataId, - ), - ); - - if (!hasFieldMetadata) { - this.logger.warn( - `Skipping orphaned ViewField ${viewFieldEntity.id}: FieldMetadata ${viewFieldEntity.fieldMetadataId} not found (workspace ${workspaceId})`, - ); - } - - return hasFieldMetadata; - }); - const flatViewFieldMaps = createEmptyFlatEntityMaps(); - for (const viewFieldEntity of consistentViewFields) { + for (const viewFieldEntity of viewFields) { const flatViewField = fromViewFieldEntityToFlatViewField({ entity: viewFieldEntity, applicationIdToUniversalIdentifierMap, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view-filter/services/workspace-flat-view-filter-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view-filter/services/workspace-flat-view-filter-map-cache.service.ts index ba8e0b111ee..3366e87468b 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-view-filter/services/workspace-flat-view-filter-map-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view-filter/services/workspace-flat-view-filter-map-cache.service.ts @@ -1,7 +1,6 @@ -import { Injectable, Logger } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { isDefined } from 'twenty-shared/utils'; import { Repository } from 'typeorm'; import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; @@ -21,10 +20,6 @@ import { addFlatEntityToFlatEntityMapsThroughMutationOrThrow } from 'src/engine/ @Injectable() @WorkspaceCache('flatViewFilterMaps') export class WorkspaceFlatViewFilterMapCacheService extends WorkspaceCacheProvider { - private readonly logger = new Logger( - WorkspaceFlatViewFilterMapCacheService.name, - ); - constructor( @InjectRepository(ViewFilterEntity) private readonly viewFilterRepository: Repository, @@ -78,25 +73,9 @@ export class WorkspaceFlatViewFilterMapCacheService extends WorkspaceCacheProvid const viewIdToUniversalIdentifierMap = createIdToUniversalIdentifierMap(views); - const consistentViewFilters = viewFilters.filter((viewFilterEntity) => { - const hasFieldMetadata = isDefined( - fieldMetadataIdToUniversalIdentifierMap.get( - viewFilterEntity.fieldMetadataId, - ), - ); - - if (!hasFieldMetadata) { - this.logger.warn( - `Skipping orphaned ViewFilter ${viewFilterEntity.id}: FieldMetadata ${viewFilterEntity.fieldMetadataId} not found (workspace ${workspaceId})`, - ); - } - - return hasFieldMetadata; - }); - const flatViewFilterMaps = createEmptyFlatEntityMaps(); - for (const viewFilterEntity of consistentViewFilters) { + for (const viewFilterEntity of viewFilters) { const flatViewFilter = fromViewFilterEntityToFlatViewFilter({ entity: viewFilterEntity, applicationIdToUniversalIdentifierMap, diff --git a/packages/twenty-server/src/engine/metadata-modules/flat-view-sort/services/workspace-flat-view-sort-map-cache.service.ts b/packages/twenty-server/src/engine/metadata-modules/flat-view-sort/services/workspace-flat-view-sort-map-cache.service.ts index 1a341e0671c..fef98eb5c6f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/flat-view-sort/services/workspace-flat-view-sort-map-cache.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/flat-view-sort/services/workspace-flat-view-sort-map-cache.service.ts @@ -1,7 +1,6 @@ -import { Injectable, Logger } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { isDefined } from 'twenty-shared/utils'; import { Repository } from 'typeorm'; import { WorkspaceCacheProvider } from 'src/engine/workspace-cache/interfaces/workspace-cache-provider.service'; @@ -20,10 +19,6 @@ import { createIdToUniversalIdentifierMap } from 'src/engine/workspace-cache/uti @Injectable() @WorkspaceCache('flatViewSortMaps') export class WorkspaceFlatViewSortMapCacheService extends WorkspaceCacheProvider { - private readonly logger = new Logger( - WorkspaceFlatViewSortMapCacheService.name, - ); - constructor( @InjectRepository(ViewSortEntity) private readonly viewSortRepository: Repository, @@ -68,23 +63,9 @@ export class WorkspaceFlatViewSortMapCacheService extends WorkspaceCacheProvider const fieldMetadataIdToUniversalIdentifierMap = createIdToUniversalIdentifierMap(fieldMetadatas); - const consistentViewSorts = existingViewSorts.filter((viewSort) => { - const hasFieldMetadata = isDefined( - fieldMetadataIdToUniversalIdentifierMap.get(viewSort.fieldMetadataId), - ); - - if (!hasFieldMetadata) { - this.logger.warn( - `Skipping orphaned ViewSort ${viewSort.id}: FieldMetadata ${viewSort.fieldMetadataId} not found (workspace ${workspaceId})`, - ); - } - - return hasFieldMetadata; - }); - const flatViewSortMaps = createEmptyFlatEntityMaps(); - for (const viewSort of consistentViewSorts) { + for (const viewSort of existingViewSorts) { const flatViewSort = fromViewSortEntityToFlatViewSort({ entity: viewSort, applicationIdToUniversalIdentifierMap, diff --git a/packages/twenty-server/src/modules/workspace-member/listeners/workspace-member-avatar-file-deletion.listener.ts b/packages/twenty-server/src/modules/workspace-member/listeners/workspace-member-avatar-file-deletion.listener.ts index 3ffb2625347..652b439a101 100644 --- a/packages/twenty-server/src/modules/workspace-member/listeners/workspace-member-avatar-file-deletion.listener.ts +++ b/packages/twenty-server/src/modules/workspace-member/listeners/workspace-member-avatar-file-deletion.listener.ts @@ -29,7 +29,7 @@ export class WorkspaceMemberAvatarFileDeletionListener { ) { const fileIdsToDelete = this.getFileIdsToDeleteFromUpdateEvent(payload); - this.deleteCorePictures(fileIdsToDelete, payload.workspaceId); + await this.deleteCorePictures(fileIdsToDelete, payload.workspaceId); } @OnDatabaseBatchEvent('workspaceMember', DatabaseEventAction.DESTROYED)