Skip to content

Conversation

@FelixMalfait
Copy link
Member

@FelixMalfait FelixMalfait commented Dec 26, 2025

Overview

This PR implements generic many-to-many relation support through junction tables (also known as associative entities or join tables). This replaces the need for hardcoded taskTarget/noteTarget logic and provides a flexible foundation for modeling complex entity relationships.

Architecture

Data Model

Many-to-many relationships are implemented using a junction object pattern:

┌─────────┐       ┌──────────────────┐       ┌─────────┐
│  Pet    │──────>│   PetRocket      │<──────│ Rocket  │
│         │ 1:N   │  (junction)      │  N:1  │         │
│ rockets ├───────┤ pet    : Pet     ├───────┤         │
└─────────┘       │ rocket : Rocket  │       └─────────┘
                  └──────────────────┘

The junction object (PetRocket) has:

  • A MANY_TO_ONE relation to Pet (the source)
  • A MANY_TO_ONE relation to Rocket (the target)

The source object (Pet) has a ONE_TO_MANY relation pointing to the junction, with field settings that specify which target field to follow.

Field Settings Schema

Junction configuration is stored in FieldMetadataRelationSettings:

{
  relationType: "ONE_TO_MANY",
  // Points to the target field on the junction object
  junctionTargetFieldId?: string;      // For regular relations
  junctionTargetMorphId?: string;      // For polymorphic relations
}

Two configuration modes:

  1. junctionTargetFieldId - References a specific RELATION field on the junction
  2. junctionTargetMorphId - References a morphId group for polymorphic targets (e.g., link to Person OR Company)

GraphQL Query Generation

When a junction relation is detected, the GraphQL fields are generated to fetch the nested target:

query GetPetWithRockets {
  pet(id: "...") {
    rockets {           # ONE_TO_MANY to junction
      id
      rocket {          # Target field on junction
        id
        name
        __typename
      }
    }
  }
}

For polymorphic junction targets:

caretakerPerson { id, name }
caretakerCompany { id, name }

Frontend Architecture

Display Flow

  1. Detection: hasJunctionConfig() checks if field has junction settings
  2. Config Resolution: getJunctionConfig() resolves junction object metadata and target fields
  3. Record Extraction: extractTargetRecordsFromJunction() extracts target records from junction records
  4. Rendering: Target records displayed as chips (not junction records)

Edit Flow

  1. Picker Opening: Initializes the multi-record picker with:

    • Searchable object types (derived from junction target fields)
    • Pre-selected items (extracted from existing junction records)
  2. Selection Handling: Manages create/delete of junction records:

    • Select: Creates new junction record with source + target IDs
    • Deselect: Finds and deletes the junction record
    • Optimistic Updates: Manually updates Recoil store before API call

Key Trade-offs

Decision Trade-off
Junction records managed manually More control over optimistic updates, but requires manual cache management
Settings stored per-field Flexible (same junction can power different views), but requires UI to configure
Polymorphic via morphId groups Supports N target types, but adds query complexity
Feature flag gated Safe rollout, but requires flag management

Backend Changes

  • Validation: Junction target field must exist and be a valid MANY_TO_ONE relation
  • Settings: Extended FieldMetadataRelationSettings type with junction fields
  • Dev Seeder: Added sample junction objects (PetRocket, EmploymentHistory, PetCareAgreement) for testing

How to Test

  1. Enable the IS_JUNCTION_RELATIONS_ENABLED feature flag
  2. Create objects with junction pattern (Pet → PetRocket → Rocket)
  3. Configure the junction target in field settings (advanced mode)
  4. Verify:
    • Display shows target objects (Rockets), not junction records (PetRockets)
    • Picker allows selecting/deselecting targets
    • Changes persist correctly
Screen.Recording.2026-01-07.at.13.01.47.mov

- Add junctionTargetRelationFieldIds to field metadata settings
- Backend validation for junction target relation field IDs
- Settings UI to configure junction relations (advanced mode only)
- Frontend hooks for junction relation CRUD operations
- Display junction target objects instead of junction records
- Proper GraphQL field generation for nested junction targets
@github-actions
Copy link
Contributor

github-actions bot commented Dec 26, 2025

TODOs/FIXMEs:

  • // TODO: Remove this once taskTarget/noteTarget are migrated to use junction configuration: packages/twenty-front/src/modules/object-record/record-field/ui/hooks/useOpenFieldInputEditMode.ts

Generated by 🚫 dangerJS against 6c8461f

@github-actions
Copy link
Contributor

github-actions bot commented Dec 26, 2025

🚀 Preview Environment Ready!

Your preview environment is available at: http://bore.pub:28618

This environment will automatically shut down when the PR is closed or after 5 hours.

FelixMalfait and others added 26 commits December 26, 2025 18:48
- Consolidated all junction-related utilities into junction/ subdirectory
- Each file now has a single export for better tree-shaking
- Added barrel export for clean imports
- Extracted field categorization logic into reusable utility
…elPayload utility, use joinColumnName from settings
objectMetadataItems: ObjectMetadataItem[];
};

// Build GraphQL fields for a regular RELATION target field
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove comment

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done - removed the comment.

objectNameSingular,
});

// Order must match the rendering order in RecordFieldList
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove comment

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done - removed the comment.

boxedRelationFieldMetadataItems,
inlineFieldMetadataItems: allInlineFieldMetadataItems,
// Activity targets need special rendering with ActivityTargetsInlineCell
inlineRelationFieldMetadataItems: activityTargetFields,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe rename to something with LegacyActivityTarget or something like that so we understand it's just that and will be migrated

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done! Renamed inlineRelationFieldMetadataItems to legacyActivityTargetFieldMetadataItems to make it clear this is specifically for noteTargets/taskTargets that will be migrated.

boxedRelationFields: FieldMetadataItem[];
};

// Check if field is a hardcoded activity target relation (noteTargets/taskTargets)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove comment (not helpful/needed)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done - removed the comment.

(objectNameSingular === CoreObjectNameSingular.Task &&
fieldMetadataItem.name === 'taskTargets');

// Check if user has read permission for the relation's target object(s)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove comment (not helpful/needed)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done - removed the comment.

.getLoadable(currentWorkspaceState)
.getValue();

const isJunctionRelationsEnabled =
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure the feature flag check is needed here? Maybe we can get rid of currentWorkspace / this check, because this happens in "open input" and it's too late, it means the field is already shown the the user anyway if we arrived there

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[AI Reply] Good question! The feature flag check IS still needed here because while the field is shown in the UI (as a regular relation), the junction behavior (opening MultipleRecordPicker with checkbox selection) should only activate if the flag is enabled. Without this check, users would get the junction interaction but the backend wouldn't support it. However, I can see how this might be confusing - in a future iteration when the flag is removed, this check can be cleaned up.

)?.value ?? false;

// Get the actual field metadata item with saved settings from the database
const { fieldMetadataItem } = getFieldMetadataItemById({
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need this? Why don't we have settings on fieldDefinition?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch! You're right - settings IS already on fieldDefinition.metadata.settings (populated by formatFieldMetadataItemAsFieldDefinition). I've removed the redundant getFieldMetadataItemById call and now use fieldDefinition.metadata.settings directly. Much cleaner!

objectMetadataItems,
});

// Check for metadata-driven junction relation (many-to-many through junction)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove these two comments

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done - removed both comments.

.getLoadable(objectMetadataItemsState)
.getValue();

// Get the actual field metadata item with saved settings from the database
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question as before, do we really need this? Setting not available on fieldDefinition?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed! Same issue - now using fieldDefinition.metadata.settings directly instead of fetching separately.

fieldDefinition?.metadata.relationObjectMetadataNameSingular;

// Get the actual field metadata item with saved settings
const { fieldMetadataItem } = getFieldMetadataItemById({
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again (x3) do we really need this? Definition does not have settings?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed! Same pattern - now using fieldDefinition.metadata.settings directly instead of fetching separately.

});

createRecordPayload[`${gqlField}Id`] = recordId;
(createRecordPayload as Record<string, unknown>)[`${gqlField}Id`] =
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this ugly casting? Let's keep it clean as it was

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed! The createRecordPayload is of type Record<string, unknown> so the assignment works directly without casting. Removed the ugly cast.

>();
const isMobile = useIsMobile();
const isJunctionRelationsEnabled = useIsFeatureEnabled(
'IS_JUNCTION_RELATIONS_ENABLED' as FeatureFlagKey,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Casting needed? Can we take the flag from graphql for example instead?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed! Now using the FeatureFlagKey enum directly: FeatureFlagKey.IS_JUNCTION_RELATIONS_ENABLED instead of casting a string.

});
const isJunctionRelation = hasJunctionConfig(fieldMetadataItem?.settings);

// Get junction config for display
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove comment

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done - removed the comment.

.filter(isDefined)}
{targetRecordsWithMetadata.map(({ record, objectMetadata }) => (
<RecordChip
key={record.id as string}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why cast? Please review ALL cast we introduced in this PR and everytime solve the ROOT CAUSE that led to the type issue, avoiding casting.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed! The cast was unnecessary since ObjectRecord has id: string from BaseObjectRecord. Removed the cast - now just uses record.id directly.

- Remove unnecessary comments throughout the codebase
- Rename inlineRelationFieldMetadataItems to legacyActivityTargetFieldMetadataItems for clarity
- Use fieldDefinition.metadata.settings instead of redundant getFieldMetadataItemById calls
- Remove type castings by fixing root causes (proper Pick<> types)
- Use FeatureFlagKey enum instead of string casting
…ame Pet Care Agreements to Cared For Pets

- Pass sourceObjectMetadataId in RelationFromManyFieldDisplay for proper source field identification
- Rename field label from 'Pet Care Agreements' to 'Cared For Pets' to make it clear we're showing Pets, not agreements
… in junction Add New

When creating a junction record via 'Add New', the sourceJoinColumnName was being
read directly from sourceField.settings.joinColumnName. For morph source fields
(like caretaker on PetCareAgreement), this doesn't work because the join column
depends on the actual source object type (Person vs Company).

Now the code computes the correct join column name dynamically using
computeMorphRelationFieldName when the source field is a MORPH_RELATION.
…ionRelationGqlFields, lint errors in twenty-shared
@FelixMalfait FelixMalfait marked this pull request as ready for review January 7, 2026 10:06
Comment on lines +225 to +235
? [...currentFieldValue, junctionRecordForStore]
: [junctionRecordForStore];

return {
...currentRecord,
[fieldName]: updatedJunctionRecords,
};
});

await createJunctionRecord(newJunctionRecordForApi);
}

This comment was marked as outdated.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6 issues found across 71 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellValue.tsx">

<violation number="1" location="packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellValue.tsx:45">
P2: The `onClick` handler is not guarded by `readonly` state. While visual cues (cursor, edit button) correctly hide when readonly, clicking the cell will still trigger `onOpenEditMode`. Consider conditionally applying the handler.</violation>
</file>

<file name="packages/twenty-front/src/modules/object-record/utils/buildRecordLabelPayload.ts">

<violation number="1" location="packages/twenty-front/src/modules/object-record/utils/buildRecordLabelPayload.ts:22">
P2: Whitespace handling bug in name parsing. Input with leading/trailing spaces or multiple spaces between words will incorrectly parse firstName/lastName (e.g., `'  John  Doe  '` gives `firstName=''` instead of `'John'`). Consider trimming and splitting on whitespace pattern.</violation>
</file>

<file name="packages/twenty-front/src/modules/object-record/record-field/ui/hooks/useUpdateJunctionRelationFromCell.ts">

<violation number="1" location="packages/twenty-front/src/modules/object-record/record-field/ui/hooks/useUpdateJunctionRelationFromCell.ts:234">
P2: Missing error handling for optimistic update rollback. If `createJunctionRecord` fails, the Recoil store will retain the stale junction record with no cleanup. Consider wrapping in try/catch to rollback the store update on failure, similar to how `useCreateOneRecord` handles errors internally.</violation>
</file>

<file name="packages/twenty-front/src/modules/settings/data-model/fields/forms/morph-relation/components/SettingsDataModelFieldRelationJunctionForm.tsx">

<violation number="1" location="packages/twenty-front/src/modules/settings/data-model/fields/forms/morph-relation/components/SettingsDataModelFieldRelationJunctionForm.tsx:178">
P2: The `targetObjectLabel` always uses `regularRelationFields[0]` instead of the actually selected field. When multiple regular relation fields exist and the user selects one other than the first, the description will show the wrong target object label. Should find the selected field using `junctionTargetFieldId`.</violation>
</file>

<file name="packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx">

<violation number="1" location="packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx:129">
P1: Changing from `values` to `defaultValues` breaks form reactivity. Since `fieldMetadataItem` data loads asynchronously from Recoil state, the form will initialize with fallback values (`'Icon'`, `''`, `true`, etc.) and won't update when actual data arrives. This can cause users to see incorrect values and potentially submit wrong data.

Consider keeping `values` for data that loads asynchronously, or use `reset()` in a `useEffect` when the data changes.</violation>
</file>

<file name="packages/twenty-front/src/modules/object-record/record-field-list/utils/categorizeRelationFields.ts">

<violation number="1" location="packages/twenty-front/src/modules/object-record/record-field-list/utils/categorizeRelationFields.ts:46">
P2: Empty `morphRelations` array causes `.every()` to return `true` (vacuous truth), potentially granting unintended read access. Consider adding a length check to ensure there are actual morph relations to verify permissions against.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

mode: 'onTouched',
resolver: zodResolver(settingsFieldFormSchema()),
values: {
defaultValues: {
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Changing from values to defaultValues breaks form reactivity. Since fieldMetadataItem data loads asynchronously from Recoil state, the form will initialize with fallback values ('Icon', '', true, etc.) and won't update when actual data arrives. This can cause users to see incorrect values and potentially submit wrong data.

Consider keeping values for data that loads asynchronously, or use reset() in a useEffect when the data changes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx, line 129:

<comment>Changing from `values` to `defaultValues` breaks form reactivity. Since `fieldMetadataItem` data loads asynchronously from Recoil state, the form will initialize with fallback values (`'Icon'`, `''`, `true`, etc.) and won't update when actual data arrives. This can cause users to see incorrect values and potentially submit wrong data.

Consider keeping `values` for data that loads asynchronously, or use `reset()` in a `useEffect` when the data changes.</comment>

<file context>
@@ -108,20 +108,34 @@ export const SettingsObjectFieldEdit = () => {
     mode: 'onTouched',
     resolver: zodResolver(settingsFieldFormSchema()),
-    values: {
+    defaultValues: {
       icon: fieldMetadataItem?.icon ?? 'Icon',
       type: fieldMetadataItem?.type as SettingsFieldType,
</file context>
Fix with Cubic

<RecordInlineCellDisplayMode isHovered={false}>
<RecordInlineCellDisplayMode
isHovered={isFocused}
onClick={onOpenEditMode}
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The onClick handler is not guarded by readonly state. While visual cues (cursor, edit button) correctly hide when readonly, clicking the cell will still trigger onOpenEditMode. Consider conditionally applying the handler.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellValue.tsx, line 45:

<comment>The `onClick` handler is not guarded by `readonly` state. While visual cues (cursor, edit button) correctly hide when readonly, clicking the cell will still trigger `onOpenEditMode`. Consider conditionally applying the handler.</comment>

<file context>
@@ -29,15 +30,20 @@ const StyledClickableContainer = styled.div<{
-      <RecordInlineCellDisplayMode isHovered={false}>
+      <RecordInlineCellDisplayMode
+        isHovered={isFocused}
+        onClick={onOpenEditMode}
+      >
         <FieldDisplay />
</file context>
Suggested change
onClick={onOpenEditMode}
onClick={!readonly ? onOpenEditMode : undefined}
Fix with Cubic

getLabelIdentifierFieldMetadataItem(objectMetadataItem);

if (labelIdentifierField?.type === FieldMetadataType.FULL_NAME) {
const words = searchInput?.split(' ') ?? [];
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Whitespace handling bug in name parsing. Input with leading/trailing spaces or multiple spaces between words will incorrectly parse firstName/lastName (e.g., ' John Doe ' gives firstName='' instead of 'John'). Consider trimming and splitting on whitespace pattern.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/twenty-front/src/modules/object-record/utils/buildRecordLabelPayload.ts, line 22:

<comment>Whitespace handling bug in name parsing. Input with leading/trailing spaces or multiple spaces between words will incorrectly parse firstName/lastName (e.g., `'  John  Doe  '` gives `firstName=''` instead of `'John'`). Consider trimming and splitting on whitespace pattern.</comment>

<file context>
@@ -0,0 +1,34 @@
+    getLabelIdentifierFieldMetadataItem(objectMetadataItem);
+
+  if (labelIdentifierField?.type === FieldMetadataType.FULL_NAME) {
+    const words = searchInput?.split(' ') ?? [];
+    const hasMultipleWords = words.length > 1;
+
</file context>
Fix with Cubic

};
});

await createJunctionRecord(newJunctionRecordForApi);
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Missing error handling for optimistic update rollback. If createJunctionRecord fails, the Recoil store will retain the stale junction record with no cleanup. Consider wrapping in try/catch to rollback the store update on failure, similar to how useCreateOneRecord handles errors internally.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/twenty-front/src/modules/object-record/record-field/ui/hooks/useUpdateJunctionRelationFromCell.ts, line 234:

<comment>Missing error handling for optimistic update rollback. If `createJunctionRecord` fails, the Recoil store will retain the stale junction record with no cleanup. Consider wrapping in try/catch to rollback the store update on failure, similar to how `useCreateOneRecord` handles errors internally.</comment>

<file context>
@@ -0,0 +1,259 @@
+            };
+          });
+
+          await createJunctionRecord(newJunctionRecordForApi);
+        }
+      },
</file context>
Fix with Cubic

const isMorphSelected = selectedOption?.type === 'morph';
const targetObjectLabel = isMorphSelected
? t`linked records`
: ((regularRelationFields[0]?.relation?.targetObjectMetadata
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The targetObjectLabel always uses regularRelationFields[0] instead of the actually selected field. When multiple regular relation fields exist and the user selects one other than the first, the description will show the wrong target object label. Should find the selected field using junctionTargetFieldId.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/twenty-front/src/modules/settings/data-model/fields/forms/morph-relation/components/SettingsDataModelFieldRelationJunctionForm.tsx, line 178:

<comment>The `targetObjectLabel` always uses `regularRelationFields[0]` instead of the actually selected field. When multiple regular relation fields exist and the user selects one other than the first, the description will show the wrong target object label. Should find the selected field using `junctionTargetFieldId`.</comment>

<file context>
@@ -0,0 +1,231 @@
+  const isMorphSelected = selectedOption?.type === 'morph';
+  const targetObjectLabel = isMorphSelected
+    ? t`linked records`
+    : ((regularRelationFields[0]?.relation?.targetObjectMetadata
+        ? objectMetadataItems.find(
+            (item) =>
</file context>
Fix with Cubic

fieldMetadataItem.relation?.targetObjectMetadata.id,
).canReadObjectRecords;

const canReadMorphRelation = fieldMetadataItem?.morphRelations?.every(
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Empty morphRelations array causes .every() to return true (vacuous truth), potentially granting unintended read access. Consider adding a length check to ensure there are actual morph relations to verify permissions against.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/twenty-front/src/modules/object-record/record-field-list/utils/categorizeRelationFields.ts, line 46:

<comment>Empty `morphRelations` array causes `.every()` to return `true` (vacuous truth), potentially granting unintended read access. Consider adding a length check to ensure there are actual morph relations to verify permissions against.</comment>

<file context>
@@ -0,0 +1,92 @@
+      fieldMetadataItem.relation?.targetObjectMetadata.id,
+    ).canReadObjectRecords;
+
+  const canReadMorphRelation = fieldMetadataItem?.morphRelations?.every(
+    (morphRelation) =>
+      isDefined(morphRelation.targetObjectMetadata.id) &&
</file context>
Fix with Cubic

- Split junction utility functions into separate files for better modularity and clarity.
- Update import paths to reflect the new structure.
- Remove unnecessary comments and streamline code for improved readability.
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 35 files (changes from recent commits).

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="packages/twenty-front/src/modules/object-record/record-field/ui/utils/junction/isObjectWithId.ts">

<violation number="1" location="packages/twenty-front/src/modules/object-record/record-field/ui/utils/junction/isObjectWithId.ts:4">
P2: Type guard narrowing is incomplete - `ObjectRecord` requires both `id` and `__typename`, but only `id` is validated. This could cause runtime errors if consuming code accesses `__typename` after this check. Consider either:
1. Adding `'__typename' in value && typeof value.__typename === 'string'` to the check, or
2. Using a more accurate return type like `value is { id: string; [key: string]: unknown }`</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { isDefined } from 'twenty-shared/utils';

export const isObjectWithId = (value: unknown): value is ObjectRecord =>
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Type guard narrowing is incomplete - ObjectRecord requires both id and __typename, but only id is validated. This could cause runtime errors if consuming code accesses __typename after this check. Consider either:

  1. Adding '__typename' in value && typeof value.__typename === 'string' to the check, or
  2. Using a more accurate return type like value is { id: string; [key: string]: unknown }
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/twenty-front/src/modules/object-record/record-field/ui/utils/junction/isObjectWithId.ts, line 4:

<comment>Type guard narrowing is incomplete - `ObjectRecord` requires both `id` and `__typename`, but only `id` is validated. This could cause runtime errors if consuming code accesses `__typename` after this check. Consider either:
1. Adding `'__typename' in value && typeof value.__typename === 'string'` to the check, or
2. Using a more accurate return type like `value is { id: string; [key: string]: unknown }`</comment>

<file context>
@@ -0,0 +1,8 @@
+import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
+import { isDefined } from 'twenty-shared/utils';
+
+export const isObjectWithId = (value: unknown): value is ObjectRecord =>
+  isDefined(value) &&
+  typeof value === 'object' &&
</file context>
Fix with Cubic

…lationOneToManyFieldInput

- Update the RelationOneToManyFieldInput component to ensure objectMetadataNameSingular is not null by providing a default empty string. This prevents potential runtime errors when the value is undefined.
Comment on lines +233 to +242

await createTargetRecord(targetPayload);

const newJunctionId = v4();
const createdJunction = await createJunctionRecord({
id: newJunctionId,
[sourceJoinColumnName]: recordId,
[targetJoinColumnName]: newTargetId,
});

This comment was marked as outdated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants