Skip to content

Commit 1e000b0

Browse files
committed
Improve implementation of deepFindPathToProperty function
Adopt non-recursive path calculation. Fixes #58
1 parent 55adbeb commit 1e000b0

File tree

2 files changed

+158
-24
lines changed

2 files changed

+158
-24
lines changed

src/object-helpers.ts

Lines changed: 85 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,43 +4,104 @@ const isObject = (value: any) =>
44
Object.prototype.toString.call(value) === "[object Object]";
55

66
function findPaginatedResourcePath(responseData: any): string[] {
7-
const paginatedResourcePath = deepFindPathToProperty(
7+
const paginatedResourcePath: string[] | null = deepFindPathToProperty(
88
responseData,
99
"pageInfo",
1010
);
11-
if (paginatedResourcePath.length === 0) {
11+
if (paginatedResourcePath === null) {
1212
throw new MissingPageInfo(responseData);
1313
}
1414
return paginatedResourcePath;
1515
}
1616

17-
const deepFindPathToProperty = (
18-
object: any,
19-
searchProp: string,
20-
path: string[] = [],
21-
): string[] => {
22-
for (const key of Object.keys(object)) {
23-
const currentPath = [...path, key];
24-
const currentValue = object[key];
25-
26-
if (currentValue.hasOwnProperty(searchProp)) {
27-
return currentPath;
28-
}
17+
type TreeNode = [key: string, value: any, depth: number];
2918

30-
if (isObject(currentValue)) {
31-
const result = deepFindPathToProperty(
32-
currentValue,
33-
searchProp,
34-
currentPath,
35-
);
36-
if (result.length > 0) {
37-
return result;
19+
function getDirectPropertyPath(preOrderTraversalPropertyPath: TreeNode[]) {
20+
const terminalNodeDepth: number =
21+
preOrderTraversalPropertyPath[preOrderTraversalPropertyPath.length - 1][2];
22+
23+
const alreadyConsideredDepth: { [key: string]: boolean } = {};
24+
const directPropertyPath: TreeNode[] = preOrderTraversalPropertyPath
25+
.reverse()
26+
.filter((node: TreeNode) => {
27+
const nodeDepth: number = node[2];
28+
29+
if (nodeDepth >= terminalNodeDepth || alreadyConsideredDepth[nodeDepth]) {
30+
return false;
3831
}
32+
33+
alreadyConsideredDepth[nodeDepth] = true;
34+
return true;
35+
})
36+
.reverse();
37+
38+
return directPropertyPath;
39+
}
40+
41+
function makeTreeNodeChildrenFromData(
42+
data: any,
43+
depth: number,
44+
searchProperty: string,
45+
): TreeNode[] {
46+
return isObject(data)
47+
? Object.keys(data)
48+
.reverse()
49+
.sort((a, b) => {
50+
if (searchProperty === a) {
51+
return 1;
52+
}
53+
54+
if (searchProperty === b) {
55+
return -1;
56+
}
57+
58+
return 0;
59+
})
60+
.map((key) => [key, data[key], depth])
61+
: [];
62+
}
63+
64+
function findPathToObjectContainingProperty(
65+
data: any,
66+
searchProperty: string,
67+
): string[] | null {
68+
const preOrderTraversalPropertyPath: TreeNode[] = [];
69+
const stack: TreeNode[] = makeTreeNodeChildrenFromData(
70+
data,
71+
1,
72+
searchProperty,
73+
);
74+
75+
while (stack.length > 0) {
76+
const node: TreeNode = stack.pop()!;
77+
78+
preOrderTraversalPropertyPath.push(node);
79+
80+
if (searchProperty === node[0]) {
81+
const directPropertyPath: TreeNode[] = getDirectPropertyPath(
82+
preOrderTraversalPropertyPath,
83+
);
84+
return directPropertyPath.map((node: TreeNode) => node[0]);
3985
}
86+
87+
const depth: number = node[2] + 1;
88+
const edges: TreeNode[] = makeTreeNodeChildrenFromData(
89+
node[1],
90+
depth,
91+
searchProperty,
92+
);
93+
stack.push(...edges);
4094
}
4195

42-
return [];
43-
};
96+
return null;
97+
}
98+
99+
function deepFindPathToProperty(
100+
object: any,
101+
searchProp: string,
102+
): string[] | null {
103+
return findPathToObjectContainingProperty(object, searchProp);
104+
}
44105

45106
/**
46107
* The interfaces of the "get" and "set" functions are equal to those of lodash:

test/object-helpers.test.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { findPaginatedResourcePath } from "../src/object-helpers";
2+
import { MissingPageInfo } from "../src/errors";
3+
4+
describe("findPaginatedResourcePath()", (): void => {});
5+
6+
describe("findPaginatedResourcePath()", (): void => {
7+
it("returns empty array if no pageInfo object exists", async (): Promise<void> => {
8+
expect(() => {
9+
findPaginatedResourcePath({ test: { nested: "value" } });
10+
}).toThrow(MissingPageInfo);
11+
});
12+
13+
it("returns correct path for deeply nested pageInfo", async (): Promise<void> => {
14+
const obj = {
15+
"branch-out": { x: { y: { z: {} } } },
16+
a: {
17+
"branch-out": { x: { y: { z: {} } } },
18+
b: {
19+
"branch-out": { x: { y: { z: {} } } },
20+
c: {
21+
"branch-out": { x: { y: { z: {} } } },
22+
d: {
23+
"branch-out": { x: { y: { z: {} } } },
24+
e: {
25+
"branch-out": { x: { y: { z: {} } } },
26+
f: {
27+
pageInfo: {
28+
endCursor: "Y3Vyc29yOnYyOpEB",
29+
hasNextPage: false,
30+
},
31+
"branch-out": { x: { y: { z: {} } } },
32+
},
33+
},
34+
},
35+
},
36+
},
37+
},
38+
};
39+
expect(findPaginatedResourcePath(obj)).toEqual([
40+
"a",
41+
"b",
42+
"c",
43+
"d",
44+
"e",
45+
"f",
46+
]);
47+
});
48+
49+
it("returns correct path for shallow nested pageInfo", async (): Promise<void> => {
50+
const obj = {
51+
a: {
52+
pageInfo: {
53+
endCursor: "Y3Vyc29yOnYyOpEB",
54+
hasNextPage: false,
55+
},
56+
"branch-out": { x: { y: { z: {} } } },
57+
},
58+
"branch-out": { x: { y: { z: {} } } },
59+
};
60+
expect(findPaginatedResourcePath(obj)).toEqual(["a"]);
61+
});
62+
63+
it("returns correct path for pageInfo in the root object", async (): Promise<void> => {
64+
const obj = {
65+
pageInfo: {
66+
endCursor: "Y3Vyc29yOnYyOpEB",
67+
hasNextPage: false,
68+
},
69+
"branch-out": { x: { y: { z: {} } } },
70+
};
71+
expect(findPaginatedResourcePath(obj)).toEqual([]);
72+
});
73+
});

0 commit comments

Comments
 (0)