Skip to content

Commit f55ae66

Browse files
committed
Allow options and args in DataLoader
Bump dataloader version
1 parent 080522f commit f55ae66

3 files changed

Lines changed: 121 additions & 86 deletions

File tree

package-lock.json

Lines changed: 13 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,11 @@
6868
"@apollographql/graphql-playground-html": "^1.6.24",
6969
"accept": "^3.1.3",
7070
"apollo-server-core": "^2.9.6",
71-
"dataloader": "^1.4.0",
71+
"dataloader": "^2.0.0",
7272
"graphql-subscriptions": "^1.1.0",
7373
"graphql-tools": "^4.0.5",
7474
"graphql-upload": "^8.1.0",
75-
"lodash": "^4.17.15"
75+
"lodash": "^4.17.15",
76+
"object-hash": "^2.0.1"
7677
}
7778
}

src/service.js

Lines changed: 105 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const DataLoader = require("dataloader");
1313
const { makeExecutableSchema } = require("graphql-tools");
1414
const GraphQL = require("graphql");
1515
const { PubSub, withFilter } = require("graphql-subscriptions");
16+
const hash = require("object-hash");
1617

1718
module.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

Comments
 (0)