@steedos/moleculer-apollo-server 3.0.0-beta.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/service.js ADDED
@@ -0,0 +1,834 @@
1
+ /*
2
+ * moleculer-apollo-server
3
+ * Copyright (c) 2020 MoleculerJS (https://github.com/moleculerjs/moleculer-apollo-server)
4
+ * MIT Licensed
5
+ */
6
+
7
+ "use strict";
8
+
9
+ const _ = require("lodash");
10
+ const { MoleculerServerError } = require("moleculer").Errors;
11
+ const { ApolloServer } = require("./ApolloServer");
12
+ const DataLoader = require("dataloader");
13
+ const { makeExecutableSchema } = require("graphql-tools");
14
+ const GraphQL = require("graphql");
15
+ const { PubSub, withFilter } = require("graphql-subscriptions");
16
+ const hash = require("object-hash");
17
+
18
+ module.exports = function (mixinOptions) {
19
+ mixinOptions = _.defaultsDeep(mixinOptions, {
20
+ routeOptions: {
21
+ path: "/graphql",
22
+ },
23
+ schema: null,
24
+ serverOptions: {},
25
+ createAction: true,
26
+ subscriptionEventName: "graphql.publish",
27
+ invalidateEventName: "graphql.invalidate",
28
+ autoUpdateSchema: true,
29
+ checkActionVisibility: false,
30
+ });
31
+
32
+ const serviceSchema = {
33
+ actions: {
34
+ ws: {
35
+ timeout: 0,
36
+ visibility: "private",
37
+ tracing: {
38
+ tags: {
39
+ params: ["socket.upgradeReq.url"],
40
+ },
41
+ spanName: ctx => `UPGRADE ${ctx.params.socket.upgradeReq.url}`,
42
+ },
43
+ handler(ctx) {
44
+ const { socket, connectionParams } = ctx.params;
45
+ return {
46
+ $ctx: ctx,
47
+ $socket: socket,
48
+ $service: this,
49
+ $params: { body: connectionParams, query: socket.upgradeReq.query },
50
+ };
51
+ },
52
+ },
53
+ },
54
+
55
+ events: {
56
+ [mixinOptions.invalidateEventName]() {
57
+ this.invalidateGraphQLSchema();
58
+ },
59
+ "$services.changed"() {
60
+ if (mixinOptions.autoUpdateSchema) {
61
+ this.invalidateGraphQLSchema();
62
+ }
63
+ },
64
+ [mixinOptions.subscriptionEventName](event) {
65
+ if (this.pubsub) {
66
+ this.pubsub.publish(event.tag, event.payload);
67
+ }
68
+ },
69
+ },
70
+
71
+ methods: {
72
+ /**
73
+ * Invalidate the generated GraphQL schema
74
+ */
75
+ invalidateGraphQLSchema() {
76
+ this.shouldUpdateGraphqlSchema = true;
77
+ },
78
+
79
+ /**
80
+ * Return the field name in a GraphQL Mutation, Query, or Subscription declaration
81
+ * @param {String} declaration - Mutation, Query, or Subscription declaration
82
+ * @returns {String} Field name of declaration
83
+ */
84
+ getFieldName(declaration) {
85
+ // Remove all multi-line/single-line descriptions and comments
86
+ const cleanedDeclaration = declaration
87
+ .replace(/"([\s\S]*?)"/g, "")
88
+ .replace(/^[\s]*?#.*\n?/gm, "")
89
+ .trim();
90
+ return cleanedDeclaration.split(/[(:]/g)[0];
91
+ },
92
+
93
+ /**
94
+ * Get the full name of a service including version spec.
95
+ *
96
+ * @param {Service} service - Service object
97
+ * @returns {String} Name of service including version spec
98
+ */
99
+ getServiceName(service) {
100
+ if (service.fullName) return service.fullName;
101
+
102
+ if (service.version != null)
103
+ return (
104
+ (typeof service.version == "number"
105
+ ? "v" + service.version
106
+ : service.version) +
107
+ "." +
108
+ service.name
109
+ );
110
+
111
+ return service.name;
112
+ },
113
+
114
+ /**
115
+ * Get action name for resolver
116
+ *
117
+ * @param {String} service
118
+ * @param {String} action
119
+ */
120
+ getResolverActionName(service, action) {
121
+ if (action.indexOf(".") === -1) {
122
+ return `${service}.${action}`;
123
+ } else {
124
+ return action;
125
+ }
126
+ },
127
+
128
+ /**
129
+ * Create resolvers from service settings
130
+ *
131
+ * @param {String} serviceName
132
+ * @param {Object} resolvers
133
+ */
134
+ createServiceResolvers(serviceName, resolvers) {
135
+ return Object.entries(resolvers).reduce((acc, [name, r]) => {
136
+ if (_.isPlainObject(r) && r.action != null) {
137
+ // matches signature for remote action resolver
138
+ acc[name] = this.createActionResolver(
139
+ this.getResolverActionName(serviceName, r.action),
140
+ r
141
+ );
142
+ } else {
143
+ // something else (enum, etc.)
144
+ acc[name] = r;
145
+ }
146
+
147
+ return acc;
148
+ }, {});
149
+ },
150
+
151
+ /**
152
+ * Create resolver for action
153
+ *
154
+ * @param {String} actionName
155
+ * @param {Object?} def
156
+ */
157
+ createActionResolver(actionName, def = {}) {
158
+ const {
159
+ dataLoader: useDataLoader = false,
160
+ nullIfError = false,
161
+ params: staticParams = {},
162
+ rootParams = {},
163
+ fileUploadArg = null,
164
+ } = def;
165
+ const rootKeys = Object.keys(rootParams);
166
+
167
+ return async (root, args, context) => {
168
+ try {
169
+ if (useDataLoader) {
170
+ const dataLoaderMapKey = this.getDataLoaderMapKey(
171
+ actionName,
172
+ staticParams,
173
+ args
174
+ );
175
+ // if a dataLoader batching parameter is specified, then all root params can be data loaded;
176
+ // otherwise use only the primary rootParam
177
+ const primaryDataLoaderRootKey = rootKeys[0]; // for dataloader, use the first root key only
178
+ const dataLoaderBatchParam = this.dataLoaderBatchParams.get(actionName);
179
+ const dataLoaderUseAllRootKeys = dataLoaderBatchParam != null;
180
+
181
+ // check to see if the DataLoader has already been added to the GraphQL context; if not then add it for subsequent use
182
+ let dataLoader;
183
+ if (context.dataLoaders.has(dataLoaderMapKey)) {
184
+ dataLoader = context.dataLoaders.get(dataLoaderMapKey);
185
+ } else {
186
+ const batchedParamKey =
187
+ dataLoaderBatchParam || rootParams[primaryDataLoaderRootKey];
188
+
189
+ dataLoader = this.buildDataLoader(
190
+ context.ctx,
191
+ actionName,
192
+ batchedParamKey,
193
+ staticParams,
194
+ args,
195
+ { hashCacheKey: dataLoaderUseAllRootKeys } // must hash the cache key if not loading scalar
196
+ );
197
+ context.dataLoaders.set(dataLoaderMapKey, dataLoader);
198
+ }
199
+
200
+ let dataLoaderKey;
201
+ if (dataLoaderUseAllRootKeys) {
202
+ if (root && rootKeys) {
203
+ dataLoaderKey = {};
204
+
205
+ rootKeys.forEach(key => {
206
+ _.set(dataLoaderKey, rootParams[key], _.get(root, key));
207
+ });
208
+ }
209
+ } else {
210
+ dataLoaderKey = root && _.get(root, primaryDataLoaderRootKey);
211
+ }
212
+
213
+ if (dataLoaderKey == null) {
214
+ return null;
215
+ }
216
+
217
+ return Array.isArray(dataLoaderKey)
218
+ ? await dataLoader.loadMany(dataLoaderKey)
219
+ : await dataLoader.load(dataLoaderKey);
220
+ } else if (fileUploadArg != null && args[fileUploadArg] != null) {
221
+ const additionalArgs = _.omit(args, [fileUploadArg]);
222
+
223
+ if (Array.isArray(args[fileUploadArg])) {
224
+ return await Promise.all(
225
+ args[fileUploadArg].map(async uploadPromise => {
226
+ const { createReadStream, ...$fileInfo } =
227
+ await uploadPromise;
228
+ const stream = createReadStream();
229
+ return context.ctx.call(actionName, stream, {
230
+ meta: { $fileInfo, $args: additionalArgs },
231
+ });
232
+ })
233
+ );
234
+ }
235
+
236
+ const { createReadStream, ...$fileInfo } = await args[fileUploadArg];
237
+ const stream = createReadStream();
238
+ return await context.ctx.call(actionName, stream, {
239
+ meta: { $fileInfo, $args: additionalArgs },
240
+ });
241
+ } else {
242
+ const params = {};
243
+ let hasRootKeyValue = false;
244
+ if (root && rootKeys) {
245
+ rootKeys.forEach(key => {
246
+ const v = _.get(root, key);
247
+ _.set(params, rootParams[key], v);
248
+ if (v != null) hasRootKeyValue = true;
249
+ });
250
+
251
+ if (def.skipNullKeys && !hasRootKeyValue) {
252
+ return null;
253
+ }
254
+ }
255
+
256
+ let mergedParams = _.defaultsDeep({}, args, params, staticParams);
257
+
258
+ if (this.prepareContextParams) {
259
+ mergedParams = await this.prepareContextParams(
260
+ mergedParams,
261
+ actionName,
262
+ context
263
+ );
264
+ }
265
+
266
+ return await context.ctx.call(actionName, mergedParams);
267
+ }
268
+ } catch (err) {
269
+ if (nullIfError) {
270
+ return null;
271
+ }
272
+ /* istanbul ignore next */
273
+ if (err && err.ctx) {
274
+ err.ctx = null; // Avoid circular JSON in Moleculer <= 0.13
275
+ }
276
+ throw err;
277
+ }
278
+ };
279
+ },
280
+
281
+ /**
282
+ * Get the unique key assigned to the DataLoader map
283
+ * @param {string} actionName - Fully qualified action name to bind to dataloader
284
+ * @param {Object.<string, any>} staticParams - Static parameters to use in dataloader
285
+ * @param {Object.<string, any>} args - Arguments passed to GraphQL child resolver
286
+ * @returns {string} Key to the dataloader instance
287
+ */
288
+ getDataLoaderMapKey(actionName, staticParams, args) {
289
+ if (Object.keys(staticParams).length > 0 || Object.keys(args).length > 0) {
290
+ // create a unique hash of the static params and the arguments to ensure a unique DataLoader instance
291
+ const actionParams = _.defaultsDeep({}, args, staticParams);
292
+ const paramsHash = hash(actionParams);
293
+ return `${actionName}:${paramsHash}`;
294
+ }
295
+
296
+ // if no static params or arguments are present then the action name can serve as the key
297
+ return actionName;
298
+ },
299
+
300
+ /**
301
+ * Build a DataLoader instance
302
+ *
303
+ * @param {Object} ctx - Moleculer context
304
+ * @param {string} actionName - Fully qualified action name to bind to dataloader
305
+ * @param {string} batchedParamKey - Parameter key to use for loaded values
306
+ * @param {Object} staticParams - Static parameters to use in dataloader
307
+ * @param {Object} args - Arguments passed to GraphQL child resolver
308
+ * @param {Object} [options={}] - Optional arguments
309
+ * @param {Boolean} [options.hashCacheKey=false] - Use a hash for the cacheKeyFn
310
+ * @returns {DataLoader} Dataloader instance
311
+ */
312
+ buildDataLoader(
313
+ ctx,
314
+ actionName,
315
+ batchedParamKey,
316
+ staticParams,
317
+ args,
318
+ { hashCacheKey = false } = {}
319
+ ) {
320
+ const batchLoadFn = keys => {
321
+ const rootParams = { [batchedParamKey]: keys };
322
+ return ctx.call(actionName, _.defaultsDeep({}, args, rootParams, staticParams));
323
+ };
324
+
325
+ const dataLoaderOptions = this.dataLoaderOptions.get(actionName) || {};
326
+ const cacheKeyFn = hashCacheKey && (key => hash(key));
327
+ const options = {
328
+ ...(cacheKeyFn && { cacheKeyFn }),
329
+ ...dataLoaderOptions,
330
+ };
331
+
332
+ return new DataLoader(batchLoadFn, options);
333
+ },
334
+
335
+ /**
336
+ * Create resolver for subscription
337
+ *
338
+ * @param {String} actionName
339
+ * @param {Array?} tags
340
+ * @param {String?} filter
341
+ */
342
+ createAsyncIteratorResolver(actionName, tags = [], filter) {
343
+ return {
344
+ subscribe: filter
345
+ ? withFilter(
346
+ () => this.pubsub.asyncIterator(tags),
347
+ async (payload, params, { ctx }) =>
348
+ payload !== undefined
349
+ ? ctx.call(filter, { ...params, payload })
350
+ : false
351
+ )
352
+ : () => this.pubsub.asyncIterator(tags),
353
+ resolve: (payload, params, { ctx }) =>
354
+ ctx.call(actionName, { ...params, payload }),
355
+ };
356
+ },
357
+
358
+ /**
359
+ * Generate GraphQL Schema
360
+ *
361
+ * @param {Object[]} services
362
+ * @returns {Object} Generated schema
363
+ */
364
+ generateGraphQLSchema(services) {
365
+ let str;
366
+ try {
367
+ let typeDefs = [];
368
+ let resolvers = {};
369
+ let schemaDirectives = null;
370
+
371
+ if (mixinOptions.typeDefs) {
372
+ typeDefs = typeDefs.concat(mixinOptions.typeDefs);
373
+ }
374
+
375
+ if (mixinOptions.resolvers) {
376
+ resolvers = _.cloneDeep(mixinOptions.resolvers);
377
+ }
378
+
379
+ if (mixinOptions.schemaDirectives) {
380
+ schemaDirectives = _.cloneDeep(mixinOptions.schemaDirectives);
381
+ }
382
+
383
+ let queries = [];
384
+ let mutations = [];
385
+ let subscriptions = [];
386
+ let types = [];
387
+ let interfaces = [];
388
+ let unions = [];
389
+ let enums = [];
390
+ let inputs = [];
391
+
392
+ const processedServices = new Set();
393
+
394
+ services.forEach(service => {
395
+ const serviceName = this.getServiceName(service);
396
+
397
+ // Skip multiple instances of services
398
+ if (processedServices.has(serviceName)) return;
399
+ processedServices.add(serviceName);
400
+
401
+ if (service.settings && service.settings.graphql) {
402
+ // --- COMPILE SERVICE-LEVEL DEFINITIONS ---
403
+ if (_.isObject(service.settings.graphql)) {
404
+ const globalDef = service.settings.graphql;
405
+
406
+ if (globalDef.query) {
407
+ queries = queries.concat(globalDef.query);
408
+ }
409
+
410
+ if (globalDef.mutation) {
411
+ mutations = mutations.concat(globalDef.mutation);
412
+ }
413
+
414
+ if (globalDef.subscription) {
415
+ subscriptions = subscriptions.concat(globalDef.subscription);
416
+ }
417
+
418
+ if (globalDef.type) {
419
+ types = types.concat(globalDef.type);
420
+ }
421
+
422
+ if (globalDef.interface) {
423
+ interfaces = interfaces.concat(globalDef.interface);
424
+ }
425
+
426
+ if (globalDef.union) {
427
+ unions = unions.concat(globalDef.union);
428
+ }
429
+
430
+ if (globalDef.enum) {
431
+ enums = enums.concat(globalDef.enum);
432
+ }
433
+
434
+ if (globalDef.input) {
435
+ inputs = inputs.concat(globalDef.input);
436
+ }
437
+
438
+ if (globalDef.resolvers) {
439
+ resolvers = Object.entries(globalDef.resolvers).reduce(
440
+ (acc, [name, resolver]) => {
441
+ acc[name] = _.merge(
442
+ acc[name] || {},
443
+ this.createServiceResolvers(serviceName, resolver)
444
+ );
445
+ return acc;
446
+ },
447
+ resolvers
448
+ );
449
+ }
450
+ }
451
+ }
452
+
453
+ // --- COMPILE ACTION-LEVEL DEFINITIONS ---
454
+ const resolver = {};
455
+
456
+ Object.values(service.actions).forEach(action => {
457
+ const { graphql: def } = action;
458
+ if (
459
+ mixinOptions.checkActionVisibility &&
460
+ action.visibility != null &&
461
+ action.visibility != "published"
462
+ )
463
+ return;
464
+
465
+ if (def && _.isObject(def)) {
466
+ if (def.query) {
467
+ if (!resolver["Query"]) resolver.Query = {};
468
+
469
+ _.castArray(def.query).forEach(query => {
470
+ const name = this.getFieldName(query);
471
+ queries.push(query);
472
+ resolver.Query[name] = this.createActionResolver(
473
+ action.name
474
+ );
475
+ });
476
+ }
477
+
478
+ if (def.mutation) {
479
+ if (!resolver["Mutation"]) resolver.Mutation = {};
480
+
481
+ _.castArray(def.mutation).forEach(mutation => {
482
+ const name = this.getFieldName(mutation);
483
+ mutations.push(mutation);
484
+ resolver.Mutation[name] = this.createActionResolver(
485
+ action.name,
486
+ {
487
+ fileUploadArg: def.fileUploadArg,
488
+ }
489
+ );
490
+ });
491
+ }
492
+
493
+ if (def.subscription) {
494
+ if (!resolver["Subscription"]) resolver.Subscription = {};
495
+
496
+ _.castArray(def.subscription).forEach(subscription => {
497
+ const name = this.getFieldName(subscription);
498
+ subscriptions.push(subscription);
499
+ resolver.Subscription[name] =
500
+ this.createAsyncIteratorResolver(
501
+ action.name,
502
+ def.tags,
503
+ def.filter
504
+ );
505
+ });
506
+ }
507
+
508
+ if (def.type) {
509
+ types = types.concat(def.type);
510
+ }
511
+
512
+ if (def.interface) {
513
+ interfaces = interfaces.concat(def.interface);
514
+ }
515
+
516
+ if (def.union) {
517
+ unions = unions.concat(def.union);
518
+ }
519
+
520
+ if (def.enum) {
521
+ enums = enums.concat(def.enum);
522
+ }
523
+
524
+ if (def.input) {
525
+ inputs = inputs.concat(def.input);
526
+ }
527
+ }
528
+ });
529
+
530
+ if (Object.keys(resolver).length > 0) {
531
+ resolvers = _.merge(resolvers, resolver);
532
+ }
533
+ });
534
+
535
+ if (
536
+ queries.length > 0 ||
537
+ types.length > 0 ||
538
+ mutations.length > 0 ||
539
+ subscriptions.length > 0 ||
540
+ interfaces.length > 0 ||
541
+ unions.length > 0 ||
542
+ enums.length > 0 ||
543
+ inputs.length > 0
544
+ ) {
545
+ str = "";
546
+ if (queries.length > 0) {
547
+ str += `
548
+ type Query {
549
+ ${queries.join("\n")}
550
+ }
551
+ `;
552
+ }
553
+
554
+ if (mutations.length > 0) {
555
+ str += `
556
+ type Mutation {
557
+ ${mutations.join("\n")}
558
+ }
559
+ `;
560
+ }
561
+
562
+ if (subscriptions.length > 0) {
563
+ str += `
564
+ type Subscription {
565
+ ${subscriptions.join("\n")}
566
+ }
567
+ `;
568
+ }
569
+
570
+ if (types.length > 0) {
571
+ str += `
572
+ ${types.join("\n")}
573
+ `;
574
+ }
575
+
576
+ if (interfaces.length > 0) {
577
+ str += `
578
+ ${interfaces.join("\n")}
579
+ `;
580
+ }
581
+
582
+ if (unions.length > 0) {
583
+ str += `
584
+ ${unions.join("\n")}
585
+ `;
586
+ }
587
+
588
+ if (enums.length > 0) {
589
+ str += `
590
+ ${enums.join("\n")}
591
+ `;
592
+ }
593
+
594
+ if (inputs.length > 0) {
595
+ str += `
596
+ ${inputs.join("\n")}
597
+ `;
598
+ }
599
+
600
+ typeDefs.push(str);
601
+ }
602
+
603
+ return this.makeExecutableSchema({ typeDefs, resolvers, schemaDirectives });
604
+ } catch (err) {
605
+ throw new MoleculerServerError(
606
+ "Unable to compile GraphQL schema",
607
+ 500,
608
+ "UNABLE_COMPILE_GRAPHQL_SCHEMA",
609
+ { err, str }
610
+ );
611
+ }
612
+ },
613
+
614
+ /**
615
+ * Call the `makeExecutableSchema`. If you would like
616
+ * to manipulate the concatenated typeDefs, or the generated schema,
617
+ * just overwrite it in your service file.
618
+ * @param {Object} schemaDef
619
+ */
620
+ makeExecutableSchema(schemaDef) {
621
+ return makeExecutableSchema(schemaDef);
622
+ },
623
+
624
+ /**
625
+ * Create PubSub instance.
626
+ */
627
+ createPubSub() {
628
+ return new PubSub();
629
+ },
630
+
631
+ /**
632
+ * Prepare GraphQL schemas based on Moleculer services.
633
+ */
634
+ async prepareGraphQLSchema() {
635
+ // Schema is up-to-date
636
+ if (!this.shouldUpdateGraphqlSchema && this.graphqlHandler) {
637
+ return;
638
+ }
639
+
640
+ if (this.apolloServer) {
641
+ await this.apolloServer.stop();
642
+ }
643
+
644
+ // Create new server & regenerate GraphQL schema
645
+ this.logger.info(
646
+ "â™» Recreate Apollo GraphQL server and regenerate GraphQL schema..."
647
+ );
648
+
649
+ try {
650
+ this.pubsub = this.createPubSub();
651
+ const services = this.broker.registry.getServiceList({ withActions: true });
652
+ const schema = this.generateGraphQLSchema(services);
653
+
654
+ this.logger.debug(
655
+ "Generated GraphQL schema:\n\n" + GraphQL.printSchema(schema)
656
+ );
657
+
658
+ this.apolloServer = new ApolloServer({
659
+ schema,
660
+ ..._.defaultsDeep({}, mixinOptions.serverOptions, {
661
+ context: ({ req, connection }) => ({
662
+ ...(req
663
+ ? {
664
+ ctx: req.$ctx,
665
+ service: req.$service,
666
+ params: req.$params,
667
+ }
668
+ : {
669
+ ctx: connection.context.$ctx,
670
+ service: connection.context.$service,
671
+ params: connection.context.$params,
672
+ }),
673
+ dataLoaders: new Map(), // create an empty map to load DataLoader instances into
674
+ }),
675
+ subscriptions: {
676
+ onConnect: (connectionParams, socket) =>
677
+ this.actions.ws({ connectionParams, socket }),
678
+ },
679
+ }),
680
+ });
681
+
682
+ this.graphqlHandler = this.apolloServer.createHandler(
683
+ mixinOptions.serverOptions
684
+ );
685
+
686
+ if (mixinOptions.serverOptions.subscriptions !== false) {
687
+ // Avoid installing the subscription handlers if they have been disabled
688
+ this.apolloServer.installSubscriptionHandlers(this.server);
689
+ }
690
+
691
+ this.graphqlSchema = schema;
692
+
693
+ this.buildLoaderOptionMap(services); // rebuild the options for DataLoaders
694
+
695
+ this.shouldUpdateGraphqlSchema = false;
696
+
697
+ this.broker.broadcast("graphql.schema.updated", {
698
+ schema: GraphQL.printSchema(schema),
699
+ });
700
+ } catch (err) {
701
+ this.logger.error(err);
702
+ throw err;
703
+ }
704
+ },
705
+
706
+ /**
707
+ * Build a map of options to use with DataLoader
708
+ *
709
+ * @param {Object[]} services
710
+ * @modifies {this.dataLoaderOptions}
711
+ * @modifies {this.dataLoaderBatchParams}
712
+ */
713
+ buildLoaderOptionMap(services) {
714
+ this.dataLoaderOptions.clear(); // clear map before rebuilding
715
+ this.dataLoaderBatchParams.clear(); // clear map before rebuilding
716
+
717
+ services.forEach(service => {
718
+ Object.values(service.actions).forEach(action => {
719
+ const { graphql: graphqlDefinition, name: actionName } = action;
720
+ if (
721
+ graphqlDefinition &&
722
+ (graphqlDefinition.dataLoaderOptions ||
723
+ graphqlDefinition.dataLoaderBatchParam)
724
+ ) {
725
+ const serviceName = this.getServiceName(service);
726
+ const fullActionName = this.getResolverActionName(
727
+ serviceName,
728
+ actionName
729
+ );
730
+
731
+ if (graphqlDefinition.dataLoaderOptions) {
732
+ this.dataLoaderOptions.set(
733
+ fullActionName,
734
+ graphqlDefinition.dataLoaderOptions
735
+ );
736
+ }
737
+
738
+ if (graphqlDefinition.dataLoaderBatchParam) {
739
+ this.dataLoaderBatchParams.set(
740
+ fullActionName,
741
+ graphqlDefinition.dataLoaderBatchParam
742
+ );
743
+ }
744
+ }
745
+ });
746
+ });
747
+ },
748
+ },
749
+
750
+ created() {
751
+ this.apolloServer = null;
752
+ this.graphqlHandler = null;
753
+ this.graphqlSchema = null;
754
+ this.pubsub = null;
755
+ this.shouldUpdateGraphqlSchema = true;
756
+ this.dataLoaderOptions = new Map();
757
+ this.dataLoaderBatchParams = new Map();
758
+
759
+ // Bind service to onConnect method
760
+ if (
761
+ mixinOptions.serverOptions.subscriptions &&
762
+ _.isFunction(mixinOptions.serverOptions.subscriptions.onConnect)
763
+ ) {
764
+ mixinOptions.serverOptions.subscriptions.onConnect =
765
+ mixinOptions.serverOptions.subscriptions.onConnect.bind(this);
766
+ }
767
+
768
+ const route = _.defaultsDeep(mixinOptions.routeOptions, {
769
+ aliases: {
770
+ async "/"(req, res) {
771
+ try {
772
+ await this.prepareGraphQLSchema();
773
+ return await this.graphqlHandler(req, res);
774
+ } catch (err) {
775
+ this.sendError(req, res, err);
776
+ }
777
+ },
778
+ async "GET /.well-known/apollo/server-health"(req, res) {
779
+ try {
780
+ await this.prepareGraphQLSchema();
781
+ return await this.graphqlHandler(req, res);
782
+ } catch (err) {
783
+ res.statusCode = 503;
784
+ return this.sendResponse(
785
+ req,
786
+ res,
787
+ { status: "fail", schema: false },
788
+ { responseType: "application/health+json" }
789
+ );
790
+ }
791
+ },
792
+ },
793
+
794
+ mappingPolicy: "restrict",
795
+
796
+ bodyParsers: {
797
+ json: true,
798
+ urlencoded: { extended: true },
799
+ },
800
+ });
801
+
802
+ // Add route
803
+ this.settings.routes.unshift(route);
804
+ },
805
+
806
+ started() {
807
+ this.logger.info(`🚀 GraphQL server is available at ${mixinOptions.routeOptions.path}`);
808
+ },
809
+ };
810
+
811
+ if (mixinOptions.createAction) {
812
+ serviceSchema.actions = {
813
+ ...serviceSchema.actions,
814
+ graphql: {
815
+ params: {
816
+ query: { type: "string" },
817
+ variables: { type: "object", optional: true },
818
+ },
819
+ async handler(ctx) {
820
+ await this.prepareGraphQLSchema();
821
+ return GraphQL.graphql(
822
+ this.graphqlSchema,
823
+ ctx.params.query,
824
+ null,
825
+ { ctx },
826
+ ctx.params.variables
827
+ );
828
+ },
829
+ },
830
+ };
831
+ }
832
+
833
+ return serviceSchema;
834
+ };