Today was supposed to be a calm day. I just wanted to add a single value to a lookup table using a TypeORM migration. Two hours later, I had a fully working script to track migration status, a clearer understanding of TypeORM’s friction points, and a renewed appreciation for tools that just work.
The goal
I needed to add a new partner_type to the system — specifically, a "customer" entry. Since we use TypeORM with ESM, NestJS and PostgreSQL in schema mode, I expected it to be a matter of running:
npx typeorm migration:create
npx typeorm migration:run
It wasn’t.
The first wall: CLI and ESM don’t get along
Using --dataSource with a .ts file broke instantly:
Unable to open file ... Unknown file extension ".ts"
Trying ts-node with --esm failed too, as the CLI is still tightly coupled to CJS-style resolution. The solution? Bypass the CLI and write your own runner:
// scripts/run-migrations.ts
import 'dotenv/config';
import { DataSource } from 'typeorm';
import { typeOrmConfig } from '../src/config/typeorm.config.js';
const dataSource = new DataSource(typeOrmConfig);
await dataSource.initialize();
await dataSource.runMigrations();
await dataSource.destroy();
Then run it:
npx tsc --project tsconfig.build.json
node dist/apps/partner/api/scripts/run-migrations.js
Simple enough. Except…
The second wall: class name mismatch
TypeORM validates that your class name matches the filename. If your file is:
1750167523186-AddCustomerPartnerType.ts
Then your class must be:
export class AddCustomerPartnerType1750167523186 implements MigrationInterface
Anything else throws.
The third wall: missing schema
Our system uses the "xyz" schema in PostgreSQL. If you forget to write:
INSERT INTO "xyz"."partner_types" ...
…you’ll be debugging relation does not exist errors for 10 minutes. Ask me how I know.
The final script: migration status
Because typeorm CLI has no native status command, I wrote a custom script to list migrations and show which ones are pending:
// scripts/migration-status.ts
import 'dotenv/config';
import { readdirSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import { DataSource } from 'typeorm';
import { typeOrmConfig } from '../src/config/typeorm.config.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const migrationDir = join(__dirname, '../migrations');
const dataSource = new DataSource(typeOrmConfig);
await dataSource.initialize();
const executed = new Set(
(await dataSource.query('SELECT name FROM "partners"."migrations"'))
.map((row: any) => row.name)
);
const allFiles = readdirSync(migrationDir)
.filter(name => name.endsWith('.ts') || name.endsWith('.js'));
const fileInfo = allFiles.map(file => {
const [timestamp, ...rest] = file.replace(/\.(ts|js)$/, '').split('-');
const className = `${rest.join('')}${timestamp}`;
return { file, className };
});
const allStatuses = fileInfo.map(({ file, className }) => ({
file,
className,
status: executed.has(className) ? '✔ applied' : '✗ pending'
}));
console.table(allStatuses);
await dataSource.destroy();
Takeaway
If you’re using:
- TypeORM 0.3+
- ESM (
type: "module") - NestJS
- custom schema
…expect to write your own tooling. And maybe question your ORM life choices. Or switch to a SQL-based migration tool entirely.
At least now, things are working. And I have a script that tells me they are.
SELECT * FROM "xyz"."migrations";
Still feels better than guessing.

Dodaj komentarz