Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code 480db8704b fix: handle Microsoft mailFolders 404 as insufficient permissions instead of NOT_FOUND
https://sonarly.com/issue/8521?type=bug

Microsoft Graph API returns HTTP 404 on `GET /me/mailFolders` for certain deactivated/unlicensed accounts whose error message doesn't match the narrow "inactive, soft-deleted, or is hosted on-premise" check, causing the channel to be permanently marked as FAILED_UNKNOWN with a Sentry alert claiming this "should never happen".

Fix: Two changes targeting the root cause at different depths:

**1. `parseMicrosoftMessagesImportError` (deepest fix):** All HTTP 404 responses from Microsoft Graph API are now classified as `INSUFFICIENT_PERMISSIONS` instead of branching on a narrow string match. Previously, only 404s containing the exact substring "The mailbox is either inactive, soft-deleted, or is hosted on-premise." were treated as permission errors; all other 404s were classified as `NOT_FOUND`. Microsoft returns 404 for various forms of inaccessible resources (deactivated mailbox, license removed, deleted folder, etc.) with different error messages. The correct semantic for all of these is "the connected account cannot access this resource" — the same category as 401/403. The error message now includes both `error.code` and `error.message` for better diagnostics.

**2. `handleNotFoundException` (safety net):** Removed the `MESSAGE_LIST_FETCH` special case that permanently failed the channel (`FAILED_UNKNOWN`) and fired a "should never happen" Sentry alert. All NOT_FOUND exceptions now route to `resetAndMarkAsMessagesListFetchPending` (retry), matching the pattern used by `SYNC_CURSOR_ERROR`. This is correct because the `processMessageListFetch` catch block wraps both folder sync and message list fetch — a NOT_FOUND from either step should trigger a retry, not a permanent failure. The now-unused `syncStep` parameter was removed from the method signature.

With fix #1, Microsoft 404s will no longer reach `handleNotFoundException` at all (they'll go to `handleInsufficientPermissionsException`). Fix #2 ensures that if any other driver produces a NOT_FOUND during MESSAGE_LIST_FETCH, the channel retries instead of permanently failing.
2026-03-16 13:11:48 +00:00
Sonarly Claude Code b308817086 fix: convert ISO datetime to plain date string before passing to DatePicker
https://sonarly.com/issue/15175?type=bug

The DatePicker component crashes with a RangeError when editing DATE fields on custom objects because the raw value from the record store is a full ISO datetime string (e.g. `2026-06-17T22:00:00.000Z`) but `Temporal.PlainDate.from()` only accepts date-only strings.

Fix: Replaced `Temporal.PlainDate.from(string)` with the team's existing `parseToPlainDateOrThrow(string)` utility at three sites where raw record store values (full ISO datetime strings like `2026-06-17T22:00:00.000Z` from TypeORM) could be passed.

`parseToPlainDateOrThrow` (from `twenty-shared/utils`) first attempts `Temporal.Instant.from(string).toZonedDateTimeISO('UTC').toPlainDate()` (handles ISO datetime), then falls back to `Temporal.PlainDate.from(string)` (handles plain dates). This matches the existing pattern used in `buildFilterFromChartBucket.ts`.

**Files changed:**

1. **`DatePicker.tsx`** (line 359) — The primary crash site from Sentry. Added `parseToPlainDateOrThrow` import and replaced `Temporal.PlainDate.from(plainDateString)` with `parseToPlainDateOrThrow(plainDateString)`.

2. **`calendarDayRecordsComponentFamilySelector.ts`** (line 66) — Same bug in the calendar view's record-to-day matching logic for DATE fields. Added import and replaced the call.

3. **`DatePickerWithoutCalendar.tsx`** (line 326) — Defensive fix for the calendar top bar's date picker, which accepts `Nullable<string>` and could receive a datetime string. Added import and replaced the call.
2026-03-16 12:56:58 +00:00
Sonarly Claude Code 36934df3bc fix: handle missing morph relation field in timeline activity for custom objects
https://sonarly.com/issue/15168?type=bug

The timeline activity worker crashes when processing events for custom objects whose morph relation field is missing from the timelineActivity object metadata, causing timeline activity recording to fail silently for affected workspaces.

Fix: **Fix:** Added a metadata validation check in `TimelineActivityRepository.upsertTimelineActivities()` that verifies the morph relation field exists in the `timelineActivity` object metadata before constructing queries with it.

**What changed:**

1. **`timeline-activity.repository.ts`**: Added `hasTimelineActivityMorphRelationField()` method that loads the workspace's flat entity maps, finds the `timelineActivity` object metadata, and checks whether the expected join column name exists in the field maps. If the field is missing, `upsertTimelineActivities()` logs a warning and returns early instead of letting `formatData` crash.

2. **`object-metadata-repository.module.ts`**: Updated the factory to inject `WorkspaceManyOrAllFlatEntityMapsCacheService` into repositories created through this module, so `TimelineActivityRepository` can access workspace metadata for validation.

**Why this approach:**
- Fixes the crash at the right layer (timeline repository) rather than suppressing errors in the shared `formatData` utility
- Preserves the strict validation in `formatData` (which catches other metadata inconsistencies)
- Logs a clear warning with actionable guidance (run workspace:sync-metadata)
- Matches the team's pattern of using `WorkspaceManyOrAllFlatEntityMapsCacheService` for metadata lookups (already used in `TimelineActivityService`)
2026-03-16 12:51:23 +00:00
2 changed files with 5 additions and 44 deletions
@@ -36,23 +36,11 @@ export const parseMicrosoftMessagesImportError = (
}
if (error.statusCode === 404) {
if (
error.message?.includes(
'The mailbox is either inactive, soft-deleted, or is hosted on-premise.',
)
) {
return new MessageImportDriverException(
`Disabled, deleted, inactive or no licence Microsoft account - code:${error.code}`,
MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS,
{ cause: options?.cause },
);
} else {
return new MessageImportDriverException(
`Not found - code:${error.code}`,
MessageImportDriverExceptionCode.NOT_FOUND,
{ cause: options?.cause },
);
}
return new MessageImportDriverException(
`Microsoft Graph API resource not found - code:${error.code} message:${error.message}`,
MessageImportDriverExceptionCode.INSUFFICIENT_PERMISSIONS,
{ cause: options?.cause },
);
}
if (error.statusCode === 410) {
@@ -57,7 +57,6 @@ export class MessageImportExceptionHandlerService {
switch (exception.code) {
case MessageImportDriverExceptionCode.NOT_FOUND:
await this.handleNotFoundException(
syncStep,
messageChannel,
workspaceId,
);
@@ -245,35 +244,9 @@ export class MessageImportExceptionHandlerService {
}
private async handleNotFoundException(
syncStep: MessageImportSyncStep,
messageChannel: Pick<MessageChannelWorkspaceEntity, 'id'>,
workspaceId: string,
): Promise<void> {
if (syncStep === MessageImportSyncStep.MESSAGE_LIST_FETCH) {
await this.messageChannelSyncStatusService.markAsFailed(
[messageChannel.id],
workspaceId,
MessageChannelSyncStatus.FAILED_UNKNOWN,
);
this.exceptionHandlerService.captureExceptions(
[
new Error(
'Not Found exception occurred while fetching message list, which should never happen',
),
],
{
additionalData: {
messageChannelId: messageChannel.id,
syncStep,
},
workspace: { id: workspaceId },
},
);
return;
}
await this.messageChannelSyncStatusService.resetAndMarkAsMessagesListFetchPending(
[messageChannel.id],
workspaceId,