UNPKG

206 kBJavaScriptView Raw
1'use strict';
2
3function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
4
5const utils = require('@graphql-mesh/utils');
6const graphql = require('graphql');
7const graphqlScalars = require('graphql-scalars');
8const Swagger2OpenAPI = require('swagger2openapi');
9const jsonptr = require('json-ptr');
10const pluralize = require('pluralize');
11const JSONPath = require('jsonpath-plus');
12const formurlencoded = _interopDefault(require('form-urlencoded'));
13const urlJoin = _interopDefault(require('url-join'));
14const deepEqual = _interopDefault(require('deep-equal'));
15const fetchache = require('fetchache');
16
17/* eslint-disable @typescript-eslint/ban-types */
18// Copyright IBM Corp. 2018. All Rights Reserved.
19// Node module: openapi-to-graphql
20// This file is licensed under the MIT License.
21// License text available at https://opensource.org/licenses/MIT
22var GraphQLOperationType;
23(function (GraphQLOperationType) {
24 GraphQLOperationType[GraphQLOperationType["Query"] = 0] = "Query";
25 GraphQLOperationType[GraphQLOperationType["Mutation"] = 1] = "Mutation";
26 GraphQLOperationType[GraphQLOperationType["Subscription"] = 2] = "Subscription";
27})(GraphQLOperationType || (GraphQLOperationType = {}));
28
29/* eslint-disable @typescript-eslint/ban-types */
30/* eslint-disable no-sequences */
31// Copyright IBM Corp. 2018. All Rights Reserved.
32// Node module: openapi-to-graphql
33// This file is licensed under the MIT License.
34// License text available at https://opensource.org/licenses/MIT
35var MitigationTypes;
36(function (MitigationTypes) {
37 /**
38 * Problems with the OAS
39 *
40 * Should be caught by the module oas-validator
41 */
42 MitigationTypes["INVALID_OAS"] = "INVALID_OAS";
43 MitigationTypes["UNNAMED_PARAMETER"] = "UNNAMED_PARAMETER";
44 // General problems
45 MitigationTypes["AMBIGUOUS_UNION_MEMBERS"] = "AMBIGUOUS_UNION_MEMBERS";
46 MitigationTypes["CANNOT_GET_FIELD_TYPE"] = "CANNOT_GET_FIELD_TYPE";
47 MitigationTypes["COMBINE_SCHEMAS"] = "COMBINE_SCHEMAS";
48 MitigationTypes["DUPLICATE_FIELD_NAME"] = "DUPLICATE_FIELD_NAME";
49 MitigationTypes["DUPLICATE_LINK_KEY"] = "DUPLICATE_LINK_KEY";
50 MitigationTypes["INVALID_HTTP_METHOD"] = "INVALID_HTTP_METHOD";
51 MitigationTypes["INPUT_UNION"] = "INPUT_UNION";
52 MitigationTypes["MISSING_RESPONSE_SCHEMA"] = "MISSING_RESPONSE_SCHEMA";
53 MitigationTypes["MISSING_SCHEMA"] = "MISSING_SCHEMA";
54 MitigationTypes["MULTIPLE_RESPONSES"] = "MULTIPLE_RESPONSES";
55 MitigationTypes["NON_APPLICATION_JSON_SCHEMA"] = "NON_APPLICATION_JSON_SCHEMA";
56 MitigationTypes["OBJECT_MISSING_PROPERTIES"] = "OBJECT_MISSING_PROPERTIES";
57 MitigationTypes["UNKNOWN_TARGET_TYPE"] = "UNKNOWN_TARGET_TYPE";
58 MitigationTypes["UNRESOLVABLE_SCHEMA"] = "UNRESOLVABLE_SCHEMA";
59 MitigationTypes["UNSUPPORTED_HTTP_SECURITY_SCHEME"] = "UNSUPPORTED_HTTP_SECURITY_SCHEME";
60 MitigationTypes["UNSUPPORTED_JSON_SCHEMA_KEYWORD"] = "UNSUPPORTED_JSON_SCHEMA_KEYWORD";
61 MitigationTypes["CALLBACKS_MULTIPLE_OPERATION_OBJECTS"] = "CALLBACKS_MULTIPLE_OPERATION_OBJECTS";
62 // Links
63 MitigationTypes["AMBIGUOUS_LINK"] = "AMBIGUOUS_LINK";
64 MitigationTypes["LINK_NAME_COLLISION"] = "LINK_NAME_COLLISION";
65 MitigationTypes["UNRESOLVABLE_LINK"] = "UNRESOLVABLE_LINK";
66 // Multiple OAS
67 MitigationTypes["DUPLICATE_OPERATIONID"] = "DUPLICATE_OPERATIONID";
68 MitigationTypes["DUPLICATE_SECURITY_SCHEME"] = "DUPLICATE_SECURITY_SCHEME";
69 MitigationTypes["MULTIPLE_OAS_SAME_TITLE"] = "MULTIPLE_OAS_SAME_TITLE";
70 // Options
71 MitigationTypes["CUSTOM_RESOLVER_UNKNOWN_OAS"] = "CUSTOM_RESOLVER_UNKNOWN_OAS";
72 MitigationTypes["CUSTOM_RESOLVER_UNKNOWN_PATH_METHOD"] = "CUSTOM_RESOLVER_UNKNOWN_PATH_METHOD";
73 MitigationTypes["LIMIT_ARGUMENT_NAME_COLLISION"] = "LIMIT_ARGUMENT_NAME_COLLISION";
74 // Miscellaneous
75 MitigationTypes["OAUTH_SECURITY_SCHEME"] = "OAUTH_SECURITY_SCHEME";
76})(MitigationTypes || (MitigationTypes = {}));
77const mitigations = {
78 /**
79 * Problems with the OAS
80 *
81 * Should be caught by the module oas-validator
82 */
83 INVALID_OAS: 'Ignore issue and continue.',
84 UNNAMED_PARAMETER: 'Ignore parameter.',
85 // General problems
86 AMBIGUOUS_UNION_MEMBERS: 'Ignore issue and continue.',
87 CANNOT_GET_FIELD_TYPE: 'Ignore field and continue.',
88 COMBINE_SCHEMAS: 'Ignore combine schema keyword and continue.',
89 DUPLICATE_FIELD_NAME: 'Ignore field and maintain preexisting field.',
90 DUPLICATE_LINK_KEY: 'Ignore link and maintain preexisting link.',
91 INPUT_UNION: 'The data will be stored in an arbitrary JSON type.',
92 INVALID_HTTP_METHOD: 'Ignore operation and continue.',
93 MISSING_RESPONSE_SCHEMA: 'Ignore operation.',
94 MISSING_SCHEMA: 'Use arbitrary JSON type.',
95 MULTIPLE_RESPONSES: 'Select first response object with successful status code (200-299).',
96 NON_APPLICATION_JSON_SCHEMA: 'Ignore schema',
97 OBJECT_MISSING_PROPERTIES: 'The (sub-)object will be stored in an arbitray JSON type.',
98 UNKNOWN_TARGET_TYPE: 'The data will be stored in an arbitrary JSON type.',
99 UNRESOLVABLE_SCHEMA: 'Ignore and continue. May lead to unexpected behavior.',
100 UNSUPPORTED_HTTP_SECURITY_SCHEME: 'Ignore security scheme.',
101 UNSUPPORTED_JSON_SCHEMA_KEYWORD: 'Ignore keyword and continue.',
102 CALLBACKS_MULTIPLE_OPERATION_OBJECTS: 'Select arbitrary operation object',
103 // Links
104 AMBIGUOUS_LINK: `Use first occurance of '#/'.`,
105 LINK_NAME_COLLISION: 'Ignore link and maintain preexisting field.',
106 UNRESOLVABLE_LINK: 'Ignore link.',
107 // Multiple OAS
108 DUPLICATE_OPERATIONID: 'Ignore operation and maintain preexisting operation.',
109 DUPLICATE_SECURITY_SCHEME: 'Ignore security scheme and maintain preexisting scheme.',
110 MULTIPLE_OAS_SAME_TITLE: 'Ignore issue and continue.',
111 // Options
112 CUSTOM_RESOLVER_UNKNOWN_OAS: 'Ignore this set of custom resolvers.',
113 CUSTOM_RESOLVER_UNKNOWN_PATH_METHOD: 'Ignore this set of custom resolvers.',
114 LIMIT_ARGUMENT_NAME_COLLISION: `Do not override existing 'limit' argument.`,
115 // Miscellaneous
116 OAUTH_SECURITY_SCHEME: `Ignore security scheme`,
117};
118/**
119 * Utilities that are specific to OpenAPI-to-GraphQL
120 */
121function handleWarning({ mitigationType, message, mitigationAddendum, path, data, log, }) {
122 const mitigation = mitigations[mitigationType];
123 const warning = {
124 type: mitigationType,
125 message,
126 mitigation: mitigationAddendum ? `${mitigation} ${mitigationAddendum}` : mitigation,
127 };
128 if (path) {
129 warning.path = path;
130 }
131 if (data.options.strict) {
132 throw new Error(`${warning.type} - ${warning.message}`);
133 }
134 else {
135 const output = `Warning: ${warning.message} - ${warning.mitigation}`;
136 if (typeof log === 'function') {
137 log(output);
138 }
139 else {
140 console.log(output);
141 }
142 data.options.report.warnings.push(warning);
143 }
144}
145// Code provided by codename- from StackOverflow
146// Link: https://stackoverflow.com/a/29622653
147function sortObject(o) {
148 return Object.keys(o)
149 .sort()
150 .reduce((r, k) => ((r[k] = o[k]), r), {});
151}
152/**
153 * Finds the common property names between two objects
154 */
155function getCommonPropertyNames(object1, object2) {
156 return Object.keys(object1).filter(propertyName => {
157 return propertyName in object2;
158 });
159}
160// TODO: replace this with Mesh's logger
161function mockDebug(..._) {
162 // do nothing
163 return (..._) => { };
164}
165
166/* eslint-disable no-useless-escape */
167const translationLog = mockDebug('translation');
168// OAS constants
169var HTTP_METHODS;
170(function (HTTP_METHODS) {
171 HTTP_METHODS["get"] = "get";
172 HTTP_METHODS["put"] = "put";
173 HTTP_METHODS["post"] = "post";
174 HTTP_METHODS["patch"] = "patch";
175 HTTP_METHODS["delete"] = "delete";
176 HTTP_METHODS["options"] = "options";
177 HTTP_METHODS["head"] = "head";
178})(HTTP_METHODS || (HTTP_METHODS = {}));
179const SUCCESS_STATUS_RX = /2[0-9]{2}|2XX/;
180/**
181 * Given an HTTP method, convert it to the HTTP_METHODS enum
182 */
183function methodToHttpMethod(method) {
184 switch (method.toLowerCase()) {
185 case 'get':
186 return HTTP_METHODS.get;
187 case 'put':
188 return HTTP_METHODS.put;
189 case 'post':
190 return HTTP_METHODS.post;
191 case 'patch':
192 return HTTP_METHODS.patch;
193 case 'delete':
194 return HTTP_METHODS.delete;
195 case 'options':
196 return HTTP_METHODS.options;
197 case 'head':
198 return HTTP_METHODS.head;
199 default:
200 throw new Error(`Invalid HTTP method '${method}'`);
201 }
202}
203/**
204 * Resolves on a validated OAS 3 for the given spec (OAS 2 or OAS 3), or rejects
205 * if errors occur.
206 */
207async function getValidOAS3(spec) {
208 if (typeof spec.swagger === 'string' && spec.swagger === '2.0') {
209 const { openapi } = await Swagger2OpenAPI.convertObj(spec, {});
210 return openapi;
211 }
212 else {
213 return spec;
214 }
215}
216/**
217 * Counts the number of operations in an OAS.
218 */
219function countOperations(oas) {
220 let numOps = 0;
221 for (const path in oas.paths) {
222 for (const method in oas.paths[path]) {
223 if (isHttpMethod(method)) {
224 numOps++;
225 if (oas.paths[path][method].callbacks) {
226 for (const cbName in oas.paths[path][method].callbacks) {
227 for (const _ in oas.paths[path][method].callbacks[cbName]) {
228 numOps++;
229 }
230 }
231 }
232 }
233 }
234 }
235 return numOps;
236}
237/**
238 * Counts the number of operations that translate to queries in an OAS.
239 */
240function countOperationsQuery(oas) {
241 let numOps = 0;
242 for (const path in oas.paths) {
243 for (const method in oas.paths[path]) {
244 if (isHttpMethod(method) && method.toLowerCase() === HTTP_METHODS.get) {
245 numOps++;
246 }
247 }
248 }
249 return numOps;
250}
251/**
252 * Counts the number of operations that translate to mutations in an OAS.
253 */
254function countOperationsMutation(oas) {
255 let numOps = 0;
256 for (const path in oas.paths) {
257 for (const method in oas.paths[path]) {
258 if (isHttpMethod(method) && method.toLowerCase() !== HTTP_METHODS.get) {
259 numOps++;
260 }
261 }
262 }
263 return numOps;
264}
265/**
266 * Counts the number of operations that translate to subscriptions in an OAS.
267 */
268function countOperationsSubscription(oas) {
269 let numOps = 0;
270 for (const path in oas.paths) {
271 for (const method in oas.paths[path]) {
272 if (isHttpMethod(method) && method.toLowerCase() !== HTTP_METHODS.get && oas.paths[path][method].callbacks) {
273 for (const cbName in oas.paths[path][method].callbacks) {
274 for (const _ in oas.paths[path][method].callbacks[cbName]) {
275 numOps++;
276 }
277 }
278 }
279 }
280 }
281 return numOps;
282}
283/**
284 * Resolves the given reference in the given object.
285 */
286function resolveRef(ref, oas) {
287 return jsonptr.JsonPointer.get(oas, ref);
288}
289/**
290 * Returns the base URL to use for the given operation.
291 */
292function getBaseUrl(operation) {
293 // Check for servers:
294 if (!Array.isArray(operation.servers) || operation.servers.length === 0) {
295 throw new Error(`No servers defined for operation '${operation.operationString}'`);
296 }
297 // Check for local servers
298 if (Array.isArray(operation.servers) && operation.servers.length > 0) {
299 const url = buildUrl(operation.servers[0]);
300 if (Array.isArray(operation.servers) && operation.servers.length > 1) ;
301 return url.replace(/\/$/, '');
302 }
303 const oas = operation.oas;
304 if (Array.isArray(oas.servers) && oas.servers.length > 0) {
305 const url = buildUrl(oas.servers[0]);
306 if (Array.isArray(oas.servers) && oas.servers.length > 1) ;
307 return url.replace(/\/$/, '');
308 }
309 throw new Error('Cannot find a server to call');
310}
311/**
312 * Returns the default URL for a given OAS server object.
313 */
314function buildUrl(server) {
315 let url = server.url;
316 // Replace with variable defaults, if applicable
317 if (typeof server.variables === 'object' && Object.keys(server.variables).length > 0) {
318 for (const variableKey in server.variables) {
319 // TODO: check for default? Would be invalid OAS
320 url = url.replace(`{${variableKey}}`, server.variables[variableKey].default.toString());
321 }
322 }
323 return url;
324}
325/**
326 * Returns object/array/scalar where all object keys (if applicable) are
327 * sanitized.
328 */
329function sanitizeObjectKeys(obj, // obj does not necessarily need to be an object
330caseStyle = CaseStyle.camelCase) {
331 const cleanKeys = (obj) => {
332 // Case: no (response) data
333 if (obj === null || typeof obj === 'undefined') {
334 return null;
335 // Case: array
336 }
337 else if (Array.isArray(obj)) {
338 return obj.map(cleanKeys);
339 // Case: object
340 }
341 else if (typeof obj === 'object') {
342 const res = {};
343 for (const key in obj) {
344 const saneKey = sanitize(key, caseStyle);
345 if (Object.prototype.hasOwnProperty.call(obj, key)) {
346 res[saneKey] = cleanKeys(obj[key]);
347 }
348 }
349 return res;
350 // Case: scalar
351 }
352 else {
353 return obj;
354 }
355 };
356 return cleanKeys(obj);
357}
358/**
359 * Desanitizes keys in given object by replacing them with the keys stored in
360 * the given mapping.
361 */
362function desanitizeObjectKeys(obj, mapping = {}) {
363 const replaceKeys = (obj) => {
364 if (obj === null) {
365 return null;
366 }
367 else if (Array.isArray(obj)) {
368 return obj.map(replaceKeys);
369 }
370 else if (typeof obj === 'object') {
371 const res = {};
372 for (const key in obj) {
373 if (key in mapping) {
374 const rawKey = mapping[key];
375 if (Object.prototype.hasOwnProperty.call(obj, key)) {
376 res[rawKey] = replaceKeys(obj[key]);
377 }
378 }
379 else {
380 res[key] = replaceKeys(obj[key]);
381 }
382 }
383 return res;
384 }
385 else {
386 return obj;
387 }
388 };
389 return replaceKeys(obj);
390}
391/**
392 * Returns the GraphQL type that the provided schema should be made into
393 *
394 * Does not consider allOf, anyOf, oneOf, or not (handled separately)
395 */
396function getSchemaTargetGraphQLType(schema, data) {
397 // CASE: object
398 if (schema.type === 'object' || typeof schema.properties === 'object') {
399 // TODO: additionalProperties is more like a flag than a type itself
400 // CASE: arbitrary JSON
401 if (typeof schema.additionalProperties === 'object') {
402 return 'json';
403 }
404 else {
405 return 'object';
406 }
407 }
408 // CASE: array
409 if (schema.type === 'array' || 'items' in schema) {
410 return 'list';
411 }
412 // CASE: enum
413 if (Array.isArray(schema.enum)) {
414 return 'enum';
415 }
416 // CASE: a type is present
417 if (typeof schema.type === 'string') {
418 // Special edge cases involving the schema format
419 if (typeof schema.format === 'string') {
420 /**
421 * CASE: 64 bit int - return number instead of integer, leading to use of
422 * GraphQLFloat, which can support 64 bits:
423 */
424 if (schema.type === 'integer' && schema.format === 'int64') {
425 return 'number';
426 // CASE: id
427 }
428 else if (schema.type === 'string' &&
429 (schema.format === 'uuid' ||
430 // Custom ID format
431 (Array.isArray(data.options.idFormats) && data.options.idFormats.includes(schema.format)))) {
432 return 'id';
433 }
434 }
435 return schema.type;
436 }
437 return null;
438}
439function isIdParam(part) {
440 return /^{.*(id|name|key).*}$/gi.test(part);
441}
442function isSingularParam(part, nextPart) {
443 return `\{${pluralize.singular(part)}\}` === nextPart;
444}
445/**
446 * Infers a resource name from the given URL path.
447 *
448 * For example, turns "/users/{userId}/car" into "userCar".
449 */
450function inferResourceNameFromPath(path) {
451 const parts = path.split('/');
452 const pathNoPathParams = parts.reduce((path, part, i) => {
453 if (!/{/g.test(part)) {
454 if (parts[i + 1] && (isIdParam(parts[i + 1]) || isSingularParam(part, parts[i + 1]))) {
455 return path + capitalize(pluralize.singular(part));
456 }
457 else {
458 return path + capitalize(part);
459 }
460 }
461 else {
462 return path;
463 }
464 }, '');
465 return pathNoPathParams;
466}
467/**
468 * Returns JSON-compatible schema required by the given operation
469 */
470function getRequestBodyObject(operation, oas) {
471 if (typeof operation.requestBody === 'object') {
472 let requestBodyObject = operation.requestBody;
473 // Make sure we have a RequestBodyObject:
474 if (typeof requestBodyObject.$ref === 'string') {
475 requestBodyObject = resolveRef(requestBodyObject.$ref, oas);
476 }
477 else {
478 requestBodyObject = requestBodyObject;
479 }
480 if (typeof requestBodyObject.content === 'object') {
481 const content = requestBodyObject.content;
482 // Prioritize content-type JSON
483 if (Object.keys(content).includes('application/json')) {
484 return {
485 payloadContentType: 'application/json',
486 requestBodyObject,
487 };
488 }
489 else if (Object.keys(content).includes('application/x-www-form-urlencoded')) {
490 return {
491 payloadContentType: 'application/x-www-form-urlencoded',
492 requestBodyObject,
493 };
494 }
495 else {
496 // Pick first (random) content type
497 const randomContentType = Object.keys(content)[0].toString();
498 return {
499 payloadContentType: randomContentType,
500 requestBodyObject,
501 };
502 }
503 }
504 }
505 return { payloadContentType: null, requestBodyObject: null };
506}
507/**
508 * Returns the request schema (if any) for the given operation,
509 * a dictionary of names from different sources (if available), and whether the
510 * request schema is required for the operation.
511 */
512function getRequestSchemaAndNames(path, operation, oas) {
513 const { payloadContentType, requestBodyObject } = getRequestBodyObject(operation, oas);
514 if (payloadContentType) {
515 let payloadSchema = requestBodyObject.content[payloadContentType].schema;
516 // Get resource name from different sources
517 let fromRef;
518 if ('$ref' in payloadSchema) {
519 fromRef = payloadSchema.$ref.split('/').pop();
520 payloadSchema = resolveRef(payloadSchema.$ref, oas);
521 }
522 let payloadSchemaNames = {
523 fromRef,
524 fromSchema: payloadSchema.title,
525 fromPath: inferResourceNameFromPath(path),
526 };
527 // Determine if request body is required:
528 const payloadRequired = typeof requestBodyObject.required === 'boolean' ? requestBodyObject.required : false;
529 /**
530 * Edge case: if request body content-type is not application/json or
531 * application/x-www-form-urlencoded, do not parse it.
532 *
533 * Instead, treat the request body as a black box and send it as a string
534 * with the proper content-type header
535 */
536 if (payloadContentType !== 'application/json' && payloadContentType !== 'application/x-www-form-urlencoded') {
537 const saneContentTypeName = uncapitalize(payloadContentType.split('/').reduce((name, term) => {
538 return name + capitalize(term);
539 }));
540 payloadSchemaNames = {
541 fromPath: saneContentTypeName,
542 };
543 let description = `String represents payload of content type '${payloadContentType}'`;
544 if ('description' in payloadSchema && typeof payloadSchema.description === 'string') {
545 description += `\n\nOriginal top level description: '${payloadSchema.description}'`;
546 }
547 payloadSchema = {
548 description: description,
549 type: 'string',
550 };
551 }
552 return {
553 payloadContentType,
554 payloadSchema,
555 payloadSchemaNames,
556 payloadRequired,
557 };
558 }
559 return {
560 payloadRequired: false,
561 };
562}
563/**
564 * Returns JSON-compatible schema produced by the given operation
565 */
566function getResponseObject(operation, statusCode, oas) {
567 if (typeof operation.responses === 'object') {
568 const responses = operation.responses;
569 if (typeof responses[statusCode] === 'object') {
570 let responseObject = responses[statusCode];
571 // Make sure we have a ResponseObject:
572 if (typeof responseObject.$ref === 'string') {
573 responseObject = resolveRef(responseObject.$ref, oas);
574 }
575 else {
576 responseObject = responseObject;
577 }
578 if (responseObject.content && typeof responseObject.content !== 'undefined') {
579 const content = responseObject.content;
580 // Prioritize content-type JSON
581 if (Object.keys(content).includes('application/json')) {
582 return {
583 responseContentType: 'application/json',
584 responseObject,
585 };
586 }
587 else {
588 // Pick first (random) content type
589 const randomContentType = Object.keys(content)[0].toString();
590 return {
591 responseContentType: randomContentType,
592 responseObject,
593 };
594 }
595 }
596 }
597 }
598 return { responseContentType: null, responseObject: null };
599}
600/**
601 * Returns the response schema for the given operation,
602 * a successful status code, and a dictionary of names from different sources
603 * (if available).
604 */
605function getResponseSchemaAndNames(path, method, operation, oas, data, options) {
606 const statusCode = getResponseStatusCode(path, method, operation, oas, data);
607 if (!statusCode) {
608 return {};
609 }
610 const { responseContentType, responseObject } = getResponseObject(operation, statusCode, oas);
611 if (responseContentType) {
612 let responseSchema = responseObject.content[responseContentType].schema;
613 let fromRef;
614 if ('$ref' in responseSchema) {
615 fromRef = responseSchema.$ref.split('/').pop();
616 responseSchema = resolveRef(responseSchema.$ref, oas);
617 }
618 const responseSchemaNames = {
619 fromRef,
620 fromSchema: responseSchema.title,
621 fromPath: inferResourceNameFromPath(path),
622 };
623 /**
624 * Edge case: if response body content-type is not application/json, do not
625 * parse.
626 */
627 if (responseContentType !== 'application/json') {
628 let description = 'Placeholder to access non-application/json response bodies';
629 if ('description' in responseSchema && typeof responseSchema.description === 'string') {
630 description += `\n\nOriginal top level description: '${responseSchema.description}'`;
631 }
632 responseSchema = {
633 description: description,
634 type: 'string',
635 };
636 }
637 return {
638 responseContentType,
639 responseSchema,
640 responseSchemaNames,
641 statusCode,
642 };
643 }
644 else {
645 /**
646 * GraphQL requires that objects must have some properties.
647 *
648 * To allow some operations (such as those with a 204 HTTP code) to be
649 * included in the GraphQL interface, we added the fillEmptyResponses
650 * option, which will simply create a placeholder to allow access.
651 */
652 if (options.fillEmptyResponses) {
653 return {
654 responseSchemaNames: {
655 fromPath: inferResourceNameFromPath(path),
656 },
657 responseContentType: 'application/json',
658 responseSchema: {
659 description: 'Placeholder to support operations with no response schema',
660 type: 'object',
661 },
662 };
663 }
664 return {};
665 }
666}
667/**
668 * Returns a success status code for the given operation
669 */
670function getResponseStatusCode(path, method, operation, oas, data) {
671 if (typeof operation.responses === 'object') {
672 const codes = Object.keys(operation.responses);
673 const successCodes = codes.filter(code => {
674 return SUCCESS_STATUS_RX.test(code.toString());
675 });
676 if (successCodes.length === 1) {
677 return successCodes[0].toString();
678 }
679 else if (successCodes.length > 1) {
680 handleWarning({
681 mitigationType: MitigationTypes.MULTIPLE_RESPONSES,
682 message: `Operation '${formatOperationString(method, path, oas.info.title)}' ` +
683 `contains multiple possible successful response object ` +
684 `(HTTP code 200-299 or 2XX). Only one can be chosen.`,
685 mitigationAddendum: `The response object with the HTTP code ` + `${successCodes[0]} will be selected`,
686 data,
687 log: translationLog,
688 });
689 return successCodes[0].toString();
690 }
691 }
692 return null;
693}
694/**
695 * Returns a hash containing the links in the given operation.
696 */
697function getLinks(path, method, operation, oas, data) {
698 const links = {};
699 const statusCode = getResponseStatusCode(path, method, operation, oas, data);
700 if (!statusCode) {
701 return links;
702 }
703 if (typeof operation.responses === 'object') {
704 const responses = operation.responses;
705 if (typeof responses[statusCode] === 'object') {
706 let response = responses[statusCode];
707 if (typeof response.$ref === 'string') {
708 response = resolveRef(response.$ref, oas);
709 }
710 // Here, we can be certain we have a ResponseObject:
711 response = response;
712 if (typeof response.links === 'object') {
713 const epLinks = response.links;
714 for (const linkKey in epLinks) {
715 let link = epLinks[linkKey];
716 // Make sure we have LinkObjects:
717 if (typeof link.$ref === 'string') {
718 link = resolveRef(link.$ref, oas);
719 }
720 else {
721 link = link;
722 }
723 links[linkKey] = link;
724 }
725 }
726 }
727 }
728 return links;
729}
730/**
731 * Returns the list of parameters in the given operation.
732 */
733function getParameters(path, method, operation, pathItem, oas) {
734 let parameters = [];
735 if (!isHttpMethod(method)) {
736 return parameters;
737 }
738 // First, consider parameters in Path Item Object:
739 const pathParams = pathItem.parameters;
740 if (Array.isArray(pathParams)) {
741 const pathItemParameters = pathParams.map(p => {
742 if (typeof p.$ref === 'string') {
743 // Here we know we have a parameter object:
744 return resolveRef(p.$ref, oas);
745 }
746 else {
747 // Here we know we have a parameter object:
748 return p;
749 }
750 });
751 parameters = parameters.concat(pathItemParameters);
752 }
753 // Second, consider parameters in Operation Object:
754 const opObjectParameters = operation.parameters;
755 if (Array.isArray(opObjectParameters)) {
756 const operationParameters = opObjectParameters.map(p => {
757 if (typeof p.$ref === 'string') {
758 // Here we know we have a parameter object:
759 return resolveRef(p.$ref, oas);
760 }
761 else {
762 // Here we know we have a parameter object:
763 return p;
764 }
765 });
766 parameters = parameters.concat(operationParameters);
767 }
768 return parameters;
769}
770/**
771 * Returns an array of server objects for the operation at the given path and
772 * method. Considers in the following order: global server definitions,
773 * definitions at the path item, definitions at the operation, or the OAS
774 * default.
775 */
776function getServers(operation, pathItem, oas) {
777 let servers = [];
778 // Global server definitions:
779 if (Array.isArray(oas.servers) && oas.servers.length > 0) {
780 servers = oas.servers;
781 }
782 // First, consider servers defined on the path
783 if (Array.isArray(pathItem.servers) && pathItem.servers.length > 0) {
784 servers = pathItem.servers;
785 }
786 // Second, consider servers defined on the operation
787 if (Array.isArray(operation.servers) && operation.servers.length > 0) {
788 servers = operation.servers;
789 }
790 // Default, in case there is no server:
791 if (servers.length === 0) {
792 const server = {
793 url: '/',
794 };
795 servers.push(server);
796 }
797 return servers;
798}
799/**
800 * Returns a map of Security Scheme definitions, identified by keys. Resolves
801 * possible references.
802 */
803function getSecuritySchemes(oas) {
804 // Collect all security schemes:
805 const securitySchemes = {};
806 if (typeof oas.components === 'object' && typeof oas.components.securitySchemes === 'object') {
807 for (const schemeKey in oas.components.securitySchemes) {
808 const obj = oas.components.securitySchemes[schemeKey];
809 // Ensure we have actual SecuritySchemeObject:
810 if (typeof obj.$ref === 'string') {
811 // Result of resolution will be SecuritySchemeObject:
812 securitySchemes[schemeKey] = resolveRef(obj.$ref, oas);
813 }
814 else {
815 // We already have a SecuritySchemeObject:
816 securitySchemes[schemeKey] = obj;
817 }
818 }
819 }
820 return securitySchemes;
821}
822/**
823 * Returns the list of sanitized keys of non-OAuth2 security schemes
824 * required by the operation at the given path and method.
825 */
826function getSecurityRequirements(operation, securitySchemes, oas) {
827 const results = [];
828 // First, consider global requirements
829 const globalSecurity = oas.security;
830 if (globalSecurity && typeof globalSecurity !== 'undefined') {
831 for (const secReq of globalSecurity) {
832 for (const schemaKey in secReq) {
833 if (securitySchemes[schemaKey] &&
834 typeof securitySchemes[schemaKey] === 'object' &&
835 securitySchemes[schemaKey].def.type !== 'oauth2') {
836 results.push(schemaKey);
837 }
838 }
839 }
840 }
841 // Second, consider operation requirements
842 const localSecurity = operation.security;
843 if (localSecurity && typeof localSecurity !== 'undefined') {
844 for (const secReq of localSecurity) {
845 for (const schemaKey in secReq) {
846 if (securitySchemes[schemaKey] &&
847 typeof securitySchemes[schemaKey] === 'object' &&
848 securitySchemes[schemaKey].def.type !== 'oauth2') {
849 if (!results.includes(schemaKey)) {
850 results.push(schemaKey);
851 }
852 }
853 }
854 }
855 }
856 return results;
857}
858var CaseStyle;
859(function (CaseStyle) {
860 CaseStyle[CaseStyle["simple"] = 0] = "simple";
861 CaseStyle[CaseStyle["PascalCase"] = 1] = "PascalCase";
862 CaseStyle[CaseStyle["camelCase"] = 2] = "camelCase";
863 CaseStyle[CaseStyle["ALL_CAPS"] = 3] = "ALL_CAPS";
864})(CaseStyle || (CaseStyle = {}));
865/**
866 * First sanitizes given string and then also camel-cases it.
867 */
868function sanitize(str, caseStyle) {
869 /**
870 * Used in conjunction to simpleNames, which only removes illegal
871 * characters and preserves casing
872 */
873 if (caseStyle === CaseStyle.simple) {
874 return str.replace(/[^a-zA-Z0-9_]/gi, '');
875 }
876 /**
877 * Remove all GraphQL unsafe characters
878 */
879 const regex = caseStyle === CaseStyle.ALL_CAPS
880 ? /[^a-zA-Z0-9_]/g // ALL_CAPS has underscores
881 : /[^a-zA-Z0-9]/g;
882 let sanitized = str.split(regex).reduce((path, part) => {
883 if (caseStyle === CaseStyle.ALL_CAPS) {
884 return path + '_' + part;
885 }
886 else {
887 return path + capitalize(part);
888 }
889 });
890 switch (caseStyle) {
891 case CaseStyle.PascalCase:
892 // The first character in PascalCase should be uppercase
893 sanitized = capitalize(sanitized);
894 break;
895 case CaseStyle.camelCase:
896 // The first character in camelCase should be lowercase
897 sanitized = uncapitalize(sanitized);
898 break;
899 case CaseStyle.ALL_CAPS:
900 sanitized = sanitized.toUpperCase();
901 break;
902 }
903 // Special case: we cannot start with number, and cannot be empty:
904 if (/^[0-9]/.test(sanitized) || sanitized === '') {
905 sanitized = '_' + sanitized;
906 }
907 return sanitized;
908}
909/**
910 * Sanitizes the given string and stores the sanitized-to-original mapping in
911 * the given mapping.
912 */
913function storeSaneName(saneStr, str, mapping) {
914 if (saneStr in mapping && str !== mapping[saneStr]) {
915 // TODO: Follow warning model
916 translationLog(`Warning: '${str}' and '${mapping[saneStr]}' both sanitize ` +
917 `to '${saneStr}' - collision possible. Desanitize to '${str}'.`);
918 }
919 mapping[saneStr] = str;
920 return saneStr;
921}
922/**
923 * Stringifies and possibly trims the given string to the provided length.
924 */
925function trim(str, length) {
926 if (typeof str !== 'string') {
927 str = JSON.stringify(str);
928 }
929 if (str && str.length > length) {
930 str = `${str.substring(0, length)}...`;
931 }
932 return str;
933}
934/**
935 * Determines if the given "method" is indeed an operation. Alternatively, the
936 * method could point to other types of information (e.g., parameters, servers).
937 */
938function isHttpMethod(method) {
939 return Object.keys(HTTP_METHODS).includes(method.toLowerCase());
940}
941/**
942 * Formats a string that describes an operation in the form:
943 * {name of OAS} {HTTP method in ALL_CAPS} {operation path}
944 *
945 * Also used in preprocessing.ts where Operation objects are being constructed
946 */
947function formatOperationString(method, path, title) {
948 if (title) {
949 return `${title} ${method.toUpperCase()} ${path}`;
950 }
951 else {
952 return `${method.toUpperCase()} ${path}`;
953 }
954}
955/**
956 * Capitalizes a given string
957 */
958function capitalize(str) {
959 return str.charAt(0).toUpperCase() + str.slice(1);
960}
961/**
962 * Uncapitalizes a given string
963 */
964function uncapitalize(str) {
965 return str.charAt(0).toLowerCase() + str.slice(1);
966}
967/**
968 * For operations that do not have an operationId, generate one
969 */
970function generateOperationId(method, path) {
971 return sanitize(`${method} ${path}`, CaseStyle.camelCase);
972}
973
974/* eslint-disable no-case-declarations */
975const translationLog$1 = mockDebug('translation');
976const httpLog = mockDebug('http');
977const pubsubLog = mockDebug('pubsub');
978function headersToObject(headers) {
979 const headersObj = {};
980 headers.forEach((value, key) => {
981 headersObj[key] = value;
982 });
983 return headersObj;
984}
985/*
986 * If operationType is Subscription, creates and returns a resolver object that
987 * contains subscribe to perform subscription and resolve to execute payload
988 * transformation
989 */
990function getSubscribe({ operation, payloadName, data, baseUrl, connectOptions, pubsub, }) {
991 // Determine the appropriate URL:
992 if (typeof baseUrl === 'undefined') {
993 baseUrl = getBaseUrl(operation);
994 }
995 // Return custom resolver if it is defined
996 const customResolvers = data.options.customSubscriptionResolvers;
997 const title = operation.oas.info.title;
998 const path = operation.path;
999 const method = operation.method;
1000 if (typeof customResolvers === 'object' &&
1001 typeof customResolvers[title] === 'object' &&
1002 typeof customResolvers[title][path] === 'object' &&
1003 typeof customResolvers[title][path][method] === 'object' &&
1004 typeof customResolvers[title][path][method].subscribe === 'function') {
1005 translationLog$1(`Use custom publish resolver for ${operation.operationString}`);
1006 return customResolvers[title][path][method].subscribe;
1007 }
1008 return (root, args, context, info) => {
1009 /**
1010 * Determine possible topic(s) by resolving callback path
1011 *
1012 * GraphQL produces sanitized payload names, so we have to sanitize before
1013 * lookup here
1014 */
1015 const paramName = sanitize(payloadName, CaseStyle.camelCase);
1016 const resolveData = {};
1017 if (payloadName && typeof payloadName === 'string') {
1018 // The option genericPayloadArgName will change the payload name to "requestBody"
1019 const sanePayloadName = data.options.genericPayloadArgName
1020 ? 'requestBody'
1021 : sanitize(payloadName, CaseStyle.camelCase);
1022 if (sanePayloadName in args) {
1023 if (typeof args[sanePayloadName] === 'object') {
1024 const rawPayload = desanitizeObjectKeys(args[sanePayloadName], data.saneMap);
1025 resolveData.usedPayload = rawPayload;
1026 }
1027 else {
1028 const rawPayload = JSON.parse(args[sanePayloadName]);
1029 resolveData.usedPayload = rawPayload;
1030 }
1031 }
1032 }
1033 if (connectOptions) {
1034 resolveData.usedRequestOptions = connectOptions;
1035 }
1036 else {
1037 resolveData.usedRequestOptions = {
1038 method: resolveData.usedPayload.method ? resolveData.usedPayload.method : method.toUpperCase(),
1039 };
1040 }
1041 pubsubLog(`Subscription schema: ${JSON.stringify(resolveData.usedPayload)}`);
1042 let value = path;
1043 let paramNameWithoutLocation = paramName;
1044 if (paramName.indexOf('.') !== -1) {
1045 paramNameWithoutLocation = paramName.split('.')[1];
1046 }
1047 // See if the callback path contains constants expression
1048 if (value.search(/{|}/) === -1) {
1049 args[paramNameWithoutLocation] = isRuntimeExpression(value)
1050 ? resolveRuntimeExpression(paramName, value, resolveData, root)
1051 : value;
1052 }
1053 else {
1054 // Replace callback expression with appropriate values
1055 const cbParams = value.match(/{([^}]*)}/g);
1056 pubsubLog(`Analyzing subscription path: ${cbParams.toString()}`);
1057 cbParams.forEach(cbParam => {
1058 value = value.replace(cbParam, resolveRuntimeExpression(paramName, cbParam.substring(1, cbParam.length - 1), resolveData, root));
1059 });
1060 args[paramNameWithoutLocation] = value;
1061 }
1062 const topic = args[paramNameWithoutLocation] || 'test';
1063 return pubsub.asyncIterator(topic);
1064 };
1065}
1066/*
1067 * If operationType is Subscription, creates and returns a resolver function
1068 * triggered after a message has been published to the corresponding subscribe
1069 * topic(s) to execute payload transformation
1070 */
1071function getPublishResolver({ operation, responseName, data, }) {
1072 // Return custom resolver if it is defined
1073 const customResolvers = data.options.customSubscriptionResolvers;
1074 const title = operation.oas.info.title;
1075 const path = operation.path;
1076 const method = operation.method;
1077 if (typeof customResolvers === 'object' &&
1078 typeof customResolvers[title] === 'object' &&
1079 typeof customResolvers[title][path] === 'object' &&
1080 typeof customResolvers[title][path][method] === 'object' &&
1081 typeof customResolvers[title][path][method].resolve === 'function') {
1082 translationLog$1(`Use custom publish resolver for ${operation.operationString}`);
1083 return customResolvers[title][path][method].resolve;
1084 }
1085 return (payload, args, context, info) => {
1086 // Validate and format based on operation.responseDefinition
1087 const typeOfResponse = operation.responseDefinition.targetGraphQLType;
1088 pubsubLog(`Message received: ${responseName}, ${typeOfResponse}, ${JSON.stringify(payload)}`);
1089 let responseBody;
1090 let saneData;
1091 if (typeof payload === 'object') {
1092 if (typeOfResponse === 'object') {
1093 if (Buffer.isBuffer(payload)) {
1094 try {
1095 responseBody = JSON.parse(payload.toString());
1096 }
1097 catch (e) {
1098 const errorString = `Cannot JSON parse payload` +
1099 `operation ${operation.operationString} ` +
1100 `even though it has content-type 'application/json'`;
1101 return null;
1102 }
1103 }
1104 else {
1105 responseBody = payload;
1106 }
1107 saneData = sanitizeObjectKeys(payload);
1108 }
1109 else if ((Buffer.isBuffer(payload) || Array.isArray(payload)) && typeOfResponse === 'string') {
1110 saneData = payload.toString();
1111 }
1112 }
1113 else if (typeof payload === 'string') {
1114 if (typeOfResponse === 'object') {
1115 try {
1116 responseBody = JSON.parse(payload);
1117 saneData = sanitizeObjectKeys(responseBody);
1118 }
1119 catch (e) {
1120 const errorString = `Cannot JSON parse payload` +
1121 `operation ${operation.operationString} ` +
1122 `even though it has content-type 'application/json'`;
1123 return null;
1124 }
1125 }
1126 else if (typeOfResponse === 'string') {
1127 saneData = payload;
1128 }
1129 }
1130 pubsubLog(`Message forwarded: ${JSON.stringify(saneData || payload)}`);
1131 return saneData || payload;
1132 };
1133}
1134/**
1135 * Creates and returns a resolver function that performs API requests for the
1136 * given GraphQL query
1137 */
1138function getResolver(getResolverParams) {
1139 let { operation, argsFromLink = {}, payloadName, data, baseUrl, requestOptions, fetch: fetchFn = data.options.fetch, qs: customQs, } = getResolverParams();
1140 // Determine the appropriate URL:
1141 if (typeof baseUrl === 'undefined') {
1142 baseUrl = getBaseUrl(operation);
1143 }
1144 // Return custom resolver if it is defined
1145 const customResolvers = data.options.customResolvers;
1146 const title = operation.oas.info.title;
1147 const path = operation.path;
1148 const method = operation.method;
1149 if (typeof customResolvers === 'object' &&
1150 typeof customResolvers[title] === 'object' &&
1151 typeof customResolvers[title][path] === 'object' &&
1152 typeof customResolvers[title][path][method] === 'function') {
1153 translationLog$1(`Use custom resolver for ${operation.operationString}`);
1154 return customResolvers[title][path][method];
1155 }
1156 // Return resolve function:
1157 return async (root, args, ctx, info = {}) => {
1158 /**
1159 * Retch resolveData from possibly existing _openAPIToGraphQL
1160 *
1161 * NOTE: _openAPIToGraphQL is an object used to pass security info and data
1162 * from previous resolvers
1163 */
1164 let resolveData = {};
1165 if (root &&
1166 typeof root === 'object' &&
1167 typeof root._openAPIToGraphQL === 'object' &&
1168 typeof root._openAPIToGraphQL.data === 'object') {
1169 const parentIdentifier = getParentIdentifier(info);
1170 if (!(parentIdentifier.length === 0) && parentIdentifier in root._openAPIToGraphQL.data) {
1171 /**
1172 * Resolving link params may change the usedParams, but these changes
1173 * should not be present in the parent _openAPIToGraphQL, therefore copy
1174 * the object
1175 */
1176 resolveData = JSON.parse(JSON.stringify(root._openAPIToGraphQL.data[parentIdentifier]));
1177 }
1178 }
1179 if (typeof resolveData.usedParams === 'undefined') {
1180 resolveData.usedParams = {};
1181 }
1182 /**
1183 * Handle default values of parameters, if they have not yet been defined by
1184 * the user.
1185 */
1186 operation.parameters.forEach(param => {
1187 const paramName = sanitize(param.name, !data.options.simpleNames ? CaseStyle.camelCase : CaseStyle.simple);
1188 if (typeof args[paramName] === 'undefined' && param.schema && typeof param.schema === 'object') {
1189 let schema = param.schema;
1190 if (schema && schema.$ref && typeof schema.$ref === 'string') {
1191 schema = resolveRef(schema.$ref, operation.oas);
1192 }
1193 if (schema && schema.default && typeof schema.default !== 'undefined') {
1194 args[paramName] = schema.default;
1195 }
1196 }
1197 });
1198 // Handle arguments provided by links
1199 for (const paramName in argsFromLink) {
1200 const saneParamName = sanitize(paramName, !data.options.simpleNames ? CaseStyle.camelCase : CaseStyle.simple);
1201 let value = argsFromLink[paramName];
1202 /**
1203 * see if the link parameter contains constants that are appended to the link parameter
1204 *
1205 * e.g. instead of:
1206 * $response.body#/employerId
1207 *
1208 * it could be:
1209 * abc_{$response.body#/employerId}
1210 */
1211 if (value.search(/{|}/) === -1) {
1212 args[saneParamName] = isRuntimeExpression(value)
1213 ? resolveLinkParameter(paramName, value, resolveData, root)
1214 : value;
1215 }
1216 else {
1217 // Replace link parameters with appropriate values
1218 const linkParams = value.match(/{([^}]*)}/g);
1219 linkParams.forEach(linkParam => {
1220 value = value.replace(linkParam, resolveLinkParameter(paramName, linkParam.substring(1, linkParam.length - 1), resolveData, root));
1221 });
1222 args[saneParamName] = value;
1223 }
1224 }
1225 // Stored used parameters to future requests:
1226 resolveData.usedParams = Object.assign(resolveData.usedParams, args);
1227 // Build URL (i.e., fill in path parameters):
1228 const { path, qs: query, headers } = extractRequestDataFromArgs(operation.path, operation.parameters, args, data);
1229 const urlObject = new URL(urlJoin(baseUrl, path));
1230 /**
1231 * The Content-type and accept property should not be changed because the
1232 * object type has already been created and unlike these properties, it
1233 * cannot be easily changed
1234 *
1235 * NOTE: This may cause the user to encounter unexpected changes
1236 */
1237 if (operation.method !== 'get') {
1238 /**
1239 * the check is done on the 'get' operation to determine if there is a payload because
1240 * operation.payloadRequired is not always correctly initialized (at least during the unit test)
1241 */
1242 headers['content-type'] =
1243 typeof operation.payloadContentType !== 'undefined' ? operation.payloadContentType : 'application/json';
1244 }
1245 headers.accept =
1246 typeof operation.responseContentType !== 'undefined' ? operation.responseContentType : 'application/json';
1247 let options;
1248 if (requestOptions) {
1249 options = { ...requestOptions };
1250 options.method = operation.method;
1251 options.headers = options.headers || {};
1252 for (const headerName in headers) {
1253 const headerValue = headers[headerName];
1254 if (headerValue) {
1255 options.headers[headerName] = headerValue;
1256 }
1257 }
1258 }
1259 else {
1260 options = {
1261 method: operation.method,
1262 headers: headers,
1263 };
1264 }
1265 for (const paramName in query) {
1266 const val = query[paramName];
1267 if (val) {
1268 urlObject.searchParams.set(paramName, val);
1269 }
1270 }
1271 /**
1272 * Determine possible payload
1273 *
1274 * GraphQL produces sanitized payload names, so we have to sanitize before
1275 * lookup here
1276 */
1277 resolveData.usedPayload = undefined;
1278 if (typeof payloadName === 'string') {
1279 // The option genericPayloadArgName will change the payload name to "requestBody"
1280 const sanePayloadName = data.options.genericPayloadArgName
1281 ? 'requestBody'
1282 : sanitize(payloadName, CaseStyle.camelCase);
1283 let rawPayload;
1284 if (operation.payloadContentType === 'application/json') {
1285 rawPayload = JSON.stringify(desanitizeObjectKeys(args[sanePayloadName], data.saneMap));
1286 }
1287 else if (operation.payloadContentType === 'application/x-www-form-urlencoded') {
1288 rawPayload = formurlencoded(desanitizeObjectKeys(args[sanePayloadName], data.saneMap));
1289 }
1290 else {
1291 // Payload is not an object
1292 rawPayload = args[sanePayloadName];
1293 }
1294 options.body = rawPayload;
1295 resolveData.usedPayload = rawPayload;
1296 }
1297 /**
1298 * Pass on OpenAPI-to-GraphQL options
1299 */
1300 if (typeof data.options === 'object') {
1301 // Headers:
1302 if (typeof data.options.headers === 'object') {
1303 for (const header in data.options.headers) {
1304 const val = data.options.headers[header];
1305 if (val) {
1306 options.headers[header] = val;
1307 }
1308 }
1309 }
1310 // Query string:
1311 if (typeof data.options.qs === 'object') {
1312 for (const query in data.options.qs) {
1313 const val = data.options.qs[query];
1314 if (val) {
1315 urlObject.searchParams.set(query, val);
1316 }
1317 }
1318 }
1319 }
1320 if (typeof customQs === 'object') {
1321 for (const query in customQs) {
1322 const val = customQs[query];
1323 if (val) {
1324 urlObject.searchParams.set(query, val);
1325 }
1326 }
1327 }
1328 // Get authentication headers and query parameters
1329 if (root && typeof root === 'object' && typeof root._openAPIToGraphQL === 'object') {
1330 const { authHeaders, authQs, authCookie } = getAuthOptions(operation, root._openAPIToGraphQL, data);
1331 // ...and pass them to the options
1332 for (const headerName in authHeaders) {
1333 const headerValue = authHeaders[headerName];
1334 if (headerValue) {
1335 options.headers[headerName] = headerValue;
1336 }
1337 }
1338 for (const query in authQs) {
1339 const val = authQs[query];
1340 if (val) {
1341 urlObject.searchParams.set(query, val);
1342 }
1343 }
1344 // Add authentication cookie if created
1345 if (authCookie !== null) {
1346 const cookieHeaderName = 'cookie';
1347 options.headers[cookieHeaderName] = authCookie;
1348 }
1349 }
1350 // Extract OAuth token from context (if available)
1351 if (data.options.sendOAuthTokenInQuery) {
1352 const oauthQueryObj = createOAuthQS(data, ctx);
1353 for (const query in oauthQueryObj) {
1354 const val = oauthQueryObj[query];
1355 if (val) {
1356 urlObject.searchParams.set(query, val);
1357 }
1358 }
1359 }
1360 else {
1361 const oauthHeader = createOAuthHeader(data, ctx);
1362 for (const headerName in oauthHeader) {
1363 const headerValue = oauthHeader[headerName];
1364 if (headerValue) {
1365 options.headers[headerName] = headerValue;
1366 }
1367 }
1368 }
1369 const urlWithoutQuery = urlObject.href.replace(urlObject.search, '');
1370 resolveData.url = urlWithoutQuery;
1371 resolveData.usedRequestOptions = options;
1372 resolveData.usedStatusCode = operation.statusCode;
1373 // Make the call
1374 httpLog(`Call ${options.method.toUpperCase()} ${urlWithoutQuery}?${urlObject.search}\n` +
1375 `headers: ${JSON.stringify(options.headers)}\n` +
1376 `request body: ${options.body}`);
1377 let response;
1378 try {
1379 response = await fetchFn(urlObject.href, options);
1380 }
1381 catch (err) {
1382 throw err;
1383 }
1384 const body = await response.text();
1385 if (response.status < 200 || response.status > 299) {
1386 httpLog(`${response.status} - ${trim(body, 100)}`);
1387 const errorString = `Could not invoke operation ${operation.operationString}`;
1388 if (data.options.provideErrorExtensions) {
1389 let responseBody;
1390 try {
1391 responseBody = JSON.parse(body);
1392 }
1393 catch (e) {
1394 responseBody = body;
1395 }
1396 const extensions = {
1397 method: operation.method,
1398 path: operation.path,
1399 statusText: response.statusText,
1400 statusCode: response.status,
1401 responseHeaders: headersToObject(response.headers),
1402 responseBody,
1403 };
1404 throw graphQLErrorWithExtensions(errorString, extensions);
1405 }
1406 else {
1407 throw new Error(errorString);
1408 }
1409 // Successful response 200-299
1410 }
1411 else {
1412 httpLog(`${response.status} - ${trim(body, 100)}`);
1413 if (response.headers.get('content-type')) {
1414 /**
1415 * If the response body is type JSON, then parse it
1416 *
1417 * content-type may not be necessarily 'application/json' it can be
1418 * 'application/json; charset=utf-8' for example
1419 */
1420 if (response.headers.get('content-type').includes('json')) {
1421 let responseBody;
1422 try {
1423 responseBody = JSON.parse(body);
1424 }
1425 catch (e) {
1426 const errorString = `Cannot JSON parse response body of ` +
1427 `operation ${operation.operationString} ` +
1428 `even though it has content-type '${response.headers.get('content-type')}'`;
1429 throw errorString;
1430 }
1431 resolveData.responseHeaders = headersToObject(response.headers);
1432 // Deal with the fact that the server might send unsanitized data
1433 let saneData = sanitizeObjectKeys(responseBody, !data.options.simpleNames ? CaseStyle.camelCase : CaseStyle.simple);
1434 // Pass on _openAPIToGraphQL to subsequent resolvers
1435 if (saneData && typeof saneData === 'object') {
1436 if (Array.isArray(saneData)) {
1437 saneData.forEach(element => {
1438 if (typeof element._openAPIToGraphQL === 'undefined') {
1439 element._openAPIToGraphQL = {
1440 data: {},
1441 };
1442 }
1443 if (root && typeof root === 'object' && typeof root._openAPIToGraphQL === 'object') {
1444 Object.assign(element._openAPIToGraphQL, root._openAPIToGraphQL);
1445 }
1446 element._openAPIToGraphQL.data[getIdentifier(info)] = resolveData;
1447 });
1448 }
1449 else {
1450 if (typeof saneData._openAPIToGraphQL === 'undefined') {
1451 saneData._openAPIToGraphQL = {
1452 data: {},
1453 };
1454 }
1455 if (root && typeof root === 'object' && typeof root._openAPIToGraphQL === 'object') {
1456 Object.assign(saneData._openAPIToGraphQL, root._openAPIToGraphQL);
1457 }
1458 saneData._openAPIToGraphQL.data[getIdentifier(info)] = resolveData;
1459 }
1460 }
1461 // Apply limit argument
1462 if (data.options.addLimitArgument &&
1463 /**
1464 * NOTE: Does not differentiate between autogenerated args and
1465 * preexisting args
1466 *
1467 * Ensure that there is not preexisting 'limit' argument
1468 */
1469 !operation.parameters.find(parameter => {
1470 return parameter.name === 'limit';
1471 }) &&
1472 // Only array data
1473 Array.isArray(saneData) &&
1474 // Only array of objects/arrays
1475 saneData.some(data => {
1476 return typeof data === 'object';
1477 })) {
1478 let arraySaneData = saneData;
1479 if ('limit' in args) {
1480 const limit = args.limit;
1481 if (limit >= 0) {
1482 arraySaneData = arraySaneData.slice(0, limit);
1483 }
1484 else {
1485 throw new Error(`Auto-generated 'limit' argument must be greater than or equal to 0`);
1486 }
1487 }
1488 else {
1489 throw new Error(`Cannot get value for auto-generated 'limit' argument`);
1490 }
1491 saneData = arraySaneData;
1492 }
1493 return saneData;
1494 }
1495 else {
1496 // TODO: Handle YAML
1497 return body;
1498 }
1499 }
1500 else {
1501 /**
1502 * Check to see if there is not supposed to be a response body,
1503 * if that is the case, that would explain why there is not
1504 * a content-type
1505 */
1506 const { responseContentType } = getResponseObject(operation, operation.statusCode, operation.oas);
1507 if (responseContentType === null) {
1508 return null;
1509 }
1510 else {
1511 const errorString = 'Response does not have a Content-Type property';
1512 throw errorString;
1513 }
1514 }
1515 }
1516 };
1517}
1518/**
1519 * Attempts to create an object to become an OAuth query string by extracting an
1520 * OAuth token from the ctx based on the JSON path provided in the options.
1521 */
1522function createOAuthQS(data, context) {
1523 return typeof data.options.tokenJSONpath !== 'string' ? {} : extractToken(data, context);
1524}
1525function extractToken(data, ctx) {
1526 const tokenJSONpath = data.options.tokenJSONpath;
1527 const tokens = JSONPath.JSONPath({ path: tokenJSONpath, json: ctx });
1528 if (Array.isArray(tokens) && tokens.length > 0) {
1529 const token = tokens[0];
1530 return {
1531 access_token: token,
1532 };
1533 }
1534 else {
1535 return {};
1536 }
1537}
1538/**
1539 * Attempts to create an OAuth authorization header by extracting an OAuth token
1540 * from the ctx based on the JSON path provided in the options.
1541 */
1542function createOAuthHeader(data, ctx) {
1543 if (typeof data.options.tokenJSONpath !== 'string') {
1544 return {};
1545 }
1546 // Extract token
1547 const tokenJSONpath = data.options.tokenJSONpath;
1548 const tokens = JSONPath.JSONPath({ path: tokenJSONpath, json: ctx });
1549 if (Array.isArray(tokens) && tokens.length > 0) {
1550 const token = tokens[0];
1551 return {
1552 Authorization: `Bearer ${token}`,
1553 'User-Agent': 'openapi-to-graphql',
1554 };
1555 }
1556 else {
1557 return {};
1558 }
1559}
1560/**
1561 * Returns the headers and query strings to authenticate a request (if any).
1562 * Object containing authHeader and authQs object,
1563 * which hold headers and query parameters respectively to authentication a
1564 * request.
1565 */
1566function getAuthOptions(operation, _openAPIToGraphQL, data) {
1567 const authHeaders = {};
1568 const authQs = {};
1569 let authCookie = null;
1570 /**
1571 * Determine if authentication is required, and which protocol (if any) we can
1572 * use
1573 */
1574 const { authRequired, sanitizedSecurityRequirement } = getAuthReqAndProtcolName(operation, _openAPIToGraphQL);
1575 const securityRequirement = data.saneMap[sanitizedSecurityRequirement];
1576 // Possibly, we don't need to do anything:
1577 if (!authRequired) {
1578 return { authHeaders, authQs, authCookie };
1579 }
1580 // If authentication is required, but we can't fulfill the protocol, throw:
1581 if (authRequired && typeof securityRequirement !== 'string') {
1582 throw new Error(`Missing information to authenticate API request.`);
1583 }
1584 if (typeof securityRequirement === 'string') {
1585 const security = data.security[securityRequirement];
1586 switch (security.def.type) {
1587 case 'apiKey':
1588 const apiKey = _openAPIToGraphQL.security[sanitizedSecurityRequirement].apiKey;
1589 if ('in' in security.def) {
1590 if (typeof security.def.name === 'string') {
1591 if (security.def.in === 'header') {
1592 authHeaders[security.def.name] = apiKey;
1593 }
1594 else if (security.def.in === 'query') {
1595 authQs[security.def.name] = apiKey;
1596 }
1597 else if (security.def.in === 'cookie') {
1598 authCookie = `${security.def.name}=${apiKey}`;
1599 }
1600 }
1601 else {
1602 throw new Error(`Cannot send API key in '${JSON.stringify(security.def.in)}'`);
1603 }
1604 }
1605 break;
1606 case 'http':
1607 switch (security.def.scheme) {
1608 case 'basic':
1609 const username = _openAPIToGraphQL.security[sanitizedSecurityRequirement].username;
1610 const password = _openAPIToGraphQL.security[sanitizedSecurityRequirement].password;
1611 const credentials = `${username}:${password}`;
1612 authHeaders.Authorization = `Basic ${Buffer.from(credentials).toString('base64')}`;
1613 break;
1614 default:
1615 throw new Error(`Cannot recognize http security scheme ` + `'${JSON.stringify(security.def.scheme)}'`);
1616 }
1617 break;
1618 case 'oauth2':
1619 break;
1620 case 'openIdConnect':
1621 break;
1622 default:
1623 throw new Error(`Cannot recognize security type '${security.def.type}'`);
1624 }
1625 }
1626 return { authHeaders, authQs, authCookie };
1627}
1628/**
1629 * Determines whether given operation requires authentication, and which of the
1630 * (possibly multiple) authentication protocols can be used based on the data
1631 * present in the given context.
1632 */
1633function getAuthReqAndProtcolName(operation, _openAPIToGraphQL) {
1634 let authRequired = false;
1635 if (Array.isArray(operation.securityRequirements) && operation.securityRequirements.length > 0) {
1636 authRequired = true;
1637 for (const securityRequirement of operation.securityRequirements) {
1638 const sanitizedSecurityRequirement = sanitize(securityRequirement, CaseStyle.camelCase);
1639 if (typeof _openAPIToGraphQL.security[sanitizedSecurityRequirement] === 'object') {
1640 return {
1641 authRequired,
1642 sanitizedSecurityRequirement,
1643 };
1644 }
1645 }
1646 }
1647 return {
1648 authRequired,
1649 };
1650}
1651/**
1652 * Given a link parameter, determine the value
1653 *
1654 * The link parameter is a reference to data contained in the
1655 * url/method/statuscode or response/request body/query/path/header
1656 */
1657function resolveLinkParameter(paramName, value, resolveData, root, args) {
1658 if (value === '$url') {
1659 return resolveData.url;
1660 }
1661 else if (value === '$method') {
1662 return resolveData.usedRequestOptions.method;
1663 }
1664 else if (value === '$statusCode') {
1665 return resolveData.usedStatusCode;
1666 }
1667 else if (value.startsWith('$request.')) {
1668 // CASE: parameter is previous body
1669 if (value === '$request.body') {
1670 return resolveData.usedPayload;
1671 // CASE: parameter in previous body
1672 }
1673 else if (value.startsWith('$request.body#')) {
1674 const tokens = JSONPath.JSONPath({
1675 path: value.split('body#/')[1],
1676 json: resolveData.usedPayload,
1677 });
1678 if (Array.isArray(tokens) && tokens.length > 0) {
1679 return tokens[0];
1680 }
1681 // CASE: parameter in previous query parameter
1682 }
1683 else if (value.startsWith('$request.query')) {
1684 return resolveData.usedParams[sanitize(value.split('query.')[1], CaseStyle.camelCase)];
1685 // CASE: parameter in previous path parameter
1686 }
1687 else if (value.startsWith('$request.path')) {
1688 return resolveData.usedParams[sanitize(value.split('path.')[1], CaseStyle.camelCase)];
1689 // CASE: parameter in previous header parameter
1690 }
1691 else if (value.startsWith('$request.header')) {
1692 return resolveData.usedRequestOptions.headers[value.split('header.')[1]];
1693 }
1694 }
1695 else if (value.startsWith('$response.')) {
1696 /**
1697 * CASE: parameter is body
1698 *
1699 * NOTE: may not be used because it implies that the operation does not
1700 * return a JSON object and OpenAPI-to-GraphQL does not create GraphQL
1701 * objects for non-JSON data and links can only exists between objects.
1702 */
1703 if (value === '$response.body') {
1704 const result = JSON.parse(JSON.stringify(root));
1705 /**
1706 * _openAPIToGraphQL contains data used by OpenAPI-to-GraphQL to create the GraphQL interface
1707 * and should not be exposed
1708 */
1709 result._openAPIToGraphQL = undefined;
1710 return result;
1711 // CASE: parameter in body
1712 }
1713 else if (value.startsWith('$response.body#')) {
1714 const tokens = JSONPath.JSONPath({
1715 path: value.split('body#/')[1],
1716 json: root,
1717 });
1718 if (Array.isArray(tokens) && tokens.length > 0) {
1719 return tokens[0];
1720 }
1721 // CASE: parameter in query parameter
1722 }
1723 else if (value.startsWith('$response.query')) {
1724 // NOTE: handled the same way $request.query is handled
1725 return resolveData.usedParams[sanitize(value.split('query.')[1], CaseStyle.camelCase)];
1726 // CASE: parameter in path parameter
1727 }
1728 else if (value.startsWith('$response.path')) {
1729 // NOTE: handled the same way $request.path is handled
1730 return resolveData.usedParams[sanitize(value.split('path.')[1], CaseStyle.camelCase)];
1731 // CASE: parameter in header parameter
1732 }
1733 else if (value.startsWith('$response.header')) {
1734 return resolveData.responseHeaders[value.split('header.')[1]];
1735 }
1736 }
1737 throw new Error(`Cannot create link because '${value}' is an invalid runtime expression`);
1738}
1739/**
1740 * Given a link parameter or callback path, determine the value from the runtime
1741 * expression
1742 *
1743 * The link parameter or callback path is a reference to data contained in the
1744 * url/method/statuscode or response/request body/query/path/header
1745 */
1746function resolveRuntimeExpression(paramName, value, resolveData, root, args) {
1747 if (value === '$url') {
1748 return resolveData.usedRequestOptions.url;
1749 }
1750 else if (value === '$method') {
1751 return resolveData.usedRequestOptions.method;
1752 }
1753 else if (value === '$statusCode') {
1754 return resolveData.usedStatusCode;
1755 }
1756 else if (value.startsWith('$request.')) {
1757 // CASE: parameter is previous body
1758 if (value === '$request.body') {
1759 return resolveData.usedPayload;
1760 // CASE: parameter in previous body
1761 }
1762 else if (value.startsWith('$request.body#')) {
1763 const tokens = JSONPath.JSONPath({
1764 path: value.split('body#/')[1],
1765 json: resolveData.usedPayload,
1766 });
1767 if (Array.isArray(tokens) && tokens.length > 0) {
1768 return tokens[0];
1769 }
1770 // CASE: parameter in previous query parameter
1771 }
1772 else if (value.startsWith('$request.query')) {
1773 return resolveData.usedParams[sanitize(value.split('query.')[1], CaseStyle.camelCase)];
1774 // CASE: parameter in previous path parameter
1775 }
1776 else if (value.startsWith('$request.path')) {
1777 return resolveData.usedParams[sanitize(value.split('path.')[1], CaseStyle.camelCase)];
1778 // CASE: parameter in previous header parameter
1779 }
1780 else if (value.startsWith('$request.header')) {
1781 return resolveData.usedRequestOptions.headers[value.split('header.')[1]];
1782 }
1783 }
1784 else if (value.startsWith('$response.')) {
1785 /**
1786 * CASE: parameter is body
1787 *
1788 * NOTE: may not be used because it implies that the operation does not
1789 * return a JSON object and OpenAPI-to-GraphQL does not create GraphQL
1790 * objects for non-JSON data and links can only exists between objects.
1791 */
1792 if (value === '$response.body') {
1793 const result = JSON.parse(JSON.stringify(root));
1794 /**
1795 * _openAPIToGraphQL contains data used by OpenAPI-to-GraphQL to create the GraphQL interface
1796 * and should not be exposed
1797 */
1798 result._openAPIToGraphQL = undefined;
1799 return result;
1800 // CASE: parameter in body
1801 }
1802 else if (value.startsWith('$response.body#')) {
1803 const tokens = JSONPath.JSONPath({
1804 path: value.split('body#/')[1],
1805 json: root,
1806 });
1807 if (Array.isArray(tokens) && tokens.length > 0) {
1808 return tokens[0];
1809 }
1810 // CASE: parameter in query parameter
1811 }
1812 else if (value.startsWith('$response.query')) {
1813 // NOTE: handled the same way $request.query is handled
1814 return resolveData.usedParams[sanitize(value.split('query.')[1], CaseStyle.camelCase)];
1815 // CASE: parameter in path parameter
1816 }
1817 else if (value.startsWith('$response.path')) {
1818 // NOTE: handled the same way $request.path is handled
1819 return resolveData.usedParams[sanitize(value.split('path.')[1], CaseStyle.camelCase)];
1820 // CASE: parameter in header parameter
1821 }
1822 else if (value.startsWith('$response.header')) {
1823 return resolveData.responseHeaders[value.split('header.')[1]];
1824 }
1825 }
1826 throw new Error(`Cannot create link because '${value}' is an invalid runtime expression`);
1827}
1828/**
1829 * Check if a string is a runtime expression in the context of link parameters
1830 */
1831function isRuntimeExpression(str) {
1832 const references = ['header.', 'query.', 'path.', 'body'];
1833 if (str === '$url' || str === '$method' || str === '$statusCode') {
1834 return true;
1835 }
1836 else if (str.startsWith('$request.')) {
1837 for (let i = 0; i < references.length; i++) {
1838 if (str.startsWith(`$request.${references[i]}`)) {
1839 return true;
1840 }
1841 }
1842 }
1843 else if (str.startsWith('$response.')) {
1844 for (let i = 0; i < references.length; i++) {
1845 if (str.startsWith(`$response.${references[i]}`)) {
1846 return true;
1847 }
1848 }
1849 }
1850 return false;
1851}
1852/**
1853 * From the info object provided by the resolver, get a unique identifier, which
1854 * is the path formed from the nested field names (or aliases if provided)
1855 *
1856 * Used to store and retrieve the _openAPIToGraphQL of parent field
1857 */
1858function getIdentifier(info) {
1859 return getIdentifierRecursive(info.path);
1860}
1861/**
1862 * From the info object provided by the resolver, get the unique identifier of
1863 * the parent object
1864 */
1865function getParentIdentifier(info) {
1866 return getIdentifierRecursive(info.path.prev);
1867}
1868/**
1869 * Get the path of nested field names (or aliases if provided)
1870 */
1871function getIdentifierRecursive(path) {
1872 return typeof path.prev === 'undefined'
1873 ? path.key.toString()
1874 : /**
1875 * Check if the identifier contains array indexing, if so remove.
1876 *
1877 * i.e. instead of 0/friends/1/friends/2/friends/user, create
1878 * friends/friends/friends/user
1879 */
1880 isNaN(parseInt(path.key.toString()))
1881 ? `${path.key}/${getIdentifierRecursive(path.prev)}`
1882 : getIdentifierRecursive(path.prev);
1883}
1884/**
1885 * Create a new GraphQLError with an extensions field
1886 */
1887function graphQLErrorWithExtensions(message, extensions) {
1888 return new graphql.GraphQLError(message, null, null, null, null, null, extensions);
1889}
1890/**
1891 * Extracts data from the GraphQL arguments of a particular field
1892 *
1893 * Replaces the path parameter in the given path with values in the given args.
1894 * Furthermore adds the query parameters for a request.
1895 */
1896function extractRequestDataFromArgs(path, parameters, args, // NOTE: argument keys are sanitized!
1897data) {
1898 const qs = {};
1899 const headers = {};
1900 // Iterate parameters:
1901 for (const param of parameters) {
1902 const sanitizedParamName = sanitize(param.name, !data.options.simpleNames ? CaseStyle.camelCase : CaseStyle.simple);
1903 if (sanitizedParamName && sanitizedParamName in args) {
1904 switch (param.in) {
1905 // Path parameters
1906 case 'path':
1907 path = path.replace(`{${param.name}}`, args[sanitizedParamName]);
1908 break;
1909 // Query parameters
1910 case 'query':
1911 qs[param.name] = args[sanitizedParamName];
1912 break;
1913 // Header parameters
1914 case 'header':
1915 headers[param.name] = args[sanitizedParamName];
1916 break;
1917 // Cookie parameters
1918 case 'cookie':
1919 if (!('cookie' in headers)) {
1920 headers.cookie = '';
1921 }
1922 headers.cookie += `${param.name}=${args[sanitizedParamName]}; `;
1923 break;
1924 default:
1925 httpLog(`Warning: The parameter location '${param.in}' in the ` +
1926 `parameter '${param.name}' of operation '${path}' is not ` +
1927 `supported`);
1928 }
1929 }
1930 }
1931 return { path, qs, headers };
1932}
1933
1934// Copyright IBM Corp. 2018. All Rights Reserved.
1935const preprocessingLog = mockDebug('preprocessing');
1936/**
1937 * Given an operation object from the OAS, create an Operation, which contains
1938 * the necessary data to create a GraphQL wrapper for said operation object.
1939 *
1940 * @param path The path of the operation object
1941 * @param method The method of the operation object
1942 * @param operationString A string representation of the path and the method (and the OAS title if applicable)
1943 * @param operationType Whether the operation should be turned into a Query/Mutation/Subscription operation
1944 * @param operation The operation object from the OAS
1945 * @param pathItem The path item object from the OAS from which the operation object is derived from
1946 * @param oas The OAS from which the path item and operation object are derived from
1947 * @param data An assortment of data which at this point is mainly used enable logging
1948 * @param options The options passed by the user
1949 */
1950function processOperation(path, method, operationString, operationType, operation, pathItem, oas, data, options) {
1951 // Determine description
1952 let description = operation.description;
1953 if ((typeof description !== 'string' || description === '') && typeof operation.summary === 'string') {
1954 description = operation.summary;
1955 }
1956 if (data.options.equivalentToMessages) {
1957 // Description may not exist
1958 if (typeof description !== 'string') {
1959 description = '';
1960 }
1961 description += `\n\nEquivalent to ${operationString}`;
1962 }
1963 // Hold on to the operationId
1964 const operationId = typeof operation.operationId !== 'undefined' ? operation.operationId : generateOperationId(method, path);
1965 // Request schema
1966 const { payloadContentType, payloadSchema, payloadSchemaNames, payloadRequired } = getRequestSchemaAndNames(path, operation, oas);
1967 const payloadDefinition = payloadSchema && typeof payloadSchema !== 'undefined'
1968 ? createDataDef(payloadSchemaNames, payloadSchema, true, data, oas)
1969 : undefined;
1970 // Response schema
1971 const { responseContentType, responseSchema, responseSchemaNames, statusCode } = getResponseSchemaAndNames(path, method, operation, oas, data, options);
1972 if (!responseSchema || typeof responseSchema !== 'object') {
1973 handleWarning({
1974 mitigationType: MitigationTypes.MISSING_RESPONSE_SCHEMA,
1975 message: `Operation ${operationString} has no (valid) response schema. ` +
1976 `You can use the fillEmptyResponses option to create a ` +
1977 `placeholder schema`,
1978 data,
1979 log: preprocessingLog,
1980 });
1981 return undefined;
1982 }
1983 // Links
1984 const links = getLinks(path, method, operation, oas, data);
1985 const responseDefinition = createDataDef(responseSchemaNames, responseSchema, false, data, oas, links);
1986 // Parameters
1987 const parameters = getParameters(path, method, operation, pathItem, oas);
1988 // Security protocols
1989 const securityRequirements = options.viewer ? getSecurityRequirements(operation, data.security, oas) : [];
1990 // Servers
1991 const servers = getServers(operation, pathItem, oas);
1992 // Whether to place this operation into an authentication viewer
1993 const inViewer = securityRequirements.length > 0 && data.options.viewer !== false;
1994 return {
1995 operationId,
1996 operationString,
1997 operationType,
1998 description,
1999 path,
2000 method,
2001 payloadContentType,
2002 payloadDefinition,
2003 payloadRequired,
2004 responseContentType,
2005 responseDefinition,
2006 parameters,
2007 securityRequirements,
2008 servers,
2009 inViewer,
2010 statusCode,
2011 oas,
2012 };
2013}
2014/**
2015 * Extract information from the OAS and put it inside a data structure that
2016 * is easier for OpenAPI-to-GraphQL to use
2017 */
2018function preprocessOas(oass, options) {
2019 const data = {
2020 operations: {},
2021 callbackOperations: {},
2022 usedTypeNames: [
2023 'Query',
2024 'Mutation',
2025 'Subscription',
2026 ],
2027 defs: [],
2028 security: {},
2029 saneMap: {},
2030 options,
2031 oass,
2032 };
2033 oass.forEach(oas => {
2034 // Store stats on OAS:
2035 data.options.report.numOps += countOperations(oas);
2036 data.options.report.numOpsMutation += countOperationsMutation(oas);
2037 data.options.report.numOpsQuery += countOperationsQuery(oas);
2038 if (data.options.createSubscriptionsFromCallbacks) {
2039 data.options.report.numOpsSubscription += countOperationsSubscription(oas);
2040 }
2041 else {
2042 data.options.report.numOpsSubscription = 0;
2043 }
2044 // Get security schemes
2045 const currentSecurity = getProcessedSecuritySchemes(oas, data);
2046 const commonSecurityPropertyName = getCommonPropertyNames(data.security, currentSecurity);
2047 commonSecurityPropertyName.forEach(propertyName => {
2048 handleWarning({
2049 mitigationType: MitigationTypes.DUPLICATE_SECURITY_SCHEME,
2050 message: `Multiple OASs share security schemes with the same name '${propertyName}'`,
2051 mitigationAddendum: `The security scheme from OAS ` + `'${currentSecurity[propertyName].oas.info.title}' will be ignored`,
2052 data,
2053 log: preprocessingLog,
2054 });
2055 });
2056 // Do not overwrite preexisting security schemes
2057 data.security = { ...currentSecurity, ...data.security };
2058 // Process all operations
2059 for (const path in oas.paths) {
2060 const pathItem = !('$ref' in oas.paths[path])
2061 ? oas.paths[path]
2062 : resolveRef(oas.paths[path].$ref, oas);
2063 Object.keys(pathItem)
2064 .filter(objectKey => {
2065 /**
2066 * Get only fields that contain operation objects
2067 *
2068 * Can also contain other fields such as summary or description
2069 */
2070 return isHttpMethod(objectKey);
2071 })
2072 .forEach(rawMethod => {
2073 const operationString = oass.length === 1
2074 ? formatOperationString(rawMethod, path)
2075 : formatOperationString(rawMethod, path, oas.info.title);
2076 let httpMethod;
2077 try {
2078 httpMethod = methodToHttpMethod(rawMethod);
2079 }
2080 catch (e) {
2081 handleWarning({
2082 mitigationType: MitigationTypes.INVALID_HTTP_METHOD,
2083 message: `Invalid HTTP method '${rawMethod}' in operation '${operationString}'`,
2084 data,
2085 log: preprocessingLog,
2086 });
2087 return;
2088 }
2089 const operation = pathItem[httpMethod];
2090 let operationType = httpMethod === HTTP_METHODS.get ? GraphQLOperationType.Query : GraphQLOperationType.Mutation;
2091 // Option selectQueryOrMutationField can override operation type
2092 if (typeof options.selectQueryOrMutationField === 'object' &&
2093 typeof options.selectQueryOrMutationField[oas.info.title] === 'object' &&
2094 typeof options.selectQueryOrMutationField[oas.info.title][path] === 'object' &&
2095 typeof options.selectQueryOrMutationField[oas.info.title][path][httpMethod] === 'number' // This is an TS enum, which is translated to have a integer value
2096 ) {
2097 operationType =
2098 options.selectQueryOrMutationField[oas.info.title][path][httpMethod] === GraphQLOperationType.Mutation
2099 ? GraphQLOperationType.Mutation
2100 : GraphQLOperationType.Query;
2101 }
2102 const operationData = processOperation(path, httpMethod, operationString, operationType, operation, pathItem, oas, data, options);
2103 if (operationData) {
2104 /**
2105 * Handle operationId property name collision
2106 * May occur if multiple OAS are provided
2107 */
2108 if (operationData && !(operationData.operationId in data.operations)) {
2109 data.operations[operationData.operationId] = operationData;
2110 }
2111 else {
2112 handleWarning({
2113 mitigationType: MitigationTypes.DUPLICATE_OPERATIONID,
2114 message: `Multiple OASs share operations with the same operationId '${operationData.operationId}'`,
2115 mitigationAddendum: `The operation from the OAS '${operationData.oas.info.title}' will be ignored`,
2116 data,
2117 log: preprocessingLog,
2118 });
2119 }
2120 }
2121 // Process all callbacks
2122 if (data.options.createSubscriptionsFromCallbacks && operation.callbacks) {
2123 Object.entries(operation.callbacks).forEach(([callbackName, callback]) => {
2124 const resolvedCallback = !('$ref' in callback)
2125 ? callback
2126 : resolveRef(callback.$ref, oas);
2127 Object.entries(resolvedCallback).forEach(([callbackExpression, callbackPathItem]) => {
2128 const resolvedCallbackPathItem = !('$ref' in callbackPathItem)
2129 ? callbackPathItem
2130 : resolveRef(callbackPathItem.$ref, oas);
2131 const callbackOperationObjectMethods = Object.keys(resolvedCallbackPathItem).filter(objectKey => {
2132 /**
2133 * Get only fields that contain operation objects
2134 *
2135 * Can also contain other fields such as summary or description
2136 */
2137 return isHttpMethod(objectKey.toString());
2138 });
2139 if (callbackOperationObjectMethods.length > 0) {
2140 if (callbackOperationObjectMethods.length > 1) {
2141 handleWarning({
2142 mitigationType: MitigationTypes.CALLBACKS_MULTIPLE_OPERATION_OBJECTS,
2143 message: `Callback '${callbackExpression}' on operation '${operationString}' has multiple operation objects with the methods '${callbackOperationObjectMethods}'. OpenAPI-to-GraphQL can only utilize one of these operation objects.`,
2144 mitigationAddendum: `The operation with the method '${callbackOperationObjectMethods[0].toString()}' will be selected and all others will be ignored.`,
2145 data,
2146 log: preprocessingLog,
2147 });
2148 }
2149 // Select only one of the operation object methods
2150 const callbackRawMethod = callbackOperationObjectMethods[0];
2151 const callbackOperationString = oass.length === 1
2152 ? formatOperationString(httpMethod, callbackName)
2153 : formatOperationString(httpMethod, callbackName, oas.info.title);
2154 let callbackHttpMethod;
2155 try {
2156 callbackHttpMethod = methodToHttpMethod(callbackRawMethod.toString());
2157 }
2158 catch (e) {
2159 handleWarning({
2160 mitigationType: MitigationTypes.INVALID_HTTP_METHOD,
2161 message: `Invalid HTTP method '${rawMethod}' in callback '${callbackOperationString}' in operation '${operationString}'`,
2162 data,
2163 log: preprocessingLog,
2164 });
2165 return;
2166 }
2167 const callbackOperation = processOperation(callbackExpression, callbackHttpMethod, callbackOperationString, GraphQLOperationType.Subscription, resolvedCallbackPathItem[callbackHttpMethod], callbackPathItem, oas, data, options);
2168 if (callbackOperation) {
2169 /**
2170 * Handle operationId property name collision
2171 * May occur if multiple OAS are provided
2172 */
2173 if (callbackOperation && !(callbackOperation.operationId in data.callbackOperations)) {
2174 data.callbackOperations[callbackOperation.operationId] = callbackOperation;
2175 }
2176 else {
2177 handleWarning({
2178 mitigationType: MitigationTypes.DUPLICATE_OPERATIONID,
2179 message: `Multiple OASs share callback operations with the same operationId '${callbackOperation.operationId}'`,
2180 mitigationAddendum: `The callback operation from the OAS '${operationData.oas.info.title}' will be ignored`,
2181 data,
2182 log: preprocessingLog,
2183 });
2184 }
2185 }
2186 }
2187 });
2188 });
2189 }
2190 });
2191 }
2192 });
2193 return data;
2194}
2195/**
2196 * Extracts the security schemes from given OAS and organizes the information in
2197 * a data structure that is easier for OpenAPI-to-GraphQL to use
2198 *
2199 * Here is the structure of the data:
2200 * {
2201 * {string} [sanitized name] { Contains information about the security protocol
2202 * {string} rawName Stores the raw security protocol name
2203 * {object} def Definition provided by OAS
2204 * {object} parameters Stores the names of the authentication credentials
2205 * NOTE: Structure will depend on the type of the protocol
2206 * (e.g. basic authentication, API key, etc.)
2207 * NOTE: Mainly used for the AnyAuth viewers
2208 * {object} schema Stores the GraphQL schema to create the viewers
2209 * }
2210 * }
2211 *
2212 * Here is an example:
2213 * {
2214 * MyApiKey: {
2215 * rawName: "My_api_key",
2216 * def: { ... },
2217 * parameters: {
2218 * apiKey: MyKeyApiKey
2219 * },
2220 * schema: { ... }
2221 * }
2222 * MyBasicAuth: {
2223 * rawName: "My_basic_auth",
2224 * def: { ... },
2225 * parameters: {
2226 * username: MyBasicAuthUsername,
2227 * password: MyBasicAuthPassword,
2228 * },
2229 * schema: { ... }
2230 * }
2231 * }
2232 */
2233function getProcessedSecuritySchemes(oas, data) {
2234 const result = {};
2235 const security = getSecuritySchemes(oas);
2236 // Loop through all the security protocols
2237 for (const key in security) {
2238 const protocol = security[key];
2239 // Determine the schema and the parameters for the security protocol
2240 let schema;
2241 let parameters = {};
2242 let description;
2243 switch (protocol.type) {
2244 case 'apiKey':
2245 description = `API key credentials for the security protocol '${key}'`;
2246 if (data.oass.length > 1) {
2247 description += ` in ${oas.info.title}`;
2248 }
2249 parameters = {
2250 apiKey: sanitize(`${key}_apiKey`, CaseStyle.camelCase),
2251 };
2252 schema = {
2253 type: 'object',
2254 description,
2255 properties: {
2256 apiKey: {
2257 type: 'string',
2258 },
2259 },
2260 };
2261 break;
2262 case 'http':
2263 switch (protocol.scheme) {
2264 /**
2265 * TODO: HTTP has a number of authentication types
2266 *
2267 * See http://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml
2268 */
2269 case 'basic':
2270 description = `Basic auth credentials for security protocol '${key}'`;
2271 parameters = {
2272 username: sanitize(`${key}_username`, CaseStyle.camelCase),
2273 password: sanitize(`${key}_password`, CaseStyle.camelCase),
2274 };
2275 schema = {
2276 type: 'object',
2277 description,
2278 properties: {
2279 username: {
2280 type: 'string',
2281 },
2282 password: {
2283 type: 'string',
2284 },
2285 },
2286 };
2287 break;
2288 default:
2289 handleWarning({
2290 mitigationType: MitigationTypes.UNSUPPORTED_HTTP_SECURITY_SCHEME,
2291 message: `Currently unsupported HTTP authentication protocol ` +
2292 `type 'http' and scheme '${protocol.scheme}' in OAS ` +
2293 `'${oas.info.title}'`,
2294 data,
2295 log: preprocessingLog,
2296 });
2297 }
2298 break;
2299 // TODO: Implement
2300 case 'openIdConnect':
2301 handleWarning({
2302 mitigationType: MitigationTypes.UNSUPPORTED_HTTP_SECURITY_SCHEME,
2303 message: `Currently unsupported HTTP authentication protocol ` + `type 'openIdConnect' in OAS '${oas.info.title}'`,
2304 data,
2305 log: preprocessingLog,
2306 });
2307 break;
2308 case 'oauth2':
2309 handleWarning({
2310 mitigationType: MitigationTypes.OAUTH_SECURITY_SCHEME,
2311 message: `OAuth security scheme found in OAS '${oas.info.title}'. ` +
2312 `OAuth support is provided using the 'tokenJSONpath' option`,
2313 data,
2314 log: preprocessingLog,
2315 });
2316 // Continue because we do not want to create an OAuth viewer
2317 continue;
2318 default:
2319 handleWarning({
2320 mitigationType: MitigationTypes.UNSUPPORTED_HTTP_SECURITY_SCHEME,
2321 message: `Unsupported HTTP authentication protocol` + `type '${protocol.type}' in OAS '${oas.info.title}'`,
2322 data,
2323 log: preprocessingLog,
2324 });
2325 }
2326 // Add protocol data to the output
2327 result[key] = {
2328 rawName: key,
2329 def: protocol,
2330 parameters,
2331 schema,
2332 oas,
2333 };
2334 }
2335 return result;
2336}
2337/**
2338 * Method to either create a new or reuse an existing, centrally stored data
2339 * definition.
2340 */
2341function createDataDef(names, schema, isInputObjectType, data, oas, links) {
2342 const preferredName = getPreferredName(names);
2343 // Basic validation test
2344 if (typeof schema !== 'object') {
2345 handleWarning({
2346 mitigationType: MitigationTypes.MISSING_SCHEMA,
2347 message: `Could not create data definition for schema with ` +
2348 `preferred name '${preferredName}' and schema '${JSON.stringify(schema)}'`,
2349 data,
2350 log: preprocessingLog,
2351 });
2352 // TODO: Does this change make the option fillEmptyResponses obsolete?
2353 return {
2354 preferredName,
2355 schema: null,
2356 required: [],
2357 links: null,
2358 subDefinitions: null,
2359 graphQLTypeName: null,
2360 graphQLInputObjectTypeName: null,
2361 targetGraphQLType: 'json',
2362 };
2363 }
2364 else {
2365 if ('$ref' in schema) {
2366 schema = resolveRef(schema.$ref, oas);
2367 }
2368 const saneLinks = {};
2369 if (typeof links === 'object') {
2370 Object.keys(links).forEach(linkKey => {
2371 saneLinks[sanitize(linkKey.toString(), !data.options.simpleNames ? CaseStyle.camelCase : CaseStyle.simple)] = links[linkKey];
2372 });
2373 }
2374 // Determine the index of possible existing data definition
2375 const index = getSchemaIndex(preferredName, schema, data.defs);
2376 if (index !== -1) {
2377 // Found existing data definition and fetch it
2378 const existingDataDef = data.defs[index];
2379 /**
2380 * Collapse links if possible, i.e. if the current operation has links,
2381 * combine them with the prexisting ones
2382 */
2383 if (typeof saneLinks !== 'undefined') {
2384 if (typeof existingDataDef.links !== 'undefined') {
2385 // Check if there are any overlapping links
2386 Object.keys(existingDataDef.links).forEach(saneLinkKey => {
2387 if (typeof saneLinks[saneLinkKey] !== 'undefined' &&
2388 !deepEqual(existingDataDef.links[saneLinkKey], saneLinks[saneLinkKey])) {
2389 handleWarning({
2390 mitigationType: MitigationTypes.DUPLICATE_LINK_KEY,
2391 message: `Multiple operations with the same response body share the same sanitized ` +
2392 `link key '${saneLinkKey}' but have different link definitions ` +
2393 `'${JSON.stringify(existingDataDef.links[saneLinkKey])}' and ` +
2394 `'${JSON.stringify(saneLinks[saneLinkKey])}'.`,
2395 data,
2396 log: preprocessingLog,
2397 });
2398 }
2399 });
2400 /**
2401 * Collapse the links
2402 *
2403 * Avoid overwriting preexisting links
2404 */
2405 existingDataDef.links = { ...saneLinks, ...existingDataDef.links };
2406 }
2407 else {
2408 // No preexisting links, so simply assign the links
2409 existingDataDef.links = saneLinks;
2410 }
2411 }
2412 return existingDataDef;
2413 }
2414 else {
2415 // Else, define a new name, store the def, and return it
2416 const name = getSchemaName(names, data.usedTypeNames);
2417 // Store and sanitize the name
2418 const saneName = !data.options.simpleNames
2419 ? sanitize(name, CaseStyle.PascalCase)
2420 : capitalize(sanitize(name, CaseStyle.simple));
2421 const saneInputName = capitalize(saneName + 'Input');
2422 storeSaneName(saneName, name, data.saneMap);
2423 /**
2424 * TODO: is there a better way of copying the schema object?
2425 *
2426 * Perhaps, just copy it at the root level (operation schema)
2427 */
2428 const collapsedSchema = resolveAllOf(schema, {}, data, oas);
2429 const targetGraphQLType = getSchemaTargetGraphQLType(collapsedSchema, data);
2430 const def = {
2431 preferredName,
2432 /**
2433 * Note that schema may contain $ref or schema composition (e.g. allOf)
2434 *
2435 * TODO: the schema is used in getSchemaIndex, which allows us to check
2436 * whether a dataDef has already been created for that particular
2437 * schema and name pair. The look up should resolve references but
2438 * currently, it does not.
2439 */
2440 schema,
2441 required: [],
2442 targetGraphQLType,
2443 subDefinitions: undefined,
2444 links: saneLinks,
2445 graphQLTypeName: saneName,
2446 graphQLInputObjectTypeName: saneInputName,
2447 };
2448 // Used type names and defs of union and object types are pushed during creation
2449 if (targetGraphQLType === 'object' || targetGraphQLType === 'list' || targetGraphQLType === 'enum') {
2450 data.usedTypeNames.push(saneName);
2451 data.usedTypeNames.push(saneInputName);
2452 // Add the def to the master list
2453 data.defs.push(def);
2454 }
2455 // We currently only support simple cases of anyOf and oneOf
2456 if (
2457 // TODO: Should also consider if the member schema contains type data
2458 (Array.isArray(collapsedSchema.anyOf) && Array.isArray(collapsedSchema.oneOf)) || // anyOf and oneOf used concurrently
2459 hasNestedAnyOfUsage(collapsedSchema, oas) ||
2460 hasNestedOneOfUsage(collapsedSchema, oas)) {
2461 handleWarning({
2462 mitigationType: MitigationTypes.COMBINE_SCHEMAS,
2463 message: `Schema '${JSON.stringify(schema)}' contains either both ` +
2464 `'anyOf' and 'oneOf' or nested 'anyOf' and 'oneOf' which ` +
2465 `is currently not supported.`,
2466 mitigationAddendum: `Use arbitrary JSON type instead.`,
2467 data,
2468 log: preprocessingLog,
2469 });
2470 def.targetGraphQLType = 'json';
2471 return def;
2472 }
2473 // oneOf will ideally be turned into a union type
2474 if (Array.isArray(collapsedSchema.oneOf)) {
2475 const oneOfDataDef = createDataDefFromOneOf(saneName, saneInputName, collapsedSchema, isInputObjectType, def, data, oas);
2476 if (typeof oneOfDataDef === 'object') {
2477 return oneOfDataDef;
2478 }
2479 }
2480 /**
2481 * anyOf will ideally be turned into an object type
2482 *
2483 * Fields common to all member schemas will be made non-null
2484 */
2485 if (Array.isArray(collapsedSchema.anyOf)) {
2486 const anyOfDataDef = createDataDefFromAnyOf(saneName, saneInputName, collapsedSchema, isInputObjectType, def, data, oas);
2487 if (typeof anyOfDataDef === 'object') {
2488 return anyOfDataDef;
2489 }
2490 }
2491 if (targetGraphQLType) {
2492 switch (targetGraphQLType) {
2493 case 'list':
2494 if (typeof collapsedSchema.items === 'object') {
2495 // Break schema down into component parts
2496 // I.e. if it is an list type, create a reference to the list item type
2497 // Or if it is an object type, create references to all of the field types
2498 const itemsSchema = collapsedSchema.items;
2499 let itemsName = `${name}ListItem`;
2500 if ('$ref' in itemsSchema) {
2501 itemsName = collapsedSchema.items.$ref.split('/').pop();
2502 }
2503 const subDefinition = createDataDef(
2504 // Is this the correct classification for this name? It does not matter in the long run.
2505 { fromRef: itemsName }, itemsSchema, isInputObjectType, data, oas);
2506 // Add list item reference
2507 def.subDefinitions = subDefinition;
2508 }
2509 break;
2510 case 'object':
2511 def.subDefinitions = {};
2512 if (typeof collapsedSchema.properties === 'object' && Object.keys(collapsedSchema.properties).length > 0) {
2513 addObjectPropertiesToDataDef(def, collapsedSchema, def.required, isInputObjectType, data, oas);
2514 }
2515 else {
2516 handleWarning({
2517 mitigationType: MitigationTypes.OBJECT_MISSING_PROPERTIES,
2518 message: `Schema ${JSON.stringify(schema)} does not have ` + `any properties`,
2519 data,
2520 log: preprocessingLog,
2521 });
2522 def.targetGraphQLType = 'json';
2523 }
2524 break;
2525 }
2526 }
2527 else {
2528 // No target GraphQL type
2529 handleWarning({
2530 mitigationType: MitigationTypes.UNKNOWN_TARGET_TYPE,
2531 message: `No GraphQL target type could be identified for schema '${JSON.stringify(schema)}'.`,
2532 data,
2533 log: preprocessingLog,
2534 });
2535 def.targetGraphQLType = 'json';
2536 }
2537 return def;
2538 }
2539 }
2540}
2541/**
2542 * Returns the index of the data definition object in the given list that
2543 * contains the same schema and preferred name as the given one. Returns -1 if
2544 * that schema could not be found.
2545 */
2546function getSchemaIndex(preferredName, schema, dataDefs) {
2547 /**
2548 * TODO: instead of iterating through the whole list every time, create a
2549 * hashing function and store all of the DataDefinitions in a hashmap.
2550 */
2551 for (let index = 0; index < dataDefs.length; index++) {
2552 const def = dataDefs[index];
2553 /**
2554 * TODO: deepEquals is not sufficient. We also need to resolve references.
2555 * However, deepEquals should work for vast majority of cases.
2556 */
2557 if (preferredName === def.preferredName && deepEqual(schema, def.schema)) {
2558 return index;
2559 }
2560 }
2561 // The schema could not be found in the master list
2562 return -1;
2563}
2564/**
2565 * Determines the preferred name to use for schema regardless of name collisions.
2566 *
2567 * In other words, determines the ideal name for a schema.
2568 *
2569 * Similar to getSchemaName() except it does not check if the name has already
2570 * been taken.
2571 */
2572function getPreferredName(names) {
2573 if (typeof names.preferred === 'string') {
2574 return sanitize(names.preferred, CaseStyle.PascalCase); // CASE: preferred name already known
2575 }
2576 else if (typeof names.fromRef === 'string') {
2577 return sanitize(names.fromRef, CaseStyle.PascalCase); // CASE: name from reference
2578 }
2579 else if (typeof names.fromSchema === 'string') {
2580 return sanitize(names.fromSchema, CaseStyle.PascalCase); // CASE: name from schema (i.e., "title" property in schema)
2581 }
2582 else if (typeof names.fromPath === 'string') {
2583 return sanitize(names.fromPath, CaseStyle.PascalCase); // CASE: name from path
2584 }
2585 else {
2586 return 'PlaceholderName'; // CASE: placeholder name
2587 }
2588}
2589/**
2590 * Determines name to use for schema from previously determined schemaNames and
2591 * considering not reusing existing names.
2592 */
2593function getSchemaName(names, usedNames) {
2594 if (Object.keys(names).length === 1 && typeof names.preferred === 'string') {
2595 throw new Error(`Cannot create data definition without name(s), excluding the preferred name.`);
2596 }
2597 let schemaName;
2598 // CASE: name from reference
2599 if (typeof names.fromRef === 'string') {
2600 const saneName = sanitize(names.fromRef, CaseStyle.PascalCase);
2601 if (!usedNames.includes(saneName)) {
2602 schemaName = names.fromRef;
2603 }
2604 }
2605 // CASE: name from schema (i.e., "title" property in schema)
2606 if (!schemaName && typeof names.fromSchema === 'string') {
2607 const saneName = sanitize(names.fromSchema, CaseStyle.PascalCase);
2608 if (!usedNames.includes(saneName)) {
2609 schemaName = names.fromSchema;
2610 }
2611 }
2612 // CASE: name from path
2613 if (!schemaName && typeof names.fromPath === 'string') {
2614 const saneName = sanitize(names.fromPath, CaseStyle.PascalCase);
2615 if (!usedNames.includes(saneName)) {
2616 schemaName = names.fromPath;
2617 }
2618 }
2619 // CASE: all names are already used - create approximate name
2620 if (!schemaName) {
2621 schemaName = sanitize(typeof names.fromRef === 'string'
2622 ? names.fromRef
2623 : typeof names.fromSchema === 'string'
2624 ? names.fromSchema
2625 : typeof names.fromPath === 'string'
2626 ? names.fromPath
2627 : 'PlaceholderName', CaseStyle.PascalCase);
2628 }
2629 if (usedNames.includes(schemaName)) {
2630 let appendix = 2;
2631 /**
2632 * GraphQL Objects cannot share the name so if the name already exists in
2633 * the master list append an incremental number until the name does not
2634 * exist anymore.
2635 */
2636 while (usedNames.includes(`${schemaName}${appendix}`)) {
2637 appendix++;
2638 }
2639 schemaName = `${schemaName}${appendix}`;
2640 }
2641 return schemaName;
2642}
2643/**
2644 * Recursively add all of the properties of an object to the data definition
2645 */
2646function addObjectPropertiesToDataDef(def, schema, required, isInputObjectType, data, oas) {
2647 /**
2648 * Resolve all required properties
2649 *
2650 * TODO: required may contain duplicates, which is not necessarily a problem
2651 */
2652 if (Array.isArray(schema.required)) {
2653 schema.required.forEach(requiredProperty => {
2654 required.push(requiredProperty);
2655 });
2656 }
2657 for (const propertyKey in schema.properties) {
2658 let propSchemaName = propertyKey;
2659 let propSchema = schema.properties[propertyKey];
2660 if ('$ref' in propSchema) {
2661 propSchemaName = propSchema.$ref.split('/').pop();
2662 propSchema = resolveRef(propSchema.$ref, oas);
2663 }
2664 if (!(propertyKey in def.subDefinitions)) {
2665 const subDefinition = createDataDef({
2666 fromRef: propSchemaName,
2667 fromSchema: propSchema.title,
2668 }, propSchema, isInputObjectType, data, oas);
2669 // Add field type references
2670 def.subDefinitions[propertyKey] = subDefinition;
2671 }
2672 else {
2673 handleWarning({
2674 mitigationType: MitigationTypes.DUPLICATE_FIELD_NAME,
2675 message: `By way of resolving 'allOf', multiple schemas contain ` +
2676 `properties with the same name, preventing consolidation. Cannot ` +
2677 `add property '${propertyKey}' from schema '${JSON.stringify(schema)}' ` +
2678 `to dataDefinition '${JSON.stringify(def)}'`,
2679 data,
2680 log: preprocessingLog,
2681 });
2682 }
2683 }
2684}
2685/**
2686 * Recursively traverse a schema and resolve allOf by appending the data to the
2687 * parent schema
2688 */
2689function resolveAllOf(schema, references, data, oas) {
2690 // Dereference schema
2691 if ('$ref' in schema) {
2692 const referenceLocation = schema.$ref;
2693 schema = resolveRef(schema.$ref, oas);
2694 if (referenceLocation in references) {
2695 return references[referenceLocation];
2696 }
2697 else {
2698 // Store references in case of circular allOf
2699 references[referenceLocation] = schema;
2700 }
2701 }
2702 const collapsedSchema = JSON.parse(JSON.stringify(schema));
2703 // Resolve allOf
2704 if (Array.isArray(collapsedSchema.allOf)) {
2705 collapsedSchema.allOf.forEach(memberSchema => {
2706 // Collapse type if applicable
2707 const resolvedSchema = resolveAllOf(memberSchema, references, data, oas);
2708 if (resolvedSchema.type) {
2709 if (!collapsedSchema.type) {
2710 collapsedSchema.type = resolvedSchema.type;
2711 // Add type if applicable
2712 }
2713 else if (collapsedSchema.type !== resolvedSchema.type) {
2714 // Incompatible schema type
2715 handleWarning({
2716 mitigationType: MitigationTypes.UNRESOLVABLE_SCHEMA,
2717 message: `Resolving 'allOf' field in schema '${collapsedSchema}' ` +
2718 `results in incompatible schema type from partial schema '${resolvedSchema}'.`,
2719 data,
2720 log: preprocessingLog,
2721 });
2722 }
2723 }
2724 // Collapse properties if applicable
2725 if ('properties' in resolvedSchema) {
2726 if (!('properties' in collapsedSchema)) {
2727 collapsedSchema.properties = {};
2728 }
2729 Object.entries(resolvedSchema.properties).forEach(([propertyName, property]) => {
2730 if (propertyName in collapsedSchema) {
2731 // Conflicting property
2732 handleWarning({
2733 mitigationType: MitigationTypes.UNRESOLVABLE_SCHEMA,
2734 message: `Resolving 'allOf' field in schema '${collapsedSchema}' ` +
2735 `results in incompatible property field from partial schema '${resolvedSchema}'.`,
2736 data,
2737 log: preprocessingLog,
2738 });
2739 }
2740 else {
2741 collapsedSchema.properties[propertyName] = property;
2742 }
2743 });
2744 }
2745 // Collapse oneOf if applicable
2746 if ('oneOf' in resolvedSchema) {
2747 if (!('oneOf' in collapsedSchema)) {
2748 collapsedSchema.oneOf = [];
2749 }
2750 resolvedSchema.oneOf.forEach(oneOfProperty => {
2751 collapsedSchema.oneOf.push(oneOfProperty);
2752 });
2753 }
2754 // Collapse anyOf if applicable
2755 if ('anyOf' in resolvedSchema) {
2756 if (!('anyOf' in collapsedSchema)) {
2757 collapsedSchema.anyOf = [];
2758 }
2759 resolvedSchema.anyOf.forEach(anyOfProperty => {
2760 collapsedSchema.anyOf.push(anyOfProperty);
2761 });
2762 }
2763 // Collapse required if applicable
2764 if ('required' in resolvedSchema) {
2765 if (!('required' in collapsedSchema)) {
2766 collapsedSchema.required = [];
2767 }
2768 resolvedSchema.required.forEach(requiredProperty => {
2769 if (!collapsedSchema.required.includes(requiredProperty)) {
2770 collapsedSchema.required.push(requiredProperty);
2771 }
2772 });
2773 }
2774 });
2775 }
2776 return collapsedSchema;
2777}
2778/**
2779 * In the context of schemas that use keywords that combine member schemas,
2780 * collect data on certain aspects so it is all in one place for processing.
2781 */
2782function getMemberSchemaData(schemas, data, oas) {
2783 const result = {
2784 allTargetGraphQLTypes: [],
2785 allProperties: [],
2786 allRequired: [],
2787 };
2788 schemas.forEach(schema => {
2789 // Dereference schemas
2790 if ('$ref' in schema) {
2791 schema = resolveRef(schema.$ref, oas);
2792 }
2793 // Consolidate target GraphQL type
2794 const memberTargetGraphQLType = getSchemaTargetGraphQLType(schema, data);
2795 if (memberTargetGraphQLType) {
2796 result.allTargetGraphQLTypes.push(memberTargetGraphQLType);
2797 }
2798 // Consolidate properties
2799 if (schema.properties) {
2800 result.allProperties.push(schema.properties);
2801 }
2802 // Consolidate required
2803 if (schema.required) {
2804 result.allRequired = result.allRequired.concat(schema.required);
2805 }
2806 });
2807 return result;
2808}
2809/**
2810 * Check to see if there are cases of nested oneOf fields in the member schemas
2811 *
2812 * We currently cannot handle complex cases of oneOf and anyOf
2813 */
2814function hasNestedOneOfUsage(collapsedSchema, oas) {
2815 // TODO: Should also consider if the member schema contains type data
2816 return (Array.isArray(collapsedSchema.oneOf) &&
2817 collapsedSchema.oneOf.some(memberSchema => {
2818 // anyOf and oneOf are nested
2819 if ('$ref' in memberSchema) {
2820 memberSchema = resolveRef(memberSchema.$ref, oas);
2821 }
2822 return (Array.isArray(memberSchema.anyOf) || Array.isArray(memberSchema.oneOf) // Nested oneOf would result in nested unions which are not allowed by GraphQL
2823 );
2824 }));
2825}
2826/**
2827 * Check to see if there are cases of nested anyOf fields in the member schemas
2828 *
2829 * We currently cannot handle complex cases of oneOf and anyOf
2830 */
2831function hasNestedAnyOfUsage(collapsedSchema, oas) {
2832 // TODO: Should also consider if the member schema contains type data
2833 return (Array.isArray(collapsedSchema.anyOf) &&
2834 collapsedSchema.anyOf.some(memberSchema => {
2835 // anyOf and oneOf are nested
2836 if ('$ref' in memberSchema) {
2837 memberSchema = resolveRef(memberSchema.$ref, oas);
2838 }
2839 return Array.isArray(memberSchema.anyOf) || Array.isArray(memberSchema.oneOf);
2840 }));
2841}
2842/**
2843 * Create a data definition for anyOf is applicable
2844 *
2845 * anyOf should resolve into an object that contains the superset of all
2846 * properties from the member schemas
2847 */
2848function createDataDefFromAnyOf(saneName, saneInputName, collapsedSchema, isInputObjectType, def, data, oas) {
2849 const anyOfData = getMemberSchemaData(collapsedSchema.anyOf, data, oas);
2850 if (anyOfData.allTargetGraphQLTypes.some(memberTargetGraphQLType => {
2851 return memberTargetGraphQLType === 'object';
2852 })) {
2853 // Every member type should be an object
2854 if (anyOfData.allTargetGraphQLTypes.every(memberTargetGraphQLType => {
2855 return memberTargetGraphQLType === 'object';
2856 }) &&
2857 anyOfData.allProperties.length > 0 // Redundant check
2858 ) {
2859 // Ensure that parent schema is compatible with oneOf
2860 if (def.targetGraphQLType === null || def.targetGraphQLType === 'object') {
2861 const allProperties = {};
2862 const incompatibleProperties = new Set();
2863 /**
2864 * TODO: Check for consistent properties across all member schemas and
2865 * make them into non-nullable properties by manipulating the
2866 * required field
2867 */
2868 if (typeof collapsedSchema.properties === 'object') {
2869 Object.keys(collapsedSchema.properties).forEach(propertyName => {
2870 allProperties[propertyName] = [collapsedSchema.properties[propertyName]];
2871 });
2872 }
2873 // Check if any member schema has conflicting properties
2874 anyOfData.allProperties.forEach(properties => {
2875 Object.keys(properties).forEach(propertyName => {
2876 if (!incompatibleProperties.has(propertyName.toString()) && // Has not been already identified as a problematic property
2877 typeof allProperties[propertyName] === 'object' &&
2878 allProperties[propertyName].some(property => {
2879 // Property does not match a recorded one
2880 return !deepEqual(property, properties[propertyName]);
2881 })) {
2882 incompatibleProperties.add(propertyName.toString());
2883 }
2884 // Add property in the store
2885 if (!(propertyName in allProperties)) {
2886 allProperties[propertyName] = [];
2887 }
2888 allProperties[propertyName].push(properties[propertyName]);
2889 });
2890 });
2891 def.subDefinitions = {};
2892 if (typeof collapsedSchema.properties === 'object' && Object.keys(collapsedSchema.properties).length > 0) {
2893 addObjectPropertiesToDataDef(def, collapsedSchema, def.required, isInputObjectType, data, oas);
2894 }
2895 anyOfData.allProperties.forEach(properties => {
2896 Object.keys(properties).forEach(propertyName => {
2897 if (!incompatibleProperties.has(propertyName.toString())) {
2898 // Dereferenced by processing anyOfData
2899 const propertySchema = properties[propertyName];
2900 const subDefinition = createDataDef({
2901 fromRef: propertyName.toString(),
2902 fromSchema: propertySchema.title,
2903 }, propertySchema, isInputObjectType, data, oas);
2904 /**
2905 * Add field type references
2906 * There should not be any collisions
2907 */
2908 def.subDefinitions[propertyName] = subDefinition;
2909 }
2910 });
2911 });
2912 // Add in incompatible properties
2913 incompatibleProperties.forEach(propertyName => {
2914 // TODO: add description
2915 def.subDefinitions[propertyName] = {
2916 targetGraphQLType: 'json',
2917 };
2918 });
2919 data.usedTypeNames.push(saneName);
2920 data.usedTypeNames.push(saneInputName);
2921 data.defs.push(def);
2922 def.targetGraphQLType = 'object';
2923 return def;
2924 }
2925 else {
2926 // The parent schema is incompatible with the member schemas
2927 handleWarning({
2928 mitigationType: MitigationTypes.COMBINE_SCHEMAS,
2929 message: `Schema '${JSON.stringify(def.schema)}' contains 'anyOf' and ` +
2930 `some member schemas are object types so create a GraphQL ` +
2931 `object type but the parent schema is a non-object type ` +
2932 `so they are not compatible.`,
2933 mitigationAddendum: `Use arbitrary JSON type instead.`,
2934 data,
2935 log: preprocessingLog,
2936 });
2937 def.targetGraphQLType = 'json';
2938 return def;
2939 }
2940 }
2941 else {
2942 // The member schemas are not all object types
2943 handleWarning({
2944 mitigationType: MitigationTypes.COMBINE_SCHEMAS,
2945 message: `Schema '${def.schema}' contains 'anyOf' and ` +
2946 `some member schemas are object types so create a GraphQL ` +
2947 `object type but some member schemas are non-object types ` +
2948 `so they are not compatible.`,
2949 data,
2950 log: preprocessingLog,
2951 });
2952 def.targetGraphQLType = 'json';
2953 return def;
2954 }
2955 }
2956}
2957function createDataDefFromOneOf(saneName, saneInputName, collapsedSchema, isInputObjectType, def, data, oas) {
2958 const oneOfData = getMemberSchemaData(collapsedSchema.oneOf, data, oas);
2959 if (oneOfData.allTargetGraphQLTypes.some(memberTargetGraphQLType => {
2960 return memberTargetGraphQLType === 'object';
2961 })) {
2962 // unions must be created from object types
2963 if (oneOfData.allTargetGraphQLTypes.every(memberTargetGraphQLType => {
2964 return memberTargetGraphQLType === 'object';
2965 }) &&
2966 oneOfData.allProperties.length > 0 // Redundant check
2967 ) {
2968 // Input object types cannot be composed of unions
2969 if (isInputObjectType) {
2970 handleWarning({
2971 mitigationType: MitigationTypes.INPUT_UNION,
2972 message: `Input object types cannot be composed of union types.`,
2973 data,
2974 log: preprocessingLog,
2975 });
2976 def.targetGraphQLType = 'json';
2977 return def;
2978 }
2979 // Ensure that parent schema is compatible with oneOf
2980 if (def.targetGraphQLType === null || def.targetGraphQLType === 'object') {
2981 def.subDefinitions = [];
2982 collapsedSchema.oneOf.forEach(memberSchema => {
2983 // Dereference member schema
2984 let fromRef;
2985 if ('$ref' in memberSchema) {
2986 fromRef = memberSchema.$ref.split('/').pop();
2987 memberSchema = resolveRef(memberSchema.$ref, oas);
2988 }
2989 // Member types of GraphQL unions must be object types
2990 if (getSchemaTargetGraphQLType(memberSchema, data) === 'object') {
2991 const subDefinition = createDataDef({
2992 fromRef,
2993 fromSchema: memberSchema.title,
2994 fromPath: `${saneName}Member`,
2995 }, memberSchema, isInputObjectType, data, oas);
2996 def.subDefinitions.push(subDefinition);
2997 }
2998 else {
2999 handleWarning({
3000 mitigationType: MitigationTypes.COMBINE_SCHEMAS,
3001 message: `Schema '${JSON.stringify(def.schema)}' contains 'oneOf' so ` +
3002 `create a GraphQL union type but member schema '${JSON.stringify(memberSchema)}' ` +
3003 `is not an object type and union member types must be ` +
3004 `object base types.`,
3005 data,
3006 log: preprocessingLog,
3007 });
3008 }
3009 });
3010 // Not all member schemas may have been turned into GraphQL member types
3011 if (def.subDefinitions.length > 0 &&
3012 def.subDefinitions.every(subDefinition => {
3013 return subDefinition.targetGraphQLType === 'object';
3014 })) {
3015 // Ensure all member schemas have been verified as object types
3016 data.usedTypeNames.push(saneName);
3017 data.usedTypeNames.push(saneInputName);
3018 data.defs.push(def);
3019 def.targetGraphQLType = 'union';
3020 return def;
3021 }
3022 else {
3023 handleWarning({
3024 mitigationType: MitigationTypes.COMBINE_SCHEMAS,
3025 message: `Schema '${JSON.stringify(def.schema)}' contains 'oneOf' so ` +
3026 `create a GraphQL union type but all member schemas are not` +
3027 `object types and union member types must be object types.`,
3028 mitigationAddendum: `Use arbitrary JSON type instead.`,
3029 data,
3030 log: preprocessingLog,
3031 });
3032 // Default arbitrary JSON type
3033 def.targetGraphQLType = 'json';
3034 return def;
3035 }
3036 }
3037 else {
3038 // The parent schema is incompatible with the member schemas
3039 handleWarning({
3040 mitigationType: MitigationTypes.COMBINE_SCHEMAS,
3041 message: `Schema '${JSON.stringify(def.schema)}' contains 'oneOf' so create ` +
3042 `a GraphQL union type but the parent schema is a non-object ` +
3043 `type and member types must be object types.`,
3044 mitigationAddendum: `Use arbitrary JSON type instead.`,
3045 data,
3046 log: preprocessingLog,
3047 });
3048 def.targetGraphQLType = 'json';
3049 return def;
3050 }
3051 }
3052 else {
3053 // The member schemas are not all object types
3054 handleWarning({
3055 mitigationType: MitigationTypes.COMBINE_SCHEMAS,
3056 message: `Schema '${JSON.stringify(def.schema)}' contains 'oneOf' so create ` +
3057 `a GraphQL union type but some member schemas are non-object ` +
3058 `types and union member types must be object types.`,
3059 mitigationAddendum: `Use arbitrary JSON type instead.`,
3060 data,
3061 log: preprocessingLog,
3062 });
3063 def.targetGraphQLType = 'json';
3064 return def;
3065 }
3066 }
3067}
3068
3069/* eslint-disable @typescript-eslint/ban-ts-comment */
3070const translationLog$2 = mockDebug('translation');
3071/**
3072 * Creates and returns a GraphQL type for the given JSON schema.
3073 */
3074function getGraphQLType({ def, operation, data, iteration = 0, isInputObjectType = false, includeHttpDetails = false, }) {
3075 const name = isInputObjectType ? def.graphQLInputObjectTypeName : def.graphQLTypeName;
3076 // Avoid excessive iterations
3077 if (iteration === 50) {
3078 throw new Error(`GraphQL type ${name} has excessive nesting of other types`);
3079 }
3080 switch (def.targetGraphQLType) {
3081 // CASE: object - create object type
3082 case 'object':
3083 return createOrReuseOt({
3084 def,
3085 operation,
3086 data,
3087 iteration,
3088 isInputObjectType,
3089 includeHttpDetails,
3090 });
3091 // CASE: union - create union type
3092 case 'union':
3093 return createOrReuseUnion({
3094 def,
3095 operation,
3096 data,
3097 iteration,
3098 includeHttpDetails,
3099 });
3100 // CASE: list - create list type
3101 case 'list':
3102 return createOrReuseList({
3103 def,
3104 operation,
3105 data,
3106 iteration,
3107 isInputObjectType,
3108 includeHttpDetails,
3109 });
3110 // CASE: enum - create enum type
3111 case 'enum':
3112 return createOrReuseEnum({
3113 def,
3114 data,
3115 });
3116 // CASE: scalar - return scalar type
3117 default:
3118 return getScalarType({
3119 def,
3120 data,
3121 });
3122 }
3123}
3124/**
3125 * Creates an (input) object type or return an existing one, and stores it
3126 * in data
3127 *
3128 * A returned GraphQLObjectType has the following internal structure:
3129 *
3130 * new GraphQLObjectType({
3131 * name // Optional name of the type
3132 * description // Optional description of type
3133 * fields // REQUIRED returning fields
3134 * type // REQUIRED definition of the field type
3135 * args // Optional definition of types
3136 * resolve // Optional function defining how to obtain this type
3137 * })
3138 */
3139function createOrReuseOt({ def, operation, data, iteration, isInputObjectType, includeHttpDetails, }) {
3140 // Try to reuse a preexisting (input) object type
3141 // CASE: query - reuse object type
3142 if (!isInputObjectType) {
3143 if (def.graphQLType && typeof def.graphQLType !== 'undefined') {
3144 translationLog$2(`Reuse object type '${def.graphQLTypeName}'` +
3145 (typeof operation === 'object' ? ` (for operation '${operation.operationString}')` : ''));
3146 return def.graphQLType;
3147 }
3148 // CASE: mutation - reuse input object type
3149 }
3150 else {
3151 if (def.graphQLInputObjectType && typeof def.graphQLInputObjectType !== 'undefined') {
3152 translationLog$2(`Reuse input object type '${def.graphQLInputObjectTypeName}'` +
3153 (typeof operation === 'object' ? ` (for operation '${operation.operationString}')` : ''));
3154 return def.graphQLInputObjectType;
3155 }
3156 }
3157 // Cannot reuse preexisting (input) object type, therefore create one
3158 const schema = def.schema;
3159 const description = schema.description;
3160 // CASE: query - create object type
3161 if (!isInputObjectType) {
3162 translationLog$2(`Create object type '${def.graphQLTypeName}'` +
3163 (typeof operation === 'object' ? ` (for operation '${operation.operationString}')` : ''));
3164 def.graphQLType = new graphql.GraphQLObjectType({
3165 name: def.graphQLTypeName,
3166 description,
3167 fields: () => {
3168 return createFields({
3169 def,
3170 links: def.links,
3171 operation,
3172 data,
3173 iteration,
3174 isInputObjectType: false,
3175 includeHttpDetails,
3176 });
3177 },
3178 });
3179 return def.graphQLType;
3180 // CASE: mutation - create input object type
3181 }
3182 else {
3183 translationLog$2(`Create input object type '${def.graphQLInputObjectTypeName}'` +
3184 (typeof operation === 'object' ? ` (for operation '${operation.operationString}')` : ''));
3185 def.graphQLInputObjectType = new graphql.GraphQLInputObjectType({
3186 name: def.graphQLInputObjectTypeName,
3187 description,
3188 fields: () => {
3189 return createFields({
3190 def,
3191 links: {},
3192 operation,
3193 data,
3194 iteration,
3195 isInputObjectType: true,
3196 includeHttpDetails,
3197 });
3198 },
3199 });
3200 return def.graphQLInputObjectType;
3201 }
3202}
3203/**
3204 * Creates a union type or return an existing one, and stores it in data
3205 */
3206function createOrReuseUnion({ def, operation, data, iteration, includeHttpDetails, }) {
3207 // Try to reuse existing union type
3208 if (typeof def.graphQLType !== 'undefined') {
3209 translationLog$2(`Reuse union type '${def.graphQLTypeName}'` +
3210 (typeof operation === 'object' ? ` (for operation '${operation.operationString}')` : ''));
3211 return def.graphQLType;
3212 }
3213 else {
3214 translationLog$2(`Create union type '${def.graphQLTypeName}'` +
3215 (typeof operation === 'object' ? ` (for operation '${operation.operationString}')` : ''));
3216 const schema = def.schema;
3217 const description = typeof schema.description !== 'undefined' ? schema.description : 'No description available.';
3218 const memberTypeDefinitions = def.subDefinitions;
3219 const types = Object.values(memberTypeDefinitions).map(memberTypeDefinition => {
3220 return getGraphQLType({
3221 def: memberTypeDefinition,
3222 operation,
3223 data,
3224 iteration: iteration + 1,
3225 isInputObjectType: false,
3226 includeHttpDetails,
3227 });
3228 });
3229 /**
3230 * Check for ambiguous member types
3231 *
3232 * i.e. member types that can be confused with each other.
3233 */
3234 checkAmbiguousMemberTypes(def, types, data);
3235 def.graphQLType = new graphql.GraphQLUnionType({
3236 name: def.graphQLTypeName,
3237 description,
3238 types,
3239 resolveType: (source, context, info) => {
3240 const properties = Object.keys(source)
3241 // Remove custom _openAPIToGraphQL property used to pass data
3242 .filter(property => property !== '_openAPIToGraphQL');
3243 /**
3244 * Find appropriate member type
3245 *
3246 * TODO: currently, the check is performed by only checking the property
3247 * names. In the future, we should also check the types of those
3248 * properties.
3249 *
3250 * TODO: there is a chance a that an intended member type cannot be
3251 * identified if, for whatever reason, the return data is a superset
3252 * of the fields specified in the OAS
3253 */
3254 return types.find(type => {
3255 const typeFields = Object.keys(type.getFields());
3256 // The type should be a superset of the properties
3257 if (properties.length <= typeFields.length) {
3258 return properties.every(property => typeFields.includes(property.toString()));
3259 }
3260 return false;
3261 });
3262 },
3263 });
3264 return def.graphQLType;
3265 }
3266}
3267/**
3268 * Check for ambiguous member types
3269 *
3270 * i.e. member types that can be confused with each other.
3271 */
3272function checkAmbiguousMemberTypes(def, types, data) {
3273 types.sort((a, b) => {
3274 const aFieldLength = Object.keys(a.getFields()).length;
3275 const bFieldLength = Object.keys(b.getFields()).length;
3276 if (aFieldLength < bFieldLength) {
3277 return -1;
3278 }
3279 else if (aFieldLength < bFieldLength) {
3280 return 1;
3281 }
3282 else {
3283 return 0;
3284 }
3285 });
3286 for (let i = 0; i < types.length - 1; i++) {
3287 const currentType = types[i];
3288 for (let j = i + 1; j < types.length; j++) {
3289 const otherType = types[j];
3290 // TODO: Check the value, not just the field name
3291 if (Object.keys(currentType.getFields()).every(field => {
3292 return Object.keys(otherType.getFields()).includes(field);
3293 })) {
3294 handleWarning({
3295 mitigationType: MitigationTypes.AMBIGUOUS_UNION_MEMBERS,
3296 message: `Union created from schema '${JSON.stringify(def)}' contains ` +
3297 `member types such as '${currentType}' and '${otherType}' ` +
3298 `which are ambiguous. Ambiguous member types can cause ` +
3299 `problems when trying to resolve types.`,
3300 data,
3301 log: translationLog$2,
3302 });
3303 return;
3304 }
3305 }
3306 }
3307}
3308/**
3309 * Creates a list type or returns an existing one, and stores it in data
3310 */
3311function createOrReuseList({ def, operation, iteration, isInputObjectType, data, includeHttpDetails, }) {
3312 const name = isInputObjectType ? def.graphQLInputObjectTypeName : def.graphQLTypeName;
3313 // Try to reuse existing Object Type
3314 if (!isInputObjectType && def.graphQLType && typeof def.graphQLType !== 'undefined') {
3315 translationLog$2(`Reuse GraphQLList '${def.graphQLTypeName}'`);
3316 return def.graphQLType;
3317 }
3318 else if (isInputObjectType && def.graphQLInputObjectType && typeof def.graphQLInputObjectType !== 'undefined') {
3319 translationLog$2(`Reuse GraphQLList '${def.graphQLInputObjectTypeName}'`);
3320 return def.graphQLInputObjectType;
3321 }
3322 // Create new List Object Type
3323 translationLog$2(`Create GraphQLList '${def.graphQLTypeName}'`);
3324 // Get definition of the list item, which should be in the sub definitions
3325 const itemDef = def.subDefinitions;
3326 // Equivalent to schema.items
3327 const itemsSchema = itemDef.schema;
3328 // Equivalent to `{name}ListItem`
3329 const itemsName = itemDef.graphQLTypeName;
3330 const itemsType = getGraphQLType({
3331 def: itemDef,
3332 data,
3333 operation,
3334 iteration: iteration + 1,
3335 isInputObjectType,
3336 includeHttpDetails,
3337 });
3338 if (itemsType !== null) {
3339 const listObjectType = new graphql.GraphQLList(itemsType);
3340 // Store newly created list type
3341 if (!isInputObjectType) {
3342 def.graphQLType = listObjectType;
3343 }
3344 else {
3345 def.graphQLInputObjectType = listObjectType;
3346 }
3347 return listObjectType;
3348 }
3349 else {
3350 throw new Error(`Cannot create list item object type '${itemsName}' in list
3351 '${name}' with schema '${JSON.stringify(itemsSchema)}'`);
3352 }
3353}
3354/**
3355 * Creates an enum type or returns an existing one, and stores it in data
3356 */
3357function createOrReuseEnum({ def, }) {
3358 /**
3359 * Try to reuse existing enum type
3360 *
3361 * Enum types do not have an input variant so only check def.ot
3362 */
3363 if (def.graphQLType && typeof def.graphQLType !== 'undefined') {
3364 translationLog$2(`Reuse GraphQLEnumType '${def.graphQLTypeName}'`);
3365 return def.graphQLType;
3366 }
3367 else {
3368 translationLog$2(`Create GraphQLEnumType '${def.graphQLTypeName}'`);
3369 const values = {};
3370 def.schema.enum.forEach(e => {
3371 // Force enum values to string and value should be in ALL_CAPS
3372 values[sanitize(e.toString(), CaseStyle.ALL_CAPS)] = {
3373 value: e,
3374 };
3375 });
3376 // Store newly created Enum Object Type
3377 def.graphQLType = new graphql.GraphQLEnumType({
3378 name: def.graphQLTypeName,
3379 values,
3380 });
3381 return def.graphQLType;
3382 }
3383}
3384/**
3385 * Returns the GraphQL scalar type matching the given JSON schema type
3386 */
3387function getScalarType({ def, data, }) {
3388 switch (def.targetGraphQLType) {
3389 case 'id':
3390 def.graphQLType = graphql.GraphQLID;
3391 break;
3392 case 'string':
3393 def.graphQLType = graphql.GraphQLString;
3394 break;
3395 case 'integer':
3396 def.graphQLType = graphql.GraphQLInt;
3397 break;
3398 case 'int64':
3399 def.graphQLType = graphqlScalars.GraphQLBigInt;
3400 break;
3401 case 'number':
3402 def.graphQLType = graphql.GraphQLFloat;
3403 break;
3404 case 'float':
3405 def.graphQLType = graphql.GraphQLFloat;
3406 break;
3407 case 'boolean':
3408 def.graphQLType = graphql.GraphQLBoolean;
3409 break;
3410 case 'json':
3411 def.graphQLType = graphqlScalars.GraphQLJSON;
3412 break;
3413 default:
3414 throw new Error(`Cannot process schema type '${def.targetGraphQLType}'.`);
3415 }
3416 return def.graphQLType;
3417}
3418/**
3419 * Creates the fields object to be used by an (input) object type
3420 */
3421function createFields({ def, links, operation, data, iteration, isInputObjectType, includeHttpDetails, }) {
3422 let fields = {};
3423 if (includeHttpDetails && !isInputObjectType) {
3424 fields.httpDetails = {
3425 type: graphqlScalars.GraphQLJSON,
3426 resolve: root => { var _a; return (_a = root === null || root === void 0 ? void 0 : root._openAPIToGraphQL) === null || _a === void 0 ? void 0 : _a.data; },
3427 };
3428 }
3429 const fieldTypeDefinitions = def.subDefinitions;
3430 // Create fields for properties
3431 for (const fieldTypeKey in fieldTypeDefinitions) {
3432 const fieldTypeDefinition = fieldTypeDefinitions[fieldTypeKey];
3433 const fieldSchema = fieldTypeDefinition.schema;
3434 // Get object type describing the property
3435 const objectType = getGraphQLType({
3436 def: fieldTypeDefinition,
3437 operation,
3438 data,
3439 iteration: iteration + 1,
3440 isInputObjectType,
3441 includeHttpDetails,
3442 });
3443 const requiredProperty = typeof def.required === 'object' && def.required.includes(fieldTypeKey);
3444 // Finally, add the object type to the fields (using sanitized field name)
3445 if (objectType) {
3446 const saneFieldTypeKey = sanitize(fieldTypeKey, !data.options.simpleNames ? CaseStyle.camelCase : CaseStyle.simple);
3447 const sanePropName = storeSaneName(saneFieldTypeKey, fieldTypeKey, data.saneMap);
3448 fields[sanePropName] = {
3449 type: requiredProperty ? new graphql.GraphQLNonNull(objectType) : objectType,
3450 description: typeof fieldSchema === 'object' ? fieldSchema.description : null,
3451 };
3452 }
3453 else {
3454 handleWarning({
3455 mitigationType: MitigationTypes.CANNOT_GET_FIELD_TYPE,
3456 message: `Cannot obtain GraphQL type for field '${fieldTypeKey}' in ` +
3457 `GraphQL type '${JSON.stringify(def.schema)}'.`,
3458 data,
3459 log: translationLog$2,
3460 });
3461 }
3462 }
3463 if (typeof links === 'object' && // Links are present
3464 !isInputObjectType // Only object type (input object types cannot make use of links)
3465 ) {
3466 for (const saneLinkKey in links) {
3467 // Check if key is already in fields
3468 if (saneLinkKey in fields) {
3469 handleWarning({
3470 mitigationType: MitigationTypes.LINK_NAME_COLLISION,
3471 message: `Cannot create link '${saneLinkKey}' because parent ` +
3472 `object type already contains a field with the same (sanitized) name.`,
3473 data,
3474 log: translationLog$2,
3475 });
3476 }
3477 else {
3478 const link = links[saneLinkKey];
3479 // Get linked operation
3480 let linkedOpId;
3481 // TODO: href is yet another alternative to operationRef and operationId
3482 if (typeof link.operationId === 'string') {
3483 linkedOpId = link.operationId;
3484 }
3485 else if (typeof link.operationRef === 'string') {
3486 linkedOpId = linkOpRefToOpId({
3487 links,
3488 linkKey: saneLinkKey,
3489 operation,
3490 data,
3491 });
3492 }
3493 /**
3494 * linkedOpId may not be initialized because operationRef may lead to an
3495 * operation object that does not have an operationId
3496 */
3497 if (typeof linkedOpId === 'string' && linkedOpId in data.operations) {
3498 const linkedOp = data.operations[linkedOpId];
3499 // Determine parameters provided via link
3500 const argsFromLink = link.parameters;
3501 // Get arguments that are not provided by the linked operation
3502 let dynamicParams = linkedOp.parameters;
3503 if (typeof argsFromLink === 'object') {
3504 dynamicParams = dynamicParams.filter(param => {
3505 return typeof argsFromLink[param.name] === 'undefined';
3506 });
3507 }
3508 // Get resolve function for link
3509 const linkResolver = data.options.resolverMiddleware(() => ({
3510 operation: linkedOp,
3511 argsFromLink: argsFromLink,
3512 data,
3513 baseUrl: data.options.baseUrl,
3514 requestOptions: data.options.requestOptions,
3515 }), getResolver);
3516 // Get arguments for link
3517 const args = getArgs({
3518 parameters: dynamicParams,
3519 operation: linkedOp,
3520 data,
3521 includeHttpDetails,
3522 });
3523 // Get response object type
3524 const resObjectType = linkedOp.responseDefinition.graphQLType !== undefined
3525 ? linkedOp.responseDefinition.graphQLType
3526 : getGraphQLType({
3527 def: linkedOp.responseDefinition,
3528 operation,
3529 data,
3530 iteration: iteration + 1,
3531 isInputObjectType: false,
3532 includeHttpDetails,
3533 });
3534 let description = link.description;
3535 if (data.options.equivalentToMessages && description) {
3536 description += `\n\nEquivalent to ${linkedOp.operationString}`;
3537 }
3538 // Finally, add the object type to the fields (using sanitized field name)
3539 // TODO: check if fields already has this field name
3540 fields[saneLinkKey] = {
3541 type: resObjectType,
3542 resolve: linkResolver,
3543 args,
3544 description,
3545 };
3546 }
3547 else {
3548 handleWarning({
3549 mitigationType: MitigationTypes.UNRESOLVABLE_LINK,
3550 message: `Cannot resolve target of link '${saneLinkKey}'`,
3551 data,
3552 log: translationLog$2,
3553 });
3554 }
3555 }
3556 }
3557 }
3558 fields = sortObject(fields);
3559 return fields;
3560}
3561/**
3562 * Returns the operationId that an operationRef is associated to
3563 *
3564 * NOTE: If the operation does not natively have operationId, this function
3565 * will try to produce an operationId the same way preprocessor.js does it.
3566 *
3567 * Any changes to constructing operationIds in preprocessor.js should be
3568 * reflected here.
3569 */
3570function linkOpRefToOpId({ links, linkKey, operation, data, }) {
3571 const link = links[linkKey];
3572 if (typeof link.operationRef === 'string') {
3573 // TODO: external refs
3574 const operationRef = link.operationRef;
3575 let linkLocation;
3576 let linkRelativePathAndMethod;
3577 /**
3578 * Example relative path: '#/paths/~12.0~1repositories~1{username}/get'
3579 * Example absolute path: 'https://na2.gigantic-server.com/#/paths/~12.0~1repositories~1{username}/get'
3580 * Extract relative path from relative path
3581 */
3582 if (operationRef.substring(0, 8) === '#/paths/') {
3583 linkRelativePathAndMethod = operationRef;
3584 // Extract relative path from absolute path
3585 }
3586 else {
3587 /**
3588 * '#' may exist in other places in the path
3589 * '/#/' is more likely to point to the beginning of the path
3590 */
3591 const firstPathIndex = operationRef.indexOf('#/paths/');
3592 // Found a relative path candidate
3593 if (firstPathIndex !== -1) {
3594 // Check to see if there are other relative path candidates
3595 const lastPathIndex = operationRef.lastIndexOf('#/paths/');
3596 if (firstPathIndex !== lastPathIndex) {
3597 handleWarning({
3598 mitigationType: MitigationTypes.AMBIGUOUS_LINK,
3599 message: `The link '${linkKey}' in operation '${operation.operationString}' ` +
3600 `contains an ambiguous operationRef '${operationRef}', ` +
3601 `meaning it has multiple instances of the string '#/paths/'`,
3602 data,
3603 log: translationLog$2,
3604 });
3605 return null;
3606 }
3607 linkLocation = operationRef.substring(0, firstPathIndex);
3608 linkRelativePathAndMethod = operationRef.substring(firstPathIndex);
3609 // Cannot find relative path candidate
3610 }
3611 else {
3612 handleWarning({
3613 mitigationType: MitigationTypes.UNRESOLVABLE_LINK,
3614 message: `The link '${linkKey}' in operation '${operation.operationString}' ` +
3615 `does not contain a valid path in operationRef '${operationRef}', ` +
3616 `meaning it does not contain a string '#/paths/'`,
3617 data,
3618 log: translationLog$2,
3619 });
3620 return null;
3621 }
3622 }
3623 // Infer operationId from relative path
3624 if (typeof linkRelativePathAndMethod === 'string') {
3625 let linkPath;
3626 let linkMethod;
3627 /**
3628 * NOTE: I wish we could extract the linkedOpId by matching the
3629 * linkedOpObject with an operation in data and extracting the operationId
3630 * there but that does not seem to be possible especiially because you
3631 * need to know the operationId just to access the operations so what I
3632 * have to do is reconstruct the operationId the same way preprocessing
3633 * does it
3634 */
3635 /**
3636 * linkPath should be the path followed by the method
3637 *
3638 * Find the slash that divides the path from the method
3639 */
3640 const pivotSlashIndex = linkRelativePathAndMethod.lastIndexOf('/');
3641 // Check if there are any '/' in the linkPath
3642 if (pivotSlashIndex !== -1) {
3643 // Get method
3644 // Check if there is a method at the end of the linkPath
3645 if (pivotSlashIndex !== linkRelativePathAndMethod.length - 1) {
3646 try {
3647 // Start at +1 because we do not want the starting '/'
3648 linkMethod = methodToHttpMethod(linkRelativePathAndMethod.substring(pivotSlashIndex + 1));
3649 }
3650 catch (_a) {
3651 handleWarning({
3652 mitigationType: MitigationTypes.UNRESOLVABLE_LINK,
3653 message: `The operationRef '${operationRef}' contains an ` + `invalid HTTP method '${linkMethod}'`,
3654 data,
3655 log: translationLog$2,
3656 });
3657 return null;
3658 }
3659 // There is no method at the end of the path
3660 }
3661 else {
3662 handleWarning({
3663 mitigationType: MitigationTypes.UNRESOLVABLE_LINK,
3664 message: `The operationRef '${operationRef}' does not contain an` + `HTTP method`,
3665 data,
3666 log: translationLog$2,
3667 });
3668 return null;
3669 }
3670 /**
3671 * Get path
3672 *
3673 * Substring starts at index 8 and ends at pivotSlashIndex to exclude
3674 * the '/'s at the ends of the path
3675 *
3676 * TODO: improve removing '/#/paths'?
3677 */
3678 linkPath = linkRelativePathAndMethod.substring(8, pivotSlashIndex);
3679 /**
3680 * linkPath is currently a JSON Pointer
3681 *
3682 * Revert the escaped '/', represented by '~1', to form intended path
3683 */
3684 linkPath = linkPath.replace(/~1/g, '/');
3685 // Find the right oas
3686 const oas = typeof linkLocation === 'undefined' ? operation.oas : getOasFromLinkLocation(linkLocation, link, data);
3687 // If the link was external, make sure that an OAS could be identified
3688 if (typeof oas !== 'undefined') {
3689 if (typeof linkMethod === 'string' && typeof linkPath === 'string') {
3690 let linkedOpId;
3691 if (linkPath in oas.paths && linkMethod in oas.paths[linkPath]) {
3692 const linkedOpObject = oas.paths[linkPath][linkMethod];
3693 if ('operationId' in linkedOpObject) {
3694 linkedOpId = linkedOpObject.operationId;
3695 }
3696 }
3697 if (typeof linkedOpId !== 'string') {
3698 linkedOpId = generateOperationId(linkMethod, linkPath);
3699 }
3700 if (linkedOpId in data.operations) {
3701 return linkedOpId;
3702 }
3703 else {
3704 handleWarning({
3705 mitigationType: MitigationTypes.UNRESOLVABLE_LINK,
3706 message: `The link '${linkKey}' references an operation with ` +
3707 `operationId '${linkedOpId}' but no such operation exists. ` +
3708 `Note that the operationId may be autogenerated but ` +
3709 `regardless, the link could not be matched to an operation.`,
3710 data,
3711 log: translationLog$2,
3712 });
3713 return null;
3714 }
3715 // Path and method could not be found
3716 }
3717 else {
3718 handleWarning({
3719 mitigationType: MitigationTypes.UNRESOLVABLE_LINK,
3720 message: `Cannot identify path and/or method, '${linkPath} and ` +
3721 `'${linkMethod}' respectively, from operationRef ` +
3722 `'${operationRef}' in link '${linkKey}'`,
3723 data,
3724 log: translationLog$2,
3725 });
3726 return null;
3727 }
3728 // External link could not be resolved
3729 }
3730 else {
3731 handleWarning({
3732 mitigationType: MitigationTypes.UNRESOLVABLE_LINK,
3733 message: `The link '${link.operationRef}' references an external OAS ` + `but it was not provided`,
3734 data,
3735 log: translationLog$2,
3736 });
3737 return null;
3738 }
3739 // Cannot split relative path into path and method sections
3740 }
3741 else {
3742 handleWarning({
3743 mitigationType: MitigationTypes.UNRESOLVABLE_LINK,
3744 message: `Cannot extract path and/or method from operationRef ` + `'${operationRef}' in link '${linkKey}'`,
3745 data,
3746 log: translationLog$2,
3747 });
3748 return null;
3749 }
3750 // Cannot extract relative path from absolute path
3751 }
3752 else {
3753 handleWarning({
3754 mitigationType: MitigationTypes.UNRESOLVABLE_LINK,
3755 message: `Cannot extract path and/or method from operationRef ` + `'${operationRef}' in link '${linkKey}'`,
3756 data,
3757 log: translationLog$2,
3758 });
3759 return null;
3760 }
3761 }
3762}
3763/**
3764 * Determin if an argument should be created if the argument has already been
3765 * provided through the options
3766 */
3767function skipArg(parameter, operation, data, includeHttpDetails) {
3768 if (typeof data.options === 'object') {
3769 switch (parameter.in) {
3770 case 'header':
3771 // Check header option
3772 if (typeof data.options.headers === 'object' && parameter.name in data.options.headers) {
3773 return true;
3774 }
3775 else if (typeof data.options.headers === 'function') {
3776 const headers = data.options.headers(operation.method, operation.path, operation.oas.info.title);
3777 if (typeof headers === 'object') {
3778 return true;
3779 }
3780 // Check requestOptions option
3781 }
3782 else if (typeof data.options.requestOptions === 'object') {
3783 if (typeof data.options.requestOptions.headers === 'object' &&
3784 parameter.name in data.options.requestOptions.headers) {
3785 return true;
3786 } /* else if (
3787 typeof data.options.requestOptions.headers === 'function'
3788 ) {
3789 const headers = data.options.requestOptions.headers(
3790 operation.method,
3791 operation.path,
3792 operation.oas.info.title
3793 )
3794
3795 if (typeof headers === 'object') {
3796 return true
3797 }
3798 } */
3799 }
3800 break;
3801 case 'query':
3802 // Check header option
3803 if (typeof data.options.qs === 'object' && parameter.name in data.options.qs) {
3804 return true;
3805 // Check requestOptions option
3806 } /* else if (
3807 typeof data.options.requestOptions === 'object' &&
3808 typeof data.options.requestOptions.qs === 'object' &&
3809 parameter.name in data.options.requestOptions.qs
3810 ) {
3811 return true
3812 } */
3813 break;
3814 }
3815 }
3816 return false;
3817}
3818/**
3819 * Creates the arguments for resolving a field
3820 *
3821 * Arguments that are provided via options will be ignored
3822 */
3823function getArgs({ requestPayloadDef, parameters, operation, data, includeHttpDetails, }) {
3824 let args = {};
3825 // Handle params:
3826 parameters.forEach(parameter => {
3827 // We need at least a name
3828 if (typeof parameter.name !== 'string') {
3829 handleWarning({
3830 mitigationType: MitigationTypes.INVALID_OAS,
3831 message: `The operation '${operation.operationString}' contains a ` +
3832 `parameter '${JSON.stringify(parameter)}' with no 'name' property`,
3833 data,
3834 log: translationLog$2,
3835 });
3836 return;
3837 }
3838 // If this parameter is provided via options, ignore
3839 if (skipArg(parameter, operation, data)) {
3840 return;
3841 }
3842 /**
3843 * Determine type of parameter
3844 *
3845 * The type of the parameter can either be contained in the "schema" field
3846 * or the "content" field (but not both)
3847 */
3848 let schema;
3849 if (typeof parameter.schema === 'object') {
3850 schema = parameter.schema;
3851 }
3852 else if (typeof parameter.content === 'object') {
3853 if (typeof parameter.content['application/json'] === 'object' &&
3854 typeof parameter.content['application/json'].schema === 'object') {
3855 schema = parameter.content['application/json'].schema;
3856 }
3857 else {
3858 handleWarning({
3859 mitigationType: MitigationTypes.NON_APPLICATION_JSON_SCHEMA,
3860 message: `The operation '${operation.operationString}' contains a ` +
3861 `parameter '${JSON.stringify(parameter)}' that has a 'content' ` +
3862 `property but no schemas in application/json format. The ` +
3863 `parameter will not be created`,
3864 data,
3865 log: translationLog$2,
3866 });
3867 return;
3868 }
3869 }
3870 else {
3871 // Invalid OAS according to 3.0.2
3872 handleWarning({
3873 mitigationType: MitigationTypes.INVALID_OAS,
3874 message: `The operation '${operation.operationString}' contains a ` +
3875 `parameter '${JSON.stringify(parameter)}' with no 'schema' or ` +
3876 `'content' property`,
3877 data,
3878 log: translationLog$2,
3879 });
3880 return;
3881 }
3882 /**
3883 * Resolving the reference is necessary later in the code and by doing it,
3884 * we can avoid doing it a second time in resolveRev()
3885 */
3886 if ('$ref' in schema) {
3887 schema = resolveRef(schema.$ref, operation.oas);
3888 }
3889 const paramDef = createDataDef({ fromSchema: parameter.name }, schema, true, data, operation.oas);
3890 // @ts-ignore
3891 const type = getGraphQLType({
3892 def: paramDef,
3893 operation,
3894 data,
3895 iteration: 0,
3896 isInputObjectType: true,
3897 });
3898 /**
3899 * Sanitize the argument name
3900 *
3901 * NOTE: when matching these parameters back to requests, we need to again
3902 * use the real parameter name
3903 */
3904 const saneName = sanitize(parameter.name, !data.options.simpleNames ? CaseStyle.camelCase : CaseStyle.simple);
3905 // Parameters are not required when a default exists:
3906 let hasDefault = false;
3907 if (typeof parameter.schema === 'object') {
3908 let schema = parameter.schema;
3909 if (typeof schema.$ref === 'string') {
3910 schema = resolveRef(parameter.schema.$ref, operation.oas);
3911 }
3912 if (typeof schema.default !== 'undefined') {
3913 hasDefault = true;
3914 }
3915 }
3916 const paramRequired = parameter.required && !hasDefault;
3917 args[saneName] = {
3918 type: paramRequired ? new graphql.GraphQLNonNull(type) : type,
3919 description: parameter.description,
3920 };
3921 });
3922 // Add limit argument
3923 if (data.options.addLimitArgument &&
3924 typeof operation.responseDefinition === 'object' &&
3925 operation.responseDefinition.schema.type === 'array' &&
3926 // Only add limit argument to lists of object types, not to lists of scalar types
3927 (operation.responseDefinition.subDefinitions.schema.type === 'object' ||
3928 operation.responseDefinition.subDefinitions.schema.type === 'array')) {
3929 // Make sure slicing arguments will not overwrite preexisting arguments
3930 if ('limit' in args) {
3931 handleWarning({
3932 mitigationType: MitigationTypes.LIMIT_ARGUMENT_NAME_COLLISION,
3933 message: `The 'limit' argument cannot be added ` +
3934 `because of a preexisting argument in ` +
3935 `operation ${operation.operationString}`,
3936 data,
3937 log: translationLog$2,
3938 });
3939 }
3940 else {
3941 args.limit = {
3942 type: graphql.GraphQLInt,
3943 description: `Auto-generated argument that limits the size of ` +
3944 `returned list of objects/list, selecting the first \`n\` ` +
3945 `elements of the list`,
3946 };
3947 }
3948 }
3949 // Handle request payload (if present):
3950 if (typeof requestPayloadDef === 'object') {
3951 const reqObjectType = getGraphQLType({
3952 def: requestPayloadDef,
3953 data,
3954 operation,
3955 isInputObjectType: true,
3956 includeHttpDetails,
3957 });
3958 // Sanitize the argument name
3959 const saneName = data.options.genericPayloadArgName
3960 ? 'requestBody'
3961 : uncapitalize(requestPayloadDef.graphQLInputObjectTypeName); // Already sanitized
3962 const reqRequired = typeof operation === 'object' && typeof operation.payloadRequired === 'boolean'
3963 ? operation.payloadRequired
3964 : false;
3965 args[saneName] = {
3966 type: reqRequired ? new graphql.GraphQLNonNull(reqObjectType) : reqObjectType,
3967 // TODO: addendum to the description explaining this is the request body
3968 description: requestPayloadDef.schema.description,
3969 };
3970 }
3971 args = sortObject(args);
3972 return args;
3973}
3974/**
3975 * Used in the context of links, specifically those using an external operationRef
3976 * If the reference is an absolute reference, determine the type of location
3977 *
3978 * For example, name reference, file path, web-hosted OAS link, etc.
3979 */
3980function getLinkLocationType(linkLocation) {
3981 // TODO: currently we only support the title as a link location
3982 return 'title';
3983}
3984/**
3985 * Used in the context of links, specifically those using an external operationRef
3986 * Based on the location of the OAS, retrieve said OAS
3987 */
3988function getOasFromLinkLocation(linkLocation, link, data) {
3989 // May be an external reference
3990 switch (getLinkLocationType()) {
3991 case 'title':
3992 // Get the possible
3993 const possibleOass = data.oass.filter(oas => {
3994 return oas.info.title === linkLocation;
3995 });
3996 // Check if there are an ambiguous OASs
3997 if (possibleOass.length === 1) {
3998 // No ambiguity
3999 return possibleOass[0];
4000 }
4001 else if (possibleOass.length > 1) {
4002 // Some ambiguity
4003 handleWarning({
4004 mitigationType: MitigationTypes.AMBIGUOUS_LINK,
4005 message: `The operationRef '${link.operationRef}' references an ` +
4006 `OAS '${linkLocation}' but multiple OASs share the same title`,
4007 data,
4008 log: translationLog$2,
4009 });
4010 }
4011 else {
4012 // No OAS had the expected title
4013 handleWarning({
4014 mitigationType: MitigationTypes.UNRESOLVABLE_LINK,
4015 message: `The operationRef '${link.operationRef}' references an ` +
4016 `OAS '${linkLocation}' but no such OAS was provided`,
4017 data,
4018 log: translationLog$2,
4019 });
4020 }
4021 break;
4022 // // TODO
4023 // case 'url':
4024 // break
4025 // // TODO
4026 // case 'file':
4027 // break
4028 // TODO: should title be default?
4029 // In cases of names like api.io
4030 default:
4031 handleWarning({
4032 mitigationType: MitigationTypes.UNRESOLVABLE_LINK,
4033 message: `The link location of the operationRef ` +
4034 `'${link.operationRef}' is currently not supported\n` +
4035 `Currently only the title of the OAS is supported`,
4036 data,
4037 log: translationLog$2,
4038 });
4039 }
4040}
4041
4042// Copyright IBM Corp. 2018. All Rights Reserved.
4043/**
4044 * Returns empty GraphQLObjectType.
4045 */
4046function getEmptyObjectType(name) {
4047 return new graphql.GraphQLObjectType({
4048 name: name + 'Placeholder',
4049 description: 'Placeholder object',
4050 fields: {
4051 message: {
4052 type: graphql.GraphQLString,
4053 description: 'Placeholder field',
4054 resolve: () => {
4055 return 'This is a placeholder field.';
4056 },
4057 },
4058 },
4059 });
4060}
4061
4062// Copyright IBM Corp. 2018. All Rights Reserved.
4063const translationLog$3 = mockDebug('translation');
4064/**
4065 * Load the field object in the appropriate root object
4066 *
4067 * i.e. inside either rootQueryFields/rootMutationFields or inside
4068 * rootQueryFields/rootMutationFields for further processing
4069 */
4070function createAndLoadViewer(queryFields, operationType, data, includeHttpDetails) {
4071 const results = {};
4072 /**
4073 * To ensure that viewers have unique names, we add a numerical postfix.
4074 *
4075 * This object keeps track of what the postfix should be.
4076 *
4077 * The key is the security scheme type and the value is
4078 * the current highest postfix used for viewers of that security scheme type.
4079 */
4080 const viewerNamePostfix = {};
4081 /**
4082 * Used to collect all fields in the given querFields object, no matter which
4083 * protocol. Used to populate anyAuthViewer.
4084 */
4085 const anyAuthFields = {};
4086 for (const protocolName in queryFields) {
4087 Object.assign(anyAuthFields, queryFields[protocolName]);
4088 /**
4089 * Check if the name has already been used (i.e. in the list)
4090 * if so, create a new name and add it to the list
4091 */
4092 const securityType = data.security[protocolName].def.type;
4093 let viewerType;
4094 /**
4095 * HTTP is not an authentication protocol
4096 * HTTP covers a number of different authentication type
4097 * change the typeName to match the exact authentication type (e.g. basic
4098 * authentication)
4099 */
4100 if (securityType === 'http') {
4101 const scheme = data.security[protocolName].def.scheme;
4102 switch (scheme) {
4103 case 'basic':
4104 viewerType = 'basicAuth';
4105 break;
4106 default:
4107 handleWarning({
4108 mitigationType: MitigationTypes.UNSUPPORTED_HTTP_SECURITY_SCHEME,
4109 message: `Currently unsupported HTTP authentication protocol ` + `type 'http' and scheme '${scheme}'`,
4110 data,
4111 log: translationLog$3,
4112 });
4113 continue;
4114 }
4115 }
4116 else {
4117 viewerType = securityType;
4118 }
4119 // Create name for the viewer
4120 let viewerName = operationType === GraphQLOperationType.Query
4121 ? sanitize(`viewer ${viewerType}`, CaseStyle.camelCase)
4122 : operationType === GraphQLOperationType.Mutation
4123 ? sanitize(`mutation viewer ${viewerType}`, CaseStyle.camelCase)
4124 : sanitize(`subscription viewer ${viewerType}`, CaseStyle.camelCase);
4125 // Ensure unique viewer name
4126 // If name already exists, append a number at the end of the name
4127 if (!(viewerType in viewerNamePostfix)) {
4128 viewerNamePostfix[viewerType] = 1;
4129 }
4130 else {
4131 viewerName += ++viewerNamePostfix[viewerType];
4132 }
4133 // Add the viewer object type to the specified root query object type
4134 results[viewerName] = getViewerOT(viewerName, protocolName, securityType, queryFields[protocolName], data);
4135 }
4136 // Create name for the AnyAuth viewer
4137 const anyAuthObjectName = operationType === GraphQLOperationType.Query
4138 ? 'viewerAnyAuth'
4139 : operationType === GraphQLOperationType.Mutation
4140 ? 'mutationViewerAnyAuth'
4141 : 'subscriptionViewerAnyAuth';
4142 // Add the AnyAuth object type to the specified root query object type
4143 results[anyAuthObjectName] = getViewerAnyAuthOT(anyAuthObjectName, anyAuthFields, data, includeHttpDetails);
4144 return results;
4145}
4146/**
4147 * Gets the viewer Object, resolve function, and arguments
4148 */
4149function getViewerOT(name, protocolName, securityType, queryFields, data) {
4150 const scheme = data.security[protocolName];
4151 // Resolve function:
4152 const resolve = (root, args, context) => {
4153 const security = {};
4154 const saneProtocolName = sanitize(protocolName, CaseStyle.camelCase);
4155 security[storeSaneName(saneProtocolName, protocolName, data.saneMap)] = args;
4156 /**
4157 * Viewers are always root, so we can instantiate _openAPIToGraphQL here without
4158 * previously checking for its existence
4159 */
4160 return {
4161 _openAPIToGraphQL: {
4162 security,
4163 },
4164 };
4165 };
4166 // Arguments:
4167 /**
4168 * Do not sort because they are already "sorted" in preprocessing.
4169 * Otherwise, for basic auth, "password" will appear before "username"
4170 */
4171 const args = {};
4172 if (typeof scheme === 'object') {
4173 for (const parameterName in scheme.parameters) {
4174 args[parameterName] = { type: new graphql.GraphQLNonNull(graphql.GraphQLString) };
4175 }
4176 }
4177 let typeDescription = `A viewer for security scheme '${protocolName}'`;
4178 /**
4179 * HTTP authentication uses different schemes. It is not sufficient to name
4180 * only the security type
4181 */
4182 let description = securityType === 'http'
4183 ? `A viewer that wraps all operations authenticated via security scheme ` +
4184 `'${protocolName}', which is of type 'http' '${scheme.def.scheme}'`
4185 : `A viewer that wraps all operations authenticated via security scheme ` +
4186 `'${protocolName}', which is of type '${securityType}'`;
4187 if (data.oass.length !== 1) {
4188 typeDescription += ` in OAS '${scheme.oas.info.title}'`;
4189 description = `, in OAS '${scheme.oas.info.title}`;
4190 }
4191 return {
4192 type: new graphql.GraphQLObjectType({
4193 name: capitalize(name),
4194 description: typeDescription,
4195 fields: () => queryFields,
4196 }),
4197 resolve,
4198 args,
4199 description,
4200 };
4201}
4202/**
4203 * Create an object containing an AnyAuth viewer, its resolve function,
4204 * and its args.
4205 */
4206function getViewerAnyAuthOT(name, queryFields, data, includeHttpDetails) {
4207 let args = {};
4208 for (const protocolName in data.security) {
4209 // Create input object types for the viewer arguments
4210 const def = createDataDef({ fromRef: protocolName }, data.security[protocolName].schema, true, data, data.security[protocolName].oas);
4211 const type = getGraphQLType({
4212 def,
4213 data,
4214 isInputObjectType: true,
4215 includeHttpDetails,
4216 });
4217 const saneProtocolName = sanitize(protocolName, CaseStyle.camelCase);
4218 args[storeSaneName(saneProtocolName, protocolName, data.saneMap)] = { type };
4219 }
4220 args = sortObject(args);
4221 // Pass object containing security information to fields
4222 const resolve = (root, args, context) => {
4223 return {
4224 _openAPIToGraphQL: {
4225 security: args,
4226 },
4227 };
4228 };
4229 return {
4230 type: new graphql.GraphQLObjectType({
4231 name: capitalize(name),
4232 description: 'Warning: Not every request will work with this viewer type',
4233 fields: () => queryFields,
4234 }),
4235 resolve,
4236 args,
4237 description: `A viewer that wraps operations for all available ` + `authentication mechanisms`,
4238 };
4239}
4240
4241// Copyright IBM Corp. 2018. All Rights Reserved.
4242const translationLog$4 = mockDebug('translation');
4243/**
4244 * Creates a GraphQL interface from the given OpenAPI Specification (2 or 3).
4245 */
4246async function createGraphQLSchema(spec, options = {}) {
4247 // Setting default options
4248 options.strict = typeof options.strict === 'boolean' ? options.strict : false;
4249 // Schema options
4250 options.operationIdFieldNames =
4251 typeof options.operationIdFieldNames === 'boolean' ? options.operationIdFieldNames : false;
4252 options.fillEmptyResponses = typeof options.fillEmptyResponses === 'boolean' ? options.fillEmptyResponses : false;
4253 options.addLimitArgument = typeof options.addLimitArgument === 'boolean' ? options.addLimitArgument : false;
4254 options.genericPayloadArgName =
4255 typeof options.genericPayloadArgName === 'boolean' ? options.genericPayloadArgName : false;
4256 options.simpleNames = typeof options.simpleNames === 'boolean' ? options.simpleNames : false;
4257 options.singularNames = typeof options.singularNames === 'boolean' ? options.singularNames : false;
4258 options.createSubscriptionsFromCallbacks =
4259 typeof options.createSubscriptionsFromCallbacks === 'boolean' ? options.createSubscriptionsFromCallbacks : false;
4260 // Authentication options
4261 options.viewer = typeof options.viewer === 'boolean' ? options.viewer : true;
4262 options.sendOAuthTokenInQuery =
4263 typeof options.sendOAuthTokenInQuery === 'boolean' ? options.sendOAuthTokenInQuery : false;
4264 // Logging options
4265 options.provideErrorExtensions =
4266 typeof options.provideErrorExtensions === 'boolean' ? options.provideErrorExtensions : true;
4267 options.equivalentToMessages =
4268 typeof options.equivalentToMessages === 'boolean' ? options.equivalentToMessages : true;
4269 options.resolverMiddleware =
4270 typeof options.resolverMiddleware === 'function'
4271 ? options.resolverMiddleware
4272 : (resolverFactoryParams, factory) => factory(resolverFactoryParams);
4273 options.report = {
4274 warnings: [],
4275 numOps: 0,
4276 numOpsQuery: 0,
4277 numOpsMutation: 0,
4278 numOpsSubscription: 0,
4279 numQueriesCreated: 0,
4280 numMutationsCreated: 0,
4281 numSubscriptionsCreated: 0,
4282 };
4283 options.includeHttpDetails = typeof options.includeHttpDetails === 'boolean' ? options.includeHttpDetails : false;
4284 let oass;
4285 if (Array.isArray(spec)) {
4286 /**
4287 * Convert all non-OAS 3.0.x into OAS 3.0.x
4288 */
4289 oass = await Promise.all(spec.map(ele => {
4290 return getValidOAS3(ele);
4291 }));
4292 }
4293 else {
4294 /**
4295 * Check if the spec is a valid OAS 3.0.x
4296 * If the spec is OAS 2.0, attempt to translate it into 3.0.x, then try to
4297 * translate the spec into a GraphQL schema
4298 */
4299 oass = [await getValidOAS3(spec)];
4300 }
4301 const { schema, report } = await translateOpenAPIToGraphQL(oass, options);
4302 return {
4303 schema,
4304 report,
4305 };
4306}
4307/**
4308 * Creates a GraphQL interface from the given OpenAPI Specification 3.0.x
4309 */
4310async function translateOpenAPIToGraphQL(oass, { strict, report,
4311// Schema options
4312operationIdFieldNames, fillEmptyResponses, addLimitArgument, idFormats, selectQueryOrMutationField, genericPayloadArgName, simpleNames, singularNames, createSubscriptionsFromCallbacks, includeHttpDetails,
4313// Resolver options
4314headers, qs, requestOptions, connectOptions, baseUrl, customResolvers, fetch, resolverMiddleware, pubsub,
4315// Authentication options
4316viewer, tokenJSONpath, sendOAuthTokenInQuery,
4317// Logging options
4318provideErrorExtensions, equivalentToMessages, }) {
4319 const options = {
4320 strict,
4321 report,
4322 // Schema options
4323 operationIdFieldNames,
4324 fillEmptyResponses,
4325 addLimitArgument,
4326 idFormats,
4327 selectQueryOrMutationField,
4328 genericPayloadArgName,
4329 simpleNames,
4330 singularNames,
4331 createSubscriptionsFromCallbacks,
4332 includeHttpDetails,
4333 // Resolver options
4334 headers,
4335 qs,
4336 requestOptions,
4337 connectOptions,
4338 baseUrl,
4339 customResolvers,
4340 fetch,
4341 resolverMiddleware,
4342 pubsub,
4343 // Authentication options
4344 viewer,
4345 tokenJSONpath,
4346 sendOAuthTokenInQuery,
4347 // Logging options
4348 provideErrorExtensions,
4349 equivalentToMessages,
4350 };
4351 translationLog$4(`Options: ${JSON.stringify(options)}`);
4352 /**
4353 * Extract information from the OASs and put it inside a data structure that
4354 * is easier for OpenAPI-to-GraphQL to use
4355 */
4356 const data = preprocessOas(oass, options);
4357 preliminaryChecks(options, data);
4358 // Query, Mutation, and Subscription fields
4359 let queryFields = {};
4360 let mutationFields = {};
4361 let subscriptionFields = {};
4362 // Authenticated Query, Mutation, and Subscription fields
4363 let authQueryFields = {};
4364 let authMutationFields = {};
4365 let authSubscriptionFields = {};
4366 // Add Query and Mutation fields
4367 Object.entries(data.operations).forEach(([operationId, operation]) => {
4368 translationLog$4(`Process operation '${operation.operationString}'...`);
4369 const field = getFieldForOperation(operation, options.baseUrl, data, requestOptions, connectOptions, includeHttpDetails, pubsub);
4370 const saneOperationId = sanitize(operationId, CaseStyle.camelCase);
4371 // Check if the operation should be added as a Query or Mutation
4372 if (operation.operationType === GraphQLOperationType.Query) {
4373 let fieldName = !singularNames
4374 ? uncapitalize(operation.responseDefinition.graphQLTypeName)
4375 : sanitize(inferResourceNameFromPath(operation.path), CaseStyle.camelCase);
4376 if (operation.inViewer) {
4377 for (const securityRequirement of operation.securityRequirements) {
4378 if (typeof authQueryFields[securityRequirement] !== 'object') {
4379 authQueryFields[securityRequirement] = {};
4380 }
4381 // Avoid overwriting fields that return the same data:
4382 if (fieldName in authQueryFields[securityRequirement] ||
4383 /**
4384 * If the option is set operationIdFieldNames, the fieldName is
4385 * forced to be the operationId
4386 */
4387 operationIdFieldNames) {
4388 fieldName = storeSaneName(saneOperationId, operationId, data.saneMap);
4389 }
4390 if (fieldName in authQueryFields[securityRequirement]) {
4391 handleWarning({
4392 mitigationType: MitigationTypes.DUPLICATE_FIELD_NAME,
4393 message: `Multiple operations have the same name ` +
4394 `'${fieldName}' and security requirement ` +
4395 `'${securityRequirement}'. GraphQL field names must be ` +
4396 `unique so only one can be added to the authentication ` +
4397 `viewer. Operation '${operation.operationString}' will be ignored.`,
4398 data,
4399 log: translationLog$4,
4400 });
4401 }
4402 else {
4403 authQueryFields[securityRequirement][fieldName] = field;
4404 }
4405 }
4406 }
4407 else {
4408 // Avoid overwriting fields that return the same data:
4409 if (fieldName in queryFields ||
4410 /**
4411 * If the option is set operationIdFieldNames, the fieldName is
4412 * forced to be the operationId
4413 */
4414 operationIdFieldNames) {
4415 fieldName = storeSaneName(saneOperationId, operationId, data.saneMap);
4416 }
4417 if (fieldName in queryFields) {
4418 handleWarning({
4419 mitigationType: MitigationTypes.DUPLICATE_FIELD_NAME,
4420 message: `Multiple operations have the same name ` +
4421 `'${fieldName}'. GraphQL field names must be ` +
4422 `unique so only one can be added to the Query object. ` +
4423 `Operation '${operation.operationString}' will be ignored.`,
4424 data,
4425 log: translationLog$4,
4426 });
4427 }
4428 else {
4429 queryFields[fieldName] = field;
4430 }
4431 }
4432 }
4433 else {
4434 let saneFieldName;
4435 if (!singularNames) {
4436 /**
4437 * Use operationId to avoid problems differentiating operations with the
4438 * same path but differnet methods
4439 */
4440 saneFieldName = storeSaneName(saneOperationId, operationId, data.saneMap);
4441 }
4442 else {
4443 const fieldName = `${operation.method}${inferResourceNameFromPath(operation.path)}`;
4444 saneFieldName = storeSaneName(sanitize(fieldName, CaseStyle.camelCase), fieldName, data.saneMap);
4445 }
4446 if (operation.inViewer) {
4447 for (const securityRequirement of operation.securityRequirements) {
4448 if (typeof authMutationFields[securityRequirement] !== 'object') {
4449 authMutationFields[securityRequirement] = {};
4450 }
4451 if (saneFieldName in authMutationFields[securityRequirement]) {
4452 handleWarning({
4453 mitigationType: MitigationTypes.DUPLICATE_FIELD_NAME,
4454 message: `Multiple operations have the same name ` +
4455 `'${saneFieldName}' and security requirement ` +
4456 `'${securityRequirement}'. GraphQL field names must be ` +
4457 `unique so only one can be added to the authentication ` +
4458 `viewer. Operation '${operation.operationString}' will be ignored.`,
4459 data,
4460 log: translationLog$4,
4461 });
4462 }
4463 else {
4464 authMutationFields[securityRequirement][saneFieldName] = field;
4465 }
4466 }
4467 }
4468 else {
4469 if (saneFieldName in mutationFields) {
4470 handleWarning({
4471 mitigationType: MitigationTypes.DUPLICATE_FIELD_NAME,
4472 message: `Multiple operations have the same name ` +
4473 `'${saneFieldName}'. GraphQL field names must be ` +
4474 `unique so only one can be added to the Mutation object. ` +
4475 `Operation '${operation.operationString}' will be ignored.`,
4476 data,
4477 log: translationLog$4,
4478 });
4479 }
4480 else {
4481 mutationFields[saneFieldName] = field;
4482 }
4483 }
4484 }
4485 });
4486 // Add Subscription fields
4487 Object.entries(data.callbackOperations).forEach(([operationId, operation]) => {
4488 const field = getFieldForOperation(operation, options.baseUrl, data, requestOptions, connectOptions, includeHttpDetails, pubsub);
4489 const saneOperationId = sanitize(operationId, CaseStyle.camelCase);
4490 const saneFieldName = storeSaneName(saneOperationId, operationId, data.saneMap);
4491 if (operation.inViewer) {
4492 for (const securityRequirement of operation.securityRequirements) {
4493 if (typeof authSubscriptionFields[securityRequirement] !== 'object') {
4494 authSubscriptionFields[securityRequirement] = {};
4495 }
4496 if (saneFieldName in authSubscriptionFields[securityRequirement]) {
4497 handleWarning({
4498 mitigationType: MitigationTypes.DUPLICATE_FIELD_NAME,
4499 message: `Multiple operations have the same name ` +
4500 `'${saneFieldName}' and security requirement ` +
4501 `'${securityRequirement}'. GraphQL field names must be ` +
4502 `unique so only one can be added to the authentication ` +
4503 `viewer. Operation '${operation.operationString}' will be ignored.`,
4504 data,
4505 log: translationLog$4,
4506 });
4507 }
4508 else {
4509 authSubscriptionFields[securityRequirement][saneFieldName] = field;
4510 }
4511 }
4512 }
4513 else {
4514 if (saneFieldName in subscriptionFields) {
4515 handleWarning({
4516 mitigationType: MitigationTypes.DUPLICATE_FIELD_NAME,
4517 message: `Multiple operations have the same name ` +
4518 `'${saneFieldName}'. GraphQL field names must be ` +
4519 `unique so only one can be added to the Mutation object. ` +
4520 `Operation '${operation.operationString}' will be ignored.`,
4521 data,
4522 log: translationLog$4,
4523 });
4524 }
4525 else {
4526 subscriptionFields[saneFieldName] = field;
4527 }
4528 }
4529 });
4530 // Sorting fields
4531 queryFields = sortObject(queryFields);
4532 mutationFields = sortObject(mutationFields);
4533 subscriptionFields = sortObject(subscriptionFields);
4534 authQueryFields = sortObject(authQueryFields);
4535 Object.keys(authQueryFields).forEach(key => {
4536 authQueryFields[key] = sortObject(authQueryFields[key]);
4537 });
4538 authMutationFields = sortObject(authMutationFields);
4539 Object.keys(authMutationFields).forEach(key => {
4540 authMutationFields[key] = sortObject(authMutationFields[key]);
4541 });
4542 authSubscriptionFields = sortObject(authSubscriptionFields);
4543 Object.keys(authSubscriptionFields).forEach(key => {
4544 authSubscriptionFields[key] = sortObject(authSubscriptionFields[key]);
4545 });
4546 // Count created Query, Mutation, and Subscription fields
4547 options.report.numQueriesCreated =
4548 Object.keys(queryFields).length +
4549 Object.keys(authQueryFields).reduce((sum, key) => {
4550 return sum + Object.keys(authQueryFields[key]).length;
4551 }, 0);
4552 options.report.numMutationsCreated =
4553 Object.keys(mutationFields).length +
4554 Object.keys(authMutationFields).reduce((sum, key) => {
4555 return sum + Object.keys(authMutationFields[key]).length;
4556 }, 0);
4557 options.report.numSubscriptionsCreated =
4558 Object.keys(subscriptionFields).length +
4559 Object.keys(authSubscriptionFields).reduce((sum, key) => {
4560 return sum + Object.keys(authSubscriptionFields[key]).length;
4561 }, 0);
4562 /**
4563 * Organize authenticated Query, Mutation, and Subscriptions fields into
4564 * viewer objects.
4565 */
4566 if (Object.keys(authQueryFields).length > 0) {
4567 Object.assign(queryFields, createAndLoadViewer(authQueryFields, GraphQLOperationType.Query, data, includeHttpDetails));
4568 }
4569 if (Object.keys(authMutationFields).length > 0) {
4570 Object.assign(mutationFields, createAndLoadViewer(authMutationFields, GraphQLOperationType.Mutation, data, includeHttpDetails));
4571 }
4572 if (Object.keys(authSubscriptionFields).length > 0) {
4573 Object.assign(subscriptionFields, createAndLoadViewer(authSubscriptionFields, GraphQLOperationType.Subscription, data, includeHttpDetails));
4574 }
4575 // Build up the schema
4576 const schemaConfig = {
4577 query: Object.keys(queryFields).length > 0
4578 ? new graphql.GraphQLObjectType({
4579 name: 'Query',
4580 fields: queryFields,
4581 })
4582 : getEmptyObjectType('Query'),
4583 mutation: Object.keys(mutationFields).length > 0
4584 ? new graphql.GraphQLObjectType({
4585 name: 'Mutation',
4586 fields: mutationFields,
4587 })
4588 : null,
4589 subscription: Object.keys(subscriptionFields).length > 0
4590 ? new graphql.GraphQLObjectType({
4591 name: 'Subscription',
4592 fields: subscriptionFields,
4593 })
4594 : null,
4595 };
4596 /**
4597 * Fill in yet undefined object types to avoid GraphQLSchema from breaking.
4598 *
4599 * The reason: once creating the schema, the 'fields' thunks will resolve and
4600 * if a field references an undefined object type, GraphQL will throw.
4601 */
4602 Object.entries(data.operations).forEach(([, operation]) => {
4603 if (typeof operation.responseDefinition.graphQLType === 'undefined') {
4604 operation.responseDefinition.graphQLType = getEmptyObjectType(operation.responseDefinition.graphQLTypeName);
4605 }
4606 });
4607 const schema = new graphql.GraphQLSchema(schemaConfig);
4608 return { schema, report: options.report };
4609}
4610/**
4611 * Creates the field object for the given operation.
4612 */
4613function getFieldForOperation(operation, baseUrl, data, requestOptions, connectOptions, includeHttpDetails, pubsub) {
4614 // Create GraphQL Type for response:
4615 const type = getGraphQLType({
4616 def: operation.responseDefinition,
4617 data,
4618 operation,
4619 includeHttpDetails,
4620 });
4621 const payloadSchemaName = operation.payloadDefinition ? operation.payloadDefinition.graphQLInputObjectTypeName : null;
4622 const args = getArgs({
4623 /**
4624 * Even though these arguments seems redundent because of the operation
4625 * argument, the function cannot be refactored because it is also used to
4626 * create arguments for links. The operation argument is really used to pass
4627 * data to other functions.
4628 */
4629 requestPayloadDef: operation.payloadDefinition,
4630 parameters: operation.parameters,
4631 operation,
4632 data,
4633 includeHttpDetails,
4634 });
4635 // Get resolver and subscribe function for Subscription fields
4636 if (operation.operationType === GraphQLOperationType.Subscription) {
4637 const responseSchemaName = operation.responseDefinition ? operation.responseDefinition.graphQLTypeName : null;
4638 const resolve = getPublishResolver({
4639 operation,
4640 responseName: responseSchemaName,
4641 data,
4642 });
4643 const subscribe = getSubscribe({
4644 operation,
4645 payloadName: payloadSchemaName,
4646 data,
4647 baseUrl,
4648 connectOptions,
4649 pubsub,
4650 });
4651 return {
4652 type,
4653 resolve,
4654 subscribe,
4655 args,
4656 description: operation.description,
4657 };
4658 // Get resolver for Query and Mutation fields
4659 }
4660 else {
4661 const resolve = getResolver(() => ({
4662 operation,
4663 payloadName: payloadSchemaName,
4664 data,
4665 baseUrl,
4666 requestOptions,
4667 }));
4668 return {
4669 type,
4670 resolve,
4671 args,
4672 description: operation.description,
4673 };
4674 }
4675}
4676/**
4677 * Ensure that the customResolvers/customSubscriptionResolvers object is a
4678 * triply nested object using the name of the OAS, the path, and the method
4679 * as keys.
4680 */
4681function checkCustomResolversStructure(customResolvers, data) {
4682 if (typeof customResolvers === 'object') {
4683 // Check that all OASs that are referenced in the customResolvers are provided
4684 Object.keys(customResolvers)
4685 .filter(title => {
4686 // If no OAS contains this title
4687 return !data.oass.some(oas => {
4688 return title === oas.info.title;
4689 });
4690 })
4691 .forEach(title => {
4692 handleWarning({
4693 mitigationType: MitigationTypes.CUSTOM_RESOLVER_UNKNOWN_OAS,
4694 message: `Custom resolvers reference OAS '${title.toString()}' but no such ` + `OAS was provided`,
4695 data,
4696 log: translationLog$4,
4697 });
4698 });
4699 // TODO: Only run the following test on OASs that exist. See previous check.
4700 Object.keys(customResolvers).forEach(title => {
4701 // Get all operations from a particular OAS
4702 const operations = Object.values(data.operations).filter(operation => {
4703 return title === operation.oas.info.title;
4704 });
4705 Object.keys(customResolvers[title]).forEach(path => {
4706 Object.keys(customResolvers[title][path]).forEach(method => {
4707 if (!operations.some(operation => {
4708 return path === operation.path && method === operation.method;
4709 })) {
4710 handleWarning({
4711 mitigationType: MitigationTypes.CUSTOM_RESOLVER_UNKNOWN_PATH_METHOD,
4712 message: `A custom resolver references an operation with ` +
4713 `path '${path.toString()}' and method '${method.toString()}' but no such operation ` +
4714 `exists in OAS '${title.toString()}'`,
4715 data,
4716 log: translationLog$4,
4717 });
4718 }
4719 });
4720 });
4721 });
4722 }
4723}
4724/**
4725 * Ensures that the options are valid
4726 */
4727function preliminaryChecks(options, data) {
4728 // Check if OASs have unique titles
4729 const titles = data.oass.map(oas => {
4730 return oas.info.title;
4731 });
4732 // Find duplicates among titles
4733 new Set(titles.filter((title, index) => {
4734 return titles.indexOf(title) !== index;
4735 })).forEach(title => {
4736 handleWarning({
4737 mitigationType: MitigationTypes.MULTIPLE_OAS_SAME_TITLE,
4738 message: `Multiple OAS share the same title '${title}'`,
4739 data,
4740 log: translationLog$4,
4741 });
4742 });
4743 // Check customResolvers
4744 checkCustomResolversStructure(options.customResolvers, data);
4745 // Check customSubscriptionResolvers
4746 checkCustomResolversStructure(options.customSubscriptionResolvers, data);
4747}
4748
4749class OpenAPIHandler {
4750 constructor({ config, cache, pubsub }) {
4751 this.config = config;
4752 this.cache = cache;
4753 this.pubsub = pubsub;
4754 }
4755 async getMeshSource() {
4756 var _a, _b, _c;
4757 const path = this.config.source;
4758 const spec = await utils.readFileOrUrlWithCache(path, this.cache, {
4759 headers: this.config.schemaHeaders,
4760 });
4761 let fetch;
4762 if (this.config.customFetch) {
4763 fetch = await utils.loadFromModuleExportExpression(this.config.customFetch, 'default');
4764 }
4765 else {
4766 fetch = (...args) => fetchache.fetchache(args[0] instanceof fetchache.Request ? args[0] : new fetchache.Request(...args), this.cache);
4767 }
4768 const headersFactory = utils.getInterpolatedHeadersFactory(this.config.operationHeaders);
4769 const queryStringFactoryMap = new Map();
4770 for (const queryName in this.config.qs || {}) {
4771 queryStringFactoryMap.set(queryName, utils.getInterpolatedStringFactory(this.config.qs[queryName]));
4772 }
4773 const searchParamsFactory = (resolverData, searchParams) => {
4774 for (const queryName in this.config.qs || {}) {
4775 searchParams.set(queryName, queryStringFactoryMap.get(queryName)(resolverData));
4776 }
4777 return searchParams;
4778 };
4779 const { schema } = await createGraphQLSchema(spec, {
4780 fetch,
4781 baseUrl: this.config.baseUrl,
4782 operationIdFieldNames: true,
4783 fillEmptyResponses: true,
4784 includeHttpDetails: this.config.includeHttpDetails,
4785 addLimitArgument: true,
4786 sendOAuthTokenInQuery: true,
4787 viewer: false,
4788 equivalentToMessages: true,
4789 createSubscriptionsFromCallbacks: true,
4790 pubsub: this.pubsub,
4791 resolverMiddleware: (getResolverParams, originalFactory) => (root, args, context, info) => {
4792 const resolverData = { root, args, context, info };
4793 const resolverParams = getResolverParams();
4794 resolverParams.requestOptions = {
4795 headers: utils.getHeadersObject(headersFactory(resolverData)),
4796 };
4797 if (context === null || context === void 0 ? void 0 : context.baseUrl) {
4798 resolverParams.baseUrl = context.baseUrl;
4799 }
4800 if (resolverParams.baseUrl) {
4801 const urlObj = new URL(resolverParams.baseUrl);
4802 searchParamsFactory(resolverData, urlObj.searchParams);
4803 }
4804 else {
4805 console.warn(`There is no 'baseUrl' defined for this OpenAPI definition. We recommend you to define one manually!`);
4806 }
4807 if (context === null || context === void 0 ? void 0 : context.fetch) {
4808 resolverParams.fetch = context.fetch;
4809 }
4810 if (context === null || context === void 0 ? void 0 : context.qs) {
4811 resolverParams.qs = context.qs;
4812 }
4813 return originalFactory(() => resolverParams)(root, args, context, info);
4814 },
4815 });
4816 const { args, contextVariables } = utils.parseInterpolationStrings(Object.values(this.config.operationHeaders || {}));
4817 const rootFields = [
4818 ...Object.values(((_a = schema.getQueryType()) === null || _a === void 0 ? void 0 : _a.getFields()) || {}),
4819 ...Object.values(((_b = schema.getMutationType()) === null || _b === void 0 ? void 0 : _b.getFields()) || {}),
4820 ...Object.values(((_c = schema.getSubscriptionType()) === null || _c === void 0 ? void 0 : _c.getFields()) || {}),
4821 ];
4822 for (const rootField of rootFields) {
4823 for (const argName in args) {
4824 const argConfig = args[argName];
4825 rootField.args.push({
4826 name: argName,
4827 description: undefined,
4828 defaultValue: undefined,
4829 extensions: undefined,
4830 astNode: undefined,
4831 ...argConfig,
4832 });
4833 }
4834 }
4835 contextVariables.push('fetch', 'baseUrl');
4836 return {
4837 schema,
4838 contextVariables,
4839 };
4840 }
4841}
4842
4843module.exports = OpenAPIHandler;
4844//# sourceMappingURL=index.cjs.js.map