metal-orm 1.0.5 → 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +299 -113
- package/docs/CHANGES.md +104 -0
- package/docs/advanced-features.md +92 -1
- package/docs/api-reference.md +13 -4
- package/docs/dml-operations.md +156 -0
- package/docs/getting-started.md +122 -55
- package/docs/hydration.md +78 -13
- package/docs/index.md +19 -14
- package/docs/multi-dialect-support.md +25 -0
- package/docs/query-builder.md +60 -0
- package/docs/runtime.md +105 -0
- package/docs/schema-definition.md +52 -1
- package/package.json +1 -1
- package/src/ast/expression.ts +38 -18
- package/src/builder/hydration-planner.ts +74 -74
- package/src/builder/select.ts +427 -395
- package/src/constants/sql-operator-config.ts +3 -0
- package/src/constants/sql.ts +38 -32
- package/src/index.ts +16 -8
- package/src/playground/features/playground/data/scenarios/types.ts +18 -15
- package/src/playground/features/playground/data/schema.ts +10 -10
- package/src/playground/features/playground/services/QueryExecutionService.ts +2 -1
- package/src/runtime/entity-meta.ts +52 -0
- package/src/runtime/entity.ts +252 -0
- package/src/runtime/execute.ts +36 -0
- package/src/runtime/hydration.ts +99 -49
- package/src/runtime/lazy-batch.ts +205 -0
- package/src/runtime/orm-context.ts +539 -0
- package/src/runtime/relations/belongs-to.ts +92 -0
- package/src/runtime/relations/has-many.ts +111 -0
- package/src/runtime/relations/many-to-many.ts +149 -0
- package/src/schema/column.ts +15 -1
- package/src/schema/relation.ts +82 -58
- package/src/schema/table.ts +34 -22
- package/src/schema/types.ts +76 -0
- package/tests/orm-runtime.test.ts +254 -0
package/docs/runtime.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
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.
|
|
@@ -37,7 +37,9 @@ You can also chain modifiers to define column constraints:
|
|
|
37
37
|
|
|
38
38
|
## Relations
|
|
39
39
|
|
|
40
|
-
You can define relations between tables using `hasMany` and `
|
|
40
|
+
You can define relations between tables using `hasMany`, `belongsTo`, and `belongsToMany`:
|
|
41
|
+
|
|
42
|
+
### One-to-Many Relations
|
|
41
43
|
|
|
42
44
|
```typescript
|
|
43
45
|
import { defineTable, col, hasMany } from 'metal-orm';
|
|
@@ -59,3 +61,52 @@ const users = defineTable(
|
|
|
59
61
|
}
|
|
60
62
|
);
|
|
61
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/package.json
CHANGED
package/src/ast/expression.ts
CHANGED
|
@@ -270,30 +270,50 @@ const createBinaryExpression = (
|
|
|
270
270
|
* @param right - Right operand
|
|
271
271
|
* @returns Binary expression node with equality operator
|
|
272
272
|
*/
|
|
273
|
-
export const eq = (left: OperandNode | ColumnDef, right: OperandNode | ColumnDef | string | number): BinaryExpressionNode =>
|
|
274
|
-
createBinaryExpression('=', left, right);
|
|
275
|
-
|
|
273
|
+
export const eq = (left: OperandNode | ColumnDef, right: OperandNode | ColumnDef | string | number): BinaryExpressionNode =>
|
|
274
|
+
createBinaryExpression('=', left, right);
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Creates a not equal expression (left != right)
|
|
278
|
+
*/
|
|
279
|
+
export const neq = (
|
|
280
|
+
left: OperandNode | ColumnDef,
|
|
281
|
+
right: OperandNode | ColumnDef | string | number
|
|
282
|
+
): BinaryExpressionNode => createBinaryExpression('!=', left, right);
|
|
283
|
+
|
|
276
284
|
/**
|
|
277
285
|
* Creates a greater-than expression (left > right)
|
|
278
286
|
* @param left - Left operand
|
|
279
287
|
* @param right - Right operand
|
|
280
288
|
* @returns Binary expression node with greater-than operator
|
|
281
289
|
*/
|
|
282
|
-
export const gt = (left: OperandNode | ColumnDef, right: OperandNode | ColumnDef | string | number): BinaryExpressionNode =>
|
|
283
|
-
createBinaryExpression('>', left, right);
|
|
284
|
-
|
|
285
|
-
/**
|
|
286
|
-
* Creates a
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
*
|
|
296
|
-
|
|
290
|
+
export const gt = (left: OperandNode | ColumnDef, right: OperandNode | ColumnDef | string | number): BinaryExpressionNode =>
|
|
291
|
+
createBinaryExpression('>', left, right);
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Creates a greater than or equal expression (left >= right)
|
|
295
|
+
*/
|
|
296
|
+
export const gte = (left: OperandNode | ColumnDef, right: OperandNode | ColumnDef | string | number): BinaryExpressionNode =>
|
|
297
|
+
createBinaryExpression('>=', left, right);
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Creates a less-than expression (left < right)
|
|
301
|
+
* @param left - Left operand
|
|
302
|
+
* @param right - Right operand
|
|
303
|
+
* @returns Binary expression node with less-than operator
|
|
304
|
+
*/
|
|
305
|
+
export const lt = (left: OperandNode | ColumnDef, right: OperandNode | ColumnDef | string | number): BinaryExpressionNode =>
|
|
306
|
+
createBinaryExpression('<', left, right);
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Creates a less than or equal expression (left <= right)
|
|
310
|
+
*/
|
|
311
|
+
export const lte = (left: OperandNode | ColumnDef, right: OperandNode | ColumnDef | string | number): BinaryExpressionNode =>
|
|
312
|
+
createBinaryExpression('<=', left, right);
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Creates a LIKE pattern matching expression
|
|
316
|
+
* @param left - Left operand
|
|
297
317
|
* @param pattern - Pattern to match
|
|
298
318
|
* @param escape - Optional escape character
|
|
299
319
|
* @returns Binary expression node with LIKE operator
|
|
@@ -14,55 +14,55 @@ export const findPrimaryKey = (table: TableDef): string => {
|
|
|
14
14
|
const pk = Object.values(table.columns).find(c => c.primary);
|
|
15
15
|
return pk?.name || 'id';
|
|
16
16
|
};
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Manages hydration planning for query results
|
|
20
|
-
*/
|
|
21
|
-
export class HydrationPlanner {
|
|
22
|
-
/**
|
|
23
|
-
* Creates a new HydrationPlanner instance
|
|
24
|
-
* @param table - Table definition
|
|
25
|
-
* @param plan - Optional existing hydration plan
|
|
26
|
-
*/
|
|
27
|
-
constructor(private readonly table: TableDef, private readonly plan?: HydrationPlan) { }
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Captures root table columns for hydration planning
|
|
31
|
-
* @param columns - Columns to capture
|
|
32
|
-
* @returns Updated HydrationPlanner with captured columns
|
|
33
|
-
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Manages hydration planning for query results
|
|
20
|
+
*/
|
|
21
|
+
export class HydrationPlanner {
|
|
22
|
+
/**
|
|
23
|
+
* Creates a new HydrationPlanner instance
|
|
24
|
+
* @param table - Table definition
|
|
25
|
+
* @param plan - Optional existing hydration plan
|
|
26
|
+
*/
|
|
27
|
+
constructor(private readonly table: TableDef, private readonly plan?: HydrationPlan) { }
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Captures root table columns for hydration planning
|
|
31
|
+
* @param columns - Columns to capture
|
|
32
|
+
* @returns Updated HydrationPlanner with captured columns
|
|
33
|
+
*/
|
|
34
34
|
captureRootColumns(columns: ProjectionNode[]): HydrationPlanner {
|
|
35
35
|
const currentPlan = this.getPlanOrDefault();
|
|
36
|
-
const rootCols = new Set(currentPlan.rootColumns);
|
|
37
|
-
let changed = false;
|
|
38
|
-
|
|
39
|
-
columns.forEach(node => {
|
|
40
|
-
if (node.type !== 'Column') return;
|
|
41
|
-
if (node.table !== this.table.name) return;
|
|
42
|
-
|
|
43
|
-
const alias = node.alias || node.name;
|
|
44
|
-
if (isRelationAlias(alias)) return;
|
|
45
|
-
if (!rootCols.has(alias)) {
|
|
46
|
-
rootCols.add(alias);
|
|
47
|
-
changed = true;
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
if (!changed) return this;
|
|
52
|
-
return new HydrationPlanner(this.table, {
|
|
53
|
-
...currentPlan,
|
|
54
|
-
rootColumns: Array.from(rootCols)
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Includes a relation in the hydration plan
|
|
60
|
-
* @param rel - Relation definition
|
|
61
|
-
* @param relationName - Name of the relation
|
|
62
|
-
* @param aliasPrefix - Alias prefix for relation columns
|
|
63
|
-
* @param columns - Columns to include from the relation
|
|
64
|
-
* @returns Updated HydrationPlanner with included relation
|
|
65
|
-
*/
|
|
36
|
+
const rootCols = new Set(currentPlan.rootColumns);
|
|
37
|
+
let changed = false;
|
|
38
|
+
|
|
39
|
+
columns.forEach(node => {
|
|
40
|
+
if (node.type !== 'Column') return;
|
|
41
|
+
if (node.table !== this.table.name) return;
|
|
42
|
+
|
|
43
|
+
const alias = node.alias || node.name;
|
|
44
|
+
if (isRelationAlias(alias)) return;
|
|
45
|
+
if (!rootCols.has(alias)) {
|
|
46
|
+
rootCols.add(alias);
|
|
47
|
+
changed = true;
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (!changed) return this;
|
|
52
|
+
return new HydrationPlanner(this.table, {
|
|
53
|
+
...currentPlan,
|
|
54
|
+
rootColumns: Array.from(rootCols)
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Includes a relation in the hydration plan
|
|
60
|
+
* @param rel - Relation definition
|
|
61
|
+
* @param relationName - Name of the relation
|
|
62
|
+
* @param aliasPrefix - Alias prefix for relation columns
|
|
63
|
+
* @param columns - Columns to include from the relation
|
|
64
|
+
* @returns Updated HydrationPlanner with included relation
|
|
65
|
+
*/
|
|
66
66
|
includeRelation(
|
|
67
67
|
rel: RelationDef,
|
|
68
68
|
relationName: string,
|
|
@@ -78,31 +78,31 @@ export class HydrationPlanner {
|
|
|
78
78
|
relations
|
|
79
79
|
});
|
|
80
80
|
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Gets the current hydration plan
|
|
84
|
-
* @returns Current hydration plan or undefined
|
|
85
|
-
*/
|
|
86
|
-
getPlan(): HydrationPlan | undefined {
|
|
87
|
-
return this.plan;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Gets the current hydration plan or creates a default one
|
|
92
|
-
* @returns Current hydration plan or default plan
|
|
93
|
-
*/
|
|
94
|
-
private getPlanOrDefault(): HydrationPlan {
|
|
95
|
-
return this.plan ?? buildDefaultHydrationPlan(this.table);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Builds a relation plan for hydration
|
|
100
|
-
* @param rel - Relation definition
|
|
101
|
-
* @param relationName - Name of the relation
|
|
102
|
-
* @param aliasPrefix - Alias prefix for relation columns
|
|
103
|
-
* @param columns - Columns to include from the relation
|
|
104
|
-
* @returns Hydration relation plan
|
|
105
|
-
*/
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Gets the current hydration plan
|
|
84
|
+
* @returns Current hydration plan or undefined
|
|
85
|
+
*/
|
|
86
|
+
getPlan(): HydrationPlan | undefined {
|
|
87
|
+
return this.plan;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Gets the current hydration plan or creates a default one
|
|
92
|
+
* @returns Current hydration plan or default plan
|
|
93
|
+
*/
|
|
94
|
+
private getPlanOrDefault(): HydrationPlan {
|
|
95
|
+
return this.plan ?? buildDefaultHydrationPlan(this.table);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Builds a relation plan for hydration
|
|
100
|
+
* @param rel - Relation definition
|
|
101
|
+
* @param relationName - Name of the relation
|
|
102
|
+
* @param aliasPrefix - Alias prefix for relation columns
|
|
103
|
+
* @param columns - Columns to include from the relation
|
|
104
|
+
* @returns Hydration relation plan
|
|
105
|
+
*/
|
|
106
106
|
private buildRelationPlan(
|
|
107
107
|
rel: RelationDef,
|
|
108
108
|
relationName: string,
|
|
@@ -167,8 +167,8 @@ export class HydrationPlanner {
|
|
|
167
167
|
}
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
|
-
}
|
|
171
|
-
|
|
170
|
+
}
|
|
171
|
+
|
|
172
172
|
/**
|
|
173
173
|
* Builds a default hydration plan for a table
|
|
174
174
|
* @param table - Table definition
|