Compare commits

...
Author SHA1 Message Date
Sonarly Claude Code 37425f4aae Relation filter key lookup fails when settings.joinColumnName is missing
https://sonarly.com/issue/8007?type=bug

The optimistic cache update for company records crashes because a RELATION filter key (`farmId`) cannot be resolved back to its field metadata — the filter is built using a hardcoded `name + 'Id'` convention, but the reverse lookup relies on `settings.joinColumnName` which may not be populated.

Fix: ## Root Cause Classification: Category A — Logic Error

The naming-convention fallback (`name + 'Id'`) was added for `MORPH_RELATION` fields but was **omitted for regular `RELATION` fields** in two places within `isRecordMatchingFilter.ts`. This is a direct logic omission.

## What was changed

**Fix 1 — Field metadata lookup (lines 200–220)**

Added a 4th fallback in the `objectMetadataField` lookup chain that resolves a filter key like `farmId` back to the `farm` RELATION field using the `name + 'Id'` naming convention:

```typescript file=packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts lines=200-220
const objectMetadataField =
  objectMetadataItem.fields.find((field) => field.name === filterKey) ??
  objectMetadataItem.fields.find(
    (field) =>
      (field.type === FieldMetadataType.RELATION ||
        field.type === FieldMetadataType.MORPH_RELATION) &&
      field.settings?.joinColumnName === filterKey,
  ) ??
  objectMetadataItem.fields.find(
    (field) =>
      field.type === FieldMetadataType.MORPH_RELATION &&
      isMorphRelationJoinColumnKey({ fieldMetadataItem: field, key: filterKey }),
  ) ??
  objectMetadataItem.fields.find(
    (field) =>
      field.type === FieldMetadataType.RELATION &&
      filterKey === `${field.name}Id`,        // NEW: naming-convention fallback
  );
```

**Fix 2 — `isJoinColumn` check in the RELATION switch case (lines 424–432)**

Added the same naming-convention check so that when the resolved field is a `RELATION` type matched via the fallback above, `isJoinColumn` is still `true` and the UUID filter comparison proceeds correctly:

```typescript file=packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts lines=424-432
const isJoinColumn =
  objectMetadataField.settings?.joinColumnName === filterKey ||
  (objectMetadataField.type === FieldMetadataType.RELATION &&
    filterKey === `${objectMetadataField.name}Id`) ||   // NEW: naming-convention fallback
  (objectMetadataField.type === FieldMetadataType.MORPH_RELATION &&
    isMorphRelationJoinColumnKey({
      fieldMetadataItem: objectMetadataField,
      key: filterKey,
    }));
```

## Why this fixes the bug

When a MANY_TO_ONE relation field (e.g., `farm`) has `settings.joinColumnName` not populated (due to the migration gap from the old relation system), the filter key `farmId` produced by `turnRecordFilterIntoGqlOperationFilter` could not be resolved back to any field. The new fallback mirrors exactly what filter construction does — appending `Id` to the field name — so the round-trip always succeeds regardless of whether `settings.joinColumnName` is populated.
2026-03-02 22:25:35 +00:00
@@ -212,6 +212,11 @@ export const isRecordMatchingFilter = ({
fieldMetadataItem: field,
key: filterKey,
}),
) ??
objectMetadataItem.fields.find(
(field) =>
field.type === FieldMetadataType.RELATION &&
filterKey === `${field.name}Id`,
);
if (!isDefined(objectMetadataField)) {
@@ -418,6 +423,8 @@ export const isRecordMatchingFilter = ({
case FieldMetadataType.MORPH_RELATION: {
const isJoinColumn =
objectMetadataField.settings?.joinColumnName === filterKey ||
(objectMetadataField.type === FieldMetadataType.RELATION &&
filterKey === `${objectMetadataField.name}Id`) ||
(objectMetadataField.type === FieldMetadataType.MORPH_RELATION &&
isMorphRelationJoinColumnKey({
fieldMetadataItem: objectMetadataField,