Compare commits
6 Commits
be01fcc4bf
...
300c58533f
Author | SHA1 | Date |
---|---|---|
Sven Slootweg | 300c58533f | 10 months ago |
Sven Slootweg | a768a3f246 | 10 months ago |
Sven Slootweg | ea9a7b2c43 | 10 months ago |
Sven Slootweg | 87c95dc60a | 10 months ago |
Sven Slootweg | b33cc34550 | 11 months ago |
Sven Slootweg | 8610aed04c | 2 years ago |
@ -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"}
|
@ -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,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);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
};
|
@ -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
|
||||
}));
|
||||
}));
|
||||
};
|
||||
};
|
@ -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;
|
||||
}
|
||||
});
|
||||
};
|
@ -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
|
Loading…
Reference in New Issue