Relations
Relations are declared in // comment annotations above interface fields. SlintORM reads these at migration time to create foreign keys and pivot tables, and at query time to resolve preload(), throughRelation(), and relatedTo().
One-to-Many
// One-to-Many: User has many Posts
// Annotation goes on the parent side
interface User {
id: number;
name: string;
// @relation onetomany:Post;foreignKey:userId
posts?: Post[];
}
interface Post {
id: number;
title: string;
userId: number;
// @relation manytoone:User;foreignKey:userId
user?: User;
}
// Preload posts with their users:
const users = await User.query()
.preload('posts')
.get();
// users[0].posts -> Post[]Many-to-One
// Many-to-One: Post belongs to User
// Annotation goes on the child side (same field as foreignKey)
interface Comment {
id: number;
body: string;
// @index
postId: number;
// @relation manytoone:Post;foreignKey:postId
post?: Post;
}
// Preload comments with their post:
const comments = await Comment.query()
.preload('post')
.get();
// comments[0].post -> PostOne-to-One
// One-to-One: User has one Profile
// Use @relationship (alias for @relation)
interface User {
id: number;
email: string;
// @relationship onetoone:Profile;foreignKey:userId
profile?: Profile;
}
interface Profile {
id: number;
// @unique
userId: number;
// @nullable
bio: string;
// @nullable
avatarUrl: string;
// @relationship onetoone:User;foreignKey:userId;onDelete:CASCADE
user?: User;
}
// Preload profile with user:
const user = await User.get({ id: 1 });
const withProfile = await User.query()
.where('id', '=', 1)
.preload('profile')
.first();
// withProfile.profile -> ProfileMany-to-Many
The pivot table named in through: is created automatically during migration. Declare the relation on both sides to make it bidirectional.
// Many-to-Many: User belongs to many Teams
// Requires a pivot table — auto-synthesized at migrate time
interface User {
id: number;
name: string;
// @relation manytomany:Team;through:team_members;foreignKey:userId;relatedKey:teamId
teams?: Team[];
}
interface Team {
id: number;
name: string;
// @relation manytomany:User;through:team_members;foreignKey:teamId;relatedKey:userId
members?: User[];
}
// SlintORM creates team_members(userId, teamId) pivot table on migrate()
// Preload team members:
const teams = await Team.query()
.preload('members')
.get();
// teams[0].members -> User[]
// Preload user's teams:
const user = await User.query()
.where('id', '=', 42)
.preload('teams')
.first();
// user.teams -> Team[]onDelete options
// onDelete options: CASCADE | SET NULL
interface Post {
id: number;
// @relation manytoone:User;foreignKey:userId;onDelete:CASCADE
userId: number;
// Deleting the user deletes their posts too
}
interface Comment {
id: number;
// @relation manytoone:Post;foreignKey:postId;onDelete:SET NULL
postId: number | null;
// Deleting the post sets postId to NULL
}Annotation syntax
// Both @relation and @relationship are supported
// They are exact aliases — use whichever reads more naturally
// These are identical:
// @relation onetomany:Comment;foreignKey:postId
// @relationship onetomany:Comment;foreignKey:postId
// Annotation syntax breakdown:
// @relation <kind>:<ModelName>;foreignKey:<column>[;relatedKey:<col>][;onDelete:<CASCADE|SET NULL>]
// kind: onetomany | manytoone | onetoone | manytomany