Skip to content

Commit c75f07c

Browse files
committed
fix(cdk): preserve prototype chains in array helpers
1 parent 876bb64 commit c75f07c

File tree

6 files changed

+215
-23
lines changed

6 files changed

+215
-23
lines changed

libs/cdk/transformations/spec/array/toDictionary.spec.ts

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ describe('toDictionary', () => {
8181
it('should create dictionary by symbol', () => {
8282
const dictionaryResult = toDictionary(
8383
Object.values(dictionaryBySymbol),
84-
genus
84+
genus,
8585
);
8686

8787
expect(dictionaryResult).toEqual(dictionaryBySymbol);
@@ -142,4 +142,73 @@ describe('toDictionary', () => {
142142
expect(toDictionary(arr2, '')).toEqual(undefined);
143143
});
144144
});
145+
146+
describe('prototype preservation', () => {
147+
class TestClass {
148+
constructor(
149+
public id: number,
150+
public value: string,
151+
) {}
152+
153+
getDescription(): string {
154+
return `${this.id}: ${this.value}`;
155+
}
156+
}
157+
158+
it('should preserve prototype chain when converting class instances to dictionary', () => {
159+
const instances = [new TestClass(1, 'first'), new TestClass(2, 'second')];
160+
161+
const result = toDictionary(instances, 'id');
162+
163+
expect(result['1']).toBeInstanceOf(TestClass);
164+
expect(result['2']).toBeInstanceOf(TestClass);
165+
166+
expect(result['1'].getDescription()).toBe('1: first');
167+
expect(result['2'].getDescription()).toBe('2: second');
168+
});
169+
170+
it('should preserve prototype chain with string keys', () => {
171+
const instances = [new TestClass(1, 'cat'), new TestClass(2, 'dog')];
172+
173+
const result = toDictionary(instances, 'value');
174+
175+
expect(result['cat']).toBeInstanceOf(TestClass);
176+
expect(result['dog']).toBeInstanceOf(TestClass);
177+
178+
expect(result['cat'].getDescription()).toBe('1: cat');
179+
expect(result['dog'].getDescription()).toBe('2: dog');
180+
});
181+
182+
it('should preserve prototype chain with symbol keys', () => {
183+
const testSymbol = Symbol('test');
184+
185+
class SymbolClass {
186+
[testSymbol]: string;
187+
188+
constructor(
189+
public id: number,
190+
symbolValue: string,
191+
) {
192+
this[testSymbol] = symbolValue;
193+
}
194+
195+
getSymbolValue(): string {
196+
return this[testSymbol];
197+
}
198+
}
199+
200+
const instances = [
201+
new SymbolClass(1, 'first'),
202+
new SymbolClass(2, 'second'),
203+
];
204+
205+
const result = toDictionary(instances, testSymbol);
206+
207+
expect(result['first']).toBeInstanceOf(SymbolClass);
208+
expect(result['second']).toBeInstanceOf(SymbolClass);
209+
210+
expect(result['first'].getSymbolValue()).toBe('first');
211+
expect(result['second'].getSymbolValue()).toBe('second');
212+
});
213+
});
145214
});

libs/cdk/transformations/spec/array/update.spec.ts

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ describe('update', () => {
6868
const result = update(
6969
originalCreatures,
7070
creaturesForUpdate[0],
71-
(o, n) => o.id === n.id
71+
(o, n) => o.id === n.id,
7272
);
7373

7474
originalCreatures[0] = null as any;
@@ -89,25 +89,25 @@ describe('update', () => {
8989
describe('functionality', () => {
9090
it('should update value if matching by compareFn', () => {
9191
expect(
92-
update(creatures, creaturesForUpdate, (a, b) => a.id === b.id)
92+
update(creatures, creaturesForUpdate, (a, b) => a.id === b.id),
9393
).toEqual(creaturesAfterMultipleItemsUpdate);
9494
});
9595

9696
it('should update value if matching by key', () => {
9797
expect(update(creatures, creaturesForUpdate, 'id')).toEqual(
98-
creaturesAfterMultipleItemsUpdate
98+
creaturesAfterMultipleItemsUpdate,
9999
);
100100
});
101101

102102
it('should update value if matching by array of keys', () => {
103103
expect(update(creatures, creaturesForUpdate, ['id'])).toEqual(
104-
creaturesAfterMultipleItemsUpdate
104+
creaturesAfterMultipleItemsUpdate,
105105
);
106106
});
107107

108108
it('should update partials', () => {
109109
expect(update(creatures, { id: 1, type: 'lion' }, 'id')).toEqual(
110-
creaturesAfterSingleItemUpdate
110+
creaturesAfterSingleItemUpdate,
111111
);
112112
});
113113
});
@@ -179,4 +179,55 @@ describe('update', () => {
179179
});
180180
});
181181
});
182+
183+
describe('prototype preservation', () => {
184+
class TestClass {
185+
constructor(
186+
public id: number,
187+
public value: string,
188+
) {}
189+
190+
getDescription(): string {
191+
return `${this.id}: ${this.value}`;
192+
}
193+
}
194+
195+
it('should preserve prototype chain when updating class instances', () => {
196+
const instances = [new TestClass(1, 'first'), new TestClass(2, 'second')];
197+
198+
const result = update(instances, { id: 1, value: 'updated' }, 'id');
199+
200+
expect(result[0]).toBeInstanceOf(TestClass);
201+
expect(result[0].getDescription()).toBe('1: updated');
202+
203+
expect(result[1]).toBeInstanceOf(TestClass);
204+
expect(result[1].getDescription()).toBe('2: second');
205+
});
206+
207+
it('should preserve prototype chain when updating with comparison function', () => {
208+
const instances = [new TestClass(1, 'first'), new TestClass(2, 'second')];
209+
210+
const result = update(
211+
instances,
212+
new TestClass(1, 'updated'),
213+
(a, b) => a.id === b.id,
214+
);
215+
216+
expect(result[0]).toBeInstanceOf(TestClass);
217+
expect(result[1]).toBeInstanceOf(TestClass);
218+
expect(result[0].getDescription()).toBe('1: updated');
219+
expect(result[1].getDescription()).toBe('2: second');
220+
});
221+
222+
it('should preserve prototype chain when no match is found', () => {
223+
const instances = [new TestClass(1, 'first'), new TestClass(2, 'second')];
224+
225+
const result = update(instances, { id: 99, value: 'nonexistent' }, 'id');
226+
227+
expect(result[0]).toBeInstanceOf(TestClass);
228+
expect(result[1]).toBeInstanceOf(TestClass);
229+
expect(result[0].getDescription()).toBe('1: first');
230+
expect(result[1].getDescription()).toBe('2: second');
231+
});
232+
});
182233
});

libs/cdk/transformations/spec/array/upsert.spec.ts

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ describe('upsert', () => {
6565
upsert(
6666
originalCreatures,
6767
creaturesForUpdate[0],
68-
(o, n) => o.id === n.id
68+
(o, n) => o.id === n.id,
6969
);
7070

7171
expect(originalCreatures).toEqual(creatures);
@@ -76,7 +76,7 @@ describe('upsert', () => {
7676
const result = upsert(
7777
originalCreatures,
7878
creaturesForUpdate[0],
79-
(o, n) => o.id === n.id
79+
(o, n) => o.id === n.id,
8080
);
8181
const result2 = upsert(null as any, originalCreatures);
8282

@@ -98,25 +98,25 @@ describe('upsert', () => {
9898
describe('functionality', () => {
9999
it('should update value if matching by compareFn', () => {
100100
expect(
101-
upsert(creatures, creaturesForUpdate, (a, b) => a.id === b.id)
101+
upsert(creatures, creaturesForUpdate, (a, b) => a.id === b.id),
102102
).toEqual(creaturesAfterMultipleItemsUpdate);
103103
});
104104

105105
it('should update value if matching by key', () => {
106106
expect(upsert(creatures, creaturesForUpdate, 'id')).toEqual(
107-
creaturesAfterMultipleItemsUpdate
107+
creaturesAfterMultipleItemsUpdate,
108108
);
109109
});
110110

111111
it('should update value if matching by array of keys', () => {
112112
expect(upsert(creatures, creaturesForUpdate, ['id'])).toEqual(
113-
creaturesAfterMultipleItemsUpdate
113+
creaturesAfterMultipleItemsUpdate,
114114
);
115115
});
116116

117117
it('should update partials', () => {
118118
expect(upsert(creatures, { id: 1, type: 'lion' }, 'id')).toEqual(
119-
creaturesAfterSingleItemUpdate
119+
creaturesAfterSingleItemUpdate,
120120
);
121121
});
122122
});
@@ -318,4 +318,56 @@ describe('upsert', () => {
318318
});
319319
});
320320
});
321+
322+
describe('prototype preservation', () => {
323+
class TestClass {
324+
constructor(
325+
public id: number,
326+
public value: string,
327+
) {}
328+
329+
getDescription(): string {
330+
return `${this.id}: ${this.value}`;
331+
}
332+
}
333+
334+
it('should preserve prototype chain when updating class instances', () => {
335+
const instances = [new TestClass(1, 'first'), new TestClass(2, 'second')];
336+
337+
const result = upsert(instances, { id: 1, value: 'updated' }, 'id');
338+
339+
expect(result[0]).toBeInstanceOf(TestClass);
340+
expect(result[0].getDescription()).toBe('1: updated');
341+
342+
expect(result[1]).toBeInstanceOf(TestClass);
343+
expect(result[1].getDescription()).toBe('2: second');
344+
});
345+
346+
it('should preserve prototype chain when inserting class instances', () => {
347+
const instances = [new TestClass(1, 'first'), new TestClass(2, 'second')];
348+
349+
const result = upsert(instances, new TestClass(3, 'third'), 'id');
350+
351+
expect(result[0]).toBeInstanceOf(TestClass);
352+
expect(result[1]).toBeInstanceOf(TestClass);
353+
354+
expect(result[2]).toBeInstanceOf(TestClass);
355+
expect(result[2].getDescription()).toBe('3: third');
356+
});
357+
358+
it('should preserve prototype chain when upserting with comparison function', () => {
359+
const instances = [new TestClass(1, 'first'), new TestClass(2, 'second')];
360+
361+
const result = upsert(
362+
instances,
363+
new TestClass(1, 'updated'),
364+
(a, b) => a.id === b.id,
365+
);
366+
367+
expect(result[0]).toBeInstanceOf(TestClass);
368+
expect(result[1]).toBeInstanceOf(TestClass);
369+
expect(result[0].getDescription()).toBe('1: updated');
370+
expect(result[1].getDescription()).toBe('2: second');
371+
});
372+
});
321373
});

libs/cdk/transformations/src/lib/array/toDictionary.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { isDefined, isKeyOf, OnlyKeysOfSpecificType } from '../_internals/guards';
1+
import {
2+
isDefined,
3+
isKeyOf,
4+
OnlyKeysOfSpecificType,
5+
} from '../_internals/guards';
26

37
/**
48
* @description
@@ -53,7 +57,7 @@ export function toDictionary<T extends object>(
5357
key:
5458
| OnlyKeysOfSpecificType<T, number>
5559
| OnlyKeysOfSpecificType<T, string>
56-
| OnlyKeysOfSpecificType<T, symbol>
60+
| OnlyKeysOfSpecificType<T, symbol>,
5761
): { [key: string]: T } {
5862
if (!isDefined(source)) {
5963
return source;
@@ -73,7 +77,10 @@ export function toDictionary<T extends object>(
7377
let i = 0;
7478

7579
for (i; i < length; i++) {
76-
dictionary[`${source[i][key]}`] = Object.assign({}, source[i]);
80+
dictionary[`${source[i][key]}`] = Object.assign(
81+
Object.create(Object.getPrototypeOf(source[i])),
82+
source[i],
83+
);
7784
}
7885

7986
return dictionary;

libs/cdk/transformations/src/lib/array/update.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ import { ComparableData } from '../interfaces/comparable-data-type';
7676
export function update<T extends object>(
7777
source: T[],
7878
updates: Partial<T>[] | Partial<T>,
79-
compare?: ComparableData<T>
79+
compare?: ComparableData<T>,
8080
): T[] {
8181
const updatesDefined = updates != null;
8282
const updatesAsArray = updatesDefined
@@ -101,10 +101,18 @@ export function update<T extends object>(
101101
const x: T[] = [];
102102
for (const existingItem of source) {
103103
const match = customFind(updatesAsArray, (item) =>
104-
valuesComparer(item as T, existingItem, compare)
104+
valuesComparer(item as T, existingItem, compare),
105105
);
106106

107-
x.push(match ? { ...existingItem, ...match } : existingItem);
107+
x.push(
108+
match
109+
? Object.assign(
110+
Object.create(Object.getPrototypeOf(existingItem)),
111+
existingItem,
112+
match,
113+
)
114+
: existingItem,
115+
);
108116
}
109117

110118
return x;

libs/cdk/transformations/src/lib/array/upsert.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ import { ComparableData } from '../interfaces/comparable-data-type';
9494
export function upsert<T>(
9595
source: T[],
9696
update: Partial<T>[] | Partial<T>,
97-
compare?: ComparableData<T>
97+
compare?: ComparableData<T>,
9898
): T[] {
9999
// check inputs for validity
100100
const updatesAsArray =
@@ -120,16 +120,17 @@ export function upsert<T>(
120120
// process updates/inserts
121121
for (const item of updatesAsArray) {
122122
const match = source.findIndex((sourceItem) =>
123-
valuesComparer(item as T, sourceItem, compare)
123+
valuesComparer(item as T, sourceItem, compare),
124124
);
125125
// if item already exists, save it as update
126126
if (match !== -1) {
127127
updates[match] = item;
128128
} else {
129129
// otherwise consider this as insert
130130
if (isObjectGuard(item)) {
131-
// create a shallow copy if item is an object
132-
inserts.push({ ...(item as T) });
131+
inserts.push(
132+
Object.assign(Object.create(Object.getPrototypeOf(item)), item) as T,
133+
);
133134
} else {
134135
// otherwise just push it
135136
inserts.push(item);
@@ -142,7 +143,11 @@ export function upsert<T>(
142143
// process the updated
143144
if (updatedItem !== null && updatedItem !== undefined) {
144145
if (isObjectGuard(item)) {
145-
return { ...item, ...updatedItem } as T;
146+
return Object.assign(
147+
Object.create(Object.getPrototypeOf(item)),
148+
item,
149+
updatedItem,
150+
) as T;
146151
} else {
147152
return updatedItem as T;
148153
}

0 commit comments

Comments
 (0)