SIGN IN SIGN UP

Model the object navigation command menu item target as a foreign key (#24690)

Closes twentyhq/core-team-issues#2789 (schema, aggregator and backfill
only).

The object navigation command ("Go to Companies") pointed at its object
through `payload.objectMetadataItemId`, a JSON column. No foreign key,
therefore no one-to-many on `objectMetadata`, therefore no aggregator:
every "the navigation command of this object" lookup is a workspace-wide
scan with a payload comparison, and the database knows nothing about the
link.

This PR adds the column, the aggregator and the backfill. Migrating the
readers (`computeCommandMenuItemToDelete`, the validator singleton
guard, the handler utils) is deliberately left out and lands in a
follow-up, per the issue's staging.

### Schema


`2-35-instance-command-fast-1787573269955-add-command-menu-item-target-object-metadata.ts`,
generated against a live database:

- `commandMenuItem.targetObjectMetadataId`, nullable, with a cascading
`ManyToOne` to `objectMetadata` and its `@JoinColumn`, mirroring
`NavigationMenuItemEntity:73-90`.
- The `CHK_CMD_MENU_ITEM_ENGINE_KEY_COHERENCE` expression now pins the
column to null on every non-`NAVIGATION` branch. The `NAVIGATION` branch
stays permissive: instance commands run before workspace commands, so
the column is still null when the constraint is installed. Tightening it
to require the target on object-keyed rows belongs in a later version,
once the backfill has run everywhere.
- Partial unique index on `(targetObjectMetadataId, workspaceId)` where
the target is not null: the singleton invariant the validator enforces
by hand. It is created while every value is null, so it can never reject
an existing workspace on install; duplicates are resolved by the
backfill instead of blocking the migration.
- `@WasIntroducedInUpgrade` on the column so a 2.35 pod booting against
a 2.34 database does not select it.

### Aggregator

- `@OneToMany` `objectMetadata.targetCommandMenuItems`, which makes the
two registry entries (`inverseOneToManyProperty` and the
`targetCommandMenuItemIds` / `targetCommandMenuItemUniversalIdentifiers`
aggregators) type-enforced.
- Populated in `fromObjectMetadataEntityToFlatObjectMetadata` and loaded
in `WorkspaceFlatObjectMetadataMapCacheService`, following the
narrow-find-and-regroup shape of the existing six.
- Cache invalidation and the related-maps sets are derived from the
registry, so they picked the relation up without further changes.

### Backfill


`2-35-workspace-command-1787572700000-backfill-command-menu-item-target-object-metadata.command.ts`
derives `targetObjectMetadataId` from `payload.objectMetadataItemId` for
every object-keyed `NAVIGATION` row. The decision logic lives in a pure
`computeObjectNavigationTargetBackfill` util so it is unit-testable:
idempotent, leaves path-based rows alone, logs and skips rows whose
payload points at a missing object, and backfills at most one command
per target object so the partial unique index cannot be violated.

### The payload's fate

**Dual-write.** The mint path (`buildNavigationFlatCommandMenuItem`) now
writes the column alongside the payload, and `payload` stays the public
read surface until the front migrates off `CommandMenuItemPayloadUnion`.
Removing the object variant from the union is a breaking change to a
public GraphQL type and belongs in a later version. The decision is
written down in a comment on the column.

The public create input keeps `payload` as its only authoring surface
and leaves the column null: the validator does not yet enforce the
singleton invariant, so deriving the column there would turn a
today-legal second object navigation command into a raw
unique-violation.

### Verification

- Entity and database in sync: applied the instance command, re-ran the
generator, got "No changes in database schema were found".
- `database:reset` seeds 96 `NAVIGATION` rows: the 62 object-keyed ones
all carry a target, the 34 path-based ones are all null.
- Nulled the column and ran the backfill: 62 rows restored; a second run
reports "already backfilled"; a payload rewritten to point at a missing
object is logged and left untouched.
- `nx test twenty-server`: 907 suites, 6499 tests green. Five snapshots
updated, all mechanical (the new aggregator and universal property
appearing in registry snapshots), except
`sortMetadataNamesChildrenFirst`, where `commandMenuItem` correctly
moves ahead of `navigationMenuItem` now that it carries one more
many-to-one relation.
- Lint and format clean on the diff.

### Two things worth a reviewer's eye

**This PR needs the `ci:allow-previous-version-upgrade-mutation`
label.** `FlatCommandMenuItem` is a mapped type over the entity, so
adding a column forces a compile fix in two files under older upgrade
directories (`1-21/...refactor-navigation-commands.command.ts` and a
`2-28` spec). Both are a literal `targetObjectMetadataId: null` added to
an object literal; no migration logic is touched.

**`targetObjectMetadataId` is `toCompare: true`**, unlike the sibling
`navigationMenuItem.targetObjectMetadataId` and the other
`commandMenuItem` foreign keys. It has to be:
`sanitizeUniversalFlatEntityUpdate` filters updates down to
`propertiesToCompare`, so with `false` the backfill's update would be
silently dropped. The side effect is that a twenty-standard sync now
repairs drift on the column rather than ignoring it, which is desirable
but does mean the column participates in sync diffs.

### Follow-up

Migrating the readers to index on the aggregator instead of scanning,
and deciding when to drop the object variant from the payload union.


---
_Generated by [Claude
Code](https://claude.ai/code/session_0117Gkrsh4F3ZNVXtEThs5CA)_

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/24690?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
P
Paul Rastoin committed
9364bccbeddb6380944d89f304768d275c343752
Parent: b8125d2
Committed by GitHub <noreply@github.com> on 8/25/2026, 9:51:49 AM