Model Definition API
Define models with columns, relationships, scopes, and options
defineModel()
function defineModel(config: ModelConfig): typeof Model
// The returned value is a class generated for this config. Its constructor// takes a plain object and assigns it onto the instance:class Model { constructor(data: any);}Registers the model and returns its generated class. Pass that class to orm.getRepository(), to autoMigrate(), and to MetadataStorage.
ModelConfig:
interface ModelConfig { tableName: string; // required columns: Record<string, ColumnConfig>; // required versioned?: boolean; // version history table softDelete?: boolean; // enable soft deletes relations?: RelationConfig[]; scopes?: Record< string, (qb: QueryBuilder<any>, ...args: any[]) => QueryBuilder<any> >; timestamps?: TimestampsConfig; hooks?: Partial<Record<HookType, HookCallback | HookCallback[]>>;}Options:
- tableName Database table name (required)
- columns Column definitions (required)
- versioned Enable version history tracking
- timestamps Auto-manage createdAt/updatedAt columns
- relations Relationship definitions
- scopes Reusable query filters
- softDelete Enable soft deletes. A column must also be marked
softDelete: trueto name the field. - hooks Lifecycle callbacks, usually attached with
registerHooks()rather than set here
ColumnConfig
interface ColumnConfig { type: DataTypes; // Column data type (required) name?: string; // Override SQL column name length?: number; // Max length for STRING precision?: number; // Precision for DECIMAL scale?: number; // Scale for DECIMAL required?: boolean; // NOT NULL constraint unique?: boolean; // UNIQUE constraint defaultValue?: any; // Default value on insert defaultExpression?: DefaultExpression; // SQL default expression index?: string; // Index name softDelete?: boolean; // Mark as soft delete field optimisticLock?: boolean; // Enable optimistic locking encrypted?: boolean; // Field-level encryption minLength?: number; // Min string length (validation) maxLength?: number; // Max string length (validation) pattern?: RegExp; // Regex validation customValidator?: (val: any) => boolean | string; // Custom validation}There is no primaryKey or autoIncrement option. The column keyed id is the primary key; whether it auto-increments is decided by its type. See the Data Types page.
Not every option reaches DDL. name, type, required, unique, defaultValue, defaultExpression, index, length, precision and scale are used when generating CREATE TABLE. The last three also bound the value on write, so they are enforced even on dialects whose DDL cannot express them — Postgres and SQLite, which get TEXT and NUMERIC respectively.
RelationConfig
enum RelationType { OneToOne, OneToMany, ManyToOne, ManyToMany,}
interface RelationConfig { type: RelationType; // required target: () => any; // required — a thunk returning the target model property: string; // required — property name for the relation foreignKey?: string; inverseKey?: string; joinTable?: string; // ManyToMany only}- foreignKey The join column on the child/join table
- inverseKey The other side of the join — required for
ManyToMany, and used byattach()/detach()/sync() - joinTable The junction table for
ManyToMany
relations: [ { type: RelationType.OneToMany, target: () => Post, property: "posts", foreignKey: "authorId", }, { type: RelationType.ManyToMany, target: () => Role, property: "roles", joinTable: "user_roles", foreignKey: "userId", inverseKey: "roleId", },]target is a function rather than the class itself so two models can reference each other before both are defined.
TimestampsConfig & Scopes
interface TimestampsConfig { createdAt?: string; updatedAt?: string;}The values are column names, not property names — they must match the SQL columns the ORM should keep updated. A column listed here is also created if the model does not declare it. The ORM sets createdAt on insert and updatedAt on every update.
// A scope receives the builder plus any arguments the caller passesscopes?: Record< string, (qb: QueryBuilder<any>, ...args: any[]) => QueryBuilder<any>>;
const User = defineModel({ tableName: "users", timestamps: { createdAt: "createdAt", updatedAt: "updatedAt" }, columns: { /* ... */ }, scopes: { active: (qb) => qb.where("isActive = ?", true), byRole: (qb, role: string) => qb.where("role = ?", role), },});
// Applied through the repositoryconst admins = await userRepo.scope("byRole", "admin").execute(orm.client);Full Example
import { defineModel, DataTypes, RelationType, generateUUID } from "stabilize-orm";// registerHooks is exported from the `/hooks` subpath, not the package rootimport { registerHooks } from "stabilize-orm/hooks";
const User = defineModel({ tableName: "users", versioned: true, timestamps: { createdAt: "createdAt", updatedAt: "updatedAt" }, columns: { id: { type: DataTypes.UUID, defaultValue: generateUUID() }, email: { type: DataTypes.STRING, length: 255, required: true, unique: true, pattern: /^[^@]+@[^@]+\.[^@]+$/, }, name: { type: DataTypes.STRING, length: 100, required: true }, bio: { type: DataTypes.TEXT }, isActive: { type: DataTypes.BOOLEAN, defaultValue: true }, role: { type: DataTypes.STRING, length: 50, defaultValue: "user" }, metadata: { type: DataTypes.JSON, encrypted: true }, version: { type: DataTypes.INTEGER, optimisticLock: true }, deletedAt: { type: DataTypes.DATETIME, softDelete: true }, }, relations: [ { type: RelationType.OneToMany, target: () => Post, property: "posts", foreignKey: "authorId", }, ], scopes: { active: (qb) => qb.where("isActive = ?", true), admin: (qb) => qb.where("role = ?", "admin"), byRole: (qb, role: string) => qb.where("role = ?", role), },});
// Register hooks after model definitionregisterHooks(User, { beforeCreate: async (entity) => { console.log("Creating user:", entity.email); },});
export { User };