@hodfords/typeorm-migrations
A fluent, Laravel-style schema builder on top of TypeORM migrations.
Description
@hodfords/typeorm-migrations wraps TypeORM's QueryRunner and schema-builder classes in a small, readable API so migrations are easier to write and review. Instead of constructing Table, TableColumn, TableIndex and TableForeignKey objects by hand, you describe your schema with chainable methods:
await this.create('User', (table) => {
table.primaryUuid('id');
table.string('email').index();
table.integer('age').nullable();
table.uuid('enterpriseId').index().foreign('Enterprise');
table.baseTime();
});
It is designed for PostgreSQL but works with any database TypeORM supports (a few helpers such as unsigned() or zerofill() are driver-specific and noted as such below).
Installation 🤖
npm install @hodfords/typeorm-migrations --save
typeorm is a peer dependency and must be installed in your project.
Usage
Extend BaseMigration instead of implementing MigrationInterface directly. Implement run() (executed on up) and optionally rollback() (executed on down):
import { BaseMigration } from '@hodfords/typeorm-migrations';
import { QueryRunner } from 'typeorm';
export class CreateUserTable1626749239046 extends BaseMigration {
async run(queryRunner: QueryRunner): Promise<void> {
await this.create('User', (table) => {
table.primaryUuid('id');
table.string('email').index();
table.string('firstName').nullable();
table.string('lastName').nullable();
table.string('password').nullable();
table.integer('role');
table.string('language').length(10).default("'en'");
table.timestamp('lastLoginAt').nullable();
table.uuid('enterpriseId').nullable().index().foreign('Enterprise');
table.createdAt().index();
table.updatedAt();
table.deletedAt();
});
}
async rollback(queryRunner: QueryRunner): Promise<void> {
await this.drop('User');
}
}
Updating an existing table
update() lets you add columns, drop columns and index existing columns in one place:
export class UpdateUserTable1726749239046 extends BaseMigration {
async run(queryRunner: QueryRunner): Promise<void> {
await this.update('User', (table) => {
table.string('phone').nullable();
table.dropColumn('legacyColumn');
table.addIndexAlreadyColumn('email');
});
}
}
Table methods
Inside create() and update() callbacks you receive a BaseTable. Every column method returns a BaseColumn, so you can chain the column modifiers below.
Primary keys
| Method | Description |
|---|---|
id(name?) | Auto-incrementing integer primary key (default name id) |
increments(name) | Auto-incrementing integer primary key |
smallIncrements(name) | Auto-incrementing smallint primary key |
bigIncrements(name) | Auto-incrementing bigint primary key |
primaryUuid(name?) | uuid primary key with uuid_generate_v4() default |
primaryUuidV7(name?) | uuid primary key with uuidv7() default (Postgres 18+) |
Character types
| Method | Description |
|---|---|
string(name, length = 255) | character varying |
varchar(name, length = 255) | Alias of string() |
char(name, length) | char |
text(name) | text |
citext(name) | citext (case-insensitive text) |
Numeric types
| Method | Description |
|---|---|
integer(name) | integer |
smallint(name) | smallint |
bigint(name) | bigint |
decimal(name, precision = 10, scale = 2) | decimal |
numeric(name, precision = 10, scale = 2) | numeric |
money(name) | money |
real(name) / float4(name) | real |
double(name) / doublePrecision(name) / float(name) / float8(name) | double precision |
Date and time types
| Method | Description |
|---|---|
timestamp(name) | timestamp |
timestamptz(name) | timestamp with time zone |
date(name) | date |
time(name) | time |
timetz(name) | time with time zone |
interval(name) | interval |
createdAt() | timestamp named createdAt, defaults to now() |
updatedAt() | timestamp named updatedAt, defaults to now() |
deletedAt() | Nullable timestamp named deletedAt |
baseTime() | Shortcut for createdAt() + updatedAt() |
JSON, UUID and other types
| Method | Description |
|---|---|
uuid(name = 'id') | uuid |
json(name) / jsonb(name) | json / jsonb |
boolean(name) | boolean |
enum(name, enumName, values) | enum — accepts a string array or a Record<string, string> (e.g. a TS enum) |
bytea(name) | bytea |
bit(name, length = 1) / varbit(name, length?) | bit / bit varying |
xml(name) | xml |
hstore(name) | hstore |
tsvector(name) / tsquery(name) | Full-text search types |
cube(name) | cube |
ltree(name) | ltree |
column(name, type, options?) | Escape hatch — any type supported by your database driver |
Array types
| Method | Description |
|---|---|
strings(name) | character varying[] |
texts(name) | text[] |
uuids(name) | uuid[] |
integers(name) | integer[] |
smallints(name) / bigints(name) | smallint[] / bigint[] |
booleans(name) | boolean[] |
jsonbs(name) | jsonb[] |
Any column can also be made an array with the .array() modifier.
Network and geometric types
| Method | Description |
|---|---|
inet(name) / cidr(name) | IP address types |
macaddr(name) / macaddr8(name) | MAC address types |
point(name), line(name), lseg(name), box(name), path(name), polygon(name), circle(name) | Geometric types |
geography(name, spatialFeatureType, srid = 4326) | PostGIS geography |
geometry(name, spatialFeatureType, srid = 0) | PostGIS geometry |
Range types
| Method | Description |
|---|---|
int4range(name) / int8range(name) | Integer ranges |
numrange(name) | Numeric range |
tsrange(name) / tstzrange(name) | Timestamp ranges |
daterange(name) | Date range |
Other table helpers
| Method | Description |
|---|---|
dropColumn(name) | Drop a column (only meaningful inside update()) |
addIndexAlreadyColumn(name) | Create an index on a column that already exists |
Every column method also accepts a final options argument (Partial<TableColumnOptions>) that is merged into the underlying TypeORM column definition:
table.string('email', 255, { isUnique: true, comment: 'login email' });
Column modifiers
Methods on BaseColumn are chainable and cover all of TypeORM's TableColumnOptions:
| Modifier | Description |
|---|---|
nullable() / notNullable() | Set NULL / NOT NULL |
unique() | Add a unique constraint |
index() | Create an index for this column |
primary(constraintName?) | Mark as primary key |
default(value) | Set the default value (e.g. default("'en'")) |
useCurrent() | Default to now() |
length(length) | Set the column length |
precision(precision, scale?) / scale(scale) | Numeric precision and scale |
array() | Store an array of the column type |
comment(text) | Column comment |
collation(collation) | Column collation |
charset(charset) | Column character set |
enum(values, enumName?) | Enum values and the exact enum type name |
autoIncrement() | Auto-increment generation |
generated(strategy?) | Generation strategy: 'increment', 'uuid', 'rowid' or 'identity' |
generatedIdentity(value?) | Identity column, 'ALWAYS' or 'BY DEFAULT' (Postgres 10+) |
asExpression(expression, type?) | Generated (computed) column, 'STORED' or 'VIRTUAL' |
spatial(featureType, srid?) | Spatial feature type and SRID |
width(width) | Display width (MySQL only) |
unsigned() / zerofill() | Numeric attributes (MySQL only) |
onUpdate(value) | ON UPDATE trigger (MySQL only) |
foreign(table, column = 'id', onDelete = 'CASCADE', onUpdate = 'CASCADE', name?) | Add a foreign key referencing another table |
Example:
table.decimal('price', 12, 4).notNullable().default(0).comment('unit price');
table.uuid('ownerId').index().foreign('User', 'id', 'SET NULL');
table.string('fullName').asExpression('"firstName" || \' \' || "lastName"');
Migration methods
BaseMigration exposes the full QueryRunner surface as small helpers. All of them can be called from run() and rollback().
Tables
| Method | Description |
|---|---|
create(tableName, callback) | Create a table, its indexes and foreign keys |
update(tableName, callback) | Add/drop columns and create indexes/foreign keys on an existing table |
drop(table) | Drop a table |
dropIfExists(table) | Drop a table only if it exists |
rename(oldTable, newTableName) | Rename a table |
hasTable(tableName) | Check whether a table exists |
getTable(tableName) | Load the full Table definition |
clearTable(tableName) | Truncate a table |
changeTableComment(table, comment?) | Change the table comment |
Columns
| Method | Description |
|---|---|
hasColumn(tableName, columnName) | Check whether a column exists |
addColumn(table, column) / addColumns(table, columns) | Add raw TableColumns |
renameColumn(table, oldName, newName) | Rename a column |
changeColumn(table, oldColumn, newColumn) / changeColumns(table, changes) | Change column definitions |
dropColumn(table, columnName) / dropColumns(table, columnNames) | Drop columns |
Keys, constraints and indexes
| Method | Description |
|---|---|
createPrimaryKey(table, columnNames, constraintName?) | Create a primary key |
updatePrimaryKeys(table, columns) | Update primary keys |
dropPrimaryKey(table, constraintName?) | Drop the primary key |
createUnique(table, columnName, name?) | Create a unique constraint (name defaults to `${table}-${column}Unique`) |
dropUnique(table, uniqueOrName) | Drop a unique constraint |
dropUniqueColumn(table, columnName) | Drop a unique constraint by its generated name |
createCheck(table, expression, name?) | Create a check constraint |
dropCheck(table, checkOrName) | Drop a check constraint |
createExclusion(table, expression, name?) | Create an exclusion constraint (Postgres) |
dropExclusion(table, exclusionOrName) | Drop an exclusion constraint |
addIndex(tableName, columnNames, options?) | Create an index (name defaults to `${table}-${columns}Index`) |
dropIndex(table, indexOrName) / dropIndices(table, indices) | Drop indexes |
addForeign(tableName, columnName, referencedTable, referencedColumn = 'id', onDelete = 'CASCADE', onUpdate = 'CASCADE') | Create a foreign key |
dropForeign(table, foreignKeyOrName) | Drop a foreign key |
Views, schemas and databases
| Method | Description |
|---|---|
createView(name, expression, materialized = false) | Create a (materialized) view |
dropView(viewOrName) | Drop a view |
createSchema(schemaPath, ifNotExist = true) | Create a schema |
dropSchema(schemaPath, ifExist = true, isCascade = false) | Drop a schema |
hasSchema(schema) | Check whether a schema exists |
createDatabase(database, ifNotExist = true) | Create a database |
dropDatabase(database, ifExist = true) | Drop a database |
hasDatabase(database) | Check whether a database exists |
Raw queries
await this.query('UPDATE "User" SET "role" = $1 WHERE "role" IS NULL', [1]);
You also always have direct access to the QueryRunner passed into run()/rollback() for anything not covered above.
Testing
npm test
The test suite covers BaseTable, BaseColumn and BaseMigration against a mocked QueryRunner, so it does not require a database.
License 📝
This project is licensed under the MIT License