Skip to content

Commit

Permalink
Improve implementation of deepFindPathToProperty function
Browse files Browse the repository at this point in the history
Adopt non-recursive path calculation.

Fixes octokit#58
  • Loading branch information
igwejk committed Dec 9, 2023
1 parent 55adbeb commit 1e000b0
Show file tree
Hide file tree
Showing 2 changed files with 158 additions and 24 deletions.
109 changes: 85 additions & 24 deletions src/object-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,43 +4,104 @@ const isObject = (value: any) =>
Object.prototype.toString.call(value) === "[object Object]";

function findPaginatedResourcePath(responseData: any): string[] {
const paginatedResourcePath = deepFindPathToProperty(
const paginatedResourcePath: string[] | null = deepFindPathToProperty(
responseData,
"pageInfo",
);
if (paginatedResourcePath.length === 0) {
if (paginatedResourcePath === null) {
throw new MissingPageInfo(responseData);
}
return paginatedResourcePath;
}

const deepFindPathToProperty = (
object: any,
searchProp: string,
path: string[] = [],
): string[] => {
for (const key of Object.keys(object)) {
const currentPath = [...path, key];
const currentValue = object[key];

if (currentValue.hasOwnProperty(searchProp)) {
return currentPath;
}
type TreeNode = [key: string, value: any, depth: number];

if (isObject(currentValue)) {
const result = deepFindPathToProperty(
currentValue,
searchProp,
currentPath,
);
if (result.length > 0) {
return result;
function getDirectPropertyPath(preOrderTraversalPropertyPath: TreeNode[]) {
const terminalNodeDepth: number =
preOrderTraversalPropertyPath[preOrderTraversalPropertyPath.length - 1][2];

const alreadyConsideredDepth: { [key: string]: boolean } = {};
const directPropertyPath: TreeNode[] = preOrderTraversalPropertyPath
.reverse()
.filter((node: TreeNode) => {
const nodeDepth: number = node[2];

if (nodeDepth >= terminalNodeDepth || alreadyConsideredDepth[nodeDepth]) {
return false;
}

alreadyConsideredDepth[nodeDepth] = true;
return true;
})
.reverse();

return directPropertyPath;
}

function makeTreeNodeChildrenFromData(
data: any,
depth: number,
searchProperty: string,
): TreeNode[] {
return isObject(data)
? Object.keys(data)
.reverse()
.sort((a, b) => {
if (searchProperty === a) {
return 1;
}

if (searchProperty === b) {
return -1;
}

return 0;
})
.map((key) => [key, data[key], depth])
: [];
}

function findPathToObjectContainingProperty(
data: any,
searchProperty: string,
): string[] | null {
const preOrderTraversalPropertyPath: TreeNode[] = [];
const stack: TreeNode[] = makeTreeNodeChildrenFromData(
data,
1,
searchProperty,
);

while (stack.length > 0) {
const node: TreeNode = stack.pop()!;

preOrderTraversalPropertyPath.push(node);

if (searchProperty === node[0]) {
const directPropertyPath: TreeNode[] = getDirectPropertyPath(
preOrderTraversalPropertyPath,
);
return directPropertyPath.map((node: TreeNode) => node[0]);
}

const depth: number = node[2] + 1;
const edges: TreeNode[] = makeTreeNodeChildrenFromData(
node[1],
depth,
searchProperty,
);
stack.push(...edges);
}

return [];
};
return null;
}

function deepFindPathToProperty(
object: any,
searchProp: string,
): string[] | null {
return findPathToObjectContainingProperty(object, searchProp);
}

/**
* The interfaces of the "get" and "set" functions are equal to those of lodash:
Expand Down
73 changes: 73 additions & 0 deletions test/object-helpers.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { findPaginatedResourcePath } from "../src/object-helpers";
import { MissingPageInfo } from "../src/errors";

describe("findPaginatedResourcePath()", (): void => {});

describe("findPaginatedResourcePath()", (): void => {
it("returns empty array if no pageInfo object exists", async (): Promise<void> => {
expect(() => {
findPaginatedResourcePath({ test: { nested: "value" } });
}).toThrow(MissingPageInfo);
});

it("returns correct path for deeply nested pageInfo", async (): Promise<void> => {
const obj = {
"branch-out": { x: { y: { z: {} } } },
a: {
"branch-out": { x: { y: { z: {} } } },
b: {
"branch-out": { x: { y: { z: {} } } },
c: {
"branch-out": { x: { y: { z: {} } } },
d: {
"branch-out": { x: { y: { z: {} } } },
e: {
"branch-out": { x: { y: { z: {} } } },
f: {
pageInfo: {
endCursor: "Y3Vyc29yOnYyOpEB",
hasNextPage: false,
},
"branch-out": { x: { y: { z: {} } } },
},
},
},
},
},
},
};
expect(findPaginatedResourcePath(obj)).toEqual([
"a",
"b",
"c",
"d",
"e",
"f",
]);
});

it("returns correct path for shallow nested pageInfo", async (): Promise<void> => {
const obj = {
a: {
pageInfo: {
endCursor: "Y3Vyc29yOnYyOpEB",
hasNextPage: false,
},
"branch-out": { x: { y: { z: {} } } },
},
"branch-out": { x: { y: { z: {} } } },
};
expect(findPaginatedResourcePath(obj)).toEqual(["a"]);
});

it("returns correct path for pageInfo in the root object", async (): Promise<void> => {
const obj = {
pageInfo: {
endCursor: "Y3Vyc29yOnYyOpEB",
hasNextPage: false,
},
"branch-out": { x: { y: { z: {} } } },
};
expect(findPaginatedResourcePath(obj)).toEqual([]);
});
});

0 comments on commit 1e000b0

Please sign in to comment.