mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Introduce way to create hierarchical composites from a flat array
Co-authored-by: Janne Savolainen <janne.savolainen@live.fi> Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>
This commit is contained in:
parent
3add4255cf
commit
043afc3ac8
@ -0,0 +1,52 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { Composite } from "../get-composite";
|
||||
import getComposite from "../get-composite";
|
||||
import { findComposite } from "./find-composite";
|
||||
|
||||
describe("find-composite", () => {
|
||||
let composite: Composite<{ id: string; parentId?: string; someProperty: string }>;
|
||||
|
||||
beforeEach(() => {
|
||||
composite = getComposite({
|
||||
source: [
|
||||
{ id: "some-root-id", someProperty: "some-value" },
|
||||
{ id: "some-child-id", parentId: "some-root-id", someProperty: "some-value" },
|
||||
{ id: "some-irrelevant-grandchild-id", parentId: "some-child-id", someProperty: "some-value" },
|
||||
{ id: "some-grandchild-id", parentId: "some-child-id", someProperty: "some-value" },
|
||||
],
|
||||
|
||||
rootId: "some-root-id",
|
||||
});
|
||||
});
|
||||
|
||||
it("when finding root using path, does so", () => {
|
||||
const actual = findComposite("some-root-id")(composite);
|
||||
|
||||
expect(actual?.id).toBe("some-root-id");
|
||||
});
|
||||
|
||||
it("when finding child using path, does so", () => {
|
||||
const actual = findComposite("some-root-id.some-child-id")(composite);
|
||||
|
||||
expect(actual?.id).toBe("some-child-id");
|
||||
});
|
||||
|
||||
it("when finding grandchild using path, does so", () => {
|
||||
const actual = findComposite(
|
||||
"some-root-id.some-child-id.some-grandchild-id",
|
||||
)(composite);
|
||||
|
||||
expect(actual?.id).toBe("some-grandchild-id");
|
||||
});
|
||||
|
||||
it("when finding with non existing path, returns undefined", () => {
|
||||
const actual = findComposite("some-root-id.some-non-existing-path")(
|
||||
composite,
|
||||
);
|
||||
|
||||
expect(actual).toBe(undefined);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { Composite } from "../get-composite";
|
||||
import { normalizeComposite } from "../normalize-composite/normalize-composite";
|
||||
|
||||
export const findComposite =
|
||||
(path: string) =>
|
||||
<T>(composite: Composite<T>): Composite<T> | undefined =>
|
||||
new Map(normalizeComposite(composite)).get(path);
|
||||
@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import getComposite from "../get-composite";
|
||||
import { getCompositePaths } from "./get-composite-paths";
|
||||
|
||||
describe("get-composite-paths", () => {
|
||||
it("given composite with ordered children, returns ordered paths", () => {
|
||||
const someRootItem = {
|
||||
id: "some-root-id",
|
||||
};
|
||||
|
||||
const someItem1 = {
|
||||
id: "some-id-1",
|
||||
parentId: "some-root-id",
|
||||
orderNumber: 1,
|
||||
};
|
||||
|
||||
const someItem2 = {
|
||||
id: "some-id-2",
|
||||
parentId: "some-root-id",
|
||||
orderNumber: 2,
|
||||
};
|
||||
|
||||
const someChildItem1 = {
|
||||
id: "some-child-id-1",
|
||||
parentId: "some-id-1",
|
||||
orderNumber: 1,
|
||||
};
|
||||
|
||||
const someChildItem2 = {
|
||||
id: "some-child-id-2",
|
||||
parentId: "some-id-1",
|
||||
orderNumber: 2,
|
||||
};
|
||||
|
||||
const items = [
|
||||
someRootItem,
|
||||
// Note: not in order yet.
|
||||
someItem2,
|
||||
someItem1,
|
||||
someChildItem2,
|
||||
someChildItem1,
|
||||
];
|
||||
|
||||
const composite = getComposite({
|
||||
source: items,
|
||||
});
|
||||
|
||||
const actual = getCompositePaths(composite);
|
||||
|
||||
expect(actual).toEqual([
|
||||
"some-root-id",
|
||||
"some-root-id.some-id-1",
|
||||
"some-root-id.some-id-1.some-child-id-1",
|
||||
"some-root-id.some-id-1.some-child-id-2",
|
||||
"some-root-id.some-id-2",
|
||||
]);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { pipeline } from "@ogre-tools/fp";
|
||||
import { flatMap } from "lodash/fp";
|
||||
import type { Composite } from "../get-composite";
|
||||
|
||||
export const getCompositePaths = (
|
||||
composite: Composite<any>,
|
||||
previousPath: string[] = [],
|
||||
): string[] => {
|
||||
const currentPath = [...previousPath, composite.id];
|
||||
|
||||
const currentPathString = currentPath.join(".");
|
||||
|
||||
return [
|
||||
currentPathString,
|
||||
|
||||
...pipeline(
|
||||
composite.children,
|
||||
|
||||
flatMap((childComposite) =>
|
||||
getCompositePaths(childComposite, currentPath),
|
||||
),
|
||||
),
|
||||
];
|
||||
};
|
||||
@ -0,0 +1,380 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { sortBy } from "lodash/fp";
|
||||
import getComposite from "./get-composite";
|
||||
import { getCompositePaths } from "./get-composite-paths/get-composite-paths";
|
||||
|
||||
describe("get-composite", () => {
|
||||
it("given items and a specified root id, creates a composite", () => {
|
||||
const someRootItem = {
|
||||
someId: "some-root-id",
|
||||
someParentId: undefined,
|
||||
someProperty: "some-root-content",
|
||||
};
|
||||
|
||||
const someItem = {
|
||||
someId: "some-id",
|
||||
someParentId: "some-root-id",
|
||||
someProperty: "some-content",
|
||||
};
|
||||
|
||||
const someNestedItem = {
|
||||
someId: "some-nested-id",
|
||||
someParentId: "some-id",
|
||||
someProperty: "some-nested-content",
|
||||
};
|
||||
|
||||
const items = [someRootItem, someItem, someNestedItem];
|
||||
|
||||
const composite = getComposite({
|
||||
source: items,
|
||||
rootId: "some-root-id",
|
||||
getId: (x) => x.someId,
|
||||
getParentId: (x) => x.someParentId,
|
||||
});
|
||||
|
||||
expect(composite).toEqual({
|
||||
id: "some-root-id",
|
||||
value: someRootItem,
|
||||
|
||||
children: [
|
||||
{
|
||||
id: "some-id",
|
||||
parentId: "some-root-id",
|
||||
value: someItem,
|
||||
|
||||
children: [
|
||||
{
|
||||
id: "some-nested-id",
|
||||
parentId: "some-id",
|
||||
value: someNestedItem,
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("given items and an unspecified root id and single item without parent as root, creates a composite", () => {
|
||||
const someRootItem = {
|
||||
someId: "some-root-id",
|
||||
someProperty: "some-root-content",
|
||||
// Notice: no "someParentId" makes this the root.
|
||||
someParentId: undefined,
|
||||
};
|
||||
|
||||
const someItem = {
|
||||
someId: "some-id",
|
||||
someParentId: "some-root-id",
|
||||
someProperty: "some-content",
|
||||
};
|
||||
|
||||
const someNestedItem = {
|
||||
someId: "some-nested-id",
|
||||
someParentId: "some-id",
|
||||
someProperty: "some-nested-content",
|
||||
};
|
||||
|
||||
const items = [someRootItem, someItem, someNestedItem];
|
||||
|
||||
const composite = getComposite({
|
||||
source: items,
|
||||
// Notice: no root id
|
||||
// rootId: "some-root-id",
|
||||
getId: (x) => x.someId,
|
||||
getParentId: (x) => x.someParentId,
|
||||
});
|
||||
|
||||
expect(composite).toEqual({
|
||||
id: "some-root-id",
|
||||
value: someRootItem,
|
||||
|
||||
children: [
|
||||
{
|
||||
id: "some-id",
|
||||
parentId: "some-root-id",
|
||||
value: someItem,
|
||||
|
||||
children: [
|
||||
{
|
||||
id: "some-nested-id",
|
||||
parentId: "some-id",
|
||||
value: someNestedItem,
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("given items and an unspecified root id and multiple items without parent as root, throws", () => {
|
||||
const someRootItem = {
|
||||
someId: "some-root-id",
|
||||
// Notice: no "someParentId" makes this a root.
|
||||
someParentId: undefined,
|
||||
};
|
||||
|
||||
const someOtherRootItem = {
|
||||
someId: "some-other-root-id",
|
||||
// Notice: no "someParentId" makes also this a root.
|
||||
someParentId: undefined,
|
||||
};
|
||||
|
||||
const items = [someRootItem, someOtherRootItem];
|
||||
|
||||
expect(() => {
|
||||
getComposite({
|
||||
source: items,
|
||||
// Notice: no root id
|
||||
// rootId: "some-root-id",
|
||||
getId: (x) => x.someId,
|
||||
getParentId: (x) => x.someParentId,
|
||||
});
|
||||
}).toThrow(
|
||||
'Tried to get a composite, but multiple roots where encountered: "some-root-id", "some-other-root-id"',
|
||||
);
|
||||
});
|
||||
|
||||
it("given non-unique ids, throws", () => {
|
||||
const someItem = {
|
||||
someId: "some-id",
|
||||
someParentId: "irrelevant",
|
||||
};
|
||||
|
||||
const someOtherItem = {
|
||||
someId: "some-id",
|
||||
someParentId: "irrelevant",
|
||||
};
|
||||
|
||||
const items = [someItem, someOtherItem];
|
||||
|
||||
expect(() => {
|
||||
getComposite({
|
||||
source: items,
|
||||
rootId: "irrelevant",
|
||||
getId: (x) => x.someId,
|
||||
getParentId: (x) => x.someParentId,
|
||||
});
|
||||
}).toThrow(
|
||||
'Tried to get a composite but encountered non-unique ids: "some-id"',
|
||||
);
|
||||
});
|
||||
|
||||
it("given missing parent ids, throws", () => {
|
||||
const someItem = {
|
||||
someId: "some-id",
|
||||
someParentId: undefined,
|
||||
};
|
||||
|
||||
const someItemWithMissingParentId = {
|
||||
someId: "some-other-id",
|
||||
someParentId: "some-missing-id",
|
||||
};
|
||||
|
||||
const items = [someItem, someItemWithMissingParentId];
|
||||
|
||||
expect(() => {
|
||||
getComposite({
|
||||
source: items,
|
||||
rootId: "irrelevant",
|
||||
getId: (x) => x.someId,
|
||||
getParentId: (x) => x.someParentId,
|
||||
});
|
||||
}).toThrow(
|
||||
'Tried to get a composite but encountered missing parent ids: "some-missing-id"',
|
||||
);
|
||||
});
|
||||
|
||||
it("given undefined ids, throws", () => {
|
||||
const root = {
|
||||
someParentId: undefined,
|
||||
someId: "some-root",
|
||||
};
|
||||
|
||||
const someItem = {
|
||||
someParentId: "some-root",
|
||||
someId: undefined,
|
||||
};
|
||||
|
||||
const someOtherItem = {
|
||||
someParentId: "some-root",
|
||||
someId: undefined,
|
||||
};
|
||||
|
||||
const items = [root, someItem, someOtherItem];
|
||||
|
||||
expect(() => {
|
||||
getComposite({
|
||||
source: items,
|
||||
rootId: "some-root",
|
||||
getId: (x) => x.someId as any,
|
||||
getParentId: (x) => x.someParentId,
|
||||
});
|
||||
}).toThrow("Tried to get a composite but encountered 2 undefined ids");
|
||||
});
|
||||
|
||||
it("given items with default properties for id and parentId, creates a composite", () => {
|
||||
const someRootItem = {
|
||||
id: "some-root-id",
|
||||
};
|
||||
|
||||
const someItem = {
|
||||
id: "some-id",
|
||||
parentId: "some-root-id",
|
||||
};
|
||||
|
||||
const someNestedItem = {
|
||||
id: "some-nested-id",
|
||||
parentId: "some-id",
|
||||
};
|
||||
|
||||
const items = [someRootItem, someItem, someNestedItem];
|
||||
|
||||
const composite = getComposite({
|
||||
source: items,
|
||||
// Notice: no need for functions
|
||||
// getId: (x) => x.id,
|
||||
// getParentId: (x) => x.parentId,
|
||||
});
|
||||
|
||||
expect(composite).toEqual({
|
||||
id: "some-root-id",
|
||||
value: someRootItem,
|
||||
|
||||
children: [
|
||||
{
|
||||
id: "some-id",
|
||||
parentId: "some-root-id",
|
||||
value: someItem,
|
||||
|
||||
children: [
|
||||
{
|
||||
id: "some-nested-id",
|
||||
parentId: "some-id",
|
||||
value: someNestedItem,
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("given explicitly ordered items, creates a composite with ordered children", () => {
|
||||
const someRootItem = {
|
||||
id: "some-root-id",
|
||||
someOrderNumber: 1,
|
||||
};
|
||||
|
||||
const someItem1 = {
|
||||
id: "some-id-1",
|
||||
parentId: "some-root-id",
|
||||
someOrderNumber: 1,
|
||||
};
|
||||
|
||||
const someItem2 = {
|
||||
id: "some-id-2",
|
||||
parentId: "some-root-id",
|
||||
someOrderNumber: 2,
|
||||
};
|
||||
|
||||
const someChildItem1 = {
|
||||
id: "some-child-id-1",
|
||||
parentId: "some-id-1",
|
||||
someOrderNumber: 1,
|
||||
};
|
||||
|
||||
const someChildItem2 = {
|
||||
id: "some-child-id-2",
|
||||
parentId: "some-id-1",
|
||||
someOrderNumber: 2,
|
||||
};
|
||||
|
||||
const items = [
|
||||
someRootItem,
|
||||
// Note: not in order yet.
|
||||
someItem2,
|
||||
someItem1,
|
||||
someChildItem2,
|
||||
someChildItem1,
|
||||
];
|
||||
|
||||
const composite = getComposite({
|
||||
source: items,
|
||||
// Note: this is the explicit function to order a composite's children.
|
||||
getOrderedChildren: (things) =>
|
||||
sortBy((thing) => thing.someOrderNumber, things),
|
||||
});
|
||||
|
||||
const orderedPaths = getCompositePaths(composite);
|
||||
|
||||
expect(orderedPaths).toEqual([
|
||||
"some-root-id",
|
||||
"some-root-id.some-id-1",
|
||||
"some-root-id.some-id-1.some-child-id-1",
|
||||
"some-root-id.some-id-1.some-child-id-2",
|
||||
"some-root-id.some-id-2",
|
||||
]);
|
||||
});
|
||||
|
||||
it("given implicitly ordered items, creates a composite with ordered children", () => {
|
||||
const someRootItem = {
|
||||
id: "some-root-id",
|
||||
orderNumber: 1,
|
||||
};
|
||||
|
||||
const someItem1 = {
|
||||
id: "some-id-1",
|
||||
parentId: "some-root-id",
|
||||
orderNumber: 1,
|
||||
};
|
||||
|
||||
const someItem2 = {
|
||||
id: "some-id-2",
|
||||
parentId: "some-root-id",
|
||||
orderNumber: 2,
|
||||
};
|
||||
|
||||
const someChildItem1 = {
|
||||
id: "some-child-id-1",
|
||||
parentId: "some-id-1",
|
||||
orderNumber: 1,
|
||||
};
|
||||
|
||||
const someChildItem2 = {
|
||||
id: "some-child-id-2",
|
||||
parentId: "some-id-1",
|
||||
orderNumber: 2,
|
||||
};
|
||||
|
||||
const items = [
|
||||
someRootItem,
|
||||
// Note: not in order yet.
|
||||
someItem2,
|
||||
someItem1,
|
||||
someChildItem2,
|
||||
someChildItem1,
|
||||
];
|
||||
|
||||
const composite = getComposite({
|
||||
source: items,
|
||||
// Note: without explicit getOrderedChildren for ordering, implicit default value of "orderNumber" will be used, if it exists.
|
||||
// getOrderedChildren: things => sortBy(thing => thing.orderNumber, things),
|
||||
});
|
||||
|
||||
const orderedPaths = getCompositePaths(composite);
|
||||
|
||||
expect(orderedPaths).toEqual([
|
||||
"some-root-id",
|
||||
"some-root-id.some-id-1",
|
||||
"some-root-id.some-id-1.some-child-id-1",
|
||||
"some-root-id.some-id-1.some-child-id-2",
|
||||
"some-root-id.some-id-2",
|
||||
]);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,120 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import { pipeline } from "@ogre-tools/fp";
|
||||
import {
|
||||
countBy,
|
||||
filter,
|
||||
toPairs,
|
||||
nth,
|
||||
map,
|
||||
uniq,
|
||||
without,
|
||||
compact,
|
||||
get,
|
||||
sortBy,
|
||||
} from "lodash/fp";
|
||||
|
||||
export interface Composite<T> {
|
||||
id: string;
|
||||
parentId: string | undefined;
|
||||
value: T;
|
||||
children: Composite<T>[];
|
||||
}
|
||||
|
||||
export default <T>({
|
||||
source,
|
||||
rootId,
|
||||
getId = get("id"),
|
||||
getParentId = get("parentId"),
|
||||
getOrderedChildren = (things: T[]) => sortBy("orderNumber", things),
|
||||
}: {
|
||||
source: T[];
|
||||
rootId?: string;
|
||||
getId?: (thing: T) => string;
|
||||
getParentId?: (thing: T) => string | undefined;
|
||||
getOrderedChildren?: (things: T[]) => T[];
|
||||
}) => {
|
||||
const undefinedIds = pipeline(
|
||||
source,
|
||||
filter((x) => getId(x) === undefined),
|
||||
);
|
||||
|
||||
if(undefinedIds.length) {
|
||||
throw new Error(
|
||||
`Tried to get a composite but encountered ${undefinedIds.length} undefined ids`,
|
||||
);
|
||||
}
|
||||
|
||||
const duplicateIds = pipeline(
|
||||
source,
|
||||
countBy(getId),
|
||||
toPairs,
|
||||
filter(([, count]) => count > 1),
|
||||
map(nth(0)),
|
||||
);
|
||||
|
||||
if (duplicateIds.length) {
|
||||
throw new Error(
|
||||
`Tried to get a composite but encountered non-unique ids: "${duplicateIds
|
||||
.map((x) => String(x))
|
||||
.join('", "')}"`,
|
||||
);
|
||||
}
|
||||
|
||||
const allIds = pipeline(source, map(getId));
|
||||
|
||||
const allParentIds = pipeline(source, map(getParentId), uniq, compact);
|
||||
|
||||
const unknownParentIds = without(allIds, allParentIds);
|
||||
|
||||
if (unknownParentIds.length) {
|
||||
throw new Error(
|
||||
`Tried to get a composite but encountered missing parent ids: "${unknownParentIds
|
||||
.map((x) => String(x))
|
||||
.join('", "')}"`,
|
||||
);
|
||||
}
|
||||
|
||||
const toComposite = (thing: T): Composite<T> => {
|
||||
const thingId = getId(thing);
|
||||
|
||||
return {
|
||||
id: thingId,
|
||||
parentId: getParentId(thing),
|
||||
value: thing,
|
||||
|
||||
children: pipeline(
|
||||
source,
|
||||
|
||||
filter((childThing) => {
|
||||
const parentId = getParentId(childThing);
|
||||
|
||||
return parentId !== undefined && parentId === thingId;
|
||||
}),
|
||||
|
||||
getOrderedChildren,
|
||||
|
||||
map(toComposite),
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
const isRootId = rootId
|
||||
? (thing: T) => getId(thing) === rootId
|
||||
: (thing: T) => getParentId(thing) === undefined;
|
||||
|
||||
const roots = source.filter(isRootId);
|
||||
|
||||
if (roots.length > 1) {
|
||||
throw new Error(
|
||||
`Tried to get a composite, but multiple roots where encountered: "${roots
|
||||
.map(getId)
|
||||
.join('", "')}"`,
|
||||
);
|
||||
}
|
||||
|
||||
return toComposite(roots[0]);
|
||||
};
|
||||
@ -0,0 +1,44 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { normalizeComposite } from "./normalize-composite";
|
||||
import getComposite from "../get-composite";
|
||||
|
||||
describe("normalize-composite", () => {
|
||||
it("given a composite, flattens it to path and composite", () => {
|
||||
const someRootItem = {
|
||||
id: "some-root-id",
|
||||
parentId: undefined,
|
||||
};
|
||||
|
||||
const someItem = {
|
||||
id: "some-id",
|
||||
parentId: "some-root-id",
|
||||
};
|
||||
|
||||
const someNestedItem = {
|
||||
id: "some-child-id",
|
||||
parentId: "some-id",
|
||||
};
|
||||
|
||||
const items = [someRootItem, someItem, someNestedItem];
|
||||
|
||||
const composite = getComposite({
|
||||
source: items,
|
||||
});
|
||||
|
||||
const actual = normalizeComposite(composite);
|
||||
|
||||
expect(actual).toEqual([
|
||||
["some-root-id", expect.objectContaining({ value: someRootItem })],
|
||||
|
||||
["some-root-id.some-id", expect.objectContaining({ value: someItem })],
|
||||
|
||||
[
|
||||
"some-root-id.some-id.some-child-id",
|
||||
expect.objectContaining({ value: someNestedItem }),
|
||||
],
|
||||
]);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,20 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import type { Composite } from "../get-composite";
|
||||
|
||||
export const normalizeComposite = <T>(
|
||||
composite: Composite<T>,
|
||||
previousPath: string[] = [],
|
||||
): (readonly [path: string, composite: Composite<T>])[] => {
|
||||
const currentPath = [...previousPath, composite.id];
|
||||
|
||||
const pathAndCompositeTuple = [currentPath.join("."), composite] as const;
|
||||
|
||||
return [
|
||||
pathAndCompositeTuple,
|
||||
|
||||
...composite.children.flatMap((x) => normalizeComposite(x, currentPath)),
|
||||
];
|
||||
};
|
||||
Loading…
Reference in New Issue
Block a user