metal-orm 1.0.8 → 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 +12 -1
- 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
|
@@ -1,539 +0,0 @@
|
|
|
1
|
-
import { Dialect, CompiledQuery } from '../dialect/abstract';
|
|
2
|
-
import { findPrimaryKey } from '../builder/hydration-planner';
|
|
3
|
-
import { TableDef, TableHooks } from '../schema/table';
|
|
4
|
-
import {
|
|
5
|
-
RelationDef,
|
|
6
|
-
HasManyRelation,
|
|
7
|
-
BelongsToRelation,
|
|
8
|
-
BelongsToManyRelation,
|
|
9
|
-
RelationKinds
|
|
10
|
-
} from '../schema/relation';
|
|
11
|
-
import { InsertQueryBuilder } from '../builder/insert';
|
|
12
|
-
import { UpdateQueryBuilder } from '../builder/update';
|
|
13
|
-
import { DeleteQueryBuilder } from '../builder/delete';
|
|
14
|
-
import { and, eq } from '../ast/expression';
|
|
15
|
-
|
|
16
|
-
export type QueryResult = {
|
|
17
|
-
columns: string[];
|
|
18
|
-
values: unknown[][];
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export interface DbExecutor {
|
|
22
|
-
executeSql(sql: string, params?: unknown[]): Promise<QueryResult[]>;
|
|
23
|
-
beginTransaction?(): Promise<void>;
|
|
24
|
-
commitTransaction?(): Promise<void>;
|
|
25
|
-
rollbackTransaction?(): Promise<void>;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export interface OrmInterceptor {
|
|
29
|
-
beforeFlush?(ctx: OrmContext): Promise<void> | void;
|
|
30
|
-
afterFlush?(ctx: OrmContext): Promise<void> | void;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface DomainEventHandler {
|
|
34
|
-
(event: any, ctx: OrmContext): Promise<void> | void;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export interface HasDomainEvents {
|
|
38
|
-
domainEvents?: any[];
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export type RelationKey = string;
|
|
42
|
-
|
|
43
|
-
export type RelationChange<T> =
|
|
44
|
-
| { kind: 'add'; entity: T }
|
|
45
|
-
| { kind: 'attach'; entity: T }
|
|
46
|
-
| { kind: 'remove'; entity: T }
|
|
47
|
-
| { kind: 'detach'; entity: T };
|
|
48
|
-
|
|
49
|
-
export interface RelationChangeEntry {
|
|
50
|
-
root: any;
|
|
51
|
-
relationKey: RelationKey;
|
|
52
|
-
rootTable: TableDef;
|
|
53
|
-
relationName: string;
|
|
54
|
-
relation: RelationDef;
|
|
55
|
-
change: RelationChange<any>;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export enum EntityStatus {
|
|
59
|
-
New = 'new',
|
|
60
|
-
Managed = 'managed',
|
|
61
|
-
Dirty = 'dirty',
|
|
62
|
-
Removed = 'removed',
|
|
63
|
-
Detached = 'detached'
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
interface TrackedEntity {
|
|
67
|
-
table: TableDef;
|
|
68
|
-
entity: any;
|
|
69
|
-
pk: string | number | null;
|
|
70
|
-
status: EntityStatus;
|
|
71
|
-
original: Record<string, any> | null;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export interface OrmContextOptions {
|
|
75
|
-
dialect: Dialect;
|
|
76
|
-
executor: DbExecutor;
|
|
77
|
-
interceptors?: OrmInterceptor[];
|
|
78
|
-
domainEventHandlers?: Record<string, DomainEventHandler[]>;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export class OrmContext {
|
|
82
|
-
private readonly identityMap = new Map<string, Map<string, TrackedEntity>>();
|
|
83
|
-
private readonly trackedEntities = new Map<any, TrackedEntity>();
|
|
84
|
-
private readonly relationChanges: RelationChangeEntry[] = [];
|
|
85
|
-
private readonly interceptors: OrmInterceptor[];
|
|
86
|
-
private readonly domainEventHandlers = new Map<string, DomainEventHandler[]>();
|
|
87
|
-
|
|
88
|
-
constructor(private readonly options: OrmContextOptions) {
|
|
89
|
-
this.interceptors = [...(options.interceptors ?? [])];
|
|
90
|
-
const handlers = options.domainEventHandlers ?? {};
|
|
91
|
-
Object.entries(handlers).forEach(([name, list]) => {
|
|
92
|
-
this.domainEventHandlers.set(name, [...list]);
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
get dialect(): Dialect {
|
|
97
|
-
return this.options.dialect;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
get executor(): DbExecutor {
|
|
101
|
-
return this.options.executor;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
get identityBuckets(): Map<string, Map<string, TrackedEntity>> {
|
|
105
|
-
return this.identityMap;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
get tracked(): TrackedEntity[] {
|
|
109
|
-
return Array.from(this.trackedEntities.values());
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
getEntity(table: TableDef, pk: string | number): any | undefined {
|
|
113
|
-
const bucket = this.identityMap.get(table.name);
|
|
114
|
-
return bucket?.get(this.toIdentityKey(pk))?.entity;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
setEntity(table: TableDef, pk: string | number, entity: any): void {
|
|
118
|
-
if (pk === null || pk === undefined) return;
|
|
119
|
-
let tracked = this.trackedEntities.get(entity);
|
|
120
|
-
if (!tracked) {
|
|
121
|
-
tracked = {
|
|
122
|
-
table,
|
|
123
|
-
entity,
|
|
124
|
-
pk,
|
|
125
|
-
status: EntityStatus.Managed,
|
|
126
|
-
original: this.createSnapshot(table, entity)
|
|
127
|
-
};
|
|
128
|
-
this.trackedEntities.set(entity, tracked);
|
|
129
|
-
} else {
|
|
130
|
-
tracked.pk = pk;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
this.registerIdentity(tracked);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
trackNew(table: TableDef, entity: any, pk?: string | number): void {
|
|
137
|
-
const tracked: TrackedEntity = {
|
|
138
|
-
table,
|
|
139
|
-
entity,
|
|
140
|
-
pk: pk ?? null,
|
|
141
|
-
status: EntityStatus.New,
|
|
142
|
-
original: null
|
|
143
|
-
};
|
|
144
|
-
this.trackedEntities.set(entity, tracked);
|
|
145
|
-
if (pk != null) {
|
|
146
|
-
this.registerIdentity(tracked);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
trackManaged(table: TableDef, pk: string | number, entity: any): void {
|
|
151
|
-
const tracked: TrackedEntity = {
|
|
152
|
-
table,
|
|
153
|
-
entity,
|
|
154
|
-
pk,
|
|
155
|
-
status: EntityStatus.Managed,
|
|
156
|
-
original: this.createSnapshot(table, entity)
|
|
157
|
-
};
|
|
158
|
-
this.trackedEntities.set(entity, tracked);
|
|
159
|
-
this.registerIdentity(tracked);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
markDirty(entity: any): void {
|
|
163
|
-
const tracked = this.trackedEntities.get(entity);
|
|
164
|
-
if (!tracked) return;
|
|
165
|
-
if (tracked.status === EntityStatus.New || tracked.status === EntityStatus.Removed) return;
|
|
166
|
-
tracked.status = EntityStatus.Dirty;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
markRemoved(entity: any): void {
|
|
170
|
-
const tracked = this.trackedEntities.get(entity);
|
|
171
|
-
if (!tracked) return;
|
|
172
|
-
tracked.status = EntityStatus.Removed;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
registerRelationChange(
|
|
176
|
-
root: any,
|
|
177
|
-
relationKey: RelationKey,
|
|
178
|
-
rootTable: TableDef,
|
|
179
|
-
relationName: string,
|
|
180
|
-
relation: RelationDef,
|
|
181
|
-
change: RelationChange<any>
|
|
182
|
-
): void {
|
|
183
|
-
this.relationChanges.push({
|
|
184
|
-
root,
|
|
185
|
-
relationKey,
|
|
186
|
-
rootTable,
|
|
187
|
-
relationName,
|
|
188
|
-
relation,
|
|
189
|
-
change
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
registerInterceptor(interceptor: OrmInterceptor): void {
|
|
194
|
-
this.interceptors.push(interceptor);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
registerDomainEventHandler(name: string, handler: DomainEventHandler): void {
|
|
198
|
-
const existing = this.domainEventHandlers.get(name) ?? [];
|
|
199
|
-
existing.push(handler);
|
|
200
|
-
this.domainEventHandlers.set(name, existing);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
async saveChanges(): Promise<void> {
|
|
204
|
-
await this.runInTransaction(async () => {
|
|
205
|
-
for (const interceptor of this.interceptors) {
|
|
206
|
-
await interceptor.beforeFlush?.(this);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
await this.flushEntities();
|
|
210
|
-
await this.processRelationChanges();
|
|
211
|
-
await this.flushEntities();
|
|
212
|
-
|
|
213
|
-
for (const interceptor of this.interceptors) {
|
|
214
|
-
await interceptor.afterFlush?.(this);
|
|
215
|
-
}
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
await this.dispatchDomainEvents();
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
getEntitiesForTable(table: TableDef): TrackedEntity[] {
|
|
222
|
-
const bucket = this.identityMap.get(table.name);
|
|
223
|
-
return bucket ? Array.from(bucket.values()) : [];
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
protected async flushEntities(): Promise<void> {
|
|
227
|
-
const toFlush = Array.from(this.trackedEntities.values());
|
|
228
|
-
for (const tracked of toFlush) {
|
|
229
|
-
switch (tracked.status) {
|
|
230
|
-
case EntityStatus.New:
|
|
231
|
-
await this.flushInsert(tracked);
|
|
232
|
-
break;
|
|
233
|
-
case EntityStatus.Dirty:
|
|
234
|
-
await this.flushUpdate(tracked);
|
|
235
|
-
break;
|
|
236
|
-
case EntityStatus.Removed:
|
|
237
|
-
await this.flushDelete(tracked);
|
|
238
|
-
break;
|
|
239
|
-
default:
|
|
240
|
-
break;
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
private async flushInsert(tracked: TrackedEntity): Promise<void> {
|
|
246
|
-
await this.runHook(tracked.table.hooks?.beforeInsert, tracked);
|
|
247
|
-
|
|
248
|
-
const payload = this.extractColumns(tracked.table, tracked.entity);
|
|
249
|
-
const builder = new InsertQueryBuilder(tracked.table).values(payload);
|
|
250
|
-
const compiled = builder.compile(this.dialect);
|
|
251
|
-
await this.executeCompiled(compiled);
|
|
252
|
-
|
|
253
|
-
tracked.status = EntityStatus.Managed;
|
|
254
|
-
tracked.original = this.createSnapshot(tracked.table, tracked.entity);
|
|
255
|
-
tracked.pk = this.getPrimaryKeyValue(tracked);
|
|
256
|
-
this.registerIdentity(tracked);
|
|
257
|
-
|
|
258
|
-
await this.runHook(tracked.table.hooks?.afterInsert, tracked);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
private async flushUpdate(tracked: TrackedEntity): Promise<void> {
|
|
262
|
-
if (tracked.pk == null) return;
|
|
263
|
-
const changes = this.computeChanges(tracked);
|
|
264
|
-
if (!Object.keys(changes).length) {
|
|
265
|
-
tracked.status = EntityStatus.Managed;
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
await this.runHook(tracked.table.hooks?.beforeUpdate, tracked);
|
|
270
|
-
|
|
271
|
-
const pkColumn = tracked.table.columns[findPrimaryKey(tracked.table)];
|
|
272
|
-
if (!pkColumn) return;
|
|
273
|
-
|
|
274
|
-
const builder = new UpdateQueryBuilder(tracked.table)
|
|
275
|
-
.set(changes)
|
|
276
|
-
.where(eq(pkColumn, tracked.pk));
|
|
277
|
-
|
|
278
|
-
const compiled = builder.compile(this.dialect);
|
|
279
|
-
await this.executeCompiled(compiled);
|
|
280
|
-
|
|
281
|
-
tracked.status = EntityStatus.Managed;
|
|
282
|
-
tracked.original = this.createSnapshot(tracked.table, tracked.entity);
|
|
283
|
-
this.registerIdentity(tracked);
|
|
284
|
-
|
|
285
|
-
await this.runHook(tracked.table.hooks?.afterUpdate, tracked);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
private async flushDelete(tracked: TrackedEntity): Promise<void> {
|
|
289
|
-
if (tracked.pk == null) return;
|
|
290
|
-
await this.runHook(tracked.table.hooks?.beforeDelete, tracked);
|
|
291
|
-
|
|
292
|
-
const pkColumn = tracked.table.columns[findPrimaryKey(tracked.table)];
|
|
293
|
-
if (!pkColumn) return;
|
|
294
|
-
|
|
295
|
-
const builder = new DeleteQueryBuilder(tracked.table).where(
|
|
296
|
-
eq(pkColumn, tracked.pk)
|
|
297
|
-
);
|
|
298
|
-
const compiled = builder.compile(this.dialect);
|
|
299
|
-
await this.executeCompiled(compiled);
|
|
300
|
-
|
|
301
|
-
tracked.status = EntityStatus.Detached;
|
|
302
|
-
this.trackedEntities.delete(tracked.entity);
|
|
303
|
-
this.removeIdentity(tracked);
|
|
304
|
-
|
|
305
|
-
await this.runHook(tracked.table.hooks?.afterDelete, tracked);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
private async processRelationChanges(): Promise<void> {
|
|
309
|
-
if (!this.relationChanges.length) return;
|
|
310
|
-
const entries = [...this.relationChanges];
|
|
311
|
-
this.relationChanges.length = 0;
|
|
312
|
-
|
|
313
|
-
for (const entry of entries) {
|
|
314
|
-
switch (entry.relation.type) {
|
|
315
|
-
case RelationKinds.HasMany:
|
|
316
|
-
await this.handleHasManyChange(entry);
|
|
317
|
-
break;
|
|
318
|
-
case RelationKinds.BelongsToMany:
|
|
319
|
-
await this.handleBelongsToManyChange(entry);
|
|
320
|
-
break;
|
|
321
|
-
case RelationKinds.BelongsTo:
|
|
322
|
-
await this.handleBelongsToChange(entry);
|
|
323
|
-
break;
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
private async handleHasManyChange(entry: RelationChangeEntry): Promise<void> {
|
|
329
|
-
const relation = entry.relation as HasManyRelation;
|
|
330
|
-
const target = entry.change.entity;
|
|
331
|
-
if (!target) return;
|
|
332
|
-
|
|
333
|
-
const tracked = this.trackedEntities.get(target);
|
|
334
|
-
if (!tracked) return;
|
|
335
|
-
|
|
336
|
-
const localKey = relation.localKey || findPrimaryKey(entry.rootTable);
|
|
337
|
-
const rootValue = entry.root[localKey];
|
|
338
|
-
if (rootValue === undefined || rootValue === null) return;
|
|
339
|
-
|
|
340
|
-
if (entry.change.kind === 'add' || entry.change.kind === 'attach') {
|
|
341
|
-
this.assignHasManyForeignKey(tracked, relation, rootValue);
|
|
342
|
-
return;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
if (entry.change.kind === 'remove') {
|
|
346
|
-
this.detachHasManyChild(tracked, relation);
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
private async handleBelongsToChange(_entry: RelationChangeEntry): Promise<void> {
|
|
351
|
-
// Reserved for future cascade/persist behaviors for belongs-to relations.
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
private async handleBelongsToManyChange(entry: RelationChangeEntry): Promise<void> {
|
|
355
|
-
const relation = entry.relation as BelongsToManyRelation;
|
|
356
|
-
const rootKey = relation.localKey || findPrimaryKey(entry.rootTable);
|
|
357
|
-
const rootId = entry.root[rootKey];
|
|
358
|
-
if (rootId === undefined || rootId === null) return;
|
|
359
|
-
|
|
360
|
-
const targetId = this.resolvePrimaryKeyValue(entry.change.entity, relation.target);
|
|
361
|
-
if (targetId === null) return;
|
|
362
|
-
|
|
363
|
-
if (entry.change.kind === 'attach' || entry.change.kind === 'add') {
|
|
364
|
-
await this.insertPivotRow(relation, rootId, targetId);
|
|
365
|
-
return;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
if (entry.change.kind === 'detach' || entry.change.kind === 'remove') {
|
|
369
|
-
await this.deletePivotRow(relation, rootId, targetId);
|
|
370
|
-
|
|
371
|
-
if (relation.cascade === 'all' || relation.cascade === 'remove') {
|
|
372
|
-
this.markRemoved(entry.change.entity);
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
private assignHasManyForeignKey(
|
|
378
|
-
tracked: TrackedEntity,
|
|
379
|
-
relation: HasManyRelation,
|
|
380
|
-
rootValue: unknown
|
|
381
|
-
): void {
|
|
382
|
-
const child = tracked.entity;
|
|
383
|
-
const current = child[relation.foreignKey];
|
|
384
|
-
if (current === rootValue) return;
|
|
385
|
-
child[relation.foreignKey] = rootValue;
|
|
386
|
-
this.markDirty(child);
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
private detachHasManyChild(tracked: TrackedEntity, relation: HasManyRelation): void {
|
|
390
|
-
const child = tracked.entity;
|
|
391
|
-
if (relation.cascade === 'all' || relation.cascade === 'remove') {
|
|
392
|
-
this.markRemoved(child);
|
|
393
|
-
return;
|
|
394
|
-
}
|
|
395
|
-
child[relation.foreignKey] = null;
|
|
396
|
-
this.markDirty(child);
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
private async insertPivotRow(relation: BelongsToManyRelation, rootId: string | number, targetId: string | number): Promise<void> {
|
|
400
|
-
const payload = {
|
|
401
|
-
[relation.pivotForeignKeyToRoot]: rootId,
|
|
402
|
-
[relation.pivotForeignKeyToTarget]: targetId
|
|
403
|
-
};
|
|
404
|
-
const builder = new InsertQueryBuilder(relation.pivotTable).values(payload);
|
|
405
|
-
await this.executeCompiled(builder.compile(this.dialect));
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
private async deletePivotRow(relation: BelongsToManyRelation, rootId: string | number, targetId: string | number): Promise<void> {
|
|
409
|
-
const rootCol = relation.pivotTable.columns[relation.pivotForeignKeyToRoot];
|
|
410
|
-
const targetCol = relation.pivotTable.columns[relation.pivotForeignKeyToTarget];
|
|
411
|
-
if (!rootCol || !targetCol) return;
|
|
412
|
-
|
|
413
|
-
const builder = new DeleteQueryBuilder(relation.pivotTable).where(
|
|
414
|
-
and(eq(rootCol, rootId), eq(targetCol, targetId))
|
|
415
|
-
);
|
|
416
|
-
await this.executeCompiled(builder.compile(this.dialect));
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
private resolvePrimaryKeyValue(entity: any, table: TableDef): string | number | null {
|
|
420
|
-
if (!entity) return null;
|
|
421
|
-
const key = findPrimaryKey(table);
|
|
422
|
-
const value = entity[key];
|
|
423
|
-
if (value === undefined || value === null) return null;
|
|
424
|
-
return value;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
private async dispatchDomainEvents(): Promise<void> {
|
|
428
|
-
for (const tracked of this.trackedEntities.values()) {
|
|
429
|
-
const entity = tracked.entity as HasDomainEvents;
|
|
430
|
-
if (!entity.domainEvents || !entity.domainEvents.length) continue;
|
|
431
|
-
|
|
432
|
-
for (const event of entity.domainEvents) {
|
|
433
|
-
const eventName = this.getEventName(event);
|
|
434
|
-
const handlers = this.domainEventHandlers.get(eventName);
|
|
435
|
-
if (!handlers) continue;
|
|
436
|
-
|
|
437
|
-
for (const handler of handlers) {
|
|
438
|
-
await handler(event, this);
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
entity.domainEvents = [];
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
private async runHook(
|
|
447
|
-
hook: TableHooks[keyof TableHooks] | undefined,
|
|
448
|
-
tracked: TrackedEntity
|
|
449
|
-
): Promise<void> {
|
|
450
|
-
if (!hook) return;
|
|
451
|
-
await hook(this, tracked.entity);
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
private computeChanges(tracked: TrackedEntity): Record<string, unknown> {
|
|
455
|
-
const snapshot = tracked.original ?? {};
|
|
456
|
-
const changes: Record<string, unknown> = {};
|
|
457
|
-
for (const column of Object.keys(tracked.table.columns)) {
|
|
458
|
-
const current = tracked.entity[column];
|
|
459
|
-
if (snapshot[column] !== current) {
|
|
460
|
-
changes[column] = current;
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
return changes;
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
private extractColumns(table: TableDef, entity: any): Record<string, unknown> {
|
|
467
|
-
const payload: Record<string, unknown> = {};
|
|
468
|
-
for (const column of Object.keys(table.columns)) {
|
|
469
|
-
payload[column] = entity[column];
|
|
470
|
-
}
|
|
471
|
-
return payload;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
private async executeCompiled(compiled: CompiledQuery): Promise<void> {
|
|
475
|
-
await this.executor.executeSql(compiled.sql, compiled.params);
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
private registerIdentity(tracked: TrackedEntity): void {
|
|
479
|
-
if (tracked.pk == null) return;
|
|
480
|
-
const bucket = this.identityMap.get(tracked.table.name) ?? new Map<string, TrackedEntity>();
|
|
481
|
-
bucket.set(this.toIdentityKey(tracked.pk), tracked);
|
|
482
|
-
this.identityMap.set(tracked.table.name, bucket);
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
private removeIdentity(tracked: TrackedEntity): void {
|
|
486
|
-
if (tracked.pk == null) return;
|
|
487
|
-
const bucket = this.identityMap.get(tracked.table.name);
|
|
488
|
-
bucket?.delete(this.toIdentityKey(tracked.pk));
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
private createSnapshot(table: TableDef, entity: any): Record<string, any> {
|
|
492
|
-
const snapshot: Record<string, any> = {};
|
|
493
|
-
for (const column of Object.keys(table.columns)) {
|
|
494
|
-
snapshot[column] = entity[column];
|
|
495
|
-
}
|
|
496
|
-
return snapshot;
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
private getPrimaryKeyValue(tracked: TrackedEntity): string | number | null {
|
|
500
|
-
const key = findPrimaryKey(tracked.table);
|
|
501
|
-
const val = tracked.entity[key];
|
|
502
|
-
if (val === undefined || val === null) return null;
|
|
503
|
-
return val;
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
private async runInTransaction(action: () => Promise<void>): Promise<void> {
|
|
507
|
-
const executor = this.executor;
|
|
508
|
-
if (!executor.beginTransaction) {
|
|
509
|
-
await action();
|
|
510
|
-
return;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
await executor.beginTransaction();
|
|
514
|
-
try {
|
|
515
|
-
await action();
|
|
516
|
-
await executor.commitTransaction?.();
|
|
517
|
-
} catch (error) {
|
|
518
|
-
await executor.rollbackTransaction?.();
|
|
519
|
-
throw error;
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
private toIdentityKey(pk: string | number): string {
|
|
524
|
-
return String(pk);
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
private getEventName(event: any): string {
|
|
528
|
-
if (!event) return 'Unknown';
|
|
529
|
-
if (typeof event === 'string') return event;
|
|
530
|
-
return event.constructor?.name ?? 'Unknown';
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
export const addDomainEvent = (entity: HasDomainEvents, event: any): void => {
|
|
535
|
-
if (!entity.domainEvents) {
|
|
536
|
-
entity.domainEvents = [];
|
|
537
|
-
}
|
|
538
|
-
entity.domainEvents.push(event);
|
|
539
|
-
};
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { hydrateRows } from '../src/runtime/hydration';
|
|
3
|
-
import { SelectQueryBuilder } from '../src/builder/select';
|
|
4
|
-
import { SqliteDialect } from '../src/dialect/sqlite';
|
|
5
|
-
import { makeRelationAlias } from '../src/utils/relation-alias';
|
|
6
|
-
import { Users } from '../src/playground/features/playground/data/schema';
|
|
7
|
-
|
|
8
|
-
describe('BelongsToMany hydration', () => {
|
|
9
|
-
it('includes pivot metadata for a projects include', () => {
|
|
10
|
-
const builder = new SelectQueryBuilder(Users).include('projects', {
|
|
11
|
-
columns: ['id', 'name', 'client'],
|
|
12
|
-
pivot: { columns: ['assigned_at', 'role_id'] }
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
const compiled = builder.compile(new SqliteDialect());
|
|
16
|
-
expect(compiled.sql).toContain('JOIN "project_assignments"');
|
|
17
|
-
expect(compiled.sql).toContain('JOIN "projects"');
|
|
18
|
-
|
|
19
|
-
const plan = builder.getHydrationPlan();
|
|
20
|
-
expect(plan).toBeDefined();
|
|
21
|
-
|
|
22
|
-
const relationPlan = plan!.relations.find(rel => rel.name === 'projects');
|
|
23
|
-
expect(relationPlan).toBeDefined();
|
|
24
|
-
expect(relationPlan!.pivot).toBeDefined();
|
|
25
|
-
expect(relationPlan!.pivot!.columns).toEqual(['assigned_at', 'role_id']);
|
|
26
|
-
expect(relationPlan!.pivot!.aliasPrefix).toBe('projects_pivot');
|
|
27
|
-
|
|
28
|
-
const row: Record<string, any> = {};
|
|
29
|
-
plan!.rootColumns.forEach(col => {
|
|
30
|
-
row[col] = col === plan!.rootPrimaryKey ? 1 : `root-${col}`;
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
row[makeRelationAlias(relationPlan!.aliasPrefix, relationPlan!.targetPrimaryKey)] = 42;
|
|
34
|
-
relationPlan!.columns.forEach(col => {
|
|
35
|
-
const alias = makeRelationAlias(relationPlan!.aliasPrefix, col);
|
|
36
|
-
row[alias] = col === relationPlan!.targetPrimaryKey ? 42 : `project-${col}`;
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
relationPlan!.pivot!.columns.forEach((col, idx) => {
|
|
40
|
-
const alias = makeRelationAlias(relationPlan!.pivot!.aliasPrefix, col);
|
|
41
|
-
row[alias] = `pivot-${col}-${idx}`;
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
const hydrated = hydrateRows([row], plan);
|
|
45
|
-
expect(hydrated).toHaveLength(1);
|
|
46
|
-
expect(hydrated[0].projects).toHaveLength(1);
|
|
47
|
-
expect(hydrated[0].projects[0]).toEqual({
|
|
48
|
-
id: 42,
|
|
49
|
-
name: 'project-name',
|
|
50
|
-
client: 'project-client',
|
|
51
|
-
_pivot: {
|
|
52
|
-
assigned_at: 'pivot-assigned_at-0',
|
|
53
|
-
role_id: 'pivot-role_id-1'
|
|
54
|
-
}
|
|
55
|
-
});
|
|
56
|
-
});
|
|
57
|
-
});
|
package/tests/between.test.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { between, notBetween, eq } from '../src/ast/expression';
|
|
3
|
-
import { Users, Orders } from '../src/playground/features/playground/data/schema';
|
|
4
|
-
import { SqliteDialect } from '../src/dialect/sqlite';
|
|
5
|
-
import { SelectQueryBuilder } from '../src/builder/select';
|
|
6
|
-
|
|
7
|
-
describe('between', () => {
|
|
8
|
-
const dialect = new SqliteDialect();
|
|
9
|
-
|
|
10
|
-
it('should handle a basic BETWEEN condition', () => {
|
|
11
|
-
const q = new SelectQueryBuilder(Users)
|
|
12
|
-
.selectRaw('*')
|
|
13
|
-
.where(between(Users.columns.id, 1, 10));
|
|
14
|
-
const compiled = q.compile(dialect);
|
|
15
|
-
expect(compiled.sql).toBe(
|
|
16
|
-
'SELECT "users"."*" FROM "users" WHERE "users"."id" BETWEEN ? AND ?;'
|
|
17
|
-
);
|
|
18
|
-
expect(compiled.params).toEqual([1, 10]);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it('should handle a NOT BETWEEN condition', () => {
|
|
22
|
-
const q = new SelectQueryBuilder(Users)
|
|
23
|
-
.selectRaw('*')
|
|
24
|
-
.where(notBetween(Users.columns.id, 1, 10));
|
|
25
|
-
const compiled = q.compile(dialect);
|
|
26
|
-
expect(compiled.sql).toBe(
|
|
27
|
-
'SELECT "users"."*" FROM "users" WHERE "users"."id" NOT BETWEEN ? AND ?;'
|
|
28
|
-
);
|
|
29
|
-
expect(compiled.params).toEqual([1, 10]);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('should handle multiple conditions', () => {
|
|
33
|
-
const q = new SelectQueryBuilder(Orders)
|
|
34
|
-
.selectRaw('*')
|
|
35
|
-
.where(between(Orders.columns.total, 100, 200))
|
|
36
|
-
.where(eq(Orders.columns.user_id, 1));
|
|
37
|
-
const compiled = q.compile(dialect);
|
|
38
|
-
expect(compiled.sql).toBe(
|
|
39
|
-
'SELECT "orders"."*" FROM "orders" WHERE "orders"."total" BETWEEN ? AND ? AND "orders"."user_id" = ?;'
|
|
40
|
-
);
|
|
41
|
-
expect(compiled.params).toEqual([100, 200, 1]);
|
|
42
|
-
});
|
|
43
|
-
});
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { Users } from '../src/playground/features/playground/data/schema';
|
|
3
|
-
import { caseWhen, gt, eq } from '../src/ast/expression';
|
|
4
|
-
import { SqliteDialect } from '../src/dialect/sqlite';
|
|
5
|
-
import { SelectQueryBuilder } from '../src/builder/select';
|
|
6
|
-
|
|
7
|
-
describe('CASE Expressions', () => {
|
|
8
|
-
const dialect = new SqliteDialect();
|
|
9
|
-
|
|
10
|
-
it('should compile simple CASE WHEN ... THEN ... ELSE ... END', () => {
|
|
11
|
-
const query = new SelectQueryBuilder(Users)
|
|
12
|
-
.select({
|
|
13
|
-
status: caseWhen([
|
|
14
|
-
{ when: gt(Users.columns.id, 10), then: 'High' },
|
|
15
|
-
{ when: gt(Users.columns.id, 5), then: 'Medium' }
|
|
16
|
-
], 'Low')
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
const compiled = query.compile(dialect);
|
|
20
|
-
const { sql, params } = compiled;
|
|
21
|
-
|
|
22
|
-
expect(sql).toContain('CASE WHEN "users"."id" > ? THEN ? WHEN "users"."id" > ? THEN ? ELSE ? END');
|
|
23
|
-
expect(params).toEqual([10, 'High', 5, 'Medium', 'Low']);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it('should compile CASE without ELSE', () => {
|
|
27
|
-
const query = new SelectQueryBuilder(Users)
|
|
28
|
-
.select({
|
|
29
|
-
status: caseWhen([
|
|
30
|
-
{ when: eq(Users.columns.name, 'Alice'), then: 'Admin' }
|
|
31
|
-
])
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
const compiled = query.compile(dialect);
|
|
35
|
-
const { sql, params } = compiled;
|
|
36
|
-
|
|
37
|
-
expect(sql).toContain('CASE WHEN "users"."name" = ? THEN ? END');
|
|
38
|
-
expect(params).toEqual(['Alice', 'Admin']);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('should work in WHERE clause', () => {
|
|
42
|
-
const query = new SelectQueryBuilder(Users)
|
|
43
|
-
.where(
|
|
44
|
-
eq(
|
|
45
|
-
caseWhen([
|
|
46
|
-
{ when: gt(Users.columns.id, 10), then: 'High' }
|
|
47
|
-
], 'Low'),
|
|
48
|
-
'High'
|
|
49
|
-
)
|
|
50
|
-
);
|
|
51
|
-
|
|
52
|
-
const compiled = query.compile(dialect);
|
|
53
|
-
const { sql, params } = compiled;
|
|
54
|
-
|
|
55
|
-
expect(sql).toContain('WHERE CASE WHEN "users"."id" > ? THEN ? ELSE ? END = ?');
|
|
56
|
-
expect(params).toEqual([10, 'High', 'Low', 'High']);
|
|
57
|
-
});
|
|
58
|
-
});
|