metal-orm 1.0.0 → 1.0.1

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.
Files changed (2) hide show
  1. package/README.md +549 -30
  2. package/package.json +2 -2
package/README.md CHANGED
@@ -1,30 +1,549 @@
1
- <div align="center">
2
- <img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
3
- </div>
4
-
5
- # Run and deploy your AI Studio app
6
-
7
- This contains everything you need to run your app locally.
8
-
9
- View your app in AI Studio: https://ai.studio/apps/drive/1vXchpJ36cRXpwzrizwz6I-M1Ks1GAkz2
10
-
11
- ## Run Locally
12
-
13
- **Prerequisites:** Node.js
14
-
15
-
16
- 1. Install dependencies:
17
- `npm install`
18
- 2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
19
- 3. Run the app:
20
- `npm run dev`
21
-
22
- ## Project layout
23
-
24
- - `src/` hosts the React-based playground UI that backs the demo.
25
- - `src/metal-orm/src/` contains the real MetalORM implementation (AST, builder, dialects, runtime) consumed by the playground.
26
- - Legacy `orm/` and `services/orm/` directories were removed because they were unused duplicates, so future work belongs in `src/metal-orm/src`.
27
-
28
- ## Parameterized Queries
29
-
30
- Literal values in expressions are now emitted as parameter placeholders and collected in a binding list. Use `SelectQueryBuilder.compile(dialect)` to get `{ sql, params }` and pass both to your database driver (`client.executeSql(sql, params)`); `SelectQueryBuilder.toSql(dialect)` still returns just the SQL string for quick debugging.
1
+ # MetalORM
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
+ **A TypeScript-first SQL query builder with schema-driven AST, hydrated relations, and multi-dialect compilation.**
8
+
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
+
11
+ ## Philosophy
12
+
13
+ MetalORM follows these core principles:
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
20
+
21
+ ## Features
22
+
23
+ ### Declarative Schema Definition
24
+ Define your database structure in TypeScript with full type inference:
25
+
26
+ ```typescript
27
+ const users = defineTable(
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
+ ```
157
+
158
+ ## Installation
159
+
160
+ ```bash
161
+ # npm
162
+ npm install metal-orm
163
+
164
+ # yarn
165
+ yarn add metal-orm
166
+
167
+ # pnpm
168
+ pnpm add metal-orm
169
+ ```
170
+
171
+ ## Quick Start
172
+
173
+ Here's a complete example to get you started:
174
+
175
+ ```typescript
176
+ import {
177
+ defineTable,
178
+ col,
179
+ hasMany,
180
+ SelectQueryBuilder,
181
+ eq,
182
+ count,
183
+ hydrateRows,
184
+ MySqlDialect
185
+ } from 'metal-orm';
186
+
187
+ // 1. Define your schema
188
+ const posts = defineTable(
189
+ 'posts',
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
+ );
199
+
200
+ const users = defineTable(
201
+ 'users',
202
+ {
203
+ id: col.int().primaryKey(),
204
+ name: col.varchar(255).notNull(),
205
+ email: col.varchar(255).unique(),
206
+ createdAt: col.timestamp().default('CURRENT_TIMESTAMP')
207
+ },
208
+ {
209
+ posts: hasMany(posts, 'userId')
210
+ }
211
+ );
212
+
213
+ // 2. Build your query
214
+ const builder = new SelectQueryBuilder(users)
215
+ .select({
216
+ id: users.columns.id,
217
+ name: users.columns.name,
218
+ email: users.columns.email,
219
+ totalPosts: count(posts.columns.id)
220
+ })
221
+ .leftJoin(posts, eq(posts.columns.userId, users.columns.id))
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
+ });
228
+
229
+ // 3. Compile to SQL
230
+ const dialect = new MySqlDialect();
231
+ const { sql, params } = builder.compile(dialect);
232
+
233
+ // 4. Execute and hydrate
234
+ const rows = await connection.execute(sql, params);
235
+ const hydrated = hydrateRows(rows, builder.getHydrationPlan());
236
+
237
+ 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
+ ```
299
+
300
+ ## Performance Considerations
301
+
302
+ ### Query Optimization
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
307
+
308
+ ### Caching Strategies
309
+ ```typescript
310
+ // Implement query result caching
311
+ const cache = new Map<string, any>();
312
+
313
+ async function getUserWithPosts(userId: number) {
314
+ const cacheKey = `user:${userId}:with-posts`;
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
+
325
+ const { sql, params } = builder.compile(dialect);
326
+ const rows = await db.execute(sql, params);
327
+ const result = hydrateRows(rows, builder.getHydrationPlan());
328
+
329
+ cache.set(cacheKey, result);
330
+ return result;
331
+ }
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
+
347
+ ## Migration Guide
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);
371
+ ```
372
+
373
+ ### From TypeORM
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
+ ```
476
+
477
+ ## Contributing
478
+
479
+ We welcome contributions! Here's how you can help:
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
511
+
512
+ ## License
513
+
514
+ MetalORM is [MIT licensed](https://github.com/celsowm/metal-orm/blob/main/LICENSE).
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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metal-orm",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -42,4 +42,4 @@
42
42
  "vite": "^5.0.0",
43
43
  "vitest": "^4.0.14"
44
44
  }
45
- }
45
+ }