SIGN IN SIGN UP

fix(server): restore standard default-relation fields wiped by pre-2.20 application syncs (#24001)

## Context

Investigation of
[TWENTY-SERVER-GDY](https://twenty-v7.sentry.io/issues/7450212024)
(`Field metadata for field "targetPersonId" is missing in object
metadata timelineActivity`). Two workspaces lost ALL their standard
default-relation pairs: the `target*` morph legs on
`attachment`/`noteTarget`/`taskTarget`/`timelineActivity` AND the
forward relation fields (`timelineActivities`, `noteTargets`,
`taskTargets`, `attachments`) on person/company/etc. — metadata, columns
and indexes. Only the legs pointing at the workspace's own custom app
objects survived. Symptoms: Timeline/Notes/Tasks tabs gone from all
record pages, timeline worker throwing on every event.

Root cause, reproduced end-to-end on v2.19 code: the standard pairs in
those workspaces were owned (`applicationId`) by the customer's
application instead of twenty-standard (ownership provenance still under
investigation — no shipped code path reassigns it, pointing at an
out-of-band write). A routine app apply then wiped them: the app sync
diffs everything the app owns against its manifest with
`inferDeletionFromMissingEntities: true`, and until #22594 (v2.20,
shipped one day after the first wipe) inferred deletion had no
field-level protection — anything app-owned and absent from the manifest
was destroyed. #22594 + #22882 + the 2-23 reconcile closed the deletion
path on current versions, but nothing recreates the deleted pairs: app
syncs only emit app-owned metadata and the twenty-standard sync only
runs at workspace creation. The damaged workspaces stay broken forever
without a repair.

## What this PR does

Adds the `2-35 restore-standard-default-relation-fields` workspace
command:

- Recomputes the twenty-standard catalog and recreates every missing
default-relation pair (morph legs on the 4 relation objects + their
forward fields), identified by universal identifier, along with the
join-column indexes referencing the recreated legs. Recreated entities
carry the canonical twenty-standard ownership, deterministic universal
identifiers and `isSystemSideEffect` flag straight from the catalog.
- Runs through `validateBuildAndRunLegacyWorkspaceMigration` with
explicit create operations only — no deletion inference involved, so the
command cannot destroy anything.
- Safe on every workspace: no-op when the pairs are complete, and skips
at pair granularity (warn-logged with object-qualified names) when a
member's host object is missing, its name is already taken by a drifted
duplicate, or one member survived while its mate is gone. Skipping must
be pair-level because migration validation rejects a relation field
whose target is neither existing nor in the create batch — a per-field
skip would poison the whole workspace's restore over one drifted
duplicate. Restores only whole missing pairs: the wipe always deleted
both members, so a lone survivor indicates out-of-band drift that needs
manual review, and recreating its mate would leave the survivor's own
relation target pointer dangling. Supports `--dry-run`.

## Tests

- Recreates a missing pair with its index.
- No-op when the pair exists.
- Skips and warns instead of recreating half of a pair when one member
survives.
- Skips the whole pair on a name collision with a drifted duplicate, and
on a missing host object.
- A blocked pair does not block another restorable pair.
- Dry run does not execute the migration.

---------

Co-authored-by: Paul Rastoin <45004772+prastoin@users.noreply.github.com>
T
Thomas Trompette committed
d7170793b1c9f9be7dbf20a093ef5c4281cdbf40
Parent: 0967816
Committed by GitHub <noreply@github.com> on 8/25/2026, 1:04:36 PM