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.
- package/README.md +549 -30
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,30 +1,549 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
##
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
+
**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)
|