#12870·typeorm

Allow the owning side of a ManyToMany to set onDelete/onUpdate for the inverse junction foreign key

Author: jonaskordtCreated Sep 16, 2026Updated Sep 16, 2026

Feature Description

A @ManyToMany junction table has two foreign keys. TypeORM derives their referential actions from two different relations (JunctionEntityMetadataBuilder):

  • the foreign key to the owning entity uses relation.onDelete ?? "CASCADE"
  • the foreign key to the inverse entity uses relation.inverseRelation?.onDelete ?? "CASCADE" (same for onUpdate and deferrable)

So the only way to control the inverse foreign key is to declare the inverse relation on the other entity, set onDelete there, and point at it from the owner with an inverse-side function. If the inverse relation is missing, or the owner does not reference it, the inverse foreign key silently gets ON DELETE CASCADE ON UPDATE CASCADE. JoinTableOptions.inverseJoinColumn only accepts name, referencedColumnName and foreignKeyConstraintName, so there is no place on the owning side to express this.

Example that cannot be expressed today without touching Topic:

typescript
@Entity()
export class Post {
  @ManyToMany(() => Topic)
  @JoinTable({
    name: 'post_topics',
    joinColumn: { name: 'post_id' },
    inverseJoinColumn: { name: 'topic_id' },
  })
  topics: Topic[];
}

Desired schema: deleting a post removes its junction rows (CASCADE), deleting a topic that is still assigned to a post must fail (NO ACTION).

The Solution

Add onDelete, onUpdate and deferrable to the inverseJoinColumn options of @JoinTable, so the owning side can describe both junction foreign keys:

typescript
@ManyToMany(() => Topic, { onDelete: 'CASCADE' })
@JoinTable({
  name: 'post_topics',
  joinColumn: { name: 'post_id' },
  inverseJoinColumn: {
    name: 'topic_id',
    onDelete: 'NO ACTION',
    onUpdate: 'NO ACTION',
  },
})
topics: Topic[];

Types:

typescript
export interface JoinTableOptions {
  joinColumn?: JoinColumnOptions;
  inverseJoinColumn?: JoinColumnOptions & {
    onDelete?: OnDeleteType;
    onUpdate?: OnUpdateType;
    deferrable?: DeferrableType;
  };
  // ...
}

Resolution order for the inverse junction foreign key in JunctionEntityMetadataBuilder:

  1. relation.inverseRelation.onDelete if the inverse relation is declared and sets a value (existing behaviour, unchanged)
  2. otherwise joinTable.inverseJoinColumn.onDelete if set (new)
  3. otherwise "CASCADE" (existing default)

The same order applies to onUpdate and deferrable. Driver overrides (Spanner, Oracle) stay as they are.

Because the new option only applies when the inverse relation does not set a value, existing schemas are unaffected and no migration is generated for anyone who does not use it. The joinColumn side could accept the same fields for symmetry, with relation.onDelete taking precedence, but that is not required for this request.

Considered Alternatives

Adding a lot of unnecessary inverse side definitions. This is no fun.

Additional Context

No response

Relevant Database Driver(s)

  • aurora-mysql
  • aurora-postgres
  • better-sqlite3
  • capacitor
  • cockroachdb
  • cordova
  • expo
  • mongodb
  • mysql
  • nativescript
  • oracle
  • postgres
  • react-native
  • sap
  • spanner
  • sqlite
  • sqljs
  • sqlserver

Are you willing to resolve this issue by submitting a Pull Request?

No, I don’t have the time and I’m okay to wait for the community / maintainers to resolve this issue.