Objection.js: skipFetched not working for nested relation

Created on 20 May 2020  路  6Comments  路  Source: Vincit/objection.js

Hi there,

Thanks for the awesome library. We recently upgraded to v2.1.3 and start using the skipFetched option to replace patterns like if (!person.children) person.$fetchGraph('children'). It works great in most cases but I found an edge case not working.

For codes like below, assuming:

  • animal => person is a many-to-one relationship
  • person => country is many-to-one, too
const animal = await Animal.findById(1);

// This line will fetch as expected
await animal.$fetchGraph('owner');

// This line will throw an error like "unknown relation "country" in an eager expression"
await animal.$fetchGraph('owner.country', { skipFetched: true });

// If I change the line above to `await animal.$fetchGraph('owner.country');`, it works

Please confirm if this is an issue of skipFetched. Thanks.

bug

Most helpful comment

The fix has now been released in version 2.2.1

All 6 comments

I just looked into this and I'm not able to reproduce this. A similar test case works just fine. Could you provide a reproducible example? You can use the reproduction_template.js file as a starting point if you want.

Thanks. I attached a modified reproduction_template.js to show the issue. The only modified part is in the main() function. The output I'm getting:

objection-reproduce node reproduction-template
Works without skipFetched
ValidationError: unknown relation "parent" in an eager expression
    at Function.createValidationError (/Users/wwei/repos/objection-reproduce/node_modules/objection/lib/model/Model.js:357:12)
    at findRelationsToFetch (/Users/wwei/repos/objection-reproduce/node_modules/objection/lib/queryBuilder/operations/eager/WhereInEagerOperation.js:224:24)
    at WhereInEagerOperation.shouldSkipFetched (/Users/wwei/repos/objection-reproduce/node_modules/objection/lib/queryBuilder/operations/eager/WhereInEagerOperation.js:160:30)
    at WhereInEagerOperation.fetchRelationBatch (/Users/wwei/repos/objection-reproduce/node_modules/objection/lib/queryBuilder/operations/eager/WhereInEagerOperation.js:112:14)
    at /Users/wwei/repos/objection-reproduce/node_modules/objection/lib/queryBuilder/operations/eager/WhereInEagerOperation.js:102:21
    at mapOne (/Users/wwei/repos/objection-reproduce/node_modules/objection/lib/utils/promiseUtils/map.js:23:26)
    at Object.promiseMap [as map] (/Users/wwei/repos/objection-reproduce/node_modules/objection/lib/utils/promiseUtils/map.js:11:14)
    at WhereInEagerOperation.fetchRelation (/Users/wwei/repos/objection-reproduce/node_modules/objection/lib/queryBuilder/operations/eager/WhereInEagerOperation.js:100:39)
    at /Users/wwei/repos/objection-reproduce/node_modules/objection/lib/queryBuilder/operations/eager/WhereInEagerOperation.js:72:18
    at mapOne (/Users/wwei/repos/objection-reproduce/node_modules/objection/lib/utils/promiseUtils/map.js:23:26) {
  name: 'ValidationError',
  type: 'RelationExpression',
  data: {},
  statusCode: 400
}
/**
 * This is a simple template for bug reproductions. It contains three models `Person`, `Animal` and `Movie`.
 * They create a simple IMDB-style database. Try to add minimal modifications to this file to reproduce
 * your bug.
 *
 * install:
 *    npm install objection knex sqlite3 chai
 *
 * run:
 *    node reproduction-template
 */

let Model;

try {
  Model = require('./').Model;
} catch (err) {
  Model = require('objection').Model;
}

const Knex = require('knex');
const chai = require('chai');

async function main() {
  await createSchema();

  ///////////////////////////////////////////////////////////////
  // Your reproduction
  ///////////////////////////////////////////////////////////////

  await Person.query().insertGraph({
    firstName: 'JL',
    lastName: 'Mom',
    children: [
      {
        firstName: 'Jennifer',
        lastName: 'Lawrence',

        pets: [
          {
            name: 'Doggo',
            species: 'dog'
          }
        ]
      }
    ]
  });

  let doggo = await Animal.query()
    .findOne({ name: 'Doggo' });

  await doggo.$fetchGraph('owner');
  await doggo.$fetchGraph('owner.parent');

  chai.expect(doggo.owner.parent.firstName).to.equal('JL');
  console.log('Works without skipFetched')

  // Reload doggo
  doggo = await Animal.query()
    .findOne({ name: 'Doggo' });

  await doggo.$fetchGraph('owner', { skipFetched: true });
  await doggo.$fetchGraph('owner.parent', { skipFetched: true });

  chai.expect(doggo.owner.parent.firstName).to.equal('JL');
}

///////////////////////////////////////////////////////////////
// Database
///////////////////////////////////////////////////////////////

const knex = Knex({
  client: 'sqlite3',
  useNullAsDefault: true,
  debug: false,
  connection: {
    filename: ':memory:'
  }
});

Model.knex(knex);

///////////////////////////////////////////////////////////////
// Models
///////////////////////////////////////////////////////////////

class Person extends Model {
  static get tableName() {
    return 'Person';
  }

  static get jsonSchema() {
    return {
      type: 'object',
      required: ['firstName', 'lastName'],

      properties: {
        id: { type: 'integer' },
        parentId: { type: ['integer', 'null'] },
        firstName: { type: 'string', minLength: 1, maxLength: 255 },
        lastName: { type: 'string', minLength: 1, maxLength: 255 },
        age: { type: 'number' },

        address: {
          type: 'object',
          properties: {
            street: { type: 'string' },
            city: { type: 'string' },
            zipCode: { type: 'string' }
          }
        }
      }
    };
  }

  static get relationMappings() {
    return {
      pets: {
        relation: Model.HasManyRelation,
        modelClass: Animal,
        join: {
          from: 'Person.id',
          to: 'Animal.ownerId'
        }
      },

      movies: {
        relation: Model.ManyToManyRelation,
        modelClass: Movie,
        join: {
          from: 'Person.id',
          through: {
            from: 'Person_Movie.personId',
            to: 'Person_Movie.movieId'
          },
          to: 'Movie.id'
        }
      },

      children: {
        relation: Model.HasManyRelation,
        modelClass: Person,
        join: {
          from: 'Person.id',
          to: 'Person.parentId'
        }
      },

      parent: {
        relation: Model.BelongsToOneRelation,
        modelClass: Person,
        join: {
          from: 'Person.parentId',
          to: 'Person.id'
        }
      }
    };
  }
}

class Animal extends Model {
  static get tableName() {
    return 'Animal';
  }

  static get jsonSchema() {
    return {
      type: 'object',
      required: ['name'],

      properties: {
        id: { type: 'integer' },
        ownerId: { type: ['integer', 'null'] },
        name: { type: 'string', minLength: 1, maxLength: 255 },
        species: { type: 'string', minLength: 1, maxLength: 255 }
      }
    };
  }

  static get relationMappings() {
    return {
      owner: {
        relation: Model.BelongsToOneRelation,
        modelClass: Person,
        join: {
          from: 'Animal.ownerId',
          to: 'Person.id'
        }
      }
    };
  }
}

class Movie extends Model {
  static get tableName() {
    return 'Movie';
  }

  static get jsonSchema() {
    return {
      type: 'object',
      required: ['name'],

      properties: {
        id: { type: 'integer' },
        name: { type: 'string', minLength: 1, maxLength: 255 }
      }
    };
  }

  static get relationMappings() {
    return {
      actors: {
        relation: Model.ManyToManyRelation,
        modelClass: Person,
        join: {
          from: 'Movie.id',
          through: {
            from: 'Person_Movie.movieId',
            to: 'Person_Movie.personId'
          },
          to: 'Person.id'
        }
      }
    };
  }
}

///////////////////////////////////////////////////////////////
// Schema
///////////////////////////////////////////////////////////////

async function createSchema() {
  await knex.schema
    .dropTableIfExists('Person_Movie')
    .dropTableIfExists('Animal')
    .dropTableIfExists('Movie')
    .dropTableIfExists('Person');

  await knex.schema
    .createTable('Person', table => {
      table.increments('id').primary();
      table
        .integer('parentId')
        .unsigned()
        .references('id')
        .inTable('Person');
      table.string('firstName');
      table.string('lastName');
      table.integer('age');
      table.json('address');
    })
    .createTable('Movie', table => {
      table.increments('id').primary();
      table.string('name');
    })
    .createTable('Animal', table => {
      table.increments('id').primary();
      table
        .integer('ownerId')
        .unsigned()
        .references('id')
        .inTable('Person');
      table.string('name');
      table.string('species');
    })
    .createTable('Person_Movie', table => {
      table.increments('id').primary();
      table
        .integer('personId')
        .unsigned()
        .references('id')
        .inTable('Person')
        .onDelete('CASCADE');
      table
        .integer('movieId')
        .unsigned()
        .references('id')
        .inTable('Movie')
        .onDelete('CASCADE');
    });
}

main()
  .then(() => {
    console.log('success')
    return knex.destroy()
  })
  .catch(err => {
    console.error(err)
    return knex.destroy()
  });

Seconded that Objection is a wonderful library!

I've encountered this in my project as well when attempting to migrate if (!model.relationType) await model.$fetchGraph('relationType') style code.

Was @broom9 's example above sufficient for purposes of finding and fixing the issue?

Experiencing the same issue here, only occurs for nested relationships.

@broom9 Thank you for the reproduction. I'm going to take a look at this now. Sorry it took so long :grimacing:

The fix has now been released in version 2.2.1

Was this page helpful?
0 / 5 - 0 ratings

Related issues

AhmadRaza786 picture AhmadRaza786  路  3Comments

sgangwisch picture sgangwisch  路  4Comments

officer-rosmarino picture officer-rosmarino  路  4Comments

mycahjay-nms picture mycahjay-nms  路  4Comments

chen7david picture chen7david  路  3Comments