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