metal-orm 1.0.19 → 1.0.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,435 +1,435 @@
1
- # MetalORM ⚙️ - Type-safe SQL, layered ORM, decorator-based entities – all on the same core.
2
-
3
- [![npm version](https://img.shields.io/npm/v/metal-orm.svg)](https://www.npmjs.com/package/metal-orm)
4
- [![license](https://img.shields.io/npm/l/metal-orm.svg)](https://github.com/celsowm/metal-orm/blob/main/LICENSE)
5
- [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%23007ACC.svg)](https://www.typescriptlang.org/)
6
-
7
- MetalORM is a TypeScript-first, AST-driven SQL toolkit you can dial up or down depending on how “ORM-y” you want to be:
8
-
9
- - **Level 1 – Query builder & hydration 🧩**
10
- Define tables with `defineTable` / `col.*`, build strongly-typed queries on a real SQL AST, and hydrate flat result sets into nested objects – no ORM runtime involved.
11
- - **Level 2 – ORM runtime (entities + Unit of Work 🧠)**
12
- Let `OrmSession` (created from `Orm`) turn rows into tracked entities with lazy relations, cascades, and a [Unit of Work](https://en.wikipedia.org/wiki/Unit_of_work) that flushes changes with `session.commit()`.
13
- - **Level 3 – Decorator entities (classes + metadata ✨)**
14
- Use `@Entity`, `@Column`, `@PrimaryKey`, relation decorators, `bootstrapEntities()` and `selectFromEntity()` to describe your model classes. MetalORM bootstraps schema & relations from metadata and plugs them into the same runtime and query builder.
15
-
16
- Use only the layer you need in each part of your codebase.
17
-
18
- ---
19
-
20
- <a id="table-of-contents"></a>
21
- ## Table of Contents 🧭
22
-
23
- - [Documentation](#documentation)
24
- - [Features](#features)
25
- - [Installation](#installation)
26
- - [Quick start - three levels](#quick-start)
27
- - [Level 1 – Query builder & hydration](#level-1)
28
- - [Level 2 – Entities + Unit of Work](#level-2)
29
- - [Level 3 – Decorator entities](#level-3)
30
- - [When to use which level?](#when-to-use-which-level)
31
- - [Design notes](#design-notes)
32
- - [Contributing](#contributing)
33
- - [License](#license)
34
-
35
- ---
36
-
37
- <a id="documentation"></a>
38
- ## Documentation 📚
39
-
40
- Full docs live in the `docs/` folder:
41
-
42
- - [Introduction](https://github.com/celsowm/metal-orm/blob/main/docs/index.md)
43
- - [Getting Started](https://github.com/celsowm/metal-orm/blob/main/docs/getting-started.md)
44
- - [Level 3 Backend Tutorial](https://github.com/celsowm/metal-orm/blob/main/docs/level-3-backend-tutorial.md)
45
- - [Schema Definition](https://github.com/celsowm/metal-orm/blob/main/docs/schema-definition.md)
46
- - [Query Builder](https://github.com/celsowm/metal-orm/blob/main/docs/query-builder.md)
47
- - [DML Operations](https://github.com/celsowm/metal-orm/blob/main/docs/dml-operations.md)
48
- - [Hydration & Entities](https://github.com/celsowm/metal-orm/blob/main/docs/hydration.md)
49
- - [Runtime & Unit of Work](https://github.com/celsowm/metal-orm/blob/main/docs/runtime.md)
50
- - [Advanced Features](https://github.com/celsowm/metal-orm/blob/main/docs/advanced-features.md)
51
- - [Multi-Dialect Support](https://github.com/celsowm/metal-orm/blob/main/docs/multi-dialect-support.md)
52
- - [Schema Generation (DDL)](https://github.com/celsowm/metal-orm/blob/main/docs/schema-generation.md)
53
- - [API Reference](https://github.com/celsowm/metal-orm/blob/main/docs/api-reference.md)
54
-
55
- ---
56
-
57
- <a id="features"></a>
58
- ## Features 🚀
59
-
60
- ### Level 1 – Query builder & hydration
61
-
62
- - **Declarative schema definition** with `defineTable`, `col.*`, and typed relations.
63
- - **Fluent query builder** over a real SQL AST
64
- (`SelectQueryBuilder`, `InsertQueryBuilder`, `UpdateQueryBuilder`, `DeleteQueryBuilder`).
65
- - **Advanced SQL**: CTEs, aggregates, window functions, subqueries, JSON, CASE, EXISTS.
66
- - **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.
67
- - **Set operations**: `union`, `unionAll`, `intersect`, `except` across all dialects (ORDER/LIMIT apply to the combined result; hydration is disabled for compound queries so rows are returned as-is without collapsing duplicates).
68
- - **Expression builders**: `eq`, `and`, `or`, `between`, `inList`, `exists`, `jsonPath`, `caseWhen`, window functions like `rowNumber`, `rank`, `lag`, `lead`, etc., all backed by typed AST nodes.
69
- - **Relation-aware hydration**: turn flat rows into nested objects (`user.posts`, `user.roles`, etc.) using a hydration plan derived from the AST metadata.
70
- - **Multi-dialect**: compile once, run on MySQL/MariaDB, PostgreSQL, SQLite, or SQL Server via pluggable dialects.
71
- - **DML**: type-safe INSERT / UPDATE / DELETE with `RETURNING` where supported.
72
-
73
- Level 1 is ideal when you:
74
-
75
- - Already have a domain model and just want a serious SQL builder.
76
- - Want deterministic SQL (no magical query generation).
77
- - Need to share the same AST across tooling (e.g. codegen, diagnostics, logging).
78
-
79
- ### Level 2 – ORM runtime (`OrmSession`)
80
-
81
- On top of the query builder, MetalORM ships a focused runtime managed by `Orm` and its request-scoped `OrmSession`s:
82
-
83
- - **Entities inferred from your `TableDef`s** (no separate mapping file).
84
- - **Lazy, batched relations**: `user.posts.load()`, `user.roles.syncByIds([...])`, etc.
85
- - **Identity map**: the same row becomes the same entity instance within a session (see the [Identity map pattern](https://en.wikipedia.org/wiki/Identity_map_pattern)).
86
- - **Unit of Work (`OrmSession`)** tracking New/Dirty/Removed entities and relation changes, inspired by the classic [Unit of Work pattern](https://en.wikipedia.org/wiki/Unit_of_work).
87
- - **Graph persistence**: mutate a whole object graph and flush once with `session.commit()`.
88
- - **Relation change processor** that knows how to deal with has-many and many-to-many pivot tables.
89
- - **Interceptors**: `beforeFlush` / `afterFlush` hooks for cross-cutting concerns (auditing, multi-tenant filters, soft delete filters, etc.).
90
- - **Domain events**: `addDomainEvent` and a DomainEventBus integrated into `session.commit()`, aligned with domain events from [Domain-driven design](https://en.wikipedia.org/wiki/Domain-driven_design).
91
- - **JSON-safe entities**: relation wrappers hide internal references and implement `toJSON`, so `JSON.stringify` of hydrated entities works without circular reference errors.
92
-
93
- Use this layer where:
94
-
95
- - A request-scoped context fits (web/API handlers, jobs).
96
- - You want change tracking, cascades, and relation helpers instead of manual SQL for every update.
97
-
98
- ### Level 3 – Decorator entities
99
-
100
- If you like explicit model classes, you can add a thin decorator layer on top of the same schema/runtime:
101
-
102
- - `@Entity()` on a class to derive and register a table name (by default snake_case plural of the class name, with an optional `tableName` override).
103
- - `@Column(...)` and `@PrimaryKey(...)` on properties; decorators collect column metadata and later build `TableDef`s from it.
104
- - Relation decorators:
105
- - `@HasMany({ target, foreignKey, ... })`
106
- - `@HasOne({ target, foreignKey, ... })`
107
- - `@BelongsTo({ target, foreignKey, ... })`
1
+ # MetalORM ⚙️ - Type-safe SQL, layered ORM, decorator-based entities – all on the same core.
2
+
3
+ [![npm version](https://img.shields.io/npm/v/metal-orm.svg)](https://www.npmjs.com/package/metal-orm)
4
+ [![license](https://img.shields.io/npm/l/metal-orm.svg)](https://github.com/celsowm/metal-orm/blob/main/LICENSE)
5
+ [![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%23007ACC.svg)](https://www.typescriptlang.org/)
6
+
7
+ MetalORM is a TypeScript-first, AST-driven SQL toolkit you can dial up or down depending on how “ORM-y” you want to be:
8
+
9
+ - **Level 1 – Query builder & hydration 🧩**
10
+ Define tables with `defineTable` / `col.*`, build strongly-typed queries on a real SQL AST, and hydrate flat result sets into nested objects – no ORM runtime involved.
11
+ - **Level 2 – ORM runtime (entities + Unit of Work 🧠)**
12
+ Let `OrmSession` (created from `Orm`) turn rows into tracked entities with lazy relations, cascades, and a [Unit of Work](https://en.wikipedia.org/wiki/Unit_of_work) that flushes changes with `session.commit()`.
13
+ - **Level 3 – Decorator entities (classes + metadata ✨)**
14
+ Use `@Entity`, `@Column`, `@PrimaryKey`, relation decorators, `bootstrapEntities()` and `selectFromEntity()` to describe your model classes. MetalORM bootstraps schema & relations from metadata and plugs them into the same runtime and query builder.
15
+
16
+ Use only the layer you need in each part of your codebase.
17
+
18
+ ---
19
+
20
+ <a id="table-of-contents"></a>
21
+ ## Table of Contents 🧭
22
+
23
+ - [Documentation](#documentation)
24
+ - [Features](#features)
25
+ - [Installation](#installation)
26
+ - [Quick start - three levels](#quick-start)
27
+ - [Level 1 – Query builder & hydration](#level-1)
28
+ - [Level 2 – Entities + Unit of Work](#level-2)
29
+ - [Level 3 – Decorator entities](#level-3)
30
+ - [When to use which level?](#when-to-use-which-level)
31
+ - [Design notes](#design-notes)
32
+ - [Contributing](#contributing)
33
+ - [License](#license)
34
+
35
+ ---
36
+
37
+ <a id="documentation"></a>
38
+ ## Documentation 📚
39
+
40
+ Full docs live in the `docs/` folder:
41
+
42
+ - [Introduction](https://github.com/celsowm/metal-orm/blob/main/docs/index.md)
43
+ - [Getting Started](https://github.com/celsowm/metal-orm/blob/main/docs/getting-started.md)
44
+ - [Level 3 Backend Tutorial](https://github.com/celsowm/metal-orm/blob/main/docs/level-3-backend-tutorial.md)
45
+ - [Schema Definition](https://github.com/celsowm/metal-orm/blob/main/docs/schema-definition.md)
46
+ - [Query Builder](https://github.com/celsowm/metal-orm/blob/main/docs/query-builder.md)
47
+ - [DML Operations](https://github.com/celsowm/metal-orm/blob/main/docs/dml-operations.md)
48
+ - [Hydration & Entities](https://github.com/celsowm/metal-orm/blob/main/docs/hydration.md)
49
+ - [Runtime & Unit of Work](https://github.com/celsowm/metal-orm/blob/main/docs/runtime.md)
50
+ - [Advanced Features](https://github.com/celsowm/metal-orm/blob/main/docs/advanced-features.md)
51
+ - [Multi-Dialect Support](https://github.com/celsowm/metal-orm/blob/main/docs/multi-dialect-support.md)
52
+ - [Schema Generation (DDL)](https://github.com/celsowm/metal-orm/blob/main/docs/schema-generation.md)
53
+ - [API Reference](https://github.com/celsowm/metal-orm/blob/main/docs/api-reference.md)
54
+
55
+ ---
56
+
57
+ <a id="features"></a>
58
+ ## Features 🚀
59
+
60
+ ### Level 1 – Query builder & hydration
61
+
62
+ - **Declarative schema definition** with `defineTable`, `col.*`, and typed relations.
63
+ - **Fluent query builder** over a real SQL AST
64
+ (`SelectQueryBuilder`, `InsertQueryBuilder`, `UpdateQueryBuilder`, `DeleteQueryBuilder`).
65
+ - **Advanced SQL**: CTEs, aggregates, window functions, subqueries, JSON, CASE, EXISTS.
66
+ - **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.
67
+ - **Set operations**: `union`, `unionAll`, `intersect`, `except` across all dialects (ORDER/LIMIT apply to the combined result; hydration is disabled for compound queries so rows are returned as-is without collapsing duplicates).
68
+ - **Expression builders**: `eq`, `and`, `or`, `between`, `inList`, `exists`, `jsonPath`, `caseWhen`, window functions like `rowNumber`, `rank`, `lag`, `lead`, etc., all backed by typed AST nodes.
69
+ - **Relation-aware hydration**: turn flat rows into nested objects (`user.posts`, `user.roles`, etc.) using a hydration plan derived from the AST metadata.
70
+ - **Multi-dialect**: compile once, run on MySQL/MariaDB, PostgreSQL, SQLite, or SQL Server via pluggable dialects.
71
+ - **DML**: type-safe INSERT / UPDATE / DELETE with `RETURNING` where supported.
72
+
73
+ Level 1 is ideal when you:
74
+
75
+ - Already have a domain model and just want a serious SQL builder.
76
+ - Want deterministic SQL (no magical query generation).
77
+ - Need to share the same AST across tooling (e.g. codegen, diagnostics, logging).
78
+
79
+ ### Level 2 – ORM runtime (`OrmSession`)
80
+
81
+ On top of the query builder, MetalORM ships a focused runtime managed by `Orm` and its request-scoped `OrmSession`s:
82
+
83
+ - **Entities inferred from your `TableDef`s** (no separate mapping file).
84
+ - **Lazy, batched relations**: `user.posts.load()`, `user.roles.syncByIds([...])`, etc.
85
+ - **Identity map**: the same row becomes the same entity instance within a session (see the [Identity map pattern](https://en.wikipedia.org/wiki/Identity_map_pattern)).
86
+ - **Unit of Work (`OrmSession`)** tracking New/Dirty/Removed entities and relation changes, inspired by the classic [Unit of Work pattern](https://en.wikipedia.org/wiki/Unit_of_work).
87
+ - **Graph persistence**: mutate a whole object graph and flush once with `session.commit()`.
88
+ - **Relation change processor** that knows how to deal with has-many and many-to-many pivot tables.
89
+ - **Interceptors**: `beforeFlush` / `afterFlush` hooks for cross-cutting concerns (auditing, multi-tenant filters, soft delete filters, etc.).
90
+ - **Domain events**: `addDomainEvent` and a DomainEventBus integrated into `session.commit()`, aligned with domain events from [Domain-driven design](https://en.wikipedia.org/wiki/Domain-driven_design).
91
+ - **JSON-safe entities**: relation wrappers hide internal references and implement `toJSON`, so `JSON.stringify` of hydrated entities works without circular reference errors.
92
+
93
+ Use this layer where:
94
+
95
+ - A request-scoped context fits (web/API handlers, jobs).
96
+ - You want change tracking, cascades, and relation helpers instead of manual SQL for every update.
97
+
98
+ ### Level 3 – Decorator entities
99
+
100
+ If you like explicit model classes, you can add a thin decorator layer on top of the same schema/runtime:
101
+
102
+ - `@Entity()` on a class to derive and register a table name (by default snake_case plural of the class name, with an optional `tableName` override).
103
+ - `@Column(...)` and `@PrimaryKey(...)` on properties; decorators collect column metadata and later build `TableDef`s from it.
104
+ - Relation decorators:
105
+ - `@HasMany({ target, foreignKey, ... })`
106
+ - `@HasOne({ target, foreignKey, ... })`
107
+ - `@BelongsTo({ target, foreignKey, ... })`
108
108
  - `@BelongsToMany({ target, pivotTable, ... })`
109
109
  - `bootstrapEntities()` scans metadata, builds `TableDef`s, wires relations with the same `hasOne` / `hasMany` / `belongsTo` / `belongsToMany` helpers you would use manually, and returns the resulting tables.
110
110
  - `selectFromEntity(MyEntity)` lets you start a `SelectQueryBuilder` directly from the class.
111
- - **Generate entities from an existing DB**: `node scripts/generate-entities.mjs --dialect=postgres --url=$DATABASE_URL --schema=public --out=src/entities.ts` introspects your schema and spits out `@Entity` / `@Column` classes you can immediately `bootstrapEntities()` with.
112
-
113
- You don’t have to use decorators, but when you do, you’re still on the same AST + dialect + runtime foundation.
114
-
115
- ---
116
-
117
- <a id="installation"></a>
118
- ## Installation 📦
119
-
120
- ```bash
121
- # npm
122
- npm install metal-orm
123
-
124
- # yarn
125
- yarn add metal-orm
126
-
127
- # pnpm
128
- pnpm add metal-orm
129
- ```
130
-
131
- MetalORM compiles SQL; you bring your own driver:
132
-
133
- | Dialect | Driver | Install |
134
- | ------------------ | --------- | ---------------------- |
135
- | MySQL / MariaDB | `mysql2` | `npm install mysql2` |
136
- | SQLite | `sqlite3` | `npm install sqlite3` |
137
- | PostgreSQL | `pg` | `npm install pg` |
138
- | SQL Server | `tedious` | `npm install tedious` |
139
-
140
- Pick the matching dialect (`MySqlDialect`, `SQLiteDialect`, `PostgresDialect`, `MSSQLDialect`) when compiling queries.
141
-
142
- > Drivers are declared as optional peer dependencies. Install only the ones you actually use in your project.
143
-
144
- ### Playground (optional) 🧪
145
-
146
- The React playground lives in `playground/` and is no longer part of the published package or its dependency tree. To run it locally:
147
-
148
- 1. `cd playground && npm install`
149
- 2. `npm run dev` (uses the root `vite.config.ts`)
150
-
151
- It boots against an in-memory SQLite database seeded from fixtures under `playground/shared/`.
152
-
153
- ---
154
-
155
- <a id="quick-start"></a>
156
- ## Quick start – three levels
157
-
158
- <a id="level-1"></a>
159
- ### Level 1: Query builder & hydration 🧩
160
-
161
- #### 1. Tiny table, tiny query
162
-
163
- MetalORM can be just a straightforward query builder.
164
-
165
- ```ts
166
- import mysql from 'mysql2/promise';
167
- import {
168
- defineTable,
169
- col,
170
- SelectQueryBuilder,
171
- eq,
172
- MySqlDialect,
173
- } from 'metal-orm';
174
-
175
- // 1) A very small table
176
- const todos = defineTable('todos', {
177
- id: col.primaryKey(col.int()),
178
- title: col.varchar(255),
179
- done: col.boolean(),
180
- });
181
- // Add constraints
182
- todos.columns.title.notNull = true;
183
- todos.columns.done.default = false;
184
-
185
- // 2) Build a simple query
186
- const listOpenTodos = new SelectQueryBuilder(todos)
187
- .select({
188
- id: todos.columns.id,
189
- title: todos.columns.title,
190
- done: todos.columns.done,
191
- })
192
- .where(eq(todos.columns.done, false))
193
- .orderBy(todos.columns.id, 'ASC');
194
-
195
- // 3) Compile to SQL + params
196
- const dialect = new MySqlDialect();
197
- const { sql, params } = listOpenTodos.compile(dialect);
198
-
199
- // 4) Run with your favorite driver
200
- const connection = await mysql.createConnection({ /* ... */ });
201
- const [rows] = await connection.execute(sql, params);
202
-
203
- console.log(rows);
204
- // [
205
- // { id: 1, title: 'Write docs', done: 0 },
206
- // { id: 2, title: 'Ship feature', done: 0 },
207
- // ]
208
- ```
209
-
210
- That’s it: schema, query, SQL, done.
211
-
212
- #### 2. Relations & hydration (still no ORM)
213
-
214
- Now add relations and get nested objects, still without committing to a runtime.
215
-
216
- ```ts
217
- import {
218
- defineTable,
219
- col,
220
- hasMany,
221
- SelectQueryBuilder,
222
- eq,
223
- count,
224
- rowNumber,
225
- hydrateRows,
226
- } from 'metal-orm';
227
-
228
- const posts = defineTable('posts', {
229
- id: col.primaryKey(col.int()),
230
- title: col.varchar(255),
231
- userId: col.int(),
232
- createdAt: col.timestamp(),
233
- });
234
-
235
- // Add constraints
236
- posts.columns.title.notNull = true;
237
- posts.columns.userId.notNull = true;
238
-
239
- const users = defineTable('users', {
240
- id: col.primaryKey(col.int()),
241
- name: col.varchar(255),
242
- email: col.varchar(255),
243
- });
244
-
245
- // Add relations and constraints
246
- users.relations = {
247
- posts: hasMany(posts, 'userId'),
248
- };
249
- users.columns.name.notNull = true;
250
- users.columns.email.unique = true;
251
-
252
- // Build a query with relation & window function
253
- const builder = new SelectQueryBuilder(users)
254
- .select({
255
- id: users.columns.id,
256
- name: users.columns.name,
257
- email: users.columns.email,
258
- postCount: count(posts.columns.id),
259
- rank: rowNumber(), // window function helper
260
- })
261
- .leftJoin(posts, eq(posts.columns.userId, users.columns.id))
262
- .groupBy(users.columns.id, users.columns.name, users.columns.email)
263
- .orderBy(count(posts.columns.id), 'DESC')
264
- .limit(10)
265
- .include('posts', {
266
- columns: [posts.columns.id, posts.columns.title, posts.columns.createdAt],
267
- }); // eager relation for hydration
268
-
269
- const { sql, params } = builder.compile(dialect);
270
- const [rows] = await connection.execute(sql, params);
271
-
272
- // Turn flat rows into nested objects
273
- const hydrated = hydrateRows(
274
- rows as Record<string, unknown>[],
275
- builder.getHydrationPlan(),
276
- );
277
-
278
- console.log(hydrated);
279
- // [
280
- // {
281
- // id: 1,
282
- // name: 'John Doe',
283
- // email: 'john@example.com',
284
- // postCount: 15,
285
- // rank: 1,
286
- // posts: [
287
- // { id: 101, title: 'Latest Post', createdAt: '2023-05-15T10:00:00Z' },
288
- // // ...
289
- // ],
290
- // },
291
- // // ...
292
- // ]
293
- ```
294
-
295
- Use this mode anywhere you want powerful SQL + nice nested results, without changing how you manage your models.
296
-
297
- <a id="level-2"></a>
298
- ### Level 2: Entities + Unit of Work (ORM runtime) 🧠
299
-
300
- When you're ready, you can let MetalORM manage entities and relations for you.
301
-
302
- Instead of “naked objects”, your queries can return entities attached to an `OrmSession`:
303
-
304
- ```ts
305
- import mysql from 'mysql2/promise';
306
- import {
307
- Orm,
308
- OrmSession,
309
- MySqlDialect,
310
- SelectQueryBuilder,
311
- eq,
312
- createMysqlExecutor,
313
- } from 'metal-orm';
314
-
315
- // 1) Create an Orm + session for this request
316
-
317
- const connection = await mysql.createConnection({ /* ... */ });
318
- const executor = createMysqlExecutor(connection);
319
- const orm = new Orm({
320
- dialect: new MySqlDialect(),
321
- executorFactory: {
322
- createExecutor: () => executor,
323
- createTransactionalExecutor: () => executor,
324
- },
325
- });
326
- const session = new OrmSession({ orm, executor });
327
-
328
- // 2) Load entities with lazy relations
111
+ - **Generate entities from an existing DB**: `npx metal-orm-gen -- --dialect=postgres --url=$DATABASE_URL --schema=public --out=src/entities.ts` introspects your schema and spits out `@Entity` / `@Column` classes you can immediately `bootstrapEntities()` with.
112
+
113
+ You don’t have to use decorators, but when you do, you’re still on the same AST + dialect + runtime foundation.
114
+
115
+ ---
116
+
117
+ <a id="installation"></a>
118
+ ## Installation 📦
119
+
120
+ ```bash
121
+ # npm
122
+ npm install metal-orm
123
+
124
+ # yarn
125
+ yarn add metal-orm
126
+
127
+ # pnpm
128
+ pnpm add metal-orm
129
+ ```
130
+
131
+ MetalORM compiles SQL; you bring your own driver:
132
+
133
+ | Dialect | Driver | Install |
134
+ | ------------------ | --------- | ---------------------- |
135
+ | MySQL / MariaDB | `mysql2` | `npm install mysql2` |
136
+ | SQLite | `sqlite3` | `npm install sqlite3` |
137
+ | PostgreSQL | `pg` | `npm install pg` |
138
+ | SQL Server | `tedious` | `npm install tedious` |
139
+
140
+ Pick the matching dialect (`MySqlDialect`, `SQLiteDialect`, `PostgresDialect`, `MSSQLDialect`) when compiling queries.
141
+
142
+ > Drivers are declared as optional peer dependencies. Install only the ones you actually use in your project.
143
+
144
+ ### Playground (optional) 🧪
145
+
146
+ The React playground lives in `playground/` and is no longer part of the published package or its dependency tree. To run it locally:
147
+
148
+ 1. `cd playground && npm install`
149
+ 2. `npm run dev` (uses the root `vite.config.ts`)
150
+
151
+ It boots against an in-memory SQLite database seeded from fixtures under `playground/shared/`.
152
+
153
+ ---
154
+
155
+ <a id="quick-start"></a>
156
+ ## Quick start – three levels
157
+
158
+ <a id="level-1"></a>
159
+ ### Level 1: Query builder & hydration 🧩
160
+
161
+ #### 1. Tiny table, tiny query
162
+
163
+ MetalORM can be just a straightforward query builder.
164
+
165
+ ```ts
166
+ import mysql from 'mysql2/promise';
167
+ import {
168
+ defineTable,
169
+ col,
170
+ SelectQueryBuilder,
171
+ eq,
172
+ MySqlDialect,
173
+ } from 'metal-orm';
174
+
175
+ // 1) A very small table
176
+ const todos = defineTable('todos', {
177
+ id: col.primaryKey(col.int()),
178
+ title: col.varchar(255),
179
+ done: col.boolean(),
180
+ });
181
+ // Add constraints
182
+ todos.columns.title.notNull = true;
183
+ todos.columns.done.default = false;
184
+
185
+ // 2) Build a simple query
186
+ const listOpenTodos = new SelectQueryBuilder(todos)
187
+ .select({
188
+ id: todos.columns.id,
189
+ title: todos.columns.title,
190
+ done: todos.columns.done,
191
+ })
192
+ .where(eq(todos.columns.done, false))
193
+ .orderBy(todos.columns.id, 'ASC');
194
+
195
+ // 3) Compile to SQL + params
196
+ const dialect = new MySqlDialect();
197
+ const { sql, params } = listOpenTodos.compile(dialect);
198
+
199
+ // 4) Run with your favorite driver
200
+ const connection = await mysql.createConnection({ /* ... */ });
201
+ const [rows] = await connection.execute(sql, params);
202
+
203
+ console.log(rows);
204
+ // [
205
+ // { id: 1, title: 'Write docs', done: 0 },
206
+ // { id: 2, title: 'Ship feature', done: 0 },
207
+ // ]
208
+ ```
209
+
210
+ That’s it: schema, query, SQL, done.
211
+
212
+ #### 2. Relations & hydration (still no ORM)
213
+
214
+ Now add relations and get nested objects, still without committing to a runtime.
215
+
216
+ ```ts
217
+ import {
218
+ defineTable,
219
+ col,
220
+ hasMany,
221
+ SelectQueryBuilder,
222
+ eq,
223
+ count,
224
+ rowNumber,
225
+ hydrateRows,
226
+ } from 'metal-orm';
227
+
228
+ const posts = defineTable('posts', {
229
+ id: col.primaryKey(col.int()),
230
+ title: col.varchar(255),
231
+ userId: col.int(),
232
+ createdAt: col.timestamp(),
233
+ });
234
+
235
+ // Add constraints
236
+ posts.columns.title.notNull = true;
237
+ posts.columns.userId.notNull = true;
238
+
239
+ const users = defineTable('users', {
240
+ id: col.primaryKey(col.int()),
241
+ name: col.varchar(255),
242
+ email: col.varchar(255),
243
+ });
244
+
245
+ // Add relations and constraints
246
+ users.relations = {
247
+ posts: hasMany(posts, 'userId'),
248
+ };
249
+ users.columns.name.notNull = true;
250
+ users.columns.email.unique = true;
251
+
252
+ // Build a query with relation & window function
253
+ const builder = new SelectQueryBuilder(users)
254
+ .select({
255
+ id: users.columns.id,
256
+ name: users.columns.name,
257
+ email: users.columns.email,
258
+ postCount: count(posts.columns.id),
259
+ rank: rowNumber(), // window function helper
260
+ })
261
+ .leftJoin(posts, eq(posts.columns.userId, users.columns.id))
262
+ .groupBy(users.columns.id, users.columns.name, users.columns.email)
263
+ .orderBy(count(posts.columns.id), 'DESC')
264
+ .limit(10)
265
+ .include('posts', {
266
+ columns: [posts.columns.id, posts.columns.title, posts.columns.createdAt],
267
+ }); // eager relation for hydration
268
+
269
+ const { sql, params } = builder.compile(dialect);
270
+ const [rows] = await connection.execute(sql, params);
271
+
272
+ // Turn flat rows into nested objects
273
+ const hydrated = hydrateRows(
274
+ rows as Record<string, unknown>[],
275
+ builder.getHydrationPlan(),
276
+ );
277
+
278
+ console.log(hydrated);
279
+ // [
280
+ // {
281
+ // id: 1,
282
+ // name: 'John Doe',
283
+ // email: 'john@example.com',
284
+ // postCount: 15,
285
+ // rank: 1,
286
+ // posts: [
287
+ // { id: 101, title: 'Latest Post', createdAt: '2023-05-15T10:00:00Z' },
288
+ // // ...
289
+ // ],
290
+ // },
291
+ // // ...
292
+ // ]
293
+ ```
294
+
295
+ Use this mode anywhere you want powerful SQL + nice nested results, without changing how you manage your models.
296
+
297
+ <a id="level-2"></a>
298
+ ### Level 2: Entities + Unit of Work (ORM runtime) 🧠
299
+
300
+ When you're ready, you can let MetalORM manage entities and relations for you.
301
+
302
+ Instead of “naked objects”, your queries can return entities attached to an `OrmSession`:
303
+
304
+ ```ts
305
+ import mysql from 'mysql2/promise';
306
+ import {
307
+ Orm,
308
+ OrmSession,
309
+ MySqlDialect,
310
+ SelectQueryBuilder,
311
+ eq,
312
+ createMysqlExecutor,
313
+ } from 'metal-orm';
314
+
315
+ // 1) Create an Orm + session for this request
316
+
317
+ const connection = await mysql.createConnection({ /* ... */ });
318
+ const executor = createMysqlExecutor(connection);
319
+ const orm = new Orm({
320
+ dialect: new MySqlDialect(),
321
+ executorFactory: {
322
+ createExecutor: () => executor,
323
+ createTransactionalExecutor: () => executor,
324
+ },
325
+ });
326
+ const session = new OrmSession({ orm, executor });
327
+
328
+ // 2) Load entities with lazy relations
329
329
  const [user] = await new SelectQueryBuilder(users)
330
330
  .selectColumns('id', 'name', 'email')
331
331
  .includeLazy('posts') // HasMany as a lazy collection
332
332
  .includeLazy('roles') // BelongsToMany as a lazy collection
333
333
  .where(eq(users.columns.id, 1))
334
334
  .execute(session);
335
-
336
- // user is an Entity<typeof users>
337
- // scalar props are normal:
338
- user.name = 'Updated Name'; // marks entity as Dirty
339
-
340
- // relations are live collections:
341
- const postsCollection = await user.posts.load(); // batched lazy load
342
- const newPost = user.posts.add({ title: 'Hello from ORM mode' });
343
-
344
- // Many-to-many via pivot:
345
- await user.roles.syncByIds([1, 2, 3]);
346
-
347
- // 3) Persist the entire graph
348
- await session.commit();
349
- // INSERT/UPDATE/DELETE + pivot updates happen in a single Unit of Work.
350
- ```
351
-
352
- What the runtime gives you:
353
-
354
- - [Identity map](https://en.wikipedia.org/wiki/Identity_map_pattern) (per context).
335
+
336
+ // user is an Entity<typeof users>
337
+ // scalar props are normal:
338
+ user.name = 'Updated Name'; // marks entity as Dirty
339
+
340
+ // relations are live collections:
341
+ const postsCollection = await user.posts.load(); // batched lazy load
342
+ const newPost = user.posts.add({ title: 'Hello from ORM mode' });
343
+
344
+ // Many-to-many via pivot:
345
+ await user.roles.syncByIds([1, 2, 3]);
346
+
347
+ // 3) Persist the entire graph
348
+ await session.commit();
349
+ // INSERT/UPDATE/DELETE + pivot updates happen in a single Unit of Work.
350
+ ```
351
+
352
+ What the runtime gives you:
353
+
354
+ - [Identity map](https://en.wikipedia.org/wiki/Identity_map_pattern) (per context).
355
355
  - [Unit of Work](https://en.wikipedia.org/wiki/Unit_of_work) style change tracking on scalar properties.
356
356
  - Relation tracking (add/remove/sync on collections).
357
357
  - Cascades on relations: `'all' | 'persist' | 'remove' | 'link'`.
358
358
  - Single flush: `session.commit()` figures out inserts, updates, deletes, and pivot changes.
359
359
  - Column pickers to stay DRY: `selectColumns` on the root table, `selectRelationColumns` / `includePick` on relations, and `selectColumnsDeep` or the `sel`/`esel` helpers to build typed selection maps without repeating `table.columns.*`.
360
-
361
- <a id="level-3"></a>
362
- ### Level 3: Decorator entities ✨
363
-
364
- Finally, you can describe your models with decorators and still use the same runtime and query builder.
365
-
366
- > Import paths here assume a `metal-orm/decorators` subpath export – adjust if your bundle exposes them differently.
367
-
368
- ```ts
369
- import mysql from 'mysql2/promise';
370
- import { Orm, OrmSession, MySqlDialect, col, createMysqlExecutor } from 'metal-orm';
360
+
361
+ <a id="level-3"></a>
362
+ ### Level 3: Decorator entities ✨
363
+
364
+ Finally, you can describe your models with decorators and still use the same runtime and query builder.
365
+
366
+ > Import paths here assume a `metal-orm/decorators` subpath export – adjust if your bundle exposes them differently.
367
+
368
+ ```ts
369
+ import mysql from 'mysql2/promise';
370
+ import { Orm, OrmSession, MySqlDialect, col, createMysqlExecutor } from 'metal-orm';
371
371
  import {
372
372
  Entity,
373
373
  Column,
374
374
  PrimaryKey,
375
375
  HasMany,
376
- BelongsTo,
377
- bootstrapEntities,
378
- selectFromEntity,
379
- } from 'metal-orm/decorators';
380
-
381
- @Entity()
382
- class User {
383
- @PrimaryKey(col.int())
384
- id!: number;
385
-
386
- @Column(col.varchar(255))
387
- name!: string;
388
-
389
- @Column(col.varchar(255))
390
- email?: string;
391
-
392
- @HasMany({
393
- target: () => Post,
394
- foreignKey: 'userId',
395
- })
396
- posts!: any; // relation wrapper; type omitted for brevity
397
- }
398
-
399
- @Entity()
400
- class Post {
401
- @PrimaryKey(col.int())
402
- id!: number;
403
-
404
- @Column(col.varchar(255))
405
- title!: string;
406
-
407
- @Column(col.int())
408
- userId!: number;
409
-
410
- @BelongsTo({
411
- target: () => User,
412
- foreignKey: 'userId',
413
- })
414
- user!: any;
415
- }
416
-
417
- // 1) Bootstrap metadata once at startup
418
- const tables = bootstrapEntities();
419
- // tables: TableDef[] – compatible with the rest of MetalORM
420
-
421
- // 2) Create an Orm + session
422
- const connection = await mysql.createConnection({ /* ... */ });
423
- const executor = createMysqlExecutor(connection);
424
- const orm = new Orm({
425
- dialect: new MySqlDialect(),
426
- executorFactory: {
427
- createExecutor: () => executor,
428
- createTransactionalExecutor: () => executor,
429
- },
430
- });
431
- const session = new OrmSession({ orm, executor });
432
-
376
+ BelongsTo,
377
+ bootstrapEntities,
378
+ selectFromEntity,
379
+ } from 'metal-orm/decorators';
380
+
381
+ @Entity()
382
+ class User {
383
+ @PrimaryKey(col.int())
384
+ id!: number;
385
+
386
+ @Column(col.varchar(255))
387
+ name!: string;
388
+
389
+ @Column(col.varchar(255))
390
+ email?: string;
391
+
392
+ @HasMany({
393
+ target: () => Post,
394
+ foreignKey: 'userId',
395
+ })
396
+ posts!: any; // relation wrapper; type omitted for brevity
397
+ }
398
+
399
+ @Entity()
400
+ class Post {
401
+ @PrimaryKey(col.int())
402
+ id!: number;
403
+
404
+ @Column(col.varchar(255))
405
+ title!: string;
406
+
407
+ @Column(col.int())
408
+ userId!: number;
409
+
410
+ @BelongsTo({
411
+ target: () => User,
412
+ foreignKey: 'userId',
413
+ })
414
+ user!: any;
415
+ }
416
+
417
+ // 1) Bootstrap metadata once at startup
418
+ const tables = bootstrapEntities();
419
+ // tables: TableDef[] – compatible with the rest of MetalORM
420
+
421
+ // 2) Create an Orm + session
422
+ const connection = await mysql.createConnection({ /* ... */ });
423
+ const executor = createMysqlExecutor(connection);
424
+ const orm = new Orm({
425
+ dialect: new MySqlDialect(),
426
+ executorFactory: {
427
+ createExecutor: () => executor,
428
+ createTransactionalExecutor: () => executor,
429
+ },
430
+ });
431
+ const session = new OrmSession({ orm, executor });
432
+
433
433
  // 3) Query starting from the entity class
434
434
  const [user] = await selectFromEntity(User)
435
435
  .selectColumns('id', 'name')
@@ -447,50 +447,50 @@ This level is nice when:
447
447
 
448
448
  - You want classes as your domain model, but don't want a separate schema DSL.
449
449
  - You like decorators for explicit mapping but still want AST-first SQL and a disciplined runtime.
450
-
451
- ---
452
-
453
- <a id="when-to-use-which-level"></a>
454
- ## When to use which level? 🤔
455
-
456
- - **Query builder + hydration (Level 1)**
457
- Great for reporting/analytics, existing codebases with their own models, and services that need strong SQL but minimal runtime magic.
458
-
459
- - **ORM runtime (Level 2)**
460
- Great for request-scoped application logic and domain modeling where lazy relations, cascades, and graph persistence pay off.
461
-
462
- - **Decorator entities (Level 3)**
463
- Great when you want class-based entities and decorators, but still want to keep the underlying architecture explicit and layered.
464
-
465
- All three levels share the same schema, AST, and dialects, so you can mix them as needed and migrate gradually.
466
-
467
- ---
468
-
469
- <a id="design-notes"></a>
470
- ## Design notes 🧱
471
-
472
- Under the hood, MetalORM leans on well-known patterns:
473
-
474
- - **AST + dialect abstraction**: SQL is modeled as typed AST nodes, compiled by dialects that you can extend.
475
- - **Separation of concerns**: schema, AST, SQL compilation, execution, and ORM runtime are separate layers.
476
- - **Executor abstraction**: built-in executor creators (`createMysqlExecutor`, `createPostgresExecutor`, etc.) provide a clean separation between database drivers and ORM operations.
477
- - **Unit of Work + Identity Map**: `OrmSession` coordinates changes and enforces one entity instance per row, following the [Unit of Work](https://en.wikipedia.org/wiki/Unit_of_work) and [Identity map](https://en.wikipedia.org/wiki/Identity_map_pattern) patterns.
478
- - **Domain events + interceptors**: decouple side-effects from persistence and let cross-cutting concerns hook into flush points, similar in spirit to domain events in [Domain-driven design](https://en.wikipedia.org/wiki/Domain-driven_design).
479
-
480
- You can stay at the low level (just AST + dialects) or adopt the higher levels when it makes your code simpler.
481
-
482
- ---
483
-
484
- <a id="contributing"></a>
485
- ## Contributing 🤝
486
-
487
- 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.
488
-
489
- See the contributing guide for details.
490
-
491
- ---
492
-
493
- <a id="license"></a>
494
- ## License 📄
495
-
496
- MetalORM is MIT licensed.
450
+
451
+ ---
452
+
453
+ <a id="when-to-use-which-level"></a>
454
+ ## When to use which level? 🤔
455
+
456
+ - **Query builder + hydration (Level 1)**
457
+ Great for reporting/analytics, existing codebases with their own models, and services that need strong SQL but minimal runtime magic.
458
+
459
+ - **ORM runtime (Level 2)**
460
+ Great for request-scoped application logic and domain modeling where lazy relations, cascades, and graph persistence pay off.
461
+
462
+ - **Decorator entities (Level 3)**
463
+ Great when you want class-based entities and decorators, but still want to keep the underlying architecture explicit and layered.
464
+
465
+ All three levels share the same schema, AST, and dialects, so you can mix them as needed and migrate gradually.
466
+
467
+ ---
468
+
469
+ <a id="design-notes"></a>
470
+ ## Design notes 🧱
471
+
472
+ Under the hood, MetalORM leans on well-known patterns:
473
+
474
+ - **AST + dialect abstraction**: SQL is modeled as typed AST nodes, compiled by dialects that you can extend.
475
+ - **Separation of concerns**: schema, AST, SQL compilation, execution, and ORM runtime are separate layers.
476
+ - **Executor abstraction**: built-in executor creators (`createMysqlExecutor`, `createPostgresExecutor`, etc.) provide a clean separation between database drivers and ORM operations.
477
+ - **Unit of Work + Identity Map**: `OrmSession` coordinates changes and enforces one entity instance per row, following the [Unit of Work](https://en.wikipedia.org/wiki/Unit_of_work) and [Identity map](https://en.wikipedia.org/wiki/Identity_map_pattern) patterns.
478
+ - **Domain events + interceptors**: decouple side-effects from persistence and let cross-cutting concerns hook into flush points, similar in spirit to domain events in [Domain-driven design](https://en.wikipedia.org/wiki/Domain-driven_design).
479
+
480
+ You can stay at the low level (just AST + dialects) or adopt the higher levels when it makes your code simpler.
481
+
482
+ ---
483
+
484
+ <a id="contributing"></a>
485
+ ## Contributing 🤝
486
+
487
+ 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.
488
+
489
+ See the contributing guide for details.
490
+
491
+ ---
492
+
493
+ <a id="license"></a>
494
+ ## License 📄
495
+
496
+ MetalORM is MIT licensed.