Skip to content

Commit fd576ba

Browse files
authored
refactor: dot prop delete mutable (#14)
1 parent 845aec6 commit fd576ba

10 files changed

Lines changed: 299 additions & 175 deletions

File tree

.changeset/busy-paws-worry.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@webiny/stdlib": patch
3+
---
4+
5+
refactor: add array index support to immutableDelete and mutableDelete

.yarnrc.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ nodeLinker: node-modules
22
yarnPath: .yarn/releases/yarn-4.14.1.cjs
33
approvedGitRepositories: []
44
enableScripts: false
5-
npmMinimalAgeGate: 24h
5+
npmMinimalAgeGate: 48h

__tests__/dotProp.test.ts

Lines changed: 0 additions & 167 deletions
This file was deleted.
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { describe, expect, it } from "vitest";
2+
import { immutableDelete } from "../../src/common/utils/dotProp.js";
3+
4+
describe("immutableDelete", () => {
5+
it("returns a new object reference", () => {
6+
const obj = { a: 1 };
7+
const result = immutableDelete(obj, "a");
8+
expect(result).not.toBe(obj);
9+
});
10+
11+
it("removes a top-level property", () => {
12+
expect(immutableDelete({ a: 1, b: 2 }, "a")).toEqual({ b: 2 });
13+
});
14+
15+
it("removes a nested property by dot path", () => {
16+
expect(immutableDelete({ a: { b: 1, c: 2 } }, "a.b")).toEqual({ a: { c: 2 } });
17+
});
18+
19+
it("does not mutate the original object", () => {
20+
const obj = { a: 1, b: 2 };
21+
immutableDelete(obj, "a");
22+
expect(obj).toEqual({ a: 1, b: 2 });
23+
});
24+
25+
it("is a no-op when the path does not exist", () => {
26+
const obj = { a: 1 };
27+
expect(immutableDelete(obj, "b")).toEqual({ a: 1 });
28+
});
29+
30+
it("splices an element from a cloned array by numeric index", () => {
31+
const arr = ["a", "b", "c"];
32+
const result = immutableDelete(arr, 1);
33+
expect(result).toEqual(["a", "c"]);
34+
});
35+
36+
it("splices the first element from a cloned array", () => {
37+
const arr = [10, 20, 30];
38+
expect(immutableDelete(arr, 0)).toEqual([20, 30]);
39+
});
40+
41+
it("splices the last element from a cloned array", () => {
42+
const arr = [10, 20, 30];
43+
expect(immutableDelete(arr, 2)).toEqual([10, 20]);
44+
});
45+
46+
it("does not mutate the original array", () => {
47+
const arr = ["a", "b", "c"];
48+
immutableDelete(arr, 1);
49+
expect(arr).toEqual(["a", "b", "c"]);
50+
});
51+
52+
it("returns a new array reference", () => {
53+
const arr = [1, 2, 3];
54+
const result = immutableDelete(arr, 0);
55+
expect(result).not.toBe(arr);
56+
});
57+
58+
it("returns a clone when the index is out of bounds", () => {
59+
const arr = ["x"];
60+
const result = immutableDelete(arr, 5);
61+
expect(result).toEqual(["x"]);
62+
expect(result).not.toBe(arr);
63+
});
64+
65+
it("returns a clone for a negative index", () => {
66+
const arr = [1, 2, 3];
67+
const result = immutableDelete(arr, -1);
68+
expect(result).toEqual([1, 2, 3]);
69+
expect(result).not.toBe(arr);
70+
});
71+
});
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { describe, expect, it } from "vitest";
2+
import { immutableGet } from "../../src/common/utils/dotProp.js";
3+
4+
describe("immutableGet", () => {
5+
it("gets a top-level property", () => {
6+
expect(immutableGet({ a: 1 }, "a")).toBe(1);
7+
});
8+
9+
it("gets a nested property by dot path", () => {
10+
expect(immutableGet({ a: { b: { c: 42 } } }, "a.b.c")).toBe(42);
11+
});
12+
13+
it("returns undefined when path does not exist and no default is given", () => {
14+
expect(immutableGet({ a: 1 }, "b")).toBeUndefined();
15+
});
16+
17+
it("returns the default value when path does not exist", () => {
18+
expect(immutableGet({ a: 1 }, "b", "fallback")).toBe("fallback");
19+
});
20+
21+
it("returns the default value when object is null", () => {
22+
expect(immutableGet(null, "a.b", 99)).toBe(99);
23+
});
24+
25+
it("returns the default value when object is undefined", () => {
26+
expect(immutableGet(undefined, "a.b", "x")).toBe("x");
27+
});
28+
29+
it("does not mutate the source object", () => {
30+
const obj = { a: { b: 1 } };
31+
const newObj = immutableGet<any>(obj, "a");
32+
newObj.b = 2;
33+
expect(newObj).toEqual({ b: 2 });
34+
expect(obj).toEqual({ a: { b: 1 } });
35+
});
36+
});
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { describe, expect, it } from "vitest";
2+
import { immutableSet } from "../../src/common/utils/dotProp.js";
3+
4+
describe("immutableSet", () => {
5+
it("returns a new object reference", () => {
6+
const obj = { a: 1 };
7+
const result = immutableSet(obj, "a", 2);
8+
expect(result).not.toBe(obj);
9+
});
10+
11+
it("sets a top-level property", () => {
12+
expect(immutableSet({ a: 1 }, "a", 2)).toEqual({ a: 2 });
13+
});
14+
15+
it("sets a nested property by dot path", () => {
16+
expect(immutableSet({ a: { b: 1 } }, "a.b", 99)).toEqual({ a: { b: 99 } });
17+
});
18+
19+
it("creates intermediate objects for missing paths", () => {
20+
expect(immutableSet({} as Record<string, any>, "a.b.c", 7)).toEqual({ a: { b: { c: 7 } } });
21+
});
22+
23+
it("does not mutate the original object", () => {
24+
const obj = { a: { b: 1 } };
25+
immutableSet(obj, "a.b", 99);
26+
expect(obj.a.b).toBe(1);
27+
});
28+
29+
it("deep-clones nested objects so the result shares no references with the original", () => {
30+
const obj = { a: { b: 1 } };
31+
const result = immutableSet(obj, "a.b", 2);
32+
expect(result.a).not.toBe(obj.a);
33+
});
34+
35+
it("accepts a functional updater that receives the current value", () => {
36+
const result = immutableSet({ count: 5 }, "count", (n: number) => n + 1);
37+
expect(result).toEqual({ count: 6 });
38+
});
39+
40+
it("functional updater receives undefined for a missing path", () => {
41+
let received: unknown = "sentinel";
42+
immutableSet({} as Record<string, any>, "missing", (v: unknown) => {
43+
received = v;
44+
return 0;
45+
});
46+
expect(received).toBeUndefined();
47+
});
48+
});

0 commit comments

Comments
 (0)