metal-orm 1.0.5 → 1.0.6
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
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { HasManyCollection } from '../../schema/types';
|
|
2
|
+
import { OrmContext, RelationKey } from '../orm-context';
|
|
3
|
+
import { HasManyRelation } from '../../schema/relation';
|
|
4
|
+
import { TableDef } from '../../schema/table';
|
|
5
|
+
import { EntityMeta, getHydrationRows } from '../entity-meta';
|
|
6
|
+
|
|
7
|
+
type Rows = Record<string, any>[];
|
|
8
|
+
|
|
9
|
+
const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
|
|
10
|
+
|
|
11
|
+
export class DefaultHasManyCollection<TChild> implements HasManyCollection<TChild> {
|
|
12
|
+
private loaded = false;
|
|
13
|
+
private items: TChild[] = [];
|
|
14
|
+
private readonly added = new Set<TChild>();
|
|
15
|
+
private readonly removed = new Set<TChild>();
|
|
16
|
+
|
|
17
|
+
constructor(
|
|
18
|
+
private readonly ctx: OrmContext,
|
|
19
|
+
private readonly meta: EntityMeta<any>,
|
|
20
|
+
private readonly root: any,
|
|
21
|
+
private readonly relationName: string,
|
|
22
|
+
private readonly relation: HasManyRelation,
|
|
23
|
+
private readonly rootTable: TableDef,
|
|
24
|
+
private readonly loader: () => Promise<Map<string, Rows>>,
|
|
25
|
+
private readonly createEntity: (row: Record<string, any>) => TChild,
|
|
26
|
+
private readonly localKey: string
|
|
27
|
+
) {
|
|
28
|
+
this.hydrateFromCache();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async load(): Promise<TChild[]> {
|
|
32
|
+
if (this.loaded) return this.items;
|
|
33
|
+
const map = await this.loader();
|
|
34
|
+
const key = toKey(this.root[this.localKey]);
|
|
35
|
+
const rows = map.get(key) ?? [];
|
|
36
|
+
this.items = rows.map(row => this.createEntity(row));
|
|
37
|
+
this.loaded = true;
|
|
38
|
+
return this.items;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
getItems(): TChild[] {
|
|
42
|
+
return this.items;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
add(data: Partial<TChild>): TChild {
|
|
46
|
+
const keyValue = this.root[this.localKey];
|
|
47
|
+
const childRow: Record<string, any> = {
|
|
48
|
+
...data,
|
|
49
|
+
[this.relation.foreignKey]: keyValue
|
|
50
|
+
};
|
|
51
|
+
const entity = this.createEntity(childRow);
|
|
52
|
+
this.added.add(entity);
|
|
53
|
+
this.items.push(entity);
|
|
54
|
+
this.ctx.registerRelationChange(
|
|
55
|
+
this.root,
|
|
56
|
+
this.relationKey,
|
|
57
|
+
this.rootTable,
|
|
58
|
+
this.relationName,
|
|
59
|
+
this.relation,
|
|
60
|
+
{ kind: 'add', entity }
|
|
61
|
+
);
|
|
62
|
+
return entity;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
attach(entity: TChild): void {
|
|
66
|
+
const keyValue = this.root[this.localKey];
|
|
67
|
+
(entity as Record<string, any>)[this.relation.foreignKey] = keyValue;
|
|
68
|
+
this.ctx.markDirty(entity);
|
|
69
|
+
this.items.push(entity);
|
|
70
|
+
this.ctx.registerRelationChange(
|
|
71
|
+
this.root,
|
|
72
|
+
this.relationKey,
|
|
73
|
+
this.rootTable,
|
|
74
|
+
this.relationName,
|
|
75
|
+
this.relation,
|
|
76
|
+
{ kind: 'attach', entity }
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
remove(entity: TChild): void {
|
|
81
|
+
this.items = this.items.filter(item => item !== entity);
|
|
82
|
+
this.removed.add(entity);
|
|
83
|
+
this.ctx.registerRelationChange(
|
|
84
|
+
this.root,
|
|
85
|
+
this.relationKey,
|
|
86
|
+
this.rootTable,
|
|
87
|
+
this.relationName,
|
|
88
|
+
this.relation,
|
|
89
|
+
{ kind: 'remove', entity }
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
clear(): void {
|
|
94
|
+
for (const entity of [...this.items]) {
|
|
95
|
+
this.remove(entity);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private get relationKey(): RelationKey {
|
|
100
|
+
return `${this.rootTable.name}.${this.relationName}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private hydrateFromCache(): void {
|
|
104
|
+
const keyValue = this.root[this.localKey];
|
|
105
|
+
if (keyValue === undefined || keyValue === null) return;
|
|
106
|
+
const rows = getHydrationRows(this.meta, this.relationName, keyValue);
|
|
107
|
+
if (!rows?.length) return;
|
|
108
|
+
this.items = rows.map(row => this.createEntity(row));
|
|
109
|
+
this.loaded = true;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { ManyToManyCollection } from '../../schema/types';
|
|
2
|
+
import { OrmContext, RelationKey } from '../orm-context';
|
|
3
|
+
import { BelongsToManyRelation } from '../../schema/relation';
|
|
4
|
+
import { TableDef } from '../../schema/table';
|
|
5
|
+
import { findPrimaryKey } from '../../builder/hydration-planner';
|
|
6
|
+
import { EntityMeta, getHydrationRows } from '../entity-meta';
|
|
7
|
+
|
|
8
|
+
type Rows = Record<string, any>[];
|
|
9
|
+
|
|
10
|
+
const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
|
|
11
|
+
|
|
12
|
+
export class DefaultManyToManyCollection<TTarget> implements ManyToManyCollection<TTarget> {
|
|
13
|
+
private loaded = false;
|
|
14
|
+
private items: TTarget[] = [];
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
private readonly ctx: OrmContext,
|
|
18
|
+
private readonly meta: EntityMeta<any>,
|
|
19
|
+
private readonly root: any,
|
|
20
|
+
private readonly relationName: string,
|
|
21
|
+
private readonly relation: BelongsToManyRelation,
|
|
22
|
+
private readonly rootTable: TableDef,
|
|
23
|
+
private readonly loader: () => Promise<Map<string, Rows>>,
|
|
24
|
+
private readonly createEntity: (row: Record<string, any>) => TTarget,
|
|
25
|
+
private readonly localKey: string
|
|
26
|
+
) {
|
|
27
|
+
this.hydrateFromCache();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async load(): Promise<TTarget[]> {
|
|
31
|
+
if (this.loaded) return this.items;
|
|
32
|
+
const map = await this.loader();
|
|
33
|
+
const key = toKey(this.root[this.localKey]);
|
|
34
|
+
const rows = map.get(key) ?? [];
|
|
35
|
+
this.items = rows.map(row => {
|
|
36
|
+
const entity = this.createEntity(row);
|
|
37
|
+
if ((row as any)._pivot) {
|
|
38
|
+
(entity as any)._pivot = row._pivot;
|
|
39
|
+
}
|
|
40
|
+
return entity;
|
|
41
|
+
});
|
|
42
|
+
this.loaded = true;
|
|
43
|
+
return this.items;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
getItems(): TTarget[] {
|
|
47
|
+
return this.items;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
attach(target: TTarget | number | string): void {
|
|
51
|
+
const entity = this.ensureEntity(target);
|
|
52
|
+
const id = this.extractId(entity);
|
|
53
|
+
if (id == null) return;
|
|
54
|
+
if (this.items.some(item => this.extractId(item) === id)) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
this.items.push(entity);
|
|
58
|
+
this.ctx.registerRelationChange(
|
|
59
|
+
this.root,
|
|
60
|
+
this.relationKey,
|
|
61
|
+
this.rootTable,
|
|
62
|
+
this.relationName,
|
|
63
|
+
this.relation,
|
|
64
|
+
{ kind: 'attach', entity }
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
detach(target: TTarget | number | string): void {
|
|
69
|
+
const id = typeof target === 'number' || typeof target === 'string'
|
|
70
|
+
? target
|
|
71
|
+
: this.extractId(target);
|
|
72
|
+
|
|
73
|
+
if (id == null) return;
|
|
74
|
+
|
|
75
|
+
const existing = this.items.find(item => this.extractId(item) === id);
|
|
76
|
+
if (!existing) return;
|
|
77
|
+
|
|
78
|
+
this.items = this.items.filter(item => this.extractId(item) !== id);
|
|
79
|
+
this.ctx.registerRelationChange(
|
|
80
|
+
this.root,
|
|
81
|
+
this.relationKey,
|
|
82
|
+
this.rootTable,
|
|
83
|
+
this.relationName,
|
|
84
|
+
this.relation,
|
|
85
|
+
{ kind: 'detach', entity: existing }
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async syncByIds(ids: (number | string)[]): Promise<void> {
|
|
90
|
+
await this.load();
|
|
91
|
+
const targetKey = this.relation.targetKey || findPrimaryKey(this.relation.target);
|
|
92
|
+
const normalized = new Set(ids.map(id => toKey(id)));
|
|
93
|
+
const currentIds = new Set(this.items.map(item => toKey(this.extractId(item))));
|
|
94
|
+
|
|
95
|
+
for (const id of normalized) {
|
|
96
|
+
if (!currentIds.has(id)) {
|
|
97
|
+
this.attach(id);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for (const item of [...this.items]) {
|
|
102
|
+
const itemId = toKey(this.extractId(item));
|
|
103
|
+
if (!normalized.has(itemId)) {
|
|
104
|
+
this.detach(item);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private ensureEntity(target: TTarget | number | string): TTarget {
|
|
110
|
+
if (typeof target === 'number' || typeof target === 'string') {
|
|
111
|
+
const stub: Record<string, any> = {
|
|
112
|
+
[this.targetKey]: target
|
|
113
|
+
};
|
|
114
|
+
return this.createEntity(stub);
|
|
115
|
+
}
|
|
116
|
+
return target;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private extractId(entity: TTarget | number | string | null | undefined): number | string | null {
|
|
120
|
+
if (entity === null || entity === undefined) return null;
|
|
121
|
+
if (typeof entity === 'number' || typeof entity === 'string') {
|
|
122
|
+
return entity;
|
|
123
|
+
}
|
|
124
|
+
return (entity as any)[this.targetKey] ?? null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private get relationKey(): RelationKey {
|
|
128
|
+
return `${this.rootTable.name}.${this.relationName}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private get targetKey(): string {
|
|
132
|
+
return this.relation.targetKey || findPrimaryKey(this.relation.target);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private hydrateFromCache(): void {
|
|
136
|
+
const keyValue = this.root[this.localKey];
|
|
137
|
+
if (keyValue === undefined || keyValue === null) return;
|
|
138
|
+
const rows = getHydrationRows(this.meta, this.relationName, keyValue);
|
|
139
|
+
if (!rows?.length) return;
|
|
140
|
+
this.items = rows.map(row => {
|
|
141
|
+
const entity = this.createEntity(row);
|
|
142
|
+
if ((row as any)._pivot) {
|
|
143
|
+
(entity as any)._pivot = (row as any)._pivot;
|
|
144
|
+
}
|
|
145
|
+
return entity;
|
|
146
|
+
});
|
|
147
|
+
this.loaded = true;
|
|
148
|
+
}
|
|
149
|
+
}
|
package/src/schema/column.ts
CHANGED
|
@@ -1,7 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Supported column data types for database schema definitions
|
|
3
3
|
*/
|
|
4
|
-
export type ColumnType =
|
|
4
|
+
export type ColumnType =
|
|
5
|
+
| 'INT'
|
|
6
|
+
| 'INTEGER'
|
|
7
|
+
| 'VARCHAR'
|
|
8
|
+
| 'TEXT'
|
|
9
|
+
| 'JSON'
|
|
10
|
+
| 'ENUM'
|
|
11
|
+
| 'BOOLEAN'
|
|
12
|
+
| 'int'
|
|
13
|
+
| 'integer'
|
|
14
|
+
| 'varchar'
|
|
15
|
+
| 'text'
|
|
16
|
+
| 'json'
|
|
17
|
+
| 'enum'
|
|
18
|
+
| 'boolean';
|
|
5
19
|
|
|
6
20
|
/**
|
|
7
21
|
* Definition of a database column
|
package/src/schema/relation.ts
CHANGED
|
@@ -17,32 +17,36 @@ export const RelationKinds = {
|
|
|
17
17
|
*/
|
|
18
18
|
export type RelationType = (typeof RelationKinds)[keyof typeof RelationKinds];
|
|
19
19
|
|
|
20
|
+
export type CascadeMode = 'none' | 'all' | 'persist' | 'remove' | 'link';
|
|
21
|
+
|
|
20
22
|
/**
|
|
21
23
|
* One-to-many relationship definition
|
|
22
24
|
*/
|
|
23
|
-
export interface HasManyRelation {
|
|
25
|
+
export interface HasManyRelation<TTarget extends TableDef = TableDef> {
|
|
24
26
|
type: typeof RelationKinds.HasMany;
|
|
25
|
-
target:
|
|
27
|
+
target: TTarget;
|
|
26
28
|
foreignKey: string;
|
|
27
29
|
localKey?: string;
|
|
30
|
+
cascade?: CascadeMode;
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
/**
|
|
31
34
|
* Many-to-one relationship definition
|
|
32
35
|
*/
|
|
33
|
-
export interface BelongsToRelation {
|
|
36
|
+
export interface BelongsToRelation<TTarget extends TableDef = TableDef> {
|
|
34
37
|
type: typeof RelationKinds.BelongsTo;
|
|
35
|
-
target:
|
|
38
|
+
target: TTarget;
|
|
36
39
|
foreignKey: string;
|
|
37
40
|
localKey?: string;
|
|
41
|
+
cascade?: CascadeMode;
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
/**
|
|
41
45
|
* Many-to-many relationship definition with rich pivot metadata
|
|
42
46
|
*/
|
|
43
|
-
export interface BelongsToManyRelation {
|
|
47
|
+
export interface BelongsToManyRelation<TTarget extends TableDef = TableDef> {
|
|
44
48
|
type: typeof RelationKinds.BelongsToMany;
|
|
45
|
-
target:
|
|
49
|
+
target: TTarget;
|
|
46
50
|
pivotTable: TableDef;
|
|
47
51
|
pivotForeignKeyToRoot: string;
|
|
48
52
|
pivotForeignKeyToTarget: string;
|
|
@@ -50,70 +54,89 @@ export interface BelongsToManyRelation {
|
|
|
50
54
|
targetKey?: string;
|
|
51
55
|
pivotPrimaryKey?: string;
|
|
52
56
|
defaultPivotColumns?: string[];
|
|
57
|
+
cascade?: CascadeMode;
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
/**
|
|
56
61
|
* Union type representing any supported relationship definition
|
|
57
62
|
*/
|
|
58
|
-
export type RelationDef =
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
*
|
|
65
|
-
* @
|
|
66
|
-
*
|
|
67
|
-
* @
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
|
|
72
|
-
|
|
63
|
+
export type RelationDef =
|
|
64
|
+
| HasManyRelation
|
|
65
|
+
| BelongsToRelation
|
|
66
|
+
| BelongsToManyRelation;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Creates a one-to-many relationship definition
|
|
70
|
+
* @param target - Target table of the relationship
|
|
71
|
+
* @param foreignKey - Foreign key column name on the child table
|
|
72
|
+
* @param localKey - Local key column name (optional)
|
|
73
|
+
* @returns HasManyRelation definition
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* ```typescript
|
|
77
|
+
* hasMany(usersTable, 'user_id')
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
export const hasMany = <TTarget extends TableDef>(
|
|
81
|
+
target: TTarget,
|
|
82
|
+
foreignKey: string,
|
|
83
|
+
localKey?: string,
|
|
84
|
+
cascade?: CascadeMode
|
|
85
|
+
): HasManyRelation<TTarget> => ({
|
|
73
86
|
type: RelationKinds.HasMany,
|
|
74
87
|
target,
|
|
75
88
|
foreignKey,
|
|
76
|
-
localKey
|
|
89
|
+
localKey,
|
|
90
|
+
cascade
|
|
77
91
|
});
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Creates a many-to-one relationship definition
|
|
81
|
-
* @param target - Target table of the relationship
|
|
82
|
-
* @param foreignKey - Foreign key column name on the child table
|
|
83
|
-
* @param localKey - Local key column name (optional)
|
|
84
|
-
* @returns BelongsToRelation definition
|
|
85
|
-
*
|
|
86
|
-
* @example
|
|
87
|
-
* ```typescript
|
|
88
|
-
* belongsTo(usersTable, 'user_id')
|
|
89
|
-
* ```
|
|
90
|
-
*/
|
|
91
|
-
export const belongsTo =
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Creates a many-to-one relationship definition
|
|
95
|
+
* @param target - Target table of the relationship
|
|
96
|
+
* @param foreignKey - Foreign key column name on the child table
|
|
97
|
+
* @param localKey - Local key column name (optional)
|
|
98
|
+
* @returns BelongsToRelation definition
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```typescript
|
|
102
|
+
* belongsTo(usersTable, 'user_id')
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
export const belongsTo = <TTarget extends TableDef>(
|
|
106
|
+
target: TTarget,
|
|
107
|
+
foreignKey: string,
|
|
108
|
+
localKey?: string,
|
|
109
|
+
cascade?: CascadeMode
|
|
110
|
+
): BelongsToRelation<TTarget> => ({
|
|
92
111
|
type: RelationKinds.BelongsTo,
|
|
93
112
|
target,
|
|
94
113
|
foreignKey,
|
|
95
|
-
localKey
|
|
114
|
+
localKey,
|
|
115
|
+
cascade
|
|
96
116
|
});
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Creates a many-to-many relationship definition with pivot metadata
|
|
100
|
-
* @param target - Target table
|
|
101
|
-
* @param pivotTable - Intermediate pivot table definition
|
|
102
|
-
* @param options - Pivot metadata configuration
|
|
103
|
-
* @returns BelongsToManyRelation definition
|
|
104
|
-
*/
|
|
105
|
-
export const belongsToMany =
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Creates a many-to-many relationship definition with pivot metadata
|
|
120
|
+
* @param target - Target table
|
|
121
|
+
* @param pivotTable - Intermediate pivot table definition
|
|
122
|
+
* @param options - Pivot metadata configuration
|
|
123
|
+
* @returns BelongsToManyRelation definition
|
|
124
|
+
*/
|
|
125
|
+
export const belongsToMany = <
|
|
126
|
+
TTarget extends TableDef
|
|
127
|
+
>(
|
|
128
|
+
target: TTarget,
|
|
129
|
+
pivotTable: TableDef,
|
|
130
|
+
options: {
|
|
131
|
+
pivotForeignKeyToRoot: string;
|
|
132
|
+
pivotForeignKeyToTarget: string;
|
|
133
|
+
localKey?: string;
|
|
134
|
+
targetKey?: string;
|
|
135
|
+
pivotPrimaryKey?: string;
|
|
136
|
+
defaultPivotColumns?: string[];
|
|
137
|
+
cascade?: CascadeMode;
|
|
138
|
+
}
|
|
139
|
+
): BelongsToManyRelation<TTarget> => ({
|
|
117
140
|
type: RelationKinds.BelongsToMany,
|
|
118
141
|
target,
|
|
119
142
|
pivotTable,
|
|
@@ -122,5 +145,6 @@ export const belongsToMany = (
|
|
|
122
145
|
localKey: options.localKey,
|
|
123
146
|
targetKey: options.targetKey,
|
|
124
147
|
pivotPrimaryKey: options.pivotPrimaryKey,
|
|
125
|
-
defaultPivotColumns: options.defaultPivotColumns
|
|
148
|
+
defaultPivotColumns: options.defaultPivotColumns,
|
|
149
|
+
cascade: options.cascade
|
|
126
150
|
});
|
package/src/schema/table.ts
CHANGED
|
@@ -1,18 +1,29 @@
|
|
|
1
|
-
import { ColumnDef } from './column';
|
|
2
|
-
import { RelationDef } from './relation';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
1
|
+
import { ColumnDef } from './column';
|
|
2
|
+
import { RelationDef } from './relation';
|
|
3
|
+
|
|
4
|
+
export interface TableHooks {
|
|
5
|
+
beforeInsert?(ctx: unknown, entity: any): Promise<void> | void;
|
|
6
|
+
afterInsert?(ctx: unknown, entity: any): Promise<void> | void;
|
|
7
|
+
beforeUpdate?(ctx: unknown, entity: any): Promise<void> | void;
|
|
8
|
+
afterUpdate?(ctx: unknown, entity: any): Promise<void> | void;
|
|
9
|
+
beforeDelete?(ctx: unknown, entity: any): Promise<void> | void;
|
|
10
|
+
afterDelete?(ctx: unknown, entity: any): Promise<void> | void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Definition of a database table with its columns and relationships
|
|
15
|
+
* @typeParam T - Type of the columns record
|
|
16
|
+
*/
|
|
17
|
+
export interface TableDef<T extends Record<string, ColumnDef> = Record<string, ColumnDef>> {
|
|
18
|
+
/** Name of the table */
|
|
19
|
+
name: string;
|
|
20
|
+
/** Record of column definitions keyed by column name */
|
|
21
|
+
columns: T;
|
|
22
|
+
/** Record of relationship definitions keyed by relation name */
|
|
23
|
+
relations: Record<string, RelationDef>;
|
|
24
|
+
/** Optional lifecycle hooks */
|
|
25
|
+
hooks?: TableHooks;
|
|
26
|
+
}
|
|
16
27
|
|
|
17
28
|
/**
|
|
18
29
|
* Creates a table definition with columns and relationships
|
|
@@ -31,16 +42,17 @@ export interface TableDef<T extends Record<string, ColumnDef> = Record<string, C
|
|
|
31
42
|
* });
|
|
32
43
|
* ```
|
|
33
44
|
*/
|
|
34
|
-
export const defineTable = <T extends Record<string, ColumnDef>>(
|
|
35
|
-
name: string,
|
|
36
|
-
columns: T,
|
|
37
|
-
relations: Record<string, RelationDef> = {}
|
|
38
|
-
|
|
45
|
+
export const defineTable = <T extends Record<string, ColumnDef>>(
|
|
46
|
+
name: string,
|
|
47
|
+
columns: T,
|
|
48
|
+
relations: Record<string, RelationDef> = {},
|
|
49
|
+
hooks?: TableHooks
|
|
50
|
+
): TableDef<T> => {
|
|
39
51
|
// Runtime mutability to assign names to column definitions for convenience
|
|
40
52
|
const colsWithNames = Object.entries(columns).reduce((acc, [key, def]) => {
|
|
41
53
|
(acc as any)[key] = { ...def, name: key, table: name };
|
|
42
54
|
return acc;
|
|
43
55
|
}, {} as T);
|
|
44
56
|
|
|
45
|
-
return { name, columns: colsWithNames, relations };
|
|
46
|
-
};
|
|
57
|
+
return { name, columns: colsWithNames, relations, hooks };
|
|
58
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { ColumnDef } from './column';
|
|
2
|
+
import { TableDef } from './table';
|
|
3
|
+
import {
|
|
4
|
+
RelationDef,
|
|
5
|
+
HasManyRelation,
|
|
6
|
+
BelongsToRelation,
|
|
7
|
+
BelongsToManyRelation
|
|
8
|
+
} from './relation';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Maps a ColumnDef to its TypeScript type representation
|
|
12
|
+
*/
|
|
13
|
+
export type ColumnToTs<T extends ColumnDef> =
|
|
14
|
+
T['type'] extends 'INT' | 'INTEGER' | 'int' | 'integer' ? number :
|
|
15
|
+
T['type'] extends 'BOOLEAN' | 'boolean' ? boolean :
|
|
16
|
+
T['type'] extends 'JSON' | 'json' ? unknown :
|
|
17
|
+
string;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Infers a row shape from a table definition
|
|
21
|
+
*/
|
|
22
|
+
export type InferRow<TTable extends TableDef> = {
|
|
23
|
+
[K in keyof TTable['columns']]: ColumnToTs<TTable['columns'][K]>;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type RelationResult<T extends RelationDef> =
|
|
27
|
+
T extends HasManyRelation<infer TTarget> ? InferRow<TTarget>[] :
|
|
28
|
+
T extends BelongsToRelation<infer TTarget> ? InferRow<TTarget> | null :
|
|
29
|
+
T extends BelongsToManyRelation<infer TTarget> ? (InferRow<TTarget> & { _pivot?: any })[] :
|
|
30
|
+
never;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Maps relation names to the expected row results
|
|
34
|
+
*/
|
|
35
|
+
export type RelationMap<TTable extends TableDef> = {
|
|
36
|
+
[K in keyof TTable['relations']]: RelationResult<TTable['relations'][K]>;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export interface HasManyCollection<TChild> {
|
|
40
|
+
load(): Promise<TChild[]>;
|
|
41
|
+
getItems(): TChild[];
|
|
42
|
+
add(data: Partial<TChild>): TChild;
|
|
43
|
+
attach(entity: TChild): void;
|
|
44
|
+
remove(entity: TChild): void;
|
|
45
|
+
clear(): void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface BelongsToReference<TParent> {
|
|
49
|
+
load(): Promise<TParent | null>;
|
|
50
|
+
get(): TParent | null;
|
|
51
|
+
set(data: Partial<TParent> | TParent | null): TParent | null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ManyToManyCollection<TTarget> {
|
|
55
|
+
load(): Promise<TTarget[]>;
|
|
56
|
+
getItems(): TTarget[];
|
|
57
|
+
attach(target: TTarget | number | string): void;
|
|
58
|
+
detach(target: TTarget | number | string): void;
|
|
59
|
+
syncByIds(ids: (number | string)[]): Promise<void>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type Entity<
|
|
63
|
+
TTable extends TableDef,
|
|
64
|
+
TRow = InferRow<TTable>
|
|
65
|
+
> = TRow & {
|
|
66
|
+
[K in keyof RelationMap<TTable>]:
|
|
67
|
+
TTable['relations'][K] extends HasManyRelation<infer TTarget>
|
|
68
|
+
? HasManyCollection<Entity<TTarget>>
|
|
69
|
+
: TTable['relations'][K] extends BelongsToManyRelation<infer TTarget>
|
|
70
|
+
? ManyToManyCollection<Entity<TTarget>>
|
|
71
|
+
: TTable['relations'][K] extends BelongsToRelation<infer TTarget>
|
|
72
|
+
? BelongsToReference<Entity<TTarget>>
|
|
73
|
+
: never;
|
|
74
|
+
} & {
|
|
75
|
+
$load<K extends keyof RelationMap<TTable>>(relation: K): Promise<RelationMap<TTable>[K]>;
|
|
76
|
+
};
|