prorm.define() takes a plain object of@Table,@Column) declare the same thing on a TypeScript class and are registered withprorm.addModel(). Both produce the same model object with the same staticprorm.define()import { Prorm, DataTypes } from 'ts-prorm-orm';
const prorm = new Prorm({ dialect: 'sqlite', storage: ':memory:' });
await prorm.connect();
const User = prorm.define(
'User',
{
id: { type: DataTypes.INTEGER(), primaryKey: true, autoIncrement: true },
name: { type: DataTypes.STRING(100), allowNull: false },
email: { type: DataTypes.STRING(255), allowNull: false, unique: true },
role: { type: DataTypes.ENUM('admin', 'user'), defaultValue: 'user' },
bio: { type: DataTypes.TEXT() },
age: { type: DataTypes.INTEGER() },
meta: { type: DataTypes.JSON() },
},
{ tableName: 'users', timestamps: true },
);
await prorm.sync();sync() call emits one CREATE TABLE. The attribute map is the same onENUM becomes a real ENUM(...) on MySQL and a CHECK constraint on SQLite;JSON stays JSON whereDataTypes.JSONB() when you want PostgreSQL's binary JSON. See[!NOTE] define()is synchronous but schedules theCREATE TABLEin the background.prorm.sync()awaits those pending creates before it runs, which is why the
normal startup shape is define every model, thenawait prorm.sync()once.
src/
db/
index.ts # creates the Prorm, calls every define*, then sync()
models/
user.ts # defineUser(prorm)
post.ts # definePost(prorm)
server.tsimport { DataTypes, type Prorm } from 'ts-prorm-orm';
export function defineUser(prorm: Prorm) {
return prorm.define(
'User',
{
id: { type: DataTypes.INTEGER(), primaryKey: true, autoIncrement: true },
email: {
type: DataTypes.STRING(255),
allowNull: false,
unique: true,
validate: { isEmail: true },
},
name: { type: DataTypes.STRING(100), allowNull: false },
role: { type: DataTypes.ENUM('admin', 'member', 'viewer'), defaultValue: 'member' },
passwordHash: { type: DataTypes.STRING(255), hidden: true },
lastLoginAt: { type: DataTypes.DATE() },
},
{
tableName: 'users',
paranoid: true,
indexes: [{ fields: ['role'] }],
},
);
}import { Prorm } from 'ts-prorm-orm';
import { defineUser } from './models/user';
import { definePost } from './models/post';
export const prorm = new Prorm({
dialect: 'postgres',
host: process.env.PGHOST,
database: process.env.PGDATABASE,
username: process.env.PGUSER,
password: process.env.PGPASSWORD,
});
export const User = defineUser(prorm);
export const Post = definePost(prorm);
User.hasMany(Post, { foreignKey: 'authorId', as: 'posts' });
Post.belongsTo(User, { foreignKey: 'authorId', as: 'author' });
export async function initDb() {
await prorm.connect();
if (process.env.NODE_ENV !== 'production') await prorm.sync();
}hidden: true on passwordHash means user.toJSON() — and sores.json(user) — never carries it, while the value is still readable asuser.passwordHash inside the server.AttributeOptions object (or a bare{ type }).| Option | Effect |
|---|---|
type | The column's data type. Required. |
allowNull | false emits NOT NULL. Defaults to nullable. |
defaultValue | Column default. A function is evaluated per-insert in JS; DataTypes.NOW maps to the dialect's current-timestamp default. |
primaryKey | Marks the column part of the primary key. |
autoIncrement | AUTOINCREMENT / SERIAL / IDENTITY depending on dialect. |
unique | true, a constraint name, or { name, msg } for a custom UniqueConstraintError message. A duplicate the database rejects is thrown as UniqueConstraintError — see Validation. |
uniqueKey | Groups several columns into one composite unique constraint by sharing a name. |
references | { model, key } — emits a REFERENCES clause. See Indexes & constraints. |
field | The physical column name when it differs from the attribute name — see Column names. |
comment | Column comment, where the dialect supports one. |
validate | Per-field validation rules — see Validation. |
get / set | Attribute-level accessors, used by virtual fields. |
hidden | Excluded from toJSON() by default. |
virtual | Computed on the instance, never stored — see virtual fields. |
secure | Hash the value before every write and never store the plain text. true (scrypt), 'bcrypt', 'argon2', or an adapter — see Passwords. |
const Tag = prorm.define('Tag', {
label: DataTypes.STRING(50), // same as { type: DataTypes.STRING(50) }
slug: { type: DataTypes.STRING(60), allowNull: false, unique: true },
createdBy: { type: DataTypes.INTEGER(), references: { model: 'users', key: 'id' } },
});| Option | Effect |
|---|---|
tableName | Explicit table name. Without it the model name is lower-cased and pluralised (User → users). |
freezeTableName | Use the model name verbatim instead of pluralising. |
schema / schemaDelimiter | Qualify the table (schema.table). PostgreSQL schema, MySQL database. |
timestamps | Adds createdAt / updatedAt. On by default; set false to opt out. |
createdAt / updatedAt / deletedAt | Rename those columns. |
paranoid | Soft deletes — see below. |
underscored | Renames no column: attributes keep their names, and a snake_case column is mapped with field (see the note below). |
primaryKey | A string, or an array for a composite key. |
indexes / constraints / uniqueKeys | Declarative index and constraint definitions created during sync(). |
hooks | Lifecycle handlers — see Hooks. |
scopes / defaultScope | Reusable query fragments — see Scopes. |
validate | Model-level (multi-field) validators. |
classMethods / instanceMethods | Extra statics and instance methods merged onto the model. |
hidden | Map of attribute names to hide from toJSON(). |
engine, charset, collate, rowFormat, initialAutoIncrement, comment | MySQL/MariaDB CREATE TABLE options. |
tablespace, inherit, partitionBy | PostgreSQL CREATE TABLE options. |
validateOnInsert / validateOnUpdate / skipValidations | When validation runs. |
[!WARNING] underscoredand the timestamps. In adefine()/addModel()model
theCREATE TABLE,update(),save()and the paranoid queries all usecreatedAt/updatedAt/deletedAtwhether or notunderscoredis set;
only the objectcreate()returns carriescreated_at/updated_atkeys
underunderscored: true. To store snake_case timestamps, name them
explicitly:{ createdAt: 'created_at', updatedAt: 'updated_at', deletedAt: 'deleted_at' }, which every path honours. The SQL-objects layer (triggers,
routines) snake-cases every column underunderscored; the two are being
aligned.
import { Prorm } from 'ts-prorm-orm';
const config = {
dialect: 'postgres' as const,
database: 'app',
// `define` is read at runtime but is not declared on PrormOptions yet,
// so pass the config through a variable rather than an inline literal.
define: { timestamps: true, paranoid: true },
};
const prorm = new Prorm(config);id, andtimestamps: false removes both stamp columns:const Tag = prorm.define('Tag', { label: DataTypes.STRING(50) }, { timestamps: false });field)const User = prorm.define('User', {
id: { type: DataTypes.INTEGER, primaryKey: true, autoIncrement: true, field: 'user_id' },
email: { type: DataTypes.STRING, unique: true, field: 'email_address' },
});
await User.create({ email: 'ann@example.com' });
// INSERT INTO "users" ("email_address") VALUES (?)
await User.findAll({ attributes: ['id', 'email'], where: { email: { [Op.like]: '%@example.com' } } });
// SELECT "user_id" AS "id", "email_address" AS "email" FROM "users" WHERE "email_address" LIKE ?sync() creates (columns, indexes, uniquecreate,bulkCreate, update, upsert, increment, destroy, restore), to everywhere, col(), order, group, having, attributes, count andraw: true), and to the typed query builder,deletedAt declaredfield are stored there too. In a decorated class, use@Field('email_address') or @Column({ type, field }).field to an attribute of an existing table is a rename:sync({ alter: true }) adds the new column and treats the old one as a columnliteral(), prorm.query() — is sent as written; it namestimestamps: false). Prorm addsDATE columns, both NOT NULL DEFAULT CURRENT_TIMESTAMP, so the databaseimport { Op } from 'ts-prorm-orm';
const lastWeek = await User.findAll({
where: { createdAt: { [Op.gte]: new Date(Date.now() - 7 * 86_400_000) } },
order: [['createdAt', 'DESC']],
});// Instance save: every attribute, including the stamps, is written back.
const grace = await User.findByPk(7);
grace.name = 'Grace';
await grace.save();
// Static update: the columns you pass, plus the updatedAt stamp.
await User.update({ age: 31 }, { where: { id: 7 } });
// silent: true leaves updatedAt alone.
await User.update({ age: 31 }, { where: { id: 7 }, silent: true });Model.update() stamps updatedAt in the same statement unlessbeforeUpdate hook) set it yourself, or pass silent: true. Beforeprorm.define('Event', { name: DataTypes.STRING(50) }, {
tableName: 'events',
createdAt: 'created_on',
});paranoid)DELETE. destroy() writes a timestamp intodeletedAt, and every read silently adds deletedAt IS NULL.const attrs = {
id: { type: DataTypes.INTEGER(), primaryKey: true, autoIncrement: true },
name: { type: DataTypes.STRING(100), allowNull: false },
};
const User = prorm.define('User', attrs, { tableName: 'users', paranoid: true });
await User.destroy({ where: { id: 1 } });
await User.findAll(); // excludes the row
await User.findAll({ paranoid: false }); // includes it
await User.restore({ where: { id: 1 } });
await User.count(); // aggregates honour it too
await User.destroy({ where: { id: 1 }, force: true }); // a real DELETEdeletedAt column is added to the attribute map automatically whenparanoid is set, so it exists in the CREATE TABLE (as a nullableTIMESTAMP/DATETIME) without you declaring it.[!TIP]
A unique column on a paranoid table still sees soft-deleted rows: a user who
deleted their account cannot sign up again with the same email until the old
row is force-deleted. Eitherdestroy({ force: true })on account deletion,
or scrub the unique value before the soft delete.
prorm.addModel() readsimport { Table, Column, PrimaryKey, AutoIncrement, AllowNull, Unique, Default, Field, DataTypes, Model } from 'ts-prorm-orm';
@Table({ tableName: 'users', timestamps: true, paranoid: true })
class User extends Model {
@PrimaryKey
@AutoIncrement
@Column(DataTypes.INTEGER())
id!: number;
@AllowNull(false)
@Column(DataTypes.STRING(100))
name!: string;
@Unique
@Column(DataTypes.STRING(255))
email!: string;
@Default('user')
@Column(DataTypes.ENUM('admin', 'user'))
role!: string;
// Stored in the `display_name` column. `@Column({ type, field })` does the same.
@Field('display_name')
@Column(DataTypes.STRING(100))
displayName!: string;
}
const UserModel = prorm.addModel(User);
await prorm.sync();addModel() returns the model static, and the class itself is also usable — thefindAll, create and the rest. The fullclassMethods and instanceMethods attach behaviour without subclassing:const User = prorm.define('User', attrs, {
tableName: 'users',
classMethods: {
async findByEmail(this: ModelStatic, email: string) {
return this.findOne({ where: { email } });
},
},
instanceMethods: {
displayName(this: { name: string; email: string }) {
return `${this.name} <${this.email}>`;
},
},
});
const u = await User.findByEmail('ada@example.com');
u.displayName();| Call | Returns |
|---|---|
Model.getTableName() | The resolved table name (schema-qualified if set). |
Model.getAttributes() | The raw attribute map. |
Model.rawAttributes | Same map, as a property. |
Model.describe() | The table as the database reports it, via DESCRIBE/information_schema. |
Model.associations | Registered associations keyed by alias. |
prorm.getModels() | Map of every model on the connection. |
prorm.model(name) / prorm.hasModel(name) | Look one up by name. |
const described = await User.describe();
const missing = Object.keys(User.getAttributes()).filter((name) => !(name in described));
if (missing.length) {
throw new Error(`users is missing column(s): ${missing.join(', ')}; run the migrations`);
}prorm.sync() creates the tables for every defined model, in dependency order| Call | Behaviour |
|---|---|
sync() | CREATE TABLE IF NOT EXISTS for each model. |
sync({ force: true }) | DROP TABLE then CREATE TABLE — destroys data. |
sync({ alter: true }) | Diffs the live table against the model and issues ALTER TABLE for added/changed columns. Columns the model no longer has are kept, with a warning. |
sync({ alter: { drop: true } }) | Also drops columns that no longer exist on the model ('hard' is the same). |
sync({ alter: { drop: 'soft' } }) | 4.9.2+: parks those columns under a _prorm_dropped_ name instead of dropping them — see Soft-dropping columns. |
sync({ match: /_dev$/ }) | Only runs when the configured database name matches. |
Model.sync() | Just that one model. |
Model.drop() / Model.truncate() | DROP TABLE / TRUNCATE. |
sync() call decides what to do for each model:alter run on SQLite: the table was created with name nullable andnickname column; the model now makes name required, adds email and nonickname.prorm.define('User', {
id: { type: DataTypes.INTEGER(), primaryKey: true, autoIncrement: true },
name: { type: DataTypes.STRING(100), allowNull: false }, // was nullable
email: DataTypes.STRING(255), // new
// nickname: removed from the model
}, { tableName: 'users' });
await prorm.sync({ alter: true });
await prorm.sync({ alter: { drop: 'soft' } });CREATE TABLE is the table's own definition with the one column edited, soCHECK and UNIQUE constraints survive; before 4.9.2 thenickname survives the[!CAUTION] sync({ force: true })drops every table the connection can see, not
only the ones your models declare. Never point it at a shared or production
database, and guard it withmatch(sync({ force: true, match: /_test$/ }))
so a mis-set database name skips instead of wiping.
sync() is a development convenience. For versioned, reviewable schema changeDataTypes.* and its per-dialect SQLhasOne / hasMany / belongsTo / belongsToMany