npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@typeforge-orm/orm

v0.1.0

Published

TypeScript ORM with EF Core-style snapshot migrations, change tracking, and TypeORM-style decorators — PostgreSQL first.

Downloads

162

Readme

@typeforge-orm/orm

A TypeScript ORM for PostgreSQL with:

  • EF Core-style snapshot migrations — diff your entities, generate typed migration files, apply with one command.
  • Change trackingDbContext.saveChanges() detects inserts, updates, and deletes automatically.
  • TypeORM-style decorators@Entity, @Column, @PrimaryKey, relations, indexes, soft-delete.
  • Typed query API — fluent where / orderBy / take / skip / include with full TypeScript inference.
  • Advisory-lock runner — safe concurrent deployments; all pending migrations apply in one atomic transaction.
  • typeforge CLIgenerate, run, revert, status commands, reads a plain TypeScript config file.

Installation

npm install @typeforge-orm/orm reflect-metadata pg
npm install -D @types/pg ts-node typescript

tsconfig.json must have:

{
  "compilerOptions": {
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "useDefineForClassFields": false
  }
}

reflect-metadata must be imported once before any entities are loaded — typically in your entry point:

import 'reflect-metadata';

Quick start

1. Define entities

// user.entity.ts
import 'reflect-metadata';
import { Entity, PrimaryKey, Column, Index, SoftDelete } from '@typeforge-orm/orm';

@SoftDelete()
@Index(['email'], { unique: true })
@Entity({ table: 'users' })
export class User {
  @PrimaryKey({ generated: 'uuid' })
  id!: string;

  @Column({ type: 'varchar', length: 255 })
  email!: string;

  @Column({ type: 'varchar', length: 100 })
  name!: string;

  @Column({ type: 'int' })
  age!: number;

  @Column({ type: 'boolean', default: true })
  isActive!: boolean;

  deletedAt!: Date | null;
}
// post.entity.ts
import { Entity, PrimaryKey, Column, ManyToOne } from '@typeforge-orm/orm';
import { User } from './user.entity';

@Entity({ table: 'posts' })
export class Post {
  @PrimaryKey({ generated: 'uuid' })
  id!: string;

  @Column({ type: 'varchar', length: 500 })
  title!: string;

  @Column({ type: 'text' })
  body!: string;

  @Column({ type: 'uuid' })
  authorId!: string;

  @Column({ type: 'boolean', default: false })
  published!: boolean;

  @ManyToOne(() => User, u => u.id)
  author?: User;
}

2. Create a DbContext

// app-db-context.ts
import { DbContext, type DbContextOptions } from '@typeforge-orm/orm';
import { User } from './user.entity';
import { Post } from './post.entity';

export class AppDbContext extends DbContext {
  get users() { return this.set(User); }
  get posts()  { return this.set(Post); }
}

3. Connect and use

import 'reflect-metadata';
import { PostgresDriver } from '@typeforge-orm/orm';
import { AppDbContext } from './app-db-context';

const driver = new PostgresDriver({
  host: 'localhost', port: 5432,
  database: 'myapp', user: 'postgres', password: 'postgres',
  max: 10, // pool size
});

// One context per request / unit of work
const connection = await driver.connect();
const db = new AppDbContext({ driver, connection });

// INSERT
const user = new User();
user.email = '[email protected]';
user.name  = 'Alice';
user.age   = 30;
db.users.add(user);
await db.saveChanges();
console.log(user.id); // generated UUID

// SELECT
const alice = await db.users.find(user.id);        // by PK
const active = await db.users
  .where(u => u.isActive)
  .orderBy(u => u.name)
  .toList();

// UPDATE
alice!.name = 'Alice Smith';
await db.saveChanges(); // only the changed column is sent

// Soft DELETE
db.users.remove(alice!);
await db.saveChanges(); // sets deleted_at, not a hard DELETE

await connection.release();
await driver.end();

Decorator reference

| Decorator | Target | Description | |---|---|---| | @Entity(opts?) | class | Mark as a mapped table. opts.table overrides the auto snake_case name. | | @PrimaryKey(opts?) | property | Primary key. opts.generated: 'uuid' \| 'identity' \| 'sequence'. | | @Column(opts?) | property | Mapped column. opts.type, opts.nullable, opts.length, opts.default. | | @RowVersion() | property | Optimistic-concurrency token (int, auto-incremented by DB). | | @Index(cols, opts?) | class | Declare an index. opts.unique, opts.name. | | @SoftDelete(col?) | class | Enable soft-delete via a nullable timestamp column (default deleted_at). | | @OneToOne(target, inverse?, opts?) | property | One-to-one relation. | | @OneToMany(target, inverse?, opts?) | property | One-to-many relation. | | @ManyToOne(target, inverse?, opts?) | property | Many-to-one relation. | | @ManyToMany(target, inverse?, opts?) | property | Many-to-many relation. |

Column types: 'string' | 'int' | 'float' | 'boolean' | 'date' | 'datetime' | 'uuid' | 'text' | 'json' | 'varchar'


Query API

const dbSet = db.users; // DbSet<User>

// Fluent chain — all methods return a new Queryable
const results = await dbSet
  .where(u => u.isActive)              // filter
  .where(u => gt(u.age, 18))           // additional filter (AND)
  .orderBy(u => u.name)                // ORDER BY name ASC
  .orderByDesc(u => u.age)             // then BY age DESC
  .take(20)                            // LIMIT 20
  .skip(40)                            // OFFSET 40
  .asNoTracking()                      // skip change tracker
  .includeDeleted()                    // include soft-deleted rows
  .toList();                           // execute → User[]

// Terminators
await dbSet.firstOrDefault();          // first row or undefined
await dbSet.where(...).count();        // SELECT COUNT(*)
const exists = await dbSet.where(...).any(); // SELECT 1 … LIMIT 1

Expression helpers

import { eq, ne, lt, lte, gt, gte, and, or, not,
         isNull, isNotNull, like, ilike, inList, between } from '@typeforge-orm/orm';

db.users.where(u => eq(u.email, '[email protected]'));
db.users.where(u => like(u.name, 'Ali%'));
db.users.where(u => inList(u.age, [18, 21, 25]));
db.users.where(u => between(u.age, 18, 65));
db.users.where(u => and(u.isActive, gt(u.age, 18)));
db.users.where(u => or(isNull(u.deletedAt), gt(u.age, 0)));

Change tracking

DbContext tracks every entity returned by queries:

// Tracked automatically on load
const user = await db.users.find(id);

user!.name = 'Updated';         // change detected
db.users.remove(user!);         // marks for deletion

const { inserted, updated, deleted } = await db.saveChanges();

Use asNoTracking() for read-only queries that don't need tracking (better performance):

const readOnly = await db.users.asNoTracking().toList();

Migrations

CLI setup

Create typeforge.config.ts in your project root:

// typeforge.config.ts
import 'reflect-metadata';
import * as path from 'path';
import * as dotenv from 'dotenv';
dotenv.config();

// Import all entity files so decorators register
import './src/entities/user.entity';
import './src/entities/post.entity';

import type { TypeForgeCliConfig } from '@typeforge-orm/orm';

const config: TypeForgeCliConfig = {
  migrationsDir: path.resolve(__dirname, 'src/migrations'),
  connection: {
    host:     process.env.DB_HOST ?? 'localhost',
    port:     Number(process.env.DB_PORT ?? 5432),
    database: process.env.DB_NAME ?? 'myapp_dev',
    user:     process.env.DB_USER ?? 'postgres',
    password: process.env.DB_PASS ?? '',
  },
  getMigrations: () => require('./src/migrations').migrations,
};

export default config;

Add scripts to package.json:

{
  "scripts": {
    "migration:generate": "typeforge generate",
    "migration:run":      "typeforge run",
    "migration:status":   "typeforge status",
    "migration:revert":   "typeforge revert"
  }
}

Commands

| Command | What it does | |---|---| | typeforge generate [Label] | Diff current entities against snapshot → emit a typed migration file | | typeforge run | Create DB if missing, apply all pending migrations in one transaction | | typeforge status | List applied and pending migrations with timestamps | | typeforge revert [N] | Roll back the last N migrations (default 1) in one transaction |

Migration files

Generated files are plain TypeScript — review and edit them freely before running:

// src/migrations/1700000000000_AddEmailIndex.migration.ts
import type { Migration, MigrationContext } from '@typeforge-orm/orm';

export class AddEmailIndex_1700000000000 implements Migration {
  readonly name = 'AddEmailIndex_1700000000000';

  async up(ctx: MigrationContext): Promise<void> {
    await ctx.execute(`CREATE UNIQUE INDEX "idx_users_email" ON "users" ("email")`);
  }

  async down(ctx: MigrationContext): Promise<void> {
    await ctx.execute(`DROP INDEX "idx_users_email"`);
  }
}

Register every migration in your barrel:

// src/migrations/index.ts
import type { Migration } from '@typeforge-orm/orm';
import { Init_1699000000000 } from './1699000000000_init.migration';
import { AddEmailIndex_1700000000000 } from './1700000000000_AddEmailIndex.migration';

export const migrations: Migration[] = [
  new Init_1699000000000(),
  new AddEmailIndex_1700000000000(),
];

NestJS integration

// database.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { PostgresDriver, ensureDatabase, MigrationRunner, connectPg } from '@typeforge-orm/orm';

@Module({ imports: [ConfigModule] })
export class DatabaseModule {
  static forRoot() {
    return {
      module: DatabaseModule,
      providers: [
        {
          provide: PostgresDriver,
          useFactory: async (cfg: ConfigService) => {
            const opts = {
              host: cfg.get('DB_HOST', 'localhost'),
              port: cfg.get<number>('DB_PORT', 5432),
              database: cfg.get('DB_NAME', 'myapp'),
              user: cfg.get('DB_USER', 'postgres'),
              password: cfg.get('DB_PASS', ''),
            };
            // create DB + run migrations on startup
            await ensureDatabase(opts, opts.database);
            const { conn, end } = await connectPg(opts);
            const runner = new MigrationRunner(conn);
            await runner.run(require('./migrations').migrations);
            await end();
            // return pool driver for request-scoped connections
            return new PostgresDriver({ ...opts, max: cfg.get<number>('DB_POOL', 10) });
          },
          inject: [ConfigService],
        },
      ],
      exports: [PostgresDriver],
    };
  }
}

Error types

All errors extend TypeForgeError and carry a stable code string:

| Class | Code | Thrown when | |---|---|---| | ConfigurationError | TFE_CONFIG | Invalid decorator usage or missing metadata | | ConnectionError | TFE_CONNECTION | Pool / client cannot connect | | QueryError | TFE_QUERY | SQL execution fails (includes .sql property) | | EntityNotFoundError | TFE_NOT_FOUND | find() / first() finds no row | | OptimisticConcurrencyError | TFE_CONCURRENCY | @RowVersion mismatch on save | | ValidationError | TFE_VALIDATION | Pre-save validation failure | | TransactionError | TFE_TRANSACTION | Transaction begin / commit / rollback failure |

import { TypeForgeError, QueryError } from '@typeforge-orm/orm';

try {
  await db.saveChanges();
} catch (e) {
  if (e instanceof QueryError) {
    console.error('SQL that failed:', e.sql);
  }
}

PostgresDriver options

new PostgresDriver({
  host:     'localhost',   // default
  port:     5432,          // default
  database: 'myapp',
  user:     'postgres',
  password: 'secret',
  ssl:      false,         // set true for managed cloud DBs
  max:      10,            // connection pool size
});

Requirements

| Peer | Version | |---|---| | Node.js | ≥ 18 | | PostgreSQL | ≥ 13 (uses gen_random_uuid(), advisory locks) | | reflect-metadata | ≥ 0.2 | | ts-node | ≥ 10 (optional — needed only if typeforge.config.ts is TypeScript) |


License

MIT © TypeForge Contributors