@@ -13,6 +13,7 @@ const DataLoader = require("dataloader");
1313const { makeExecutableSchema } = require ( "graphql-tools" ) ;
1414const GraphQL = require ( "graphql" ) ;
1515const { PubSub, withFilter } = require ( "graphql-subscriptions" ) ;
16+ const hash = require ( "object-hash" ) ;
1617
1718module . exports = function ( mixinOptions ) {
1819 mixinOptions = _ . defaultsDeep ( mixinOptions , {
@@ -25,6 +26,12 @@ module.exports = function(mixinOptions) {
2526 subscriptionEventName : "graphql.publish" ,
2627 } ) ;
2728
29+ /**
30+ * DataLoader options
31+ * @type {Map<string, Object.<string, any>> } Action name key; value of options object to pass to DataLoader constructor
32+ */
33+ const dataLoaderOptions = new Map ( ) ;
34+
2835 const serviceSchema = {
2936 events : {
3037 "$services.changed" ( ) {
@@ -127,35 +134,54 @@ module.exports = function(mixinOptions) {
127134 const {
128135 dataLoader = false ,
129136 nullIfError = false ,
130- params = { } ,
137+ params : staticParams = { } ,
131138 rootParams = { } ,
132139 } = def ;
133140 const rootKeys = Object . keys ( rootParams ) ;
134141
135142 return async ( root , args , context ) => {
136143 try {
137144 if ( dataLoader ) {
138- const dataLoaderKey = rootKeys [ 0 ] ; // use the first root key
139- const rootValue = root && _ . get ( root , dataLoaderKey ) ;
145+ const dataLoaderMapKey = this . getDataLoaderMapKey (
146+ actionName ,
147+ staticParams ,
148+ args
149+ ) ;
150+ const dataLoaderRootKey = rootKeys [ 0 ] ; // for dataloader, use the first root key only
151+
152+ // check to see if the DataLoader has already been added to the GraphQL context; if not then add it for subsequent use
153+ let dataLoader ;
154+ if ( context . dataLoaders . has ( dataLoaderMapKey ) ) {
155+ dataLoader = context . dataLoaders . get ( dataLoaderMapKey ) ;
156+ } else {
157+ dataLoader = this . buildDataLoader (
158+ context . ctx ,
159+ actionName ,
160+ dataLoaderRootKey ,
161+ staticParams ,
162+ args
163+ ) ;
164+ context . dataLoaders . set ( dataLoaderMapKey , dataLoader ) ;
165+ }
166+
167+ const rootValue = root && _ . get ( root , dataLoaderRootKey ) ;
140168 if ( rootValue == null ) {
141169 return null ;
142170 }
143171
144172 return Array . isArray ( rootValue )
145- ? await Promise . all (
146- rootValue . map ( item =>
147- context . loaders [ actionName ] . load ( item )
148- )
149- )
150- : await context . loaders [ actionName ] . load ( rootValue ) ;
173+ ? await dataLoader . loadMany ( rootValue )
174+ : await dataLoader . load ( rootValue ) ;
151175 } else {
152- const p = { } ;
176+ const params = { } ;
153177 if ( root && rootKeys ) {
154- rootKeys . forEach ( k => _ . set ( p , def . rootParams [ k ] , _ . get ( root , k ) ) ) ;
178+ rootKeys . forEach ( key =>
179+ _ . set ( params , rootParams [ key ] , _ . get ( root , key ) )
180+ ) ;
155181 }
156182 return await context . ctx . call (
157183 actionName ,
158- _ . defaultsDeep ( args , p , params )
184+ _ . defaultsDeep ( args , params , staticParams )
159185 ) ;
160186 }
161187 } catch ( err ) {
@@ -171,6 +197,48 @@ module.exports = function(mixinOptions) {
171197 } ;
172198 } ,
173199
200+ /**
201+ * Get the unique key assigned to the DataLoader map
202+ * @param {string } actionName - Fully qualified action name to bind to dataloader
203+ * @param {Object.<string, any> } staticParams - Static parameters to use in dataloader
204+ * @param {Object.<string, any> } args - Arguments passed to GraphQL child resolver
205+ */
206+ getDataLoaderMapKey ( actionName , staticParams , args ) {
207+ if ( Object . keys ( staticParams ) . length > 0 || Object . keys ( args ) . length > 0 ) {
208+ // create a unique hash of the static params and the arguments to ensure a unique DataLoader instance
209+ const actionParams = _ . defaultsDeep ( args , staticParams ) ;
210+ const paramsHash = hash ( actionParams ) ;
211+ return `${ actionName } :${ paramsHash } ` ;
212+ }
213+
214+ // if no static params or arguments are present then the action name can serve as the key
215+ return actionName ;
216+ } ,
217+
218+ /**
219+ * Build a DataLoader instance
220+ *
221+ * @param {Object } ctx - Moleculer context
222+ * @param {string } actionName - Fully qualified action name to bind to dataloader
223+ * @param {string } batchedParamKey - Parameter key to use for loaded values
224+ * @param {Object } staticParams - Static parameters to use in dataloader
225+ * @param {Object } args - Arguments passed to GraphQL child resolver
226+ */
227+ buildDataLoader ( ctx , actionName , batchedParamKey , staticParams , args ) {
228+ const batchLoadFn = keys => {
229+ const rootParams = { [ batchedParamKey ] : keys } ;
230+ return ctx . call ( actionName , _ . defaultsDeep ( args , rootParams , staticParams ) ) ;
231+ } ;
232+
233+ if ( dataLoaderOptions . has ( actionName ) ) {
234+ // use any specified options assigned to this action
235+ const options = dataLoaderOptions . get ( actionName ) ;
236+ return new DataLoader ( batchLoadFn , options ) ;
237+ }
238+
239+ return new DataLoader ( batchLoadFn ) ;
240+ } ,
241+
174242 /**
175243 * Create resolver for subscription
176244 *
@@ -469,7 +537,7 @@ module.exports = function(mixinOptions) {
469537 ctx : req . $ctx ,
470538 service : req . $service ,
471539 params : req . $params ,
472- loaders : this . createLoaders ( req , services ) ,
540+ dataLoaders : new Map ( ) , // create an empty map to load DataLoader instances into
473541 }
474542 : {
475543 service : connection . $service ,
@@ -490,6 +558,8 @@ module.exports = function(mixinOptions) {
490558 this . apolloServer . installSubscriptionHandlers ( this . server ) ;
491559 this . graphqlSchema = schema ;
492560
561+ this . buildLoaderOptionMap ( services ) ; // rebuild the options for DataLoaders
562+
493563 this . shouldUpdateGraphqlSchema = false ;
494564
495565 this . broker . broadcast ( "graphql.schema.updated" , {
@@ -502,71 +572,30 @@ module.exports = function(mixinOptions) {
502572 } ,
503573
504574 /**
505- * Create the DataLoader instances to be used for batch resolution
506- * @param { Object } req
575+ * Build a map of options to use with DataLoader
576+ *
507577 * @param {Object[] } services
508- * @returns { Object.<string, Object> } Key/value pairs of DataLoader instances
578+ * @modifies {dataLoaderOptions}
509579 */
510- createLoaders ( req , services ) {
511- return services . reduce ( ( serviceAccum , service ) => {
512- const serviceName = this . getServiceName ( service ) ;
513- if ( ! service . settings ) {
514- service . settings = { } ;
515- }
516- const { graphql } = service . settings ;
517- if ( graphql && graphql . resolvers ) {
518- const { resolvers } = graphql ;
519-
520- const typeLoaders = Object . values ( resolvers ) . reduce (
521- ( resolverAccum , type ) => {
522- const resolverLoaders = Object . values ( type ) . reduce (
523- ( fieldAccum , resolver ) => {
524- if ( _ . isPlainObject ( resolver ) ) {
525- const {
526- action,
527- dataLoader = false ,
528- params = { } ,
529- rootParams = { } ,
530- } = resolver ;
531- const actionParam = Object . values ( rootParams ) [ 0 ] ; // use the first root parameter
532- if ( dataLoader && actionParam ) {
533- const resolverActionName = this . getResolverActionName (
534- serviceName ,
535- action
536- ) ;
537- if ( fieldAccum [ resolverActionName ] == null ) {
538- // create a new DataLoader instance
539- fieldAccum [ resolverActionName ] = new DataLoader (
540- keys =>
541- req . $ctx . call (
542- resolverActionName ,
543- _ . defaultsDeep (
544- {
545- [ actionParam ] : keys ,
546- } ,
547- params
548- )
549- )
550- ) ;
551- }
552- }
553- }
554-
555- return fieldAccum ;
556- } ,
557- { }
558- ) ;
559-
560- return { ...resolverAccum , ...resolverLoaders } ;
561- } ,
562- { }
563- ) ;
564-
565- serviceAccum = { ...serviceAccum , ...typeLoaders } ;
566- }
567-
568- return serviceAccum ;
569- } , { } ) ;
580+ buildLoaderOptionMap ( services ) {
581+ dataLoaderOptions . clear ( ) ; // clear map before rebuilding
582+
583+ services . forEach ( service => {
584+ Object . values ( service . actions ) . forEach ( action => {
585+ const { graphql : graphqlDefinition , name : actionName } = action ;
586+ if ( graphqlDefinition && graphqlDefinition . dataLoaderOptions ) {
587+ const serviceName = this . getServiceName ( service ) ;
588+ const fullActionName = this . getResolverActionName (
589+ serviceName ,
590+ actionName
591+ ) ;
592+ dataLoaderOptions . set (
593+ fullActionName ,
594+ graphqlDefinition . dataLoaderOptions
595+ ) ;
596+ }
597+ } ) ;
598+ } ) ;
570599 } ,
571600 } ,
572601
0 commit comments