@stingerloom/orm
v0.22.0
Published
A standalone, framework-agnostic TypeScript ORM that can be used with any Node.js framework
Downloads
919
Maintainers
Readme
Why Stingerloom?
- Multi-tenancy built in — Layered metadata system (inspired by Docker OverlayFS) with
AsyncLocalStorage-based context isolation. Zero cross-tenant leakage by design. - 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 (plusiff/mapValues/bucketsshortcuts), subqueries, and JSON-path navigation — all returning type-safe expressions that compose freely acrosswhere() / having() / select(). Import the namespace as{ Expressions as exp }to keep call sites short. - 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. - 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.
Quick Start
npm install @stingerloom/orm reflect-metadata
npm install pg # or mysql2, better-sqlite3tsconfig.json — decorator metadata must be on:
{ "compilerOptions": { "experimentalDecorators": true, "emitDecoratorMetadata": true } }1. Define entities
import "reflect-metadata";
import {
Entity, PrimaryGeneratedColumn, Column,
ManyToOne, OneToMany, RelationColumn,
} from "@stingerloom/orm";
@Entity()
class Author {
@PrimaryGeneratedColumn() id!: number;
@Column() name!: string;
@OneToMany(() => Post, p => p.author) posts!: Post[];
}
@Entity()
class Post {
@PrimaryGeneratedColumn() id!: number;
@Column() title!: string;
@Column({ type: "int" }) views!: number;
@Column({ type: "datetime" }) publishedAt!: Date;
@ManyToOne(() => Author, a => a.posts)
@RelationColumn({ name: "author_id" })
author!: Author;
authorId?: number;
}2. Connect — same API across MySQL, PostgreSQL, and SQLite
import { EntityManager, bufferPlugin } from "@stingerloom/orm";
const em = new EntityManager();
await em.register({
type: "postgres", // or "mysql" / "sqlite" — query code stays identical
host: "localhost", port: 5432,
username: "postgres", password: "postgres", database: "app",
entities: [Author, Post],
synchronize: true, // disable in production; use migrations instead
});
em.extend(bufferPlugin()); // enable Identity Map + dirty checking3. Typed QueryDSL with JOIN — IDE autocomplete, no codegen, no string columns
import { qAlias } from "@stingerloom/orm";
const p = qAlias(Post, "p");
const a = qAlias(Author, "a");
const trending = await em.createQueryBuilder(p)
.innerJoinRelation("author", "a") // FK derived from @ManyToOne metadata — no ON clause needed
.select([
p.title.as("title"),
a.name.as("author"),
p.views.as("views"),
p.publishedAt.year().as("yr"),
])
.where(p.title.containsIgnoreCase("typescript"))
.andWhere(p.views.gt(100))
.andWhere(a.name.startsWith("J"))
.orderBy(p.views.desc())
.getRawMany();Every reference to p.title, a.name, p.publishedAt.year(), etc. is resolved against the entity at compile time — typo a column name and TypeScript fails the build. innerJoinRelation reads the FK from the @ManyToOne metadata so you never write the join condition by hand.
4. Unit of Work — Identity Map + dirty checking
const buf = em.buffer();
const p1 = await buf.findOne(Post, { where: { id: 1 } });
const p2 = await buf.findOne(Post, { where: { id: 1 } });
console.log(p1 === p2); // true — second lookup hits the Identity Map, no extra SELECT
p1!.views = 500;
await buf.flush(); // BEGIN → single UPDATE with only the dirty column → COMMIT5. Multi-tenancy — AsyncLocalStorage-scoped, zero leakage
import { MetadataContext } from "@stingerloom/orm";
await MetadataContext.run("tenant_a", async () => {
const posts = await em.find(Post);
// every metadata lookup inside this frame resolves through tenant_a's
// overlay layer first, then falls through to the public layer
});A NestJS interceptor or Express middleware wraps the per-request handler in MetadataContext.run(tenantId, …) — concurrent requests for different tenants stay isolated by AsyncLocalStorage with no shared mutable state.
See the Getting Started guide for full setup, and the
nestjs-multitenant/nestjs-linear-cloneexamples for production-shaped tenant wiring.
Features
| 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 | StingerloomOrmModule.forRoot / forFeature, @InjectRepository, @InjectEntityManager, multi-DB named connections |
Database Support
| | 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 | ✓ | ✓ | — |
Examples
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 |
Contributing
Contributions are welcome. Please open an issue first to discuss what you'd like to change.
