metal-orm 1.0.1 → 1.0.3
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/.github/workflows/publish-metal-orm.yml +38 -0
- package/README.md +46 -482
- package/docs/advanced-features.md +85 -0
- package/docs/api-reference.md +22 -0
- package/docs/getting-started.md +104 -0
- package/docs/hydration.md +41 -0
- package/docs/index.md +31 -0
- package/docs/multi-dialect-support.md +34 -0
- package/docs/query-builder.md +75 -0
- package/docs/schema-definition.md +61 -0
- package/package.json +1 -1
- package/src/ast/expression.ts +433 -175
- package/src/ast/join.ts +8 -1
- package/src/ast/query.ts +64 -9
- package/src/builder/hydration-manager.ts +42 -11
- package/src/builder/hydration-planner.ts +80 -31
- package/src/builder/operations/column-selector.ts +37 -1
- package/src/builder/operations/cte-manager.ts +16 -0
- package/src/builder/operations/filter-manager.ts +32 -0
- package/src/builder/operations/join-manager.ts +17 -7
- package/src/builder/operations/pagination-manager.ts +19 -0
- package/src/builder/operations/relation-manager.ts +58 -3
- package/src/builder/query-ast-service.ts +100 -29
- package/src/builder/relation-conditions.ts +30 -1
- package/src/builder/relation-projection-helper.ts +43 -1
- package/src/builder/relation-service.ts +68 -13
- package/src/builder/relation-types.ts +6 -0
- package/src/builder/select-query-builder-deps.ts +64 -3
- package/src/builder/select-query-state.ts +72 -0
- package/src/builder/select.ts +166 -0
- package/src/codegen/typescript.ts +142 -44
- package/src/constants/sql-operator-config.ts +36 -0
- package/src/constants/sql.ts +125 -57
- package/src/dialect/abstract.ts +97 -22
- package/src/dialect/mssql/index.ts +27 -0
- package/src/dialect/mysql/index.ts +22 -0
- package/src/dialect/postgres/index.ts +103 -0
- package/src/dialect/sqlite/index.ts +22 -0
- package/src/runtime/als.ts +15 -1
- package/src/runtime/hydration.ts +20 -15
- package/src/schema/column.ts +45 -5
- package/src/schema/relation.ts +49 -2
- package/src/schema/table.ts +27 -3
- package/src/utils/join-node.ts +20 -0
- package/src/utils/raw-column-parser.ts +32 -0
- package/src/utils/relation-alias.ts +43 -0
- package/tests/postgres.test.ts +30 -0
- package/tests/window-function.test.ts +14 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
name: Publish metal-orm to npm
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
id-token: write # OIDC for Trusted Publishing
|
|
11
|
+
packages: write
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
publish:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
|
|
17
|
+
steps:
|
|
18
|
+
- name: Checkout
|
|
19
|
+
uses: actions/checkout@v4
|
|
20
|
+
|
|
21
|
+
- name: Setup Node
|
|
22
|
+
uses: actions/setup-node@v4
|
|
23
|
+
with:
|
|
24
|
+
node-version: '20'
|
|
25
|
+
registry-url: 'https://registry.npmjs.org'
|
|
26
|
+
|
|
27
|
+
- name: Install deps
|
|
28
|
+
run: npm ci
|
|
29
|
+
|
|
30
|
+
- name: Test
|
|
31
|
+
run: npm test
|
|
32
|
+
continue-on-error: true # ou remove se quiser quebrar se falhar
|
|
33
|
+
|
|
34
|
+
- name: Build
|
|
35
|
+
run: npm run build
|
|
36
|
+
|
|
37
|
+
- name: Publish (Trusted Publishing)
|
|
38
|
+
run: npm publish --provenance --access public
|
package/README.md
CHANGED
|
@@ -8,152 +8,17 @@
|
|
|
8
8
|
|
|
9
9
|
MetalORM keeps SQL generation deterministic (CTEs, aggregates, window functions, EXISTS/subqueries) while letting you introspect the AST or reuse builders inside larger queries. It's designed for developers who want the power of raw SQL with the convenience of a modern ORM.
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Documentation
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
- **Type Safety First**: Leverage TypeScript to catch errors at compile time
|
|
16
|
-
- **SQL Transparency**: Generate predictable, readable SQL that you can inspect
|
|
17
|
-
- **Composition Over Configuration**: Build complex queries by composing simple parts
|
|
18
|
-
- **Zero Magic**: Explicit operations with clear AST representation
|
|
19
|
-
- **Multi-Dialect Support**: Write once, compile to MySQL, SQLite, or SQL Server
|
|
13
|
+
For detailed information and API reference, please visit our [full documentation](docs/index.md).
|
|
20
14
|
|
|
21
15
|
## Features
|
|
22
16
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
'users',
|
|
29
|
-
{
|
|
30
|
-
id: col.int().primaryKey(),
|
|
31
|
-
name: col.varchar(255).notNull(),
|
|
32
|
-
email: col.varchar(255).unique(),
|
|
33
|
-
createdAt: col.timestamp().default('CURRENT_TIMESTAMP')
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
posts: hasMany(posts, 'userId'),
|
|
37
|
-
profile: hasOne(profiles, 'userId')
|
|
38
|
-
}
|
|
39
|
-
);
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
### Rich Query Building
|
|
43
|
-
```typescript
|
|
44
|
-
// Simple queries
|
|
45
|
-
const simpleQuery = new SelectQueryBuilder(users)
|
|
46
|
-
.selectRaw('*')
|
|
47
|
-
.where(eq(users.columns.id, 1))
|
|
48
|
-
.limit(1);
|
|
49
|
-
|
|
50
|
-
// Complex joins with relations
|
|
51
|
-
const complexQuery = new SelectQueryBuilder(users)
|
|
52
|
-
.select({
|
|
53
|
-
userId: users.columns.id,
|
|
54
|
-
userName: users.columns.name,
|
|
55
|
-
postCount: count(posts.columns.id),
|
|
56
|
-
})
|
|
57
|
-
.leftJoin(posts, eq(posts.columns.userId, users.columns.id))
|
|
58
|
-
.where(and(
|
|
59
|
-
like(users.columns.name, '%John%'),
|
|
60
|
-
gt(posts.columns.createdAt, new Date('2023-01-01'))
|
|
61
|
-
))
|
|
62
|
-
.groupBy(users.columns.id)
|
|
63
|
-
.having(gt(count(posts.columns.id), 5))
|
|
64
|
-
.orderBy(count(posts.columns.id), 'DESC');
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
### Advanced SQL Features
|
|
68
|
-
```typescript
|
|
69
|
-
// CTEs (Common Table Expressions)
|
|
70
|
-
const since = new Date();
|
|
71
|
-
since.setDate(since.getDate() - 30);
|
|
72
|
-
|
|
73
|
-
const activeUsers = new SelectQueryBuilder(users)
|
|
74
|
-
.selectRaw('*')
|
|
75
|
-
.where(gt(users.columns.lastLogin, since))
|
|
76
|
-
.as('active_users');
|
|
77
|
-
|
|
78
|
-
const query = new SelectQueryBuilder(activeUsers)
|
|
79
|
-
.with(activeUsers)
|
|
80
|
-
.selectRaw('*')
|
|
81
|
-
.where(eq(activeUsers.columns.id, 1));
|
|
82
|
-
|
|
83
|
-
// Window Functions
|
|
84
|
-
const rankedPosts = new SelectQueryBuilder(posts)
|
|
85
|
-
.select({
|
|
86
|
-
id: posts.columns.id,
|
|
87
|
-
title: posts.columns.title,
|
|
88
|
-
rank: windowFunction('RANK', [], [posts.columns.userId], [
|
|
89
|
-
{ column: posts.columns.createdAt, direction: 'DESC' }
|
|
90
|
-
])
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
// Subqueries and EXISTS
|
|
94
|
-
const usersWithPosts = new SelectQueryBuilder(users)
|
|
95
|
-
.selectRaw('*')
|
|
96
|
-
.where(exists(
|
|
97
|
-
new SelectQueryBuilder(posts)
|
|
98
|
-
.selectRaw('1')
|
|
99
|
-
.where(eq(posts.columns.userId, users.columns.id))
|
|
100
|
-
));
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
### Relation Hydration
|
|
104
|
-
Automatically transform flat database rows into nested JavaScript objects:
|
|
105
|
-
|
|
106
|
-
```typescript
|
|
107
|
-
const builder = new SelectQueryBuilder(users)
|
|
108
|
-
.selectRaw('*')
|
|
109
|
-
.include('posts', {
|
|
110
|
-
columns: ['id', 'title', 'content'],
|
|
111
|
-
include: {
|
|
112
|
-
comments: {
|
|
113
|
-
columns: ['id', 'content', 'createdAt']
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
const { sql, params } = builder.compile(new MySqlDialect());
|
|
119
|
-
const rows = await db.execute(sql, params);
|
|
120
|
-
|
|
121
|
-
// Automatically hydrates to:
|
|
122
|
-
// {
|
|
123
|
-
// id: 1,
|
|
124
|
-
// name: 'John',
|
|
125
|
-
// posts: [
|
|
126
|
-
// {
|
|
127
|
-
// id: 1,
|
|
128
|
-
// title: 'First Post',
|
|
129
|
-
// comments: [...]
|
|
130
|
-
// }
|
|
131
|
-
// ]
|
|
132
|
-
// }
|
|
133
|
-
const hydrated = hydrateRows(rows, builder.getHydrationPlan());
|
|
134
|
-
```
|
|
135
|
-
|
|
136
|
-
### Multi-Dialect Support
|
|
137
|
-
Compile the same query to different SQL dialects:
|
|
138
|
-
|
|
139
|
-
```typescript
|
|
140
|
-
const query = new SelectQueryBuilder(users)
|
|
141
|
-
.selectRaw('*')
|
|
142
|
-
.where(eq(users.columns.id, 1))
|
|
143
|
-
.limit(10);
|
|
144
|
-
|
|
145
|
-
// MySQL
|
|
146
|
-
const mysql = query.compile(new MySqlDialect());
|
|
147
|
-
// SQL: SELECT * FROM users WHERE id = ? LIMIT ?
|
|
148
|
-
|
|
149
|
-
// SQLite
|
|
150
|
-
const sqlite = query.compile(new SQLiteDialect());
|
|
151
|
-
// SQL: SELECT * FROM users WHERE id = ? LIMIT ?
|
|
152
|
-
|
|
153
|
-
// SQL Server
|
|
154
|
-
const mssql = query.compile(new MSSQLDialect());
|
|
155
|
-
// SQL: SELECT TOP 10 * FROM users WHERE id = @p1
|
|
156
|
-
```
|
|
17
|
+
- **Declarative Schema Definition**: Define your database structure in TypeScript with full type inference.
|
|
18
|
+
- **Rich Query Building**: A fluent API to build simple and complex queries.
|
|
19
|
+
- **Advanced SQL Features**: Support for CTEs, window functions, subqueries, and more.
|
|
20
|
+
- **Relation Hydration**: Automatically transform flat database rows into nested JavaScript objects.
|
|
21
|
+
- **Multi-Dialect Support**: Compile the same query to different SQL dialects.
|
|
157
22
|
|
|
158
23
|
## Installation
|
|
159
24
|
|
|
@@ -168,382 +33,81 @@ yarn add metal-orm
|
|
|
168
33
|
pnpm add metal-orm
|
|
169
34
|
```
|
|
170
35
|
|
|
171
|
-
##
|
|
36
|
+
## Database Drivers
|
|
172
37
|
|
|
173
|
-
|
|
38
|
+
MetalORM compiles SQL; it does not bundle a database driver. Install one that matches your database, compile your query with the matching dialect, and hand the SQL + parameters to the driver.
|
|
39
|
+
|
|
40
|
+
| Dialect | Driver | Install |
|
|
41
|
+
| --- | --- | --- |
|
|
42
|
+
| MySQL / MariaDB | `mysql2` | `npm install mysql2` |
|
|
43
|
+
| SQLite | `sqlite3` | `npm install sqlite3` |
|
|
44
|
+
| SQL Server | `tedious` | `npm install tedious` |
|
|
45
|
+
|
|
46
|
+
After installing the driver, pick the matching dialect implementation before compiling the query (`MySqlDialect`, `SQLiteDialect`, `MSSQLDialect`), and execute the resulting SQL string with the driver of your choice.
|
|
47
|
+
|
|
48
|
+
## Quick Start
|
|
174
49
|
|
|
175
50
|
```typescript
|
|
51
|
+
import mysql from 'mysql2/promise';
|
|
176
52
|
import {
|
|
177
53
|
defineTable,
|
|
178
54
|
col,
|
|
179
|
-
hasMany,
|
|
180
55
|
SelectQueryBuilder,
|
|
181
56
|
eq,
|
|
182
|
-
count,
|
|
183
57
|
hydrateRows,
|
|
184
|
-
MySqlDialect
|
|
58
|
+
MySqlDialect,
|
|
185
59
|
} from 'metal-orm';
|
|
186
60
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
id: col.int().primaryKey(),
|
|
192
|
-
title: col.varchar(255).notNull(),
|
|
193
|
-
content: col.text(),
|
|
194
|
-
userId: col.int().notNull(),
|
|
195
|
-
createdAt: col.timestamp().default('CURRENT_TIMESTAMP'),
|
|
196
|
-
updatedAt: col.timestamp()
|
|
197
|
-
}
|
|
198
|
-
);
|
|
61
|
+
const users = defineTable('users', {
|
|
62
|
+
id: col.int().primaryKey(),
|
|
63
|
+
name: col.varchar(255).notNull(),
|
|
64
|
+
});
|
|
199
65
|
|
|
200
|
-
const
|
|
201
|
-
'
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
email: col.varchar(255).unique(),
|
|
206
|
-
createdAt: col.timestamp().default('CURRENT_TIMESTAMP')
|
|
207
|
-
},
|
|
208
|
-
{
|
|
209
|
-
posts: hasMany(posts, 'userId')
|
|
210
|
-
}
|
|
211
|
-
);
|
|
66
|
+
const connection = await mysql.createConnection({
|
|
67
|
+
host: 'localhost',
|
|
68
|
+
user: 'root',
|
|
69
|
+
database: 'test',
|
|
70
|
+
});
|
|
212
71
|
|
|
213
|
-
// 2. Build your query
|
|
214
72
|
const builder = new SelectQueryBuilder(users)
|
|
215
73
|
.select({
|
|
216
74
|
id: users.columns.id,
|
|
217
75
|
name: users.columns.name,
|
|
218
|
-
email: users.columns.email,
|
|
219
|
-
totalPosts: count(posts.columns.id)
|
|
220
76
|
})
|
|
221
|
-
.
|
|
222
|
-
.groupBy(users.columns.id, users.columns.name, users.columns.email)
|
|
223
|
-
.orderBy(count(posts.columns.id), 'DESC')
|
|
224
|
-
.limit(20)
|
|
225
|
-
.include('posts', {
|
|
226
|
-
columns: [posts.columns.id, posts.columns.title, posts.columns.createdAt]
|
|
227
|
-
});
|
|
77
|
+
.where(eq(users.columns.id, 1));
|
|
228
78
|
|
|
229
|
-
// 3. Compile to SQL
|
|
230
79
|
const dialect = new MySqlDialect();
|
|
231
80
|
const { sql, params } = builder.compile(dialect);
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
const rows = await connection.execute(sql, params);
|
|
235
|
-
const hydrated = hydrateRows(rows, builder.getHydrationPlan());
|
|
81
|
+
const [rows] = await connection.execute(sql, params);
|
|
82
|
+
const hydrated = hydrateRows(rows as Record<string, unknown>[], builder.getHydrationPlan());
|
|
236
83
|
|
|
237
84
|
console.log(hydrated);
|
|
238
|
-
// [
|
|
239
|
-
// {
|
|
240
|
-
// id: 1,
|
|
241
|
-
// name: 'John Doe',
|
|
242
|
-
// email: 'john@example.com',
|
|
243
|
-
// totalPosts: 15,
|
|
244
|
-
// posts: [
|
|
245
|
-
// {
|
|
246
|
-
// id: 101,
|
|
247
|
-
// title: 'Latest Post',
|
|
248
|
-
// createdAt: '2023-05-15T10:00:00.000Z'
|
|
249
|
-
// },
|
|
250
|
-
// // ... more posts
|
|
251
|
-
// ]
|
|
252
|
-
// }
|
|
253
|
-
// // ... more users
|
|
254
|
-
// ]
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
## Advanced Expression Helpers
|
|
258
|
-
|
|
259
|
-
### JSON filters and comparisons
|
|
260
|
-
```typescript
|
|
261
|
-
const userData = defineTable('user_data', {
|
|
262
|
-
id: col.int().primaryKey(),
|
|
263
|
-
userId: col.int().notNull(),
|
|
264
|
-
preferences: col.json().notNull()
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
const jsonQuery = new SelectQueryBuilder(userData)
|
|
268
|
-
.select({
|
|
269
|
-
id: userData.columns.id,
|
|
270
|
-
userId: userData.columns.userId,
|
|
271
|
-
theme: jsonPath(userData.columns.preferences, '$.theme')
|
|
272
|
-
})
|
|
273
|
-
.where(and(
|
|
274
|
-
eq(jsonPath(userData.columns.preferences, '$.theme'), 'dark'),
|
|
275
|
-
inList(userData.columns.userId, [1, 2, 3])
|
|
276
|
-
));
|
|
277
|
-
```
|
|
278
|
-
|
|
279
|
-
### CASE expressions and window helpers
|
|
280
|
-
```typescript
|
|
281
|
-
const tieredUsers = new SelectQueryBuilder(users)
|
|
282
|
-
.select({
|
|
283
|
-
id: users.columns.id,
|
|
284
|
-
tier: caseWhen([
|
|
285
|
-
{ when: gt(count(posts.columns.id), 10), then: 'power user' }
|
|
286
|
-
], 'regular')
|
|
287
|
-
})
|
|
288
|
-
.groupBy(users.columns.id);
|
|
289
|
-
|
|
290
|
-
const rankedPosts = new SelectQueryBuilder(posts)
|
|
291
|
-
.select({
|
|
292
|
-
id: posts.columns.id,
|
|
293
|
-
createdAt: posts.columns.createdAt,
|
|
294
|
-
rank: windowFunction('RANK', [], [posts.columns.userId], [
|
|
295
|
-
{ column: posts.columns.createdAt, direction: 'DESC' }
|
|
296
|
-
])
|
|
297
|
-
});
|
|
298
85
|
```
|
|
299
86
|
|
|
300
|
-
##
|
|
87
|
+
## Helper idea
|
|
301
88
|
|
|
302
|
-
|
|
303
|
-
- **Use `.select()` explicitly** instead of `select('*')` to only fetch needed columns
|
|
304
|
-
- **Leverage CTEs** for complex queries to improve readability and sometimes performance
|
|
305
|
-
- **Use indexes** on frequently queried columns and join conditions
|
|
306
|
-
- **Reuse compiled hydration plans** when transforming rows to avoid repeating row reconstruction
|
|
89
|
+
If you find yourself compiling/executing the same way across your app, wrap the compiler + driver interaction in a tiny helper instead of repeating it:
|
|
307
90
|
|
|
308
|
-
### Caching Strategies
|
|
309
91
|
```typescript
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
if (cache.has(cacheKey)) {
|
|
317
|
-
return cache.get(cacheKey);
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
const builder = new SelectQueryBuilder(users)
|
|
321
|
-
.selectRaw('*')
|
|
322
|
-
.where(eq(users.columns.id, userId))
|
|
323
|
-
.include('posts');
|
|
324
|
-
|
|
92
|
+
async function runMetalQuery<T>(
|
|
93
|
+
builder: SelectQueryBuilder<T>,
|
|
94
|
+
connection: mysql.Connection,
|
|
95
|
+
dialect = new MySqlDialect()
|
|
96
|
+
) {
|
|
325
97
|
const { sql, params } = builder.compile(dialect);
|
|
326
|
-
const rows = await
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
cache.set(cacheKey, result);
|
|
330
|
-
return result;
|
|
98
|
+
const [rows] = await connection.execute(sql, params);
|
|
99
|
+
return hydrateRows(rows as Record<string, unknown>[], builder.getHydrationPlan());
|
|
331
100
|
}
|
|
332
|
-
```
|
|
333
|
-
|
|
334
|
-
## Comparison with Other ORMs
|
|
335
|
-
|
|
336
|
-
| Feature | MetalORM | TypeORM | Prisma | Knex |
|
|
337
|
-
|------------------------|------------------------|------------------|-------------------|------------------|
|
|
338
|
-
| Type Safety | ✅ Full TypeScript | ✅ Good | ✅ Excellent | ❌ Limited |
|
|
339
|
-
| SQL Generation | ✅ Deterministic | ❌ ORM-style | ✅ Good | ✅ Good |
|
|
340
|
-
| AST Inspection | ✅ Full access | ❌ No | ❌ No | ❌ No |
|
|
341
|
-
| Multi-Dialect | ✅ MySQL/SQLite/MSSQL | ✅ Many | ✅ Many | ✅ Many |
|
|
342
|
-
| Relation Hydration | ✅ Automatic | ✅ Manual | ✅ Automatic | ❌ Manual |
|
|
343
|
-
| Query Builder | ✅ Rich | ✅ Good | ❌ Limited | ✅ Good |
|
|
344
|
-
| Learning Curve | ⚠️ Moderate | ⚠️ Moderate | ✅ Low | ⚠️ Moderate |
|
|
345
|
-
| Bundle Size | ✅ Small | ⚠️ Medium | ⚠️ Medium | ✅ Small |
|
|
346
101
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
### From Knex
|
|
350
|
-
```typescript
|
|
351
|
-
// Before (Knex)
|
|
352
|
-
const users = await knex('users')
|
|
353
|
-
.select('users.*', knex.raw('COUNT(posts.id) as post_count'))
|
|
354
|
-
.leftJoin('posts', 'users.id', 'posts.user_id')
|
|
355
|
-
.groupBy('users.id')
|
|
356
|
-
.orderBy('post_count', 'desc');
|
|
357
|
-
|
|
358
|
-
// After (MetalORM)
|
|
359
|
-
const users = defineTable('users', { /* ... */ });
|
|
360
|
-
const posts = defineTable('posts', { /* ... */ });
|
|
361
|
-
|
|
362
|
-
const result = await new SelectQueryBuilder(users)
|
|
363
|
-
.select({
|
|
364
|
-
...allUserColumns,
|
|
365
|
-
postCount: count(posts.columns.id)
|
|
366
|
-
})
|
|
367
|
-
.leftJoin(posts, eq(posts.columns.userId, users.columns.id))
|
|
368
|
-
.groupBy(users.columns.id)
|
|
369
|
-
.orderBy(count(posts.columns.id), 'DESC')
|
|
370
|
-
.compile(dialect);
|
|
102
|
+
const results = await runMetalQuery(builder, connection);
|
|
371
103
|
```
|
|
372
104
|
|
|
373
|
-
|
|
374
|
-
```typescript
|
|
375
|
-
// Before (TypeORM)
|
|
376
|
-
const users = await userRepository
|
|
377
|
-
.createQueryBuilder('user')
|
|
378
|
-
.leftJoinAndSelect('user.posts', 'post')
|
|
379
|
-
.where('user.name LIKE :name', { name: '%John%' })
|
|
380
|
-
.orderBy('user.createdAt', 'DESC')
|
|
381
|
-
.getMany();
|
|
382
|
-
|
|
383
|
-
// After (MetalORM)
|
|
384
|
-
const result = await new SelectQueryBuilder(users)
|
|
385
|
-
.selectRaw('*')
|
|
386
|
-
.leftJoin(posts, eq(posts.columns.userId, users.columns.id))
|
|
387
|
-
.where(like(users.columns.name, '%John%'))
|
|
388
|
-
.orderBy(users.columns.createdAt, 'DESC')
|
|
389
|
-
.include('posts')
|
|
390
|
-
.compile(dialect);
|
|
391
|
-
```
|
|
392
|
-
|
|
393
|
-
## Project Structure
|
|
394
|
-
|
|
395
|
-
```
|
|
396
|
-
metal-orm/
|
|
397
|
-
├── src/
|
|
398
|
-
│ ├── schema/ # Table/column/relation definitions
|
|
399
|
-
│ ├── builder/ # Query builder and managers
|
|
400
|
-
│ ├── ast/ # AST nodes and expression builders
|
|
401
|
-
│ ├── dialect/ # SQL dialect implementations
|
|
402
|
-
│ ├── runtime/ # Hydration and utility functions
|
|
403
|
-
│ ├── codegen/ # Code generation helpers
|
|
404
|
-
│ └── playground/ # Interactive playground
|
|
405
|
-
├── tests/ # Test suites
|
|
406
|
-
├── dist/ # Compiled output
|
|
407
|
-
└── playground/ # Vite-based playground app
|
|
408
|
-
```
|
|
409
|
-
|
|
410
|
-
## API Documentation
|
|
411
|
-
|
|
412
|
-
### Core Classes
|
|
413
|
-
- `SelectQueryBuilder` - Main query builder class
|
|
414
|
-
- `MySqlDialect` / `SQLiteDialect` / `MSSQLDialect` - SQL dialect compilers
|
|
415
|
-
- `HydrationManager` - Handles relation hydration logic
|
|
416
|
-
|
|
417
|
-
### Key Functions
|
|
418
|
-
- `defineTable()` - Define database tables
|
|
419
|
-
- `col.*()` - Column type definitions
|
|
420
|
-
- `hasMany()` / `belongsTo()` - Relation definitions
|
|
421
|
-
- `eq()`, `and()`, `or()`, etc. - Expression builders
|
|
422
|
-
- `hydrateRows()` - Transform flat rows to nested objects
|
|
423
|
-
|
|
424
|
-
### Utility Functions
|
|
425
|
-
- `count()`, `sum()`, `avg()` - Aggregate functions
|
|
426
|
-
- `like()`, `between()`, `inList()`, `notInList()` - Comparison operators
|
|
427
|
-
- `jsonPath()` - JSON extraction
|
|
428
|
-
- `caseWhen()`, `exists()`, `notExists()` - Conditional and subquery helpers
|
|
429
|
-
- `rowNumber()`, `rank()`, `denseRank()`, `lag()`, `lead()`, `windowFunction()` - Window function helpers
|
|
430
|
-
|
|
431
|
-
## Project Commands
|
|
432
|
-
|
|
433
|
-
```bash
|
|
434
|
-
# Build the project
|
|
435
|
-
npm run build
|
|
436
|
-
|
|
437
|
-
# Type checking
|
|
438
|
-
npm run check
|
|
439
|
-
|
|
440
|
-
# Run tests
|
|
441
|
-
npm run test
|
|
442
|
-
|
|
443
|
-
# Interactive test UI
|
|
444
|
-
npm run test:ui
|
|
445
|
-
|
|
446
|
-
# Launch playground
|
|
447
|
-
npm run playground
|
|
448
|
-
|
|
449
|
-
# Clean build
|
|
450
|
-
npm run clean
|
|
451
|
-
```
|
|
452
|
-
|
|
453
|
-
## Development Setup
|
|
454
|
-
|
|
455
|
-
1. **Clone the repository:**
|
|
456
|
-
```bash
|
|
457
|
-
git clone https://github.com/celsowm/metal-orm.git
|
|
458
|
-
cd metal-orm
|
|
459
|
-
```
|
|
460
|
-
|
|
461
|
-
2. **Install dependencies:**
|
|
462
|
-
```bash
|
|
463
|
-
npm install
|
|
464
|
-
```
|
|
465
|
-
|
|
466
|
-
3. **Run the playground:**
|
|
467
|
-
```bash
|
|
468
|
-
npm run playground
|
|
469
|
-
```
|
|
470
|
-
This launches a Vite-based UI where you can experiment with queries.
|
|
471
|
-
|
|
472
|
-
4. **Run tests:**
|
|
473
|
-
```bash
|
|
474
|
-
npm test
|
|
475
|
-
```
|
|
105
|
+
This keeps the ORM-focused pieces in one place while letting you reuse any pooling/transaction strategy the driver provides.
|
|
476
106
|
|
|
477
107
|
## Contributing
|
|
478
108
|
|
|
479
|
-
We welcome contributions!
|
|
480
|
-
|
|
481
|
-
### Getting Started
|
|
482
|
-
1. Fork the repository
|
|
483
|
-
2. Clone your fork and install dependencies
|
|
484
|
-
3. Run `npm run playground` to see MetalORM in action
|
|
485
|
-
|
|
486
|
-
### Development Workflow
|
|
487
|
-
1. **Write tests first** - Add test cases in the `tests/` directory
|
|
488
|
-
2. **Implement features** - Follow existing code patterns
|
|
489
|
-
3. **Update documentation** - Keep docs in sync with changes
|
|
490
|
-
4. **Run all checks** - Ensure tests pass and formatting is correct
|
|
491
|
-
|
|
492
|
-
### Code Guidelines
|
|
493
|
-
- Follow existing TypeScript patterns and conventions
|
|
494
|
-
- Keep functions small and focused
|
|
495
|
-
- Add JSDoc comments for public APIs
|
|
496
|
-
- Write comprehensive tests for new features
|
|
497
|
-
- Maintain 100% type coverage
|
|
498
|
-
|
|
499
|
-
### Pull Request Process
|
|
500
|
-
1. Create a feature branch from `main`
|
|
501
|
-
2. Implement your changes with tests
|
|
502
|
-
3. Update documentation as needed
|
|
503
|
-
4. Ensure all tests pass (`npm test`)
|
|
504
|
-
5. Submit PR with clear description of changes
|
|
505
|
-
|
|
506
|
-
### Documentation Standards
|
|
507
|
-
- Update `ROADMAP.md` for major feature plans
|
|
508
|
-
- Add `SOLID_*.md` files for architectural decisions
|
|
509
|
-
- Keep API documentation in sync with code changes
|
|
510
|
-
- Add examples for new features
|
|
109
|
+
We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for more details.
|
|
511
110
|
|
|
512
111
|
## License
|
|
513
112
|
|
|
514
|
-
MetalORM is [MIT licensed](
|
|
515
|
-
|
|
516
|
-
## Support
|
|
517
|
-
|
|
518
|
-
- **Issues**: Report bugs or request features on [GitHub Issues](https://github.com/celsowm/metal-orm/issues)
|
|
519
|
-
- **Discussions**: Join the conversation on [GitHub Discussions](https://github.com/celsowm/metal-orm/discussions)
|
|
520
|
-
- **Contributing**: See the [Contributing](#contributing) section above
|
|
521
|
-
|
|
522
|
-
## Roadmap
|
|
523
|
-
|
|
524
|
-
Check out our [ROADMAP.md](ROADMAP.md) for upcoming features and long-term vision.
|
|
525
|
-
|
|
526
|
-
## Examples
|
|
527
|
-
|
|
528
|
-
See the [playground scenarios](playground/src/data/scenarios/) for comprehensive examples covering:
|
|
529
|
-
- Basic CRUD operations
|
|
530
|
-
- Complex joins and relations
|
|
531
|
-
- Aggregations and window functions
|
|
532
|
-
- CTEs and subqueries
|
|
533
|
-
- JSON operations
|
|
534
|
-
- And much more!
|
|
535
|
-
|
|
536
|
-
## Who's Using MetalORM?
|
|
537
|
-
|
|
538
|
-
MetalORM is used in production by various projects. If you're using MetalORM, consider adding your project to this list!
|
|
539
|
-
|
|
540
|
-
## Acknowledgements
|
|
541
|
-
|
|
542
|
-
Special thanks to all contributors and the open-source community for their support and feedback.
|
|
543
|
-
|
|
544
|
-
---
|
|
545
|
-
|
|
546
|
-
**Star this repo if you find it useful!** ⭐
|
|
547
|
-
|
|
548
|
-
[GitHub Repository](https://github.com/celsowm/metal-orm) |
|
|
549
|
-
[npm package](https://www.npmjs.com/package/metal-orm)
|
|
113
|
+
MetalORM is [MIT licensed](LICENSE).
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Advanced Features
|
|
2
|
+
|
|
3
|
+
MetalORM supports a wide range of advanced SQL features to handle complex scenarios.
|
|
4
|
+
|
|
5
|
+
## Common Table Expressions (CTEs)
|
|
6
|
+
|
|
7
|
+
CTEs help organize complex queries. You can define a CTE using a `SelectQueryBuilder` and reference it in the main query.
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
const since = new Date();
|
|
11
|
+
since.setDate(since.getDate() - 30);
|
|
12
|
+
|
|
13
|
+
const activeUsers = new SelectQueryBuilder(users)
|
|
14
|
+
.selectRaw('*')
|
|
15
|
+
.where(gt(users.columns.lastLogin, since))
|
|
16
|
+
.as('active_users');
|
|
17
|
+
|
|
18
|
+
const query = new SelectQueryBuilder(activeUsers)
|
|
19
|
+
.with(activeUsers)
|
|
20
|
+
.selectRaw('*')
|
|
21
|
+
.where(eq(activeUsers.columns.id, 1));
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Window Functions
|
|
25
|
+
|
|
26
|
+
MetalORM provides helpers for window functions like `RANK()`, `ROW_NUMBER()`, etc.
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
const rankedPosts = new SelectQueryBuilder(posts)
|
|
30
|
+
.select({
|
|
31
|
+
id: posts.columns.id,
|
|
32
|
+
title: posts.columns.title,
|
|
33
|
+
rank: windowFunction('RANK', [], [posts.columns.userId], [
|
|
34
|
+
{ column: posts.columns.createdAt, direction: 'DESC' }
|
|
35
|
+
])
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Subqueries and EXISTS
|
|
40
|
+
|
|
41
|
+
You can use subqueries and `EXISTS` to perform complex checks.
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
const usersWithPosts = new SelectQueryBuilder(users)
|
|
45
|
+
.selectRaw('*')
|
|
46
|
+
.where(exists(
|
|
47
|
+
new SelectQueryBuilder(posts)
|
|
48
|
+
.selectRaw('1')
|
|
49
|
+
.where(eq(posts.columns.userId, users.columns.id))
|
|
50
|
+
));
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## JSON Operations
|
|
54
|
+
|
|
55
|
+
MetalORM provides helpers for working with JSON data.
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
const userData = defineTable('user_data', {
|
|
59
|
+
id: col.int().primaryKey(),
|
|
60
|
+
userId: col.int().notNull(),
|
|
61
|
+
preferences: col.json().notNull()
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const jsonQuery = new SelectQueryBuilder(userData)
|
|
65
|
+
.select({
|
|
66
|
+
id: userData.columns.id,
|
|
67
|
+
theme: jsonPath(userData.columns.preferences, '$.theme')
|
|
68
|
+
})
|
|
69
|
+
.where(eq(jsonPath(userData.columns.preferences, '$.theme'), 'dark'));
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## CASE Expressions
|
|
73
|
+
|
|
74
|
+
You can use `caseWhen()` to create `CASE` expressions for conditional logic.
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
const tieredUsers = new SelectQueryBuilder(users)
|
|
78
|
+
.select({
|
|
79
|
+
id: users.columns.id,
|
|
80
|
+
tier: caseWhen([
|
|
81
|
+
{ when: gt(count(posts.columns.id), 10), then: 'power user' }
|
|
82
|
+
], 'regular')
|
|
83
|
+
})
|
|
84
|
+
.groupBy(users.columns.id);
|
|
85
|
+
```
|