metal-orm 1.1.9 → 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.
Files changed (73) hide show
  1. package/README.md +769 -764
  2. package/dist/index.cjs +2147 -239
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +559 -39
  5. package/dist/index.d.ts +559 -39
  6. package/dist/index.js +2119 -239
  7. package/dist/index.js.map +1 -1
  8. package/package.json +17 -12
  9. package/src/bulk/bulk-context.ts +83 -0
  10. package/src/bulk/bulk-delete-executor.ts +89 -0
  11. package/src/bulk/bulk-executor.base.ts +73 -0
  12. package/src/bulk/bulk-insert-executor.ts +74 -0
  13. package/src/bulk/bulk-types.ts +70 -0
  14. package/src/bulk/bulk-update-executor.ts +192 -0
  15. package/src/bulk/bulk-upsert-executor.ts +95 -0
  16. package/src/bulk/bulk-utils.ts +91 -0
  17. package/src/bulk/index.ts +18 -0
  18. package/src/codegen/typescript.ts +30 -21
  19. package/src/core/ast/expression-builders.ts +107 -10
  20. package/src/core/ast/expression-nodes.ts +52 -22
  21. package/src/core/ast/expression-visitor.ts +23 -13
  22. package/src/core/dialect/abstract.ts +30 -17
  23. package/src/core/dialect/mysql/index.ts +20 -5
  24. package/src/core/execution/db-executor.ts +96 -64
  25. package/src/core/execution/executors/better-sqlite3-executor.ts +94 -0
  26. package/src/core/execution/executors/mssql-executor.ts +66 -34
  27. package/src/core/execution/executors/mysql-executor.ts +98 -66
  28. package/src/core/execution/executors/postgres-executor.ts +33 -11
  29. package/src/core/execution/executors/sqlite-executor.ts +86 -30
  30. package/src/decorators/bootstrap.ts +482 -398
  31. package/src/decorators/column-decorator.ts +87 -96
  32. package/src/decorators/decorator-metadata.ts +100 -24
  33. package/src/decorators/entity.ts +27 -24
  34. package/src/decorators/relations.ts +231 -149
  35. package/src/decorators/transformers/transformer-decorators.ts +26 -29
  36. package/src/decorators/validators/country-validators-decorators.ts +9 -15
  37. package/src/dto/apply-filter.ts +568 -551
  38. package/src/index.ts +16 -9
  39. package/src/orm/entity-hydration.ts +116 -72
  40. package/src/orm/entity-metadata.ts +347 -301
  41. package/src/orm/entity-relations.ts +264 -207
  42. package/src/orm/entity.ts +199 -199
  43. package/src/orm/execute.ts +13 -13
  44. package/src/orm/lazy-batch/morph-many.ts +70 -0
  45. package/src/orm/lazy-batch/morph-one.ts +69 -0
  46. package/src/orm/lazy-batch/morph-to.ts +59 -0
  47. package/src/orm/lazy-batch.ts +4 -1
  48. package/src/orm/orm-session.ts +170 -104
  49. package/src/orm/pooled-executor-factory.ts +99 -58
  50. package/src/orm/query-logger.ts +49 -40
  51. package/src/orm/relation-change-processor.ts +198 -96
  52. package/src/orm/relations/belongs-to.ts +143 -143
  53. package/src/orm/relations/has-many.ts +204 -204
  54. package/src/orm/relations/has-one.ts +174 -174
  55. package/src/orm/relations/many-to-many.ts +288 -288
  56. package/src/orm/relations/morph-many.ts +156 -0
  57. package/src/orm/relations/morph-one.ts +151 -0
  58. package/src/orm/relations/morph-to.ts +162 -0
  59. package/src/orm/save-graph.ts +116 -1
  60. package/src/query-builder/expression-table-mapper.ts +5 -0
  61. package/src/query-builder/hydration-manager.ts +345 -345
  62. package/src/query-builder/hydration-planner.ts +178 -148
  63. package/src/query-builder/relation-conditions.ts +171 -151
  64. package/src/query-builder/relation-cte-builder.ts +5 -1
  65. package/src/query-builder/relation-filter-utils.ts +9 -6
  66. package/src/query-builder/relation-include-strategies.ts +44 -2
  67. package/src/query-builder/relation-join-strategies.ts +8 -1
  68. package/src/query-builder/relation-service.ts +250 -241
  69. package/src/query-builder/select/select-operations.ts +110 -105
  70. package/src/query-builder/update-include.ts +4 -0
  71. package/src/schema/relation.ts +296 -188
  72. package/src/schema/types.ts +138 -123
  73. package/src/tree/tree-decorator.ts +127 -137
@@ -0,0 +1,156 @@
1
+ import { HasManyCollection } from '../../schema/types.js';
2
+ import { EntityContext } from '../entity-context.js';
3
+ import { RelationKey } from '../runtime-types.js';
4
+ import { MorphManyRelation } from '../../schema/relation.js';
5
+ import { TableDef } from '../../schema/table.js';
6
+ import { EntityMeta, getHydrationRows } 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
+ export class DefaultMorphManyCollection<TChild> implements HasManyCollection<TChild> {
36
+ private loaded = false;
37
+ private items: TChild[] = [];
38
+ private readonly added = new Set<TChild>();
39
+ private readonly removed = new Set<TChild>();
40
+
41
+ constructor(
42
+ private readonly ctx: EntityContext,
43
+ private readonly meta: EntityMeta<TableDef>,
44
+ private readonly root: unknown,
45
+ private readonly relationName: string,
46
+ private readonly relation: MorphManyRelation,
47
+ private readonly rootTable: TableDef,
48
+ private readonly loader: () => Promise<Map<string, Rows>>,
49
+ private readonly createEntity: (row: Record<string, unknown>) => TChild,
50
+ private readonly localKey: string
51
+ ) {
52
+ hideInternal(this, ['ctx', 'meta', 'root', 'relationName', 'relation', 'rootTable', 'loader', 'createEntity', 'localKey']);
53
+ hideWritable(this, ['loaded', 'items', 'added', 'removed']);
54
+ this.hydrateFromCache();
55
+ }
56
+
57
+ async load(): Promise<TChild[]> {
58
+ if (this.loaded) return this.items;
59
+ const map = await this.loader();
60
+ const key = toKey((this.root as Record<string, unknown>)[this.localKey]);
61
+ const rows = map.get(key) ?? [];
62
+ this.items = rows.map(row => this.createEntity(row));
63
+ this.loaded = true;
64
+ return this.items;
65
+ }
66
+
67
+ getItems(): TChild[] {
68
+ return this.items;
69
+ }
70
+
71
+ get length(): number {
72
+ return this.items.length;
73
+ }
74
+
75
+ [Symbol.iterator](): Iterator<TChild> {
76
+ return this.items[Symbol.iterator]();
77
+ }
78
+
79
+ add(data: Partial<TChild>): TChild {
80
+ const keyValue = (this.root as Record<string, unknown>)[this.localKey];
81
+ const childRow: Record<string, unknown> = {
82
+ ...data,
83
+ [this.relation.idField]: keyValue,
84
+ [this.relation.typeField]: this.relation.typeValue
85
+ };
86
+ const entity = this.createEntity(childRow);
87
+ this.added.add(entity);
88
+ this.items.push(entity);
89
+ this.ctx.registerRelationChange(
90
+ this.root,
91
+ this.relationKey,
92
+ this.rootTable,
93
+ this.relationName,
94
+ this.relation,
95
+ { kind: 'add', entity }
96
+ );
97
+ return entity;
98
+ }
99
+
100
+ attach(entity: TChild): void {
101
+ const keyValue = (this.root as Record<string, unknown>)[this.localKey];
102
+ (entity as Record<string, unknown>)[this.relation.idField] = keyValue;
103
+ (entity as Record<string, unknown>)[this.relation.typeField] = this.relation.typeValue;
104
+ this.ctx.markDirty(entity as object);
105
+ this.items.push(entity);
106
+ this.ctx.registerRelationChange(
107
+ this.root,
108
+ this.relationKey,
109
+ this.rootTable,
110
+ this.relationName,
111
+ this.relation,
112
+ { kind: 'attach', entity }
113
+ );
114
+ }
115
+
116
+ remove(entity: TChild): void {
117
+ this.items = this.items.filter(item => item !== entity);
118
+ this.removed.add(entity);
119
+ this.ctx.registerRelationChange(
120
+ this.root,
121
+ this.relationKey,
122
+ this.rootTable,
123
+ this.relationName,
124
+ this.relation,
125
+ { kind: 'remove', entity }
126
+ );
127
+ }
128
+
129
+ clear(): void {
130
+ for (const entity of [...this.items]) {
131
+ this.remove(entity);
132
+ }
133
+ }
134
+
135
+ private get relationKey(): RelationKey {
136
+ return `${this.rootTable.name}.${this.relationName}`;
137
+ }
138
+
139
+ private hydrateFromCache(): void {
140
+ const keyValue = (this.root as Record<string, unknown>)[this.localKey];
141
+ if (keyValue === undefined || keyValue === null) return;
142
+ const rows = getHydrationRows(this.meta, this.relationName, keyValue);
143
+ if (!rows?.length) return;
144
+ this.items = rows.map(row => this.createEntity(row));
145
+ this.loaded = true;
146
+ }
147
+
148
+ toJSON(): unknown[] {
149
+ return this.items.map(item => {
150
+ const entityWithToJSON = item as { toJSON?: () => unknown };
151
+ return typeof entityWithToJSON.toJSON === 'function'
152
+ ? entityWithToJSON.toJSON()
153
+ : item;
154
+ });
155
+ }
156
+ }
@@ -0,0 +1,151 @@
1
+ import { HasOneReferenceApi } from '../../schema/types.js';
2
+ import { EntityContext } from '../entity-context.js';
3
+ import { RelationKey } from '../runtime-types.js';
4
+ import { MorphOneRelation } from '../../schema/relation.js';
5
+ import { TableDef } from '../../schema/table.js';
6
+ import { EntityMeta, getHydrationRecord, hasEntityMeta } from '../entity-meta.js';
7
+
8
+ type Row = 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
+ export class DefaultMorphOneReference<TChild extends object> implements HasOneReferenceApi<TChild> {
36
+ private loaded = false;
37
+ private current: TChild | null = null;
38
+
39
+ constructor(
40
+ private readonly ctx: EntityContext,
41
+ private readonly meta: EntityMeta<TableDef>,
42
+ private readonly root: unknown,
43
+ private readonly relationName: string,
44
+ private readonly relation: MorphOneRelation,
45
+ private readonly rootTable: TableDef,
46
+ private readonly loader: () => Promise<Map<string, Row>>,
47
+ private readonly createEntity: (row: Row) => TChild,
48
+ private readonly localKey: string
49
+ ) {
50
+ hideInternal(this, [
51
+ 'ctx', 'meta', 'root', 'relationName', 'relation',
52
+ 'rootTable', 'loader', 'createEntity', 'localKey'
53
+ ]);
54
+ hideWritable(this, ['loaded', 'current']);
55
+ this.populateFromHydrationCache();
56
+ }
57
+
58
+ async load(): Promise<TChild | null> {
59
+ if (this.loaded) return this.current;
60
+ const map = await this.loader();
61
+ const keyValue = (this.root as Record<string, unknown>)[this.localKey];
62
+ if (keyValue === undefined || keyValue === null) {
63
+ this.loaded = true;
64
+ return this.current;
65
+ }
66
+ const row = map.get(toKey(keyValue));
67
+ this.current = row ? this.createEntity(row) : null;
68
+ this.loaded = true;
69
+ return this.current;
70
+ }
71
+
72
+ get(): TChild | null {
73
+ return this.current;
74
+ }
75
+
76
+ set(data: Partial<TChild> | TChild | null): TChild | null {
77
+ if (data === null) {
78
+ return this.detachCurrent();
79
+ }
80
+
81
+ const entity = hasEntityMeta(data) ? (data as TChild) : this.createEntity(data as Row);
82
+ if (this.current && this.current !== entity) {
83
+ this.ctx.registerRelationChange(
84
+ this.root,
85
+ this.relationKey,
86
+ this.rootTable,
87
+ this.relationName,
88
+ this.relation,
89
+ { kind: 'remove', entity: this.current }
90
+ );
91
+ }
92
+
93
+ this.assignMorphKeys(entity);
94
+ this.current = entity;
95
+ this.loaded = true;
96
+
97
+ this.ctx.registerRelationChange(
98
+ this.root,
99
+ this.relationKey,
100
+ this.rootTable,
101
+ this.relationName,
102
+ this.relation,
103
+ { kind: 'attach', entity }
104
+ );
105
+
106
+ return entity;
107
+ }
108
+
109
+ toJSON(): unknown {
110
+ if (!this.current) return null;
111
+ const entityWithToJSON = this.current as { toJSON?: () => unknown };
112
+ return typeof entityWithToJSON.toJSON === 'function'
113
+ ? entityWithToJSON.toJSON()
114
+ : this.current;
115
+ }
116
+
117
+ private detachCurrent(): TChild | null {
118
+ const previous = this.current;
119
+ if (!previous) return null;
120
+ this.current = null;
121
+ this.loaded = true;
122
+ this.ctx.registerRelationChange(
123
+ this.root,
124
+ this.relationKey,
125
+ this.rootTable,
126
+ this.relationName,
127
+ this.relation,
128
+ { kind: 'remove', entity: previous }
129
+ );
130
+ return null;
131
+ }
132
+
133
+ private assignMorphKeys(entity: TChild): void {
134
+ const keyValue = (this.root as Record<string, unknown>)[this.localKey];
135
+ (entity as Row)[this.relation.idField] = keyValue;
136
+ (entity as Row)[this.relation.typeField] = this.relation.typeValue;
137
+ }
138
+
139
+ private get relationKey(): RelationKey {
140
+ return `${this.rootTable.name}.${this.relationName}`;
141
+ }
142
+
143
+ private populateFromHydrationCache(): void {
144
+ const keyValue = (this.root as Record<string, unknown>)[this.localKey];
145
+ if (keyValue === undefined || keyValue === null) return;
146
+ const row = getHydrationRecord(this.meta, this.relationName, keyValue);
147
+ if (!row) return;
148
+ this.current = this.createEntity(row);
149
+ this.loaded = true;
150
+ }
151
+ }
@@ -0,0 +1,162 @@
1
+ import { BelongsToReferenceApi } from '../../schema/types.js';
2
+ import { EntityContext } from '../entity-context.js';
3
+ import { RelationKey } from '../runtime-types.js';
4
+ import { MorphToRelation } from '../../schema/relation.js';
5
+ import { TableDef } from '../../schema/table.js';
6
+ import { EntityMeta, getHydrationRecord, hasEntityMeta } from '../entity-meta.js';
7
+
8
+ type Row = 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
+ export class DefaultMorphToReference<TParent extends object> implements BelongsToReferenceApi<TParent> {
36
+ private loaded = false;
37
+ private current: TParent | null = null;
38
+
39
+ constructor(
40
+ private readonly ctx: EntityContext,
41
+ private readonly meta: EntityMeta<TableDef>,
42
+ private readonly root: unknown,
43
+ private readonly relationName: string,
44
+ private readonly relation: MorphToRelation,
45
+ private readonly rootTable: TableDef,
46
+ private readonly loader: () => Promise<Map<string, Row>>,
47
+ private readonly createEntity: (table: TableDef, row: Row) => TParent,
48
+ private readonly resolveTargetTable: (typeValue: string) => TableDef | undefined
49
+ ) {
50
+ hideInternal(this, [
51
+ 'ctx', 'meta', 'root', 'relationName', 'relation',
52
+ 'rootTable', 'loader', 'createEntity', 'resolveTargetTable'
53
+ ]);
54
+ hideWritable(this, ['loaded', 'current']);
55
+ this.populateFromHydrationCache();
56
+ }
57
+
58
+ async load(): Promise<TParent | null> {
59
+ if (this.loaded) return this.current;
60
+ const rootObj = this.root as Row;
61
+ const typeValue = rootObj[this.relation.typeField];
62
+ const idValue = rootObj[this.relation.idField];
63
+ if (!typeValue || idValue === undefined || idValue === null) {
64
+ this.loaded = true;
65
+ return this.current;
66
+ }
67
+
68
+ const map = await this.loader();
69
+ const compositeKey = `${toKey(typeValue)}:${toKey(idValue)}`;
70
+ const row = map.get(compositeKey);
71
+ if (row) {
72
+ const targetTable = this.resolveTargetTable(toKey(typeValue));
73
+ if (targetTable) {
74
+ this.current = this.createEntity(targetTable, row);
75
+ }
76
+ }
77
+ this.loaded = true;
78
+ return this.current;
79
+ }
80
+
81
+ get(): TParent | null {
82
+ return this.current;
83
+ }
84
+
85
+ set(data: Partial<TParent> | TParent | null): TParent | null {
86
+ if (data === null) {
87
+ return this.detachCurrent();
88
+ }
89
+
90
+ const entity = hasEntityMeta(data) ? (data as TParent) : (data as TParent);
91
+ if (this.current && this.current !== entity) {
92
+ this.ctx.registerRelationChange(
93
+ this.root,
94
+ this.relationKey,
95
+ this.rootTable,
96
+ this.relationName,
97
+ this.relation,
98
+ { kind: 'remove', entity: this.current }
99
+ );
100
+ }
101
+
102
+ this.current = entity;
103
+ this.loaded = true;
104
+
105
+ this.ctx.registerRelationChange(
106
+ this.root,
107
+ this.relationKey,
108
+ this.rootTable,
109
+ this.relationName,
110
+ this.relation,
111
+ { kind: 'attach', entity }
112
+ );
113
+
114
+ return entity;
115
+ }
116
+
117
+ toJSON(): unknown {
118
+ if (!this.current) return null;
119
+ const entityWithToJSON = this.current as { toJSON?: () => unknown };
120
+ return typeof entityWithToJSON.toJSON === 'function'
121
+ ? entityWithToJSON.toJSON()
122
+ : this.current;
123
+ }
124
+
125
+ private detachCurrent(): TParent | null {
126
+ const previous = this.current;
127
+ if (!previous) return null;
128
+ this.current = null;
129
+ this.loaded = true;
130
+ const rootObj = this.root as Row;
131
+ rootObj[this.relation.typeField] = null;
132
+ rootObj[this.relation.idField] = null;
133
+ this.ctx.registerRelationChange(
134
+ this.root,
135
+ this.relationKey,
136
+ this.rootTable,
137
+ this.relationName,
138
+ this.relation,
139
+ { kind: 'remove', entity: previous }
140
+ );
141
+ return null;
142
+ }
143
+
144
+ private get relationKey(): RelationKey {
145
+ return `${this.rootTable.name}.${this.relationName}`;
146
+ }
147
+
148
+ private populateFromHydrationCache(): void {
149
+ const rootObj = this.root as Row;
150
+ const typeValue = rootObj[this.relation.typeField];
151
+ const idValue = rootObj[this.relation.idField];
152
+ if (!typeValue || idValue === undefined || idValue === null) return;
153
+ const compositeKey = `${toKey(typeValue)}:${toKey(idValue)}`;
154
+ const row = getHydrationRecord(this.meta, this.relationName, compositeKey);
155
+ if (!row) return;
156
+ const targetTable = this.resolveTargetTable(toKey(typeValue));
157
+ if (targetTable) {
158
+ this.current = this.createEntity(targetTable, row);
159
+ this.loaded = true;
160
+ }
161
+ }
162
+ }
@@ -12,7 +12,10 @@ import {
12
12
  type BelongsToRelation,
13
13
  type HasManyRelation,
14
14
  type HasOneRelation,
15
- type RelationDef
15
+ type RelationDef,
16
+ type MorphOneRelation,
17
+ type MorphManyRelation,
18
+ type MorphToRelation
16
19
  } from '../schema/relation.js';
17
20
  import type { TableDef } from '../schema/table.js';
18
21
  import { findPrimaryKey } from '../query-builder/hydration-planner.js';
@@ -323,6 +326,112 @@ const handleBelongsToMany = async (
323
326
  }
324
327
  };
325
328
 
329
+ const handleMorphOne = async (
330
+ session: OrmSession,
331
+ root: AnyEntity,
332
+ relationName: string,
333
+ relation: MorphOneRelation,
334
+ payload: unknown,
335
+ options: SaveGraphOptions
336
+ ): Promise<void> => {
337
+ const ref = root[relationName] as unknown as HasOneReference<object>;
338
+ if (payload === undefined) return;
339
+ if (payload === null) {
340
+ ref.set(null);
341
+ return;
342
+ }
343
+ const pk = findPrimaryKey(relation.target);
344
+ if (typeof payload === 'number' || typeof payload === 'string') {
345
+ const entity = ref.set({ [pk]: payload });
346
+ if (entity) {
347
+ await applyGraphToEntity(session, relation.target, entity as AnyEntity, { [pk]: payload as PrimaryKey }, options);
348
+ }
349
+ return;
350
+ }
351
+ const attached = ref.set(payload as AnyEntity);
352
+ if (attached) {
353
+ await applyGraphToEntity(session, relation.target, attached as AnyEntity, payload as AnyEntity, options);
354
+ }
355
+ };
356
+
357
+ const handleMorphMany = async (
358
+ session: OrmSession,
359
+ root: AnyEntity,
360
+ relationName: string,
361
+ relation: MorphManyRelation,
362
+ payload: unknown,
363
+ options: SaveGraphOptions
364
+ ): Promise<void> => {
365
+ if (!Array.isArray(payload)) return;
366
+ const collection = root[relationName] as unknown as HasManyCollection<unknown>;
367
+ await collection.load();
368
+
369
+ const targetTable = relation.target;
370
+ const targetPk = findPrimaryKey(targetTable);
371
+ const existing = collection.getItems() as unknown as AnyEntity[];
372
+ const seen = new Set<string>();
373
+
374
+ for (const item of payload) {
375
+ if (item === null || item === undefined) continue;
376
+ const asObj = typeof item === 'object' ? (item as AnyEntity) : { [targetPk]: item };
377
+ const pkValue = asObj[targetPk];
378
+
379
+ const current =
380
+ findInCollectionByPk(existing, targetPk, pkValue) ??
381
+ (pkValue !== undefined && pkValue !== null ? session.getEntity(targetTable, pkValue as PrimaryKey) : undefined);
382
+
383
+ const entity = current ?? ensureEntity(session, targetTable, asObj, options);
384
+ assignColumns(targetTable, entity as AnyEntity, asObj, options);
385
+ await applyGraphToEntity(session, targetTable, entity as AnyEntity, asObj, options);
386
+
387
+ if (!isEntityInCollection(collection.getItems() as unknown as AnyEntity[], targetPk, entity as unknown as AnyEntity)) {
388
+ collection.attach(entity);
389
+ }
390
+
391
+ if (pkValue !== undefined && pkValue !== null) {
392
+ seen.add(toKey(pkValue));
393
+ }
394
+ }
395
+
396
+ if (options.pruneMissing) {
397
+ for (const item of [...collection.getItems()]) {
398
+ const pkValue = item[targetPk];
399
+ if (pkValue !== undefined && pkValue !== null && !seen.has(toKey(pkValue))) {
400
+ collection.remove(item);
401
+ }
402
+ }
403
+ }
404
+ };
405
+
406
+ const handleMorphTo = async (
407
+ session: OrmSession,
408
+ root: AnyEntity,
409
+ relationName: string,
410
+ relation: MorphToRelation,
411
+ payload: unknown,
412
+ options: SaveGraphOptions
413
+ ): Promise<void> => {
414
+ const ref = root[relationName] as unknown as BelongsToReference<object>;
415
+ if (payload === undefined) return;
416
+ if (payload === null) {
417
+ ref.set(null);
418
+ return;
419
+ }
420
+ // MorphTo set — the wrapper handles setting typeField/idField on the root
421
+ const attached = ref.set(payload as AnyEntity);
422
+ if (attached) {
423
+ // Try to find the right target table
424
+ for (const [, targetTable] of Object.entries(relation.targets)) {
425
+ const pk = relation.targetKey || findPrimaryKey(targetTable);
426
+ const pkValue = (attached as AnyEntity)[pk];
427
+ if (pkValue !== undefined && pkValue !== null) {
428
+ await applyGraphToEntity(session, targetTable, attached as AnyEntity, payload as AnyEntity, options);
429
+ break;
430
+ }
431
+ }
432
+ }
433
+ };
434
+
326
435
  const applyRelation = async (
327
436
  session: OrmSession,
328
437
  table: TableDef,
@@ -341,6 +450,12 @@ const applyRelation = async (
341
450
  return handleBelongsTo(session, entity, relationName, relation, payload, options);
342
451
  case RelationKinds.BelongsToMany:
343
452
  return handleBelongsToMany(session, entity, relationName, relation, payload, options);
453
+ case RelationKinds.MorphOne:
454
+ return handleMorphOne(session, entity, relationName, relation as MorphOneRelation, payload, options);
455
+ case RelationKinds.MorphMany:
456
+ return handleMorphMany(session, entity, relationName, relation as MorphManyRelation, payload, options);
457
+ case RelationKinds.MorphTo:
458
+ return handleMorphTo(session, entity, relationName, relation as MorphToRelation, payload, options);
344
459
  }
345
460
  };
346
461
 
@@ -34,6 +34,11 @@ const mapExpression = (expr: ExpressionNode, fromTable: string, toTable: string)
34
34
  if (nextOperands.every((op, i) => op === expr.operands[i])) return expr;
35
35
  return { ...expr, operands: nextOperands };
36
36
  }
37
+ case 'NotExpression': {
38
+ const operand = mapExpression(expr.operand, fromTable, toTable);
39
+ if (operand === expr.operand) return expr;
40
+ return { ...expr, operand };
41
+ }
37
42
  case 'NullExpression': {
38
43
  const left = mapOperand(expr.left, fromTable, toTable);
39
44
  if (left === expr.left) return expr;