Model Definition API

Define models with columns, relationships, scopes, and options

defineModel()

typescript
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:

typescript
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: true to name the field.
  • hooks Lifecycle callbacks, usually attached with registerHooks() rather than set here

ColumnConfig

typescript
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

typescript
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 by attach()/detach()/sync()
  • joinTable The junction table for ManyToMany
models/User.ts
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

typescript
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.

typescript
// A scope receives the builder plus any arguments the caller passes
scopes?: 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 repository
const admins = await userRepo.scope("byRole", "admin").execute(orm.client);

Full Example

models/User.ts
import { defineModel, DataTypes, RelationType, generateUUID } from "stabilize-orm";
// registerHooks is exported from the `/hooks` subpath, not the package root
import { 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 definition
registerHooks(User, {
beforeCreate: async (entity) => {
console.log("Creating user:", entity.email);
},
});
export { User };