metal-orm 1.1.8 → 1.1.10
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 +769 -764
- package/dist/index.cjs +2352 -226
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +605 -40
- package/dist/index.d.ts +605 -40
- package/dist/index.js +2324 -226
- package/dist/index.js.map +1 -1
- package/package.json +22 -17
- package/src/bulk/bulk-context.ts +83 -0
- package/src/bulk/bulk-delete-executor.ts +89 -0
- package/src/bulk/bulk-executor.base.ts +73 -0
- package/src/bulk/bulk-insert-executor.ts +74 -0
- package/src/bulk/bulk-types.ts +70 -0
- package/src/bulk/bulk-update-executor.ts +192 -0
- package/src/bulk/bulk-upsert-executor.ts +95 -0
- package/src/bulk/bulk-utils.ts +91 -0
- package/src/bulk/index.ts +18 -0
- package/src/codegen/typescript.ts +30 -21
- package/src/core/ast/expression-builders.ts +107 -10
- package/src/core/ast/expression-nodes.ts +52 -22
- package/src/core/ast/expression-visitor.ts +23 -13
- package/src/core/dialect/abstract.ts +30 -17
- package/src/core/dialect/mysql/index.ts +20 -5
- package/src/core/execution/db-executor.ts +96 -64
- package/src/core/execution/executors/better-sqlite3-executor.ts +94 -0
- package/src/core/execution/executors/mssql-executor.ts +66 -34
- package/src/core/execution/executors/mysql-executor.ts +98 -66
- package/src/core/execution/executors/postgres-executor.ts +33 -11
- package/src/core/execution/executors/sqlite-executor.ts +86 -30
- package/src/decorators/bootstrap.ts +482 -398
- package/src/decorators/column-decorator.ts +87 -96
- package/src/decorators/decorator-metadata.ts +100 -24
- package/src/decorators/entity.ts +27 -24
- package/src/decorators/relations.ts +231 -149
- package/src/decorators/transformers/transformer-decorators.ts +26 -29
- package/src/decorators/validators/country-validators-decorators.ts +9 -15
- package/src/dto/apply-filter.ts +568 -551
- package/src/index.ts +16 -9
- package/src/orm/entity-hydration.ts +116 -72
- package/src/orm/entity-metadata.ts +347 -301
- package/src/orm/entity-relations.ts +264 -207
- package/src/orm/entity.ts +199 -199
- package/src/orm/execute.ts +13 -13
- package/src/orm/lazy-batch/morph-many.ts +70 -0
- package/src/orm/lazy-batch/morph-one.ts +69 -0
- package/src/orm/lazy-batch/morph-to.ts +59 -0
- package/src/orm/lazy-batch.ts +4 -1
- package/src/orm/orm-session.ts +170 -104
- package/src/orm/pooled-executor-factory.ts +99 -58
- package/src/orm/query-logger.ts +49 -40
- package/src/orm/relation-change-processor.ts +198 -96
- package/src/orm/relations/belongs-to.ts +143 -143
- package/src/orm/relations/has-many.ts +204 -204
- package/src/orm/relations/has-one.ts +174 -174
- package/src/orm/relations/many-to-many.ts +288 -288
- package/src/orm/relations/morph-many.ts +156 -0
- package/src/orm/relations/morph-one.ts +151 -0
- package/src/orm/relations/morph-to.ts +162 -0
- package/src/orm/save-graph.ts +116 -1
- package/src/query-builder/expression-table-mapper.ts +5 -0
- package/src/query-builder/hydration-manager.ts +345 -345
- package/src/query-builder/hydration-planner.ts +178 -148
- package/src/query-builder/relation-conditions.ts +171 -151
- package/src/query-builder/relation-cte-builder.ts +5 -1
- package/src/query-builder/relation-filter-utils.ts +9 -6
- package/src/query-builder/relation-include-strategies.ts +44 -2
- package/src/query-builder/relation-join-strategies.ts +8 -1
- package/src/query-builder/relation-service.ts +250 -241
- package/src/query-builder/select/cursor-pagination.ts +323 -0
- package/src/query-builder/select/select-operations.ts +110 -105
- package/src/query-builder/select.ts +42 -1
- package/src/query-builder/update-include.ts +4 -0
- package/src/schema/relation.ts +296 -188
- package/src/schema/types.ts +138 -123
- package/src/tree/tree-decorator.ts +127 -137
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { and, eq } from '../core/ast/expression.js';
|
|
2
|
-
import type { ValueOperandInput } from '../core/ast/expression.js';
|
|
3
|
-
import type { Dialect } from '../core/dialect/abstract.js';
|
|
4
|
-
import { DeleteQueryBuilder } from '../query-builder/delete.js';
|
|
5
|
-
import { InsertQueryBuilder } from '../query-builder/insert.js';
|
|
6
|
-
import { UpdateQueryBuilder } from '../query-builder/update.js';
|
|
7
|
-
import { findPrimaryKey } from '../query-builder/hydration-planner.js';
|
|
8
|
-
import type { BelongsToManyRelation, HasManyRelation, HasOneRelation } from '../schema/relation.js';
|
|
9
|
-
import { RelationKinds } from '../schema/relation.js';
|
|
10
|
-
import type { TableDef } from '../schema/table.js';
|
|
11
|
-
import type { DbExecutor } from '../core/execution/db-executor.js';
|
|
1
|
+
import { and, eq } from '../core/ast/expression.js';
|
|
2
|
+
import type { ValueOperandInput } from '../core/ast/expression.js';
|
|
3
|
+
import type { Dialect } from '../core/dialect/abstract.js';
|
|
4
|
+
import { DeleteQueryBuilder } from '../query-builder/delete.js';
|
|
5
|
+
import { InsertQueryBuilder } from '../query-builder/insert.js';
|
|
6
|
+
import { UpdateQueryBuilder } from '../query-builder/update.js';
|
|
7
|
+
import { findPrimaryKey } from '../query-builder/hydration-planner.js';
|
|
8
|
+
import type { BelongsToManyRelation, HasManyRelation, HasOneRelation, MorphOneRelation, MorphManyRelation, MorphToRelation } from '../schema/relation.js';
|
|
9
|
+
import { RelationKinds } from '../schema/relation.js';
|
|
10
|
+
import type { TableDef } from '../schema/table.js';
|
|
11
|
+
import type { DbExecutor } from '../core/execution/db-executor.js';
|
|
12
12
|
import type { RelationChangeEntry } from './runtime-types.js';
|
|
13
13
|
import { UnitOfWork } from './unit-of-work.js';
|
|
14
14
|
|
|
@@ -67,6 +67,15 @@ export class RelationChangeProcessor {
|
|
|
67
67
|
case RelationKinds.BelongsTo:
|
|
68
68
|
await this.handleBelongsToChange(entry);
|
|
69
69
|
break;
|
|
70
|
+
case RelationKinds.MorphOne:
|
|
71
|
+
await this.handleMorphOneChange(entry);
|
|
72
|
+
break;
|
|
73
|
+
case RelationKinds.MorphMany:
|
|
74
|
+
await this.handleMorphManyChange(entry);
|
|
75
|
+
break;
|
|
76
|
+
case RelationKinds.MorphTo:
|
|
77
|
+
await this.handleMorphToChange(entry);
|
|
78
|
+
break;
|
|
70
79
|
}
|
|
71
80
|
}
|
|
72
81
|
}
|
|
@@ -138,28 +147,28 @@ export class RelationChangeProcessor {
|
|
|
138
147
|
* Handles changes for belongs-to-many relations.
|
|
139
148
|
* @param entry - The relation change entry
|
|
140
149
|
*/
|
|
141
|
-
private async handleBelongsToManyChange(entry: RelationChangeEntry): Promise<void> {
|
|
142
|
-
const relation = entry.relation as BelongsToManyRelation;
|
|
143
|
-
const rootKey = relation.localKey || findPrimaryKey(entry.rootTable);
|
|
144
|
-
const rootId = entry.root[rootKey];
|
|
145
|
-
if (rootId === undefined || rootId === null) return;
|
|
146
|
-
|
|
147
|
-
const targetId = this.resolvePrimaryKeyValue(entry.change.entity as Record<string, unknown>, relation.target);
|
|
148
|
-
if (targetId === null) return;
|
|
149
|
-
|
|
150
|
-
const pivotPayload = this.buildPivotPayload(relation, entry.change.pivot);
|
|
151
|
-
|
|
152
|
-
if (entry.change.kind === 'update') {
|
|
153
|
-
if (pivotPayload) {
|
|
154
|
-
await this.updatePivotRow(relation, rootId, targetId, pivotPayload);
|
|
155
|
-
}
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
if (entry.change.kind === 'attach' || entry.change.kind === 'add') {
|
|
160
|
-
await this.insertPivotRow(relation, rootId, targetId, pivotPayload);
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
150
|
+
private async handleBelongsToManyChange(entry: RelationChangeEntry): Promise<void> {
|
|
151
|
+
const relation = entry.relation as BelongsToManyRelation;
|
|
152
|
+
const rootKey = relation.localKey || findPrimaryKey(entry.rootTable);
|
|
153
|
+
const rootId = entry.root[rootKey];
|
|
154
|
+
if (rootId === undefined || rootId === null) return;
|
|
155
|
+
|
|
156
|
+
const targetId = this.resolvePrimaryKeyValue(entry.change.entity as Record<string, unknown>, relation.target);
|
|
157
|
+
if (targetId === null) return;
|
|
158
|
+
|
|
159
|
+
const pivotPayload = this.buildPivotPayload(relation, entry.change.pivot);
|
|
160
|
+
|
|
161
|
+
if (entry.change.kind === 'update') {
|
|
162
|
+
if (pivotPayload) {
|
|
163
|
+
await this.updatePivotRow(relation, rootId, targetId, pivotPayload);
|
|
164
|
+
}
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (entry.change.kind === 'attach' || entry.change.kind === 'add') {
|
|
169
|
+
await this.insertPivotRow(relation, rootId, targetId, pivotPayload);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
163
172
|
|
|
164
173
|
if (entry.change.kind === 'detach' || entry.change.kind === 'remove') {
|
|
165
174
|
await this.deletePivotRow(relation, rootId, targetId);
|
|
@@ -228,46 +237,46 @@ export class RelationChangeProcessor {
|
|
|
228
237
|
* @param rootId - The root entity's primary key value
|
|
229
238
|
* @param targetId - The target entity's primary key value
|
|
230
239
|
*/
|
|
231
|
-
private async insertPivotRow(
|
|
232
|
-
relation: BelongsToManyRelation,
|
|
233
|
-
rootId: string | number,
|
|
234
|
-
targetId: string | number,
|
|
235
|
-
pivotPayload?: Record<string, unknown>
|
|
236
|
-
): Promise<void> {
|
|
237
|
-
const payload = {
|
|
238
|
-
[relation.pivotForeignKeyToRoot]: rootId,
|
|
239
|
-
[relation.pivotForeignKeyToTarget]: targetId,
|
|
240
|
-
...(pivotPayload ?? {})
|
|
241
|
-
};
|
|
242
|
-
const builder = new InsertQueryBuilder(relation.pivotTable)
|
|
243
|
-
.values(payload as Record<string, ValueOperandInput>);
|
|
244
|
-
const compiled = builder.compile(this.dialect);
|
|
245
|
-
await this.executor.executeSql(compiled.sql, compiled.params);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Updates a pivot row for belongs-to-many relations.
|
|
250
|
-
* @param relation - The belongs-to-many relation
|
|
251
|
-
* @param rootId - The root entity's primary key value
|
|
252
|
-
* @param targetId - The target entity's primary key value
|
|
253
|
-
* @param pivotPayload - The pivot columns to update
|
|
254
|
-
*/
|
|
255
|
-
private async updatePivotRow(
|
|
256
|
-
relation: BelongsToManyRelation,
|
|
257
|
-
rootId: string | number,
|
|
258
|
-
targetId: string | number,
|
|
259
|
-
pivotPayload: Record<string, unknown>
|
|
260
|
-
): Promise<void> {
|
|
261
|
-
const rootCol = relation.pivotTable.columns[relation.pivotForeignKeyToRoot];
|
|
262
|
-
const targetCol = relation.pivotTable.columns[relation.pivotForeignKeyToTarget];
|
|
263
|
-
if (!rootCol || !targetCol) return;
|
|
264
|
-
|
|
265
|
-
const builder = new UpdateQueryBuilder(relation.pivotTable)
|
|
266
|
-
.set(pivotPayload)
|
|
267
|
-
.where(and(eq(rootCol, rootId), eq(targetCol, targetId)));
|
|
268
|
-
const compiled = builder.compile(this.dialect);
|
|
269
|
-
await this.executor.executeSql(compiled.sql, compiled.params);
|
|
270
|
-
}
|
|
240
|
+
private async insertPivotRow(
|
|
241
|
+
relation: BelongsToManyRelation,
|
|
242
|
+
rootId: string | number,
|
|
243
|
+
targetId: string | number,
|
|
244
|
+
pivotPayload?: Record<string, unknown>
|
|
245
|
+
): Promise<void> {
|
|
246
|
+
const payload = {
|
|
247
|
+
[relation.pivotForeignKeyToRoot]: rootId,
|
|
248
|
+
[relation.pivotForeignKeyToTarget]: targetId,
|
|
249
|
+
...(pivotPayload ?? {})
|
|
250
|
+
};
|
|
251
|
+
const builder = new InsertQueryBuilder(relation.pivotTable)
|
|
252
|
+
.values(payload as Record<string, ValueOperandInput>);
|
|
253
|
+
const compiled = builder.compile(this.dialect);
|
|
254
|
+
await this.executor.executeSql(compiled.sql, compiled.params);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Updates a pivot row for belongs-to-many relations.
|
|
259
|
+
* @param relation - The belongs-to-many relation
|
|
260
|
+
* @param rootId - The root entity's primary key value
|
|
261
|
+
* @param targetId - The target entity's primary key value
|
|
262
|
+
* @param pivotPayload - The pivot columns to update
|
|
263
|
+
*/
|
|
264
|
+
private async updatePivotRow(
|
|
265
|
+
relation: BelongsToManyRelation,
|
|
266
|
+
rootId: string | number,
|
|
267
|
+
targetId: string | number,
|
|
268
|
+
pivotPayload: Record<string, unknown>
|
|
269
|
+
): Promise<void> {
|
|
270
|
+
const rootCol = relation.pivotTable.columns[relation.pivotForeignKeyToRoot];
|
|
271
|
+
const targetCol = relation.pivotTable.columns[relation.pivotForeignKeyToTarget];
|
|
272
|
+
if (!rootCol || !targetCol) return;
|
|
273
|
+
|
|
274
|
+
const builder = new UpdateQueryBuilder(relation.pivotTable)
|
|
275
|
+
.set(pivotPayload)
|
|
276
|
+
.where(and(eq(rootCol, rootId), eq(targetCol, targetId)));
|
|
277
|
+
const compiled = builder.compile(this.dialect);
|
|
278
|
+
await this.executor.executeSql(compiled.sql, compiled.params);
|
|
279
|
+
}
|
|
271
280
|
|
|
272
281
|
/**
|
|
273
282
|
* Deletes a pivot row for belongs-to-many relations.
|
|
@@ -293,26 +302,119 @@ export class RelationChangeProcessor {
|
|
|
293
302
|
* @param table - The table definition
|
|
294
303
|
* @returns The primary key value or null
|
|
295
304
|
*/
|
|
296
|
-
private resolvePrimaryKeyValue(entity: Record<string, unknown>, table: TableDef): string | number | null {
|
|
297
|
-
if (!entity) return null;
|
|
298
|
-
const key = findPrimaryKey(table);
|
|
299
|
-
const value = entity[key];
|
|
300
|
-
if (value === undefined || value === null) return null;
|
|
301
|
-
return (value as string | number | null | undefined) ?? null;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
private buildPivotPayload(
|
|
305
|
-
relation: BelongsToManyRelation,
|
|
306
|
-
pivot: Record<string, unknown> | undefined
|
|
307
|
-
): Record<string, unknown> | undefined {
|
|
308
|
-
if (!pivot) return undefined;
|
|
309
|
-
const payload: Record<string, unknown> = {};
|
|
310
|
-
for (const [key, value] of Object.entries(pivot)) {
|
|
311
|
-
if (value === undefined) continue;
|
|
312
|
-
if (!relation.pivotTable.columns[key]) continue;
|
|
313
|
-
if (key === relation.pivotForeignKeyToRoot || key === relation.pivotForeignKeyToTarget) continue;
|
|
314
|
-
payload[key] = value;
|
|
315
|
-
}
|
|
316
|
-
return Object.keys(payload).length ? payload : undefined;
|
|
317
|
-
}
|
|
318
|
-
|
|
305
|
+
private resolvePrimaryKeyValue(entity: Record<string, unknown>, table: TableDef): string | number | null {
|
|
306
|
+
if (!entity) return null;
|
|
307
|
+
const key = findPrimaryKey(table);
|
|
308
|
+
const value = entity[key];
|
|
309
|
+
if (value === undefined || value === null) return null;
|
|
310
|
+
return (value as string | number | null | undefined) ?? null;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
private buildPivotPayload(
|
|
314
|
+
relation: BelongsToManyRelation,
|
|
315
|
+
pivot: Record<string, unknown> | undefined
|
|
316
|
+
): Record<string, unknown> | undefined {
|
|
317
|
+
if (!pivot) return undefined;
|
|
318
|
+
const payload: Record<string, unknown> = {};
|
|
319
|
+
for (const [key, value] of Object.entries(pivot)) {
|
|
320
|
+
if (value === undefined) continue;
|
|
321
|
+
if (!relation.pivotTable.columns[key]) continue;
|
|
322
|
+
if (key === relation.pivotForeignKeyToRoot || key === relation.pivotForeignKeyToTarget) continue;
|
|
323
|
+
payload[key] = value;
|
|
324
|
+
}
|
|
325
|
+
return Object.keys(payload).length ? payload : undefined;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private async handleMorphOneChange(entry: RelationChangeEntry): Promise<void> {
|
|
329
|
+
const relation = entry.relation as MorphOneRelation;
|
|
330
|
+
const target = entry.change.entity;
|
|
331
|
+
if (!target) return;
|
|
332
|
+
|
|
333
|
+
const tracked = this.unitOfWork.findTracked(target as object);
|
|
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
|
+
const child = tracked.entity as Record<string, unknown>;
|
|
342
|
+
child[relation.idField] = rootValue;
|
|
343
|
+
child[relation.typeField] = relation.typeValue;
|
|
344
|
+
this.unitOfWork.markDirty(tracked.entity);
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (entry.change.kind === 'remove') {
|
|
349
|
+
const child = tracked.entity as Record<string, unknown>;
|
|
350
|
+
if (relation.cascade === 'all' || relation.cascade === 'remove') {
|
|
351
|
+
this.unitOfWork.markRemoved(child);
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
child[relation.idField] = null;
|
|
355
|
+
child[relation.typeField] = null;
|
|
356
|
+
this.unitOfWork.markDirty(child);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
private async handleMorphManyChange(entry: RelationChangeEntry): Promise<void> {
|
|
361
|
+
const relation = entry.relation as MorphManyRelation;
|
|
362
|
+
const target = entry.change.entity;
|
|
363
|
+
if (!target) return;
|
|
364
|
+
|
|
365
|
+
const tracked = this.unitOfWork.findTracked(target as object);
|
|
366
|
+
if (!tracked) return;
|
|
367
|
+
|
|
368
|
+
const localKey = relation.localKey || findPrimaryKey(entry.rootTable);
|
|
369
|
+
const rootValue = entry.root[localKey];
|
|
370
|
+
if (rootValue === undefined || rootValue === null) return;
|
|
371
|
+
|
|
372
|
+
if (entry.change.kind === 'add' || entry.change.kind === 'attach') {
|
|
373
|
+
const child = tracked.entity as Record<string, unknown>;
|
|
374
|
+
child[relation.idField] = rootValue;
|
|
375
|
+
child[relation.typeField] = relation.typeValue;
|
|
376
|
+
this.unitOfWork.markDirty(tracked.entity);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (entry.change.kind === 'remove') {
|
|
381
|
+
const child = tracked.entity as Record<string, unknown>;
|
|
382
|
+
if (relation.cascade === 'all' || relation.cascade === 'remove') {
|
|
383
|
+
this.unitOfWork.markRemoved(child);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
child[relation.idField] = null;
|
|
387
|
+
child[relation.typeField] = null;
|
|
388
|
+
this.unitOfWork.markDirty(child);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
private async handleMorphToChange(entry: RelationChangeEntry): Promise<void> {
|
|
393
|
+
const relation = entry.relation as MorphToRelation;
|
|
394
|
+
const target = entry.change.entity;
|
|
395
|
+
if (!target) return;
|
|
396
|
+
|
|
397
|
+
if (entry.change.kind === 'attach' || entry.change.kind === 'add') {
|
|
398
|
+
const targetEntity = target as Record<string, unknown>;
|
|
399
|
+
for (const [typeValue, targetTable] of Object.entries(relation.targets)) {
|
|
400
|
+
const targetPk = relation.targetKey || findPrimaryKey(targetTable);
|
|
401
|
+
const pkValue = targetEntity[targetPk];
|
|
402
|
+
if (pkValue !== undefined && pkValue !== null) {
|
|
403
|
+
const rootEntity = entry.root as Record<string, unknown>;
|
|
404
|
+
rootEntity[relation.typeField] = typeValue;
|
|
405
|
+
rootEntity[relation.idField] = pkValue;
|
|
406
|
+
this.unitOfWork.markDirty(entry.root as object);
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (entry.change.kind === 'remove') {
|
|
414
|
+
const rootEntity = entry.root as Record<string, unknown>;
|
|
415
|
+
rootEntity[relation.typeField] = null;
|
|
416
|
+
rootEntity[relation.idField] = null;
|
|
417
|
+
this.unitOfWork.markDirty(entry.root as object);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
@@ -1,143 +1,143 @@
|
|
|
1
|
-
import { BelongsToReferenceApi } from '../../schema/types.js';
|
|
2
|
-
import { EntityContext } from '../entity-context.js';
|
|
3
|
-
import { RelationKey } from '../runtime-types.js';
|
|
4
|
-
import { BelongsToRelation } from '../../schema/relation.js';
|
|
5
|
-
import { TableDef } from '../../schema/table.js';
|
|
6
|
-
import { EntityMeta, getHydrationRecord, hasEntityMeta } from '../entity-meta.js';
|
|
7
|
-
|
|
8
|
-
type Rows = Record<string, unknown>;
|
|
9
|
-
|
|
10
|
-
const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
|
|
11
|
-
|
|
12
|
-
const hideInternal = (obj: object, keys: string[]): void => {
|
|
13
|
-
for (const key of keys) {
|
|
14
|
-
Object.defineProperty(obj, key, {
|
|
15
|
-
value: obj[key],
|
|
16
|
-
writable: false,
|
|
17
|
-
configurable: false,
|
|
18
|
-
enumerable: false
|
|
19
|
-
});
|
|
20
|
-
}
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
const hideWritable = (obj: object, keys: string[]): void => {
|
|
24
|
-
for (const key of keys) {
|
|
25
|
-
const value = obj[key as keyof typeof obj];
|
|
26
|
-
Object.defineProperty(obj, key, {
|
|
27
|
-
value,
|
|
28
|
-
writable: true,
|
|
29
|
-
configurable: true,
|
|
30
|
-
enumerable: false
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Default implementation of a belongs-to reference.
|
|
37
|
-
* Manages a reference to a parent entity from a child entity through a foreign key.
|
|
38
|
-
*
|
|
39
|
-
* @template TParent The type of the parent entity.
|
|
40
|
-
*/
|
|
41
|
-
export class DefaultBelongsToReference<TParent extends object> implements BelongsToReferenceApi<TParent> {
|
|
42
|
-
private loaded = false;
|
|
43
|
-
private current: TParent | null = null;
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* @param ctx The entity context for tracking changes.
|
|
47
|
-
* @param meta Metadata for the child entity.
|
|
48
|
-
* @param root The child entity instance (carrying the foreign key).
|
|
49
|
-
* @param relationName The name of the relation.
|
|
50
|
-
* @param relation Relation definition.
|
|
51
|
-
* @param rootTable Table definition of the child entity.
|
|
52
|
-
* @param loader Function to load the parent entity.
|
|
53
|
-
* @param createEntity Function to create entity instances from rows.
|
|
54
|
-
* @param targetKey The primary key of the target (parent) table.
|
|
55
|
-
*/
|
|
56
|
-
constructor(
|
|
57
|
-
private readonly ctx: EntityContext,
|
|
58
|
-
private readonly meta: EntityMeta<TableDef>,
|
|
59
|
-
private readonly root: unknown,
|
|
60
|
-
private readonly relationName: string,
|
|
61
|
-
private readonly relation: BelongsToRelation,
|
|
62
|
-
private readonly rootTable: TableDef,
|
|
63
|
-
private readonly loader: () => Promise<Map<string, Rows>>,
|
|
64
|
-
private readonly createEntity: (row: Record<string, unknown>) => TParent,
|
|
65
|
-
private readonly targetKey: string
|
|
66
|
-
) {
|
|
67
|
-
hideInternal(this, ['ctx', 'meta', 'root', 'relationName', 'relation', 'rootTable', 'loader', 'createEntity', 'targetKey']);
|
|
68
|
-
hideWritable(this, ['loaded', 'current']);
|
|
69
|
-
this.populateFromHydrationCache();
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
async load(): Promise<TParent | null> {
|
|
73
|
-
if (this.loaded) return this.current;
|
|
74
|
-
const map = await this.loader();
|
|
75
|
-
const fkValue = (this.root as Record<string, unknown>)[this.relation.foreignKey];
|
|
76
|
-
if (fkValue === null || fkValue === undefined) {
|
|
77
|
-
this.current = null;
|
|
78
|
-
} else {
|
|
79
|
-
const row = map.get(toKey(fkValue));
|
|
80
|
-
this.current = row ? this.createEntity(row) : null;
|
|
81
|
-
}
|
|
82
|
-
this.loaded = true;
|
|
83
|
-
return this.current;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
get(): TParent | null {
|
|
87
|
-
return this.current;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
set(data: Partial<TParent> | TParent | null): TParent | null {
|
|
91
|
-
if (data === null) {
|
|
92
|
-
const previous = this.current;
|
|
93
|
-
(this.root as Record<string, unknown>)[this.relation.foreignKey] = null;
|
|
94
|
-
this.current = null;
|
|
95
|
-
this.ctx.registerRelationChange(
|
|
96
|
-
this.root,
|
|
97
|
-
this.relationKey,
|
|
98
|
-
this.rootTable,
|
|
99
|
-
this.relationName,
|
|
100
|
-
this.relation,
|
|
101
|
-
{ kind: 'remove', entity: previous }
|
|
102
|
-
);
|
|
103
|
-
return null;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const entity = hasEntityMeta(data) ? (data as TParent) : this.createEntity(data as Record<string, unknown>);
|
|
107
|
-
const pkValue = (entity as Record<string, unknown>)[this.targetKey];
|
|
108
|
-
if (pkValue !== undefined) {
|
|
109
|
-
(this.root as Record<string, unknown>)[this.relation.foreignKey] = pkValue;
|
|
110
|
-
}
|
|
111
|
-
this.current = entity;
|
|
112
|
-
this.ctx.registerRelationChange(
|
|
113
|
-
this.root,
|
|
114
|
-
this.relationKey,
|
|
115
|
-
this.rootTable,
|
|
116
|
-
this.relationName,
|
|
117
|
-
this.relation,
|
|
118
|
-
{ kind: 'attach', entity }
|
|
119
|
-
);
|
|
120
|
-
return entity;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
private get relationKey(): RelationKey {
|
|
124
|
-
return `${this.rootTable.name}.${this.relationName}`;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
private populateFromHydrationCache(): void {
|
|
128
|
-
const fkValue = (this.root as Record<string, unknown>)[this.relation.foreignKey];
|
|
129
|
-
if (fkValue === undefined || fkValue === null) return;
|
|
130
|
-
const row = getHydrationRecord(this.meta, this.relationName, fkValue);
|
|
131
|
-
if (!row) return;
|
|
132
|
-
this.current = this.createEntity(row);
|
|
133
|
-
this.loaded = true;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
toJSON(): unknown {
|
|
137
|
-
if (!this.current) return null;
|
|
138
|
-
const entityWithToJSON = this.current as { toJSON?: () => unknown };
|
|
139
|
-
return typeof entityWithToJSON.toJSON === 'function'
|
|
140
|
-
? entityWithToJSON.toJSON()
|
|
141
|
-
: this.current;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
1
|
+
import { BelongsToReferenceApi } from '../../schema/types.js';
|
|
2
|
+
import { EntityContext } from '../entity-context.js';
|
|
3
|
+
import { RelationKey } from '../runtime-types.js';
|
|
4
|
+
import { BelongsToRelation } from '../../schema/relation.js';
|
|
5
|
+
import { TableDef } from '../../schema/table.js';
|
|
6
|
+
import { EntityMeta, getHydrationRecord, hasEntityMeta } from '../entity-meta.js';
|
|
7
|
+
|
|
8
|
+
type Rows = Record<string, unknown>;
|
|
9
|
+
|
|
10
|
+
const toKey = (value: unknown): string => (value === null || value === undefined ? '' : String(value));
|
|
11
|
+
|
|
12
|
+
const hideInternal = (obj: object, keys: string[]): void => {
|
|
13
|
+
for (const key of keys) {
|
|
14
|
+
Object.defineProperty(obj, key, {
|
|
15
|
+
value: obj[key],
|
|
16
|
+
writable: false,
|
|
17
|
+
configurable: false,
|
|
18
|
+
enumerable: false
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const hideWritable = (obj: object, keys: string[]): void => {
|
|
24
|
+
for (const key of keys) {
|
|
25
|
+
const value = obj[key as keyof typeof obj];
|
|
26
|
+
Object.defineProperty(obj, key, {
|
|
27
|
+
value,
|
|
28
|
+
writable: true,
|
|
29
|
+
configurable: true,
|
|
30
|
+
enumerable: false
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Default implementation of a belongs-to reference.
|
|
37
|
+
* Manages a reference to a parent entity from a child entity through a foreign key.
|
|
38
|
+
*
|
|
39
|
+
* @template TParent The type of the parent entity.
|
|
40
|
+
*/
|
|
41
|
+
export class DefaultBelongsToReference<TParent extends object> implements BelongsToReferenceApi<TParent> {
|
|
42
|
+
private loaded = false;
|
|
43
|
+
private current: TParent | null = null;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param ctx The entity context for tracking changes.
|
|
47
|
+
* @param meta Metadata for the child entity.
|
|
48
|
+
* @param root The child entity instance (carrying the foreign key).
|
|
49
|
+
* @param relationName The name of the relation.
|
|
50
|
+
* @param relation Relation definition.
|
|
51
|
+
* @param rootTable Table definition of the child entity.
|
|
52
|
+
* @param loader Function to load the parent entity.
|
|
53
|
+
* @param createEntity Function to create entity instances from rows.
|
|
54
|
+
* @param targetKey The primary key of the target (parent) table.
|
|
55
|
+
*/
|
|
56
|
+
constructor(
|
|
57
|
+
private readonly ctx: EntityContext,
|
|
58
|
+
private readonly meta: EntityMeta<TableDef>,
|
|
59
|
+
private readonly root: unknown,
|
|
60
|
+
private readonly relationName: string,
|
|
61
|
+
private readonly relation: BelongsToRelation,
|
|
62
|
+
private readonly rootTable: TableDef,
|
|
63
|
+
private readonly loader: () => Promise<Map<string, Rows>>,
|
|
64
|
+
private readonly createEntity: (row: Record<string, unknown>) => TParent,
|
|
65
|
+
private readonly targetKey: string
|
|
66
|
+
) {
|
|
67
|
+
hideInternal(this, ['ctx', 'meta', 'root', 'relationName', 'relation', 'rootTable', 'loader', 'createEntity', 'targetKey']);
|
|
68
|
+
hideWritable(this, ['loaded', 'current']);
|
|
69
|
+
this.populateFromHydrationCache();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async load(): Promise<TParent | null> {
|
|
73
|
+
if (this.loaded) return this.current;
|
|
74
|
+
const map = await this.loader();
|
|
75
|
+
const fkValue = (this.root as Record<string, unknown>)[this.relation.foreignKey];
|
|
76
|
+
if (fkValue === null || fkValue === undefined) {
|
|
77
|
+
this.current = null;
|
|
78
|
+
} else {
|
|
79
|
+
const row = map.get(toKey(fkValue));
|
|
80
|
+
this.current = row ? this.createEntity(row) : null;
|
|
81
|
+
}
|
|
82
|
+
this.loaded = true;
|
|
83
|
+
return this.current;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
get(): TParent | null {
|
|
87
|
+
return this.current;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
set(data: Partial<TParent> | TParent | null): TParent | null {
|
|
91
|
+
if (data === null) {
|
|
92
|
+
const previous = this.current;
|
|
93
|
+
(this.root as Record<string, unknown>)[this.relation.foreignKey] = null;
|
|
94
|
+
this.current = null;
|
|
95
|
+
this.ctx.registerRelationChange(
|
|
96
|
+
this.root,
|
|
97
|
+
this.relationKey,
|
|
98
|
+
this.rootTable,
|
|
99
|
+
this.relationName,
|
|
100
|
+
this.relation,
|
|
101
|
+
{ kind: 'remove', entity: previous }
|
|
102
|
+
);
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const entity = hasEntityMeta(data) ? (data as TParent) : this.createEntity(data as Record<string, unknown>);
|
|
107
|
+
const pkValue = (entity as Record<string, unknown>)[this.targetKey];
|
|
108
|
+
if (pkValue !== undefined) {
|
|
109
|
+
(this.root as Record<string, unknown>)[this.relation.foreignKey] = pkValue;
|
|
110
|
+
}
|
|
111
|
+
this.current = entity;
|
|
112
|
+
this.ctx.registerRelationChange(
|
|
113
|
+
this.root,
|
|
114
|
+
this.relationKey,
|
|
115
|
+
this.rootTable,
|
|
116
|
+
this.relationName,
|
|
117
|
+
this.relation,
|
|
118
|
+
{ kind: 'attach', entity }
|
|
119
|
+
);
|
|
120
|
+
return entity;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private get relationKey(): RelationKey {
|
|
124
|
+
return `${this.rootTable.name}.${this.relationName}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private populateFromHydrationCache(): void {
|
|
128
|
+
const fkValue = (this.root as Record<string, unknown>)[this.relation.foreignKey];
|
|
129
|
+
if (fkValue === undefined || fkValue === null) return;
|
|
130
|
+
const row = getHydrationRecord(this.meta, this.relationName, fkValue);
|
|
131
|
+
if (!row) return;
|
|
132
|
+
this.current = this.createEntity(row);
|
|
133
|
+
this.loaded = true;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
toJSON(): unknown {
|
|
137
|
+
if (!this.current) return null;
|
|
138
|
+
const entityWithToJSON = this.current as { toJSON?: () => unknown };
|
|
139
|
+
return typeof entityWithToJSON.toJSON === 'function'
|
|
140
|
+
? entityWithToJSON.toJSON()
|
|
141
|
+
: this.current;
|
|
142
|
+
}
|
|
143
|
+
}
|