'How to automatically update virtual model property when updating its foreign key reference?

I have a order table that refers to status table by foreign key status_id:

$ select order_id, status_id from "order";

 order_id | status_id 
----------+-----------
(0 rows)

The Order model in Sequelize also includes (virtual?) .status property for convenience:

I'm using NestJS and sequlize-typescript.

class Order extends Model {
  @Column({
    field: 'order_id',
    primaryKey: true,
  })
  readonly id!: string;

  readonly statusID!: string;

  @BelongsTo(() => Status, {
    foreignKey: {
      name: 'statusID',
      field: 'status_id',
    },
  })
  readonly status!: Status;
}
class Status extends Model { … }

To progress a given order to another status, I want to use .update() method of a model. To do that, I have to set both status and statusID:

const order = await Order.finByPk(…);
const newStatus = await Status.findOne(…);

await order.update({
  status: newStatus,
  statusID: newStatus.id,
});

This is logic duplication, it is error-prone, I don't want to do that. But:

  • if I set only status, the statusID is not updated on the order instance, and the foreign key reference is not updated in the table, rendering the whole change as moot;
  • if I set only statusID, the table is properly updated, but order.status still points to the old status, requiring another read from the table (Order.findByPk(…));

In both cases, the order instance becomes inconsistent: order.statusID and order.status.id have different values.

Is it possible to set only statusID in .update(…), and force Sequelize to automatically update order.status?



Solution 1:[1]

(Partial answer)

An alternative to using Model.findByPk(…) (which both performs a read and creates a new instance of Model) would be using model.reload(), which preserves model instance while making the read.

So, instead of:

await order.update({ statusID: newStatus.id });

const orderUpdated = await Order.findByPk(order.id);

return orderUpdated;

… it is:

await order.update({ statusID: newStatus.id });
await order.reload({ include: ['status'] });

return order;

This is more concise and definitely better, however, not the best, since the issue of requiring another read from the table still persists.

Solution 2:[2]

To fix logic duplication, one can define a model instance method and encapsulate the logic there:

class Order extends Model {
  readonly statusID!: string;
  readonly status!: Status;

  async setStatus(newValue: Status): Promise<void> {
    await this.update({
      statusID: newValue.id,
      status: newValue,
    });
  }
}

… which can then be generalized in order to be useful for other models:

import type { Model } from 'sequelize-typescript';
import type { Attributes } from 'sequelize/types';

const createForeignModelSetter = <
  OriginalModel extends Model = Model,
  ForeignModel extends Model = Model,
>(
  foreignKey: keyof Attributes<OriginalModel>,
  foreignModelKey: keyof Attributes<OriginalModel>,
  foreignModelPrimaryKey: keyof Attributes<ForeignModel>,
) =>
  async function (this: OriginalModel, newValue: ForeignModel): Promise<void> {
    await this.update({
      [foreignKey]: newValue[foreignModelPrimaryKey],
      [foreignModelKey]: newValue,
    });
  };
class Status extends Model<{ id: string }> { … }
class Order extends Model<{ statusID: string, status: Status }> {
  readonly setStatus = createForeignModelSetter<Order, Status>(
    'statusID',
    'status',
    'id',
  );
}

This significantly improves the logic duplication situation, but not fixes completely (hiding is not fixing).

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Dima Parzhitsky
Solution 2