From edc6e0f8be3fef525912a37fd5630d54663c5c30 Mon Sep 17 00:00:00 2001 From: Sven Slootweg Date: Thu, 15 Sep 2022 16:04:20 +0200 Subject: [PATCH] Refactor dlayer --- src/packages/dlayer/cursor.js | 33 +++++++++++---- src/packages/dlayer/index.js | 78 +++++++++++++++++------------------ 2 files changed, 65 insertions(+), 46 deletions(-) diff --git a/src/packages/dlayer/cursor.js b/src/packages/dlayer/cursor.js index e260b34..a75d150 100644 --- a/src/packages/dlayer/cursor.js +++ b/src/packages/dlayer/cursor.js @@ -3,16 +3,35 @@ // 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 }) { + console.log("create", { queryPath, schemaPath, queryObject, schemaObject }); return { - query: queryPath, - schema: schemaPath, - child: function (property, { queryOverride, schemaOverride } = {}) { + queryPath: queryPath, + schemaPath: schemaPath, + query: queryObject, + schema: schemaObject, + child: function (queryKey, schemaKey, { queryOverride, schemaOverride } = {}) { return createInstance({ - queryPath: queryPath.concat([ property ]), - schemaPath: schemaPath.concat([ property ]), - queryObject: queryOverride ?? queryObject[property], - schemaObject: schemaOverride ?? schemaObject[property] + 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(" -> "); } }; } diff --git a/src/packages/dlayer/index.js b/src/packages/dlayer/index.js index 88a0e68..b164df7 100644 --- a/src/packages/dlayer/index.js +++ b/src/packages/dlayer/index.js @@ -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,19 +21,6 @@ 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(() => { // FIXME: Only do this for actual fetch requests @@ -92,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 handlingQueryPath = queryPath.concat([ queryKey ]); - let handlingSchemaPath = 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(); @@ -137,18 +123,27 @@ 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 = handlingQueryPath.concat([i]); - let elementSchemaPath = handlingSchemaPath.concat([i]); - - return evaluate(item, effectiveSubquery, context, elementQueryPath, elementSchemaPath); - } else { - return evaluate(item, effectiveSubquery, context, handlingQueryPath, handlingSchemaPath); - } + // 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 + // NOTE: 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? + console.log({queryKey, schemaKey}); + 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 +172,7 @@ function evaluate(schemaObject, queryObject, context, queryPath, schemaPath) { } }).tapCatch((error) => { // FIXME: Chain properly - assignErrorPath(error, handlingQueryPath, handlingSchemaPath); + assignErrorPath(error, cursor.child(queryKey, schemaKey)); }); return [ queryKey, promise ]; @@ -232,8 +227,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); } }; };