metal-orm 1.0.105 → 1.0.107
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/dist/index.cjs +136 -130
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +8 -6
- package/dist/index.d.ts +8 -6
- package/dist/index.js +136 -130
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/orm/entity-relations.ts +7 -30
- package/src/orm/entity.ts +10 -17
- package/src/orm/relations/belongs-to.ts +28 -19
- package/src/orm/relations/has-many.ts +35 -26
- package/src/orm/relations/has-one.ts +33 -24
- package/src/orm/relations/many-to-many.ts +33 -24
package/package.json
CHANGED
|
@@ -17,36 +17,13 @@ export type RelationEntityFactory = (
|
|
|
17
17
|
|
|
18
18
|
const proxifyRelationWrapper = <T extends object>(wrapper: T): T => {
|
|
19
19
|
return new Proxy(wrapper, {
|
|
20
|
-
get(target, prop,
|
|
21
|
-
// Intercept toJSON to ensure proper serialization
|
|
22
|
-
if (prop === 'toJSON') {
|
|
23
|
-
return () => {
|
|
24
|
-
// Call the wrapper's toJSON method if it exists
|
|
25
|
-
const wrapperToJSON = (target as { toJSON?: () => unknown }).toJSON;
|
|
26
|
-
if (typeof wrapperToJSON === 'function') {
|
|
27
|
-
return wrapperToJSON.call(target);
|
|
28
|
-
}
|
|
29
|
-
// Fallback: return the underlying data
|
|
30
|
-
const getRef = (target as { get?: () => unknown }).get;
|
|
31
|
-
if (typeof getRef === 'function') {
|
|
32
|
-
return getRef.call(target);
|
|
33
|
-
}
|
|
34
|
-
const getItems = (target as { getItems?: () => unknown }).getItems;
|
|
35
|
-
if (typeof getItems === 'function') {
|
|
36
|
-
return getItems.call(target);
|
|
37
|
-
}
|
|
38
|
-
return target;
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
20
|
+
get(target, prop, receiver) {
|
|
42
21
|
if (typeof prop === 'symbol') {
|
|
43
|
-
|
|
44
|
-
return typeof value === 'function' ? value.bind(target) : value;
|
|
22
|
+
return Reflect.get(target, prop, receiver);
|
|
45
23
|
}
|
|
46
24
|
|
|
47
25
|
if (prop in target) {
|
|
48
|
-
|
|
49
|
-
return typeof value === 'function' ? value.bind(target) : value;
|
|
26
|
+
return Reflect.get(target, prop, receiver);
|
|
50
27
|
}
|
|
51
28
|
|
|
52
29
|
const getItems = (target as { getItems?: () => unknown }).getItems;
|
|
@@ -72,13 +49,13 @@ const proxifyRelationWrapper = <T extends object>(wrapper: T): T => {
|
|
|
72
49
|
return undefined;
|
|
73
50
|
},
|
|
74
51
|
|
|
75
|
-
set(target, prop, value,
|
|
52
|
+
set(target, prop, value, receiver) {
|
|
76
53
|
if (typeof prop === 'symbol') {
|
|
77
|
-
return Reflect.set(target, prop, value,
|
|
54
|
+
return Reflect.set(target, prop, value, receiver);
|
|
78
55
|
}
|
|
79
56
|
|
|
80
57
|
if (prop in target) {
|
|
81
|
-
return Reflect.set(target, prop, value,
|
|
58
|
+
return Reflect.set(target, prop, value, receiver);
|
|
82
59
|
}
|
|
83
60
|
|
|
84
61
|
const getRef = (target as { get?: () => unknown }).get;
|
|
@@ -95,7 +72,7 @@ const proxifyRelationWrapper = <T extends object>(wrapper: T): T => {
|
|
|
95
72
|
return Reflect.set(items as object, prop, value);
|
|
96
73
|
}
|
|
97
74
|
|
|
98
|
-
return Reflect.set(target, prop, value,
|
|
75
|
+
return Reflect.set(target, prop, value, receiver);
|
|
99
76
|
}
|
|
100
77
|
});
|
|
101
78
|
};
|
package/src/orm/entity.ts
CHANGED
|
@@ -11,16 +11,7 @@ export { relationLoaderCache } from './entity-relation-cache.js';
|
|
|
11
11
|
|
|
12
12
|
const isRelationWrapperLoaded = (value: unknown): boolean => {
|
|
13
13
|
if (!value || typeof value !== 'object') return false;
|
|
14
|
-
|
|
15
|
-
if (typeof wrapper.isLoaded === 'function') return wrapper.isLoaded();
|
|
16
|
-
return false;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const unwrapRelationForJson = (value: unknown): unknown => {
|
|
20
|
-
if (value && typeof value === 'object' && typeof (value as { toJSON?: () => unknown }).toJSON === 'function') {
|
|
21
|
-
return (value as { toJSON: () => unknown }).toJSON();
|
|
22
|
-
}
|
|
23
|
-
return value;
|
|
14
|
+
return Boolean((value as { loaded?: boolean }).loaded);
|
|
24
15
|
};
|
|
25
16
|
|
|
26
17
|
type JsonSource<TTable extends TableDef> = EntityInstance<TTable> & Record<string, unknown>;
|
|
@@ -61,14 +52,16 @@ export const createEntityProxy = <
|
|
|
61
52
|
const buildJson = (self: JsonSource<TTable>): Record<string, unknown> => {
|
|
62
53
|
const json: Record<string, unknown> = {};
|
|
63
54
|
for (const key of Object.keys(target)) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
json[key] = unwrapRelationForJson(value);
|
|
55
|
+
if (table.relations[key]) continue;
|
|
56
|
+
json[key] = self[key];
|
|
67
57
|
}
|
|
68
|
-
for (const
|
|
69
|
-
|
|
70
|
-
if (isRelationWrapperLoaded(wrapper)) {
|
|
71
|
-
|
|
58
|
+
for (const relationName of Object.keys(table.relations)) {
|
|
59
|
+
const wrapper = self[relationName];
|
|
60
|
+
if (wrapper && isRelationWrapperLoaded(wrapper)) {
|
|
61
|
+
const wrapperWithToJSON = wrapper as { toJSON?: () => unknown };
|
|
62
|
+
json[relationName] = typeof wrapperWithToJSON.toJSON === 'function'
|
|
63
|
+
? wrapperWithToJSON.toJSON()
|
|
64
|
+
: wrapper;
|
|
72
65
|
}
|
|
73
66
|
}
|
|
74
67
|
return json;
|
|
@@ -14,7 +14,19 @@ const hideInternal = (obj: object, keys: string[]): void => {
|
|
|
14
14
|
Object.defineProperty(obj, key, {
|
|
15
15
|
value: obj[key],
|
|
16
16
|
writable: false,
|
|
17
|
-
configurable:
|
|
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,
|
|
18
30
|
enumerable: false
|
|
19
31
|
});
|
|
20
32
|
}
|
|
@@ -27,8 +39,8 @@ const hideInternal = (obj: object, keys: string[]): void => {
|
|
|
27
39
|
* @template TParent The type of the parent entity.
|
|
28
40
|
*/
|
|
29
41
|
export class DefaultBelongsToReference<TParent extends object> implements BelongsToReferenceApi<TParent> {
|
|
30
|
-
|
|
31
|
-
|
|
42
|
+
private loaded = false;
|
|
43
|
+
private current: TParent | null = null;
|
|
32
44
|
|
|
33
45
|
/**
|
|
34
46
|
* @param ctx The entity context for tracking changes.
|
|
@@ -53,32 +65,33 @@ export class DefaultBelongsToReference<TParent extends object> implements Belong
|
|
|
53
65
|
private readonly targetKey: string
|
|
54
66
|
) {
|
|
55
67
|
hideInternal(this, ['ctx', 'meta', 'root', 'relationName', 'relation', 'rootTable', 'loader', 'createEntity', 'targetKey']);
|
|
68
|
+
hideWritable(this, ['loaded', 'current']);
|
|
56
69
|
this.populateFromHydrationCache();
|
|
57
70
|
}
|
|
58
71
|
|
|
59
72
|
async load(): Promise<TParent | null> {
|
|
60
|
-
if (this
|
|
73
|
+
if (this.loaded) return this.current;
|
|
61
74
|
const map = await this.loader();
|
|
62
75
|
const fkValue = (this.root as Record<string, unknown>)[this.relation.foreignKey];
|
|
63
76
|
if (fkValue === null || fkValue === undefined) {
|
|
64
|
-
this
|
|
77
|
+
this.current = null;
|
|
65
78
|
} else {
|
|
66
79
|
const row = map.get(toKey(fkValue));
|
|
67
|
-
this
|
|
80
|
+
this.current = row ? this.createEntity(row) : null;
|
|
68
81
|
}
|
|
69
|
-
this
|
|
70
|
-
return this
|
|
82
|
+
this.loaded = true;
|
|
83
|
+
return this.current;
|
|
71
84
|
}
|
|
72
85
|
|
|
73
86
|
get(): TParent | null {
|
|
74
|
-
return this
|
|
87
|
+
return this.current;
|
|
75
88
|
}
|
|
76
89
|
|
|
77
90
|
set(data: Partial<TParent> | TParent | null): TParent | null {
|
|
78
91
|
if (data === null) {
|
|
79
|
-
const previous = this
|
|
92
|
+
const previous = this.current;
|
|
80
93
|
(this.root as Record<string, unknown>)[this.relation.foreignKey] = null;
|
|
81
|
-
this
|
|
94
|
+
this.current = null;
|
|
82
95
|
this.ctx.registerRelationChange(
|
|
83
96
|
this.root,
|
|
84
97
|
this.relationKey,
|
|
@@ -95,7 +108,7 @@ export class DefaultBelongsToReference<TParent extends object> implements Belong
|
|
|
95
108
|
if (pkValue !== undefined) {
|
|
96
109
|
(this.root as Record<string, unknown>)[this.relation.foreignKey] = pkValue;
|
|
97
110
|
}
|
|
98
|
-
this
|
|
111
|
+
this.current = entity;
|
|
99
112
|
this.ctx.registerRelationChange(
|
|
100
113
|
this.root,
|
|
101
114
|
this.relationKey,
|
|
@@ -107,10 +120,6 @@ export class DefaultBelongsToReference<TParent extends object> implements Belong
|
|
|
107
120
|
return entity;
|
|
108
121
|
}
|
|
109
122
|
|
|
110
|
-
isLoaded(): boolean {
|
|
111
|
-
return this.#loaded;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
123
|
private get relationKey(): RelationKey {
|
|
115
124
|
return `${this.rootTable.name}.${this.relationName}`;
|
|
116
125
|
}
|
|
@@ -120,11 +129,11 @@ export class DefaultBelongsToReference<TParent extends object> implements Belong
|
|
|
120
129
|
if (fkValue === undefined || fkValue === null) return;
|
|
121
130
|
const row = getHydrationRecord(this.meta, this.relationName, fkValue);
|
|
122
131
|
if (!row) return;
|
|
123
|
-
this
|
|
124
|
-
this
|
|
132
|
+
this.current = this.createEntity(row);
|
|
133
|
+
this.loaded = true;
|
|
125
134
|
}
|
|
126
135
|
|
|
127
136
|
toJSON(): TParent | null {
|
|
128
|
-
return this
|
|
137
|
+
return this.current;
|
|
129
138
|
}
|
|
130
139
|
}
|
|
@@ -14,7 +14,19 @@ const hideInternal = (obj: object, keys: string[]): void => {
|
|
|
14
14
|
Object.defineProperty(obj, key, {
|
|
15
15
|
value: obj[key],
|
|
16
16
|
writable: false,
|
|
17
|
-
configurable:
|
|
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,
|
|
18
30
|
enumerable: false
|
|
19
31
|
});
|
|
20
32
|
}
|
|
@@ -25,10 +37,10 @@ const hideInternal = (obj: object, keys: string[]): void => {
|
|
|
25
37
|
* @template TChild - The type of child entities in the collection
|
|
26
38
|
*/
|
|
27
39
|
export class DefaultHasManyCollection<TChild> implements HasManyCollection<TChild> {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
readonly
|
|
31
|
-
readonly
|
|
40
|
+
private loaded = false;
|
|
41
|
+
private items: TChild[] = [];
|
|
42
|
+
private readonly added = new Set<TChild>();
|
|
43
|
+
private readonly removed = new Set<TChild>();
|
|
32
44
|
|
|
33
45
|
/**
|
|
34
46
|
* Creates a new DefaultHasManyCollection instance.
|
|
@@ -54,6 +66,7 @@ export class DefaultHasManyCollection<TChild> implements HasManyCollection<TChil
|
|
|
54
66
|
private readonly localKey: string
|
|
55
67
|
) {
|
|
56
68
|
hideInternal(this, ['ctx', 'meta', 'root', 'relationName', 'relation', 'rootTable', 'loader', 'createEntity', 'localKey']);
|
|
69
|
+
hideWritable(this, ['loaded', 'items', 'added', 'removed']);
|
|
57
70
|
this.hydrateFromCache();
|
|
58
71
|
}
|
|
59
72
|
|
|
@@ -62,13 +75,13 @@ export class DefaultHasManyCollection<TChild> implements HasManyCollection<TChil
|
|
|
62
75
|
* @returns Promise resolving to the array of child entities
|
|
63
76
|
*/
|
|
64
77
|
async load(): Promise<TChild[]> {
|
|
65
|
-
if (this
|
|
78
|
+
if (this.loaded) return this.items;
|
|
66
79
|
const map = await this.loader();
|
|
67
80
|
const key = toKey((this.root as Record<string, unknown>)[this.localKey]);
|
|
68
81
|
const rows = map.get(key) ?? [];
|
|
69
|
-
this
|
|
70
|
-
this
|
|
71
|
-
return this
|
|
82
|
+
this.items = rows.map(row => this.createEntity(row));
|
|
83
|
+
this.loaded = true;
|
|
84
|
+
return this.items;
|
|
72
85
|
}
|
|
73
86
|
|
|
74
87
|
/**
|
|
@@ -76,21 +89,21 @@ export class DefaultHasManyCollection<TChild> implements HasManyCollection<TChil
|
|
|
76
89
|
* @returns Array of child entities
|
|
77
90
|
*/
|
|
78
91
|
getItems(): TChild[] {
|
|
79
|
-
return this
|
|
92
|
+
return this.items;
|
|
80
93
|
}
|
|
81
|
-
|
|
94
|
+
|
|
82
95
|
/**
|
|
83
96
|
* Array-compatible length for testing frameworks.
|
|
84
97
|
*/
|
|
85
98
|
get length(): number {
|
|
86
|
-
return this
|
|
99
|
+
return this.items.length;
|
|
87
100
|
}
|
|
88
101
|
|
|
89
102
|
/**
|
|
90
103
|
* Enables iteration over the collection like an array.
|
|
91
104
|
*/
|
|
92
105
|
[Symbol.iterator](): Iterator<TChild> {
|
|
93
|
-
return this
|
|
106
|
+
return this.items[Symbol.iterator]();
|
|
94
107
|
}
|
|
95
108
|
|
|
96
109
|
/**
|
|
@@ -105,8 +118,8 @@ export class DefaultHasManyCollection<TChild> implements HasManyCollection<TChil
|
|
|
105
118
|
[this.relation.foreignKey]: keyValue
|
|
106
119
|
};
|
|
107
120
|
const entity = this.createEntity(childRow);
|
|
108
|
-
this
|
|
109
|
-
this
|
|
121
|
+
this.added.add(entity);
|
|
122
|
+
this.items.push(entity);
|
|
110
123
|
this.ctx.registerRelationChange(
|
|
111
124
|
this.root,
|
|
112
125
|
this.relationKey,
|
|
@@ -126,7 +139,7 @@ export class DefaultHasManyCollection<TChild> implements HasManyCollection<TChil
|
|
|
126
139
|
const keyValue = this.root[this.localKey];
|
|
127
140
|
(entity as Record<string, unknown>)[this.relation.foreignKey] = keyValue;
|
|
128
141
|
this.ctx.markDirty(entity as object);
|
|
129
|
-
this
|
|
142
|
+
this.items.push(entity);
|
|
130
143
|
this.ctx.registerRelationChange(
|
|
131
144
|
this.root,
|
|
132
145
|
this.relationKey,
|
|
@@ -142,8 +155,8 @@ export class DefaultHasManyCollection<TChild> implements HasManyCollection<TChil
|
|
|
142
155
|
* @param entity - The entity to remove
|
|
143
156
|
*/
|
|
144
157
|
remove(entity: TChild): void {
|
|
145
|
-
this
|
|
146
|
-
this
|
|
158
|
+
this.items = this.items.filter(item => item !== entity);
|
|
159
|
+
this.removed.add(entity);
|
|
147
160
|
this.ctx.registerRelationChange(
|
|
148
161
|
this.root,
|
|
149
162
|
this.relationKey,
|
|
@@ -158,15 +171,11 @@ export class DefaultHasManyCollection<TChild> implements HasManyCollection<TChil
|
|
|
158
171
|
* Clears all entities from the collection.
|
|
159
172
|
*/
|
|
160
173
|
clear(): void {
|
|
161
|
-
for (const entity of [...this
|
|
174
|
+
for (const entity of [...this.items]) {
|
|
162
175
|
this.remove(entity);
|
|
163
176
|
}
|
|
164
177
|
}
|
|
165
178
|
|
|
166
|
-
isLoaded(): boolean {
|
|
167
|
-
return this.#loaded;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
179
|
private get relationKey(): RelationKey {
|
|
171
180
|
return `${this.rootTable.name}.${this.relationName}`;
|
|
172
181
|
}
|
|
@@ -176,8 +185,8 @@ export class DefaultHasManyCollection<TChild> implements HasManyCollection<TChil
|
|
|
176
185
|
if (keyValue === undefined || keyValue === null) return;
|
|
177
186
|
const rows = getHydrationRows(this.meta, this.relationName, keyValue);
|
|
178
187
|
if (!rows?.length) return;
|
|
179
|
-
this
|
|
180
|
-
this
|
|
188
|
+
this.items = rows.map(row => this.createEntity(row));
|
|
189
|
+
this.loaded = true;
|
|
181
190
|
}
|
|
182
191
|
|
|
183
192
|
/**
|
|
@@ -185,6 +194,6 @@ export class DefaultHasManyCollection<TChild> implements HasManyCollection<TChil
|
|
|
185
194
|
* @returns Array of child entities
|
|
186
195
|
*/
|
|
187
196
|
toJSON(): TChild[] {
|
|
188
|
-
return this
|
|
197
|
+
return this.items;
|
|
189
198
|
}
|
|
190
199
|
}
|
|
@@ -14,7 +14,19 @@ const hideInternal = (obj: object, keys: string[]): void => {
|
|
|
14
14
|
Object.defineProperty(obj, key, {
|
|
15
15
|
value: obj[key],
|
|
16
16
|
writable: false,
|
|
17
|
-
configurable:
|
|
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,
|
|
18
30
|
enumerable: false
|
|
19
31
|
});
|
|
20
32
|
}
|
|
@@ -27,8 +39,8 @@ const hideInternal = (obj: object, keys: string[]): void => {
|
|
|
27
39
|
* @template TChild The type of the child entity.
|
|
28
40
|
*/
|
|
29
41
|
export class DefaultHasOneReference<TChild extends object> implements HasOneReferenceApi<TChild> {
|
|
30
|
-
|
|
31
|
-
|
|
42
|
+
private loaded = false;
|
|
43
|
+
private current: TChild | null = null;
|
|
32
44
|
|
|
33
45
|
/**
|
|
34
46
|
* @param ctx The entity context for tracking changes.
|
|
@@ -63,25 +75,26 @@ export class DefaultHasOneReference<TChild extends object> implements HasOneRefe
|
|
|
63
75
|
'createEntity',
|
|
64
76
|
'localKey'
|
|
65
77
|
]);
|
|
78
|
+
hideWritable(this, ['loaded', 'current']);
|
|
66
79
|
this.populateFromHydrationCache();
|
|
67
80
|
}
|
|
68
81
|
|
|
69
82
|
async load(): Promise<TChild | null> {
|
|
70
|
-
if (this
|
|
83
|
+
if (this.loaded) return this.current;
|
|
71
84
|
const map = await this.loader();
|
|
72
85
|
const keyValue = (this.root as Record<string, unknown>)[this.localKey];
|
|
73
86
|
if (keyValue === undefined || keyValue === null) {
|
|
74
|
-
this
|
|
75
|
-
return this
|
|
87
|
+
this.loaded = true;
|
|
88
|
+
return this.current;
|
|
76
89
|
}
|
|
77
90
|
const row = map.get(toKey(keyValue));
|
|
78
|
-
this
|
|
79
|
-
this
|
|
80
|
-
return this
|
|
91
|
+
this.current = row ? this.createEntity(row) : null;
|
|
92
|
+
this.loaded = true;
|
|
93
|
+
return this.current;
|
|
81
94
|
}
|
|
82
95
|
|
|
83
96
|
get(): TChild | null {
|
|
84
|
-
return this
|
|
97
|
+
return this.current;
|
|
85
98
|
}
|
|
86
99
|
|
|
87
100
|
set(data: Partial<TChild> | TChild | null): TChild | null {
|
|
@@ -90,20 +103,20 @@ export class DefaultHasOneReference<TChild extends object> implements HasOneRefe
|
|
|
90
103
|
}
|
|
91
104
|
|
|
92
105
|
const entity = hasEntityMeta(data) ? (data as TChild) : this.createEntity(data as Row);
|
|
93
|
-
if (this
|
|
106
|
+
if (this.current && this.current !== entity) {
|
|
94
107
|
this.ctx.registerRelationChange(
|
|
95
108
|
this.root,
|
|
96
109
|
this.relationKey,
|
|
97
110
|
this.rootTable,
|
|
98
111
|
this.relationName,
|
|
99
112
|
this.relation,
|
|
100
|
-
{ kind: 'remove', entity: this
|
|
113
|
+
{ kind: 'remove', entity: this.current }
|
|
101
114
|
);
|
|
102
115
|
}
|
|
103
116
|
|
|
104
117
|
this.assignForeignKey(entity);
|
|
105
|
-
this
|
|
106
|
-
this
|
|
118
|
+
this.current = entity;
|
|
119
|
+
this.loaded = true;
|
|
107
120
|
|
|
108
121
|
this.ctx.registerRelationChange(
|
|
109
122
|
this.root,
|
|
@@ -117,19 +130,15 @@ export class DefaultHasOneReference<TChild extends object> implements HasOneRefe
|
|
|
117
130
|
return entity;
|
|
118
131
|
}
|
|
119
132
|
|
|
120
|
-
isLoaded(): boolean {
|
|
121
|
-
return this.#loaded;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
133
|
toJSON(): TChild | null {
|
|
125
|
-
return this
|
|
134
|
+
return this.current;
|
|
126
135
|
}
|
|
127
136
|
|
|
128
137
|
private detachCurrent(): TChild | null {
|
|
129
|
-
const previous = this
|
|
138
|
+
const previous = this.current;
|
|
130
139
|
if (!previous) return null;
|
|
131
|
-
this
|
|
132
|
-
this
|
|
140
|
+
this.current = null;
|
|
141
|
+
this.loaded = true;
|
|
133
142
|
this.ctx.registerRelationChange(
|
|
134
143
|
this.root,
|
|
135
144
|
this.relationKey,
|
|
@@ -156,7 +165,7 @@ export class DefaultHasOneReference<TChild extends object> implements HasOneRefe
|
|
|
156
165
|
if (keyValue === undefined || keyValue === null) return;
|
|
157
166
|
const row = getHydrationRecord(this.meta, this.relationName, keyValue);
|
|
158
167
|
if (!row) return;
|
|
159
|
-
this
|
|
160
|
-
this
|
|
168
|
+
this.current = this.createEntity(row);
|
|
169
|
+
this.loaded = true;
|
|
161
170
|
}
|
|
162
171
|
}
|
|
@@ -15,7 +15,19 @@ const hideInternal = (obj: object, keys: string[]): void => {
|
|
|
15
15
|
Object.defineProperty(obj, key, {
|
|
16
16
|
value: obj[key],
|
|
17
17
|
writable: false,
|
|
18
|
-
configurable:
|
|
18
|
+
configurable: false,
|
|
19
|
+
enumerable: false
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const hideWritable = (obj: object, keys: string[]): void => {
|
|
25
|
+
for (const key of keys) {
|
|
26
|
+
const value = obj[key as keyof typeof obj];
|
|
27
|
+
Object.defineProperty(obj, key, {
|
|
28
|
+
value,
|
|
29
|
+
writable: true,
|
|
30
|
+
configurable: true,
|
|
19
31
|
enumerable: false
|
|
20
32
|
});
|
|
21
33
|
}
|
|
@@ -30,8 +42,8 @@ const hideInternal = (obj: object, keys: string[]): void => {
|
|
|
30
42
|
*/
|
|
31
43
|
export class DefaultManyToManyCollection<TTarget, TPivot extends object | undefined = undefined>
|
|
32
44
|
implements ManyToManyCollection<TTarget, TPivot> {
|
|
33
|
-
|
|
34
|
-
|
|
45
|
+
private loaded = false;
|
|
46
|
+
private items: TTarget[] = [];
|
|
35
47
|
|
|
36
48
|
/**
|
|
37
49
|
* @param ctx The entity context for tracking changes.
|
|
@@ -56,6 +68,7 @@ export class DefaultManyToManyCollection<TTarget, TPivot extends object | undefi
|
|
|
56
68
|
private readonly localKey: string
|
|
57
69
|
) {
|
|
58
70
|
hideInternal(this, ['ctx', 'meta', 'root', 'relationName', 'relation', 'rootTable', 'loader', 'createEntity', 'localKey']);
|
|
71
|
+
hideWritable(this, ['loaded', 'items']);
|
|
59
72
|
this.hydrateFromCache();
|
|
60
73
|
}
|
|
61
74
|
|
|
@@ -64,19 +77,19 @@ export class DefaultManyToManyCollection<TTarget, TPivot extends object | undefi
|
|
|
64
77
|
* @returns A promise that resolves to the array of target entities.
|
|
65
78
|
*/
|
|
66
79
|
async load(): Promise<TTarget[]> {
|
|
67
|
-
if (this
|
|
80
|
+
if (this.loaded) return this.items;
|
|
68
81
|
const map = await this.loader();
|
|
69
82
|
const key = toKey(this.root[this.localKey]);
|
|
70
83
|
const rows = map.get(key) ?? [];
|
|
71
|
-
this
|
|
84
|
+
this.items = rows.map(row => {
|
|
72
85
|
const entity = this.createEntity(row);
|
|
73
86
|
if ((row as { _pivot?: unknown })._pivot) {
|
|
74
87
|
(entity as { _pivot?: unknown })._pivot = (row as { _pivot?: unknown })._pivot;
|
|
75
88
|
}
|
|
76
89
|
return entity;
|
|
77
90
|
});
|
|
78
|
-
this
|
|
79
|
-
return this
|
|
91
|
+
this.loaded = true;
|
|
92
|
+
return this.items;
|
|
80
93
|
}
|
|
81
94
|
|
|
82
95
|
/**
|
|
@@ -84,21 +97,21 @@ export class DefaultManyToManyCollection<TTarget, TPivot extends object | undefi
|
|
|
84
97
|
* @returns Array of target entities.
|
|
85
98
|
*/
|
|
86
99
|
getItems(): TTarget[] {
|
|
87
|
-
return this
|
|
100
|
+
return this.items;
|
|
88
101
|
}
|
|
89
102
|
|
|
90
103
|
/**
|
|
91
104
|
* Array-compatible length for testing frameworks.
|
|
92
105
|
*/
|
|
93
106
|
get length(): number {
|
|
94
|
-
return this
|
|
107
|
+
return this.items.length;
|
|
95
108
|
}
|
|
96
109
|
|
|
97
110
|
/**
|
|
98
111
|
* Enables iteration over the collection like an array.
|
|
99
112
|
*/
|
|
100
113
|
[Symbol.iterator](): Iterator<TTarget> {
|
|
101
|
-
return this
|
|
114
|
+
return this.items[Symbol.iterator]();
|
|
102
115
|
}
|
|
103
116
|
|
|
104
117
|
/**
|
|
@@ -109,13 +122,13 @@ export class DefaultManyToManyCollection<TTarget, TPivot extends object | undefi
|
|
|
109
122
|
attach(target: TTarget | number | string): void {
|
|
110
123
|
const entity = this.ensureEntity(target);
|
|
111
124
|
const id = this.extractId(entity);
|
|
112
|
-
if (id != null && this
|
|
125
|
+
if (id != null && this.items.some(item => this.extractId(item) === id)) {
|
|
113
126
|
return;
|
|
114
127
|
}
|
|
115
|
-
if (id == null && this
|
|
128
|
+
if (id == null && this.items.includes(entity)) {
|
|
116
129
|
return;
|
|
117
130
|
}
|
|
118
|
-
this
|
|
131
|
+
this.items.push(entity);
|
|
119
132
|
this.ctx.registerRelationChange(
|
|
120
133
|
this.root,
|
|
121
134
|
this.relationKey,
|
|
@@ -138,10 +151,10 @@ export class DefaultManyToManyCollection<TTarget, TPivot extends object | undefi
|
|
|
138
151
|
|
|
139
152
|
if (id == null) return;
|
|
140
153
|
|
|
141
|
-
const existing = this
|
|
154
|
+
const existing = this.items.find(item => this.extractId(item) === id);
|
|
142
155
|
if (!existing) return;
|
|
143
156
|
|
|
144
|
-
this
|
|
157
|
+
this.items = this.items.filter(item => this.extractId(item) !== id);
|
|
145
158
|
this.ctx.registerRelationChange(
|
|
146
159
|
this.root,
|
|
147
160
|
this.relationKey,
|
|
@@ -160,7 +173,7 @@ export class DefaultManyToManyCollection<TTarget, TPivot extends object | undefi
|
|
|
160
173
|
async syncByIds(ids: (number | string)[]): Promise<void> {
|
|
161
174
|
await this.load();
|
|
162
175
|
const normalized = new Set(ids.map(id => toKey(id)));
|
|
163
|
-
const currentIds = new Set(this
|
|
176
|
+
const currentIds = new Set(this.items.map(item => toKey(this.extractId(item))));
|
|
164
177
|
|
|
165
178
|
for (const id of normalized) {
|
|
166
179
|
if (!currentIds.has(id)) {
|
|
@@ -168,7 +181,7 @@ export class DefaultManyToManyCollection<TTarget, TPivot extends object | undefi
|
|
|
168
181
|
}
|
|
169
182
|
}
|
|
170
183
|
|
|
171
|
-
for (const item of [...this
|
|
184
|
+
for (const item of [...this.items]) {
|
|
172
185
|
const itemId = toKey(this.extractId(item));
|
|
173
186
|
if (!normalized.has(itemId)) {
|
|
174
187
|
this.detach(item);
|
|
@@ -176,10 +189,6 @@ export class DefaultManyToManyCollection<TTarget, TPivot extends object | undefi
|
|
|
176
189
|
}
|
|
177
190
|
}
|
|
178
191
|
|
|
179
|
-
isLoaded(): boolean {
|
|
180
|
-
return this.#loaded;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
192
|
private ensureEntity(target: TTarget | number | string): TTarget {
|
|
184
193
|
if (typeof target === 'number' || typeof target === 'string') {
|
|
185
194
|
const stub: Record<string, unknown> = {
|
|
@@ -211,7 +220,7 @@ export class DefaultManyToManyCollection<TTarget, TPivot extends object | undefi
|
|
|
211
220
|
if (keyValue === undefined || keyValue === null) return;
|
|
212
221
|
const rows = getHydrationRows(this.meta, this.relationName, keyValue);
|
|
213
222
|
if (!rows?.length) return;
|
|
214
|
-
this
|
|
223
|
+
this.items = rows.map(row => {
|
|
215
224
|
const entity = this.createEntity(row);
|
|
216
225
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
217
226
|
if ((row as { _pivot?: unknown })._pivot) {
|
|
@@ -219,10 +228,10 @@ export class DefaultManyToManyCollection<TTarget, TPivot extends object | undefi
|
|
|
219
228
|
}
|
|
220
229
|
return entity;
|
|
221
230
|
});
|
|
222
|
-
this
|
|
231
|
+
this.loaded = true;
|
|
223
232
|
}
|
|
224
233
|
|
|
225
234
|
toJSON(): TTarget[] {
|
|
226
|
-
return this
|
|
235
|
+
return this.items;
|
|
227
236
|
}
|
|
228
237
|
}
|