sofa-api 0.5.0 → 0.7.0

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.
Files changed (55) hide show
  1. package/{dist/ast.d.ts → ast.d.ts} +7 -7
  2. package/{dist/common.d.ts → common.d.ts} +2 -1
  3. package/express.d.ts +5 -0
  4. package/index.cjs.js +1058 -0
  5. package/{dist/index.d.ts → index.d.ts} +6 -6
  6. package/index.esm.js +1050 -0
  7. package/{dist/logger.d.ts → logger.d.ts} +1 -1
  8. package/{dist/open-api → open-api}/index.d.ts +14 -12
  9. package/{dist/open-api → open-api}/interfaces.d.ts +325 -325
  10. package/{dist/open-api → open-api}/operations.d.ts +7 -7
  11. package/{dist/open-api → open-api}/types.d.ts +3 -3
  12. package/{dist/open-api → open-api}/utils.d.ts +2 -2
  13. package/{dist/operation.d.ts → operation.d.ts} +12 -12
  14. package/package.json +23 -71
  15. package/{dist/parse.d.ts → parse.d.ts} +6 -6
  16. package/{dist/sofa.d.ts → sofa.d.ts} +33 -33
  17. package/{dist/subscriptions.d.ts → subscriptions.d.ts} +38 -38
  18. package/{dist/types.d.ts → types.d.ts} +17 -17
  19. package/.DS_Store +0 -0
  20. package/.git/logs/refs/remotes/origin/changelog +0 -1
  21. package/.git/refs/remotes/origin/changelog +0 -1
  22. package/CHANGELOG.md +0 -30
  23. package/LICENSE +0 -21
  24. package/README.md +0 -276
  25. package/dist/ast.js +0 -16
  26. package/dist/ast.js.map +0 -1
  27. package/dist/common.js +0 -8
  28. package/dist/common.js.map +0 -1
  29. package/dist/express.d.ts +0 -5
  30. package/dist/express.js +0 -226
  31. package/dist/express.js.map +0 -1
  32. package/dist/index.js +0 -13
  33. package/dist/index.js.map +0 -1
  34. package/dist/logger.js +0 -8
  35. package/dist/logger.js.map +0 -1
  36. package/dist/open-api/index.js +0 -67
  37. package/dist/open-api/index.js.map +0 -1
  38. package/dist/open-api/interfaces.js +0 -3
  39. package/dist/open-api/interfaces.js.map +0 -1
  40. package/dist/open-api/operations.js +0 -112
  41. package/dist/open-api/operations.js.map +0 -1
  42. package/dist/open-api/types.js +0 -50
  43. package/dist/open-api/types.js.map +0 -1
  44. package/dist/open-api/utils.js +0 -29
  45. package/dist/open-api/utils.js.map +0 -1
  46. package/dist/operation.js +0 -258
  47. package/dist/operation.js.map +0 -1
  48. package/dist/parse.js +0 -46
  49. package/dist/parse.js.map +0 -1
  50. package/dist/sofa.js +0 -90
  51. package/dist/sofa.js.map +0 -1
  52. package/dist/subscriptions.js +0 -174
  53. package/dist/subscriptions.js.map +0 -1
  54. package/dist/types.js +0 -3
  55. package/dist/types.js.map +0 -1
package/index.cjs.js ADDED
@@ -0,0 +1,1058 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
6
+
7
+ const tslib = require('tslib');
8
+ const express = require('express');
9
+ const graphql = require('graphql');
10
+ const changeCase = require('change-case');
11
+ const winston = require('winston');
12
+ const uuid = require('uuid');
13
+ const axios = _interopDefault(require('axios'));
14
+ const iterall = require('iterall');
15
+ const yamljs = require('yamljs');
16
+ const fs = require('fs');
17
+ const titleCase = require('title-case');
18
+
19
+ const logger = winston.createLogger({
20
+ transports: [new winston.transports.Console()],
21
+ format: winston.format.combine(winston.format.colorize(), winston.format.simple()),
22
+ });
23
+
24
+ let operationVariables = [];
25
+ function addOperationVariable(variable) {
26
+ operationVariables.push(variable);
27
+ }
28
+ function resetOperationVariables() {
29
+ operationVariables = [];
30
+ }
31
+ function buildOperationName(name) {
32
+ return changeCase.camelCase(name);
33
+ }
34
+ function buildOperation({ schema, kind, field, models, ignore, depthLimit, }) {
35
+ resetOperationVariables();
36
+ logger.debug(`[Sofa] Building ${field} ${kind}`);
37
+ const document = buildDocumentNode({
38
+ schema,
39
+ fieldName: field,
40
+ kind,
41
+ models,
42
+ ignore,
43
+ depthLimit: depthLimit || 1,
44
+ });
45
+ // attach variables
46
+ document.definitions[0].variableDefinitions = [
47
+ ...operationVariables,
48
+ ];
49
+ resetOperationVariables();
50
+ return document;
51
+ }
52
+ function buildDocumentNode({ schema, fieldName, kind, models, ignore, depthLimit, }) {
53
+ const typeMap = {
54
+ query: schema.getQueryType(),
55
+ mutation: schema.getMutationType(),
56
+ subscription: schema.getSubscriptionType(),
57
+ };
58
+ const type = typeMap[kind];
59
+ const field = type.getFields()[fieldName];
60
+ const operationName = buildOperationName(`${fieldName}_${kind}`);
61
+ if (field.args) {
62
+ field.args.forEach(arg => {
63
+ addOperationVariable(resolveVariable(arg));
64
+ });
65
+ }
66
+ const operationNode = {
67
+ kind: graphql.Kind.OPERATION_DEFINITION,
68
+ operation: kind,
69
+ name: {
70
+ kind: 'Name',
71
+ value: operationName,
72
+ },
73
+ variableDefinitions: [],
74
+ selectionSet: {
75
+ kind: graphql.Kind.SELECTION_SET,
76
+ selections: [
77
+ resolveField({
78
+ type,
79
+ field,
80
+ models,
81
+ firstCall: true,
82
+ path: [],
83
+ ancestors: [],
84
+ ignore,
85
+ depthLimit,
86
+ schema,
87
+ }),
88
+ ],
89
+ },
90
+ };
91
+ const document = {
92
+ kind: graphql.Kind.DOCUMENT,
93
+ definitions: [operationNode],
94
+ };
95
+ return document;
96
+ }
97
+ function resolveSelectionSet({ parent, type, models, firstCall, path, ancestors, ignore, depthLimit, schema, }) {
98
+ if (graphql.isUnionType(type)) {
99
+ const types = type.getTypes();
100
+ return {
101
+ kind: graphql.Kind.SELECTION_SET,
102
+ selections: types
103
+ .filter(t => !hasCircularRef([...ancestors, t], {
104
+ depth: depthLimit,
105
+ }))
106
+ .map(t => {
107
+ return {
108
+ kind: graphql.Kind.INLINE_FRAGMENT,
109
+ typeCondition: {
110
+ kind: graphql.Kind.NAMED_TYPE,
111
+ name: {
112
+ kind: graphql.Kind.NAME,
113
+ value: t.name,
114
+ },
115
+ },
116
+ selectionSet: resolveSelectionSet({
117
+ parent: type,
118
+ type: t,
119
+ models,
120
+ path,
121
+ ancestors,
122
+ ignore,
123
+ depthLimit,
124
+ schema,
125
+ }),
126
+ };
127
+ }),
128
+ };
129
+ }
130
+ if (graphql.isInterfaceType(type)) {
131
+ const types = Object.values(schema.getTypeMap()).filter(t => graphql.isObjectType(t) && t.getInterfaces().includes(type));
132
+ return {
133
+ kind: graphql.Kind.SELECTION_SET,
134
+ selections: types
135
+ .filter(t => !hasCircularRef([...ancestors, t], {
136
+ depth: depthLimit,
137
+ }))
138
+ .map(t => {
139
+ return {
140
+ kind: graphql.Kind.INLINE_FRAGMENT,
141
+ typeCondition: {
142
+ kind: graphql.Kind.NAMED_TYPE,
143
+ name: {
144
+ kind: graphql.Kind.NAME,
145
+ value: t.name,
146
+ },
147
+ },
148
+ selectionSet: resolveSelectionSet({
149
+ parent: type,
150
+ type: t,
151
+ models,
152
+ path,
153
+ ancestors,
154
+ ignore,
155
+ depthLimit,
156
+ schema,
157
+ }),
158
+ };
159
+ }),
160
+ };
161
+ }
162
+ if (graphql.isObjectType(type)) {
163
+ const isIgnored = ignore.includes(type.name) ||
164
+ ignore.includes(`${parent.name}.${path[path.length - 1]}`);
165
+ const isModel = models.includes(type.name);
166
+ if (!firstCall && isModel && !isIgnored) {
167
+ return {
168
+ kind: graphql.Kind.SELECTION_SET,
169
+ selections: [
170
+ {
171
+ kind: graphql.Kind.FIELD,
172
+ name: {
173
+ kind: graphql.Kind.NAME,
174
+ value: 'id',
175
+ },
176
+ },
177
+ ],
178
+ };
179
+ }
180
+ const fields = type.getFields();
181
+ return {
182
+ kind: graphql.Kind.SELECTION_SET,
183
+ selections: Object.keys(fields)
184
+ .filter(fieldName => {
185
+ return !hasCircularRef([...ancestors, graphql.getNamedType(fields[fieldName].type)], {
186
+ depth: depthLimit,
187
+ });
188
+ })
189
+ .map(fieldName => {
190
+ return resolveField({
191
+ type: type,
192
+ field: fields[fieldName],
193
+ models,
194
+ path: [...path, fieldName],
195
+ ancestors,
196
+ ignore,
197
+ depthLimit,
198
+ schema,
199
+ });
200
+ }),
201
+ };
202
+ }
203
+ }
204
+ function resolveVariable(arg, name) {
205
+ function resolveVariableType(type) {
206
+ if (graphql.isListType(type)) {
207
+ return {
208
+ kind: graphql.Kind.LIST_TYPE,
209
+ type: resolveVariableType(type.ofType),
210
+ };
211
+ }
212
+ if (graphql.isNonNullType(type)) {
213
+ return {
214
+ kind: graphql.Kind.NON_NULL_TYPE,
215
+ type: resolveVariableType(type.ofType),
216
+ };
217
+ }
218
+ return {
219
+ kind: graphql.Kind.NAMED_TYPE,
220
+ name: {
221
+ kind: graphql.Kind.NAME,
222
+ value: type.name,
223
+ },
224
+ };
225
+ }
226
+ return {
227
+ kind: graphql.Kind.VARIABLE_DEFINITION,
228
+ variable: {
229
+ kind: graphql.Kind.VARIABLE,
230
+ name: {
231
+ kind: graphql.Kind.NAME,
232
+ value: name || arg.name,
233
+ },
234
+ },
235
+ type: resolveVariableType(arg.type),
236
+ };
237
+ }
238
+ function getArgumentName(name, path) {
239
+ return changeCase.camelCase([...path, name].join('_'));
240
+ }
241
+ function resolveField({ type, field, models, firstCall, path, ancestors, ignore, depthLimit, schema, }) {
242
+ const namedType = graphql.getNamedType(field.type);
243
+ let args = [];
244
+ if (field.args && field.args.length) {
245
+ args = field.args.map(arg => {
246
+ if (!firstCall) {
247
+ addOperationVariable(resolveVariable(arg, getArgumentName(arg.name, path)));
248
+ }
249
+ return {
250
+ kind: graphql.Kind.ARGUMENT,
251
+ name: {
252
+ kind: graphql.Kind.NAME,
253
+ value: arg.name,
254
+ },
255
+ value: {
256
+ kind: graphql.Kind.VARIABLE,
257
+ name: {
258
+ kind: graphql.Kind.NAME,
259
+ value: getArgumentName(arg.name, path),
260
+ },
261
+ },
262
+ };
263
+ });
264
+ }
265
+ if (!graphql.isScalarType(namedType)) {
266
+ return {
267
+ kind: graphql.Kind.FIELD,
268
+ name: {
269
+ kind: graphql.Kind.NAME,
270
+ value: field.name,
271
+ },
272
+ selectionSet: resolveSelectionSet({
273
+ parent: type,
274
+ type: namedType,
275
+ models,
276
+ firstCall,
277
+ path: [...path, field.name],
278
+ ancestors: [...ancestors, type],
279
+ ignore,
280
+ depthLimit,
281
+ schema,
282
+ }),
283
+ arguments: args,
284
+ };
285
+ }
286
+ return {
287
+ kind: graphql.Kind.FIELD,
288
+ name: {
289
+ kind: graphql.Kind.NAME,
290
+ value: field.name,
291
+ },
292
+ arguments: args,
293
+ };
294
+ }
295
+ function hasCircularRef(types, config = {
296
+ depth: 1,
297
+ }) {
298
+ const type = types[types.length - 1];
299
+ if (graphql.isScalarType(type)) {
300
+ return false;
301
+ }
302
+ const size = types.filter(t => t.name === type.name).length;
303
+ return size > config.depth;
304
+ }
305
+
306
+ function getOperationInfo(doc) {
307
+ const op = graphql.getOperationAST(doc, null);
308
+ if (!op) {
309
+ return;
310
+ }
311
+ return {
312
+ operation: op,
313
+ name: op.name.value,
314
+ variables: op.variableDefinitions || [],
315
+ };
316
+ }
317
+
318
+ function convertName(name) {
319
+ return changeCase.paramCase(name);
320
+ }
321
+ function isNil(val) {
322
+ return val == null;
323
+ }
324
+
325
+ function createSofa(config) {
326
+ logger.debug('[Sofa] Created');
327
+ const models = extractsModels(config.schema);
328
+ const ignore = config.ignore || [];
329
+ logger.debug(`[Sofa] models: ${models.join(', ')}`);
330
+ logger.debug(`[Sofa] ignore: ${ignore.join(', ')}`);
331
+ return Object.assign({ context({ req }) {
332
+ return { req };
333
+ }, execute: graphql.graphql, models,
334
+ ignore }, config);
335
+ }
336
+ // Objects and Unions are the only things that are used to define return types
337
+ // and both might contain an ID
338
+ // We don't treat Unions as models because
339
+ // they might represent an Object that is not a model
340
+ // We check it later, when an operation is being built
341
+ function extractsModels(schema) {
342
+ const modelMap = {};
343
+ const query = schema.getQueryType();
344
+ const fields = query.getFields();
345
+ // if Query[type] (no args) and Query[type](just id as an argument)
346
+ // loop through every field
347
+ for (const fieldName in fields) {
348
+ const field = fields[fieldName];
349
+ const namedType = graphql.getNamedType(field.type);
350
+ if (hasID(namedType)) {
351
+ if (!modelMap[namedType.name]) {
352
+ modelMap[namedType.name] = {};
353
+ }
354
+ if (isArrayOf(field.type, namedType)) {
355
+ // check if type is a list
356
+ // check if name of a field matches a name of a named type (in plural)
357
+ // check if has no non-optional arguments
358
+ // add to registry with `list: true`
359
+ const sameName = isNameEqual(field.name, namedType.name + 's');
360
+ const allOptionalArguments = !field.args.some(arg => graphql.isNonNullType(arg.type));
361
+ modelMap[namedType.name].list = sameName && allOptionalArguments;
362
+ }
363
+ else if (graphql.isObjectType(field.type) ||
364
+ (graphql.isNonNullType(field.type) && graphql.isObjectType(field.type.ofType))) {
365
+ // check if type is a graphql object type
366
+ // check if name of a field matches with name of an object type
367
+ // check if has only one argument named `id`
368
+ // add to registry with `single: true`
369
+ const sameName = isNameEqual(field.name, namedType.name);
370
+ const hasIdArgument = field.args.length === 1 && field.args[0].name === 'id';
371
+ modelMap[namedType.name].single = sameName && hasIdArgument;
372
+ }
373
+ }
374
+ }
375
+ return Object.keys(modelMap).filter(name => modelMap[name].list && modelMap[name].single);
376
+ }
377
+ // it's dumb but let's leave it for now
378
+ function isArrayOf(type, expected) {
379
+ if (isOptionalList(type)) {
380
+ return true;
381
+ }
382
+ if (graphql.isNonNullType(type) && isOptionalList(type.ofType)) {
383
+ return true;
384
+ }
385
+ function isOptionalList(list) {
386
+ if (graphql.isListType(list)) {
387
+ if (list.ofType.name === expected.name) {
388
+ return true;
389
+ }
390
+ if (graphql.isNonNullType(list.ofType) &&
391
+ list.ofType.ofType.name === expected.name) {
392
+ return true;
393
+ }
394
+ }
395
+ }
396
+ return false;
397
+ }
398
+ function hasID(type) {
399
+ return graphql.isObjectType(type) && !!type.getFields().id;
400
+ }
401
+ function isNameEqual(a, b) {
402
+ return convertName(a) === convertName(b);
403
+ }
404
+ function isContextFn(context) {
405
+ return typeof context === 'function';
406
+ }
407
+
408
+ function parseVariable({ value, variable, schema, }) {
409
+ if (isNil(value)) {
410
+ return;
411
+ }
412
+ return resolveVariable$1({
413
+ value,
414
+ type: variable.type,
415
+ schema,
416
+ });
417
+ }
418
+ function resolveVariable$1({ value, type, schema, }) {
419
+ if (type.kind === graphql.Kind.NAMED_TYPE) {
420
+ const namedType = schema.getType(type.name.value);
421
+ if (graphql.isScalarType(namedType)) {
422
+ // GraphQLBoolean.serialize expects a boolean or a number only
423
+ if (graphql.isEqualType(graphql.GraphQLBoolean, namedType)) {
424
+ // we don't support TRUE
425
+ value = value === 'true';
426
+ }
427
+ return namedType.serialize(value);
428
+ }
429
+ if (graphql.isInputObjectType(namedType)) {
430
+ return value && typeof value === 'object' ? value : JSON.parse(value);
431
+ }
432
+ return value;
433
+ }
434
+ if (type.kind === graphql.Kind.LIST_TYPE) {
435
+ return value.map(val => resolveVariable$1({
436
+ value: val,
437
+ type: type.type,
438
+ schema,
439
+ }));
440
+ }
441
+ if (type.kind === graphql.Kind.NON_NULL_TYPE) {
442
+ return resolveVariable$1({
443
+ value: value,
444
+ type: type.type,
445
+ schema,
446
+ });
447
+ }
448
+ }
449
+
450
+ class SubscriptionManager {
451
+ constructor(sofa) {
452
+ this.sofa = sofa;
453
+ this.operations = new Map();
454
+ this.clients = new Map();
455
+ this.buildOperations();
456
+ }
457
+ start(event, { req, res, }) {
458
+ return tslib.__awaiter(this, void 0, void 0, function* () {
459
+ const id = uuid.v4();
460
+ const name = event.subscription;
461
+ if (!this.operations.has(name)) {
462
+ throw new Error(`Subscription '${name}' is not available`);
463
+ }
464
+ const { document, operationName, variables } = this.operations.get(name);
465
+ logger.info(`[Subscription] Start ${id}`, event);
466
+ const result = yield this.execute({
467
+ id,
468
+ name,
469
+ url: event.url,
470
+ document,
471
+ operationName,
472
+ variables,
473
+ req,
474
+ res,
475
+ });
476
+ if (typeof result !== 'undefined') {
477
+ return result;
478
+ }
479
+ return { id };
480
+ });
481
+ }
482
+ stop(id) {
483
+ return tslib.__awaiter(this, void 0, void 0, function* () {
484
+ logger.info(`[Subscription] Stop ${id}`);
485
+ if (!this.clients.has(id)) {
486
+ throw new Error(`Subscription with ID '${id}' does not exist`);
487
+ }
488
+ const execution = this.clients.get(id);
489
+ if (execution.iterator.return) {
490
+ execution.iterator.return();
491
+ }
492
+ this.clients.delete(id);
493
+ return { id };
494
+ });
495
+ }
496
+ update(event, { req, res, }) {
497
+ return tslib.__awaiter(this, void 0, void 0, function* () {
498
+ const { variables, id } = event;
499
+ logger.info(`[Subscription] Update ${id}`, event);
500
+ if (!this.clients.has(id)) {
501
+ throw new Error(`Subscription with ID '${id}' does not exist`);
502
+ }
503
+ const { name: subscription, url } = this.clients.get(id);
504
+ this.stop(id);
505
+ return this.start({
506
+ url,
507
+ subscription,
508
+ variables,
509
+ }, {
510
+ req,
511
+ res,
512
+ });
513
+ });
514
+ }
515
+ execute({ id, document, name, url, operationName, variables, req, res, }) {
516
+ return tslib.__awaiter(this, void 0, void 0, function* () {
517
+ const variableNodes = this.operations.get(name).variables;
518
+ const variableValues = variableNodes.reduce((values, variable) => {
519
+ const value = parseVariable({
520
+ value: variables[variable.variable.name.value],
521
+ variable,
522
+ schema: this.sofa.schema,
523
+ });
524
+ if (typeof value === 'undefined') {
525
+ return values;
526
+ }
527
+ return Object.assign(Object.assign({}, values), { [name]: value });
528
+ }, {});
529
+ const C = isContextFn(this.sofa.context)
530
+ ? yield this.sofa.context({ req, res })
531
+ : this.sofa.context;
532
+ const execution = yield graphql.subscribe({
533
+ schema: this.sofa.schema,
534
+ document,
535
+ operationName,
536
+ variableValues,
537
+ contextValue: C,
538
+ });
539
+ if (iterall.isAsyncIterable(execution)) {
540
+ // successful
541
+ // add execution to clients
542
+ this.clients.set(id, {
543
+ name,
544
+ url,
545
+ iterator: execution,
546
+ });
547
+ // success
548
+ iterall.forAwaitEach(execution, (result) => tslib.__awaiter(this, void 0, void 0, function* () {
549
+ yield this.sendData({
550
+ id,
551
+ result,
552
+ });
553
+ })).then(() => {
554
+ // completes
555
+ this.stop(id);
556
+ }, e => {
557
+ logger.info(`Subscription #${id} closed`);
558
+ logger.error(e);
559
+ this.stop(id);
560
+ });
561
+ }
562
+ else {
563
+ return execution;
564
+ }
565
+ });
566
+ }
567
+ sendData({ id, result }) {
568
+ return tslib.__awaiter(this, void 0, void 0, function* () {
569
+ if (!this.clients.has(id)) {
570
+ throw new Error(`Subscription with ID '${id}' does not exist`);
571
+ }
572
+ const { url } = this.clients.get(id);
573
+ logger.info(`[Subscription] Trigger ${id}`);
574
+ yield axios.post(url, result);
575
+ });
576
+ }
577
+ buildOperations() {
578
+ const subscription = this.sofa.schema.getSubscriptionType();
579
+ if (!subscription) {
580
+ return;
581
+ }
582
+ const fieldMap = subscription.getFields();
583
+ for (const field in fieldMap) {
584
+ const document = buildOperation({
585
+ kind: 'subscription',
586
+ field,
587
+ schema: this.sofa.schema,
588
+ models: this.sofa.models,
589
+ ignore: this.sofa.ignore,
590
+ });
591
+ const { variables, name: operationName } = getOperationInfo(document);
592
+ this.operations.set(field, {
593
+ operationName,
594
+ document,
595
+ variables,
596
+ });
597
+ }
598
+ }
599
+ }
600
+
601
+ function createRouter(sofa) {
602
+ logger.debug('[Sofa] Creating router');
603
+ const router = express.Router();
604
+ const queryType = sofa.schema.getQueryType();
605
+ const mutationType = sofa.schema.getMutationType();
606
+ const subscriptionManager = new SubscriptionManager(sofa);
607
+ if (queryType) {
608
+ Object.keys(queryType.getFields()).forEach(fieldName => {
609
+ const route = createQueryRoute({ sofa, router, fieldName });
610
+ if (sofa.onRoute) {
611
+ sofa.onRoute(route);
612
+ }
613
+ });
614
+ }
615
+ if (mutationType) {
616
+ Object.keys(mutationType.getFields()).forEach(fieldName => {
617
+ const route = createMutationRoute({ sofa, router, fieldName });
618
+ if (sofa.onRoute) {
619
+ sofa.onRoute(route);
620
+ }
621
+ });
622
+ }
623
+ router.post('/webhook', useAsync((req, res) => tslib.__awaiter(this, void 0, void 0, function* () {
624
+ const { subscription, variables, url } = req.body;
625
+ try {
626
+ const result = yield subscriptionManager.start({
627
+ subscription,
628
+ variables,
629
+ url,
630
+ }, { req, res });
631
+ res.statusCode = 200;
632
+ res.statusMessage = 'OK';
633
+ res.json(result);
634
+ }
635
+ catch (e) {
636
+ res.statusCode = 500;
637
+ res.statusMessage = 'Subscription failed';
638
+ res.json(e);
639
+ }
640
+ })));
641
+ router.post('/webhook/:id', useAsync((req, res) => tslib.__awaiter(this, void 0, void 0, function* () {
642
+ const id = req.params.id;
643
+ const variables = req.body.variables;
644
+ try {
645
+ const result = yield subscriptionManager.update({
646
+ id,
647
+ variables,
648
+ }, {
649
+ req,
650
+ res,
651
+ });
652
+ res.statusCode = 200;
653
+ res.statusMessage = 'OK';
654
+ res.json(result);
655
+ }
656
+ catch (e) {
657
+ res.statusCode = 500;
658
+ res.statusMessage = 'Subscription failed to update';
659
+ res.json(e);
660
+ }
661
+ })));
662
+ router.delete('/webhook/:id', useAsync((req, res) => tslib.__awaiter(this, void 0, void 0, function* () {
663
+ const id = req.params.id;
664
+ try {
665
+ const result = yield subscriptionManager.stop(id);
666
+ res.statusCode = 200;
667
+ res.statusMessage = 'OK';
668
+ res.json(result);
669
+ }
670
+ catch (e) {
671
+ res.statusCode = 500;
672
+ res.statusMessage = 'Subscription failed to stop';
673
+ res.json(e);
674
+ }
675
+ })));
676
+ return router;
677
+ }
678
+ function createQueryRoute({ sofa, router, fieldName, }) {
679
+ logger.debug(`[Router] Creating ${fieldName} query`);
680
+ const queryType = sofa.schema.getQueryType();
681
+ const operation = buildOperation({
682
+ kind: 'query',
683
+ schema: sofa.schema,
684
+ field: fieldName,
685
+ models: sofa.models,
686
+ ignore: sofa.ignore,
687
+ });
688
+ const info = getOperationInfo(operation);
689
+ const field = queryType.getFields()[fieldName];
690
+ const fieldType = field.type;
691
+ const isSingle = graphql.isObjectType(fieldType) ||
692
+ (graphql.isNonNullType(fieldType) && graphql.isObjectType(fieldType.ofType));
693
+ const hasIdArgument = field.args.some(arg => arg.name === 'id');
694
+ const path = getPath(fieldName, isSingle && hasIdArgument);
695
+ const method = produceMethod({
696
+ typeName: queryType.name,
697
+ fieldName,
698
+ methodMap: sofa.method,
699
+ defaultValue: 'GET',
700
+ });
701
+ router[method.toLocaleLowerCase()](path, useHandler({ info, fieldName, sofa, operation }));
702
+ logger.debug(`[Router] ${fieldName} query available at ${method} ${path}`);
703
+ return {
704
+ document: operation,
705
+ path,
706
+ method: method.toUpperCase(),
707
+ };
708
+ }
709
+ function createMutationRoute({ sofa, router, fieldName, }) {
710
+ logger.debug(`[Router] Creating ${fieldName} mutation`);
711
+ const mutationType = sofa.schema.getMutationType();
712
+ const operation = buildOperation({
713
+ kind: 'mutation',
714
+ schema: sofa.schema,
715
+ field: fieldName,
716
+ models: sofa.models,
717
+ ignore: sofa.ignore,
718
+ });
719
+ const info = getOperationInfo(operation);
720
+ const path = getPath(fieldName);
721
+ const method = produceMethod({
722
+ typeName: mutationType.name,
723
+ fieldName,
724
+ methodMap: sofa.method,
725
+ defaultValue: 'POST',
726
+ });
727
+ router[method.toLowerCase()](path, useHandler({ info, fieldName, sofa, operation }));
728
+ logger.debug(`[Router] ${fieldName} mutation available at ${method} ${path}`);
729
+ return {
730
+ document: operation,
731
+ path,
732
+ method: method.toUpperCase(),
733
+ };
734
+ }
735
+ function useHandler(config) {
736
+ const { sofa, operation, fieldName } = config;
737
+ const info = config.info;
738
+ return useAsync((req, res) => tslib.__awaiter(this, void 0, void 0, function* () {
739
+ const variableValues = info.variables.reduce((variables, variable) => {
740
+ const name = variable.variable.name.value;
741
+ const value = parseVariable({
742
+ value: pickParam(req, name),
743
+ variable,
744
+ schema: sofa.schema,
745
+ });
746
+ if (typeof value === 'undefined') {
747
+ return variables;
748
+ }
749
+ return Object.assign(Object.assign({}, variables), { [name]: value });
750
+ }, {});
751
+ const contextValue = isContextFn(sofa.context)
752
+ ? yield sofa.context({ req, res })
753
+ : sofa.context;
754
+ const result = yield sofa.execute({
755
+ schema: sofa.schema,
756
+ source: graphql.print(operation),
757
+ contextValue,
758
+ variableValues,
759
+ operationName: info.operation.name && info.operation.name.value,
760
+ });
761
+ if (result.errors) {
762
+ const defaultErrorHandler = (res, errors) => {
763
+ res.statusCode = 500;
764
+ res.json(errors[0]);
765
+ };
766
+ const errorHandler = sofa.errorHandler || defaultErrorHandler;
767
+ errorHandler(res, result.errors);
768
+ return;
769
+ }
770
+ res.json(result.data && result.data[fieldName]);
771
+ }));
772
+ }
773
+ function getPath(fieldName, hasId = false) {
774
+ return `/${convertName(fieldName)}${hasId ? '/:id' : ''}`;
775
+ }
776
+ function pickParam(req, name) {
777
+ if (req.params && req.params.hasOwnProperty(name)) {
778
+ return req.params[name];
779
+ }
780
+ if (req.query && req.query.hasOwnProperty(name)) {
781
+ return req.query[name];
782
+ }
783
+ if (req.body && req.body.hasOwnProperty(name)) {
784
+ return req.body[name];
785
+ }
786
+ }
787
+ function useAsync(handler) {
788
+ return (req, res, next) => {
789
+ Promise.resolve(handler(req, res, next)).catch(e => next(e));
790
+ };
791
+ }
792
+ function produceMethod({ typeName, fieldName, methodMap, defaultValue, }) {
793
+ const path = `${typeName}.${fieldName}`;
794
+ if (methodMap && methodMap[path]) {
795
+ return methodMap[path];
796
+ }
797
+ return defaultValue;
798
+ }
799
+
800
+ function mapToPrimitive(type) {
801
+ const formatMap = {
802
+ Int: {
803
+ type: 'integer',
804
+ format: 'int32',
805
+ },
806
+ Float: {
807
+ type: 'number',
808
+ format: 'float',
809
+ },
810
+ String: {
811
+ type: 'string',
812
+ },
813
+ Boolean: {
814
+ type: 'boolean',
815
+ },
816
+ ID: {
817
+ type: 'string',
818
+ },
819
+ };
820
+ if (formatMap[type]) {
821
+ return formatMap[type];
822
+ }
823
+ }
824
+ function mapToRef(type) {
825
+ return `#/components/schemas/${type}`;
826
+ }
827
+
828
+ function buildSchemaObjectFromType(type) {
829
+ const required = [];
830
+ const properties = {};
831
+ const fields = type.getFields();
832
+ for (const fieldName in fields) {
833
+ const field = fields[fieldName];
834
+ if (graphql.isNonNullType(field.type)) {
835
+ required.push(field.name);
836
+ }
837
+ properties[fieldName] = resolveField$1(field);
838
+ if (field.description) {
839
+ properties[fieldName].description = field.description;
840
+ }
841
+ }
842
+ return Object.assign(Object.assign(Object.assign({ type: 'object' }, (required.length ? { required } : {})), { properties }), (type.description ? { description: type.description } : {}));
843
+ }
844
+ function resolveField$1(field) {
845
+ return resolveFieldType(field.type);
846
+ }
847
+ // array -> [type]
848
+ // type -> $ref
849
+ // scalar -> swagger primitive
850
+ function resolveFieldType(type) {
851
+ if (graphql.isNonNullType(type)) {
852
+ return resolveFieldType(type.ofType);
853
+ }
854
+ if (graphql.isListType(type)) {
855
+ return {
856
+ type: 'array',
857
+ items: resolveFieldType(type.ofType),
858
+ };
859
+ }
860
+ if (graphql.isObjectType(type)) {
861
+ return {
862
+ $ref: mapToRef(type.name),
863
+ };
864
+ }
865
+ if (graphql.isScalarType(type)) {
866
+ return (mapToPrimitive(type.name) || {
867
+ type: 'object',
868
+ });
869
+ }
870
+ return {
871
+ type: 'object',
872
+ };
873
+ }
874
+
875
+ function buildPathFromOperation({ url, schema, operation, useRequestBody, }) {
876
+ const info = getOperationInfo(operation);
877
+ const description = resolveDescription(schema, info.operation);
878
+ return Object.assign(Object.assign({ operationId: info.name }, (useRequestBody
879
+ ? {
880
+ requestBody: {
881
+ content: {
882
+ 'application/json': {
883
+ schema: resolveRequestBody(info.operation.variableDefinitions),
884
+ },
885
+ },
886
+ },
887
+ }
888
+ : {
889
+ parameters: resolveParameters(url, info.operation.variableDefinitions),
890
+ })), { responses: {
891
+ 200: {
892
+ description,
893
+ content: {
894
+ 'application/json': {
895
+ schema: resolveResponse({
896
+ schema,
897
+ operation: info.operation,
898
+ }),
899
+ },
900
+ },
901
+ },
902
+ } });
903
+ }
904
+ function resolveParameters(url, variables) {
905
+ if (!variables) {
906
+ return [];
907
+ }
908
+ return variables.map((variable) => {
909
+ return {
910
+ in: isInPath(url, variable.variable.name.value) ? 'path' : 'query',
911
+ name: variable.variable.name.value,
912
+ required: variable.type.kind === graphql.Kind.NON_NULL_TYPE,
913
+ schema: resolveParamSchema(variable.type),
914
+ };
915
+ });
916
+ }
917
+ function resolveRequestBody(variables) {
918
+ if (!variables) {
919
+ return {};
920
+ }
921
+ const properties = {};
922
+ const required = [];
923
+ variables.forEach(variable => {
924
+ if (variable.type.kind === graphql.Kind.NON_NULL_TYPE) {
925
+ required.push(variable.variable.name.value);
926
+ }
927
+ properties[variable.variable.name.value] = resolveParamSchema(variable.type);
928
+ });
929
+ return Object.assign({ type: 'object', properties }, (required.length ? { required } : {}));
930
+ }
931
+ // array -> [type]
932
+ // type -> $ref
933
+ // scalar -> swagger primitive
934
+ function resolveParamSchema(type) {
935
+ if (type.kind === graphql.Kind.NON_NULL_TYPE) {
936
+ return resolveParamSchema(type.type);
937
+ }
938
+ if (type.kind === graphql.Kind.LIST_TYPE) {
939
+ return {
940
+ type: 'array',
941
+ items: resolveParamSchema(type.type),
942
+ };
943
+ }
944
+ const primitive = mapToPrimitive(type.name.value);
945
+ return (primitive || {
946
+ $ref: mapToRef(type.name.value),
947
+ });
948
+ }
949
+ function resolveResponse({ schema, operation, }) {
950
+ const operationType = operation.operation;
951
+ const rootField = operation.selectionSet.selections[0];
952
+ if (rootField.kind === graphql.Kind.FIELD) {
953
+ if (operationType === 'query') {
954
+ const queryType = schema.getQueryType();
955
+ const field = queryType.getFields()[rootField.name.value];
956
+ return resolveFieldType(field.type);
957
+ }
958
+ if (operationType === 'mutation') {
959
+ const mutationType = schema.getMutationType();
960
+ const field = mutationType.getFields()[rootField.name.value];
961
+ return resolveFieldType(field.type);
962
+ }
963
+ }
964
+ }
965
+ function isInPath(url, param) {
966
+ return url.indexOf(`{${param}}`) !== -1;
967
+ }
968
+ function resolveDescription(schema, operation) {
969
+ const selection = operation.selectionSet.selections[0];
970
+ const fieldName = selection.name.value;
971
+ const typeDefinition = schema.getType(titleCase.titleCase(operation.operation));
972
+ if (!typeDefinition) {
973
+ return '';
974
+ }
975
+ const definitionNode = typeDefinition.astNode || graphql.parse(graphql.printType(typeDefinition)).definitions[0];
976
+ if (!isObjectTypeDefinitionNode(definitionNode)) {
977
+ return '';
978
+ }
979
+ const fieldNode = definitionNode.fields.find(field => field.name.value === fieldName);
980
+ const descriptionDefinition = fieldNode && fieldNode.description;
981
+ return descriptionDefinition && descriptionDefinition.value
982
+ ? descriptionDefinition.value
983
+ : '';
984
+ }
985
+ function isObjectTypeDefinitionNode(node) {
986
+ return node.kind === graphql.Kind.OBJECT_TYPE_DEFINITION;
987
+ }
988
+
989
+ function OpenAPI({ schema, info, components, security, }) {
990
+ const types = schema.getTypeMap();
991
+ const swagger = {
992
+ openapi: '3.0.0',
993
+ info,
994
+ paths: {},
995
+ components: {
996
+ schemas: {},
997
+ },
998
+ };
999
+ for (const typeName in types) {
1000
+ const type = types[typeName];
1001
+ if ((graphql.isObjectType(type) || graphql.isInputObjectType(type)) &&
1002
+ !graphql.isIntrospectionType(type)) {
1003
+ swagger.components.schemas[typeName] = buildSchemaObjectFromType(type);
1004
+ }
1005
+ }
1006
+ if (components) {
1007
+ swagger.components = Object.assign(Object.assign({}, components), swagger.components);
1008
+ }
1009
+ if (security) {
1010
+ swagger.security = security;
1011
+ }
1012
+ return {
1013
+ addRoute(info, config) {
1014
+ const basePath = (config === null || config === void 0 ? void 0 : config.basePath) || '';
1015
+ const path = basePath +
1016
+ info.path.replace(/\:[a-z0-9]+\w/i, param => `{${param.replace(':', '')}}`);
1017
+ if (!swagger.paths[path]) {
1018
+ swagger.paths[path] = {};
1019
+ }
1020
+ swagger.paths[path][info.method.toLowerCase()] = buildPathFromOperation({
1021
+ url: path,
1022
+ operation: info.document,
1023
+ schema,
1024
+ useRequestBody: ['POST', 'PUT', 'PATCH'].includes(info.method),
1025
+ });
1026
+ },
1027
+ get() {
1028
+ return swagger;
1029
+ },
1030
+ save(filepath) {
1031
+ const isJSON = /\.json$/i;
1032
+ const isYAML = /.ya?ml$/i;
1033
+ if (isJSON.test(filepath)) {
1034
+ writeOutput(filepath, JSON.stringify(swagger, null, 2));
1035
+ }
1036
+ else if (isYAML.test(filepath)) {
1037
+ writeOutput(filepath, yamljs.stringify(swagger, Infinity));
1038
+ }
1039
+ else {
1040
+ throw new Error('We only support JSON and YAML files');
1041
+ }
1042
+ },
1043
+ };
1044
+ }
1045
+ function writeOutput(filepath, contents) {
1046
+ fs.writeFileSync(filepath, contents, {
1047
+ encoding: 'utf-8',
1048
+ });
1049
+ }
1050
+
1051
+ function useSofa(config) {
1052
+ return createRouter(createSofa(config));
1053
+ }
1054
+
1055
+ exports.OpenAPI = OpenAPI;
1056
+ exports.createSofa = createSofa;
1057
+ exports.default = useSofa;
1058
+ exports.useSofa = useSofa;