Compare commits

...

6 Commits

2
.gitignore vendored

@ -4,3 +4,5 @@ images
disks
yarn-error.log
junk
virtual-disks/vdisk1
virtual-disks/vdisk2

@ -0,0 +1,4 @@
root
hardware
blockDevices

@ -12,11 +12,12 @@
"url": "git@git.cryto.net:cvm"
},
"author": "Sven Slootweg",
"license": "WTFPL",
"license": "WTFPL OR CC0-1.0",
"dependencies": {
"@babel/register": "^7.8.3",
"@invisible/pegjs-import": "^1.1.1",
"@joepie91/express-react-views": "^1.0.1",
"@joepie91/result": "^0.1.0",
"@joepie91/unreachable": "^1.0.0",
"@validatem/allow-extra-properties": "^0.1.0",
"@validatem/anything": "^0.1.0",
@ -57,6 +58,7 @@
"debounce": "^1.0.0",
"debug": "^4.1.1",
"default-value": "^1.0.0",
"dlayer": "^0.1.2",
"dotty": "^0.1.0",
"end-of-stream": "^1.1.0",
"entities": "^2.0.0",
@ -78,6 +80,7 @@
"map-obj": "^4.2.1",
"match-value": "^1.1.0",
"memoizee": "^0.4.14",
"merge-by-template": "^0.1.4",
"nanoid": "^2.1.11",
"object.fromentries": "^2.0.2",
"pegjs": "^0.10.0",

@ -139,4 +139,30 @@ table.drives th.unknown {
color: gray;
}
.volumeGroup {
max-width: 960px;
display: grid;
grid-template-columns: auto 1fr;
align-items: center;
gap: 0.2em 0.4em;
}
.bar {
border: 1px solid black;
display: grid;
}
.barSegment {
box-sizing: border-box;
font-size: 0.8em;
background-color: rgb(205, 205, 205);
padding: 0.3em 0.4em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.barSegment:nth-child(even) {
background-color: rgb(177, 177, 177);
}
/*# sourceMappingURL=style.css.map */

@ -1 +1 @@
{"version":3,"sourceRoot":"","sources":["../../src/scss/style.scss"],"names":[],"mappings":"AAIA;EACC,kBALqB;EAMrB;EACA;;;AAGD;EACC;;;AAOD;EACC;;;AAGD;EACC,kBAtBqB;;AAwBrB;EACC;EACA;;AAGD;EACC;;AAIA;EACC;EACA;EACA;;AAIA;AAEC;EACA,kBA3CqB;EA4CrB;;AAKD;EACC;EACA;;;AAMJ;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA;;AAEA;EACC;EACA;EACA;EACA;;AAEA;EACC;EACA;;AAKD;EACC,kBArFmB;EAsFnB;EACA;;;AAKH;EACC;;AAEA;EACC;EACA;;AAGD;EACC;;AAGD;EACC;;;AAKD;EACC;;AAIA;EACC;;AAGD;EACC;;AAGD;EACC;;AAGD;EACC;;AAKD;EACC;;AAIF;EACC;EACA;;AAEA;EACC;;AAGD;EACC;;AAIF;EACC;;AAEA;EACC;;AAKD;EACC;;AAGD;EACC;;AAGD;EACC;;AAGD;EACC;;;AAKH;EACC;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACC","file":"style.css"}
{"version":3,"sourceRoot":"","sources":["../../src/scss/style.scss"],"names":[],"mappings":"AAIA;EACC,kBALqB;EAMrB;EACA;;;AAGD;EACC;;;AAOD;EACC;;;AAGD;EACC,kBAtBqB;;AAwBrB;EACC;EACA;;AAGD;EACC;;AAIA;EACC;EACA;EACA;;AAIA;AAEC;EACA,kBA3CqB;EA4CrB;;AAKD;EACC;EACA;;;AAMJ;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA;;AAEA;EACC;EACA;EACA;EACA;;AAEA;EACC;EACA;;AAKD;EACC,kBArFmB;EAsFnB;EACA;;;AAKH;EACC;;AAEA;EACC;EACA;;AAGD;EACC;;AAGD;EACC;;;AAKD;EACC;;AAIA;EACC;;AAGD;EACC;;AAGD;EACC;;AAGD;EACC;;AAKD;EACC;;AAIF;EACC;EACA;;AAEA;EACC;;AAGD;EACC;;AAIF;EACC;;AAEA;EACC;;AAKD;EACC;;AAGD;EACC;;AAGD;EACC;;AAGD;EACC;;;AAKH;EACC;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACC;;;AAIF;EACC;EACA;EACA;EAEA;EACA;;;AAGD;EACC;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACC","file":"style.css"}

@ -3,36 +3,26 @@
const Promise = require("bluebird");
const memoizee = require("memoizee");
const fs = Promise.promisifyAll(require("fs"));
const treecutter = require("../../packages/treecutter");
const findmnt = require("../../packages/exec-findmnt");
const shallowMerge = require("../../packages/shallow-merge");
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 treecutter.flatten(mounts);
}).map((mount) => {
if (mount.sourceDevice?.startsWith("/")) {
return Promise.try(() => {
return fs.realpathAsync(mount.sourceDevice);
}).then((actualSourcePath) => {
return shallowMerge(mount, {
sourceDevice: actualSourcePath
});
});
} else {
return mount;
}
}).then((list) => {
let tree = treecutter.rebuild(list);
return {
tree: tree,
list: list
};
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);
});
});

@ -1,7 +1,6 @@
"use strict";
const Promise = require("bluebird");
const memoizee = require("memoizee");
const DataLoader = require("dataloader");
const mapObj = require("map-obj");
@ -10,31 +9,7 @@ const All = require("../../packages/graphql-interface/symbols/all");
const nvmeCLI = require("../../packages/exec-nvme-cli");
const smartctl = require("../../packages/exec-smartctl");
const dlayerWrap = require("../../packages/dlayer-wrap");
// This generates a (memoized) source function for commands that always produce an entire list, that needs to be filtered for the desired item(s)
function makeListCommand({ command, selectResult, selectID }) {
let commandOnce = memoizee(command);
return function (ids) {
return Promise.try(() => {
return commandOnce();
}).then((result) => {
if (selectResult != null) {
return selectResult(result);
} else {
return result;
}
}).then((items) => {
return ids.map((id) => {
if (id === All) {
return items;
} else {
return items.find((item) => selectID(item) === id);
}
});
});
};
}
const evaluateAndPick = require("../../packages/evaluate-and-pick");
function makeSingleCommand({ command, selectResult }) {
return function (ids) {
@ -57,28 +32,28 @@ function makeSingleCommand({ command, selectResult }) {
module.exports = function createSources() {
let sources = {
lvmLogicalVolumes: makeListCommand({
command: lvm.getLogicalVolumes,
selectResult: (result) => result.volumes,
selectID: (volume) => volume.path
}),
lvmPhysicalVolumes: makeListCommand({
command: lvm.getPhysicalVolumes,
selectResult: (result) => result.volumes,
selectID: (device) => device.path
}),
lvmVolumeGroups: makeListCommand({
command: lvm.getVolumeGroups,
selectResult: (result) => result.groups,
selectID: (group) => group.name
}),
// 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 })
}),
nvmeListNamespaces: makeSingleCommand({
command: (path) => nvmeCLI.listNamespaces({ devicePath: path })
}),
smartctlScan: makeListCommand({
smartctlScan: evaluateAndPick({
command: smartctl.scan,
selectID: (device) => device.path
}),
@ -94,6 +69,7 @@ module.exports = function createSources() {
}),
};
// TODO: Consider moving these to be inline as well, somehow
let factoryModules = {
lsblk: require("./lsblk")(),
findmnt: require("./findmnt")()

@ -7,29 +7,20 @@ 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 treecutter = require("../../packages/treecutter");
const findInTree = require("../../packages/find-in-tree");
const shallowMerge = require("../../packages/shallow-merge");
const treeMapAsync = require("../../packages/tree-map-async");
module.exports = function () {
let lsblkOnce = memoizee(() => {
return Promise.try(() => {
return lsblk();
}).then((tree) => {
return treecutter.flatten(tree);
}).map((device) => {
return Promise.try(() => {
return fs.realpathAsync(device.path);
}).then((actualPath) => {
return shallowMerge(device, {
path: actualPath
});
});
}).then((devices) => {
return {
tree: treecutter.rebuild(devices),
list: devices
};
return treeMapAsync(tree, async (device) => {
return {
... device,
path: await fs.realpathAsync(device.path)
};
}, true);
});
});

@ -1,27 +0,0 @@
"use strict";
const Promise = require("bluebird");
const memoizee = require("memoizee");
const lvm = require("../../../packages/exec-lvm");
const All = require("../../../packages/graphql-interface/symbols/all");
module.exports = function () {
let getLogicalVolumesOnce = memoizee(lvm.getLogicalVolumes);
return function (paths) {
return Promise.try(() => {
return getLogicalVolumesOnce();
}).then((result) => {
return result.volumes;
}).then((volumes) => {
return paths.map((path) => {
if (path === All) {
return volumes;
} else {
return volumes.find((device) => device.path === path);
}
});
});
};
};

@ -1,27 +0,0 @@
"use strict";
const Promise = require("bluebird");
const memoizee = require("memoizee");
const lvm = require("../../../packages/exec-lvm");
const All = require("../../../packages/graphql-interface/symbols/all");
module.exports = function () {
let getPhysicalVolumesOnce = memoizee(lvm.getPhysicalVolumes);
return function (paths) {
return Promise.try(() => {
return getPhysicalVolumesOnce();
}).then((result) => {
return result.volumes;
}).then((volumes) => {
return paths.map((path) => {
if (path === All) {
return volumes;
} else {
return volumes.find((device) => device.path === path);
}
});
});
};
};

@ -1,27 +0,0 @@
"use strict";
const Promise = require("bluebird");
const memoizee = require("memoizee");
const lvm = require("../../../packages/exec-lvm");
const All = require("../../../packages/graphql-interface/symbols/all");
module.exports = function () {
let getVolumeGroupsOnce = memoizee(lvm.getVolumeGroups);
return function (names) {
return Promise.try(() => {
return getVolumeGroupsOnce();
}).then((result) => {
return result.groups;
}).then((groups) => {
return names.map((name) => {
if (name === All) {
return groups;
} else {
return groups.find((group) => group.name === name);
}
});
});
};
};

@ -1,15 +0,0 @@
"use strict";
const Promise = require("bluebird");
const smartctl = require("../../../packages/exec-smartctl");
const dlayerWrap = require("../../../packages/dlayer-wrap");
module.exports = function () {
return function (paths) {
return Promise.map(paths, (path) => {
return dlayerWrap(() => smartctl.attributes({ devicePath: path }), {
allowedErrors: [ smartctl.AttributesError ]
});
});
};
};

@ -1,15 +0,0 @@
"use strict";
const Promise = require("bluebird");
const dlayerWrap = require("../../../packages/dlayer-wrap");
const smartctl = require("../../../packages/exec-smartctl");
module.exports = function () {
return function (paths) {
return Promise.map(paths, (path) => {
return dlayerWrap(() => smartctl.info({ devicePath: path }), {
allowedErrors: [ smartctl.InfoError ]
});
});
};
};

@ -1,25 +0,0 @@
"use strict";
const Promise = require("bluebird");
const memoizee = require("memoizee");
const smartctl = require("../../../packages/exec-smartctl");
const All = require("../../../packages/graphql-interface/symbols/all");
module.exports = function () {
let scanOnce = memoizee(smartctl.scan);
return function (paths) {
return Promise.try(() => {
return scanOnce();
}).then((devices) => {
return paths.map((path) => {
if (path === All) {
return devices;
} else {
return devices.find((device) => device.path === path);
}
});
});
};
};

@ -1,10 +1,16 @@
"use strict";
const Promise = require("bluebird");
const dlayer = require("../packages/dlayer");
const dlayer = require("dlayer");
const All = require("../packages/graphql-interface/symbols/all");
const loaders = require("./data-sources");
const types = require("./types");
const execLVM = require("../packages/exec-lvm");
const { validateValue } = require("@validatem/core");
const isString = require("@validatem/is-string");
const isBoolean = require("@validatem/is-boolean");
const required = require("@validatem/required");
const defaultTo = require("@validatem/default-to");
function typeFromSource(source, ids, factoryFunction) {
return Promise.try(() => {
@ -25,6 +31,9 @@ module.exports = function () {
sources: loaders()
};
},
modules: [
require("../packages/sysquery-lvm")
],
schema: {
hardware: {
drives: ({ paths }, { sources }) => {
@ -35,17 +44,6 @@ module.exports = function () {
blockDevices: ({ names }, { sources }) => {
return typeFromSource(sources.lsblk, names, (device) => types.BlockDevice({ name: device.name }));
},
lvm: {
physicalVolumes: {
$get: ({ paths }, { sources }) => {
return typeFromSource(sources.lvmPhysicalVolumes, paths, (volume) => types.LVMPhysicalVolume({ path: volume.path }));
},
$methods: {}
},
volumeGroups: ({ names }, { sources }) => {
return typeFromSource(sources.lvmVolumeGroups, names, (group) => types.LVMVolumeGroup({ name: group.name }));
}
},
images: {
installationMedia: [],
vmImages: []

@ -4,7 +4,4 @@ Object.assign(module.exports, {
Drive: require("./drive"),
BlockDevice: require("./block-device"),
Mount: require("./mount"),
LVMPhysicalVolume: require("./lvm/physical-volume"),
LVMVolumeGroup: require("./lvm/volume-group"),
LVMLogicalVolume: require("./lvm/logical-volume"),
});

@ -13,9 +13,11 @@ module.exports = function LVMPhysicalVolume ({ path }) {
format: "format",
totalSpace: "totalSpace",
freeSpace: "freeSpace",
status: "status",
isExported: "isExported",
isMissing: "isMissing",
isAllocatable: "isAllocatable",
isDuplicate: "isDuplicate",
isUsed: "isUsed",
volumeGroup: (volume) => types.LVMVolumeGroup({ name: volume.volumeGroup })
}
}

@ -39,6 +39,7 @@ module.exports = function () {
return api.query(template.query, queryArguments);
}
}).then((result) => {
console.dir({ templateInput: result }, { depth: null });
if (result == null) {
return {};
} else {

@ -3,7 +3,7 @@
const Promise = require("bluebird");
const syncpipe = require("syncpipe");
const util = require("util");
const Result = require("../result");
const Result = require("@joepie91/result");
const ID = Symbol("dlayer-source object ID");
const AllowErrors = Symbol("dlayer-source allow-errors marker");
@ -22,13 +22,13 @@ module.exports = {
let getter = function (_args, context) {
return Promise.try(() => {
if (properties[ID] != null) {
let dataSource = context.sources[source];
let dataSource = context[source];
if (dataSource != null) {
// console.log(`Calling source '${source}' with ID ${util.inspect(properties[ID])}`);
return Result.wrapAsync(() => dataSource.load(properties[ID]));
} else {
throw new Error(`Attempted to read from source '${source}', but no such source is registered`);
throw new Error(`Attempted to read from context property '${source}', but no such property exists`);
}
} else {
// FIXME: Better error message
@ -41,7 +41,7 @@ module.exports = {
// TODO: How to deal with null results? Allow them or not? Make it an option?
if (result.isOK && result.value() == null) {
// TODO: Change implementation to allow `Result.ok(null|undefined)` but not `null|undefined` directly?
throw new Error(`Null-ish result returned for ID ${util.inspect(properties[ID])} from source '${source}'; this is not allowed, and there is probably a bug in your code. Please file a ticket if you have a good usecase for null-ish results!`);
throw new Error(`Null-ish result returned for ID ${util.inspect(properties[ID])} from source at context property '${source}'; this is not allowed, and there is probably a bug in your code. Please file a ticket if you have a good usecase for null-ish results!`);
} else if (properties[AllowErrors] === true && typeof selector !== "string") {
// Custom selectors always receive the Result as-is
return selector(result);
@ -57,7 +57,7 @@ module.exports = {
if (typeof selector === "string") {
return result.value()[selector];
} else {
return selector(result.value());
return selector(result.value(), context);
}
}
});

@ -1,8 +1,8 @@
"use strict";
const Promise = require("bluebird");
const dlayer = require("../dlayer");
const Result = require("../result");
const dlayer = require("dlayer");
const Result = require("@joepie91/result");
module.exports = function dlayerWrap(callback, options = {}) {
return Promise.try(() => {

@ -1,45 +0,0 @@
"use strict";
// Simple data type to represent a query path and corresponding schema path tied together, because these are basically always used together, and it would bloat up the implementation code otherwise
function createInstance({ queryPath, schemaPath, queryObject, schemaObject }) {
return {
queryPath: queryPath,
schemaPath: schemaPath,
query: queryObject,
schema: schemaObject,
child: function (queryKey, schemaKey, { queryOverride, schemaOverride } = {}) {
return createInstance({
queryPath: (queryKey != null)
? queryPath.concat([ queryKey ])
: queryPath,
schemaPath: (schemaKey != null)
? schemaPath.concat([ schemaKey ])
: schemaPath,
queryObject: queryOverride ?? queryObject[queryKey],
schemaObject: schemaOverride ?? schemaObject[schemaKey]
});
},
toPathString: function () {
return queryPath
.map((segment, i) => {
if (segment === schemaPath[i]) {
return segment;
} else {
// This is used for representing aliases, showing the original schema key in brackets
return `${segment} [${schemaPath[i]}]`;
}
})
.join(" -> ");
}
};
}
module.exports = function createCursor({ query, schema }) {
return createInstance({
queryPath: [],
schemaPath: [],
queryObject: query,
schemaObject: schema
});
};

@ -1,46 +0,0 @@
```js
{
system: {
metrics: {
loadAverage: true
},
hardware: {
drives: {
path: true,
size: true
}
},
lvm: {
physicalVolumes: {
$collection: {
$$create: { path: "/dev/sda1" }
},
path: true
$$update: { enabled: true }
}
}
}
}
```
# Special schema keys
- `$anyKey`: specifies a wildcard/fallback handler, that will handle *any* property specified in the query that isn't explicitly specified in the schema. Use sparingly, as it poses API design/maintenance hazards; it will be difficult to add new explicit keys later on without breaking existing queries.
- `$mutations`: used to specify mutation methods that the user can call on objects. Specified as an object that maps mutation names to their corresponding functions.
- `{ $get, $mutations }`: specified as an object in place of where normally a function or literal value would be expected to be returned; specifically meant for collections where you want to both allow the collection to be fetched, *and* for the user to specify collection-related mutations (eg. "create new item") within it in a query. This special syntax exists because collections/lists are "transparent" in dlayer and there is no way to specify collection-level behaviour otherwise. `$mutations` works the same as in its standalone version.
# Special query keys
- `$recurse`: set to `true` if you want the query to recursively traverse nested objects; eg. `... children: { $recurse: true } ...`. The recursive query will be for all the keys of the *parent* object, as well as any additional properties specified adjacent to the `$recurse` key. The key under which this special object is specified, determines what key will be assumed to contain the recursive children.
- `$recurseLimit` (default `10`): how many levels of depth the recursion may continue for until being cut off. reaching the limit does not fail the query; it merely stops recursing any further.
- `$allowErrors`: this property may yield an error upon evaluation *without* failing the query as a whole. instead of returning the value for the property directly, a Result object will be produced that represents either the success value or the error that was encountered. Your code still needs to handle the error case in some way. Typically useful when components of the query are expected to fail due to external circumstances that the requesting user cannot control (eg. a dependency on another network service).
- `$arguments`: used to specify an object of named arguments for either a property or a mutation. Optional for properties; required for mutations (even if left empty).
- `$key`: used to override what schema key to fetch; it will attempt to access the specified key, instead of assuming that the schema key equals the key in the query. Typically used to either alias properties (using a different name in the response than how it is defined in the schema), or to access the same property multiple times with different arguments (eg. filters) and expose the results as different keys.
- `$collection`: used to invoke mutations on a (transparent) collection instead of its members
# Special context keys
These keys can be used within property handlers.
- `$getProperty(object, property)`: evaluate/fetch a different property on the object, and return it (in a Promise). You can reference `this` within the property handler as the `object` to evaluate a property of the object you're currently working with.
- `$getPropertyPath(object, propertyPath)`: the same as above, but for a property *path* represented as an array of property names (or a dotpath string).

@ -1,249 +0,0 @@
"use strict";
const Promise = require("bluebird");
const mapObject = require("map-obj");
const Result = require("../result");
const createCursor = require("./cursor");
// TODO: Bounded/unbounded recursion
// TODO: context
// TODO: $required query predicate
// TODO: Lazy query objects, which get initialized by calling a function that gets the parent object as an argument? This would not be serializable over the network!
// FIXME: $getProperty, $getPropertyPath, maybe $resolveObject/$query?
// FIXME: Allow setting an evaluation depth limit for queries, to limit eg. recursion
// FIXME: recurseDepth, recurseLabel/recurseGoto
/* Recursion design:
When setting `$recurse: true` on a child property, the parent schema gets duplicated with the child schema merged into it, and the resulting combined schema is used for the recursive fetching. Because the child schema can explicitly set properties to false, this allows for both "fetch in parent but not in recursed children" cases (true in parent, false in child) and "fetch in recursed children but not in parent" cases (unspecified or false in parent, true in child).
The schema merging will eventually become deep-merging, when multi-level recursion is implemented (ie. the possibility to recurse indirectly).
*/
const specialKeyRegex = /^\$[^\$]/;
function maybeCall(value, args, thisContext) {
return Promise.try(() => {
// FIXME: Only do this for actual fetch requests
let getter = (typeof value === "object" && value != null && value.$get != null)
? value.$get
: value;
if (typeof getter === "function") {
return getter.call(thisContext, ...args);
} else {
return getter;
}
});
}
function isObject(value) {
// FIXME: Replace this with a more sensible check, like is-plain-object
return (value != null && typeof value === "object" && !Array.isArray(value));
}
// TODO: Move to separate package, decide whether to keep the nested array detection or not - that should probably just be part of the handler?
function mapMaybeArray(value, handler) {
// NOTE: This is async!
if (Array.isArray(value)) {
return Promise.map(value, (item, i) => {
if (Array.isArray(item)) {
throw new Error(`Encountered a nested array, which is not allowed; maybe you forgot to flatten it?`);
} else {
return handler(item, i);
}
});
} else {
return handler(value);
}
}
/* Possible values of a schema property:
true, null, object with only special keys (but not $recurse) -- fetch value and return it as-is
false -- do not fetch value at all
object with $recurse -- recursively fetch, optionally with extra keys to fetch (or ignore) for recursed children only, inheriting the rest of the schema from the parent
object with regular non-special keys -- fetch object and continue fetching into child properties according to the schema
a "special key" is any key that is prefixed with $ - they are used to provide additional parameters to dlayer, and cannot be used for business logic keys
*/
function asyncMapObject(object, handler) {
return Promise.props(mapObject(object, handler));
}
function analyzeSubquery(subquery) {
let isRecursive = (subquery?.$recurse === true);
let allowErrors = (subquery?.$allowErrors === true);
let hasChildKeys = isObject(subquery) && Object.keys(subquery).some((key) => !specialKeyRegex.test(key));
let isLeaf = (subquery === true || subquery === null || (!hasChildKeys && !isRecursive));
let args = subquery?.$arguments ?? {};
return { isRecursive, allowErrors, hasChildKeys, isLeaf, args };
}
function analyzeQueryKey(cursor, queryKey) {
let childCursor = cursor.child(queryKey, null);
let schemaKey = childCursor.query?.$key ?? queryKey; // $key is for handling aliases
let handler = cursor.child(queryKey, schemaKey).schema ?? cursor.schema.$anyKey;
// TODO: Maybe clean up all the .child stuff here by moving the `$key` logic into the cursor implementation instead, as it seems like nothing else in dlayer needs to care about the aliasing
return {
... analyzeSubquery(childCursor.query),
schemaKey: schemaKey,
handler: handler
};
}
function assignErrorPath(error, cursor) {
if (error.path == null) {
// Only assign the path if it hasn't already happened at a deeper level; this is a recursive function after all
error.path = cursor.queryPath;
error.message = error.message + ` (${cursor.toPathString()})`;
}
}
// MARKER: build a sample todo-list schema for testing out fetches, mutations, and combinations of them, including on collections
function evaluate(cursor, context) {
// map query object -> result object
return asyncMapObject(cursor.query, (queryKey, subquery) => {
let shouldFetch = (subquery !== false);
if (!shouldFetch || specialKeyRegex.test(queryKey)) {
// When constructing the result object, we only care about the 'real' keys, not about special meta-keys like $key; those get processed in the actual resolution logic itself.
return mapObject.mapObjectSkip;
} else {
let { schemaKey, handler, args, isRecursive, allowErrors, isLeaf } = analyzeQueryKey(cursor, queryKey);
if (handler != null) {
let promise = Promise.try(() => {
// This calls the data provider in the schema
return Result.wrapAsync(() => maybeCall(handler, [ args, context ], cursor.schema));
}).then((result) => {
if (result.isOK) {
let value = result.value();
return Promise.try(() => {
if (!isLeaf && value != null) {
let effectiveSubquery = (isRecursive)
? { ... cursor.query, ... subquery }
: subquery;
return mapMaybeArray(value, (item, i) => {
// NOTE: We're adding `i` to the query path for user feedback purposes, but we're not *actually* diving down into that property on the query object; the queryOverride doesn't just handle recursion, it also ensures that the 'original' subquery is passed in regardless of what the path suggests
// TODO: schemaOverride here is used to pass in the (asynchronously/lazily) resolved result, which the cursor implementation wouldn't have access to otherwise; need to somehow make it clearer in the API design that the automatic 'schema navigation' is only used for simple objects - maybe not call it an 'override' but instead just something like newSchema and newQuery?
let subCursor = (i != null)
? cursor
.child(queryKey, schemaKey)
.child(i, i, {
queryOverride: effectiveSubquery,
schemaOverride: item
})
: cursor
.child(queryKey, schemaKey, {
queryOverride: effectiveSubquery,
schemaOverride: item
});
return evaluate(subCursor, context);
});
} else {
// null / undefined are returned as-is, so are leaves
return value;
}
}).then((evaluated) => {
// FIXME: Verify that this is still necessary here
if (allowErrors) {
return Result.ok(evaluated);
} else {
return evaluated;
}
});
} else {
let error = result.error();
if (error.__dlayerAcceptableError === true) {
if (allowErrors === true) {
return Result.error(error.inner);
} else {
throw error.inner;
}
} else {
throw error;
}
}
}).tapCatch((error) => {
// FIXME: Chain properly
assignErrorPath(error, cursor.child(queryKey, schemaKey));
});
return [ queryKey, promise ];
} else {
throw new Error(`No key '${schemaKey}' exists in the schema`);
}
}
});
}
module.exports = function createDLayer(options) {
// options = { schema, makeContext }
return {
query: function (query, context) {
let generatedContext = (options.makeContext != null)
? options.makeContext()
: {};
function getProperty(object, property, args = {}) {
// FIXME: Validatem
if (object == null) {
throw new Error(`Empty object passed`);
}
if (property in object) {
return maybeCall(object[property], [ args, combinedContext ], object);
} else {
// FIXME: Better error message with path
throw new Error(`No key '${property}' exists in the schema`);
}
}
let combinedContext = {
... generatedContext,
... context,
// FIXME: Figure out a way to annotate errors here with the path at which they occurred, *and* make clear that it was an internal property lookup
$getProperty: getProperty,
$getPropertyPath: function (object, propertyPath) {
let parsedPath = (typeof propertyPath === "string")
? propertyPath.split(".")
: propertyPath;
return Promise.reduce(parsedPath, (currentObject, pathSegment) => {
if (currentObject != null) {
return getProperty(currentObject, pathSegment);
} else {
// Effectively null-coalescing
return null;
}
}, object);
}
};
let cursor = createCursor({
query: query,
schema: options.schema
});
// FIXME: Currently, top-level errors do not get a path property assigned to them, because that assignment happens on nested calls above
return evaluate(cursor, combinedContext);
}
};
};
module.exports.markAcceptableError = function (error) {
return {
__dlayerAcceptableError: true,
inner: error
};
};

@ -1,4 +0,0 @@
TODO:
- $call, for calling non-idempotent functions, requiring a (potentially empty) list of arguments
- $repeat modifier, accepting an array of attributes to repeat the given attribute/function with, the results are an array in the same order - share the top-level properties among all of them
- for named repeats, the user can use the alias feature instead? though no way to share properties in that case

@ -1,48 +0,0 @@
"use strict";
const Promise = require("bluebird");
const dlayer = require("./");
const loaders = require("../../api/data-sources");
let schema = {
hardware: {
drives: function () {
return [{
name: "foo",
size: () => "4 GiB"
}, {
name: "bar",
size: () => "2 TiB"
}];
},
primaryNetworkInterface: function () {
return {
name: "baz",
dataRate: () => "2.5 gbps"
};
}
}
};
let api = dlayer({
schema: schema
});
return Promise.try(() => {
return api.query({
hardware: {
drives: {
name: true,
size: true
},
primaryNetworkInterface: {
name: true
}
}
});
}).then((result) => {
console.dir(result, {depth: null});
}).catch((error) => {
console.dir("Unhandled error", error);
});

@ -0,0 +1,36 @@
"use strict";
const Promise = require("bluebird");
const memoizee = require("memoizee");
// FIXME: Figure out a reasonable way to make this symbol its own (conflict-free) package, given that it'll be used all across both sysquery and CVM
const All = require("../graphql-interface/symbols/all");
// This generates a (memoized) source function for commands that always produce an entire list, that needs to be filtered for the desired item(s)
module.exports = function evaluateAndPick({ command, selectResult, selectID, many }) {
let commandOnce = memoizee(command);
return function (ids) {
return Promise.try(() => {
return commandOnce();
}).then((result) => {
if (selectResult != null) {
return selectResult(result);
} else {
return result;
}
}).then((items) => {
return ids.map((id) => {
if (id === All) {
return items;
} else if (many === true) {
// NOTE: This produces nested arrays! One array for each input ID.
return items.filter((item) => selectID(item) === id);
} else {
// TODO: Can this be more performant? Currently it is a nested loop
return items.find((item) => selectID(item) === id);
}
});
});
};
};

@ -8,6 +8,7 @@ const unattendedFlags = require("../modifiers/unattended-flags");
const handleDeviceNotFound = require("../modifiers/handle-device-not-found");
const handlePartitionExists = require("../modifiers/handle-partition-exists");
const handleDeviceInUse = require("../modifiers/handle-device-in-use");
const handleDeviceTooSmall = require("../modifiers/handle-device-too-small");
module.exports = function ({ devicePath, force }) {
return Promise.try(() => {
@ -17,6 +18,7 @@ module.exports = function ({ devicePath, force }) {
.withModifier((force === true) ? forceFlags : unattendedFlags)
.withModifier(handleDeviceNotFound(devicePath))
.withModifier(handleDeviceInUse(devicePath))
.withModifier(handleDeviceTooSmall(devicePath))
.withModifier(handlePartitionExists(devicePath, "create a Physical Volume"))
.execute();
}).then((_output) => {

@ -179,6 +179,7 @@ module.exports = function () {
queue: "QUEUE",
"": null
})
// FIXME: Remaining attributes
};
})
};

@ -11,6 +11,9 @@ module.exports = function () {
return Promise.try(() => {
return execBinary("pvs")
.asRoot()
.withFlags({
options: "pv_all,vg_name"
})
.withModifier(asJson((result) => {
return {
volumes: result.report[0].pv.map((volume) => {
@ -21,11 +24,9 @@ module.exports = function () {
// FIXME: These amounts can contain commas depending on locale (eg. https://serverfault.com/a/648302)
totalSpace: parseIECBytes(volume.pv_size),
freeSpace: parseIECBytes(volume.pv_free),
status: mapFlag(volume.pv_attr, 0, {
d: "DUPLICATE",
a: "ALLOCATABLE",
u: "USED"
}),
isAllocatable: (volume.pv_allocatable === "allocatable"),
isDuplicate: (volume.pv_duplicate === "duplicate"),
isUsed: (volume.pv_in_use === "used"),
isExported: mapFlag(volume.pv_attr, 1, {
x: true,
"-": false

@ -10,5 +10,6 @@ module.exports = {
InvalidVolumeGroup: errorChain.create("InvalidVolumeGroup"),
PhysicalVolumeInUse: errorChain.create("PhysicalVolumeInUse"),
DeviceInUse: errorChain.create("PhysicalVolumeInUse"),
IncompatibleDevice: errorChain.create("IncompatibleDevice")
IncompatibleDevice: errorChain.create("IncompatibleDevice"),
DeviceTooSmall: errorChain.create("DeviceTooSmall"),
};

@ -6,11 +6,19 @@ const createRegexParser = require("../../text-parser-regex");
const errors = require("../errors");
module.exports = function (devicePath) {
function makeError() {
return errorResult(new errors.InvalidPath(`Specified device '${devicePath}' does not exist`, {
path: devicePath
}));
}
return function handleDeviceNotFound(command) {
return command.expectOnStderr(createRegexParser(/Device .+ not found\./, () => {
return errorResult(new errors.InvalidPath(`Specified device '${devicePath}' does not exist`, {
path: devicePath
return command
.expectOnStderr(createRegexParser(/Device .+ not found\./, () => {
return makeError();
}))
.expectOnStderr(createRegexParser(/No device found for .+\./, () => {
return makeError();
}));
}));
};
};

@ -0,0 +1,17 @@
"use strict";
const { errorResult } = require("../../text-parser");
const createRegexParser = require("../../text-parser-regex");
const errors = require("../errors");
module.exports = function (devicePath) {
return function handleDeviceTooSmall(command) {
return command
.expectOnStderr(createRegexParser(/Cannot use .+: device is too small \(pv_min_size\)/, () => {
return errorResult(new errors.DeviceTooSmall(`Specified device '${devicePath}' is too small to create a physical volume on`, {
path: devicePath
}));
}));
};
};

@ -80,8 +80,9 @@ module.exports = {
// mtfa,
// hmpre,
// hmmin,
tnvmcap: { name: "totalSpace", transform: (value) => B(value) },
unvmcap: { name: "freeSpace", transform: (value) => B(value) },
// FIXME: The following two values are exported as string (in newer versions of nvme-cli), presumably for precision reasons. Eventually the units package should probably be updated to support bigints, to avoid precision loss in our own code.
tnvmcap: { name: "totalSpace", transform: (value) => B(parseInt(value)) },
unvmcap: { name: "freeSpace", transform: (value) => B(parseInt(value)) },
// TOOD:
// rpmbs,
// edstt,

@ -1,7 +1,6 @@
"use strict";
/* TODO:
toDisplay
conversion between unit scales (eg. IEC -> metric bytes)
ensure NaN is handled correctly
Track the originally-constructed value internally, so that stacked conversions can be done losslessly?

@ -0,0 +1,12 @@
"use strict";
const Promise = require("bluebird");
const All = require("../graphql-interface/symbols/all");
module.exports = async function mapFromSource(source, ids, mapper) {
let results = (ids === All || ids == null)
? await source.load(All)
: await Promise.map(ids, (id) => source.load(id));
return results.map(mapper);
};

@ -1,135 +0,0 @@
"use strict";
const assert = require("assert");
function createResultObject(isSuccessful, containedValue) {
return {
__isResultType: true,
isOK: isSuccessful,
isError: !isSuccessful,
error: function () {
if (!isSuccessful) {
return containedValue;
} else {
// FIXME: Clearer error message, definitely a bug!
throw new Error(`The Result is in a success state`);
}
},
value: function () {
if (isSuccessful) {
return containedValue;
} else {
throw containedValue;
}
},
valueOr: function (defaultValue) {
if (isSuccessful) {
return containedValue;
} else {
return defaultValue;
}
},
mapTo: function ({ ok, error }) {
let okMapper = ok ?? ((value) => Result.ok(value));
let errorMapper = error ?? ((error) => Result.error(error));
if (this.isOK) {
let mapped = okMapper(containedValue);
return (Result.isResult(mapped))
? mapped
: Result.ok(mapped);
} else {
let mapped = errorMapper(containedValue);
return (Result.isResult(mapped))
? mapped
: Result.error(mapped);
}
},
// valueOr: function (errorCode, errorMessage) {
// if (isSuccessful) {
// return containedValue;
// } else {
// // FIXME: Integrate with error-chain somehow?
// let error = new Error(errorMessage);
// error.code = errorCode;
// throw error;
// }
// },
// FIXME: Chaining, Promise chain integration?
// FIXME: Serialization
// FIXME: Chaining with error filtering
};
}
let Result = module.exports = {
isResult: function (value) {
return (value != null && value.__isResultType === true);
},
ok: function (value) {
// Emulate what Promises do on `resolve(...)`
if (this.isResult(value)) {
return value;
} else {
return createResultObject(true, value);
}
},
error: function (error) {
return createResultObject(false, error);
},
wrap: function (callback) {
// Always returns a Result
try {
let result = callback();
return this.ok(result);
} catch (error) {
return this.error(error);
}
},
wrapAsync: function (callback) {
// Always returns a Promise that resolves to a Result
return new Promise((resolve, _reject) => {
resolve(callback());
}).then((result) => {
return this.ok(result);
}).catch((error) => {
return this.error(error);
});
},
// The below methods are used when it's unknown whether something will produce a Result or just return/throw
unwrapValue: function (value) {
if (Result.isResult(value)) {
return value.unwrap();
} else {
return value;
}
},
unwrap: function (callback) {
return Result.unwrapValue(callback());
},
unwrapAsync: function (callback) {
return new Promise((resolve, _reject) => {
resolve(callback());
}).then((result) => {
return Result.unwrapValue(result);
});
}
};
/* IDEA:
result.mapTo({
ok: (value) => value * 2,
error: (error) => chain(error, ErrorType, "Foo Bar")
})
result.mapTo({
// can return either a result or any value
ok: (value) => value * 2,
// can return either a result or an Error
error: (_error) => result.ok(0)
})
*/

@ -0,0 +1,183 @@
"use strict";
const Promise = require("bluebird");
const DataLoader = require("dataloader");
const dlayerSource = require("../dlayer-source");
const evaluateAndPick = require("../evaluate-and-pick");
const mapFromSource = require("../map-from-source");
const lvm = require("../exec-lvm");
const All = require("../graphql-interface/symbols/all");
module.exports = {
name: "LVM",
makeContext: function () {
return {
physicalVolumes: new DataLoader(evaluateAndPick({
command: lvm.getPhysicalVolumes,
selectResult: (result) => result.volumes,
selectID: (device) => device.path
})),
volumeGroups: new DataLoader(evaluateAndPick({
command: lvm.getVolumeGroups,
selectResult: (result) => result.groups,
selectID: (group) => group.name
})),
logicalVolumes: new DataLoader(evaluateAndPick({
command: lvm.getLogicalVolumes,
selectResult: (result) => result.volumes,
selectID: (volume) => volume.path
}))
};
},
root: {
resources: {
lvm: {
physicalVolumes: ({ paths }, { physicalVolumes, $make }) => {
return mapFromSource(physicalVolumes, paths, (volume) => {
return $make("sysquery.lvm.PhysicalVolume", { path: volume.path });
});
},
volumeGroups: ({ names }, { volumeGroups, $make }) => {
return mapFromSource(volumeGroups, names, (group) => {
return $make("sysquery.lvm.VolumeGroup", { name: group.name });
});
},
logicalVolumes: ({ paths }, { logicalVolumes, $make }) => {
// FIXME: Aren't these scoped to a volume group?
return mapFromSource(logicalVolumes, paths, (volume) => {
return $make("sysquery.lvm.LogicalVolume", { path: volume.path });
});
}
}
}
},
extensions: {
"sysquery.core.BlockDevice": {
lvmPhysicalVolume: async function (_, { physicalVolumes, $getProperty, $make }) {
let volume = physicalVolumes.get(await $getProperty(this, "path"));
if (volume != null) {
return $make("sysquery.lvm.PhysicalVolume", { path: volume.path });
} else {
return null;
}
}
}
},
types: {
"sysquery.lvm.PhysicalVolume": function PhysicalVolume({ path }) {
return dlayerSource.withSources({
$sources: {
physicalVolumes: {
[dlayerSource.ID]: path,
path: "path",
format: "format",
totalSpace: "totalSpace",
freeSpace: "freeSpace",
isExported: "isExported",
isMissing: "isMissing",
isAllocatable: "isAllocatable",
isDuplicate: "isDuplicate",
isUsed: "isUsed",
volumeGroup: (volume, { $make }) => {
return $make("sysquery.lvm.VolumeGroup", { name: volume.volumeGroup });
}
}
}
});
},
"sysquery.lvm.VolumeGroup": function VolumeGroup({ name }) {
return dlayerSource.withSources({
physicalVolumes: function (_args, { physicalVolumes, $make }) {
return Promise.try(() => {
return physicalVolumes.load(All);
}).filter((volume) => {
return (volume.volumeGroup === name);
}).map((volume) => {
return $make("sysquery.lvm.PhysicalVolume", { path: volume.path });
});
},
logicalVolumes: function (_args, { logicalVolumes, $make }) {
return Promise.try(() => {
return logicalVolumes.load(All);
}).filter((volume) => {
return (volume.volumeGroup === name);
}).map((volume) => {
return $make("sysquery.lvm.LogicalVolume", { path: volume.path });
});
},
$sources: {
volumeGroups: {
[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"
}
}
});
},
"sysquery.lvm.LogicalVolume": function LogicalVolume({ path }) {
return dlayerSource.withSources({
$sources: {
logicalVolumes: {
[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, { $make }) => {
return $make("sysquery.lvm.VolumeGroup", { name: volume.volumeGroup });
}
}
}
});
}
}
};

@ -0,0 +1,21 @@
"use strict";
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;
}
});
};

@ -189,3 +189,31 @@ table.drives {
color: gray;
}
}
.volumeGroup {
max-width: 960px;
display: grid;
grid-template-columns: auto 1fr;
align-items: center;
gap: .2em .4em;
}
.bar {
border: 1px solid black;
display: grid;
}
.barSegment {
box-sizing: border-box;
font-size: .8em;
background-color: rgb(205, 205, 205);
padding: .3em .4em;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&:nth-child(even) {
background-color: rgb(177, 177, 177);
}
}

@ -18,7 +18,9 @@ module.exports = {
format: true,
totalSpace: true,
freeSpace: true,
status: true
isAllocatable: true,
isUsed: true,
isDuplicate: true,
},
logicalVolumes: {
name: true,
@ -35,8 +37,59 @@ module.exports = {
}
},
template: function ({ data }) {
function SegmentBar({ items }) {
let columns = items
.map((item) => `${item.value}fr`)
.join(" ");
return (
<div className="bar" style={{ gridTemplateColumns: columns }}>
{items.map((item) => {
return (
<div key={item.key} className="barSegment" title={item.label}>{item.label}</div>
);
})}
</div>
);
}
return (
<Layout title="LVM">
{data.resources.lvm.volumeGroups.map((volumeGroup) => {
return (<>
<h2>{volumeGroup.name}</h2>
<div className="volumeGroup">
<div className="label">Physical volumes:</div>
<SegmentBar
items={volumeGroup.physicalVolumes.map((physicalVolume) => {
return {
key: physicalVolume.path,
value: Math.round(physicalVolume.totalSpace.toMiB().amount),
label: `${physicalVolume.path} (${physicalVolume.totalSpace.toDisplay(2)})`
};
})}
/>
<div className="label">Volume group:</div>
<SegmentBar
items={[{
key: volumeGroup.name,
value: 1,
label: `${volumeGroup.name} (${volumeGroup.totalSpace.toDisplay(2)})`
}]}
/>
<div className="label">Logical volumes:</div>
<SegmentBar
items={volumeGroup.logicalVolumes.map((logicalVolume) => {
return {
key: logicalVolume.name,
value: Math.round(logicalVolume.size.toMiB().amount),
label: `${logicalVolume.name} (${logicalVolume.size.toDisplay(2)})`
};
})}
/>
</div>
</>);
})}
<DebugView value={data} />
</Layout>
);

@ -147,6 +147,8 @@ module.exports = {
let totalFailingStorage = sumDriveSizes(drivesByStatus.FAILING);
let totalUnknownStorage = sumDriveSizes(drivesByStatus.UNKNOWN);
// TODO: Show unallocated space
return (
<Layout title="Storage Devices">
<table className="drives">

@ -2,6 +2,7 @@
require("@babel/register");
const Promise = require("bluebird");
const errorChain = require("error-chain");
const createAPI = require("../src/api");
// const query = {
@ -41,23 +42,13 @@ const createAPI = require("../src/api");
const query = {
resources: {
lvm: {
physicalVolumes: {
createPhysicalVolume: {
$arguments: {
path: "/dev/loop3"
},
path: true,
totalSpace: true,
freeSpace: true,
status: true,
volumeGroup: {
name: true,
totalSpace: true,
freeSpace: true,
logicalVolumeCount: true,
mode: true,
physicalVolumes: {
path: true
}
}
freeSpace: true
}
}
}
@ -69,4 +60,6 @@ return Promise.try(() => {
return api.query(query);
}).then((result) => {
console.dir(result, { depth: null });
}).catch((error) => {
console.error(errorChain.render(error));
});

@ -0,0 +1,2 @@
sudo losetup -P /dev/loop1 vdisk1
sudo losetup -P /dev/loop2 vdisk2

@ -0,0 +1,2 @@
fallocate -l 5G vdisk1
fallocate -l 5G vdisk2

@ -938,6 +938,11 @@
lodash.escaperegexp "^4.1.2"
object-assign "^4.1.1"
"@joepie91/result@^0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@joepie91/result/-/result-0.1.0.tgz#187b97033edf200698ad159ea0edc907fce2cad0"
integrity sha512-2qjcinMrUV1FSA4g5AG6t32ijGTmcUzY5XIFJoNP0zQYtlM/C2NaLDcFHtwgASTMW0p3ZIkgueGlvwQe0S7Kxg==
"@joepie91/unreachable@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@joepie91/unreachable/-/unreachable-1.0.0.tgz#8032bb8a5813e81bbbe516cb3031d60818526687"
@ -1037,6 +1042,32 @@
supports-color "^7.1.0"
syncpipe "^1.0.0"
"@validatem/core@^0.3.3":
version "0.3.17"
resolved "https://registry.yarnpkg.com/@validatem/core/-/core-0.3.17.tgz#1756a7eca0523a3657794d2060273f7d42c083ef"
integrity sha512-VahE9TAKpaU13BcVQI/Dc9j/xsm/BgloRM0v1HjOMpoJ16tOkKQkUdOgiDCG4zmEek1bG3v9Zu4lS1lubgjLMw==
dependencies:
"@validatem/annotate-errors" "^0.1.2"
"@validatem/any-property" "^0.1.0"
"@validatem/error" "^1.0.0"
"@validatem/match-validation-error" "^0.1.0"
"@validatem/match-versioned-special" "^0.1.0"
"@validatem/match-virtual-property" "^0.1.0"
"@validatem/normalize-rules" "^0.1.0"
"@validatem/required" "^0.1.0"
"@validatem/validation-result" "^0.1.1"
"@validatem/virtual-property" "^0.1.0"
as-expression "^1.0.0"
assure-array "^1.0.0"
create-error "^0.3.1"
default-value "^1.0.0"
execall "^2.0.0"
flatten "^1.0.3"
indent-string "^4.0.0"
is-arguments "^1.0.4"
supports-color "^7.1.0"
syncpipe "^1.0.0"
"@validatem/default-to@^0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@validatem/default-to/-/default-to-0.1.0.tgz#62766a3ca24d2f61a96c713bcb629a5b3c6427c5"
@ -1194,6 +1225,11 @@
dependencies:
"@validatem/error" "^1.0.0"
"@validatem/remove-nullish-items@^0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@validatem/remove-nullish-items/-/remove-nullish-items-0.1.0.tgz#fe1a8b64d11276b506fae2bd2c41da4985a5b5ff"
integrity sha512-cs4YSF47TA/gHnV5muSUUqGi5PwybP5ztu5SYnPKxQVTyubvcbrFat51nOvJ2PmUasyrIccoYMmATiviXkTi6g==
"@validatem/require-either@^0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@validatem/require-either/-/require-either-0.1.0.tgz#250e35ab06f124ea90f3925d74b5f53a083923b0"
@ -1252,6 +1288,15 @@
default-value "^1.0.0"
split-filter-n "^1.1.2"
"@validatem/wrap-path@^0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@validatem/wrap-path/-/wrap-path-0.1.0.tgz#777998b62d3e74f2b2897c992dae9b3675161c33"
integrity sha512-6hOqydnr4u8FA0iRv8fyXxsr64T99+w/XL/fixmsgN0uqulEIwGMxCre3y9YkFNcEtysyPHkQl0CrGPcASsZxw==
dependencies:
"@validatem/annotate-errors" "^0.1.2"
"@validatem/combinator" "^0.1.2"
"@validatem/validation-result" "^0.1.2"
JSONStream@^1.0.3, JSONStream@^1.1.4:
version "1.3.5"
resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0"
@ -2859,6 +2904,16 @@ diffie-hellman@^5.0.0:
miller-rabin "^4.0.0"
randombytes "^2.0.0"
dlayer@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/dlayer/-/dlayer-0.1.2.tgz#3dbc43e55746988480bc8a4926aa523909661284"
integrity sha512-ZB709Ld/2TxUHMAKPfbvW2f0rscQAnu65j4/Nw8YRDpATFZd/NGmIXtOyEmYV7D904aLZLeKCaerfY+4Bu0i7Q==
dependencies:
"@joepie91/result" "^0.1.0"
bluebird "^3.4.6"
map-obj "^4.2.1"
syncpipe "^1.0.0"
dns-packet@^5.1.2:
version "5.2.1"
resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.2.1.tgz#26cec0be92252a1b97ed106482921192a7e08f72"
@ -5157,6 +5212,23 @@ memoizee@^0.4.14:
next-tick "^1.1.0"
timers-ext "^0.1.7"
merge-by-template@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/merge-by-template/-/merge-by-template-0.1.4.tgz#8a03e6383a4e2f2e4a6460bff0d6d3e7b468a535"
integrity sha512-10h5HyGLJJu1F1z02oMqpvMa6oraLr7Vp0gPxlw6Od8xlvzTFr0TQGPZXMLBmZlhZRY910AXGJ6AFc2iXGZ7uQ==
dependencies:
"@validatem/core" "^0.3.3"
"@validatem/default-to" "^0.1.0"
"@validatem/is-array" "^0.1.1"
"@validatem/is-boolean" "^0.1.1"
"@validatem/is-plain-object" "^0.1.1"
"@validatem/remove-nullish-items" "^0.1.0"
"@validatem/virtual-property" "^0.1.0"
"@validatem/wrap-path" "^0.1.0"
default-value "^1.0.0"
fromentries "^1.2.0"
range "^0.0.3"
merge-descriptors@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
@ -6346,6 +6418,11 @@ range-parser@~1.2.1:
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
range@^0.0.3:
version "0.0.3"
resolved "https://registry.yarnpkg.com/range/-/range-0.0.3.tgz#b5b8eb2463a516b624a563bd32b18fe89e70151b"
integrity sha512-OxK2nY2bmeEB4NxoBraQIBOOeOIxoBvm6yt8MA1kLappgkG3SyLf173iOtT5woWycrtESDD2g0Nl2yt8YPoUnw==
raw-body@2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.0.tgz#a1ce6fb9c9bc356ca52e89256ab59059e13d0332"

Loading…
Cancel
Save