Skip to content

biud436/stingerloom-orm

Repository files navigation

Stingerloom ORM

A decorator-driven TypeScript ORM with a typed expression DSL and layered multi-tenancy, built for PostgreSQL, MySQL, and SQLite.

npm version CI license TypeScript strict

Documentation · Getting Started · API Reference · Examples


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 codegenqAlias(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 (plus iff / mapValues / buckets shortcuts), subqueries, and JSON-path navigation — all returning type-safe expressions that compose freely across where() / 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-sqlite3

tsconfig.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 checking

3. 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 → COMMIT

5. 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-clone examples 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.

License

MIT

About

A standalone, framework-agnostic TypeScript ORM that can be used with any Node.js framework

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors