diff --git a/src/object-helpers.ts b/src/object-helpers.ts index d6b6f13..586fabd 100644 --- a/src/object-helpers.ts +++ b/src/object-helpers.ts @@ -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: diff --git a/test/object-helpers.test.ts b/test/object-helpers.test.ts new file mode 100644 index 0000000..08e2c0c --- /dev/null +++ b/test/object-helpers.test.ts @@ -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 => { + expect(() => { + findPaginatedResourcePath({ test: { nested: "value" } }); + }).toThrow(MissingPageInfo); + }); + + it("returns correct path for deeply nested pageInfo", async (): Promise => { + 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 => { + 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 => { + const obj = { + pageInfo: { + endCursor: "Y3Vyc29yOnYyOpEB", + hasNextPage: false, + }, + "branch-out": { x: { y: { z: {} } } }, + }; + expect(findPaginatedResourcePath(obj)).toEqual([]); + }); +});