objection.js: upsertGraph with m2m's not triggering $afterInsert/Delete

Using version 1.6.6

I have a many to many relationship between Assignment and Position through AssignmentPosition. I’m calling upsertGraph with the relate and unrelate options set to true. It correctly associates the assignment to the position and prying on knex, it does insert and delete the assignment_position rows, but does not trigger the AssignmentPosition $afterInsert when it’s related nor $afterDelete when unrelated.

The upsertGraph call looks something like this:

models.Assignment.query().upsertGraph({
  id: 1,
  positions: [{ id: 2 }]
}, { relate: ['positions'], unrelate: ['positions'] });

The models:

class Assignment extends Model {
  static tableName = 'assignment';
  static get relationMappings() {
    return {
      positions: {
        relation: Model.ManyToManyRelation,
        modelClass: models.Position,
        join: {
          from: 'assignment.id',
          through: {
            from: 'assignment_position.assignment_id',
            to: 'assignment_position.position_id'
          },
          to: 'position.id'
        }
      }
  }
}

class Position extends Model {
  static tableName = 'position';
}

class AssignmentPosition extends Model {
  async $afterInsert(context) {
    await super.$afterInsert(context);
    // Doesn't reach here...
  }
  async $afterDelete(context) {
    await super.$afterDelete(context);
    // Doesn't reach here...
  }
}

Calling an insert directly on the AssignmentPosition does trigger the hook as well as the upsert graph directly to assignment.assignmentPositions.

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 2
  • Comments: 16

Most upvoted comments

Objection 2.0 will have a static afterDelete and beforeDelete hooks that work in all situations. This will be a lot easier to implement then.

You need to specify the model class in through.modelClass property of the relationmapping

For future travelers, this is my work around to avoid making another call to figure out which rows are being deleted:

class AssignmentPositionQueryBuilder extends Model.QueryBuilder {
  // Override
  delete() {
    return super.select('id').runAfter(async results  => {
      // Perform "afterDelete" logic here...
      await AssignmentPosition.query()
        .whereIn('id', results.map(r => r.id))
        .hardDelete();
      return results;
    });
  }

  hardDelete() {
    return super.delete();
  }
}

export default class AssignmentPosition extends BaseModel {
  static tableName = 'assignment_position';

  static QueryBuilder = AssignmentPositionQueryBuilder;
}

@adefrutoscasado @jarommadsen

Just to confirm I got everything right in this thread. I use a custom delete() method that overrides my BaseModel while reimplementing my nativeDelete with a runAfter middleware.

At the same time I use the queryBuilder.context().transaction from my runAfter builder to run everything in one transaction.


class MyTable extends Base {
  static tableName = 'my_table';
  static relationMappings = { ...  };
}

class MyTableQueryBuilder extends Base.QueryBuilder {
 delete() {
    return super.select('*').runAfter(async (results, QueryBuilder) => {
      const { transaction: trx } = QueryBuilder.context();
      // Perform "afterDelete" logic here...

      await OrderVouchers.query(trx)
        .whereIn('id', results.map(prop('id')))
        .nativeDelete();

      return results;
    });
  }

  nativeDelete() {
    return super.delete();
  }
}

OrderVouchers.QueryBuilder = OrderVoucherQueryBuilder;

export default OrderVouchers;

Objection 2.0 will have a static afterDelete and beforeDelete hooks that work in all situations. This will be a lot easier to implement then.

It does not work.


  // asFindQuery replies with a wrong object
  // everything else is either empty or undefined

import path from 'path';
import { Base } from '.';

class OrderVouchers extends Base {
  static tableName = 'order_vouchers';

  static async beforeDelete({ asFindQuery, inputItems, items, relation }) {
    console.log('afterDelete', relation, items, inputItems, await asFindQuery().select('id'));
  }

  static async beforeInsert({ asFindQuery, items, inputItems, relation }) {
    console.log('afterInsert', relation, items, inputItems, await asFindQuery().select('id'));
  }

  static relationMappings = {
    orders: {
      relation: Base.BelongsToOneRelation,
      modelClass: path.resolve(__dirname, 'Orders'),
      join: {
        from: 'order_vouchers.order_id',
        to: 'orders.id',
      },
    },
    vouchers: {
      relation: Base.BelongsToOneRelation,
      modelClass: path.resolve(__dirname, 'Vouchers'),
      join: {
        from: 'order_vouchers.voucher_id',
        to: 'vouchers.id',
      },
    },
  };
}

That did solve the $afterInsert problem but the $afterDelete still does not appear to trigger.