Everything a define() model can express, written on the class.import {
DataTypes, Model,
Table, Column, PrimaryKey, AutoIncrement, NotNull, AllowNull, Unique, Default, Comment,
InferAttributes, InferCreationAttributes, CreationOptional,
} from 'ts-prorm-orm';
@Table({
tableName: 'posts',
timestamps: true,
paranoid: true,
indexes: [{ fields: ['published', 'createdAt'] }],
})
class Post extends Model<InferAttributes<Post>, InferCreationAttributes<Post>> {
@Column(DataTypes.INTEGER())
@PrimaryKey
@AutoIncrement
declare id: CreationOptional<number>;
@Column(DataTypes.STRING(200))
@NotNull
declare title: string;
@Column(DataTypes.TEXT())
@AllowNull(true)
declare body: string | null;
@Column(DataTypes.BOOLEAN())
@Default(false)
declare published: CreationOptional<boolean>;
@Column(DataTypes.INTEGER())
@Comment('Denormalised for the leaderboard')
declare views: CreationOptional<number>;
}
declare is required on every property. Without it TypeScript emits a real
class field that can shadow the model's accessor.The same class is a different statement on each engine — watch where the
column comment and the composite index end up:One batch: table, inline index, and the comment as an extended property
The index name differs too (idx_posts_published_createdAt vs
posts_published_createdAt_idx), which matters if a migration later drops it by
name — give indexes an explicit name when you will refer to them.Validation#
@Validate takes the same rules as the validate option — per column, or on
the class for a rule that spans columns:import { Validate, IsEmail, Len, Min } from 'ts-prorm-orm';
@Table({ tableName: 'users' })
@Validate({
endsAfterStart(this: { endsAt: Date; startsAt: Date }) {
if (this.endsAt <= this.startsAt) throw new Error('endsAt must be after startsAt');
},
})
class User extends Model {
@Column(DataTypes.STRING(255))
@IsEmail()
declare email: string;
@Column(DataTypes.STRING(50))
@Len([3, 50])
declare username: string;
@Column(DataTypes.INTEGER())
@Min(0)
declare credits: number;
}
The shorthands are one rule each; @Validate({ … }) takes several at once.
A failing rule throws before any SQL is sent:await Users.create({ email: 'nope', username: 'ada', credits: 0 });
// ValidationError: Validation isEmail on email failed
Passwords#
@Secure names a field to hash before every write. addModel() wires the
hooks:import { Secure } from 'ts-prorm-orm';
@Secure('password', { method: 'bcrypt' })
@Table({ tableName: 'users' })
class User extends Model {
@Column(DataTypes.STRING(255))
declare password: string;
}
The method is the whole configuration — 'scrypt' (the default, no install),
'bcrypt', 'argon2', or an adapter of your own. With the default, what
reaches the table is the hash, and the instance's validate(plain) checks a
password against it:const account = await Accounts.create({ email: 'ada@example.com', password: 'correct horse' });
await account.validate('correct horse'); // true
await account.validate('wrong'); // false
[!WARNING]
The hash is still an ordinary column, so toJSON() — and anything that
serialises the row — includes it. Leave it out of API responses explicitly. A define() model says the
same thing on the column: password: { type: DataTypes.STRING(255), secure: true }.
Associations#
Declared with decorators that take a thunk, so two models can reference each
other across imports:import { HasMany, BelongsTo } from 'ts-prorm-orm';
@Table({ tableName: 'users' })
class User extends Model {
@HasMany(() => Post, { foreignKey: 'userId', as: 'posts' })
declare posts?: Post[];
}
@Table({ tableName: 'posts' })
class Post extends Model {
@BelongsTo(() => User, { foreignKey: 'userId', as: 'author' })
declare author?: User;
}
Register both with addModel(). Associations are bound once both models are
registered, so the order you register them in does not matter.The @BelongsTo side is what an include from a post walks:const Users = prorm.addModel(User);
const Posts = prorm.addModel(Post);
const posts = await Posts.findAll({ include: [{ model: Users, as: 'author' }] });
posts[0].author; // { id: 1, email: 'ada@example.com' }
Inheritance#
@AbstractModel marks a base class that has no table of its own; its columns
are inherited:import { AbstractModel } from 'ts-prorm-orm';
@AbstractModel()
class Auditable extends Model {
@Column(DataTypes.DATE())
declare createdAt: CreationOptional<Date>;
@Column(DataTypes.STRING(100))
declare createdBy: string;
}
@Table({ tableName: 'invoices' })
class Invoice extends Auditable {
@Column(DataTypes.INTEGER())
@PrimaryKey
@AutoIncrement
declare id: CreationOptional<number>;
}
Registering Auditable itself throws, which is the point — it has no table:Auditable is declared @AbstractModel and has no table of its own. Register the concrete models that extend it instead.
The inherited columns come first in the table, before the subclass's own:Next#
Decorators — the full catalogue and what needs wiring Modified at 2026-10-04 00:40:16