I'm not sure what I've done wrong, but the relationship just on UserProfile keeps throwing this error:
ERROR: Error: UserProfile.relationMappings.user: modelClass is not defined
As far as I can tell I've mimicked the example from the docs, and TypeScript example, but can't see why this isn't working? Initially, I thought it was a typings error using relatedQuery, but after trying to insert the relationship column directly I had a different error so I'm assuming they are related. I posted this on stackoverflow, but the objection.js tag only has 13 watchers -14 now :)
I included the query, both models, and the user profile migration. The contents of the BaseModel are commented out so it is equivalent to inheriting from Model directly, and the jsonSchema is only for validation so I removed them for brevity.
UPDATE
Removing the relationshipMappings from UserProfile stops the error from occurring, but since I need the BelongsToOneRelation relationship I'm still trying. At least it seems to be narrowed down to the relationMappings on the UserProfile.
Query
const user = await User.query() // <--- Inserts the user
.insert({ username, password });
// const profile = await user.$relatedQuery('profile') // <--- Initial attempt, but had typings issue... might be related to next insert attempt that throws the error
// .insertAndFetch({ first_name, last_name });
const profile = await UserProfile.query() // <--- Throws error
.insert({ user_id: 1, first_name, last_name });
Models
import { Model, RelationMappings } from 'objection';
import { BaseModel } from './base.model';
import { UserProfile } from './user-profile.model';
export class User extends BaseModel {
readonly id: number;
username: string;
password: string;
role: string;
static tableName = 'users';
static jsonSchema = { ... };
static relationMappings: RelationMappings = {
profile: {
relation: Model.HasOneRelation,
modelClass: UserProfile,
join: {
from: 'users.id',
to: 'user_profiles.user_id'
}
}
};
}
import { Model, RelationMappings } from 'objection';
import { BaseModel } from './base.model';
import { User } from './user.model';
export class UserProfile extends BaseModel {
readonly id: number;
user_id: number;
first_name: string;
last_name: string;
static tableName = 'user_profiles';
static jsonSchema = { ... };
static relationMappings: RelationMappings = {
user: {
relation: Model.BelongsToOneRelation,
modelClass: User,
join: {
from: 'user_profiles.user_id',
to: 'users.id'
}
}
};
}
Migration
exports.up = function (knex, Promise) {
return knex.schema
.createTable('user_profiles', (table) => {
table.increments('id').primary();
table.integer('user_id')
.unsigned()
.notNullable();
table.foreign('user_id')
.references('users.id');
table.string('first_name');
table.string('last_name');
table.timestamps(true, true);
});
};
@mtpultz Maybe a circular reference issue since user.model and user-profile.model import one another at the top? There are several ways to solve circular references like this, but Objection already has (at least) one as you can see in the docs here.
Try this:
relationMappings.<RELATION>.modelClass entry values from pointing to the class constructors to a string pointing to the module that exports the related class.E.g:
// user.model
// This is commented out now:
// import { UserProfile } from './user-profile.model';
export class User extends BaseModel {
...
static relationMappings: RelationMappings = {
profile: {
relation: Model.HasOneRelation,
// This now points to the module that exports the related model constructor
modelClass: __dirname + '/user-profile.model',
join: {
from: 'users.id',
to: 'user_profiles.user_id'
}
}
};
...
}
Thanks @newhouse, as far as I know, modules in TypeScript can't have circular dependencies but tried it anyway. Making the change to both models it now doesn't do the user insert anymore and throws this error. The file location /var/www/app/dist/src/models/user-profile is correct within the docker container and I've verified the file exists and is up to date.
ERROR: Error: User.relationMappings.profile: modelClass: /var/www/app/dist/src/models/user-profile is an invalid file path to a model class
server | [Typescript] at HasOneRelation.createError (/var/www/app/node_modules/objection/lib/relations/Relation.js:363:14)
server | [Typescript] at HasOneRelation.setMapping (/var/www/app/node_modules/objection/lib/relations/Relation.js:54:18)
server | [Typescript] at Object.keys.reduce (/var/www/app/node_modules/objection/lib/model/Model.js:423:33)
server | [Typescript] at Array.reduce (<anonymous>)
server | [Typescript] at Function.getRelations (/var/www/app/node_modules/objection/lib/model/Model.js:419:61)
server | [Typescript] at getRelationArray (/var/www/app/node_modules/objection/lib/model/Model.js:651:32)
server | [Typescript] at Function.getRelationArray (/var/www/app/node_modules/objection/lib/model/Model.js:438:60)
server | [Typescript] at parseRelationsIntoModelInstances (/var/www/app/node_modules/objection/lib/model/modelParseRelations.js:5:32)
server | [Typescript] at User.$setJson (/var/www/app/node_modules/objection/lib/model/modelSet.js:20:5)
server | [Typescript] at User.$setJson (/var/www/app/node_modules/objection/lib/model/Model.js:168:21)
server | [Typescript] at Function.fromJson (/var/www/app/node_modules/objection/lib/model/Model.js:287:11)
server | [Typescript] at doSplit (/var/www/app/node_modules/objection/lib/model/modelFactory.js:143:22)
server | [Typescript] at obj.map.obj (/var/www/app/node_modules/objection/lib/model/modelFactory.js:76:28)
server | [Typescript] at Array.map (<anonymous>)
server | [Typescript] at fromJsonShallow (/var/www/app/node_modules/objection/lib/model/modelFactory.js:76:17)
server | [Typescript] at fromJson (/var/www/app/node_modules/objection/lib/model/modelFactory.js:14:12)
server | [Typescript] From previous event:
server | [Typescript] at QueryBuilder.execute (/var/www/app/node_modules/objection/lib/queryBuilder/QueryBuilder.js:497:20)
server | [Typescript] at QueryBuilder.then (/var/www/app/node_modules/objection/lib/queryBuilder/QueryBuilder.js:393:26)
If I keep the imports, but just remove the relationMappings from the UserProfile it works as you'd expect, but without the BelongsToOneRelation. This seems to indicate it is related to the relationMappings of UserProfile.
@mtpultz Admittedly, there a few parts of your syntax that I am not fluent with (e.g. import and export instead of require and module.exports) and others which I've never seen before (must be TypeScript) but this new error sort of suggests the same thing: there is either trouble finding the file (I think it's being found), or it's not structured and/or exporting properly. Maybe the models need to be exported differently from their respective files?
export class User ... => export default class User...? (notice the default?)
export class User ... => class User ...; module.exports = User; I use this syntax in my code base.
I'm just sort of stabbing in the dark here for ya!
BTW, you may also run into issues when trying to insert an instance of UserProfile off of an instance of User that has not had its id column fetched from the DB once you finally get past this issue:
const user = User.insert({...}); // May want to do 'insertAndFetch' here, or if you are on Postgres you can use 'returning' chain.
user.relatedQuery('profile'). insertAndFetch({...}); // No ID column on 'user' yet to use in the related insert.
Thanks @newhouse I'll definitely change my code to use insertAndFetch. I was originally using $relatedQuery to insert the UserProfile, but I ran into what appears to be a TypeScript typings issue that forced me to change it to use async/await statements instead.
const user = await User.query()
.insertAndFetch({ username, password });
const profile = await user.$relatedQuery('profile')
.insertAndFetch({ first_name, last_name }); <--- TypeScript error thrown
Throws this error Argument of type '{ first_name: any; last_name: any; }' is not assignable to parameter of type 'Partial<Model>[]'
@mtpultz If you give file paths in modelClass they must point to the transpiled __ES5__ files so that node.js can import them using require.
There is no (sane) way for typescript compiler to solve all circular dependencies. Try this for example:
// a.ts
import { B } from './b';
console.log('a.ts sees B as', B);
export class A {}
// b.ts
import { A } from './a';
console.log('b.ts sees A as', A);
export class B {}
// main.ts
import { A } from './a';
Running main.ts will print
b.ts sees A as undefined
a.ts sees B as [Function: B]
At least babel can solve this (I don't know about tsc):
// Notice that `relationMappings` is a function!
// I'm not sure if objection has typings for this though.
static relationMappings = () => ({
profile: {
relation: Model.HasOneRelation,
modelClass: UserProfile,
join: {
from: 'users.id',
to: 'user_profiles.user_id'
}
}
});
Babel can solve that because it imports the whole export object and then uses obj.UserProfile instead of referring directly to UserProfile and since relationMappings is a function, obj.UserProfile is not evaluated when the file is imported, but only when objection needs its value.
If relationMappings lacks the function typing, you can try using a getter for relationMappings.
Oh, you were already using the build folder through __dirname. If the path /var/www/app/dist/src/models/user-profile really points to a valid model class, I don't know what's wrong here. Could you post the transpiled model files here? I'm not really familiar with what tsc produces. Using export default class is one thing to try, but objection should be able to import non-default models also.
Throws this error Argument of type '{ first_name: any; last_name: any; }' is not assignable to parameter of type 'Partial<Model>[]'
This error is caused by the fact that there is no way to have correct typings for $relatedQuery. Typescript cannot know that $relatedQuery('profile') should return QueryBuilder<UserProfile>. To know that it would need to magically parse relationMappings. You can use $relatedQuery<UserProfile>('profile') if I remember correctly.
Hi @koskimas, thanks. Adding generics to the related query ($relatedQuery<UserProfile>('profile')) stopped the typings error so I don't have to manually add the foreign key to the UserProfile. Also thanks for that example on circular dependencies that puts circular dependencies on my radar since I thought they weren't an issue in TypeScript. Wish the error produced for circular dependencies was a bit more descriptive, but it's at least on my mental checklist going forward.
@newhouse thanks as well I figured out why the file wasn't found using the path for modelClass. I got burned by a recent refactor that renamed the files to have a suffix indicating their use so the filename was missing the .model suffix :(
@koskimas on a similar note is this what you would do to solve the same kind of issue on insertWithRelatedAndFetch by adding an extra interface with both the User and UserProfile model keys and set the argument to insertWithRelatedAndFetch. The extra interface seems like bloat, but maybe this is just TypeScript. Either way trying to apply the same kind of solution you showed with $relatedQuery.
Error
Argument of type '{ username: string; password: string; profile: { first_name: string; last_name: string; }; }' is not assignable to parameter of type 'Partial
[]'. Object literal may only specify known properties, and 'username' does not exist in type 'Partial []'.
Solution
interface UserWithProfile { // Extra interface combing User and UserProfile models
username: string;
password: string;
profile: {
first_name: string;
last_name: string;
};
}
const user = await User.query()
.insertWithRelatedAndFetch({
username, // <--- Error was being thrown until I added UserWithProfile interface and applied it below
password,
profile: {
first_name,
last_name
}
} as UserWithProfile); // <--- Solved the error and update occurs
@mtpultz
I ran into the same issue as you with insertWithRelatedAndFetch and found I could avoid the extra interface by adding the relation as a partial to my model attributes.
Example would look something like this:
class Profile extends Model {
public name: string;
}
class User extends Model {
public username: string;
// add relation to the user class as a partial
public profile: Partial<Profile>;
static relationMappings = {
profile: {
relation: Model.BelongsToOneRelation,
modelClass: __dirname + '/user-profile',
join: {
from: 'user_profiles.user_id',
to: 'users.id'
}
}
}
const user = await User.query().insertWithRelatedAndFetch({
username: 'some-username',
profile: {
name: 'some-name'
}
});
Most helpful comment
@mtpultz If you give file paths in
modelClassthey must point to the transpiled __ES5__ files so that node.js can import them usingrequire.There is no (sane) way for typescript compiler to solve all circular dependencies. Try this for example:
Running
main.tswill printAt least babel can solve this (I don't know about tsc):
Babel can solve that because it imports the whole export object and then uses
obj.UserProfileinstead of referring directly toUserProfileand sincerelationMappingsis a function,obj.UserProfileis not evaluated when the file is imported, but only when objection needs its value.If
relationMappingslacks the function typing, you can try using a getter forrelationMappings.