Migration from objection 3.x to 4.0
This section covers the upgrade from objection.js 3.1.5 to @abedmurrar/objection 4.0. The TypeScript page has more examples of the new types.
Runtime changes
These change what existing code does at runtime. Each one is explained, with examples, in Breaking changes:
- Node.js 24, knex 3.3 and, on MySQL, MySQL 8.0.17 or newer are required. With knex 3.3,
page()andrange()on a delete query throw on postgres and SQLite. - Only the package entry can be imported. Imports of files inside the package throw
ERR_PACKAGE_PATH_NOT_EXPORTED. - The exported classes are real classes. ES5-style subclasses (
Model.call(this)) and code compiled with TypeScript'starget: "es5"or Babel 6 throw. - The keys of JSON field expressions are escaped. Keys with
,,{,},", whitespace orNULLrefer to the actual key instead of being split. orWhereno longer escapes the conditions objection adds, like the owner condition of$relatedQuery()and relation filters.for()is checked. A query that uses it without the staticrelatedQuery(), or calls a write method before it, is rejected.- JSON features throw on unsupported databases. JSON field expressions and the
whereJson*methods only work on postgres and MySQL. - Patching a field of a
NULLJSON column creates the object.patch({ 'col:key': value })on a row wherecolisNULLsetscolto{ "key": value }. Before,colstayedNULLon postgres. - Graph operations run one query at a time on MySQL. Graph writes like
insertGraph()andupsertGraph()run one query at a time by default. Setconcurrencyon the model to run them in parallel again.
typescript: Custom query builders declare '~rebind' instead of five *QueryBuilderType properties
Custom query builders no longer redeclare ArrayQueryBuilderType, SingleQueryBuilderType, MaybeSingleQueryBuilderType, NumberQueryBuilderType and PageQueryBuilderType. Declare '~rebind' instead. The custom query builder type is now kept by every method, including throwIfNotFound(), castTo(), runAfter() and castTo(ModelClass) which used to return the base QueryBuilder.
// 3.x
class MyQueryBuilder<M extends Model, R = M[]> extends QueryBuilder<M, R> {
ArrayQueryBuilderType!: MyQueryBuilder<M, M[]>;
SingleQueryBuilderType!: MyQueryBuilder<M, M>;
MaybeSingleQueryBuilderType!: MyQueryBuilder<M, M | undefined>;
NumberQueryBuilderType!: MyQueryBuilder<M, number>;
PageQueryBuilderType!: MyQueryBuilder<M, Page<M>>;
}
class BaseModel extends Model {
QueryBuilderType!: MyQueryBuilder<this>;
static QueryBuilder = MyQueryBuilder;
}
// 4.0
class MyQueryBuilder<
M extends Model,
R = M[],
S extends QueryBuilderState = DefaultQueryBuilderState<M, R>,
> extends QueryBuilder<M, R, S> {
declare '~rebind': MyQueryBuilder<this['~M'], this['~R'], this['~S']>;
}
class BaseModel extends Model {
static QueryBuilder = MyQueryBuilder;
declare QueryBuilderType: MyQueryBuilder<this>;
}In custom methods, the model type isn't known, so pass the type of the object to patch(), update() and insert(): this.patch<{ deletedAt: Date }>({ deletedAt: new Date() }).
The type of the static query methods like query() now comes from the static QueryBuilder property, because that decides the query builder class at runtime. Before, a model that declared QueryBuilderType but didn't set static QueryBuilder type checked custom methods that didn't exist at runtime.
typescript: runAfter() changes the result type
The return value of the runAfter() callback replaces the result of the query, and the result type now reflects that. A callback that returns nothing makes the result undefined, which is what happens at runtime.
// string[]
const names = await Person.query().runAfter((people) =>
people.map((it) => it.firstName),
);typescript: castTo<T>() sets the type of the result
castTo<T>() sets the result type to T, and the methods that change the result kind are based on it. For example castTo<Row[]>().first() is Row | undefined. Before, castTo<Row>() after first() silently dropped undefined.
typescript: Relations are inferred from relationMappings
Relation properties no longer need to be declared in models, see the relations guide. To use it, remove the type annotation of relationMappings (: RelationMappings or : RelationMappingsThunk) and the relation property declarations, or wrap the mappings in defineRelations(). Declared relation properties keep working as before.
Relation names passed to relatedQuery() and $relatedQuery() are now checked. If a relation can't be inferred or declared, pass the related model type explicitly: relatedQuery<Pet>('pets').
$relatedQuery() for a to-one relation now resolves to Model | undefined, since that's what it returns when there is no related model. Add throwIfNotFound() if you expect one.
typescript: Generic functions that pass callbacks to ModelClass<M>.query()
With strictFunctionTypes disabled (as with strict: false), passing a CallbackVoid<QueryBuilder<M>> to onBuild() of a ModelClass<M>.query() query in generic code no longer compiles. Type the callback with the model's query builder type instead: CallbackVoid<QueryBuilderType<M>>.
typescript: Modifiers
Use AnyQueryBuilder<M> as the parameter type of functions that take query builders of any result kind. Modifier functions passed to modify() are only checked for the model type of their query builder parameter, so (query: QueryBuilder<Person>) => void can be used with findById() and patch() queries too. The rest of the arguments of modify() are checked against the parameters of the modifier.
typescript: Model.ref() only accepts the columns of the model
Person.ref('firstName') must name a property of Person (not a relation or a method), optionally qualified with a table or an alias ('p.firstName'), or a field of a JSON column ('address:street'). Person.ref('*') works too.
- Declare the columns the model is missing:
country?: string | null;. - For a column that belongs to another table, use
ref('alias.column')or the other model'sref(). - Dynamic column names need a column type:
columns: ModelProps<UserProfile>[]instead ofstring[], oras conston a list of allowed columns. - To skip the check, pass the expression as a type argument:
Person.ref<'legacy_column'>('legacy_column'), or<string>in generic code that works with any model class.
Models that declare no columns accept any string.
typescript: Query results only have the relations the query loaded
await Person.query() is ModelRow<Person>[]: the model without its relation properties. Relations loaded with withGraphFetched(), withGraphJoined() or fetchGraph() are added, with the type of the loaded graph. See the relations guide.
// Before: compiled, and `user.organization` was `Organization | undefined`.
const user = await User.query().findById(id).throwIfNotFound();
user.organization?.name;
// After: an error, since the query didn't load `organization`. Load it:
const user = await User.query()
.findById(id)
.withGraphFetched('organization')
.throwIfNotFound();
user.organization?.name; // `organization` is always there, but can be nullRows can still be passed where User is expected. Code that reads relations the types can't see being loaded needs one of these changes:
- A modifier loads the relation. Results after modifiers that don't return the query builder have all relations as optional properties, like before, so this keeps working. Returning the query builder from the modifier (
return query.withGraphFetched('organization')) gives the exact type. - The query is changed in place.
query.withGraphFetched('organization')as a separate statement doesn't change the type ofquery. Chain the call, or declare the variable with an explicit type:const query: QueryBuilder<User> = User.query(). Explicit types keep relations optional. - The relation is set by hand.
user.organization = orgis an error on rows. Useuser.$setRelated('organization', org), or the model type. await row.$fetchGraph('pets')and thenrow.pets. Use the value that$fetchGraph()returns.
Models with private or protected members, and models that declare a relation as required (pets!: Pet[]), keep all relations as optional properties on rows.
typescript: Relation expressions are checked
The relation names in withGraphFetched(), withGraphJoined(), fetchGraph(), $fetchGraph(), modifyGraph(), allowGraph() and joinRelated() (and its variants) must be relations of the model, both in strings ('[pets.toys, mom]') and in object notation. A misspelled name is an error like Unknown relation "toyz" at "pets".
- Relations that work at runtime but aren't in the types need a property declaration:
memberships?: Membership[];, or inferredrelationMappings. - To skip the check for one call, pass the expression type explicitly:
withGraphFetched<string>(expression). Expressions typedstringaren't checked. - After
castTo(Model)relation names aren't checked, since the runtime model is still the original one.
The callback of modifyGraph() now gets the query builder of the related model. Callbacks with a parameter type for another model are errors.
typescript: Named modifiers are checked
When the static modifiers of a model is an object without a type annotation, or a getter, modify('name') and modifier names in relation expressions ('pets(onlyDogs)') must be modifiers of the model, and the arguments of modify() are checked against the modifier. Annotate the property as static modifiers: Modifiers = {...} to opt out. Names added with modifiers() aren't known, so they aren't checked after it.
typescript: Join callbacks get JoinBuilder
Join callbacks get objection's JoinBuilder instead of knex's JoinClause. A callback whose parameter is annotated as Knex.JoinClause no longer compiles. Remove the annotation, or use JoinBuilder:
import { JoinBuilder } from 'objection';
// 3.x
Person.query().join('pets', (join: Knex.JoinClause) => {
join.on('pets.ownerId', 'persons.id');
});
// 4.0
Person.query().join('pets', (join: JoinBuilder) => {
join.on('pets.ownerId', 'persons.id');
});See Joins and model classes for what JoinBuilder adds.
typescript: ModelProps and ModelRelations count more properties as relations
Properties typed Model | null (like mom: Person | null) and unions of a model and an array of models are relations now. ModelProps<M> no longer includes them, and ModelRelations<M> does. ModelProps<M> also leaves out QueryBuilderType and the ~ keys.
Migration from objection 2.x to 3.0
This document guides you through each breaking change in objection 3.0 and attempts to provide clear steps to follow. If you find something missing, please open an issue and we'll fix this guide ASAP.
Here's a list of the breaking changes
- Dropped support for node < 12
- Dropped support for knex < 0.95
- typescript: Methods like
findByIdandfindOnethat return a single item are now typed correctly - typescript:
QueryBuildernow inheritsPromiseLikeinstead ofPromise - Dropped a bunch of deprecated methods and features
Dropped support for node < 12
Objection 3.0 needs at least node 12 to work.
Dropped support for knex < 0.95
Objection needs at least knex 0.95.0 to work.
This is because knex introduced some breaking changes in 0.95.0.
typescript: Methods like findById and findOne that return a single item are now typed correctly
In 2.0 methods like findById, findOne and first that return one item or undefined were incorrectly typed to always return a value. Now the return type of those methods is SomeModel | undefined.
You'll get compilation errors in places where you've trusted the old typings. All you need to do is to add a check for undefined values to narrow down the type or chain the throwIfNotFound method that also narrows down the type back to SomeModel.
Before:
const person = await Person.query().findById(id);
console.log(person.id);After:
const person = await Person.query().findById(id);
if (!person) {
throw new Error('Person not found');
}
console.log(person.id);Or
const person = await Person.query().findById(id).throwIfNotFound();
console.log(person.id);typescript: QueryBuilder now inherits PromiseLike instead of Promise
In 2.0 and before, the QueryBuilder class inherited Promise in the typings which made code like this possible
function findPerson(id: number): Promise<Person> {
return Person.query().findById(id);
}However, QueryBuilder doesn't actually inherit Promise but instead is "thenable". Typescript has the type PromiseLike for thenable objects which objection now correctly uses. There are couple of ways to fix the compilation errors this change causes:
- Chain
executeto the query:
function findPerson(id: number): Promise<Person> {
return Person.query().findById(id).execute();
}- Use
PromiseLike:
function findPerson(id: number): PromiseLike<Person> {
return Person.query().findById(id);
}- Make the functions
async:
async function findPerson(id: number): Promise<Person> {
return Person.query().findById(id);
}Dropped a bunch of deprecated methods and features
All the methods that were marked deprecated (and printed a deprection message during runtime) in 2.0 have now been removed.
Migration from objection 1.x to 2.0
Objection 2.0 brought a lot of great new features, but the main focus was in cleaning the API, which lead to a bunch of breaking changes. This document guides you through each change and attempts to provide clear steps to follow. If you find something missing, please open an issue and we'll fix this guide ASAP.
Here's a list of the breaking changes
- Node 6 and 7 are no longer supported
- modify method signature change
- Bluebird and lodash have been removed
- Database errors now come from db-errors library
- #ref references in insertGraph and upsertGraph now require the allowRefs: true option
- relate method now always returns the number of affected rows
- $relatedQuery no longer mutates
- context now acts like mergeContext
- Model.raw and Model.fn now return objection raw and fn
- QueryBuilder.toString and QueryBuilder.toSql have been removed
- Rewritten typings
In addition to these, a lot of methods were deprecated and replaced by a new method. The old methods still work, but they print a warning (once per process) when you use them. The warning message tells which method you should be using in the future and you can slowly replace the methods as you get annoyed by the warnings.
Most of the methods have simply been renamed, but in some cases the replacing methods work a little differently. Make sure to read the documentation of the new method.
Node 6 and 7 are no longer supported
Objection now needs at least node 8 to work.
modify method signature change
Before, you were able to provide multiple modifiers or modifier names to modify by providing multiple arguments like this:
Person.query().modify('foo', 'bar');Now only the first argument is used to specify modifiers and all the rest are arguments for the modifiers. The first argument can be an array so you can simply wrap the modifiers in an array if there are more than one of them:
Person.query().modify(['foo', 'bar']);Bluebird and lodash have been removed
Before, all async operations returned a bluebird promise. Now the bluebird dependency has been dropped and the native Promise is used instead. This also means that all bluebird-specific methods
mapreducereflectbindspreadasCallbacknodeify
have been removed from the QueryBuilder.
You need to go through your code and make sure you don't use any bluebird methods or trust that objection returns a bluebird promise.
Objection also used to export Promise and lodash properties like this:
import { Promise, lodash } from 'objection';which is not true anymore. Both of those exports have been removed.
Database errors now come from the db-errors library
Before, when a database operation failed, objection simply passed through the native error thrown by the database client. In 2.0 the errors are wrapped using the db-errors library.
The db-errors library errors expose a nativeError property. If you rely on the properties of the old errors, you can simply change code like this
try {
await Person.query().where('foo', 'bar')
} catch (err) {
if (err.code === 13514) {
...
}
}into this:
try {
await Person.query().where('foo', 'bar')
} catch (err) {
err = err.nativeError || err
if (err.code === 13514) {
...
}
}A preferred way to handle this would be to use the new db-error classes as described here, but the fastest migration path is to do the above trick.
#ref references in insertGraph and upsertGraph now require the allowRefs: true option
The usage of '#ref': 'someId' and #ref{someId.someProp} now requires an explicit allowRefs: true option to be passed to the called method:
await Person.query().insertGraph(graphWithRefs, { allowRefs: true });This change was made for security reasons. An attacker could, in theory, use a #ref{someId.someProperty} reference to access for example the password hash of a user:
const graphUpsertSentByTheAttacker = {
user: {
id: 13431,
'#id': 'user',
},
movie: {
name: '#ref{user.passwordHash}',
},
};and then the attacker could just take the password hash out of the movies's name in a client of some sort.
For this attack to work, the attacker must already have an access to the API that modifies the user's information. Additionally and more importantly, the graph described above (and all other graphs I could think of) would only yield the password hash in the movie's name if the program sets the hash to the graph before the upsertGraph call is executed. This is a highly unlikely scenario and in case of passowords, would require the attacker to be able to access a route that changes the user's password. The reference can never access the property in the database, only in the object itself.
Even though there's very little chance this kind of attack could be carried out at the moment, I'd advice you to never use upsertGraph with { allowRefs: true } and unvalidated user input with references!
relate method now always returns the number of affected rows
relate used to return the inserted pivot table row in case of ManyToManyRelation and the number of updated rows in case of other relations. Now an integer indicating the number of affected rows is always returned.
You need to go through your code and check how the return values of relate queries are used.
$relatedQuery no longer mutates
With objection 1.x, doing this
await somePerson.$relatedQuery('pets');added a property pets for somePerson and saved the result there. This no longer happens with objection 2. Also inserting a new item using
await somePerson.$relatedQuery('pets').insert(pet);would previously add the inserted pet to the pets array of somePerson. This also no longer happens.
You can use withGraphFetched and fetchGraph methods if you want to populate the relations. Also, nothing prevents you from doing this:
somePerson.pets = await somePerson.$relatedQuery('pets');You can also use the Model.relatedInsertQueryMutates and Model.relatedFindQueryMutates properties to revert back to 1.x behavior. Note that those properties are now deprecated and will be removed in 3.0.
context() now acts like mergeContext()
The context method of QueryBuilder now merges the given object with the current context object instead of replacing it. You can use clearContext to clear the context if you need the old behaviour.
Model.raw and Model.fn now return objection raw and fn
Previously Model.raw returned a knex raw builder and Model.fn returned a knex FunctionHelper instance. In 2.0 objection's raw and fn helpers are returned. To get a knex raw builder, you need to use knex.raw directly.
QueryBuilder.toString and QueryBuilder.toSql have been removed
You can use QueryBuilder.toKnexQuery().toSQL() instead.
Rewritten typings
The typings have been completely rewritten in 2.0 and many of the types have changed names. By default you shouldn't get that many errors, but whenever you have explicitly defined an objection type for something other than Model, you may need to adjust that type. For example the QueryBuilder no longer takes three generic arguments, but two.
For this breaking change we can't easily provide clear migration steps, because we don't know how much you have trusted the typescript type inference, and how much you have used explicit types.
One notable change is that you should no longer define your relations properties using Partial<Model> or Partial<Model>[]. You can simply use Model and Model[] and methods like insertGraph and upsertGraph will just work.