metal-orm 1.0.5 → 1.0.7
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 +299 -113
- package/docs/CHANGES.md +104 -0
- package/docs/advanced-features.md +92 -1
- package/docs/api-reference.md +13 -4
- package/docs/dml-operations.md +156 -0
- package/docs/getting-started.md +122 -55
- package/docs/hydration.md +78 -13
- package/docs/index.md +19 -14
- package/docs/multi-dialect-support.md +25 -0
- package/docs/query-builder.md +60 -0
- package/docs/runtime.md +105 -0
- package/docs/schema-definition.md +52 -1
- package/package.json +1 -1
- package/src/ast/expression.ts +38 -18
- package/src/builder/hydration-planner.ts +74 -74
- package/src/builder/select.ts +427 -395
- package/src/constants/sql-operator-config.ts +3 -0
- package/src/constants/sql.ts +38 -32
- package/src/index.ts +16 -8
- package/src/playground/features/playground/data/scenarios/types.ts +18 -15
- package/src/playground/features/playground/data/schema.ts +10 -10
- package/src/playground/features/playground/services/QueryExecutionService.ts +2 -1
- package/src/runtime/entity-meta.ts +52 -0
- package/src/runtime/entity.ts +252 -0
- package/src/runtime/execute.ts +36 -0
- package/src/runtime/hydration.ts +99 -49
- package/src/runtime/lazy-batch.ts +205 -0
- package/src/runtime/orm-context.ts +539 -0
- package/src/runtime/relations/belongs-to.ts +92 -0
- package/src/runtime/relations/has-many.ts +111 -0
- package/src/runtime/relations/many-to-many.ts +149 -0
- package/src/schema/column.ts +15 -1
- package/src/schema/relation.ts +82 -58
- package/src/schema/table.ts +34 -22
- package/src/schema/types.ts +76 -0
- package/tests/orm-runtime.test.ts +254 -0
package/README.md
CHANGED
|
@@ -1,113 +1,299 @@
|
|
|
1
|
-
# MetalORM
|
|
2
|
-
|
|
3
|
-
[](https://www.npmjs.com/package/metal-orm)
|
|
4
|
-
[](https://github.com/celsowm/metal-orm/blob/main/LICENSE)
|
|
5
|
-
[](https://www.typescriptlang.org/)
|
|
6
|
-
|
|
7
|
-
**
|
|
8
|
-
|
|
9
|
-
MetalORM
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
1
|
+
# MetalORM
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/metal-orm)
|
|
4
|
+
[](https://github.com/celsowm/metal-orm/blob/main/LICENSE)
|
|
5
|
+
[](https://www.typescriptlang.org/)
|
|
6
|
+
|
|
7
|
+
**Start as a type-safe SQL query builder. Grow into a full ORM with entities and a Unit of Work.**
|
|
8
|
+
|
|
9
|
+
MetalORM is a TypeScript-first, AST-driven SQL toolkit:
|
|
10
|
+
|
|
11
|
+
- At the **base level**, it's a clear, deterministic query builder with schema definitions and relation-aware hydration.:contentReference[oaicite:0]{index=0}
|
|
12
|
+
- At the **next level**, it becomes an **ORM runtime** with entities, lazy/batched relations, and a Unit of Work (`OrmContext`) that flushes graph changes in one `saveChanges()`.
|
|
13
|
+
|
|
14
|
+
Use only the parts you need: query builder + hydration for read-heavy/reporting code, or the full ORM runtime for application/business logic.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Documentation
|
|
19
|
+
|
|
20
|
+
Full docs live in the `docs/` folder:
|
|
21
|
+
|
|
22
|
+
- [Introduction](docs/index.md)
|
|
23
|
+
- [Getting Started](docs/getting-started.md)
|
|
24
|
+
- [Schema Definition](docs/schema-definition.md)
|
|
25
|
+
- [Query Builder](docs/query-builder.md)
|
|
26
|
+
- [DML Operations](docs/dml-operations.md)
|
|
27
|
+
- [Hydration & Entities](docs/hydration.md)
|
|
28
|
+
- [Runtime & Unit of Work](docs/runtime.md)
|
|
29
|
+
- [Advanced Features](docs/advanced-features.md)
|
|
30
|
+
- [Multi-Dialect Support](docs/multi-dialect-support.md)
|
|
31
|
+
- [API Reference](docs/api-reference.md)
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Features
|
|
36
|
+
|
|
37
|
+
**As a query builder:**
|
|
38
|
+
|
|
39
|
+
- **Declarative schema definition** with `defineTable`, `col.*`, and typed relations.:contentReference[oaicite:1]{index=1}
|
|
40
|
+
- **Fluent query builder** over a real SQL AST (`SelectQueryBuilder`, `InsertQueryBuilder`, `UpdateQueryBuilder`, `DeleteQueryBuilder`).
|
|
41
|
+
- **Advanced SQL**: CTEs, aggregates, window functions, subqueries, JSON, CASE, EXISTS.
|
|
42
|
+
- **Relation hydration**: turn flat rows into nested objects (`user.posts`, `user.roles`, etc.).:contentReference[oaicite:4]{index=4}
|
|
43
|
+
- **Multi-dialect**: compile once, run on MySQL, PostgreSQL, SQLite, or SQL Server.:contentReference[oaicite:5]{index=5}
|
|
44
|
+
- **DML**: type-safe INSERT / UPDATE / DELETE with `RETURNING` where supported.:contentReference[oaicite:6]{index=6}
|
|
45
|
+
|
|
46
|
+
**As an ORM runtime (optional):**
|
|
47
|
+
|
|
48
|
+
- **Entities** inferred from your `TableDef`s.
|
|
49
|
+
- **Lazy, batched relations**: `user.posts.load()`, `user.roles.load()`, etc.
|
|
50
|
+
- **Unit of Work (`OrmContext`)** tracking New/Dirty/Removed entities.
|
|
51
|
+
- **Graph persistence**: modify a whole object graph and flush with `ctx.saveChanges()`.
|
|
52
|
+
- **Hooks & domain events** for cross-cutting concerns (audit, outbox, etc.).
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Installation
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
# npm
|
|
60
|
+
npm install metal-orm
|
|
61
|
+
|
|
62
|
+
# yarn
|
|
63
|
+
yarn add metal-orm
|
|
64
|
+
|
|
65
|
+
# pnpm
|
|
66
|
+
pnpm add metal-orm
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
MetalORM compiles SQL; you bring your own driver:
|
|
70
|
+
|
|
71
|
+
Dialect Driver Install
|
|
72
|
+
MySQL / MariaDB mysql2 npm install mysql2
|
|
73
|
+
SQLite sqlite3 npm install sqlite3
|
|
74
|
+
PostgreSQL pg npm install pg
|
|
75
|
+
SQL Server tedious npm install tedious
|
|
76
|
+
|
|
77
|
+
Pick the matching dialect (MySqlDialect, SQLiteDialect, PostgresDialect, MSSQLDialect) when compiling queries.
|
|
78
|
+
|
|
79
|
+
README
|
|
80
|
+
|
|
81
|
+
1. Start simple: tiny table, tiny query
|
|
82
|
+
|
|
83
|
+
MetalORM can be just a straightforward query builder.
|
|
84
|
+
|
|
85
|
+
import mysql from 'mysql2/promise';
|
|
86
|
+
import {
|
|
87
|
+
defineTable,
|
|
88
|
+
col,
|
|
89
|
+
SelectQueryBuilder,
|
|
90
|
+
eq,
|
|
91
|
+
MySqlDialect,
|
|
92
|
+
} from 'metal-orm';
|
|
93
|
+
|
|
94
|
+
// 1) A very small table
|
|
95
|
+
const todos = defineTable('todos', {
|
|
96
|
+
id: col.int().primaryKey(),
|
|
97
|
+
title: col.varchar(255).notNull(),
|
|
98
|
+
done: col.boolean().default(false),
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// 2) Build a simple query
|
|
102
|
+
const listOpenTodos = new SelectQueryBuilder(todos)
|
|
103
|
+
.select({
|
|
104
|
+
id: todos.columns.id,
|
|
105
|
+
title: todos.columns.title,
|
|
106
|
+
done: todos.columns.done,
|
|
107
|
+
})
|
|
108
|
+
.where(eq(todos.columns.done, false))
|
|
109
|
+
.orderBy(todos.columns.id, 'ASC');
|
|
110
|
+
|
|
111
|
+
// 3) Compile to SQL + params
|
|
112
|
+
const dialect = new MySqlDialect();
|
|
113
|
+
const { sql, params } = listOpenTodos.compile(dialect);
|
|
114
|
+
|
|
115
|
+
// 4) Run with your favorite driver
|
|
116
|
+
const connection = await mysql.createConnection({ /* ... */ });
|
|
117
|
+
const [rows] = await connection.execute(sql, params);
|
|
118
|
+
|
|
119
|
+
console.log(rows);
|
|
120
|
+
// [
|
|
121
|
+
// { id: 1, title: 'Write docs', done: 0 },
|
|
122
|
+
// { id: 2, title: 'Ship feature', done: 0 },
|
|
123
|
+
// ]
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
That’s it: schema, query, SQL, done.
|
|
127
|
+
|
|
128
|
+
2. Relations & hydration: nested results without an ORM
|
|
129
|
+
|
|
130
|
+
Now let’s add relations and get nested objects, still without committing to a full ORM.
|
|
131
|
+
|
|
132
|
+
import {
|
|
133
|
+
defineTable,
|
|
134
|
+
col,
|
|
135
|
+
hasMany,
|
|
136
|
+
SelectQueryBuilder,
|
|
137
|
+
eq,
|
|
138
|
+
count,
|
|
139
|
+
rowNumber,
|
|
140
|
+
hydrateRows,
|
|
141
|
+
} from 'metal-orm';
|
|
142
|
+
|
|
143
|
+
const posts = defineTable('posts', {
|
|
144
|
+
id: col.int().primaryKey(),
|
|
145
|
+
title: col.varchar(255).notNull(),
|
|
146
|
+
userId: col.int().notNull(),
|
|
147
|
+
createdAt: col.timestamp().default('CURRENT_TIMESTAMP'),
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const users = defineTable('users', {
|
|
151
|
+
id: col.int().primaryKey(),
|
|
152
|
+
name: col.varchar(255).notNull(),
|
|
153
|
+
email: col.varchar(255).unique(),
|
|
154
|
+
}, {
|
|
155
|
+
posts: hasMany(posts, 'userId'),
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Build a query with relation & window function
|
|
159
|
+
const builder = new SelectQueryBuilder(users)
|
|
160
|
+
.select({
|
|
161
|
+
id: users.columns.id,
|
|
162
|
+
name: users.columns.name,
|
|
163
|
+
email: users.columns.email,
|
|
164
|
+
postCount: count(posts.columns.id),
|
|
165
|
+
rank: rowNumber(), // window function helper
|
|
166
|
+
})
|
|
167
|
+
.leftJoin(posts, eq(posts.columns.userId, users.columns.id))
|
|
168
|
+
.groupBy(users.columns.id, users.columns.name, users.columns.email)
|
|
169
|
+
.orderBy(count(posts.columns.id), 'DESC')
|
|
170
|
+
.limit(10)
|
|
171
|
+
.include('posts', {
|
|
172
|
+
columns: [posts.columns.id, posts.columns.title, posts.columns.createdAt],
|
|
173
|
+
}); // eager relation for hydration
|
|
174
|
+
|
|
175
|
+
const { sql, params } = builder.compile(dialect);
|
|
176
|
+
const [rows] = await connection.execute(sql, params);
|
|
177
|
+
|
|
178
|
+
// Turn flat rows into nested objects
|
|
179
|
+
const hydrated = hydrateRows(
|
|
180
|
+
rows as Record<string, unknown>[],
|
|
181
|
+
builder.getHydrationPlan(),
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
console.log(hydrated);
|
|
185
|
+
// [
|
|
186
|
+
// {
|
|
187
|
+
// id: 1,
|
|
188
|
+
// name: 'John Doe',
|
|
189
|
+
// email: 'john@example.com',
|
|
190
|
+
// postCount: 15,
|
|
191
|
+
// rank: 1,
|
|
192
|
+
// posts: [
|
|
193
|
+
// { id: 101, title: 'Latest Post', createdAt: '2023-05-15T10:00:00Z' },
|
|
194
|
+
// // ...
|
|
195
|
+
// ],
|
|
196
|
+
// },
|
|
197
|
+
// // ...
|
|
198
|
+
// ]
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
Use this mode anywhere you want powerful SQL + nice nested results, without changing how you manage your models.
|
|
202
|
+
|
|
203
|
+
3. Turn it up: entities + Unit of Work (ORM mode)
|
|
204
|
+
|
|
205
|
+
When you’re ready, you can let MetalORM manage entities and relations for you.
|
|
206
|
+
|
|
207
|
+
Instead of “naked objects”, your queries can return entities attached to an OrmContext:
|
|
208
|
+
|
|
209
|
+
import {
|
|
210
|
+
OrmContext,
|
|
211
|
+
MySqlDialect,
|
|
212
|
+
SelectQueryBuilder,
|
|
213
|
+
eq,
|
|
214
|
+
} from 'metal-orm';
|
|
215
|
+
|
|
216
|
+
// 1) Create an OrmContext for this request
|
|
217
|
+
const ctx = new OrmContext({
|
|
218
|
+
dialect: new MySqlDialect(),
|
|
219
|
+
db: {
|
|
220
|
+
async executeSql(sql, params) {
|
|
221
|
+
const [rows] = await connection.execute(sql, params);
|
|
222
|
+
// MetalORM expects columns + values; adapt as needed
|
|
223
|
+
return [{
|
|
224
|
+
columns: Object.keys(rows[0] ?? {}),
|
|
225
|
+
values: rows.map(row => Object.values(row)),
|
|
226
|
+
}];
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// 2) Load entities with lazy relations
|
|
232
|
+
const [user] = await new SelectQueryBuilder(users)
|
|
233
|
+
.select({
|
|
234
|
+
id: users.columns.id,
|
|
235
|
+
name: users.columns.name,
|
|
236
|
+
email: users.columns.email,
|
|
237
|
+
})
|
|
238
|
+
.includeLazy('posts') // HasMany as a lazy collection
|
|
239
|
+
.includeLazy('roles') // BelongsToMany as a lazy collection
|
|
240
|
+
.where(eq(users.columns.id, 1))
|
|
241
|
+
.execute(ctx);
|
|
242
|
+
|
|
243
|
+
// user is an Entity<typeof users>
|
|
244
|
+
// scalar props are normal:
|
|
245
|
+
user.name = 'Updated Name'; // marks entity as Dirty
|
|
246
|
+
|
|
247
|
+
// relations are live collections:
|
|
248
|
+
const postsCollection = await user.posts.load(); // batched lazy load
|
|
249
|
+
const newPost = user.posts.add({ title: 'Hello from ORM mode' });
|
|
250
|
+
|
|
251
|
+
// Many-to-many via pivot:
|
|
252
|
+
await user.roles.syncByIds([1, 2, 3]);
|
|
253
|
+
|
|
254
|
+
// 3) Persist the entire graph
|
|
255
|
+
await ctx.saveChanges();
|
|
256
|
+
// INSERT/UPDATE/DELETE + pivot updates happen in a single Unit of Work.
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
Here’s what the runtime gives you:
|
|
260
|
+
|
|
261
|
+
Identity map: the same row ↔ the same entity instance within a context.
|
|
262
|
+
|
|
263
|
+
Change tracking: field writes mark entities as dirty.
|
|
264
|
+
|
|
265
|
+
Relation tracking: add/remove/sync on relation collections emits relation changes.
|
|
266
|
+
|
|
267
|
+
Cascades: relation definitions can opt into cascade: 'all' | 'persist' | 'remove' | 'link'.
|
|
268
|
+
|
|
269
|
+
Single flush: ctx.saveChanges() figures out inserts, updates, deletes, and pivot changes.
|
|
270
|
+
|
|
271
|
+
You can start your project using only the query builder + hydration and gradually migrate hot paths to entities as you implement the runtime primitives.
|
|
272
|
+
|
|
273
|
+
When to use which mode?
|
|
274
|
+
|
|
275
|
+
Query builder + hydration only
|
|
276
|
+
|
|
277
|
+
Great for reporting, analytics, and places where you already have a model layer.
|
|
278
|
+
|
|
279
|
+
You keep full control over how objects map to rows.
|
|
280
|
+
|
|
281
|
+
Entity + Unit of Work runtime
|
|
282
|
+
|
|
283
|
+
Great for request-scoped application logic and domain modeling.
|
|
284
|
+
|
|
285
|
+
You want lazy relations, cascades, and less boilerplate around update/delete logic.
|
|
286
|
+
|
|
287
|
+
Both modes share the same schema, AST, and dialects, so you don't have to pick one forever.
|
|
288
|
+
|
|
289
|
+
Contributing
|
|
290
|
+
|
|
291
|
+
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.
|
|
292
|
+
|
|
293
|
+
See the Contributing Guide
|
|
294
|
+
for details.
|
|
295
|
+
|
|
296
|
+
License
|
|
297
|
+
|
|
298
|
+
MetalORM is MIT licensed
|
|
299
|
+
.
|
package/docs/CHANGES.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Documentation Updates Summary
|
|
2
|
+
|
|
3
|
+
This document summarizes the major updates made to the MetalORM documentation to reflect all new features.
|
|
4
|
+
|
|
5
|
+
## New Documentation Files
|
|
6
|
+
|
|
7
|
+
### `dml-operations.md`
|
|
8
|
+
- **New File**: Comprehensive documentation for INSERT, UPDATE, and DELETE operations
|
|
9
|
+
- **Features Covered**:
|
|
10
|
+
- `InsertQueryBuilder` with single and multi-row inserts
|
|
11
|
+
- `UpdateQueryBuilder` with conditional updates
|
|
12
|
+
- `DeleteQueryBuilder` with safety best practices
|
|
13
|
+
- RETURNING clause support across all DML operations
|
|
14
|
+
- Multi-dialect support for all DML operations
|
|
15
|
+
|
|
16
|
+
## Updated Documentation Files
|
|
17
|
+
|
|
18
|
+
### `index.md`
|
|
19
|
+
- **Updates**:
|
|
20
|
+
- Added DML Operations to the table of contents
|
|
21
|
+
- Updated philosophy to include PostgreSQL in multi-dialect support
|
|
22
|
+
- Added "Comprehensive Relation Support" to features list
|
|
23
|
+
- Added "DML Operations" to features list
|
|
24
|
+
|
|
25
|
+
### `schema-definition.md`
|
|
26
|
+
- **Updates**:
|
|
27
|
+
- Added `belongsTo` relation type documentation
|
|
28
|
+
- Added comprehensive `belongsToMany` relation documentation with pivot table examples
|
|
29
|
+
- Updated relation section with all three relation types
|
|
30
|
+
|
|
31
|
+
### `advanced-features.md`
|
|
32
|
+
- **Updates**:
|
|
33
|
+
- Enhanced window functions section with convenience helpers
|
|
34
|
+
- Added examples for `rowNumber()`, `rank()`, `lag()`, `lead()` functions
|
|
35
|
+
- Improved CTE examples with better explanations
|
|
36
|
+
|
|
37
|
+
### `multi-dialect-support.md`
|
|
38
|
+
- **Updates**:
|
|
39
|
+
- Added PostgreSQL dialect to supported dialects
|
|
40
|
+
- Added dialect-specific features section
|
|
41
|
+
- Added examples showing SQL Server TOP vs LIMIT differences
|
|
42
|
+
|
|
43
|
+
### `api-reference.md`
|
|
44
|
+
- **Updates**:
|
|
45
|
+
- Added `InsertQueryBuilder`, `UpdateQueryBuilder`, `DeleteQueryBuilder` to core classes
|
|
46
|
+
- Added `PostgresDialect` to dialect compilers
|
|
47
|
+
- Added `belongsToMany()` to relation definitions
|
|
48
|
+
- Added `notLike()`, `notBetween()`, `firstValue()`, `lastValue()`, `ntile()` to utility functions
|
|
49
|
+
- Added `isNull()`, `isNotNull()` to null checking functions
|
|
50
|
+
|
|
51
|
+
### `hydration.md`
|
|
52
|
+
- **Updates**:
|
|
53
|
+
- Enhanced pivot column hydration documentation
|
|
54
|
+
- Added comprehensive examples of `_pivot` key usage
|
|
55
|
+
- Added advanced hydration options with custom aliases
|
|
56
|
+
|
|
57
|
+
### `query-builder.md`
|
|
58
|
+
- **Updates**:
|
|
59
|
+
- Added window functions section with examples
|
|
60
|
+
- Added CTEs (Common Table Expressions) section
|
|
61
|
+
- Added subqueries section
|
|
62
|
+
- Enhanced existing sections with more comprehensive examples
|
|
63
|
+
|
|
64
|
+
### `getting-started.md`
|
|
65
|
+
- **Updates**:
|
|
66
|
+
- Added `rowNumber()` window function to the main example
|
|
67
|
+
- Updated imports to include new functions
|
|
68
|
+
- Enhanced example output to show window function results
|
|
69
|
+
|
|
70
|
+
## New Features Now Documented
|
|
71
|
+
|
|
72
|
+
### DML Operations
|
|
73
|
+
- Full INSERT, UPDATE, DELETE query builder support
|
|
74
|
+
- RETURNING clause support for all DML operations
|
|
75
|
+
- Multi-dialect compilation for DML
|
|
76
|
+
|
|
77
|
+
### Advanced SQL Features
|
|
78
|
+
- Comprehensive window function support (`rowNumber`, `rank`, `denseRank`, `lag`, `lead`, `firstValue`, `lastValue`, `ntile`)
|
|
79
|
+
- Enhanced CTE support with recursive CTEs
|
|
80
|
+
- Advanced subquery support
|
|
81
|
+
|
|
82
|
+
### Relation Types
|
|
83
|
+
- `belongsToMany` relation type with pivot table support
|
|
84
|
+
- Pivot column hydration with custom alias support
|
|
85
|
+
|
|
86
|
+
### Dialect Support
|
|
87
|
+
- PostgreSQL dialect support
|
|
88
|
+
- Dialect-specific feature documentation
|
|
89
|
+
|
|
90
|
+
### Expression Builders
|
|
91
|
+
- Additional comparison operators (`notLike`, `notBetween`)
|
|
92
|
+
- Null checking functions (`isNull`, `isNotNull`)
|
|
93
|
+
- Enhanced window function helpers
|
|
94
|
+
|
|
95
|
+
## Verification
|
|
96
|
+
|
|
97
|
+
All documented features have been verified to exist in the codebase:
|
|
98
|
+
- ✅ All DML classes exist and are exported
|
|
99
|
+
- ✅ All window functions exist and are exported
|
|
100
|
+
- ✅ PostgreSQL dialect exists and is exported
|
|
101
|
+
- ✅ `belongsToMany` relation function exists and is exported
|
|
102
|
+
- ✅ All utility functions exist and are exported
|
|
103
|
+
|
|
104
|
+
The documentation now accurately reflects the current state of the MetalORM library with all its advanced features.
|
|
@@ -23,7 +23,9 @@ const query = new SelectQueryBuilder(activeUsers)
|
|
|
23
23
|
|
|
24
24
|
## Window Functions
|
|
25
25
|
|
|
26
|
-
MetalORM provides
|
|
26
|
+
MetalORM provides comprehensive support for window functions including `ROW_NUMBER()`, `RANK()`, `DENSE_RANK()`, `LAG()`, `LEAD()`, and more.
|
|
27
|
+
|
|
28
|
+
### Basic Window Functions
|
|
27
29
|
|
|
28
30
|
```typescript
|
|
29
31
|
const rankedPosts = new SelectQueryBuilder(posts)
|
|
@@ -36,6 +38,42 @@ const rankedPosts = new SelectQueryBuilder(posts)
|
|
|
36
38
|
});
|
|
37
39
|
```
|
|
38
40
|
|
|
41
|
+
### Convenience Helpers
|
|
42
|
+
|
|
43
|
+
MetalORM provides convenience functions for common window functions:
|
|
44
|
+
|
|
45
|
+
```typescript
|
|
46
|
+
import { rowNumber, rank, denseRank, lag, lead } from 'metal-orm';
|
|
47
|
+
|
|
48
|
+
// Simple row numbering
|
|
49
|
+
const query1 = new SelectQueryBuilder(users)
|
|
50
|
+
.select({
|
|
51
|
+
id: users.columns.id,
|
|
52
|
+
name: users.columns.name,
|
|
53
|
+
rowNum: rowNumber()
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Ranking with partitioning
|
|
57
|
+
const query2 = new SelectQueryBuilder(orders)
|
|
58
|
+
.select({
|
|
59
|
+
id: orders.columns.id,
|
|
60
|
+
customerId: orders.columns.customerId,
|
|
61
|
+
amount: orders.columns.amount,
|
|
62
|
+
rank: rank()
|
|
63
|
+
})
|
|
64
|
+
.partitionBy(orders.columns.customerId)
|
|
65
|
+
.orderBy(orders.columns.amount, 'DESC');
|
|
66
|
+
|
|
67
|
+
// LAG and LEAD functions
|
|
68
|
+
const query3 = new SelectQueryBuilder(sales)
|
|
69
|
+
.select({
|
|
70
|
+
date: sales.columns.date,
|
|
71
|
+
amount: sales.columns.amount,
|
|
72
|
+
prevAmount: lag(sales.columns.amount, 1, 0),
|
|
73
|
+
nextAmount: lead(sales.columns.amount, 1, 0)
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
39
77
|
## Subqueries and EXISTS
|
|
40
78
|
|
|
41
79
|
You can use subqueries and `EXISTS` to perform complex checks.
|
|
@@ -82,4 +120,57 @@ const tieredUsers = new SelectQueryBuilder(users)
|
|
|
82
120
|
], 'regular')
|
|
83
121
|
})
|
|
84
122
|
.groupBy(users.columns.id);
|
|
123
|
+
|
|
124
|
+
## Advanced Runtime Patterns
|
|
125
|
+
|
|
126
|
+
When using the OrmContext runtime, you can implement advanced patterns like soft deletes, multi-tenant filtering, and optimistic concurrency.
|
|
127
|
+
|
|
128
|
+
### Soft Deletes
|
|
129
|
+
|
|
130
|
+
Use hooks to implement soft deletes:
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
const users = defineTable('users', {
|
|
134
|
+
id: col.int().primaryKey(),
|
|
135
|
+
name: col.varchar(255).notNull(),
|
|
136
|
+
deletedAt: col.timestamp(),
|
|
137
|
+
}, undefined, {
|
|
138
|
+
hooks: {
|
|
139
|
+
beforeRemove(ctx, user) {
|
|
140
|
+
user.deletedAt = new Date();
|
|
141
|
+
return false; // prevent actual deletion
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Multi-Tenant Filters
|
|
148
|
+
|
|
149
|
+
Apply global filters via context:
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
const ctx = new OrmContext({
|
|
153
|
+
dialect: new MySqlDialect(),
|
|
154
|
+
db: { /* ... */ },
|
|
155
|
+
tenantId: 'tenant-123',
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// All queries in this context automatically filter by tenant
|
|
159
|
+
const users = await new SelectQueryBuilder(usersTable)
|
|
160
|
+
.execute(ctx); // WHERE tenantId = 'tenant-123'
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Optimistic Concurrency
|
|
164
|
+
|
|
165
|
+
Track version columns for conflict detection:
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
const posts = defineTable('posts', {
|
|
169
|
+
id: col.int().primaryKey(),
|
|
170
|
+
title: col.varchar(255).notNull(),
|
|
171
|
+
version: col.int().default(1),
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
ctx.saveChanges(); // throws if version mismatch
|
|
175
|
+
```
|
|
85
176
|
```
|
package/docs/api-reference.md
CHANGED
|
@@ -4,19 +4,28 @@ This section provides a reference for the core classes, key functions, and utili
|
|
|
4
4
|
|
|
5
5
|
### Core Classes
|
|
6
6
|
- `SelectQueryBuilder` - Main query builder class
|
|
7
|
-
- `
|
|
7
|
+
- `InsertQueryBuilder` - INSERT query builder class
|
|
8
|
+
- `UpdateQueryBuilder` - UPDATE query builder class
|
|
9
|
+
- `DeleteQueryBuilder` - DELETE query builder class
|
|
10
|
+
- `MySqlDialect` / `SQLiteDialect` / `MSSQLDialect` / `PostgresDialect` - SQL dialect compilers
|
|
8
11
|
- `HydrationManager` - Handles relation hydration logic
|
|
12
|
+
- `OrmContext` - Unit of Work context for entities
|
|
13
|
+
- `Entity<TTable>` - Entity proxy wrapping table rows
|
|
14
|
+
- `HasManyCollection<T>` - Lazy/batched has-many relation wrapper
|
|
15
|
+
- `BelongsToReference<T>` - Belongs-to relation wrapper
|
|
16
|
+
- `ManyToManyCollection<T>` - Many-to-many relation wrapper with pivot
|
|
9
17
|
|
|
10
18
|
### Key Functions
|
|
11
19
|
- `defineTable()` - Define database tables
|
|
12
20
|
- `col.*()` - Column type definitions
|
|
13
|
-
- `hasMany()` / `belongsTo()` - Relation definitions
|
|
21
|
+
- `hasMany()` / `belongsTo()` / `belongsToMany()` - Relation definitions
|
|
14
22
|
- `eq()`, `and()`, `or()`, etc. - Expression builders
|
|
15
23
|
- `hydrateRows()` - Transform flat rows to nested objects
|
|
16
24
|
|
|
17
25
|
### Utility Functions
|
|
18
26
|
- `count()`, `sum()`, `avg()` - Aggregate functions
|
|
19
|
-
- `like()`, `between()`, `inList()`, `notInList()` - Comparison operators
|
|
27
|
+
- `like()`, `notLike()`, `between()`, `notBetween()`, `inList()`, `notInList()` - Comparison operators
|
|
20
28
|
- `jsonPath()` - JSON extraction
|
|
21
29
|
- `caseWhen()`, `exists()`, `notExists()` - Conditional and subquery helpers
|
|
22
|
-
- `rowNumber()`, `rank()`, `denseRank()`, `lag()`, `lead()`, `windowFunction()` - Window function helpers
|
|
30
|
+
- `rowNumber()`, `rank()`, `denseRank()`, `lag()`, `lead()`, `firstValue()`, `lastValue()`, `ntile()`, `windowFunction()` - Window function helpers
|
|
31
|
+
- `isNull()`, `isNotNull()` - Null checking functions
|