fix: use findOne instead of findOneOrFail in deleteCorePicture and await deletion in handleUpdate
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.
This commit is contained in:
+9
-1
@@ -161,7 +161,7 @@ export class FileCorePictureService {
|
||||
fileId: string;
|
||||
workspaceId: string;
|
||||
}): Promise<void> {
|
||||
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(
|
||||
{
|
||||
|
||||
+2
-28
@@ -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<FlatViewFieldMaps> {
|
||||
private readonly logger = new Logger(
|
||||
WorkspaceFlatViewFieldMapCacheService.name,
|
||||
);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(ViewFieldEntity)
|
||||
private readonly viewFieldRepository: Repository<ViewFieldEntity>,
|
||||
@@ -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,
|
||||
|
||||
+2
-23
@@ -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<FlatViewFilterMaps> {
|
||||
private readonly logger = new Logger(
|
||||
WorkspaceFlatViewFilterMapCacheService.name,
|
||||
);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(ViewFilterEntity)
|
||||
private readonly viewFilterRepository: Repository<ViewFilterEntity>,
|
||||
@@ -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,
|
||||
|
||||
+2
-21
@@ -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<FlatViewSortMaps> {
|
||||
private readonly logger = new Logger(
|
||||
WorkspaceFlatViewSortMapCacheService.name,
|
||||
);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(ViewSortEntity)
|
||||
private readonly viewSortRepository: Repository<ViewSortEntity>,
|
||||
@@ -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,
|
||||
|
||||
+1
-1
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user