Day#10 Migrations vs Developer: A story of friction

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.

mariusz
mariusz
20 posts
2 followers

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.


Opublikowano

w

,

przez

Komentarze

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *