Relationships API Reference
Relationship helpers are functions imported from @orphnet/d1-eloquent that return query wrappers. They are called inside model methods or inside static eagerLoaders for eager loading.
import { belongsTo, hasMany, hasOne, belongsToMany } from '@orphnet/d1-eloquent'Eager Loading Setup
To use .with(['relation']) on a query, define the relations in static eagerLoaders on the model. Each key is a relation name; each value is a function that receives the loaded model instance and returns the relationship wrapper.
class Post extends BaseModel<PostAttrs> {
static table = 'posts'
static eagerLoaders = {
comments: (post: Post) => hasMany(Comment, 'post_id').on(post),
author: (post: Post) => belongsTo(User, 'user_id').on(post),
}
}
// Usage
const posts = await Post.query()
.with(['comments', 'author'])
.get(env.DB)
// posts[0].relations.comments → Comment[]
// posts[0].relations.author → User | nullUnknown relation names throw at runtime
Calling .with(['unknownRelation']) throws a runtime error that names the unknown relation. Verify relation names match the keys in static eagerLoaders.
See BaseModel — eagerLoaders for the configuration reference.
belongsTo(relatedCtor, foreignKey, localKey?)
belongsTo(
relatedCtor: TModelCtor,
foreignKey: string,
localKey?: string
): { first(db: D1Database): Promise<TRelated | null> }Fetches the parent/owner record. The foreignKey is on the current model; localKey on the related model (defaults to its primaryKey).
Use case: A Comment belongs to a Post (via comment.post_id). A Post belongs to a User (via post.user_id).
class Comment extends BaseModel<CommentAttrs> {
static table = 'comments'
async post(db: D1Database) {
return belongsTo(Post, 'post_id').on(this).first(db)
}
async author(db: D1Database) {
return belongsTo(User, 'user_id').on(this).first(db)
}
}
const post = await comment.post(env.DB)
// SELECT * FROM posts WHERE id = comment.post_id LIMIT 1hasMany(relatedCtor, foreignKey, localKey?)
hasMany(
relatedCtor: TModelCtor,
foreignKey: string,
localKey?: string
): {
get(db: D1Database): Promise<TRelated[]>
first(db: D1Database): Promise<TRelated | null>
}Fetches a collection of child records. The foreignKey is on the related model; localKey on the current model (defaults to its primaryKey).
Use case: A User has many Post rows (via post.user_id). A Post has many Comment rows (via comment.post_id).
class User extends BaseModel<UserAttrs> {
static table = 'users'
async posts(db: D1Database) {
return hasMany(Post, 'user_id').on(this).get(db)
}
}
const posts = await user.posts(env.DB)
// SELECT * FROM posts WHERE user_id = user.idhasOne(relatedCtor, foreignKey, localKey?)
hasOne(
relatedCtor: TModelCtor,
foreignKey: string,
localKey?: string
): { first(db: D1Database): Promise<TRelated | null> }Fetches a single child record. Semantically identical to hasMany — the distinction is purely conventional: callers use .first() to signal a one-to-one relationship.
Use case: A User has one Profile (via profile.user_id).
class User extends BaseModel<UserAttrs> {
static table = 'users'
async profile(db: D1Database) {
return hasOne(Profile, 'user_id').on(this).first(db)
}
}
const profile = await user.profile(env.DB)
// SELECT * FROM profiles WHERE user_id = user.id LIMIT 1belongsToMany(opts)
belongsToMany(opts: {
pivot: string
foreignKey: string
relatedKey: string
related: TModelCtor
}): { get(db: D1Database): Promise<TRelated[]> }Fetches a many-to-many collection via a pivot table. Executes a single JOIN query — no N+1.
| Option | Description |
|---|---|
pivot | Name of the pivot table (e.g. 'post_tags') |
foreignKey | Column in the pivot that references the current model's PK (e.g. 'post_id') |
relatedKey | Column in the pivot that references the related model's PK (e.g. 'tag_id') |
related | The related model constructor (e.g. Tag) |
Use case: A Post belongs to many Tag rows via the post_tags pivot table.
class Post extends BaseModel<PostAttrs> {
static table = 'posts'
async tags(db: D1Database) {
return belongsToMany({
pivot: 'post_tags',
foreignKey: 'post_id',
relatedKey: 'tag_id',
related: Tag,
}).on(this).get(db)
}
}
const tags = await post.tags(env.DB)
// SELECT tags.* FROM tags
// INNER JOIN post_tags ON post_tags.tag_id = tags.id
// WHERE post_tags.post_id = post.idPivot tables must not have a deleted_at column
When the current model has softDeletes = true, the soft-delete scope appends an unqualified deleted_at IS NULL condition. If the pivot table also has a deleted_at column, the JOIN will produce an "ambiguous column name" SQLite error. Keep pivot tables free of deleted_at columns.