@ -4,6 +4,7 @@ const Promise = require("bluebird");
const mapObject = require ( "map-obj" ) ;
const mapObject = require ( "map-obj" ) ;
const Result = require ( "../result" ) ;
const Result = require ( "../result" ) ;
const createCursor = require ( "./cursor" ) ;
// TODO: Bounded/unbounded recursion
// TODO: Bounded/unbounded recursion
// TODO: context
// TODO: context
@ -20,19 +21,6 @@ The schema merging will eventually become deep-merging, when multi-level recursi
const specialKeyRegex = /^\$[^\$]/ ;
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 ) {
function maybeCall ( value , args , thisContext ) {
return Promise . try ( ( ) => {
return Promise . try ( ( ) => {
// FIXME: Only do this for actual fetch requests
// FIXME: Only do this for actual fetch requests
@ -92,44 +80,42 @@ function analyzeSubquery(subquery) {
return { isRecursive , allowErrors , hasChildKeys , isLeaf , args } ;
return { isRecursive , allowErrors , hasChildKeys , isLeaf , args } ;
}
}
function analyzeQueryKey ( schemaObject , queryObject , queryKey ) {
function analyzeQueryKey ( cursor , queryKey ) {
let subquery = queryObject [ queryKey ] ;
let childCursor = cursor . child ( queryKey , null ) ;
let schemaKey = subquery ? . $key ? ? queryKey ; // $key is for handling aliases
let schemaKey = childCursor . query ? . $key ? ? queryKey ; // $key is for handling aliases
let handler = schemaObject [ schemaKey ] ? ? schemaObject . $anyKey ;
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 {
return {
... analyzeSubquery ( sub query) ,
... analyzeSubquery ( childCursor. query) ,
schemaKey : schemaKey ,
schemaKey : schemaKey ,
handler : handler
handler : handler
} ;
} ;
}
}
function assignErrorPath ( error , queryPath, schemaPath ) {
function assignErrorPath ( error , cursor ) {
if ( error . path == null ) {
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
// 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 . path = cursor. queryPath;
error . message = error . message + ` ( ${ stringifyPath( queryPath , schemaPath ) } ) ` ;
error . message = error . message + ` ( ${ cursor. toPathString ( ) } ) ` ;
}
}
}
}
function evaluate ( schemaObject, queryObject , context , queryPath , schemaPath ) {
function evaluate ( cursor, context ) {
// map query object -> result object
// map query object -> result object
return asyncMapObject ( queryObject , ( queryKey , subquery ) => {
return asyncMapObject ( cursor. query, ( queryKey , subquery ) => {
let shouldFetch = ( subquery !== false ) ;
let shouldFetch = ( subquery !== false ) ;
if ( ! shouldFetch || specialKeyRegex . test ( queryKey ) ) {
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.
// 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 ;
return mapObject . mapObjectSkip ;
} else {
} 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 ) {
if ( handler != null ) {
let handlingQueryPath = queryPath . concat ( [ queryKey ] ) ;
let handlingSchemaPath = schemaPath . concat ( [ schemaKey ] ) ;
let promise = Promise . try ( ( ) => {
let promise = Promise . try ( ( ) => {
// This calls the data provider in the schema
// 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 ) => {
} ) . then ( ( result ) => {
if ( result . isOK ) {
if ( result . isOK ) {
let value = result . value ( ) ;
let value = result . value ( ) ;
@ -137,18 +123,27 @@ function evaluate(schemaObject, queryObject, context, queryPath, schemaPath) {
return Promise . try ( ( ) => {
return Promise . try ( ( ) => {
if ( ! isLeaf && value != null ) {
if ( ! isLeaf && value != null ) {
let effectiveSubquery = ( isRecursive )
let effectiveSubquery = ( isRecursive )
? { ... queryObject , ... subquery }
? { ... cursor. query, ... subquery }
: subquery ;
: subquery ;
return mapMaybeArray ( value , ( item , i ) => {
return mapMaybeArray ( value , ( item , i ) => {
if ( i != null ) {
// 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
let elementQueryPath = handlingQueryPath . concat ( [ i ] ) ;
// 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?
let elementSchemaPath = handlingSchemaPath . concat ( [ i ] ) ;
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 ( item , effectiveSubquery , context , elementQueryPath , elementSchemaPath ) ;
return evaluate ( subCursor , context ) ;
} else {
return evaluate ( item , effectiveSubquery , context , handlingQueryPath , handlingSchemaPath ) ;
}
} ) ;
} ) ;
} else {
} else {
// null / undefined are returned as-is, so are leaves
// null / undefined are returned as-is, so are leaves
@ -177,7 +172,7 @@ function evaluate(schemaObject, queryObject, context, queryPath, schemaPath) {
}
}
} ) . tapCatch ( ( error ) => {
} ) . tapCatch ( ( error ) => {
// FIXME: Chain properly
// FIXME: Chain properly
assignErrorPath ( error , handlingQueryPath, handlingSchemaPath ) ;
assignErrorPath ( error , cursor. child ( queryKey , schemaKey ) ) ;
} ) ;
} ) ;
return [ queryKey , promise ] ;
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
// 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 ) ;
}
}
} ;
} ;
} ;
} ;