An SQL-friendly ORM for Node.js built on Knex.js with powerful query building, relationship handling, and JSON Schema validation
—
Quality
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Relationship definitions and eager loading patterns for efficient data fetching.
Objection.js provides five relation types for modeling database relationships.
/**
* One-to-one relation where foreign key is in related table
*/
const HasOneRelation: RelationType;
/**
* One-to-many relation
*/
const HasManyRelation: RelationType;
/**
* Many-to-one relation where foreign key is in owner table
*/
const BelongsToOneRelation: RelationType;
/**
* Many-to-many relation through junction table
*/
const ManyToManyRelation: RelationType;
/**
* One-to-one relation through junction table
*/
const HasOneThroughRelation: RelationType;Configuration object for defining relationships between models.
/**
* Relation mapping configuration
*/
interface RelationMapping {
relation: RelationType;
modelClass: typeof Model | string | (() => typeof Model);
join: RelationJoin;
modify?: string | ((query: QueryBuilder) => void);
filter?: string | ((query: QueryBuilder) => void);
beforeInsert?: (model: Model, context: QueryContext) => void | Promise<void>;
}
interface RelationJoin {
from: string | string[];
to: string | string[];
through?: RelationThrough;
}
interface RelationThrough {
from: string | string[];
to: string | string[];
modelClass?: typeof Model | string | (() => typeof Model);
extra?: string | string[] | Record<string, string>;
modify?: string | ((query: QueryBuilder) => void);
filter?: string | ((query: QueryBuilder) => void);
beforeInsert?: (model: Model, context: QueryContext) => void | Promise<void>;
}Usage Examples:
class Person extends Model {
static get tableName() {
return 'persons';
}
static get relationMappings() {
return {
// Has many pets
pets: {
relation: Model.HasManyRelation,
modelClass: Pet,
join: {
from: 'persons.id',
to: 'pets.ownerId'
}
},
// Belongs to one parent
parent: {
relation: Model.BelongsToOneRelation,
modelClass: Person,
join: {
from: 'persons.parentId',
to: 'persons.id'
}
},
// Many-to-many movies through junction table
movies: {
relation: Model.ManyToManyRelation,
modelClass: Movie,
join: {
from: 'persons.id',
through: {
from: 'persons_movies.personId',
to: 'persons_movies.movieId',
extra: ['role', 'salary'] // Additional columns from junction table
},
to: 'movies.id'
}
},
// Has one through relation
passport: {
relation: Model.HasOneThroughRelation,
modelClass: Passport,
join: {
from: 'persons.id',
through: {
from: 'person_details.personId',
to: 'person_details.passportId'
},
to: 'passports.id'
}
}
};
}
}Methods for loading related data efficiently.
/**
* Eagerly fetch related models using joins
* @param expression - Relation expression string
* @param options - Graph options
* @returns QueryBuilder instance
*/
withGraphJoined(expression: string, options?: GraphOptions): QueryBuilder;
/**
* Eagerly fetch related models using separate queries
* @param expression - Relation expression string
* @param options - Graph options
* @returns QueryBuilder instance
*/
withGraphFetched(expression: string, options?: GraphOptions): QueryBuilder;
/**
* Fetch graph for existing model instances
* @param models - Model instance(s) to fetch relations for
* @param expression - Relation expression
* @param options - Fetch options
* @returns Promise resolving to models with relations
*/
static fetchGraph(
models: Model | Model[],
expression: string,
options?: FetchGraphOptions
): Promise<Model | Model[]>;Usage Examples:
// Basic eager loading
const people = await Person.query()
.withGraphFetched('pets')
.where('age', '>', 18);
// Nested relations
const people = await Person.query()
.withGraphFetched('pets.owner.movies')
.limit(10);
// Multiple relations
const people = await Person.query()
.withGraphFetched('[pets, movies, parent.pets]');
// With filtering
const people = await Person.query()
.withGraphFetched('pets(selectName)')
.modifiers({
selectName(builder) {
builder.select('name');
}
});
// Using aliases
const people = await Person.query()
.withGraphFetched('pets as animals, movies as films');String syntax for specifying which relations to load.
// Basic relation
'pets'
// Nested relations (dot notation)
'pets.owner'
'pets.owner.movies'
// Multiple relations (array notation)
'[pets, movies]'
'[pets, movies, parent]'
// Mixed nested and multiple
'[pets.owner, movies.actors]'
// With modifiers (parentheses)
'pets(selectName)'
'movies(filterPopular).actors(selectName)'
// With aliases (as keyword)
'pets as animals'
'movies as films'
'pets.owner as petOwner'
// Complex expressions
'[pets(selectName) as animals, movies(filterPopular).actors]'Methods for querying related models.
/**
* Create query for related models at class level
* @param relationName - Name of the relation
* @param trx - Optional transaction
* @returns QueryBuilder for related models
*/
static relatedQuery(relationName: string, trx?: Transaction): QueryBuilder;
/**
* Create query for related models at instance level
* @param relationName - Name of the relation
* @param trx - Optional transaction
* @returns QueryBuilder for related models
*/
$relatedQuery(relationName: string, trx?: Transaction): QueryBuilder;Usage Examples:
// Query all pets of a specific person
const personId = 1;
const pets = await Person.relatedQuery('pets')
.for(personId)
.where('species', 'dog');
// Query pets for a person instance
const person = await Person.query().findById(1);
const pets = await person.$relatedQuery('pets')
.where('species', 'cat');
// Insert related model
await person.$relatedQuery('pets')
.insert({ name: 'Fluffy', species: 'cat' });
// Update related models
await person.$relatedQuery('pets')
.patch({ vaccinated: true })
.where('age', '<', 1);Methods for connecting and disconnecting related models.
/**
* Relate existing models
* @param ids - IDs or model instances to relate
* @returns QueryBuilder instance
*/
relate(ids: any | any[] | Model | Model[]): QueryBuilder;
/**
* Unrelate models (remove relationship)
* @returns QueryBuilder instance
*/
unrelate(): QueryBuilder;
/**
* Specify which models to operate on
* @param ids - IDs to target for operation
* @returns QueryBuilder instance
*/
for(ids: any | any[]): QueryBuilder;Usage Examples:
// Relate existing models
await person.$relatedQuery('movies')
.relate([1, 2, 3]); // Movie IDs
// Unrelate specific models
await person.$relatedQuery('movies')
.unrelate()
.where('released', '<', '2000-01-01');
// Unrelate all
await person.$relatedQuery('movies')
.unrelate();
// Complex relation with extra properties (many-to-many)
await person.$relatedQuery('movies')
.relate({
id: 1,
role: 'Actor',
salary: 100000
});Modify related queries during eager loading.
/**
* Modify related queries in graph fetching
* @param expression - Relation expression to modify
* @param modifier - Modifier function or name
* @returns QueryBuilder instance
*/
modifyGraph(
expression: string,
modifier: string | ((query: QueryBuilder) => void)
): QueryBuilder;Usage Examples:
const people = await Person.query()
.withGraphFetched('pets')
.modifyGraph('pets', builder => {
builder.where('species', 'dog').orderBy('name');
});
// Using named modifiers
const people = await Person.query()
.withGraphFetched('pets(onlyDogs)')
.modifiers({
onlyDogs(builder) {
builder.where('species', 'dog');
}
});type RelationType = {
new (): Relation;
};
type RelationExpression = string | object;
interface GraphOptions {
minimize?: boolean;
separator?: string;
aliases?: Record<string, string>;
joinOperation?: string;
maxBatchSize?: number;
}
interface FetchGraphOptions {
transaction?: Transaction;
skipFetched?: boolean;
}
interface Relation {
name: string;
ownerModelClass: typeof Model;
relatedModelClass: typeof Model;
joinModelClass?: typeof Model;
joinTable?: string;
}Install with Tessl CLI
npx tessl i tessl/npm-objection