Modularize mounts/block devices, cleanup

feature/node-rewrite
Sven Slootweg 10 months ago
parent 300c58533f
commit e0e42cab95

@ -26,6 +26,15 @@
"skipFiles": [
"<node_internals>/**"
],
},
{
"type": "node",
"request": "launch",
"name": "tree-find testcase",
"program": "${workspaceFolder}/src/packages/tree-find/example.js",
"skipFiles": [
"<node_internals>/**"
],
}
]
}

@ -1,42 +0,0 @@
"use strict";
const Promise = require("bluebird");
const memoizee = require("memoizee");
const fs = Promise.promisifyAll(require("fs"));
const findmnt = require("../../packages/exec-findmnt");
const All = require("../../packages/graphql-interface/symbols/all");
const treeMapAsync = require("../../packages/tree-map-async");
module.exports = function () {
let findmntOnce = memoizee(() => {
return Promise.try(() => {
return findmnt();
}).then((mounts) => {
return treeMapAsync(mounts, async (mount) => {
if (mount.sourceDevice?.startsWith("/")) {
return {
... mount,
sourceDevice: await fs.realpathAsync(mount.sourceDevice)
};
} else {
// Skip mounts that don't exist at a path at all
return mount;
}
}, true);
});
});
return function (mountpoints) {
return Promise.try(() => {
return findmntOnce();
}).then(({tree, list}) => {
return mountpoints.map((mountpoint) => {
if (mountpoint === All) {
return tree;
} else {
return list.find((mount) => mount.mountpoint === mountpoint);
}
});
});
};
};

@ -32,21 +32,6 @@ function makeSingleCommand({ command, selectResult }) {
module.exports = function createSources() {
let sources = {
// lvmLogicalVolumes: evaluateAndPick({
// command: lvm.getLogicalVolumes,
// selectResult: (result) => result.volumes,
// selectID: (volume) => volume.path
// }),
// lvmPhysicalVolumes: evaluateAndPick({
// command: lvm.getPhysicalVolumes,
// selectResult: (result) => result.volumes,
// selectID: (device) => device.path
// }),
// lvmVolumeGroups: evaluateAndPick({
// command: lvm.getVolumeGroups,
// selectResult: (result) => result.groups,
// selectID: (group) => group.name
// }),
nvmeIdentifyController: makeSingleCommand({
command: (path) => nvmeCLI.identifyController({ devicePath: path })
}),
@ -70,10 +55,7 @@ module.exports = function createSources() {
};
// TODO: Consider moving these to be inline as well, somehow
let factoryModules = {
lsblk: require("./lsblk")(),
findmnt: require("./findmnt")()
};
let factoryModules = {};
return mapObj({ ... factoryModules, ... sources }, (name, factory) => {
return [

@ -1,54 +0,0 @@
"use strict";
const Promise = require("bluebird");
const memoizee = require("memoizee");
const asExpression = require("as-expression");
const fs = Promise.promisifyAll(require("fs"));
const unreachable = require("@joepie91/unreachable")("cvm");
const lsblk = require("../../packages/exec-lsblk");
const All = require("../../packages/graphql-interface/symbols/all");
const findInTree = require("../../packages/find-in-tree");
const treeMapAsync = require("../../packages/tree-map-async");
module.exports = function () {
let lsblkOnce = memoizee(() => {
return Promise.try(() => {
return lsblk();
}).then((tree) => {
return treeMapAsync(tree, async (device) => {
return {
... device,
path: await fs.realpathAsync(device.path)
};
}, true);
});
});
return function (selectors) {
return Promise.try(() => {
return lsblkOnce();
}).then(({tree, list}) => {
return selectors.map((selector) => {
if (selector === All) {
return tree;
// return list;
} else {
let { path, name } = selector;
let predicate = asExpression(() => {
if (path != null) {
return (device) => device.path === path;
} else if (name != null) {
return (device) => device.name === name;
} else {
unreachable("No selector specified for lsblk");
}
});
// TODO: Shouldn't this pick from the list instead?
return findInTree({ tree, predicate });
}
});
});
};
};

@ -32,18 +32,16 @@ module.exports = function () {
};
},
modules: [
require("../packages/sysquery-lvm")
require("../packages/sysquery-lvm"),
require("../packages/sysquery-core"),
],
schema: {
hardware: {
drives: ({ paths }, { sources }) => {
return typeFromSource(sources.smartctlScan, paths, (device) => types.Drive({ path: device.path }));
}
// drives: ({ paths }, { sources }) => {
// return typeFromSource(sources.smartctlScan, paths, (device) => types.Drive({ path: device.path }));
// }
},
resources: {
blockDevices: ({ names }, { sources }) => {
return typeFromSource(sources.lsblk, names, (device) => types.BlockDevice({ name: device.name }));
},
images: {
installationMedia: [],
vmImages: []

@ -1,64 +0,0 @@
"use strict";
const Promise = require("bluebird");
const fs = Promise.promisifyAll(require("fs"));
const matchValue = require("match-value");
const asyncpipe = require("../../packages/asyncpipe");
const dlayerSource = require("../../packages/dlayer-source");
const All = require("../../packages/graphql-interface/symbols/all"); // FIXME: Move to dlayer-source?
const treecutter = require("../../packages/treecutter");
const types = require("./");
module.exports = function BlockDevice({ name, path }) {
return dlayerSource.withSources({
// TODO: Eventually make this produce a (filtered) tree instead?
mounts: function ({ type }, { $getProperty, $getPropertyPath, sources }) {
return Promise.try(() => {
return sources.findmnt.load(All);
}).then((mountTree) => {
return asyncpipe(mountTree, [
(_) => treecutter.flatten(_),
(_) => _.map((mount) => types.Mount({ mountpoint: mount.mountpoint })),
(_) => Promise.filter(_, async (mount) => {
let sourcePath = await $getPropertyPath(mount, "sourceDevice.path");
let sourceName = await $getPropertyPath(mount, "sourceDevice.name");
return (
(sourcePath != null && sourcePath === path)
|| (sourceName != null && sourceName === name)
);
})
]);
}).then((relevantMounts) => {
if (type == null) {
return relevantMounts;
} else {
return Promise.filter(relevantMounts, async (mount) => {
return (await $getProperty(mount, "type") === type);
});
}
});
},
$sources: {
lsblk: {
[dlayerSource.ID]: { name, path },
name: "name",
path: (device) => fs.realpathAsync(device.path),
type: (device) => matchValue(device.type, {
partition: "PARTITION",
disk: "DISK",
loopDevice: "LOOP_DEVICE"
}),
size: "size",
mountpoint: "mountpoint", // FIXME: Isn't this obsoleted by `mounts`?
deviceNumber: "deviceNumber",
removable: "removable",
readOnly: "readOnly",
children: (device) => device.children.map((child) => {
return BlockDevice({ name: child.name });
})
}
}
});
};

@ -6,7 +6,7 @@ const treecutter = require("../../packages/treecutter");
const upperSnakeCase = require("../../packages/upper-snake-case");
const { B } = require("../../packages/unit-bytes-iec");
const types = require("./");
const types = require(".");
module.exports = function Drive ({ path }) {
return dlayerSource.withSources({

@ -1,7 +1,5 @@
"use strict";
Object.assign(module.exports, {
Drive: require("./drive"),
BlockDevice: require("./block-device"),
Mount: require("./mount"),
// Drive: require("./drive"),
});

@ -1,58 +0,0 @@
"use strict";
const dlayerSource = require("../../../packages/dlayer-source");
const types = require("..");
module.exports = function LVMLogicalVolume ({ path }) {
return dlayerSource.withSources({
$sources: {
lvmLogicalVolumes: {
[dlayerSource.ID]: path,
path: "path",
name: "name",
fullName: "fullName",
size: "size",
uuid: "uuid",
deviceMapperPath: "deviceMapperPath",
layoutAttributes: "layoutAttributes",
roles: "roles",
tags: "tags",
configurationProfile: "configurationProfile",
creationTime: "creationTime",
creationHost: "creationHost",
neededKernelModules: "neededKernelModules",
dataVolume: "dataVolume", // FIXME: Reference?
metadataVolume: "metadataVolume", // FIXME: Reference?
poolVolume: "poolVolume", // FIXME: Reference?
persistentMajorNumber: "persistentMajorNumber",
persistentMinorNumber: "persistentMinorNumber",
type: "type",
isReadOnly: "isReadOnly",
isCurrentlyReadOnly: "isCurrentlyReadOnly",
isAllocationLocked: "isAllocationLocked",
allocationPolicy: "allocationPolicy",
status: "status",
healthStatus: "healthStatus",
isInitiallySynchronized: "isInitiallySynchronized",
isCurrentlySynchronized: "isCurrentlySynchronized",
isMerging: "isMerging",
isConverting: "isConverting",
isSuspended: "isSuspended",
isActivationSkipped: "isActivationSkipped",
isOpened: "isOpened",
isActiveLocally: "isActiveLocally",
isActiveRemotely: "isActiveRemotely",
isActiveExclusively: "isActiveExclusively",
isMergeFailed: "isMergeFailed",
isSnapshotInvalid: "isSnapshotInvalid",
isLiveTablePresent: "isLiveTablePresent",
isInactiveTablePresent: "isInactiveTablePresent",
isZeroFilled: "isZeroFilled",
hasFixedMinorNumber: "hasFixedMinorNumber",
outOfSpacePolicy: "outOfSpacePolicy",
volumeGroup: (volume) => types.LVMVolumeGroup({ name: volume.volumeGroup })
}
}
});
};

@ -1,25 +0,0 @@
"use strict";
const dlayerSource = require("../../../packages/dlayer-source");
const types = require("../");
module.exports = function LVMPhysicalVolume ({ path }) {
return dlayerSource.withSources({
$sources: {
lvmPhysicalVolumes: {
[dlayerSource.ID]: path,
path: "path",
format: "format",
totalSpace: "totalSpace",
freeSpace: "freeSpace",
isExported: "isExported",
isMissing: "isMissing",
isAllocatable: "isAllocatable",
isDuplicate: "isDuplicate",
isUsed: "isUsed",
volumeGroup: (volume) => types.LVMVolumeGroup({ name: volume.volumeGroup })
}
}
});
};

@ -1,47 +0,0 @@
"use strict";
const Promise = require("bluebird");
const dlayerSource = require("../../../packages/dlayer-source");
const types = require("../");
const All = require("../../../packages/graphql-interface/symbols/all");
module.exports = function LVMVolumeGroup ({ name }) {
return dlayerSource.withSources({
physicalVolumes: function (_args, { sources }) {
return Promise.try(() => {
return sources.lvmPhysicalVolumes.load(All);
}).filter((volume) => {
return (volume.volumeGroup === name);
}).map((volume) => {
return types.LVMPhysicalVolume({ path: volume.path });
});
},
logicalVolumes: function (_args, { sources }) {
return Promise.try(() => {
return sources.lvmLogicalVolumes.load(All);
}).filter((volume) => {
return (volume.volumeGroup === name);
}).map((volume) => {
return types.LVMLogicalVolume({ path: volume.path });
});
},
$sources: {
lvmVolumeGroups: {
[dlayerSource.ID]: name,
name: "name",
totalSpace: "totalSpace",
freeSpace: "freeSpace",
physicalVolumeCount: "physicalVolumeCount",
logicalVolumeCount: "logicalVolumeCount",
snapshotCount: "snapshotCount",
isReadOnly: "isReadOnly",
isResizeable: "isResizeable",
isExported: "isExported",
isIncomplete: "isIncomplete",
allocationPolicy: "allocationPolicy",
mode: "mode"
}
}
});
};

@ -1,61 +0,0 @@
"use strict";
const Promise = require("bluebird");
const fs = Promise.promisifyAll(require("fs"));
const dlayerSource = require("../../packages/dlayer-source");
const types = require("./");
module.exports = function Mount({ mountpoint }) {
return dlayerSource.withSources({
mountpoint: mountpoint,
sourceDevice: async (_, { sources }) => {
let mount = await sources.findmnt.load(mountpoint);
if (mount != null) {
if (mount.sourceDevice != null) {
let sourcePath = await fs.realpathAsync(mount.sourceDevice);
if (await sources.lsblk.load({ path: sourcePath }) != null) {
return types.BlockDevice({ path: sourcePath });
} else {
// This occurs when the `sourceDevice` is a valid device, but it is not a *block* device, eg. `/dev/fuse`
return null;
}
} else {
// This occurs when the mount is not backed by a device, eg. an sshfs FUSE mount
return null;
}
} else {
// TODO: Can this ever happen for any legitimate reason?
throw new Error(`Mountpoint '${mountpoint}' not found in findmnt output`);
}
},
$sources: {
findmnt: {
[dlayerSource.ID]: mountpoint,
id: "id",
// FIXME: Aren't we inferring the below somewhere else in the code, using the square brackets?
type: (mount) => (mount.rootPath === "/")
? "ROOT_MOUNT"
: "SUBMOUNT",
filesystem: "filesystem",
options: "options",
label: "label",
uuid: "uuid",
partitionLabel: "partitionLabel",
partitionUUID: "partitionUUID",
deviceNumber: "deviceNumber",
totalSpace: "totalSpace",
freeSpace: "freeSpace",
usedSpace: "usedSpace",
rootPath: "rootPath",
taskID: "taskID",
optionalFields: "optionalFields",
propagationFlags: "propagationFlags",
children: (mount) => mount.children.map((child) => {
return Mount({ mountpoint: child.mountpoint });
})
}
}
});
};

@ -1,39 +0,0 @@
"use strict";
const assureArray = require("assure-array");
const isIterable = require("is-iterable");
const { validateOptions } = require("@validatem/core");
const required = require("@validatem/required");
const isFunction = require("@validatem/is-function");
const isString = require("@validatem/is-string");
module.exports = function findInTree(options) {
validateOptions(arguments, {
tree: [ required ],
predicate: [ required, isFunction ],
childrenProperty: [ isString ],
});
let childrenProperty = options.childrenProperty ?? "children";
let topLevelItems = assureArray(options.tree);
let predicate = options.predicate;
function find(items) {
if (isIterable(items)) {
for (let item of items) {
if (predicate(item)) {
return item;
} else {
let childResult = find(item[childrenProperty]);
if (childResult !== undefined) {
return childResult;
}
}
}
}
}
return find(topLevelItems);
};

@ -0,0 +1,20 @@
"use strict";
module.exports = function find(iterable, predicate) {
if (Array.isArray(iterable)) {
// An optimized implementation may be available
return iterable.find(predicate);
} else {
let i = 0;
for (let value of iterable) {
let found = predicate(value, i, iterable);
if (found) {
return value;
}
i++;
}
}
};

@ -0,0 +1,17 @@
"use strict";
// Kinda like `find`, but the ?? edition - evaluates items until one produces a not-nullish result
module.exports = function firstValue(iterable, predicate) {
let i = 0;
for (let value of iterable) {
let found = predicate(value, i, iterable);
if (found != null) {
return found;
}
i++;
}
};

@ -0,0 +1,17 @@
"use strict";
// TODO: Document that there's no async version of this because an async version must always map (to a Promise) anyway
module.exports = function forEach(iterable, mapper) {
if (Array.isArray(iterable)) {
// This may have an optimized implementation
return iterable.forEach(mapper);
} else {
let i = 0;
for (let item of iterable) {
mapper(item, i, iterable);
i++;
}
}
};

@ -0,0 +1,23 @@
"use strict";
// TODO: Add concurrency control?
const map = require("../map");
module.exports = async function mapAsync(iterable, mapper, options = {}) {
let concurrent = options.concurrent ?? true;
if (concurrent) {
return Promise.all(map(iterable, mapper));
} else {
let results = [];
let i = 0;
for (let value of iterable) {
let result = await mapper(value, i, iterable);
results.push(result);
}
return results;
}
};

@ -0,0 +1,18 @@
"use strict";
module.exports = function map(iterable, mapper) {
if (Array.isArray(iterable)) {
// This may have an optimized implementation
return iterable.map(mapper);
} else {
let result = [];
let i = 0;
for (let item of iterable) {
result.push(mapper(item, i, iterable));
i++;
}
return result;
}
};

@ -0,0 +1,37 @@
"use strict";
const memoizee = require("memoizee");
const fs = require("fs").promises;
const findmnt = require("../exec-findmnt");
const All = require("../graphql-interface/symbols/all");
const treeMapAsync = require("../tree-map-async");
const treeFind = require("../tree-find");
module.exports = function () {
let findmntOnce = memoizee(async () => {
return treeMapAsync(await findmnt(), async (mount) => {
return {
... mount,
sourceDevice: (mount.sourceDevice?.startsWith("/"))
? await fs.realpath(mount.sourceDevice)
: mount.sourceDevice
};
});
});
return async function (mountpoints) {
let mounts = await findmntOnce();
// TODO: It's kind of strange that it sometimes returns a tree and sometimes a list, this can probably be improved?
let matches = mountpoints.map((mountpoint) => {
if (mountpoint === All) {
return mounts;
} else {
return treeFind(mounts, (mount) => mount.mountpoint === mountpoint);
}
});
return matches;
};
};

@ -0,0 +1,152 @@
"use strict";
const fs = require("fs").promises;
const matchValue = require("match-value");
const DataLoader = require("dataloader");
const mapFromSource = require("../map-from-source");
const dlayerSource = require("../dlayer-source");
const All = require("../graphql-interface/symbols/all");
const treeFilterFlatAsync = require("../tree-filter-flat-async");
const assert = require("assert");
// FIXME: Simplify findmnt/lsblk source definitions, and probably separate out mounts and block devices into their own separate modules
module.exports = {
name: "sysquery.core",
makeContext: function () {
// MARKER: Complete sources migration, test, move smartctl
return {
lsblk: new DataLoader(require("./lsblk")()),
findmnt: new DataLoader(require("./findmnt")())
};
},
types: {
"sysquery.core.Mount": function ({ mountpoint }) {
return dlayerSource.withSources({
mountpoint: mountpoint,
sourceDevice: async (_, { lsblk, findmnt, $make }) => {
let mount = await findmnt.load(mountpoint);
if (mount == null) {
// TODO: Can this ever happen for any legitimate reason?
throw new Error(`Mountpoint '${mountpoint}' not found in findmnt output`);
} else if (mount.sourceDevice == null) {
// This occurs when the mount is not backed by a device, eg. an sshfs FUSE mount
return null;
} else {
let sourcePath = await fs.realpath(mount.sourceDevice);
if (await lsblk.load({ path: sourcePath }) != null) {
return $make("sysquery.core.BlockDevice", { path: sourcePath });
} else {
// This occurs when the `sourceDevice` is a valid device, but it is not a *block* device, eg. `/dev/fuse`
return null;
}
}
},
$sources: {
findmnt: {
[dlayerSource.ID]: mountpoint,
id: "id",
// FIXME: Aren't we inferring the below somewhere else in the code, using the square brackets?
type: (mount) => (mount.rootPath === "/")
? "ROOT_MOUNT"
: "SUBMOUNT",
filesystem: "filesystem",
options: "options",
label: "label",
uuid: "uuid",
partitionLabel: "partitionLabel",
partitionUUID: "partitionUUID",
deviceNumber: "deviceNumber",
totalSpace: "totalSpace",
freeSpace: "freeSpace",
usedSpace: "usedSpace",
rootPath: "rootPath",
taskID: "taskID",
optionalFields: "optionalFields",
propagationFlags: "propagationFlags",
children: (mount, { $make }) => mount.children.map((child) => {
return $make("sysquery.core.Mount", { mountpoint: child.mountpoint });
})
}
}
});
},
"sysquery.core.BlockDevice": function ({ name, path }) {
return dlayerSource.withSources({
// TODO: Eventually make this produce a (filtered) tree instead?
mounts: async function ({ type }, { findmnt, $make, $getProperty, $getPropertyPath }) {
let mountTree = await findmnt.load(All);
let relevantMounts = await treeFilterFlatAsync(mountTree, async (mount) => {
let mountObject = $make("sysquery.core.Mount", { mountpoint: mount.mountpoint });
// console.log({ sourceDevice: await $getProperty(mountObject, "sourceDevice") });
let sourcePath = await $getPropertyPath(mountObject, "sourceDevice.path");
let sourceName = await $getPropertyPath(mountObject, "sourceDevice.name");
// TODO: This logic looks strange. Is it actually correct, even when only one of name/path is specified upon BlockDevice construction?
let matchesDevice = (
(sourcePath != null && sourcePath === path)
|| (sourceName != null && sourceName === name)
);
let matchesType = (
type == null
|| await $getProperty(mountObject, "type" === type)
);
return matchesDevice && matchesType;
}, { recurseFilteredSubtrees: true });
// This is a bit hacky; this approach should probably be replaced by a map-filter instead. But for now, this will do - as this all happens in the same request context, there's no real penalty to re-creating the mount objects a second time.
return relevantMounts.map((mount) => {
return $make("sysquery.core.Mount", { mountpoint: mount.mountpoint });
});
},
$sources: {
lsblk: {
[dlayerSource.ID]: { name, path },
name: "name",
path: (device) => fs.realpath(device.path),
type: (device) => matchValue(device.type, {
partition: "PARTITION",
disk: "DISK",
loopDevice: "LOOP_DEVICE"
}),
size: "size",
mountpoint: "mountpoint", // FIXME: Isn't this obsoleted by `mounts`?
deviceNumber: "deviceNumber",
removable: "removable",
readOnly: "readOnly",
children: (device, { $make }) => device.children.map((child) => {
return $make("sysquery.core.BlockDevice", { name: child.name });
})
}
}
});
}
},
extensions: {
// None
},
root: {
resources: {
blockDevices: ({ names, paths }, { lsblk, $make }) => {
// TODO: Design better abstraction for this sort of case
let selectors = (names == null && paths == null)
? null
: [
... (names ?? []).map((name) => ({ name: name })),
... (paths ?? []).map((path) => ({ path: path }))
];
return mapFromSource(lsblk, selectors, (device) => {
return $make("sysquery.core.BlockDevice", { name: device.name });
});
}
// FIXME: Add mounts
}
}
};

@ -0,0 +1,42 @@
"use strict";
const memoizee = require("memoizee");
const fs = require("fs").promises;
const unreachable = require("@joepie91/unreachable")("cvm");
const lsblk = require("../exec-lsblk");
const All = require("../graphql-interface/symbols/all");
const treeFind = require("../tree-find");
const treeMapAsync = require("../tree-map-async");
function makePredicate({ path, name }) {
if (path != null) {
return (device) => device.path === path;
} else if (name != null) {
return (device) => device.name === name;
} else {
unreachable("No selector specified for lsblk");
}
}
module.exports = function () {
let lsblkOnce = memoizee(async () => {
return treeMapAsync(await lsblk(), async (device) => {
return {
... device,
path: await fs.realpath(device.path)
};
});
});
return async function (selectors) {
let blockDeviceTree = await lsblkOnce();
return selectors.map((selector) => {
if (selector === All) {
return blockDeviceTree;
} else {
return treeFind(blockDeviceTree, makePredicate(selector));
}
});
};
};

@ -9,7 +9,7 @@ const lvm = require("../exec-lvm");
const All = require("../graphql-interface/symbols/all");
module.exports = {
name: "LVM",
name: "sysquery.lvm",
makeContext: function () {
return {
physicalVolumes: new DataLoader(evaluateAndPick({

@ -0,0 +1,53 @@
"use strict";
const isIterable = require("is-iterable");
const mapAsync = require("../map-async");
const Omitted = Symbol("Omitted");
async function testValue(value, predicate) {
return (await predicate(value))
? value
: Omitted;
}
module.exports = async function treeFilterFlatAsync(tree, filter, options = {}) {
let key = options.key ?? "children";
let recurseFilteredSubtrees = options.recurseFilteredSubtrees ?? false;
let concurrent = options.concurrent ?? true;
let depthFirst = options.depthFirst ?? false; // NOTE: Only affects output order, not evaluation order
let results = [];
async function step(subtree) {
// NOTE: The reason that we're tracking promises rather than outcomes, is because we want to make sure that (in breadth-first mode) we *immediately* push it into the results array, before the first async yield point. This is to ensure that the output is always in a consistent breadth-first order, even if the evaluation itself (being async) is out-of-order. This array of promises is resolved and post-processed at the end.
// TODO: Consider rewriting this so that the step function *returns* a (correctly-ordered) array instead; however, this would require possibly a lot of array concatenations, which may negatively impact performance
let resultPromise = await testValue(subtree, filter);
if (!depthFirst) {
results.push(resultPromise);
}
if (recurseFilteredSubtrees || (await resultPromise) !== Omitted) {
if (isIterable(subtree[key])) {
await mapAsync(subtree[key], step, { concurrent: concurrent });
} else if (subtree[key] != null && typeof subtree[key] === "object") {
await step(subtree[key]);
}
}
if (depthFirst) {
results.push(resultPromise);
}
return resultPromise;
}
if (isIterable(tree)) {
await mapAsync(tree, step, { concurrent: concurrent });
} else {
await step(tree);
}
return results.filter((value) => value !== Omitted);
};

@ -0,0 +1,39 @@
"use strict";
// FIXME: Untested
const isIterable = require("is-iterable");
const forEach = require("../for-each");
module.exports = function treeFilterFlat(tree, filter, options = {}) {
let key = options.key ?? "children";
let recurseFilteredSubtrees = options.recurseFilteredSubtrees ?? false;
let results = [];
function step(subtree) {
let matchesFilter = filter(subtree);
if (matchesFilter || recurseFilteredSubtrees) {
if (isIterable(subtree[key])) {
for (let item of subtree[key]) {
step(item);
}
} else if (subtree[key] != null && typeof subtree[key] === "object") {
step(subtree[key]);
}
}
if (matchesFilter) {
results.push(subtree);
}
}
if (isIterable(tree)) {
forEach(tree, step);
} else {
step(tree);
}
return results;
};

@ -18,5 +18,5 @@ let tree = [{
name: "b"
}];
console.log(findInTree({ tree, predicate: (item) => item.name === "a2" }));
console.log(findInTree({ tree, predicate: (item) => item.name === "nonexistent" }));
console.log(findInTree(tree, (item) => item.name === "a2"));
console.log(findInTree(tree, (item) => item.name === "nonexistent"));

@ -0,0 +1,43 @@
"use strict";
const isIterable = require("is-iterable");
const firstValue = require("../first-value");
const { validateArguments } = require("@validatem/core");
const required = require("@validatem/required");
const isFunction = require("@validatem/is-function");
const isString = require("@validatem/is-string");
const defaultTo = require("@validatem/default-to");
module.exports = function findInTree(_tree, _predicate, _options) {
let [ tree, predicate, { key } ] = validateArguments(arguments, {
tree: [ required ], // TODO: Make stricter?
predicate: [ required, isFunction ],
options: [ defaultTo({}), {
key: [ defaultTo("children", isString) ]
}]
});
function step(subtree) {
if (predicate(subtree)) {
return subtree;
} else {
let nextChild = subtree[key];
if (isIterable(nextChild)) {
return firstValue(nextChild, step);
} else if (nextChild != null && typeof nextChild === "object") {
return step(nextChild);
} else {
// TODO: Should an explicit sentinel value be used for this?
return undefined;
}
}
}
let finalResult = isIterable(tree)
? firstValue(tree, step)
: step(tree);
return finalResult;
};

@ -1,21 +1,29 @@
"use strict";
const Promise = require("bluebird");
const treecutter = require("../treecutter");
const isIterable = require("is-iterable");
const mapAsync = require("../map-async");
module.exports = function treeMapAsync(tree, mapper, returnBoth = false) {
return Promise.map(treecutter.flatten(tree), (item) => {
return mapper(item);
}).then((items) => {
let newTree = treecutter.rebuild(items);
module.exports = async function treeMapAsync(tree, mapper, options = {}) {
let key = options.key ?? "children";
if (returnBoth) {
return {
tree: newTree,
list: items
};
} else {
return newTree;
async function step(subtree) {
let mapped = await mapper(subtree);
let modifiedProperties = {};
if (isIterable(mapped[key])) {
modifiedProperties[key] = await mapAsync(mapped[key], step);
} else if (mapped[key] != null && typeof mapped[key] === "object") {
modifiedProperties[key] = await step(mapped[key]);
}
});
// We track modified properties separately and (immutably) merge them at the end, because it's the fastest way to ensure that we don't mutate the input object under any circumstances
return {
... mapped,
... modifiedProperties
};
}
return isIterable(tree)
? mapAsync(tree, step)
: step(tree);
};

@ -0,0 +1,23 @@
"use strict";
// TODO: Replace this with an implementation that doesn't use treecutter? As treecutter is pretty hacky
const Promise = require("bluebird");
const treecutter = require("../treecutter");
module.exports = function treeMapAsync(tree, mapper, returnBoth = false) {
return Promise.map(treecutter.flatten(tree), (item) => {
return mapper(item);
}).then((items) => {
let newTree = treecutter.rebuild(items);
if (returnBoth) {
return {
tree: newTree,
list: items
};
} else {
return newTree;
}
});
};

@ -0,0 +1,31 @@
"use strict";
const isIterable = require("is-iterable");
const map = require("../map");
// FIXME: Untested
module.exports = function treeMap(tree, mapper, options = {}) {
let key = options.key ?? "children";
function step(subtree) {
let mapped = mapper(subtree);
let modifiedProperties = {};
if (isIterable(mapped[key])) {
modifiedProperties[key] = mapped[key].map((item) => step(item));
} else if (mapped[key] != null && typeof mapped[key] === "object") {
modifiedProperties[key] = step(mapped[key]);
}
// We track modified properties separately and (immutably) merge them at the end, because it's the fastest way to ensure that we don't mutate the input object under any circumstances (even after we add multi-key support)
return {
... mapped,
... modifiedProperties
};
}
return isIterable(tree)
? map(tree, step)
: step(tree);
};

@ -39,16 +39,37 @@ const createAPI = require("../src/api");
// }
// };
// const query = {
// resources: {
// lvm: {
// createPhysicalVolume: {
// $arguments: {
// path: "/dev/loop3"
// },
// path: true,
// totalSpace: true,
// freeSpace: true
// }
// }
// }
// };
const query = {
resources: {
lvm: {
createPhysicalVolume: {
$arguments: {
path: "/dev/loop3"
},
path: true,
blockDevices: {
// $arguments: { names: ["sdb"] },
name: true,
path: true,
mounts: {
mountpoint: true,
filesystem: true,
totalSpace: true,
freeSpace: true
// children: {
// $recurse: true
// }
},
children: {
$recurse: true
}
}
}

Loading…
Cancel
Save