Compare commits

...

3 Commits

@ -204,3 +204,5 @@ volume_id
is_primary
MARKER: Implement storage pool setup + network pools + VM spawn
FIXME: disconnecting the SMART-broken drive during a slow storage-devices overview load causes *all* storage devices to show as unknown

@ -36,8 +36,11 @@ module.exports = function () {
return typeFromSource(sources.lsblk, names, (device) => types.BlockDevice({ name: device.name }));
},
lvm: {
physicalVolumes: ({ paths }, { sources }) => {
return typeFromSource(sources.lvmPhysicalVolumes, paths, (volume) => types.LVMPhysicalVolume({ path: volume.path }));
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 }));

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

@ -0,0 +1,46 @@
```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).

@ -4,6 +4,7 @@ const Promise = require("bluebird");
const mapObject = require("map-obj");
const Result = require("../result");
const createCursor = require("./cursor");
// TODO: Bounded/unbounded recursion
// TODO: context
@ -20,25 +21,17 @@ The schema merging will eventually become deep-merging, when multi-level recursi
const specialKeyRegex = /^\$[^\$]/;
function stringifyPath(queryPath, schemaPath) {
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(" -> ");
}
function maybeCall(value, args, thisContext) {
return Promise.try(() => {
if (typeof value === "function") {
return value.call(thisContext, ...args);
// 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 value;
return getter;
}
});
}
@ -87,44 +80,42 @@ function analyzeSubquery(subquery) {
return { isRecursive, allowErrors, hasChildKeys, isLeaf, args };
}
function analyzeQueryKey(schemaObject, queryObject, queryKey) {
let subquery = queryObject[queryKey];
let schemaKey = subquery?.$key ?? queryKey; // $key is for handling aliases
let handler = schemaObject[schemaKey] ?? schemaObject.$anyKey;
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(subquery),
... analyzeSubquery(childCursor.query),
schemaKey: schemaKey,
handler: handler
};
}
function assignErrorPath(error, queryPath, schemaPath) {
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 = queryPath;
error.message = error.message + ` (${stringifyPath(queryPath, schemaPath)})`;
error.path = cursor.queryPath;
error.message = error.message + ` (${cursor.toPathString()})`;
}
}
function evaluate(schemaObject, queryObject, context, queryPath, schemaPath) {
function evaluate(cursor, context) {
// map query object -> result object
return asyncMapObject(queryObject, (queryKey, subquery) => {
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(schemaObject, queryObject, queryKey);
let { schemaKey, handler, args, isRecursive, allowErrors, isLeaf } = analyzeQueryKey(cursor, queryKey);
if (handler != null) {
let nextQueryPath = queryPath.concat([ queryKey ]);
let nextSchemaPath = schemaPath.concat([ schemaKey ]);
let promise = Promise.try(() => {
// This calls the data provider in the schema
return Result.wrapAsync(() => maybeCall(handler, [ args, context ], schemaObject));
return Result.wrapAsync(() => maybeCall(handler, [ args, context ], cursor.schema));
}).then((result) => {
if (result.isOK) {
let value = result.value();
@ -132,23 +123,26 @@ function evaluate(schemaObject, queryObject, context, queryPath, schemaPath) {
return Promise.try(() => {
if (!isLeaf && value != null) {
let effectiveSubquery = (isRecursive)
? { ... queryObject, ... subquery }
? { ... cursor.query, ... subquery }
: subquery;
return mapMaybeArray(value, (item, i) => {
if (i != null) {
let elementQueryPath = nextQueryPath.concat([i]);
let elementSchemaPath = nextSchemaPath.concat([i]);
return Promise.try(() => {
return evaluate(item, effectiveSubquery, context, elementQueryPath, elementSchemaPath);
}).tapCatch((error) => {
// FIXME: Verify that this is no longer needed, since moving the path-assigning logic
// assignErrorPath(error, elementQueryPath, elementSchemaPath);
});
} else {
return evaluate(item, effectiveSubquery, context, nextQueryPath, nextSchemaPath);
}
// 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
@ -177,7 +171,7 @@ function evaluate(schemaObject, queryObject, context, queryPath, schemaPath) {
}
}).tapCatch((error) => {
// FIXME: Chain properly
assignErrorPath(error, nextQueryPath, nextSchemaPath);
assignErrorPath(error, cursor.child(queryKey, schemaKey));
});
return [ queryKey, promise ];
@ -232,8 +226,13 @@ module.exports = function createDLayer(options) {
}
};
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(options.schema, query, combinedContext, [], []);
return evaluate(cursor, combinedContext);
}
};
};

Loading…
Cancel
Save