Skip to content

Commit dd70a2b

Browse files
authored
fix(apply): fix apply with patches non-string keys (#151)
1 parent 25e3cb2 commit dd70a2b

File tree

3 files changed

+138
-5
lines changed

3 files changed

+138
-5
lines changed

src/apply.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,19 +57,34 @@ export function apply<
5757
for (let index = 0; index < path.length - 1; index += 1) {
5858
const parentType = getType(base);
5959
let key = path[index];
60-
if (typeof key !== 'string' && typeof key !== 'number') {
61-
key = String(key);
62-
}
60+
const keyForCheck =
61+
typeof key === 'symbol' ? undefined : String(key as any);
6362
if (
6463
((parentType === DraftType.Object ||
6564
parentType === DraftType.Array) &&
66-
(key === '__proto__' || key === 'constructor')) ||
67-
(typeof base === 'function' && key === 'prototype')
65+
keyForCheck !== undefined &&
66+
(keyForCheck === '__proto__' || keyForCheck === 'constructor')) ||
67+
(typeof base === 'function' &&
68+
keyForCheck !== undefined &&
69+
keyForCheck === 'prototype')
6870
) {
6971
throw new Error(
7072
`Patching reserved attributes like __proto__ and constructor is not allowed.`
7173
);
7274
}
75+
if (
76+
(parentType === DraftType.Object ||
77+
parentType === DraftType.Array ||
78+
typeof base === 'function') &&
79+
typeof key !== 'string' &&
80+
typeof key !== 'number' &&
81+
typeof key !== 'symbol'
82+
) {
83+
// keyForCheck cannot be undefined here, because:
84+
// - If key is a symbol, this conditional block will not be entered
85+
// - All other types will be converted to String(key)
86+
key = keyForCheck!;
87+
}
7388
// use `index` in Set draft
7489
base = get(parentType === DraftType.Set ? Array.from(base) : base, key);
7590
if (typeof base !== 'object') {

test/immer-non-support.test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -735,3 +735,69 @@ test('Unexpected undefined not assigned', () => {
735735
expect(Object.prototype.hasOwnProperty.call(foo, 'name')).toBe(true);
736736
}
737737
});
738+
739+
test('apply - map with object key', () => {
740+
{
741+
enablePatches();
742+
enableMapSet();
743+
const key = { id: 1 };
744+
const base = {
745+
map: new Map([[key, { value: 1 }]]),
746+
};
747+
const [next, patches, inverse] = produceWithPatches(base, (draft) => {
748+
draft.map.get(key)!.value = 2;
749+
});
750+
expect(() => applyPatches(base, patches)).toThrow();
751+
expect(() => applyPatches(next, inverse)).toThrow();
752+
}
753+
{
754+
const key = { id: 1 };
755+
const base = {
756+
map: new Map([[key, { value: 1 }]]),
757+
};
758+
const [next, patches, inverse] = create(
759+
base,
760+
(draft) => {
761+
draft.map.get(key)!.value = 2;
762+
},
763+
{ enablePatches: true }
764+
);
765+
expect(apply(base, patches)).toEqual(next);
766+
expect(apply(next, inverse)).toEqual(base);
767+
}
768+
});
769+
770+
test('apply - symbol key on object', () => {
771+
{
772+
enablePatches();
773+
const sym = Symbol('key');
774+
const base = {
775+
obj: {
776+
[sym]: { value: 1 },
777+
},
778+
};
779+
const [next, patches, inverse] = produceWithPatches(base, (draft) => {
780+
draft.obj[sym].value = 2;
781+
});
782+
expect(() => applyPatches(base, patches)).toThrow();
783+
expect(() => applyPatches(next, inverse)).toThrow();
784+
}
785+
{
786+
const sym = Symbol('key');
787+
const base = {
788+
obj: {
789+
[sym]: { value: 1 },
790+
},
791+
};
792+
const [next, patches, inverse] = create(
793+
base,
794+
(draft) => {
795+
draft.obj[sym].value = 2;
796+
},
797+
{ enablePatches: true }
798+
);
799+
expect(apply(base, patches)).toEqual(next);
800+
expect(apply(next, inverse)).toEqual(base);
801+
}
802+
});
803+

test/index.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4086,6 +4086,58 @@ test('#59 - Failure to apply inverse patchset(Map)', () => {
40864086
expect(reverted2).toEqual(newState);
40874087
});
40884088

4089+
test('apply - map with object key', () => {
4090+
const key = { id: 1 };
4091+
const base = {
4092+
map: new Map([[key, { value: 1 }]]),
4093+
};
4094+
const [next, patches, inverse] = create(
4095+
base,
4096+
(draft) => {
4097+
draft.map.get(key)!.value = 2;
4098+
},
4099+
{ enablePatches: true }
4100+
);
4101+
expect(apply(base, patches)).toEqual(next);
4102+
expect(apply(next, inverse)).toEqual(base);
4103+
});
4104+
4105+
test('apply - symbol key on object', () => {
4106+
const sym = Symbol('key');
4107+
const base = {
4108+
obj: {
4109+
[sym]: { value: 1 },
4110+
},
4111+
};
4112+
const [next, patches, inverse] = create(
4113+
base,
4114+
(draft) => {
4115+
draft.obj[sym].value = 2;
4116+
},
4117+
{ enablePatches: true }
4118+
);
4119+
expect(apply(base, patches)).toEqual(next);
4120+
expect(apply(next, inverse)).toEqual(base);
4121+
});
4122+
4123+
test('apply - coerces non-primitive intermediate key for object', () => {
4124+
const objectKey: any = { toString: () => 'customKey' };
4125+
const base = {
4126+
foo: {
4127+
customKey: { value: 1 },
4128+
},
4129+
};
4130+
const patches: any = [
4131+
{ op: 'replace', path: ['foo', objectKey, 'value'], value: 2 },
4132+
];
4133+
const applied = apply(base, patches);
4134+
expect(applied).toEqual({
4135+
foo: {
4136+
customKey: { value: 2 },
4137+
},
4138+
});
4139+
});
4140+
40894141
test('#61 - type issue: current of Draft<T> type should return T type', () => {
40904142
function test<T extends { x: { y: ReadonlySet<string> } }>(base: T): T {
40914143
const [draft, f] = create(base);

0 commit comments

Comments
 (0)