Skip to content

Relations

jsorm uses explicit relation builders that describe the exact SQL relationship between two models. Each builder generates the appropriate join and mutation SQL without hidden eager loading or lazy proxies.

BuilderSQL relationshipWhen to use
t.belongsTo(Model)Foreign key on this tableUser has roleId column
t.hasOne(Model)Foreign key on other tableProfile has userId column
t.hasMany(Model)Foreign key on other tablePost has authorId column
t.manyToMany(Model)Junction tablePosts ↔ Tags via post_tags
import { defineModel, t } from 'jsorm';
const Role = defineModel('roles', {
id: t.number().primary(),
name: t.string().unique(),
});
const Profile = defineModel('profiles', {
id: t.number().primary(),
bio: t.string().optional(),
});
const Tag = defineModel('tags', {
id: t.number().primary(),
name: t.string().unique(),
});
const Post = defineModel('posts', {
id: t.number().primary(),
title: t.string(),
// Infer junction table name from naming config (default: alphabetical)
tags: t.manyToMany(Tag),
// Or specify junction table and FK columns explicitly:
// tags: t.manyToMany(Tag, {
// through: 'post_tags',
// throughLocalKey: 'post_id',
// throughForeignKey: 'tag_id',
// }),
});
const User = defineModel('users', {
id: t.number().primary(),
name: t.string(),
role: t.belongsTo(Role, {
onUpdate: 'cascade',
onDelete: 'restrict',
constraintName: 'fk_users_role_id', // explicit FK constraint name
}).index(),
profile: t.hasOne(Profile),
posts: t.hasMany(Post),
});

Include relations in select to load them as nested objects:

const users = await jsorm.get(User, {
select: {
id: true,
name: true,
role: { name: true },
profile: { bio: true },
posts: {
title: true,
tags: { name: true },
},
},
});
// Typed: Array<{ id: number; name: string; role: { name: string }; ... }>

Use nested where to filter by relation fields:

const admins = await jsorm.get(User, {
select: { name: true },
where: {
role: { name: { eq: 'admin' } },
posts: { title: { contains: 'release' } },
},
});
await jsorm.update(User, {
data: {
role: { connect: 1 },
},
where: { id: 5 },
});
await jsorm.update(User, {
data: {
profile: {
create: { bio: 'Builder from Day 1' },
},
},
where: { id: 5 },
});
await jsorm.update(Post, {
data: {
tags: {
connect: [1, 2, 3],
disconnect: [4],
},
},
where: { id: 10 },
});
await jsorm.insert(User, {
name: 'Alice',
role: { connect: 1 },
profile: {
create: { bio: 'Builder' },
},
posts: {
create: [
{ title: 'First post', tags: { connect: [1, 2] } },
],
},
});

Configure onUpdate and onDelete on belongsTo to control database-level referential integrity:

role: t.belongsTo(Role, {
onUpdate: 'cascade', // 'cascade' | 'restrict' | 'set-null' | 'no-action'
onDelete: 'restrict',
}),
  1. Use the relation builder that matches the real data shape — don’t use hasMany when the relationship is manyToMany.
  2. Configure onUpdate and onDelete intentionally rather than relying on database defaults.
  3. Keep relation mutations close to write operations instead of scattering pivot logic manually.
  4. Always include relation fields explicitly in select — jsorm never loads relations implicitly.