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