metal-orm 1.0.7 → 1.0.9
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 +133 -121
- package/dist/decorators/index.cjs +2564 -0
- package/dist/decorators/index.cjs.map +1 -0
- package/dist/decorators/index.d.cts +53 -0
- package/dist/decorators/index.d.ts +53 -0
- package/dist/decorators/index.js +2530 -0
- package/dist/decorators/index.js.map +1 -0
- package/dist/index.cjs +4227 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +701 -0
- package/dist/index.d.ts +701 -0
- package/dist/index.js +4131 -0
- package/dist/index.js.map +1 -0
- package/dist/select-654m4qy8.d.cts +1522 -0
- package/dist/select-654m4qy8.d.ts +1522 -0
- package/package.json +27 -20
- package/src/codegen/typescript.ts +405 -393
- package/src/core/ast/aggregate-functions.ts +30 -0
- package/src/core/ast/builders.ts +43 -0
- package/src/core/ast/expression-builders.ts +310 -0
- package/src/core/ast/expression-nodes.ts +211 -0
- package/src/core/ast/expression-visitor.ts +99 -0
- package/src/core/ast/expression.ts +5 -0
- package/src/{utils → core/ast}/join-node.ts +20 -20
- package/src/{ast → core/ast}/join.ts +18 -18
- package/src/{ast → core/ast}/query.ts +113 -113
- package/src/core/ast/window-functions.ts +140 -0
- package/src/{dialect → core/dialect}/abstract.ts +94 -94
- package/src/{dialect → core/dialect}/mssql/index.ts +31 -31
- package/src/{dialect → core/dialect}/mysql/index.ts +31 -31
- package/src/{dialect → core/dialect}/postgres/index.ts +45 -45
- package/src/{dialect → core/dialect}/sqlite/index.ts +45 -45
- package/src/{constants → core/sql}/sql-operator-config.ts +39 -39
- package/src/decorators/bootstrap.ts +126 -0
- package/src/decorators/column.ts +78 -0
- package/src/decorators/entity.ts +36 -0
- package/src/decorators/index.ts +4 -0
- package/src/decorators/relations.ts +107 -0
- package/src/global.d.ts +1 -0
- package/src/index.ts +22 -22
- package/src/orm/db-executor.ts +11 -0
- package/src/orm/domain-event-bus.ts +52 -0
- package/src/{runtime → orm}/entity-meta.ts +52 -52
- package/src/orm/entity-metadata.ts +140 -0
- package/src/{runtime → orm}/entity.ts +252 -252
- package/src/{runtime → orm}/execute.ts +36 -36
- package/src/{runtime → orm}/hydration.ts +103 -103
- package/src/orm/identity-map.ts +37 -0
- package/src/{runtime → orm}/lazy-batch.ts +205 -205
- package/src/orm/orm-context.ts +154 -0
- package/src/orm/relation-change-processor.ts +140 -0
- package/src/{runtime → orm}/relations/belongs-to.ts +92 -92
- package/src/{runtime → orm}/relations/has-many.ts +111 -111
- package/src/{runtime → orm}/relations/many-to-many.ts +149 -149
- package/src/orm/runtime-types.ts +39 -0
- package/src/orm/transaction-runner.ts +17 -0
- package/src/orm/unit-of-work.ts +232 -0
- package/src/{builder/operations → query-builder}/column-selector.ts +78 -78
- package/src/{builder → query-builder}/delete-query-state.ts +38 -42
- package/src/{builder → query-builder}/delete.ts +46 -57
- package/src/{builder → query-builder}/hydration-manager.ts +87 -87
- package/src/{builder → query-builder}/hydration-planner.ts +182 -182
- package/src/{builder → query-builder}/insert-query-state.ts +51 -62
- package/src/{builder → query-builder}/insert.ts +48 -59
- package/src/{builder → query-builder}/query-ast-service.ts +208 -226
- package/src/{utils → query-builder}/raw-column-parser.ts +32 -32
- package/src/{builder → query-builder}/relation-conditions.ts +112 -112
- package/src/{builder/operations → query-builder}/relation-manager.ts +82 -82
- package/src/{builder → query-builder}/relation-projection-helper.ts +101 -101
- package/src/{builder → query-builder}/relation-service.ts +284 -284
- package/src/{builder → query-builder}/relation-types.ts +21 -21
- package/src/{builder → query-builder}/relation-utils.ts +12 -12
- package/src/{builder → query-builder}/select-query-builder-deps.ts +112 -94
- package/src/{builder → query-builder}/select-query-state.ts +179 -179
- package/src/{builder → query-builder}/select.ts +78 -69
- package/src/{builder → query-builder}/update-query-state.ts +55 -59
- package/src/{builder → query-builder}/update.ts +50 -61
- package/src/schema/column.ts +25 -25
- package/src/schema/relation.ts +116 -116
- package/src/schema/table.ts +34 -34
- package/src/schema/types.ts +76 -76
- package/.github/workflows/publish-metal-orm.yml +0 -38
- package/ROADMAP.md +0 -125
- package/docs/CHANGES.md +0 -104
- package/docs/advanced-features.md +0 -176
- package/docs/api-reference.md +0 -31
- package/docs/dml-operations.md +0 -156
- package/docs/getting-started.md +0 -171
- package/docs/hydration.md +0 -115
- package/docs/index.md +0 -36
- package/docs/multi-dialect-support.md +0 -59
- package/docs/query-builder.md +0 -135
- package/docs/runtime.md +0 -105
- package/docs/schema-definition.md +0 -112
- package/metadata.json +0 -5
- package/playground/api/playground-api.ts +0 -94
- package/playground/index.html +0 -15
- package/playground/src/App.css +0 -1
- package/playground/src/App.tsx +0 -114
- package/playground/src/components/CodeDisplay.tsx +0 -43
- package/playground/src/components/QueryExecutor.tsx +0 -189
- package/playground/src/components/ResultsTable.tsx +0 -67
- package/playground/src/components/ResultsTabs.tsx +0 -105
- package/playground/src/components/ScenarioList.tsx +0 -56
- package/playground/src/components/logo.svg +0 -45
- package/playground/src/data/scenarios.ts +0 -2
- package/playground/src/main.tsx +0 -9
- package/playground/src/services/PlaygroundApiService.ts +0 -60
- package/postcss.config.cjs +0 -5
- package/sql_sql-ansi-cheatsheet-2025.md +0 -264
- package/src/ast/expression.ts +0 -658
- package/src/builder/operations/cte-manager.ts +0 -34
- package/src/builder/operations/filter-manager.ts +0 -68
- package/src/builder/operations/join-manager.ts +0 -36
- package/src/builder/operations/pagination-manager.ts +0 -36
- package/src/playground/features/playground/api/types.ts +0 -16
- package/src/playground/features/playground/clients/MockClient.ts +0 -17
- package/src/playground/features/playground/clients/SqliteClient.ts +0 -57
- package/src/playground/features/playground/common/IDatabaseClient.ts +0 -10
- package/src/playground/features/playground/data/scenarios/aggregation.ts +0 -36
- package/src/playground/features/playground/data/scenarios/basics.ts +0 -25
- package/src/playground/features/playground/data/scenarios/edge_cases.ts +0 -57
- package/src/playground/features/playground/data/scenarios/filtering.ts +0 -94
- package/src/playground/features/playground/data/scenarios/hydration.ts +0 -27
- package/src/playground/features/playground/data/scenarios/index.ts +0 -29
- package/src/playground/features/playground/data/scenarios/ordering.ts +0 -25
- package/src/playground/features/playground/data/scenarios/pagination.ts +0 -16
- package/src/playground/features/playground/data/scenarios/relationships.ts +0 -75
- package/src/playground/features/playground/data/scenarios/types.ts +0 -70
- package/src/playground/features/playground/data/schema.ts +0 -91
- package/src/playground/features/playground/data/seed.ts +0 -104
- package/src/playground/features/playground/services/QueryExecutionService.ts +0 -121
- package/src/runtime/orm-context.ts +0 -539
- package/tests/belongs-to-many.test.ts +0 -57
- package/tests/between.test.ts +0 -43
- package/tests/case-expression.test.ts +0 -58
- package/tests/complex-exists.test.ts +0 -230
- package/tests/cte.test.ts +0 -118
- package/tests/dml.test.ts +0 -206
- package/tests/exists.test.ts +0 -127
- package/tests/like.test.ts +0 -33
- package/tests/orm-runtime.test.ts +0 -254
- package/tests/postgres.test.ts +0 -30
- package/tests/right-join.test.ts +0 -89
- package/tests/subquery-having.test.ts +0 -193
- package/tests/window-function.test.ts +0 -151
- package/tsconfig.json +0 -30
- package/tsup.config.ts +0 -10
- package/vite.config.ts +0 -22
- package/vitest.config.ts +0 -14
- /package/src/{constants → core/sql}/sql.ts +0 -0
- /package/src/{runtime → orm}/als.ts +0 -0
- /package/src/{utils → query-builder}/relation-alias.ts +0 -0
package/docs/hydration.md
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
# Hydration & Entities
|
|
2
|
-
|
|
3
|
-
MetalORM offers two ways to go from SQL rows to richer structures:
|
|
4
|
-
|
|
5
|
-
1. **Hydration only** – `hydrateRows(rows, plan)` → nested plain objects.
|
|
6
|
-
2. **Entity runtime** – `builder.execute(ctx)` → entities with lazy relations.
|
|
7
|
-
|
|
8
|
-
## 1. Hydrating with `hydrateRows`
|
|
9
|
-
|
|
10
|
-
The `hydrateRows()` function takes an array of database rows and a hydration plan to produce nested objects. The hydration plan is generated by the `SelectQueryBuilder` when you use the `include()` method.
|
|
11
|
-
|
|
12
|
-
```typescript
|
|
13
|
-
const builder = new SelectQueryBuilder(users)
|
|
14
|
-
.selectRaw('*')
|
|
15
|
-
.include('posts', {
|
|
16
|
-
columns: ['id', 'title', 'content'],
|
|
17
|
-
include: {
|
|
18
|
-
comments: {
|
|
19
|
-
columns: ['id', 'content', 'createdAt']
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
const { sql, params } = builder.compile(new MySqlDialect());
|
|
25
|
-
const rows = await db.execute(sql, params);
|
|
26
|
-
|
|
27
|
-
// Automatically hydrates to:
|
|
28
|
-
// {
|
|
29
|
-
// id: 1,
|
|
30
|
-
// name: 'John',
|
|
31
|
-
// posts: [
|
|
32
|
-
// {
|
|
33
|
-
// id: 1,
|
|
34
|
-
// title: 'First Post',
|
|
35
|
-
// comments: [...]
|
|
36
|
-
// }
|
|
37
|
-
// ]
|
|
38
|
-
// }
|
|
39
|
-
const hydrated = hydrateRows(rows, builder.getHydrationPlan());
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
## How it Works
|
|
43
|
-
|
|
44
|
-
The `SelectQueryBuilder` analyzes the `include()` configuration and generates a `HydrationPlan`. This plan contains the necessary information to map the flat rows to a nested structure, including relation details and column aliases. The `hydrateRows()` function then uses this plan to efficiently process the result set.
|
|
45
|
-
|
|
46
|
-
## Pivot Column Hydration
|
|
47
|
-
|
|
48
|
-
For belongs-to-many relationships, you can request pivot columns via the `pivot` option. Pivot columns are hydrated alongside each child row under the `_pivot` key:
|
|
49
|
-
|
|
50
|
-
```typescript
|
|
51
|
-
.include('projects', {
|
|
52
|
-
columns: ['id', 'name', 'client'],
|
|
53
|
-
pivot: { columns: ['assigned_at', 'role_id'] }
|
|
54
|
-
})
|
|
55
|
-
```
|
|
56
|
-
|
|
57
|
-
This would hydrate to:
|
|
58
|
-
|
|
59
|
-
```typescript
|
|
60
|
-
{
|
|
61
|
-
id: 1,
|
|
62
|
-
name: 'John Doe',
|
|
63
|
-
projects: [
|
|
64
|
-
{
|
|
65
|
-
id: 101,
|
|
66
|
-
name: 'Project Alpha',
|
|
67
|
-
client: 'Acme Corp',
|
|
68
|
-
_pivot: {
|
|
69
|
-
assigned_at: '2023-01-15T10:00:00.000Z',
|
|
70
|
-
role_id: 2
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
]
|
|
74
|
-
}
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
### Advanced Hydration Options
|
|
78
|
-
|
|
79
|
-
You can also specify which pivot columns to include and customize the hydration:
|
|
80
|
-
|
|
81
|
-
```typescript
|
|
82
|
-
.include('projects', {
|
|
83
|
-
columns: ['id', 'name'],
|
|
84
|
-
pivot: {
|
|
85
|
-
columns: ['assigned_at', 'role_id'],
|
|
86
|
-
alias: 'assignment_info' // Custom alias instead of '_pivot'
|
|
87
|
-
},
|
|
88
|
-
include: {
|
|
89
|
-
client: {
|
|
90
|
-
columns: ['id', 'name']
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
## 2. Entities on top of hydration
|
|
96
|
-
|
|
97
|
-
When you call `.execute(ctx)` instead of compiling manually:
|
|
98
|
-
|
|
99
|
-
- MetalORM compiles and executes the query using the dialect in the `OrmContext`.
|
|
100
|
-
- If a `HydrationPlan` exists, it is applied.
|
|
101
|
-
- Each root row is wrapped in an entity proxy:
|
|
102
|
-
- scalar fields behave like normal properties
|
|
103
|
-
- relation properties expose `HasManyCollection`, `BelongsToReference`, or `ManyToManyCollection`.
|
|
104
|
-
|
|
105
|
-
```ts
|
|
106
|
-
const [user] = await new SelectQueryBuilder(users)
|
|
107
|
-
.select({ id: users.columns.id, name: users.columns.name })
|
|
108
|
-
.include('posts', { columns: [posts.columns.id, posts.columns.title] }) // eager
|
|
109
|
-
.includeLazy('roles') // lazy
|
|
110
|
-
.execute(ctx);
|
|
111
|
-
|
|
112
|
-
const eagerPosts = user.posts.getItems(); // hydrated from the join
|
|
113
|
-
const lazyRoles = await user.roles.load(); // resolved on demand
|
|
114
|
-
```
|
|
115
|
-
```
|
package/docs/index.md
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
# Introduction to MetalORM
|
|
2
|
-
|
|
3
|
-
MetalORM is a TypeScript-first SQL toolkit that can be used as:
|
|
4
|
-
|
|
5
|
-
1. A **typed query builder** over a real SQL AST.
|
|
6
|
-
2. A **hydration layer** for turning flat rows into nested objects.
|
|
7
|
-
3. An optional **entity runtime** with lazy relations and a Unit of Work.
|
|
8
|
-
|
|
9
|
-
You can adopt these layers independently, in this order.
|
|
10
|
-
|
|
11
|
-
## Philosophy
|
|
12
|
-
|
|
13
|
-
- **Type Safety First** – rely on TypeScript to catch mistakes early.:contentReference[oaicite:8]{index=8}
|
|
14
|
-
- **SQL Transparency** – inspect the AST and generated SQL at any time.
|
|
15
|
-
- **Composition Over Configuration** – build complex queries from small, pure pieces.
|
|
16
|
-
- **Zero Magic, Opt-in Runtime** – the query builder and ORM runtime are separate layers.
|
|
17
|
-
- **Multi-Dialect** – MySQL, PostgreSQL, SQLite, SQL Server out of the box.:contentReference[oaicite:9]{index=9}
|
|
18
|
-
|
|
19
|
-
## Features
|
|
20
|
-
|
|
21
|
-
- Declarative schema definition with relations.
|
|
22
|
-
- Fluent query builder, including CTEs, window functions, subqueries, JSON, CASE.
|
|
23
|
-
- Relation hydration from flat result sets.
|
|
24
|
-
- Optional entity runtime + Unit of Work.
|
|
25
|
-
|
|
26
|
-
## Table of Contents
|
|
27
|
-
|
|
28
|
-
- [Getting Started](./getting-started.md)
|
|
29
|
-
- [Schema Definition](./schema-definition.md)
|
|
30
|
-
- [Query Builder](./query-builder.md)
|
|
31
|
-
- [DML Operations](./dml-operations.md)
|
|
32
|
-
- [Hydration & Entities](./hydration.md)
|
|
33
|
-
- [Runtime & Unit of Work](./runtime.md)
|
|
34
|
-
- [Advanced Features](./advanced-features.md)
|
|
35
|
-
- [Multi-Dialect Support](./multi-dialect-support.md)
|
|
36
|
-
- [API Reference](./api-reference.md)
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
# Multi-Dialect Support
|
|
2
|
-
|
|
3
|
-
MetalORM is designed to be database-agnostic. You can write your queries once and compile them to different SQL dialects.
|
|
4
|
-
|
|
5
|
-
## Compiling Queries
|
|
6
|
-
|
|
7
|
-
The `compile()` method on the `SelectQueryBuilder` takes a dialect instance and returns the compiled SQL and parameters.
|
|
8
|
-
|
|
9
|
-
```typescript
|
|
10
|
-
const query = new SelectQueryBuilder(users)
|
|
11
|
-
.selectRaw('*')
|
|
12
|
-
.where(eq(users.columns.id, 1))
|
|
13
|
-
.limit(10);
|
|
14
|
-
|
|
15
|
-
// MySQL
|
|
16
|
-
const mysql = query.compile(new MySqlDialect());
|
|
17
|
-
// SQL: SELECT * FROM users WHERE id = ? LIMIT ?
|
|
18
|
-
|
|
19
|
-
// SQLite
|
|
20
|
-
const sqlite = query.compile(new SQLiteDialect());
|
|
21
|
-
// SQL: SELECT * FROM users WHERE id = ? LIMIT ?
|
|
22
|
-
|
|
23
|
-
// SQL Server
|
|
24
|
-
const mssql = query.compile(new MSSQLDialect());
|
|
25
|
-
// SQL: SELECT TOP 10 * FROM users WHERE id = @p1
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
## Supported Dialects
|
|
29
|
-
|
|
30
|
-
- **MySQL**: `MySqlDialect`
|
|
31
|
-
- **SQLite**: `SQLiteDialect`
|
|
32
|
-
- **SQL Server**: `MSSQLDialect`
|
|
33
|
-
- **PostgreSQL**: `PostgresDialect`
|
|
34
|
-
|
|
35
|
-
Each dialect handles the specific syntax and parameterization of the target database.
|
|
36
|
-
|
|
37
|
-
### Dialect-Specific Features
|
|
38
|
-
|
|
39
|
-
```typescript
|
|
40
|
-
// PostgreSQL-specific JSON operations
|
|
41
|
-
const query = new SelectQueryBuilder(users)
|
|
42
|
-
.select({
|
|
43
|
-
id: users.columns.id,
|
|
44
|
-
name: users.columns.name,
|
|
45
|
-
settings: jsonPath(users.columns.settings, '$.notifications')
|
|
46
|
-
})
|
|
47
|
-
.compile(new PostgresDialect());
|
|
48
|
-
|
|
49
|
-
// SQL Server TOP clause vs LIMIT
|
|
50
|
-
const limitedQuery = new SelectQueryBuilder(users)
|
|
51
|
-
.selectRaw('*')
|
|
52
|
-
.limit(10);
|
|
53
|
-
|
|
54
|
-
// MySQL/SQLite/PostgreSQL: SELECT * FROM users LIMIT 10
|
|
55
|
-
// SQL Server: SELECT TOP 10 * FROM users
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
> **Note**: When using the runtime (`OrmContext`), the same dialects are used to generate INSERT, UPDATE, DELETE, and pivot operations in `saveChanges()`.
|
|
59
|
-
```
|
package/docs/query-builder.md
DELETED
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
# Query Builder
|
|
2
|
-
|
|
3
|
-
MetalORM's query builder provides a fluent and expressive API for constructing SQL queries.
|
|
4
|
-
|
|
5
|
-
## Selecting Data
|
|
6
|
-
|
|
7
|
-
The `SelectQueryBuilder` is the main entry point for building `SELECT` queries.
|
|
8
|
-
|
|
9
|
-
### Basic Selections
|
|
10
|
-
|
|
11
|
-
You can select all columns using `selectRaw('*')` or specify columns using `select()`:
|
|
12
|
-
|
|
13
|
-
```typescript
|
|
14
|
-
// Select all columns
|
|
15
|
-
const query = new SelectQueryBuilder(users).selectRaw('*');
|
|
16
|
-
|
|
17
|
-
// Select specific columns
|
|
18
|
-
const query = new SelectQueryBuilder(users).select({
|
|
19
|
-
id: users.columns.id,
|
|
20
|
-
name: users.columns.name,
|
|
21
|
-
});
|
|
22
|
-
```
|
|
23
|
-
|
|
24
|
-
### Joins
|
|
25
|
-
|
|
26
|
-
You can join tables using `leftJoin`, `innerJoin`, `rightJoin`, etc.
|
|
27
|
-
|
|
28
|
-
```typescript
|
|
29
|
-
const query = new SelectQueryBuilder(users)
|
|
30
|
-
.select({
|
|
31
|
-
userId: users.columns.id,
|
|
32
|
-
postTitle: posts.columns.title,
|
|
33
|
-
})
|
|
34
|
-
.leftJoin(posts, eq(posts.columns.userId, users.columns.id));
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
### Filtering
|
|
38
|
-
|
|
39
|
-
You can filter results using the `where()` method with expression helpers:
|
|
40
|
-
|
|
41
|
-
```typescript
|
|
42
|
-
const query = new SelectQueryBuilder(users)
|
|
43
|
-
.selectRaw('*')
|
|
44
|
-
.where(and(
|
|
45
|
-
like(users.columns.name, '%John%'),
|
|
46
|
-
gt(users.columns.createdAt, new Date('2023-01-01'))
|
|
47
|
-
));
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
### Aggregation
|
|
51
|
-
|
|
52
|
-
You can use aggregate functions like `count()`, `sum()`, `avg()`, etc., and group the results.
|
|
53
|
-
|
|
54
|
-
```typescript
|
|
55
|
-
const query = new SelectQueryBuilder(users)
|
|
56
|
-
.select({
|
|
57
|
-
userId: users.columns.id,
|
|
58
|
-
postCount: count(posts.columns.id),
|
|
59
|
-
})
|
|
60
|
-
.leftJoin(posts, eq(posts.columns.userId, users.columns.id))
|
|
61
|
-
.groupBy(users.columns.id)
|
|
62
|
-
.having(gt(count(posts.columns.id), 5));
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
### Ordering and Pagination
|
|
66
|
-
|
|
67
|
-
You can order the results using `orderBy()` and paginate using `limit()` and `offset()`.
|
|
68
|
-
|
|
69
|
-
```typescript
|
|
70
|
-
const query = new SelectQueryBuilder(posts)
|
|
71
|
-
.selectRaw('*')
|
|
72
|
-
.orderBy(posts.columns.createdAt, 'DESC')
|
|
73
|
-
.limit(10)
|
|
74
|
-
.offset(20);
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
### Window Functions
|
|
78
|
-
|
|
79
|
-
The query builder supports window functions for advanced analytics:
|
|
80
|
-
|
|
81
|
-
```typescript
|
|
82
|
-
import { rowNumber, rank } from 'metal-orm';
|
|
83
|
-
|
|
84
|
-
const query = new SelectQueryBuilder(users)
|
|
85
|
-
.select({
|
|
86
|
-
id: users.columns.id,
|
|
87
|
-
name: users.columns.name,
|
|
88
|
-
rowNum: rowNumber(),
|
|
89
|
-
userRank: rank()
|
|
90
|
-
})
|
|
91
|
-
.partitionBy(users.columns.department)
|
|
92
|
-
.orderBy(users.columns.salary, 'DESC');
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
### CTEs (Common Table Expressions)
|
|
96
|
-
|
|
97
|
-
You can use CTEs to organize complex queries:
|
|
98
|
-
|
|
99
|
-
```typescript
|
|
100
|
-
const activeUsers = new SelectQueryBuilder(users)
|
|
101
|
-
.selectRaw('*')
|
|
102
|
-
.where(gt(users.columns.lastLogin, new Date('2023-01-01')))
|
|
103
|
-
.as('active_users');
|
|
104
|
-
|
|
105
|
-
const query = new SelectQueryBuilder(activeUsers)
|
|
106
|
-
.with(activeUsers)
|
|
107
|
-
.selectRaw('*')
|
|
108
|
-
.where(eq(activeUsers.columns.id, 1));
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
### Subqueries
|
|
112
|
-
|
|
113
|
-
Support for subqueries in SELECT and WHERE clauses:
|
|
114
|
-
|
|
115
|
-
```typescript
|
|
116
|
-
const subquery = new SelectQueryBuilder(posts)
|
|
117
|
-
.select({ count: count(posts.columns.id) })
|
|
118
|
-
.where(eq(posts.columns.userId, users.columns.id));
|
|
119
|
-
|
|
120
|
-
const query = new SelectQueryBuilder(users)
|
|
121
|
-
.select({
|
|
122
|
-
id: users.columns.id,
|
|
123
|
-
name: users.columns.name,
|
|
124
|
-
postCount: subquery
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
## From Builder to Entities
|
|
128
|
-
|
|
129
|
-
You can keep using the query builder on its own, or plug it into the entity runtime:
|
|
130
|
-
|
|
131
|
-
- `builder.compile(dialect)` → SQL + params → driver (builder-only usage).
|
|
132
|
-
- `builder.execute(ctx)` → entities tracked by an `OrmContext` (runtime usage).
|
|
133
|
-
|
|
134
|
-
See [Runtime & Unit of Work](./runtime.md) for how `execute(ctx)` integrates with entities and lazy relations.
|
|
135
|
-
```
|
package/docs/runtime.md
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
# Runtime & Unit of Work
|
|
2
|
-
|
|
3
|
-
This page describes MetalORM's optional entity runtime:
|
|
4
|
-
|
|
5
|
-
- `OrmContext` – the Unit of Work.
|
|
6
|
-
- entities – proxies wrapping hydrated rows.
|
|
7
|
-
- relation wrappers – lazy, batched collections and references.
|
|
8
|
-
|
|
9
|
-
## OrmContext
|
|
10
|
-
|
|
11
|
-
`OrmContext` owns:
|
|
12
|
-
|
|
13
|
-
- a SQL dialect,
|
|
14
|
-
- a DB executor (`executeSql(sql, params)`),
|
|
15
|
-
- an identity map (`table + primaryKey → entity`),
|
|
16
|
-
- change tracking for entities and relations,
|
|
17
|
-
- hooks and (optionally) domain event dispatch.
|
|
18
|
-
|
|
19
|
-
```ts
|
|
20
|
-
const ctx = new OrmContext({
|
|
21
|
-
dialect: new MySqlDialect(),
|
|
22
|
-
db: {
|
|
23
|
-
async executeSql(sql, params) {
|
|
24
|
-
// call your DB driver here
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
});
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
## Entities
|
|
31
|
-
|
|
32
|
-
Entities are created when you call `.execute(ctx)` on a SelectQueryBuilder.
|
|
33
|
-
|
|
34
|
-
They:
|
|
35
|
-
|
|
36
|
-
- expose table columns as properties (user.id, user.name, …)
|
|
37
|
-
- expose relations as wrappers:
|
|
38
|
-
- HasManyCollection<T> (e.g. user.posts)
|
|
39
|
-
- BelongsToReference<T> (e.g. post.author)
|
|
40
|
-
- ManyToManyCollection<T> (e.g. user.roles)
|
|
41
|
-
- track changes to fields and collections for the Unit of Work.
|
|
42
|
-
|
|
43
|
-
```ts
|
|
44
|
-
const [user] = await new SelectQueryBuilder(users)
|
|
45
|
-
.select({ id: users.columns.id, name: users.columns.name })
|
|
46
|
-
.includeLazy('posts')
|
|
47
|
-
.execute(ctx);
|
|
48
|
-
|
|
49
|
-
user.name = 'Updated Name'; // marks entity as Dirty
|
|
50
|
-
const posts = await user.posts.load(); // lazy-batched load
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
## Unit of Work
|
|
54
|
-
|
|
55
|
-
Each entity in an OrmContext has a status:
|
|
56
|
-
|
|
57
|
-
- New – created in memory and not yet persisted.
|
|
58
|
-
- Managed – loaded from the database and unchanged.
|
|
59
|
-
- Dirty – modified scalar properties.
|
|
60
|
-
- Removed – scheduled for deletion.
|
|
61
|
-
|
|
62
|
-
Relations track:
|
|
63
|
-
|
|
64
|
-
- additions (add, attach, syncByIds),
|
|
65
|
-
- removals (remove, detach).
|
|
66
|
-
|
|
67
|
-
`ctx.saveChanges()`:
|
|
68
|
-
|
|
69
|
-
- runs hooks / interceptors,
|
|
70
|
-
- flushes entity changes as INSERT / UPDATE / DELETE,
|
|
71
|
-
- flushes relation changes (FK / pivot),
|
|
72
|
-
- dispatches domain events (optional),
|
|
73
|
-
- resets tracking.
|
|
74
|
-
|
|
75
|
-
```ts
|
|
76
|
-
user.posts.add({ title: 'From entities' });
|
|
77
|
-
user.posts.remove(posts[0]);
|
|
78
|
-
|
|
79
|
-
await ctx.saveChanges();
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
## Hooks & Domain Events
|
|
83
|
-
|
|
84
|
-
Each TableDef can define hooks:
|
|
85
|
-
|
|
86
|
-
```ts
|
|
87
|
-
const users = defineTable('users', { /* ... */ }, undefined, {
|
|
88
|
-
hooks: {
|
|
89
|
-
beforeInsert(ctx, user) {
|
|
90
|
-
user.createdAt = new Date();
|
|
91
|
-
},
|
|
92
|
-
afterUpdate(ctx, user) {
|
|
93
|
-
// log audit event
|
|
94
|
-
},
|
|
95
|
-
},
|
|
96
|
-
});
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
Entities may accumulate domain events:
|
|
100
|
-
|
|
101
|
-
```ts
|
|
102
|
-
addDomainEvent(user, new UserRegisteredEvent(user.id));
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
After flushing, the context dispatches these events to registered handlers or writes them to an outbox table.
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
# Schema Definition
|
|
2
|
-
|
|
3
|
-
MetalORM allows you to define your database schema in TypeScript, providing full type inference and a single source of truth for your data structures.
|
|
4
|
-
|
|
5
|
-
## Defining Tables
|
|
6
|
-
|
|
7
|
-
You can define a table using the `defineTable` function. It takes the table name, a columns object, and an optional relations object.
|
|
8
|
-
|
|
9
|
-
```typescript
|
|
10
|
-
import { defineTable, col } from 'metal-orm';
|
|
11
|
-
|
|
12
|
-
const users = defineTable('users', {
|
|
13
|
-
id: col.int().primaryKey(),
|
|
14
|
-
name: col.varchar(255).notNull(),
|
|
15
|
-
email: col.varchar(255).unique(),
|
|
16
|
-
createdAt: col.timestamp().default('CURRENT_TIMESTAMP'),
|
|
17
|
-
});
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
## Column Types
|
|
21
|
-
|
|
22
|
-
MetalORM provides a variety of column types through the `col` object:
|
|
23
|
-
|
|
24
|
-
- `col.int()`: Integer
|
|
25
|
-
- `col.varchar(length)`: Variable-length string
|
|
26
|
-
- `col.text()`: Text
|
|
27
|
-
- `col.timestamp()`: Timestamp
|
|
28
|
-
- `col.json()`: JSON
|
|
29
|
-
- ...and more.
|
|
30
|
-
|
|
31
|
-
You can also chain modifiers to define column constraints:
|
|
32
|
-
|
|
33
|
-
- `.primaryKey()`: Marks the column as a primary key.
|
|
34
|
-
- `.notNull()`: Adds a `NOT NULL` constraint.
|
|
35
|
-
- `.unique()`: Adds a `UNIQUE` constraint.
|
|
36
|
-
- `.default(value)`: Sets a default value.
|
|
37
|
-
|
|
38
|
-
## Relations
|
|
39
|
-
|
|
40
|
-
You can define relations between tables using `hasMany`, `belongsTo`, and `belongsToMany`:
|
|
41
|
-
|
|
42
|
-
### One-to-Many Relations
|
|
43
|
-
|
|
44
|
-
```typescript
|
|
45
|
-
import { defineTable, col, hasMany } from 'metal-orm';
|
|
46
|
-
|
|
47
|
-
const posts = defineTable('posts', {
|
|
48
|
-
id: col.int().primaryKey(),
|
|
49
|
-
title: col.varchar(255).notNull(),
|
|
50
|
-
userId: col.int().notNull(),
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
const users = defineTable(
|
|
54
|
-
'users',
|
|
55
|
-
{
|
|
56
|
-
id: col.int().primaryKey(),
|
|
57
|
-
name: col.varchar(255).notNull(),
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
posts: hasMany(posts, 'userId'),
|
|
61
|
-
}
|
|
62
|
-
);
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
### Many-to-One Relations
|
|
66
|
-
|
|
67
|
-
```typescript
|
|
68
|
-
const posts = defineTable('posts', {
|
|
69
|
-
id: col.int().primaryKey(),
|
|
70
|
-
title: col.varchar(255).notNull(),
|
|
71
|
-
userId: col.int().notNull(),
|
|
72
|
-
}, {
|
|
73
|
-
author: belongsTo(users, 'userId')
|
|
74
|
-
});
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
### Many-to-Many Relations
|
|
78
|
-
|
|
79
|
-
```typescript
|
|
80
|
-
const projects = defineTable('projects', {
|
|
81
|
-
id: col.int().primaryKey(),
|
|
82
|
-
name: col.varchar(255).notNull(),
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
const projectAssignments = defineTable('project_assignments', {
|
|
86
|
-
id: col.int().primaryKey(),
|
|
87
|
-
userId: col.int().notNull(),
|
|
88
|
-
projectId: col.int().notNull(),
|
|
89
|
-
role: col.varchar(50),
|
|
90
|
-
assignedAt: col.timestamp(),
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
const users = defineTable('users', {
|
|
94
|
-
id: col.int().primaryKey(),
|
|
95
|
-
name: col.varchar(255).notNull(),
|
|
96
|
-
}, {
|
|
97
|
-
projects: belongsToMany(
|
|
98
|
-
projects,
|
|
99
|
-
projectAssignments,
|
|
100
|
-
{
|
|
101
|
-
pivotForeignKeyToRoot: 'userId',
|
|
102
|
-
pivotForeignKeyToTarget: 'projectId',
|
|
103
|
-
defaultPivotColumns: ['role', 'assignedAt']
|
|
104
|
-
}
|
|
105
|
-
)
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
> **Note**: When using the runtime, relation definitions (`hasMany`, `belongsTo`, `belongsToMany`) are also used to:
|
|
109
|
-
> - generate hydration plans for eager loading
|
|
110
|
-
> - configure lazy relation loaders
|
|
111
|
-
> - control cascade behavior in `OrmContext.saveChanges()`.
|
|
112
|
-
```
|
package/metadata.json
DELETED
|
@@ -1,94 +0,0 @@
|
|
|
1
|
-
import { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
-
import { SqliteClient } from '../../src/playground/features/playground/clients/SqliteClient';
|
|
3
|
-
import { SCENARIOS } from '../../src/playground/features/playground/data/scenarios';
|
|
4
|
-
import { QueryExecutionService } from '../../src/playground/features/playground/services/QueryExecutionService';
|
|
5
|
-
import type { ApiStatusResponse } from '../../src/playground/features/playground/api/types';
|
|
6
|
-
|
|
7
|
-
export const PLAYGROUND_API_PREFIX = '/api/playground';
|
|
8
|
-
|
|
9
|
-
const sqliteClient = new SqliteClient();
|
|
10
|
-
const queryExecutionService = new QueryExecutionService(sqliteClient);
|
|
11
|
-
|
|
12
|
-
const sendJson = (res: ServerResponse, payload: unknown, status = 200) => {
|
|
13
|
-
res.statusCode = status;
|
|
14
|
-
res.setHeader('Content-Type', 'application/json');
|
|
15
|
-
res.end(JSON.stringify(payload));
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
const parseJsonBody = (req: IncomingMessage) =>
|
|
19
|
-
new Promise<Record<string, unknown>>((resolve, reject) => {
|
|
20
|
-
const chunks: Buffer[] = [];
|
|
21
|
-
req.on('data', (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
|
|
22
|
-
req.on('end', () => {
|
|
23
|
-
const body = Buffer.concat(chunks).toString('utf8');
|
|
24
|
-
if (!body) {
|
|
25
|
-
resolve({});
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
try {
|
|
30
|
-
resolve(JSON.parse(body));
|
|
31
|
-
} catch (error) {
|
|
32
|
-
reject(error);
|
|
33
|
-
}
|
|
34
|
-
});
|
|
35
|
-
req.on('error', reject);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
const handleStatus = (_req: IncomingMessage, res: ServerResponse) => {
|
|
39
|
-
const body: ApiStatusResponse = {
|
|
40
|
-
ready: sqliteClient.isReady,
|
|
41
|
-
error: sqliteClient.error
|
|
42
|
-
};
|
|
43
|
-
sendJson(res, body);
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
const handleExecute = async (req: IncomingMessage, res: ServerResponse) => {
|
|
47
|
-
try {
|
|
48
|
-
const { scenarioId } = await parseJsonBody(req);
|
|
49
|
-
if (!scenarioId || typeof scenarioId !== 'string') {
|
|
50
|
-
sendJson(res, { error: 'Missing scenarioId' }, 400);
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const scenario = SCENARIOS.find(s => s.id === scenarioId);
|
|
55
|
-
if (!scenario) {
|
|
56
|
-
sendJson(res, { error: `Scenario '${scenarioId}' not found` }, 404);
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const result = await queryExecutionService.executeScenario(scenario);
|
|
61
|
-
sendJson(res, result);
|
|
62
|
-
} catch (error) {
|
|
63
|
-
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
64
|
-
sendJson(res, { error: message }, 500);
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
export const createPlaygroundApiMiddleware = () => {
|
|
69
|
-
return async (req: IncomingMessage, res: ServerResponse, next: () => void) => {
|
|
70
|
-
const pathname = req.url ? req.url.split('?')[0] : '';
|
|
71
|
-
|
|
72
|
-
if (req.method === 'GET' && pathname === '/status') {
|
|
73
|
-
handleStatus(req, res);
|
|
74
|
-
return;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
if (req.method === 'POST' && pathname === '/execute') {
|
|
78
|
-
await handleExecute(req, res);
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
next();
|
|
83
|
-
};
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
export const playgroundApiPlugin = () => ({
|
|
87
|
-
name: 'metal-orm-playground-api',
|
|
88
|
-
configureServer(server) {
|
|
89
|
-
server.middlewares.use(PLAYGROUND_API_PREFIX, createPlaygroundApiMiddleware());
|
|
90
|
-
},
|
|
91
|
-
configurePreviewServer(server) {
|
|
92
|
-
server.middlewares.use(PLAYGROUND_API_PREFIX, createPlaygroundApiMiddleware());
|
|
93
|
-
},
|
|
94
|
-
});
|
package/playground/index.html
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html lang="en">
|
|
3
|
-
|
|
4
|
-
<head>
|
|
5
|
-
<meta charset="UTF-8" />
|
|
6
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
-
<title>Metal ORM Playground</title>
|
|
8
|
-
</head>
|
|
9
|
-
|
|
10
|
-
<body>
|
|
11
|
-
<div id="root"></div>
|
|
12
|
-
<script type="module" src="/src/main.tsx"></script>
|
|
13
|
-
</body>
|
|
14
|
-
|
|
15
|
-
</html>
|
package/playground/src/App.css
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
/* App.css is no longer needed as we are using Mantine UI */
|