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