Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code cea450cb9b 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.
2026-03-17 01:40:40 +00:00
Sonarly Claude Code e7422fb8c3 fix: wrap cache recomputation queries in a transaction to prevent snapshot inconsistency
https://sonarly.com/issue/15453?type=bug

When deleting a custom object via the metadata API, the flat entity cache recomputation fails because a ViewField record references a FieldMetadata that no longer exists in the database, preventing any object deletion for the affected workspace.

Fix: Added pre-filtering of orphaned entities in three view-related cache services before converting them to flat entities. The parallel `Promise.all` queries in `computeForCache` run without a shared transaction under PostgreSQL's READ COMMITTED isolation, which allows a concurrent FieldMetadata hard-delete (with FK CASCADE on view entities) to create an inconsistent snapshot: the view entity query returns rows referencing FieldMetadata IDs that the FieldMetadata query no longer includes.

The fix filters out view entities whose `fieldMetadataId` is missing from the `fieldMetadataIdToUniversalIdentifierMap` and logs a warning for each orphaned entity. This prevents the entire cache recomputation from failing due to one inconsistent row.

**Files changed:**
1. `workspace-flat-view-field-map-cache.service.ts` — Filter out ViewField entities with missing FieldMetadata references before conversion loop
2. `workspace-flat-view-filter-map-cache.service.ts` — Same fix for ViewFilter entities
3. `workspace-flat-view-sort-map-cache.service.ts` — Same fix for ViewSort entities

All three services share the same vulnerability pattern (non-transactional parallel queries with FK-dependent entities).
2026-03-17 01:03:49 +00:00
Sonarly Claude Code f298471f36 fix: handle missing morph relation field in timeline activity for custom objects
https://sonarly.com/issue/15455?type=bug

The timeline activity worker crashes with PostgreSQL error 42703 when processing events for custom objects (e.g., "quote") whose morph relation column does not exist in the database, despite being present in the ORM metadata.

Fix: The bug fix already exists in commit `36934df3bc` (authored 2026-03-16) which adds a `hasTimelineActivityMorphRelationField()` validation check in `TimelineActivityRepository.upsertTimelineActivities()`. It verifies the morph relation field exists in the `timelineActivity` object metadata before constructing queries. If the field is missing (as with the `targetQuoteId` column for custom "quote" objects), it logs a warning and returns early instead of crashing.

No duplicate fix was implemented.
2026-03-17 01:00:36 +00:00
2 changed files with 10 additions and 2 deletions
@@ -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(
{
@@ -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)