Skip to content

Models

Single source of truth for types and queries

Section titled “Single source of truth for types and queries”

Models are the single source of truth in jsorm. A model definition drives both TypeScript types and runtime query behavior.

import type { InferInput, InferModel } from 'jsorm';
import { defineModel, t } from 'jsorm';
const User = defineModel('users', {
id: t.number().primary(),
name: t.string(),
email: t.string().optional(),
active: t.boolean().default(true),
createdAt: t.date(),
score: t.number().nullable(),
});
type UserRecord = InferModel<typeof User>;
// {
// id: number;
// name: string;
// email?: string;
// active: boolean;
// createdAt: Date;
// score: number | null;
// }
type UserInput = InferInput<typeof User>;
// {
// name: string;
// email?: string;
// active?: boolean;
// createdAt: Date;
// score?: number | null;
// }
// Note: primary keys and relation fields excluded from InferInput; FK columns stay included
BuilderTypeScript typeNotes
t.string()stringVARCHAR-equivalent
t.text()stringUnbounded text / TEXT column
t.number()numberNumeric / FLOAT
t.boolean()boolean
t.date()DateDate only
t.time()stringTime string HH:MM:SS
t.dateTime()DateFull timestamp
t.uuid()stringUUID string
t.autoIncrement()numberAuto-incrementing integer primary key
t.enum(['a', 'b'] as const)'a' | 'b'String union, checked at runtime
t.json()unknownStored as JSON, returned as parsed object
t.belongsTo(Model)ModelRecordFK → parent
t.hasOne(Model)ModelRecordReverse of belongsTo
t.hasMany(Model)ModelRecord[]One-to-many
t.manyToMany(Model)ModelRecord[]Many-to-many via junction
t.string()
.optional() // undefined allowed; excluded from required input
.nullable() // null allowed in type and DB
.primary() // marks as primary key (excluded from InferInput)
.default('unknown') // default value; makes input optional
.unique() // UNIQUE constraint in migrations
.index() // INDEX in migrations
const Post = defineModel('posts', {
id: t.autoIncrement().primary(),
title: t.string(),
slug: t.string().unique(),
body: t.text().optional(),
status: t.enum(['draft', 'published', 'archived'] as const).default('draft'),
published: t.boolean().default(false),
viewCount: t.number().default(0),
publishedAt: t.dateTime().nullable(),
deletedAt: t.date().nullable(),
metadata: t.json(),
});
const Role = defineModel('roles', {
id: t.number().primary(),
name: t.string().unique(),
});
const User = defineModel('users', {
id: t.number().primary(),
name: t.string(),
// belongsTo: FK column on this table
role: t.belongsTo(Role, {
onUpdate: 'cascade',
onDelete: 'restrict',
constraintName: 'fk_users_role_id', // explicit constraint name for migrations
}).index(),
});
const Tag = defineModel('tags', {
id: t.number().primary(),
name: t.string(),
});
const Article = defineModel('articles', {
id: t.number().primary(),
title: t.string(),
tags: t.manyToMany(Tag),
author: t.belongsTo(User),
});

Relation mutation shapes in insert/update:

  • belongsTo: role: 1, role: { connect: 1 }, role: { connect: null }
  • hasOne: profile: { connect: 1 }, profile: { create: { ... } }
  • hasMany: posts: { connect: [1, 2] }, posts: { create: [{ ... }] }
  • manyToMany: tags: [1, 2], tags: { connect: [1, 2] }, tags: { create: [{ ... }] }

For larger projects, co-locate models near domain boundaries:

src/
schema/
main/
user.ts ← defineModel('users', {...})
role.ts
posts/
article.ts
tag.ts
index.ts ← re-exports all models
jsorm.config.ts ← defines connectionSources, migrationSources, models
  1. Keep table names stable and explicit — rename only through migrations.
  2. Use t.autoIncrement().primary() for auto-incrementing integer PKs.
  3. Use t.uuid() for UUID primary keys.
  4. Prefer inferred InferModel / InferInput instead of manual interfaces.
  5. One model per file in large projects; co-locate small related models.
  6. Add explicit constraintName on belongsTo when you need predictable FK names in migrations.