Objection.js: Typescript: It seems there is no way to provide "extra" fields for mTm relation to upsertGraph

Created on 11 Apr 2019  路  8Comments  路  Source: Vincit/objection.js

I have the following relation:

interface IWorkflow extends IBaseEntity {
  forms?: Form[]
}

export class Workflow extends BaseEntity implements IWorkflow {
  static get tableName() {
    return 'workflows';
  }

  forms?: Form[];

  static relationMappings: RelationMappings = {
    forms: {
      relation: Model.ManyToManyRelation,
      modelClass: Form,
      join: {
        from: 'workflows.id',
        through: {
          from: 'workflowsForms.workflowId',
          to: 'workflowsForms.formId',
          extra: ['order']
        },
        to: 'forms.id',
      }
    }
  }
}

I'm trying to link form to workflow using updateGraph

  public async addFormToWorkflow(
    workflowId: number,
    formId: number,
    order: number,
  ): Promise<Workflow> {
    try {
      const workflow = await Workflow
        .query()
        .findById(workflowId)
        .eager('forms');
      const form = await Form.query().findById(formId);

      form.order = order; // This line will cause TS error, because order is not part of form model
      workflow.forms.push(form);

      return Workflow.query().upsertGraph(workflow, {
        relate: true
      });
    } catch (e) {
      throw e;
    }
  }

So, the problem is in line form.order = order;. I've checked it with //@ts-ignore and it works perfectly. I can create a workaround if I define model for many-to-many relation table, and add record there directly, but from my perspective using just upsertGraph is much more elegant. Could you please suggest if there is some way to pass extra fields nicely? Thank you

Most helpful comment

You don't need to use upsertGraph here though. This would be much simpler:

  public async addFormToWorkflow(
    workflowId: number,
    formId: number,
    order: number,
  ): Promise<Workflow> {
    try {
      const workflow = await Workflow
        .query()
        .findById(workflowId);

       await workflow.$relatedQuery('forms').relate({
         id: formId,
         order
       })
    } catch (e) {
      throw e;
    }
  }

That's two queries instead of something like 8 with eager and upsertGraph. You can even do that using one single query like this:

  public async addFormToWorkflow(
    workflowId: number,
    formId: number,
    order: number,
  ): Promise<Workflow> {
    try {
       await Workflow
        .fromJson({ id: workflowId })
        .$relatedQuery('forms')
        .relate({
           id: formId,
           order
         })
    } catch (e) {
      throw e;
    }
  }

All 8 comments

Just add that field for your model.

I'm not sure that it's correct to add field to the model that's actually is not related to the model itself, but it seems there is no better way to do it. Thank you

You don't need to use upsertGraph here though. This would be much simpler:

  public async addFormToWorkflow(
    workflowId: number,
    formId: number,
    order: number,
  ): Promise<Workflow> {
    try {
      const workflow = await Workflow
        .query()
        .findById(workflowId);

       await workflow.$relatedQuery('forms').relate({
         id: formId,
         order
       })
    } catch (e) {
      throw e;
    }
  }

That's two queries instead of something like 8 with eager and upsertGraph. You can even do that using one single query like this:

  public async addFormToWorkflow(
    workflowId: number,
    formId: number,
    order: number,
  ): Promise<Workflow> {
    try {
       await Workflow
        .fromJson({ id: workflowId })
        .$relatedQuery('forms')
        .relate({
           id: formId,
           order
         })
    } catch (e) {
      throw e;
    }
  }

I'm not sure that it's correct to add field to the model that's actually is not related to the model itself, but it seems there is no better way to do it

But if you use an extra, the property IS part of that model. It's not part of the model's table, but the model will always be returned with the extra field when you query it through that relation (not other relations though). You anyway need to access the field, so there's no way around adding it to the model.

@koskimas That's awesome, thanks a lot! I will try this out, currently have some TS issues with sending object in the relate, but anyway it's a great optimization. And thank you for explanation regarding table vs model

Just added typing for the object, and it worked perfectly. Thanks again!

const formRelation: Partial<Form> = {
   id: formId,
   order
};

return workflow.$relatedQuery('forms').relate(formRelation);

I'm not sure that it's correct to add field to the model that's actually is not related to the model itself, but it seems there is no better way to do it

But if you use an extra, the property IS part of that model. It's not part of the model's table, but the model will always be returned with the extra field when you query it through that relation (not other relations though). You anyway need to access the field, so there's no way around adding it to the model.

What can we do about the possibility of the model to which the extra is appended already having a column by that name? If both the model table and the join table have a created_at column, for instance, what should we do to disambiguate them?

Is an alias here the only option? Is there a way of doing this with the upsertGraph operation?

I'm also having issued with shared column names like created_at. Did you find a solution for this @GreatSchool2 ?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

apronin83 picture apronin83  路  3Comments

mycahjay-nms picture mycahjay-nms  路  4Comments

njleonzhang picture njleonzhang  路  4Comments

zacharynevin picture zacharynevin  路  4Comments

Ahlid picture Ahlid  路  3Comments