UNPKG

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