@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/.codeclimate.yml +41 -0
- package/.editorconfig +31 -0
- package/.eslintignore +3 -0
- package/.eslintrc.js +31 -0
- package/.prettierrc.js +10 -0
- package/LICENSE +21 -0
- package/README.md +448 -0
- package/index.d.ts +108 -0
- package/index.js +50 -0
- package/package.json +63 -0
- package/src/ApolloServer.js +113 -0
- package/src/gql.js +22 -0
- package/src/moleculerApollo.js +58 -0
- package/src/service.js +834 -0
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
|
+
};
|