refactor: stop reading joinColumnName from relation field settings#20304
refactor: stop reading joinColumnName from relation field settings#20304charlesBochet wants to merge 7 commits intomainfrom
Conversation
Always compute the foreign key column name from the field name (and the
target object name for morph relations) instead of reading
`joinColumnName` from `FieldMetadataSettings`. The settings field is no
longer read anywhere in production code, but is still kept in the schema
for backward compatibility (data is not migrated yet).
Adds two clearly named, shared utilities in `twenty-shared/utils` so
that frontend and backend use the same logic:
- `computeRelationFieldJoinColumnName({ name })` — appends `Id` to a
field name (e.g. `company` → `companyId`).
- `computeMorphRelationFieldJoinColumnName({ fieldName, relationType,
targetObjectMetadataNameSingular, targetObjectMetadataNamePlural })`
— combines the morph-aware field name with the `Id` suffix
(e.g. `target` + `opportunity` (MANY_TO_ONE) → `targetOpportunityId`).
Updates all read sites across `twenty-front`, `twenty-server` and
`twenty-shared` to use the computed value, and adjusts a couple of
tests that asserted the legacy `null joinColumnName` behavior.
There was a problem hiding this comment.
Pull request overview
This PR refactors relation join-column handling by eliminating production reads of field.settings?.joinColumnName and instead deriving join-column names from field names (and morph target metadata) via new shared utilities in twenty-shared.
Changes:
- Added shared utilities to compute relation and morph-relation join-column names and re-exported them from
twenty-shared/utils. - Updated many frontend/backend call sites to compute join-column names instead of reading
settings.joinColumnName. - Updated/removed tests that relied on legacy
joinColumnName: nullbehavior.
Reviewed changes
Copilot reviewed 42 out of 42 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/twenty-shared/src/utils/index.ts | Re-exports the new shared join-column computation utilities. |
| packages/twenty-shared/src/utils/fieldMetadata/compute-relation-field-join-column-name.ts | Introduces shared join-column name computation helpers for relation and morph relation fields. |
| packages/twenty-shared/src/utils/fieldMetadata/tests/computeRelationFieldJoinColumnName.test.ts | Adds unit tests for the new shared utilities. |
| packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/utils/generate-column-definitions.util.ts | Computes relation column names from field names for MANY_TO_ONE fields. |
| packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/index/utils/index-action-handler.utils.ts | Uses computed join-column names when deriving index column names. |
| packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/field/services/update-field-action-handler.service.ts | Removes joinColumnName-rename logic and relies on computed join column + field rename handling. |
| packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/field/services/create-field-action-handler.service.ts | Computes FK join-column name from field name for MANY_TO_ONE relations. |
| packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-builder/utils/build-universal-flat-object-field-by-name-and-join-column-maps.util.ts | Builds join-column maps by computing join-column names for MANY_TO_ONE relations. |
| packages/twenty-server/src/engine/twenty-orm/utils/is-record-matching-rls-row-level-permission-predicate.util.ts | Matches RLS predicates against computed join-column names instead of settings. |
| packages/twenty-server/src/engine/twenty-orm/utils/format-column-name-for-relation-field.util.ts | Formats MANY_TO_ONE relation columns using computed join-column names. |
| packages/twenty-server/src/engine/twenty-orm/utils/determine-schema-relation-details.util.ts | Uses computed join-column name when building TypeORM relation details for MANY_TO_ONE. |
| packages/twenty-server/src/engine/twenty-orm/factories/entity-schema-column.factory.ts | Builds entity schema join columns using computed join-column names. |
| packages/twenty-server/src/engine/metadata-modules/index-metadata/utils/generate-flat-index.util.ts | Generates index metadata column names using computed join-column names. |
| packages/twenty-server/src/engine/metadata-modules/flat-field-metadata/utils/get-object-field-names-and-join-column-names.util.ts | Produces join-column name lists by computing join-column names for MANY_TO_ONE. |
| packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/compute-morph-or-relation-field-join-column-name.util.ts | Keeps server wrapper but delegates to shared join-column util. |
| packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/extract-graphql-relation-field-names.util.ts | Computes join-column names for GraphQL schema generation. |
| packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select.ts | Ensures join-column columns are selected by computing join-column names. |
| packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/tests/build-columns-to-select.spec.ts | Removes legacy test that expected behavior when joinColumnName was null. |
| packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-selected-fields/graphql-selected-fields.parser.ts | Detects join-column selections via computed join-column names. |
| packages/twenty-server/src/engine/api/common/common-select-fields/utils/get-is-flat-field-a-junction-relation-field.ts | Treats MANY_TO_ONE relation fields as junction relations without reading joinColumnName. |
| packages/twenty-server/src/engine/api/common/common-query-runners/common-merge-many-query-runner.service.ts | Supplies computed join-column names to merge logic for MANY_TO_ONE relations. |
| packages/twenty-server/src/engine/api/common/common-nested-relations-processor/process-nested-relations-v2.helper.ts | Computes join-column names for nested relation processing instead of reading settings. |
| packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/data-arg-processor.service.ts | Uses computed join-column name when validating relation write inputs. |
| packages/twenty-front/src/modules/object-record/utils/sanitizeRecordInput.ts | Detects relation join-column fields via computed join-column names. |
| packages/twenty-front/src/modules/object-record/utils/prefillRecord.ts | Uses computed join-column names when pre-filling relation ID fields. |
| packages/twenty-front/src/modules/object-record/utils/computeOptimisticRecordFromInput.ts | Detects/computes relation ID field names using computed join-column names. |
| packages/twenty-front/src/modules/object-record/record-update-multiple/components/UpdateMultipleRecordsForm.tsx | Uses computed join-column name for MANY_TO_ONE relation edits. |
| packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.ts | Matches filters against computed join-column names instead of settings. |
| packages/twenty-front/src/modules/object-record/record-field/ui/utils/junction/getSourceJoinColumnName.ts | Computes join-column names for sources, including morph-target-aware join columns. |
| packages/twenty-front/src/modules/object-record/record-field/ui/utils/junction/getJoinColumnNameOrThrow.ts | Updates helper signature to take a field (name) and compute join-column name. |
| packages/twenty-front/src/modules/object-record/record-field/ui/utils/junction/getJoinColumnName.ts | Computes join-column name from field name rather than reading settings. |
| packages/twenty-front/src/modules/object-record/record-field/ui/utils/junction/findTargetFieldInfo.ts | Computes join-column name for morph and relation targets. |
| packages/twenty-front/src/modules/object-record/record-field/ui/utils/tests/getJoinColumnName.test.ts | Updates tests to reflect join-column computation from field name. |
| packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/input/components/RelationOneToManyFieldInput.tsx | Uses updated getJoinColumnName API (field-based). |
| packages/twenty-front/src/modules/object-record/record-field/ui/meta-types/hooks/useRelationToOneFieldDisplay.ts | Computes join-column name from field name for display/lookup. |
| packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationSectionDropdownToMany.tsx | Uses updated getJoinColumnName API (field-based). |
| packages/twenty-front/src/modules/object-record/cache/utils/getRecordNodeFromRecord.ts | Detects join-column keys via computed join-column names. |
| packages/twenty-front/src/modules/object-record/cache/utils/getFieldMetadataFromGqlField.ts | Maps gql field names to metadata via computed join-column names. |
| packages/twenty-front/src/modules/object-metadata/utils/shouldFieldBeQueried.ts | Treats join-column gql fields via computed join-column names. |
| packages/twenty-front/src/modules/object-metadata/utils/mapObjectMetadataToGraphQLQuery.ts | Adds join-column gql fields by computing join-column names (and morph join-column names). |
| packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts | Recognizes join-column gql fields via computed join-column names. |
| packages/twenty-front/src/modules/object-metadata/utils/tests/shouldFieldBeQueried.test.ts | Updates test inputs to match join-column derivation from field name. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const name = isMorphOrRelationUniversalFlatFieldMetadata( | ||
| relatedFlatFieldMetadata, | ||
| ) | ||
| ? (relatedFlatFieldMetadata.universalSettings.joinColumnName ?? | ||
| relatedFlatFieldMetadata.name) | ||
| ? computeRelationFieldJoinColumnName({ | ||
| name: relatedFlatFieldMetadata.name, | ||
| }) | ||
| : relatedFlatFieldMetadata.name; |
There was a problem hiding this comment.
2 issues found across 42 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select.ts">
<violation number="1" location="packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/build-columns-to-select.ts:99">
P1: MORPH_RELATION join columns are now added without restricting to MANY_TO_ONE, which can select non-existent `...Id` columns for ONE_TO_MANY morph fields.</violation>
</file>
<file name="packages/twenty-front/src/modules/object-metadata/utils/shouldFieldBeQueried.ts">
<violation number="1" location="packages/twenty-front/src/modules/object-metadata/utils/shouldFieldBeQueried.ts:25">
P1: Morph-relation join columns are matched with the non-morph `${fieldName}Id` helper, so morph join-column fields can be skipped from default GraphQL queries.
(Based on your team's feedback about computing morph/relation join column names at runtime.) [FEEDBACK_USED]</violation>
</file>
Tip: cubic used a learning from your PR history. Let your coding agent read cubic learnings directly with the cubic MCP.
📊 API Changes ReportGraphQL Schema ChangesGraphQL Schema Changes[error] Error: Unable to read JSON file: /home/runner/work/twenty/twenty/main-schema-introspection.json: Not valid JSON content GraphQL Metadata Schema ChangesGraphQL Metadata Schema Changes[error] Error: Unable to read JSON file: /home/runner/work/twenty/twenty/main-metadata-schema-introspection.json: Not valid JSON content REST API Analysis ErrorError OutputREST Metadata API Analysis ErrorError Output |
|
🚀 Preview Environment Ready! Your preview environment is available at: http://bore.pub:57756 This environment will automatically shut down after 5 hours. |
…tests - Removes the frontend `getJoinColumnName` / `getJoinColumnNameOrThrow` wrappers (and their test) and the backend `computeMorphOrRelationFieldJoinColumnName` thin wrapper. All call sites now import `computeRelationFieldJoinColumnName` directly from `twenty-shared/utils`. - Expands the shared util test suite to cover edge cases: camelCase field names, morph-aware field names, simple/irregular plurals, and invalid relation types (which throw).
There was a problem hiding this comment.
2 issues found across 19 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/twenty-server/src/modules/workflow/workflow-executor/utils/format-workflow-record-relation-fields.util.ts">
<violation number="1" location="packages/twenty-server/src/modules/workflow/workflow-executor/utils/format-workflow-record-relation-fields.util.ts:64">
P2: `computeRelationFieldJoinColumnName` only handles non-morph relations. This code now applies it to morph relations too, which will generate the wrong join column name (missing the morph target segment). Morph MANY_TO_ONE fields should use the morph-aware join column computation or be excluded here.</violation>
</file>
<file name="packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/data-arg-processor.service.ts">
<violation number="1" location="packages/twenty-server/src/engine/api/common/common-args-processors/data-arg-processor/data-arg-processor.service.ts:247">
P2: This uses the non-morph join column helper even when `fieldMetadata.type` is `MORPH_RELATION`, which ignores the target-specific naming (e.g., `targetOpportunityId`). That will cause morph join-column keys to be rejected or mismatched. Use the morph-aware join column computation for morph relations.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
Address review feedback: avoid generating ${name}Id for relation/morph
fields that don't have a database column.
- build-columns-to-select / index handlers / generate-flat-index now
only compute join column names when relationType is MANY_TO_ONE.
- shouldFieldBeQueried matches morph join columns by iterating
morphRelations and using the morph-aware join column helper.
- process-nested-relations throws if a one-to-many's target relation
cannot be resolved instead of computing 'Id'.
Make the abstraction explicit so callers can't accidentally use the non-morph helper on a frontend morph FieldMetadataItem (which carries the base name + morphRelations[]). - twenty-shared/utils now exposes only gqlField-aware helpers: computeRelationGqlFieldJoinColumnName, computeMorphRelationGqlFieldJoinColumnName. - twenty-server reintroduces computeMorphOrRelationFieldJoinColumnName, used everywhere in the backend since FlatFieldMetadata.name is already morph-resolved at the db layer. - All call sites updated; tests/typecheck/lint clean.
Mirror the gqlField/flat split already in place for join column helpers: - twenty-shared: rename `computeMorphRelationFieldName` to `computeMorphRelationGqlFieldName` (frontend / GraphQL field name). - twenty-server: introduce `computeMorphRelationFlatFieldName` that produces a `FlatFieldMetadata.name` and is only used in metadata mutation paths (create / update / object rename). Backend reads keep consuming the already-resolved `field.name` and never recompute it, so the new util's scope is the write path only.
…n-name-from-settings # Conflicts: # packages/twenty-front/src/modules/object-record/graphql/record-gql-fields/utils/generateDepthRecordGqlFieldsFromFields.ts
Summary
joinColumnNameon relation field settings is always derivable from the field name (and the target object name for morph relations). This PR stops reading it from settings anywhere in production code; the stored value is no longer used.The settings field is not removed from data yet — a follow-up can drop it once we are confident nothing depends on the stored value.
Helpers
The helpers are split by layer because frontend and backend hold morph relations differently: the frontend has a base name plus a
morphRelations[]array, the backend has one row per target with the name already morph-resolved.computeRelationGqlFieldJoinColumnNamegqlField)computeMorphRelationGqlFieldNamegqlField)targetCompany).computeMorphRelationGqlFieldJoinColumnNamegqlField)computeMorphOrRelationFieldJoinColumnNameFlatFieldMetadata.name)computeMorphRelationFlatFieldNameFlatFieldMetadata.name)field.nameand never call this.Test plan