"use strict" ;
// const mergeByTemplate = require("merge-by-template");
const syncpipe = require ( "syncpipe" ) ;
const mapObject = require ( "map-obj" ) ;
const deepMergeAndMap = require ( "./deep-merge-and-map" ) ;
const InvalidObject = require ( "./invalid-object" ) ;
/ *
Take a list of modules ; each module specifies a name , schema root , types , type extensions , and a context factory function . Each module is internally assigned a unique ID . This unique ID is associated with each type factory and type extension method , and used as the key for a map of context factories ; that way , upon invoking those methods , the module ' s own corresponding context can be injected . Only a single context should be created per module per request , so there should be a cache layer for the contexts ( keyed by module ID ) , with each request creating a new cache .
* /
// NOTE: This can be global because we identify existing assignments by object identity, and that will never conflict
let numberedModules = new WeakMap ( ) ;
let currentModuleNumber = 0 ;
function getModuleID ( module ) {
if ( ! numberedModules . has ( module ) ) {
numberedModules . set ( module , currentModuleNumber ++ ) ;
}
return numberedModules . get ( module ) ;
}
function createTypeTracker ( ) {
let typeFactories = { } ;
return {
add : function ( module , name , factory ) {
if ( typeFactories [ name ] != null ) {
let existingEntry = typeFactories [ name ] ;
throw new Error ( ` Type ' ${ name } ' already exists (from module ' ${ module . name } ', already defined by module ' ${ existingEntry . source . name } ') ` ) ;
} else {
typeFactories [ name ] = {
_ _moduleID : getModuleID ( module ) ,
source : module ,
// func: wrapModuleFunction(module, factory)
func : async function ( args , context ) {
// NOTE: We pass through the context here without modifying it; because a module-specific context will be injected from the main runtime
let instance = await factory ( args , context ) ;
if ( instance !== InvalidObject ) {
// We need to patch every created object, so that the correct context gets injected into its type-specific methods when they are called
return mapObject ( instance , ( key , value ) => {
return [ key , wrapModuleValue ( module , value ) ] ;
} ) ;
} else {
return instance ;
}
}
} ;
}
} ,
get : function ( ) {
return typeFactories ;
}
} ;
}
function createExtensionTracker ( ) {
let extendedTypes = { } ;
return {
add : function ( module , type , name , method ) {
if ( extendedTypes [ type ] == null ) {
extendedTypes [ type ] = { } ;
}
let extensions = extendedTypes [ type ] ;
if ( extensions [ name ] != null ) {
let existingEntry = extensions [ name ] ;
throw new Error ( ` Type ' ${ type } ' already has a method extension named ' ${ name } ' (from module ' ${ module . name } ', already defined by module ' ${ existingEntry . source . name } ') ` ) ;
} else {
extensions [ name ] = {
source : module ,
func : wrapModuleFunction ( module , method )
} ;
}
} ,
get : function ( ) {
return extendedTypes ;
}
} ;
}
function wrapModuleFunction ( module , func ) {
return { _ _moduleID : getModuleID ( module ) , func : func } ;
}
function wrapModuleValue ( module , value ) {
if ( typeof value === "function" ) {
return wrapModuleFunction ( module , value ) ;
} else {
return value ;
}
}
function defaultContext ( ) {
// Fallback function that generates an empty context, for when a module doesn't specify a makeContext handler
return { } ;
}
module . exports = function ( modules ) {
// TODO: Eventually replace hand-crafted merging logic with merge-by-template, once it can support this usecase properly(tm)
// TODO: Fix merge-by-template so that reasonable error messages can be generated here, that are actually aware of eg. the conflicting key
// TODO: Disallow modules with duplicate names? This may be necessary to prevent issues with getModuleContext, which provides name-based context access
let types = createTypeTracker ( ) ;
let typeExtensions = createExtensionTracker ( ) ;
let nameToID = new Map ( ) ; // TODO: Make this a tracker abstraction?
let contextFactories = syncpipe ( modules , [
_ => _ . map ( ( module ) => [ getModuleID ( module ) , module . makeContext ? ? defaultContext ] ) ,
_ => new Map ( _ )
] ) ;
let schema = modules . reduce ( ( schema , module ) => {
return deepMergeAndMap ( schema , module . root , ( value ) => wrapModuleValue ( module , value ) ) ;
} , { } ) ;
for ( let module of modules ) {
if ( module . name != null ) {
nameToID . set ( module . name , getModuleID ( module ) ) ;
}
for ( let [ type , factory ] of Object . entries ( module . types ? ? { } ) ) {
types . add ( module , type , factory ) ;
}
for ( let [ type , extensions ] of Object . entries ( module . extensions ? ? { } ) ) {
for ( let [ name , method ] of Object . entries ( extensions ) ) {
typeExtensions . add ( module , type , name , method ) ;
}
}
}
return {
root : schema ,
types : types . get ( ) ,
extensions : typeExtensions . get ( ) ,
nameToID : nameToID ,
makeContextFactory : function ( baseContext ) {
let cache = new Map ( ) ;
return function makeContextForModule ( moduleID ) {
if ( ! cache . has ( moduleID ) ) {
cache . set ( moduleID , {
... baseContext ,
// NOTE: Module context initialization gets access to the baseContext, for user-level overrides (eg. passing in transaction objects for a query)
... contextFactories . get ( moduleID ) ( baseContext )
} ) ;
}
return cache . get ( moduleID ) ;
} ;
}
} ;
} ;