SlintORM
Docsnpm

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 -> Post

One-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 -> Profile

Many-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
PreviousError HandlingNextMigrations