metal-orm
v1.1.9
Published
[](https://www.npmjs.com/package/metal-orm) [](https://github.com/celsowm/metal-orm/blob/main/LICENSE) [
- 🎨 One AST, multiple levels: All features share the same SQL AST foundation—no magic, just composable layers
- 🚀 Multi-dialect from the start: MySQL, PostgreSQL, SQLite, SQL Server support built-in
⚡ 30-Second Quick Start
import { defineTable, col, selectFrom, MySqlDialect } from 'metal-orm';
const users = defineTable('users', {
id: col.primaryKey(col.int()),
name: col.varchar(255),
});
const query = selectFrom(users).select('id', 'name').limit(10);
const { sql, params } = query.compile(new MySqlDialect());
// That's it! Use sql + params with any driver.
// ↑ Fully typed—no casting, no 'any', just strong types all the way downThree Levels of Abstraction
MetalORM is a TypeScript-first, AST-driven SQL toolkit you can dial up or down depending on how "ORM-y" you want to be:
Level 1 – Query builder & hydration 🧩
Define tables withdefineTable/col.*, build strongly-typed queries on a real SQL AST, and hydrate flat result sets into nested objects – no ORM runtime involved.Level 2 – ORM runtime (entities + Unit of Work 🧠)
LetOrmSession(created fromOrm) turn rows into tracked entities with lazy relations, cascades, and a Unit of Work that flushes changes withsession.commit().Level 3 – Decorator entities (classes + metadata ✨)
Use@Entity,@Column,@PrimaryKey, relation decorators,bootstrapEntities()(or the lazy bootstrapping ingetTableDefFromEntity/selectFromEntity) to describe your model classes. MetalORM bootstraps schema & relations from metadata and plugs them into the same runtime and query builder.
Use only the layer you need in each part of your codebase.
Table of Contents 🧭
- Documentation
- Features
- Installation
- Quick start - three levels
- When to use which level?
- Design & Architecture
- FAQ
- Performance & Production
- Community & Support
- Contributing
- License
Documentation 📚
Full docs live in the docs/ folder:
- Introduction
- Getting Started
- Level 3 Backend Tutorial
- Schema Definition
- Query Builder
- Tree Behavior (Nested Set/MPTT)
- DTO (Data Transfer Objects)
- OpenAPI Schema Generation
- DML Operations
- Hydration & Entities
- Runtime & Unit of Work
- Save Graph
- Caching
- Advanced Features
- Multi-Dialect Support
- Schema Generation (DDL)
- API Reference
- DB ➜ TS Type Mapping
- Stored Procedures
Features 🚀
Level 1 – Query builder & hydration
- Declarative schema definition with
defineTable,col.*, and typed relations. - Typed temporal columns:
col.date()/col.datetime()/col.timestamp()default tostringbut accept a generic when your driver returnsDate(e.g.col.date<Date>()). - Fluent query builder over a real SQL AST
(SelectQueryBuilder,InsertQueryBuilder,UpdateQueryBuilder,DeleteQueryBuilder). - Advanced SQL: CTEs, aggregates, window functions, subqueries, bitwise operators (
&,|,^,<<,>>), JSON, CASE, EXISTS, and the full SQL function catalog (e.g.STDDEV,VARIANCE,LOG2,CBRT,COALESCE,NULLIF,GREATEST,LEAST,IFNULL,LOCALTIME,LOCALTIMESTAMP,AGE). - Table-valued functions: use the new
tvf(key, …)helper when you want portable intents such asARRAY_UNNEST, letting the dialects’TableFunctionStrategyrenderers emit dialect-specific syntax (LATERAL/WITH ORDINALITY, alias validation, quoting, etc.).fnTable()remains available as the raw escape hatch when you need to emit a specific SQL function directly. - String helpers:
lower,upper,trim,ltrim/rtrim,concat/concatWs,substr/left/right,position/instr/locate,replace,repeat,lpad/rpad,space, and more with dialect-aware rendering. - Set operations:
union,unionAll,intersect,exceptacross all dialects (ORDER/LIMIT apply to the combined result; hydration is disabled for compound queries so rows are returned as-is without collapsing duplicates). - Expression builders:
eq,and,or,between,inList,exists,jsonPath,caseWhen, window functions likerowNumber,rank,lag,lead, etc., all backed by typed AST nodes. - Operator safety: scalar operators (
eq,neq,gt,gte,lt,lte) are for single values; for arrays, useinList/notInList.- Migration example:
where(eq(tipoAcao.columns.codigo, codigos))->where(inList(tipoAcao.columns.codigo, codigos)).
- Migration example:
- Relation-aware hydration: turn flat rows into nested objects (
user.posts,user.roles, etc.) using a hydration plan derived from the AST metadata. - Multi-dialect: compile once, run on MySQL/MariaDB, PostgreSQL, SQLite, or SQL Server via pluggable dialects.
- DML: type-safe INSERT / UPDATE / DELETE with
RETURNINGwhere supported.- Includes upsert support via
.onConflict(...).doUpdate(...)/.doNothing()with dialect-specific SQL generation.
- Includes upsert support via
Level 1 is ideal when you:
- Already have a domain model and just want a serious SQL builder.
- Want deterministic SQL (no magical query generation).
- Need to share the same AST across tooling (e.g. codegen, diagnostics, logging).
Level 2 – ORM runtime (OrmSession)
On top of the query builder, MetalORM ships a focused runtime managed by Orm and its request-scoped OrmSessions:
- Entities inferred from your
TableDefs (no separate mapping file). - Lazy, batched relations:
user.posts.load(),user.roles.syncByIds([...]), etc. - Scoped transactions:
session.transaction(async s => { ... })wrapsbegin/commit/rollbackon the existing executor;Orm.transactionremains available when you want a fresh transactional executor per call. - Identity map: the same row becomes the same entity instance within a session (see the Identity map pattern).
- Caching: Flexible caching with
MemoryCacheAdapter(dev),KeyvCacheAdapter(simple production), orRedisCacheAdapter(full-featured with tag support). Features human-readable TTL ('30m','2h'), tag-based invalidation, and multi-tenant cache isolation. - Tree Behavior (Nested Set/MPTT): hierarchical data with
TreeManager,treeQuery(), and@Treedecorators. Efficient O(log n) operations for moves, inserts, and deletes. Supports multi-tree scoping, recovery, and validation. - DTO/OpenAPI helpers: the
metal-orm/dtomodule generates DTOs and OpenAPI schemas, including tree schemas (TreeNode,TreeNodeResult, threaded trees). - Unit of Work (
OrmSession) tracking New/Dirty/Removed entities and relation changes, inspired by the classic Unit of Work pattern. - Graph persistence: mutate a whole object graph and flush once with
session.commit(). - Partial updates: use
session.patchGraph()to update only specific fields of an entity and its relations (returnsnullif entity doesn't exist). - Relation change processor that knows how to deal with has-many and many-to-many pivot tables.
- Interceptors:
beforeFlush/afterFlushhooks for cross-cutting concerns (auditing, multi-tenant filters, soft delete filters, etc.). - Domain events:
addDomainEventand a DomainEventBus integrated intosession.commit(), aligned with domain events from Domain-driven design. - JSON-safe entities: relation wrappers hide internal references and implement
toJSON, soJSON.stringifyof hydrated entities works without circular reference errors.
Use this layer where:
- A request-scoped context fits (web/API handlers, jobs).
- You want change tracking, cascades, and relation helpers instead of manual SQL for every update.
Level 3 – Decorator entities
If you like explicit model classes, you can add a thin decorator layer on top of the same schema/runtime:
@Entity()on a class to derive and register a table name (by default snake_case plural of the class name, with an optionaltableNameoverride).@Column(...)and@PrimaryKey(...)on properties; decorators collect column metadata and later buildTableDefs from it.- Relation decorators:
@HasMany({ target, foreignKey, ... })@HasOne({ target, foreignKey, ... })@BelongsTo({ target, foreignKey, ... })@BelongsToMany({ target, pivotTable, ... })bootstrapEntities()scans metadata, buildsTableDefs, wires relations with the samehasOne/hasMany/belongsTo/belongsToManyhelpers you would use manually, and returns the resulting tables. (If you forget to call it,getTableDefFromEntity/selectFromEntitywill bootstrap lazily on first use, but bootstrapping once at startup lets you reuse the same table defs and generate schema SQL.)selectFromEntity(MyEntity)lets you start aSelectQueryBuilderdirectly from the class. By default,execute(session)returns actual entity instances with all columns selected.- Generate entities from an existing DB:
npx metal-orm-gen -- --dialect=postgres --url=$DATABASE_URL --schema=public --out=src/entities.tsintrospects your schema and spits out@Entity/@Columnclasses you can immediatelybootstrapEntities()with.
You don’t have to use decorators, but when you do, you’re still on the same AST + dialect + runtime foundation.
Installation 📦
Requirements: Node.js ≥ 20.0.0. For TypeScript projects, use TS 5.6+ to get the standard decorators API and typings.
# npm
npm install metal-orm
# yarn
yarn add metal-orm
# pnpm
pnpm add metal-ormMetalORM compiles SQL; you bring your own driver:
| Dialect | Driver | Install |
| ------------------ | --------- | ---------------------- |
| MySQL / MariaDB | mysql2 | npm install mysql2 |
| SQLite | sqlite3 | npm install sqlite3 |
| PostgreSQL | pg | npm install pg |
| SQL Server | tedious | npm install tedious |
Pick the matching dialect (MySqlDialect, SQLiteDialect, PostgresDialect, MSSQLDialect) when compiling queries.
Drivers are declared as optional peer dependencies. Install only the ones you actually use in your project.
Optional: Caching Backends
For production caching, choose based on your needs:
| Adapter | Tags | Install | Use Case |
|---------|------|---------|----------|
| RedisCacheAdapter | ✅ Full support | npm install ioredis | Production with tag invalidation |
| KeyvCacheAdapter | ❌ Not supported | npm install keyv @keyv/redis | Simple production setups |
# For full-featured Redis (recommended)
npm install ioredis
# For simple Keyv-based caching
npm install keyv @keyv/redisCaching packages are optional peer dependencies. MetalORM includes
MemoryCacheAdapterfor development without external dependencies.
Playground (optional) 🧪
The React playground lives in playground/ and is no longer part of the published package or its dependency tree. To run it locally:
cd playground && npm installnpm run dev(uses the rootvite.config.ts)
It boots against an in-memory SQLite database seeded from fixtures under playground/shared/.
Quick start – three levels
Level 1: Query builder & hydration 🧩
1. Tiny table, tiny query
MetalORM can be just a straightforward query builder.
import mysql from 'mysql2/promise';
import {
defineTable,
tableRef,
col,
selectFrom,
eq,
MySqlDialect,
} from 'metal-orm';
// 1) A very small table
const todos = defineTable('todos', {
id: col.primaryKey(col.int()),
title: col.varchar(255),
done: col.boolean(),
});
// Add constraints
todos.columns.title.notNull = true;
todos.columns.done.default = false;
// Optional: opt-in ergonomic column access
const t = tableRef(todos);
// 2) Build a simple query
const listOpenTodos = selectFrom(todos)
.select('id', 'title', 'done')
.where(eq(t.done, false))
.orderBy(t.id, 'ASC');
// 3) Compile to SQL + params
const dialect = new MySqlDialect();
const { sql, params } = listOpenTodos.compile(dialect);
// 4) Run with your favorite driver
const connection = await mysql.createConnection({ /* ... */ });
const [rows] = await connection.execute(sql, params);
console.log(rows);
// [
// { id: 1, title: 'Write docs', done: 0 },
// { id: 2, title: 'Ship feature', done: 0 },
// ]If you keep a reusable array of column names (e.g. shared across helpers or pulled from config), you can spread it into .select(...) since the method accepts rest arguments:
const defaultColumns = ['id', 'title', 'done'] as const;
const listOpenTodos = selectFrom(todos).select(...defaultColumns);That's it: schema, query, SQL, done.
If you are using the Level 2 runtime (OrmSession), SelectQueryBuilder also provides count(session), executePaged(session, { page, pageSize }), and executeCursor(session, { first/after | last/before }) for common pagination patterns. See docs/pagination.md for offset pagination, eager-include pagination guards, and bidirectional cursor pagination.
Column pickers (preferred selection helpers)
defineTable still exposes the full table.columns map for schema metadata and constraint tweaks, but modern queries usually benefit from higher-level helpers instead of spelling todo.columns.* everywhere.
const t = tableRef(todos);
const listOpenTodos = selectFrom(todos)
.select('id', 'title', 'done') // typed shorthand for the same fields
.where(eq(t.done, false))
.orderBy(t.id, 'ASC');select, include (with columns), includePick, selectColumnsDeep, the sel() helpers for tables, and esel() for entities all build typed selection maps without repeating table.columns.*. Use those helpers when building query selections and reserve table.columns.* for schema definition, relations, or rare cases where you need a column reference outside of a picker. See the Query Builder docs for the reference, examples, and best practices for these helpers.
Ergonomic column access (opt-in) with tableRef
If you still want the convenience of accessing columns without spelling .columns, you can opt-in with tableRef():
import { tableRef, eq, selectFrom } from 'metal-orm';
// Existing style (always works)
const listOpenTodos = selectFrom(todos)
.select('id', 'title', 'done')
.where(eq(todos.columns.done, false))
.orderBy(todos.columns.id, 'ASC');
// Opt-in ergonomic style
const t = tableRef(todos);
const listOpenTodos2 = selectFrom(todos)
.select('id', 'title', 'done')
.where(eq(t.done, false))
.orderBy(t.id, 'ASC');Collision rule: real table fields win.
t.nameis the table name (string)t.$.nameis the column definition for a colliding column name (escape hatch)
2. Relations & hydration (still no ORM)
Now add relations and get nested objects, still without committing to a runtime.
import {
defineTable,
col,
hasMany,
selectFrom,
eq,
count,
rowNumber,
MySqlDialect,
sel,
hydrateRows,
} from 'metal-orm';
const posts = defineTable('posts', {
id: col.primaryKey(col.int()),
title: col.varchar(255),
userId: col.int(),
createdAt: col.timestamp(),
});
// Add constraints
posts.columns.title.notNull = true;
posts.columns.userId.notNull = true;
const users = defineTable('users', {
id: col.primaryKey(col.int()),
name: col.varchar(255),
email: col.varchar(255),
});
// Add relations and constraints
users.relations = {
posts: hasMany(posts, 'userId'),
};
users.columns.name.notNull = true;
users.columns.email.unique = true;
// Build a query with relation & window function
const u = sel(users, 'id', 'name', 'email');
const p = sel(posts, 'id', 'userId');
const builder = selectFrom(users)
.select({
...u,
postCount: count(p.id),
rank: rowNumber(), // window function helper
})
.leftJoin(posts, eq(p.userId, u.id))
.groupBy(u.id)
.groupBy(u.name)
.groupBy(u.email)
.orderBy(count(p.id), 'DESC')
.limit(10)
.includePick('posts', ['id', 'title', 'createdAt']); // eager relation for hydration
const dialect = new MySqlDialect();
const { sql, params } = builder.compile(dialect);
const [rows] = await connection.execute(sql, params);
// Turn flat rows into nested objects
const hydrated = hydrateRows(
rows as Record<string, unknown>[],
builder.getHydrationPlan(),
);
console.log(hydrated);
// [
// {
// id: 1,
// name: 'John Doe',
// email: '[email protected]',
// postCount: 15,
// rank: 1,
// posts: [
// { id: 101, title: 'Latest Post', createdAt: '2023-05-15T10:00:00Z' },
// // ...
// ],
// },
// // ...
// ]Use this mode anywhere you want powerful SQL + nice nested results, without changing how you manage your models.
Level 2: Entities + Unit of Work (ORM runtime) 🧠
When you're ready, you can let MetalORM manage entities and relations for you.
Instead of “naked objects”, your queries can return entities attached to an OrmSession:
import mysql from 'mysql2/promise';
import {
Orm,
OrmSession,
MySqlDialect,
selectFrom,
eq,
tableRef,
createMysqlExecutor,
} from 'metal-orm';
// 1) Create an Orm + session for this request
const connection = await mysql.createConnection({ /* ... */ });
const executor = createMysqlExecutor(connection);
const orm = new Orm({
dialect: new MySqlDialect(),
executorFactory: {
createExecutor: () => executor,
createTransactionalExecutor: () => executor,
dispose: async () => {},
},
});
const session = new OrmSession({ orm, executor });
const u = tableRef(users);
// 2) Load entities with lazy relations
const [user] = await selectFrom(users)
.select('id', 'name', 'email')
.includeLazy('posts') // HasMany as a lazy collection
.includeLazy('roles') // BelongsToMany as a lazy collection
.where(eq(u.id, 1))
.execute(session);
// user is an EntityInstance<typeof users>
// scalar props are normal:
user.name = 'Updated Name'; // marks entity as Dirty
// relations are live collections:
const postsCollection = await user.posts.load(); // batched lazy load
const newPost = user.posts.add({ title: 'Hello from ORM mode' });
// Many-to-many via pivot:
await user.roles.syncByIds([1, 2, 3]);
// 3) Persist the entire graph
await session.commit();
// INSERT/UPDATE/DELETE + pivot updates happen in a single Unit of Work.What the runtime gives you:
- Identity map (per context).
- Unit of Work style change tracking on scalar properties.
- Relation tracking (add/remove/sync on collections).
- Cascades on relations:
'all' | 'persist' | 'remove' | 'link'. - Single flush:
session.commit()figures out inserts, updates, deletes, and pivot changes. - Column pickers to stay DRY:
selecton the root table,include(withcolumns) orincludePickon relations, andselectColumnsDeepor thesel/eselhelpers to build typed selection maps without repeatingtable.columns.*. - Tip: if you assign relations after
defineTable, usesetRelations(table, { ... })so TypeScript can validateinclude(..., { columns: [...] })and pivot columns. Seedocs/query-builder.md.
Level 3: Decorator entities ✨
Finally, you can describe your models with decorators and still use the same runtime and query builder.
The decorator layer is built on the TC39 Stage 3 standard (TypeScript 5.6+), so you simply decorate class fields (or accessors if you need custom logic) and the standard ClassFieldDecoratorContext keeps a metadata bag on context.metadata/Symbol.metadata. @Entity reads that bag when it runs and builds your TableDefs—no experimentalDecorators, parameter decorators, or extra polyfills required.
import mysql from 'mysql2/promise';
import {
Orm,
OrmSession,
MySqlDialect,
col,
createMysqlExecutor,
Entity,
Column,
PrimaryKey,
HasMany,
BelongsTo,
bootstrapEntities,
selectFromEntity,
entityRef,
eq,
} from 'metal-orm';
@Entity()
class User {
@PrimaryKey(col.int())
id!: number;
@Column(col.varchar(255))
name!: string;
@Column(col.varchar(255))
email?: string;
@HasMany({
target: () => Post,
foreignKey: 'userId',
})
posts!: any; // relation wrapper; type omitted for brevity
}
@Entity()
class Post {
@PrimaryKey(col.int())
id!: number;
@Column(col.varchar(255))
title!: string;
@Column(col.int())
userId!: number;
@BelongsTo({
target: () => User,
foreignKey: 'userId',
})
user!: any;
}
// 1) Bootstrap metadata once at startup (recommended so you reuse the same TableDefs)
const tables = bootstrapEntities(); // getTableDefFromEntity/selectFromEntity can bootstrap lazily if you forget
// tables: TableDef[] – compatible with the rest of MetalORM
// 2) Create an Orm + session
const connection = await mysql.createConnection({ /* ... */ });
const executor = createMysqlExecutor(connection);
const orm = new Orm({
dialect: new MySqlDialect(),
executorFactory: {
createExecutor: () => executor,
createTransactionalExecutor: () => executor,
dispose: async () => {},
},
});
const session = new OrmSession({ orm, executor });
// 3) Query starting from the entity class
const U = entityRef(User);
const [user] = await selectFromEntity(User)
.select('id', 'name')
.includeLazy('posts')
.where(eq(U.id, 1))
.execute(session); // user is an actual instance of the User class!
// Use executePlain() if you want raw POJOs instead of class instances
// Return type is inferred from selected columns: { id: number; name: string }[]
const rawUsers = await selectFromEntity(User)
.select('id', 'name')
.executePlain(session);
// Use firstOrFail() to get a single record or throw if not found
const admin = await selectFromEntity(User)
.where(eq(U.role, 'admin'))
.firstOrFail(session); // throws Error('No results found') if no match
// firstOrFailPlain() works the same but returns a POJO
const adminPlain = await selectFromEntity(User)
.where(eq(U.role, 'admin'))
.firstOrFailPlain(session);
user.posts.add({ title: 'From decorators' });
await session.commit();Note: relation helpers like add/attach are only available on tracked entities returned by execute(session). executePlain() returns POJOs without relation wrappers, with return types inferred from your .select() calls—no manual casting needed. Make sure the primary key (e.g. id) is selected so relation adds can link correctly.
Tip: to keep selections terse, use select, include (with columns), or the sel/esel helpers instead of spelling table.columns.* over and over. By default, selectFromEntity selects all columns if you don't specify any.
This level is nice when:
- You want classes as your domain model, but don't want a separate schema DSL.
- You like decorators for explicit mapping but still want AST-first SQL and a disciplined runtime.
When to use which level? 🤔
Query builder + hydration (Level 1)
Great for reporting/analytics, existing codebases with their own models, and services that need strong SQL but minimal runtime magic.ORM runtime (Level 2)
Great for request-scoped application logic and domain modeling where lazy relations, cascades, and graph persistence pay off.Decorator entities (Level 3)
Great when you want class-based entities and decorators, but still want to keep the underlying architecture explicit and layered.
All three levels share the same schema, AST, and dialects, so you can mix them as needed and migrate gradually.
Design & Architecture 🏗️
MetalORM is built on solid software engineering principles and proven design patterns.
Architecture Layers
┌─────────────────────────────────────────────────┐
│ Your Application │
└─────────────────────────────────────────────────┘
│
┌──────────────────┼──────────────────┐
│ │ │
▼ ▼ ▼
┌─────────┐ ┌──────────┐ ┌──────────┐
│ Level 1 │ │ Level 2 │ │ Level 3 │
│ Query │◄─────┤ ORM │◄─────┤Decorators│
│ Builder │ │ Runtime │ │ │
└─────────┘ └──────────┘ └──────────┘
│ │ │
└──────────────────┼──────────────────┘
▼
┌────────────────┐
│ SQL AST │
│ (Typed Nodes) │
└────────────────┘
▼
┌────────────────────────────────────────────────┐
│ Strategy Pattern: Dialects │
│ MySQL | PostgreSQL | SQLite | SQL Server │
└────────────────────────────────────────────────┘
▼
┌────────────────┐
│ Database │
└────────────────┘Design Patterns
- Strategy Pattern: Pluggable dialects (MySQL, PostgreSQL, SQLite, SQL Server) and function renderers allow the same query to target different databases
- Visitor Pattern: AST traversal for SQL compilation and expression processing
- Builder Pattern: Fluent query builders (Select, Insert, Update, Delete) for constructing queries step-by-step
- Factory Pattern: Dialect factory and executor creation abstract instantiation logic
- Unit of Work: Change tracking and batch persistence in
OrmSessioncoordinate all modifications - Identity Map: One entity instance per row within a session prevents duplicate object issues
- Interceptor/Pipeline: Query interceptors and flush lifecycle hooks enable cross-cutting concerns
- Adapter Pattern: Connection pooling adapters allow different pool implementations
Type Safety
- Zero
anytypes: The entire src codebase contains zeroanytypes—every value is properly typed - 100% typed public API: Every public method, parameter, and return value is fully typed
- Full type inference: From schema definition through query building to result hydration
- Compile-time safety: Catch SQL errors at TypeScript compile time, not runtime
- Generic-driven: Leverages TypeScript generics extensively for type propagation
Separation of Concerns
Each layer has a clear, focused responsibility:
- Core AST layer: SQL representation independent of any specific dialect
- Dialect layer: Vendor-specific SQL compilation (MySQL, PostgreSQL, etc.)
- Schema layer: Table and column definitions with relations
- Query builder layer: Fluent API for building type-safe queries
- Hydration layer: Transforms flat result sets into nested object graphs
- ORM runtime layer: Entity management, change tracking, lazy relations, transactions
You can use just the layers you need and stay at the low level (AST + dialects) or adopt higher levels when beneficial.
Frequently Asked Questions ❓
Q: How does MetalORM differ from other ORMs?
A: MetalORM's unique three-level architecture lets you choose your abstraction level—use just the query builder, add the ORM runtime when needed, or go full decorator-based entities. This gradual adoption path is uncommon in the TypeScript ecosystem. You're not locked into an all-or-nothing ORM approach.
Q: Can I use this in production?
A: Yes! MetalORM is designed for production use with robust patterns like Unit of Work, Identity Map, and connection pooling support. The type-safe query builder ensures SQL correctness at compile time.
Q: Do I need to use all three levels?
A: No! Use only what you need. Many projects stay at Level 1 (query builder) for its type-safe SQL building without any ORM overhead. Add runtime features (Level 2) or decorators (Level 3) only where they provide value.
Q: What about migrations?
A: MetalORM provides schema generation via DDL builders. See the Schema Generation docs for details on generating CREATE TABLE statements from your table definitions.
Q: How type-safe is it really?
A: Exceptionally. The entire codebase contains zero any types—every value is properly typed with TypeScript generics and inference. All public APIs are fully typed, and your queries, entities, and results get full TypeScript checking at compile time.
Q: What design patterns are used?
A: MetalORM implements several well-known patterns: Strategy (dialects & functions), Visitor (AST traversal), Builder (query construction), Factory (dialect & executor creation), Unit of Work (change tracking), Identity Map (entity caching), Interceptor (query hooks), and Adapter (pooling). This makes the codebase maintainable and extensible.
Performance & Production 🚀
- Zero runtime overhead for Level 1 (query builder) - it's just SQL compilation and hydration
- Efficient batching for Level 2 lazy relations minimizes database round-trips
- Identity Map prevents duplicate entity instances and unnecessary queries
- Connection pooling supported via executor factory pattern (see pooling docs)
- Prepared statements with parameterized queries protect against SQL injection
Production checklist:
- ✅ Use connection pooling for better resource management
- ✅ Enable query logging in development for debugging
- ✅ Set up proper error handling and retries
- ✅ Use transactions for multi-statement operations
- ✅ Monitor query performance with interceptors
Community & Support 💬
- 🐛 Issues: GitHub Issues
- 💡 Discussions: GitHub Discussions
- 📖 Documentation: Full docs
- 🗺️ Roadmap: See what's planned
- 📦 Changelog: View releases
Contributing 🤝
Issues and PRs are welcome! If you're interested in pushing the runtime/ORM side further (soft deletes, multi-tenant filters, outbox patterns, etc.), contributions are especially appreciated.
See the contributing guide for details.
License 📄
MetalORM is MIT licensed.
