A decorator-driven TypeScript ORM with a typed expression DSL and layered multi-tenancy, built for PostgreSQL, MySQL, and SQLite.
Documentation · Getting Started · API Reference · Examples
- Decorator-first — Define entities, relations, hooks, and validation with TypeScript decorators. Column types are inferred automatically.
- Typed QueryDSL via Proxy, no codegen —
qAlias(Entity, "u")gives you IDE autocomplete on every column. Chain.eq / .gt / .like / .in, aggregates (.count() / .sum() / .avg()), CAST, date components, window functions, CASE/WHEN, subqueries, and JSON-path navigation — all returning type-safe expressions that compose freely acrosswhere() / having() / select(). - Unit of Work plugin — Identity Map, dirty checking, cascade, batch flush, lazy proxies, and pessimistic locking via
em.extend(bufferPlugin()). Single-level cache skips round-trips for repeated PK lookups. - Multi-tenancy built in — Layered metadata system (inspired by Docker OverlayFS) with
AsyncLocalStorage-based context isolation. Zero cross-tenant leakage by design. - Three databases, one API — MySQL (incl. MariaDB-specific optimizations), PostgreSQL, and SQLite share the same EntityManager interface. Switch drivers without rewriting queries.
- Schema Diff migrations — Compare live database state against entity metadata and auto-generate migration code. Supports
true / "safe" / "dry-run"synchronize modes. - NestJS-ready — First-party module with
@InjectRepository,@InjectEntityManager, and multi-DB named connections.
npm install @stingerloom/orm reflect-metadata
npm install pg # or mysql2, better-sqlite3import {
EntityManager, Entity, PrimaryGeneratedColumn, Column, qAlias,
} from "@stingerloom/orm";
@Entity()
class Post {
@PrimaryGeneratedColumn() id!: number;
@Column() title!: string;
@Column({ type: "int" }) views!: number;
@Column({ type: "datetime" }) publishedAt!: Date;
}
const em = new EntityManager();
await em.register({ type: "postgres", entities: [Post], synchronize: true, /* ... */ });
// CRUD
const post = await em.save(Post, { title: "Hello World", views: 0, publishedAt: new Date() });
const found = await em.findOne(Post, { where: { id: post.id } });
// Typed QueryDSL — IDE autocomplete on every column
const p = qAlias(Post, "p");
const trending = await em.createQueryBuilder(Post, "p")
.select([
p.title.as("title"),
p.views.as("views"),
p.publishedAt.year().as("yr"),
])
.where(p.title.containsIgnoreCase("typescript"))
.andWhere(p.views.gt(100))
.orderBy(p.views.desc())
.getRawMany();See the Getting Started guide for full setup instructions.
| Category | Highlights |
|---|---|
| Modeling | @Entity, @Column, @ManyToOne, @OneToMany, @ManyToMany, @OneToOne, eager/lazy loading, inheritance mapping (STI / TPT / TPC), UUID columns with UUIDv7 |
| Querying | find, findOne, findWithCursor, findAndCount, SelectQueryBuilder with JOIN / GROUP BY / HAVING; qAlias() typed expression chain — string / numeric / math helpers, CAST, date arithmetic + components, window functions, CASE WHEN, subquery operators, JSON-path navigation, raw SQL escape hatches |
| Mutations | save, update, delete, softDelete, restore, upsert, batchUpsert, streamBatch, batch operations |
| Transactions | @Transactional decorator, manual BEGIN / COMMIT / ROLLBACK, savepoints, isolation levels, deadlock retry, NOWAIT / SKIP LOCKED |
| Unit of Work | em.extend(bufferPlugin()) — Identity Map, dirty checking, cascade, batch flush, lazy proxies, pessimistic locking, @Version optimistic locking |
| Multi-tenancy | Layered metadata (OverlayFS model), MetadataContext.run(), PostgreSQL schema isolation, TenantMigrationRunner |
| Migrations | SchemaDiff auto-detection (column rename heuristic), MigrationGenerator, CLI runner (npx stingerloom migrate:run | rollback | status | generate) |
| Observability | N+1 detection, slow query warnings, EXPLAIN analysis, EntitySubscriber events, query listeners |
| Validation | @NotNull, @MinLength, @MaxLength, @Min, @Max, Zod / Valibot schemas via qb.selectSchema(schema) |
| Schema definition | Decorators or decorator-free EntitySchema; Prisma schema import |
| Infrastructure | Connection pooling, read replicas, retry with backoff, per-query timeout, graceful shutdown, SSL/TLS, AsyncIterable streaming (stream()), MariaDB native UUID + INSERT … RETURNING |
| NestJS | StinglerloomOrmModule.forRoot / forFeature, @InjectRepository, @InjectEntityManager, multi-DB named connections |
| MySQL / MariaDB | PostgreSQL | SQLite | |
|---|---|---|---|
| CRUD | ✓ | ✓ | ✓ |
| Transactions | ✓ | ✓ | ✓ |
| Schema Sync | ✓ | ✓ | ✓ |
| Migrations | ✓ | ✓ | ✓ |
| ENUM | ✓ | ✓ (native + sync) | — |
| JSON path queries | ✓ | ✓ (jsonb) |
✓ |
| Full-text search | ✓ | ✓ | — |
| Window functions | ✓ | ✓ | ✓ |
| Schema Isolation | — | ✓ | — |
| Read Replica | ✓ | ✓ | — |
INSERT … RETURNING |
MariaDB 10.5+ | ✓ | ✓ |
| Native UUID storage | MariaDB 10.7+ | ✓ | — (TEXT) |
| SSL / TLS | ✓ | ✓ | — |
Example projects are included in examples/:
| Project | Description |
|---|---|
| nestjs-cats | CRUD, relations, soft delete, cursor pagination, EntitySubscriber |
| nestjs-blog | ManyToMany, upsert, 59 e2e tests (Users / Posts / Tags / Categories) |
| nestjs-todo | Minimal CRUD — uses the published npm package |
| nestjs-todo-sqlite | Minimal CRUD on SQLite via better-sqlite3 |
| nestjs-multitenant | PostgreSQL schema-based tenant isolation with TenantMigrationRunner |
| prisma-import-demo | Generate Stingerloom entities from an existing Prisma schema |
Contributions are welcome. Please open an issue first to discuss what you'd like to change.