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:
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.
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
Most helpful comment
The fix has now been released in version 2.2.1