Skip to content

Commit 9ab46bd

Browse files
committed
refactor: use custom set
1 parent 57af369 commit 9ab46bd

File tree

4 files changed

+176
-1
lines changed

4 files changed

+176
-1
lines changed

packages/svelte-stores/src/lib/formStore.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import {
1010
type Patch,
1111
} from 'immer';
1212
import type { Schema } from 'zod';
13-
import { set } from 'lodash-es';
13+
import { set } from '@layerstack/utils';
1414

1515
// Needed for finishDraft() patches/inverseChanges - https://immerjs.github.io/immer/patches
1616
enablePatches();

packages/utils/src/lib/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export * from './json.js';
3434
export * from './logger.js';
3535
export { round, clamp, randomInteger } from './number.js';
3636
export { get } from './get.js';
37+
export { set } from './set.js';
3738
export { isEmptyObject, isLiteralObject, omit, pick } from './object.js';
3839
export { mergeWith, merge, defaultsDeep } from './mergeWith.js';
3940
export * from './promise.js';

packages/utils/src/lib/set.test.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { describe, it, expect } from 'vitest';
2+
3+
import { set } from './set.js';
4+
5+
describe('set', () => {
6+
it('sets value at string path', () => {
7+
const obj = { a: { b: { c: 1 } } };
8+
const result = set(obj, 'a.b.c', 3);
9+
expect(result).toBe(true);
10+
expect(obj.a.b.c).toBe(3);
11+
});
12+
13+
it('sets value at array path', () => {
14+
const obj = { a: { b: { c: 1 } } };
15+
const result = set(obj, ['a', 'b', 'c'], 3);
16+
expect(result).toBe(true);
17+
expect(obj.a.b.c).toBe(3);
18+
});
19+
20+
it('sets value at symbol path', () => {
21+
const sym = Symbol('key');
22+
const obj: Record<symbol, any> = {};
23+
const result = set(obj, sym, 'value');
24+
expect(result).toBe(true);
25+
expect(obj[sym]).toBe('value');
26+
});
27+
28+
it('creates nested objects when path does not exist', () => {
29+
const obj: Record<string, any> = {};
30+
set(obj, 'a.b.c', 'new');
31+
expect(obj).toEqual({ a: { b: { c: 'new' } } });
32+
});
33+
34+
it('overwrites existing value', () => {
35+
const obj = { a: 'old' };
36+
set(obj, 'a', 'new');
37+
expect(obj.a).toBe('new');
38+
});
39+
40+
it('handles array indices in string path', () => {
41+
const obj: Record<string, any> = { a: [{}, {}] };
42+
set(obj, 'a.0.b', 1);
43+
set(obj, 'a.1.b', 2);
44+
expect(obj.a[0].b).toBe(1);
45+
expect(obj.a[1].b).toBe(2);
46+
});
47+
48+
it('handles array indices in array path', () => {
49+
const obj: Record<string, any> = { a: [{}, {}] };
50+
set(obj, ['a', 0, 'b'], 1);
51+
set(obj, ['a', 1, 'b'], 2);
52+
expect(obj.a[0].b).toBe(1);
53+
expect(obj.a[1].b).toBe(2);
54+
});
55+
56+
it('returns false for empty path', () => {
57+
const obj = { a: 1 };
58+
expect(set(obj, [], 'value')).toBe(false);
59+
});
60+
61+
it('throws error for __proto__ path', () => {
62+
const obj = {};
63+
expect(() => set(obj, '__proto__', {})).toThrow('setting of prototype values not supported');
64+
expect(() => set(obj, ['__proto__'], {})).toThrow('setting of prototype values not supported');
65+
});
66+
67+
it('throws error for constructor path', () => {
68+
const obj = {};
69+
expect(() => set(obj, 'constructor', {})).toThrow('setting of prototype values not supported');
70+
});
71+
72+
it('throws error for prototype path', () => {
73+
const obj = {};
74+
expect(() => set(obj, 'prototype', {})).toThrow('setting of prototype values not supported');
75+
});
76+
77+
it('throws error for __proto__ in nested path', () => {
78+
const obj: Record<string, any> = { a: {} };
79+
expect(() => set(obj, 'a.__proto__.b', 'value')).toThrow(
80+
'setting of prototype values not supported'
81+
);
82+
});
83+
84+
it('returns false when intermediate path is not an object', () => {
85+
const obj = { a: 'string' };
86+
const result = set(obj, 'a.b.c', 'value');
87+
expect(result).toBe(false);
88+
});
89+
90+
it('returns false when intermediate path is null', () => {
91+
const obj: Record<string, any> = { a: null };
92+
const result = set(obj, 'a.b', 'value');
93+
expect(result).toBe(false);
94+
});
95+
96+
it('sets value with various types', () => {
97+
const obj: Record<string, any> = {};
98+
set(obj, 'string', 'hello');
99+
set(obj, 'number', 42);
100+
set(obj, 'boolean', true);
101+
set(obj, 'array', [1, 2, 3]);
102+
set(obj, 'object', { nested: true });
103+
set(obj, 'null', null);
104+
105+
expect(obj.string).toBe('hello');
106+
expect(obj.number).toBe(42);
107+
expect(obj.boolean).toBe(true);
108+
expect(obj.array).toEqual([1, 2, 3]);
109+
expect(obj.object).toEqual({ nested: true });
110+
expect(obj.null).toBe(null);
111+
});
112+
113+
it('handles deeply nested paths', () => {
114+
const obj: Record<string, any> = {};
115+
set(obj, 'a.b.c.d.e.f', 'deep');
116+
expect(obj.a.b.c.d.e.f).toBe('deep');
117+
});
118+
119+
it('mutates the original object', () => {
120+
const obj = { a: 1 };
121+
set(obj, 'a', 2);
122+
expect(obj.a).toBe(2);
123+
});
124+
});

packages/utils/src/lib/set.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**
2+
* See: https://github.com/angus-c/just/blob/d8c5dd18941062d8db7e9310ecc8f53fd607df54/packages/object-safe-set/index.mjs#L22C2-L61C2
3+
*/
4+
5+
function prototypeCheck(prop: string | number | symbol): void {
6+
// coercion is intentional to catch prop values like `['__proto__']`
7+
if (prop == '__proto__' || prop == 'constructor' || prop == 'prototype') {
8+
throw new Error('setting of prototype values not supported');
9+
}
10+
}
11+
12+
export function set(
13+
obj: Record<string | number | symbol, any>,
14+
propsArg: string | symbol | (string | number | symbol)[],
15+
value: any
16+
): boolean {
17+
let props: (string | number | symbol)[];
18+
19+
if (Array.isArray(propsArg)) {
20+
props = propsArg.slice(0);
21+
} else if (typeof propsArg === 'string') {
22+
props = propsArg.split('.');
23+
} else if (typeof propsArg === 'symbol') {
24+
props = [propsArg];
25+
} else {
26+
throw new Error('props arg must be an array, a string or a symbol');
27+
}
28+
29+
const lastProp = props.pop();
30+
if (lastProp === undefined) {
31+
return false;
32+
}
33+
34+
prototypeCheck(lastProp);
35+
36+
let thisProp: string | number | symbol | undefined;
37+
while ((thisProp = props.shift()) !== undefined) {
38+
prototypeCheck(thisProp);
39+
if (typeof obj[thisProp] === 'undefined') {
40+
obj[thisProp] = {};
41+
}
42+
obj = obj[thisProp];
43+
if (!obj || typeof obj !== 'object') {
44+
return false;
45+
}
46+
}
47+
48+
obj[lastProp] = value;
49+
return true;
50+
}

0 commit comments

Comments
 (0)