UNPKG

@omnigraph/json-schema

Version:

This package generates GraphQL Schema from JSON Schema and sample JSON request and responses. You can define your root field endpoints like below in your GraphQL Config for example;

1,015 lines (1,006 loc) • 115 kB
import { DefaultLogger, readFileOrUrl, defaultImportFn, sanitizeNameForGraphQL, getHeadersObj } from '@graphql-mesh/utils'; import { getInterpolationKeys, getInterpolatedHeadersFactory, stringInterpolator } from '@graphql-mesh/string-interpolation'; import { AnySchema, dereferenceObject, healJSONSchema, resolvePath, visitJSONSchema, referenceJSONSchema } from 'json-machete'; import toJsonSchema from 'to-json-schema'; import { process as process$1, util } from '@graphql-mesh/cross-helpers'; import { isListType, isNonNullType, isInputObjectType, GraphQLObjectType, GraphQLString, GraphQLInt, GraphQLError, getNamedType, isScalarType, isUnionType, GraphQLScalarType, Kind, GraphQLFloat, GraphQLBoolean, specifiedDirectives } from 'graphql'; import { GraphQLJSON, isSomeInputTypeComposer, ListComposer, SchemaComposer, ScalarTypeComposer, EnumTypeComposer, UnionTypeComposer, InterfaceTypeComposer, ObjectTypeComposer } from 'graphql-compose'; import { asArray, memoize1, inspect, memoize2 } from '@graphql-tools/utils'; import urlJoin from 'url-join'; import { stringify, parse } from 'qs'; import lodashSet from 'lodash.set'; import { FormData, File, Blob, fetch } from '@whatwg-node/fetch'; import { GraphQLJSON as GraphQLJSON$1, RegularExpression, GraphQLBigInt, GraphQLTimestamp, GraphQLUUID, GraphQLURL, GraphQLIPv6, GraphQLIPv4, GraphQLEmailAddress, GraphQLTime, GraphQLDateTime, GraphQLByte, GraphQLNonNegativeInt, GraphQLNonNegativeFloat, GraphQLPositiveInt, GraphQLPositiveFloat, GraphQLNonPositiveInt, GraphQLNonPositiveFloat, GraphQLNegativeInt, GraphQLNegativeFloat, GraphQLNonEmptyString } from 'graphql-scalars'; import Ajv from 'ajv'; import addFormats from 'ajv-formats'; import { pascalCase } from 'pascal-case'; function isPubSubOperationConfig(operationConfig) { return 'pubsubTopic' in operationConfig; } function getOperationMetadata(operationConfig) { let httpMethod; let operationType; let rootTypeName; if (isPubSubOperationConfig(operationConfig)) { httpMethod = null; operationType = 'subscription'; rootTypeName = 'Subscription'; } else { httpMethod = operationConfig.method; // Fix compability with Mesh handler operationType = operationConfig.type.toLowerCase(); if (!httpMethod) { if (operationType === 'mutation') { httpMethod = 'POST'; } else { httpMethod = 'GET'; } } if (!rootTypeName) { if (httpMethod === 'GET') { rootTypeName = 'Query'; } } rootTypeName = operationType === 'query' ? 'Query' : 'Mutation'; } return { httpMethod, operationType, rootTypeName, fieldName: operationConfig.field, }; } function isFileUpload(obj) { return typeof obj.createReadStream === 'function'; } async function handleOperationResponseConfig(operationResponseConfig, { schemaHeaders, cwd, fetchFn, logger = new DefaultLogger('handleOperationResponseConfig'), }) { if (operationResponseConfig.responseSchema) { const schema = typeof operationResponseConfig.responseSchema === 'string' ? { $ref: operationResponseConfig.responseSchema, title: operationResponseConfig.responseTypeName, } : operationResponseConfig.responseSchema; if (operationResponseConfig.responseSample) { schema.examples = schema.examples || [operationResponseConfig.responseSample]; } return schema; } else if (operationResponseConfig.responseSample) { const sample = typeof operationResponseConfig.responseSample === 'object' ? operationResponseConfig.responseSample : await readFileOrUrl(operationResponseConfig.responseSample, { cwd, fetch: fetchFn, logger, importFn: defaultImportFn, headers: schemaHeaders, }).catch((e) => { throw new Error(`responseSample - ${e.message}`); }); const generatedSchema = toJsonSchema(sample, { required: false, objects: { additionalProperties: false, }, strings: { detectFormat: true, }, arrays: { mode: 'first', }, }); generatedSchema.title = operationResponseConfig.responseTypeName; generatedSchema.examples = [sample]; return generatedSchema; } else { return AnySchema; } } async function getReferencedJSONSchemaFromOperations({ operations, cwd, schemaHeaders, ignoreErrorResponses, logger = new DefaultLogger('getReferencedJSONSchemaFromOperations'), fetchFn, baseUrl, operationHeaders, queryParams, }) { const finalJsonSchema = { type: 'object', title: '_schema', properties: {}, required: ['query'], }; for (const operationConfig of operations) { const { operationType, rootTypeName, fieldName } = getOperationMetadata(operationConfig); const rootTypeDefinition = (finalJsonSchema.properties[operationType] = finalJsonSchema.properties[operationType] || { type: 'object', title: rootTypeName, properties: {}, readOnly: true, }); rootTypeDefinition.properties = rootTypeDefinition.properties || {}; const interpolationStrings = [ ...Object.values(operationHeaders || {}), ...Object.values(queryParams || {}).map(val => val.toString()), baseUrl, ]; if ('pubsubTopic' in operationConfig) { interpolationStrings.push(operationConfig.pubsubTopic); } if ('headers' in operationConfig) { interpolationStrings.push(...Object.values(operationConfig.headers || {})); } if ('path' in operationConfig) { interpolationStrings.push(operationConfig.path); } if ('responseByStatusCode' in operationConfig) { rootTypeDefinition.properties[fieldName] = rootTypeDefinition.properties[fieldName] || {}; const statusCodeOneOfIndexMap = {}; const responseSchemas = []; for (const statusCode in operationConfig.responseByStatusCode) { if (ignoreErrorResponses && !statusCode.startsWith('2')) { continue; } const responseOperationConfig = operationConfig.responseByStatusCode[statusCode]; const responseOperationSchema = await handleOperationResponseConfig(responseOperationConfig, { cwd, schemaHeaders, fetchFn, logger, }); statusCodeOneOfIndexMap[statusCode] = responseSchemas.length; responseOperationSchema.title = responseOperationSchema.title || `${fieldName}_${statusCode}_response`; responseSchemas.push(responseOperationSchema); } if (responseSchemas.length === 1) { rootTypeDefinition.properties[fieldName] = responseSchemas[0]; } else if (responseSchemas.length === 0) { rootTypeDefinition.properties[fieldName] = AnySchema; } else { rootTypeDefinition.properties[fieldName] = { $comment: `statusCodeOneOfIndexMap:${JSON.stringify(statusCodeOneOfIndexMap)}`, title: fieldName + '_response', oneOf: responseSchemas, }; } } else { rootTypeDefinition.properties[fieldName] = await handleOperationResponseConfig(operationConfig, { cwd, schemaHeaders, fetchFn, logger, }); } const rootTypeInputPropertyName = operationType + 'Input'; const rootInputTypeName = rootTypeName + 'Input'; const rootTypeInputTypeDefinition = (finalJsonSchema.properties[rootTypeInputPropertyName] = finalJsonSchema .properties[rootTypeInputPropertyName] || { type: 'object', title: rootInputTypeName, properties: {}, writeOnly: true, }); const interpolationKeys = getInterpolationKeys(...interpolationStrings); if ('queryParamArgMap' in operationConfig) { interpolationKeys.push(...Object.values(operationConfig.queryParamArgMap).map(key => `args.${key}`)); } for (const interpolationKey of interpolationKeys) { const interpolationKeyParts = interpolationKey.split('.'); const initialObjectName = interpolationKeyParts.shift(); if (initialObjectName === 'args') { rootTypeInputTypeDefinition.properties[fieldName] = rootTypeInputTypeDefinition.properties[fieldName] || { title: `${rootTypeInputPropertyName}_${fieldName}`, type: 'object', properties: {}, }; const varName = interpolationKeyParts.shift(); if (operationConfig.argTypeMap != null && varName in operationConfig.argTypeMap) { const argTypeDef = operationConfig.argTypeMap[varName]; if (typeof argTypeDef === 'object') { rootTypeInputTypeDefinition.properties[fieldName].properties[varName] = argTypeDef; } else { rootTypeInputTypeDefinition.properties[fieldName].properties[varName] = { $ref: argTypeDef, }; } } else if (!rootTypeInputTypeDefinition.properties[fieldName].properties[varName]) { rootTypeInputTypeDefinition.properties[fieldName].properties[varName] = { type: 'string', }; } } } if ('binary' in operationConfig) { const generatedSchema = { type: 'string', format: 'binary', }; rootTypeInputTypeDefinition.properties[fieldName] = rootTypeInputTypeDefinition.properties[fieldName] || { title: `${rootTypeInputPropertyName}_${fieldName}`, type: 'object', properties: {}, }; rootTypeInputTypeDefinition.properties[fieldName].properties.input = generatedSchema; } else if ('requestSchema' in operationConfig && operationConfig.requestSchema) { rootTypeInputTypeDefinition.properties[fieldName] = rootTypeInputTypeDefinition.properties[fieldName] || { title: `${rootTypeInputPropertyName}_${fieldName}`, type: 'object', properties: {}, }; rootTypeInputTypeDefinition.properties[fieldName].properties.input = typeof operationConfig.requestSchema === 'string' ? { $ref: operationConfig.requestSchema, title: operationConfig.requestTypeName, } : operationConfig.requestSchema; if (operationConfig.requestSample) { rootTypeInputTypeDefinition.properties[fieldName].properties.input.examples = rootTypeInputTypeDefinition .properties[fieldName].properties.input.examples || [operationConfig.requestSample]; } } else if ('requestSample' in operationConfig) { const sample = typeof operationConfig.requestSample === 'object' ? operationConfig.requestSample : await readFileOrUrl(operationConfig.requestSample, { cwd, headers: schemaHeaders, fetch: fetchFn, logger, importFn: defaultImportFn, }).catch((e) => { throw new Error(`${operationConfig.field}.requestSample: ${operationConfig.requestSample}; ${e.message}`); }); const generatedSchema = toJsonSchema(sample, { required: false, objects: { additionalProperties: false, }, strings: { detectFormat: true, }, arrays: { mode: 'first', }, }); generatedSchema.title = operationConfig.requestTypeName; generatedSchema.examples = [sample]; rootTypeInputTypeDefinition.properties[fieldName] = rootTypeInputTypeDefinition.properties[fieldName] || { title: `${rootTypeInputPropertyName}_${fieldName}`, type: 'object', properties: {}, }; rootTypeInputTypeDefinition.properties[fieldName].properties.input = generatedSchema; } } return finalJsonSchema; } async function getDereferencedJSONSchemaFromOperations({ operations, cwd = process$1.cwd(), logger, fetchFn, schemaHeaders, ignoreErrorResponses, baseUrl, operationHeaders, queryParams, }) { const referencedJSONSchema = await getReferencedJSONSchemaFromOperations({ operations, cwd, schemaHeaders, ignoreErrorResponses, fetchFn, baseUrl, operationHeaders, queryParams, }); logger.debug(`Dereferencing JSON Schema to resolve all $refs`); const schemaHeadersFactory = getInterpolatedHeadersFactory(schemaHeaders); const fullyDeferencedSchema = await dereferenceObject(referencedJSONSchema, { cwd, fetchFn, logger: logger.child('dereferenceObject'), headers: schemaHeadersFactory({ env: process$1.env }), }); logger.debug(`Healing JSON Schema`); const healedSchema = await healJSONSchema(fullyDeferencedSchema, { logger: logger.child('healJSONSchema'), }); return healedSchema; } function resolveDataByUnionInputType(data, type, schemaComposer) { var _a; if (data) { if (isListType(type)) { return asArray(data).map(elem => resolveDataByUnionInputType(elem, type.ofType, schemaComposer)); } if (isNonNullType(type)) { return resolveDataByUnionInputType(data, type.ofType, schemaComposer); } if (isInputObjectType(type)) { const fieldMap = type.getFields(); const isOneOf = schemaComposer.getAnyTC(type).getDirectiveByName('oneOf'); data = asArray(data)[0]; for (const propertyName in data) { const fieldName = sanitizeNameForGraphQL(propertyName); const field = fieldMap[fieldName]; if (field) { if (isOneOf) { const resolvedData = resolveDataByUnionInputType(data[fieldName], field.type, schemaComposer); return resolvedData; } const fieldData = data[fieldName]; data[fieldName] = undefined; const realFieldName = ((_a = field.extensions) === null || _a === void 0 ? void 0 : _a.propertyName) || fieldName; data[realFieldName] = resolveDataByUnionInputType(fieldData, field.type, schemaComposer); } } } } return data; } const defaultQsOptions = { indices: false, }; const isListTypeOrNonNullListType = memoize1(function isListTypeOrNonNullListType(type) { if (isNonNullType(type)) { return isListType(type.ofType); } return isListType(type); }); function createError(message, extensions) { return new GraphQLError(message, undefined, undefined, undefined, undefined, undefined, extensions); } function linkResolver(linkObjArgs, actualResolver, root, args, context, info) { for (const argKey in linkObjArgs) { const argInterpolation = linkObjArgs[argKey]; const actualValue = typeof argInterpolation === 'string' ? stringInterpolator.parse(argInterpolation, { root, args, context, info, env: process$1.env, }) : argInterpolation; lodashSet(args, argKey, actualValue); } return actualResolver(root, args, context, info); } const responseMetadataType = new GraphQLObjectType({ name: 'ResponseMetadata', fields: { url: { type: GraphQLString }, method: { type: GraphQLString }, status: { type: GraphQLInt }, statusText: { type: GraphQLString }, headers: { type: GraphQLJSON }, body: { type: GraphQLJSON }, }, }); async function addExecutionLogicToComposer(name, { schemaComposer, fetch: globalFetch, logger, operations, operationHeaders, baseUrl, pubsub: globalPubsub, queryParams, queryStringOptions = {}, }) { logger.debug(`Attaching execution logic to the schema`); queryStringOptions = { ...defaultQsOptions, ...queryStringOptions }; const linkResolverMapByField = new Map(); for (const operationConfig of operations) { const { httpMethod, rootTypeName, fieldName } = getOperationMetadata(operationConfig); const operationLogger = logger.child(`${rootTypeName}.${fieldName}`); const rootTypeComposer = schemaComposer[rootTypeName]; const field = rootTypeComposer.getField(fieldName); if (isPubSubOperationConfig(operationConfig)) { field.description = operationConfig.description || `PubSub Topic: ${operationConfig.pubsubTopic}`; field.subscribe = (root, args, context, info) => { const pubsub = (context === null || context === void 0 ? void 0 : context.pubsub) || globalPubsub; if (!pubsub) { return new GraphQLError(`You should have PubSub defined in either the config or the context!`); } const interpolationData = { root, args, context, info, env: process$1.env }; let pubsubTopic = stringInterpolator.parse(operationConfig.pubsubTopic, interpolationData); if (pubsubTopic.startsWith('webhook:')) { const [, expectedMethod, expectedUrl] = pubsubTopic.split(':'); const expectedPath = new URL(expectedUrl, 'http://localhost').pathname; pubsubTopic = `webhook:${expectedMethod}:${expectedPath}`; } operationLogger.debug(`=> Subscribing to pubSubTopic: ${pubsubTopic}`); return pubsub.asyncIterator(pubsubTopic); }; field.resolve = root => { operationLogger.debug('Received ', root, ' from ', operationConfig.pubsubTopic); return root; }; } else if (operationConfig.path) { if (process$1.env.DEBUG === '1' || process$1.env.DEBUG === 'fieldDetails') { field.description = ` >**Method**: \`${operationConfig.method}\` >**Base URL**: \`${baseUrl}\` >**Path**: \`${operationConfig.path}\` ${operationConfig.description || ''} `; } else { field.description = operationConfig.description; } field.resolve = async (root, args, context, info) => { var _a, _b, _c, _d, _e; operationLogger.debug(`=> Resolving`); const interpolationData = { root, args, context, env: process$1.env }; const interpolatedBaseUrl = stringInterpolator.parse(baseUrl, interpolationData); const interpolatedPath = stringInterpolator.parse(operationConfig.path, interpolationData); let fullPath = urlJoin(interpolatedBaseUrl, interpolatedPath); const operationHeadersObj = typeof operationHeaders === 'function' ? await operationHeaders(interpolationData, operationConfig) : operationHeaders; const headers = {}; for (const headerName in operationHeadersObj) { const nonInterpolatedValue = operationHeadersObj[headerName]; const interpolatedValue = stringInterpolator.parse(nonInterpolatedValue, interpolationData); if (interpolatedValue) { headers[headerName.toLowerCase()] = interpolatedValue; } } if (operationConfig === null || operationConfig === void 0 ? void 0 : operationConfig.headers) { for (const headerName in operationConfig.headers) { const nonInterpolatedValue = operationConfig.headers[headerName]; const interpolatedValue = stringInterpolator.parse(nonInterpolatedValue, interpolationData); if (interpolatedValue) { headers[headerName.toLowerCase()] = interpolatedValue; } } } const requestInit = { method: httpMethod, headers, }; // Handle binary data if ('binary' in operationConfig) { const binaryUpload = await args.input; if (isFileUpload(binaryUpload)) { const readable = binaryUpload.createReadStream(); const chunks = []; for await (const chunk of readable) { for (const byte of chunk) { chunks.push(byte); } } requestInit.body = new Uint8Array(chunks); const [, contentType] = Object.entries(headers).find(([key]) => key.toLowerCase() === 'content-type') || []; if (!contentType) { headers['content-type'] = binaryUpload.mimetype; } } requestInit.body = binaryUpload; } else { if (operationConfig.requestBaseBody != null) { args.input = args.input || {}; for (const key in operationConfig.requestBaseBody) { const configValue = operationConfig.requestBaseBody[key]; if (typeof configValue === 'string') { const value = stringInterpolator.parse(configValue, interpolationData); lodashSet(args.input, key, value); } else { args.input[key] = configValue; } } } // Resolve union input const input = (args.input = resolveDataByUnionInputType(args.input, (_c = (_b = (_a = field.args) === null || _a === void 0 ? void 0 : _a.input) === null || _b === void 0 ? void 0 : _b.type) === null || _c === void 0 ? void 0 : _c.getType(), schemaComposer)); if (input != null) { const [, contentType] = Object.entries(headers).find(([key]) => key.toLowerCase() === 'content-type') || []; if (contentType === null || contentType === void 0 ? void 0 : contentType.startsWith('application/x-www-form-urlencoded')) { requestInit.body = stringify(input, queryStringOptions); } else if (contentType === null || contentType === void 0 ? void 0 : contentType.startsWith('multipart/form-data')) { delete headers['content-type']; delete headers['Content-Type']; const formData = new FormData(); for (const key in input) { const inputValue = input[key]; if (inputValue != null) { let formDataValue; if (typeof inputValue === 'object') { if (inputValue instanceof File) { formDataValue = inputValue; } else if (inputValue.name && inputValue instanceof Blob) { formDataValue = new File([inputValue], inputValue.name, { type: inputValue.type }); } else if (inputValue.arrayBuffer) { const arrayBuffer = await inputValue.arrayBuffer(); if (inputValue.name) { formDataValue = new File([arrayBuffer], inputValue.name, { type: inputValue.type }); } else { formDataValue = new Blob([arrayBuffer], { type: inputValue.type }); } } else { formDataValue = JSON.stringify(inputValue); } } else { formDataValue = inputValue.toString(); } formData.append(key, formDataValue); } } requestInit.body = formData; } else { requestInit.body = typeof input === 'object' ? JSON.stringify(input) : input; } } } if (queryParams) { for (const queryParamName in queryParams) { if (args != null && operationConfig.queryParamArgMap != null && queryParamName in operationConfig.queryParamArgMap && operationConfig.queryParamArgMap[queryParamName] in args) { continue; } const interpolatedQueryParam = stringInterpolator.parse(queryParams[queryParamName].toString(), interpolationData); const queryParamsString = stringify({ [queryParamName]: interpolatedQueryParam, }, { ...queryStringOptions, ...(_d = operationConfig.queryStringOptionsByParam) === null || _d === void 0 ? void 0 : _d[queryParamName], }); fullPath += fullPath.includes('?') ? '&' : '?'; fullPath += queryParamsString; } } if (operationConfig.queryParamArgMap) { for (const queryParamName in operationConfig.queryParamArgMap) { const argName = operationConfig.queryParamArgMap[queryParamName]; let argValue = args[argName]; if (argValue != null) { // Somehow it doesn't serialize URLs so we need to do it manually. if (argValue instanceof URL) { argValue = argValue.toString(); } const opts = { ...queryStringOptions, ...(_e = operationConfig.queryStringOptionsByParam) === null || _e === void 0 ? void 0 : _e[queryParamName], }; let queryParamObj = argValue; if (Array.isArray(argValue) || !opts.destructObject) { queryParamObj = { [queryParamName]: argValue, }; } const queryParamsString = stringify(queryParamObj, opts); fullPath += fullPath.includes('?') ? '&' : '?'; fullPath += queryParamsString; } } } operationLogger.debug(`=> Fetching `, fullPath, `=>`, requestInit); const fetch = (context === null || context === void 0 ? void 0 : context.fetch) || globalFetch; if (!fetch) { return createError(`You should have fetch defined in either the config or the context!`, { url: fullPath, method: httpMethod, }); } // Trick to pass `sourceName` to the `fetch` function for tracing const response = await fetch(fullPath, requestInit, context, { ...info, sourceName: name, }); // If return type is a file if (field.type.getTypeName() === 'File') { return response.blob(); } const responseText = await response.text(); operationLogger.debug(`=> Received`, { headers: response.headers, text: responseText, }); let responseJson; try { responseJson = JSON.parse(responseText); } catch (error) { const returnNamedGraphQLType = getNamedType(field.type.getType()); // The result might be defined as scalar if (isScalarType(returnNamedGraphQLType)) { operationLogger.debug(` => Return type is not a JSON so returning ${responseText}`); return responseText; } else if (response.status === 204) { responseJson = {}; } else { logger.debug(`Unexpected response in ${fieldName};\n\t${responseText}`); return createError(`Unexpected response`, { url: fullPath, method: httpMethod, responseText, error, }); } } if (!response.status.toString().startsWith('2')) { const returnNamedGraphQLType = getNamedType(field.type.getType()); if (!isUnionType(returnNamedGraphQLType)) { return createError(`HTTP Error: ${response.status}, Could not invoke operation ${operationConfig.method} ${operationConfig.path}`, { method: httpMethod, url: fullPath, statusCode: response.status, statusText: response.statusText, responseBody: responseJson, }); } } operationLogger.debug(`Returning `, responseJson); // Sometimes API returns an array but the return type is not an array const isListReturnType = isListTypeOrNonNullListType(field.type.getType()); const isArrayResponse = Array.isArray(responseJson); if (isListReturnType && !isArrayResponse) { operationLogger.debug(`Response is not array but return type is list. Normalizing the response`); responseJson = [responseJson]; } if (!isListReturnType && isArrayResponse) { operationLogger.debug(`Response is array but return type is not list. Normalizing the response`); responseJson = responseJson[0]; } const addResponseMetadata = (obj) => { if (typeof obj !== 'object') { return obj; } Object.defineProperties(obj, { $field: { get() { return operationConfig.field; }, }, $url: { get() { return fullPath.split('?')[0]; }, }, $method: { get() { return httpMethod; }, }, $statusCode: { get() { return response.status; }, }, $statusText: { get() { return response.statusText; }, }, $headers: { get() { return requestInit.headers; }, }, $request: { get() { return new Proxy({}, { get(_, requestProp) { switch (requestProp) { case 'query': return parse(fullPath.split('?')[1]); case 'path': return new Proxy(args, { get(_, prop) { var _a; return args[prop] || ((_a = args.input) === null || _a === void 0 ? void 0 : _a[prop]) || (obj === null || obj === void 0 ? void 0 : obj[prop]); }, has(_, prop) { return prop in args || (args.input && prop in args.input) || (obj === null || obj === void 0 ? void 0 : obj[prop]); }, }); case 'header': return getHeadersObj(requestInit.headers); case 'body': return requestInit.body; } }, }); }, }, $response: { get() { return new Proxy({}, { get(_, responseProp) { switch (responseProp) { case 'header': return getHeadersObj(response.headers); case 'body': return obj; case 'query': return parse(fullPath.split('?')[1]); case 'path': return new Proxy(args, { get(_, prop) { var _a; return args[prop] || ((_a = args.input) === null || _a === void 0 ? void 0 : _a[prop]) || (obj === null || obj === void 0 ? void 0 : obj[prop]); }, has(_, prop) { return prop in args || (args.input && prop in args.input) || (obj === null || obj === void 0 ? void 0 : obj[prop]); }, }); } }, }); }, }, }); return obj; }; operationLogger.debug(`Adding response metadata to the response object`); return Array.isArray(responseJson) ? responseJson.map(obj => addResponseMetadata(obj)) : addResponseMetadata(responseJson); }; const handleLinkMap = (linkMap, typeTC) => { for (const linkName in linkMap) { typeTC.addFields({ [linkName]: () => { const linkObj = linkMap[linkName]; let linkResolverFieldMap = linkResolverMapByField.get(operationConfig.field); if (!linkResolverFieldMap) { linkResolverFieldMap = {}; linkResolverMapByField.set(operationConfig.field, linkResolverFieldMap); } let targetField; try { targetField = schemaComposer.Query.getField(linkObj.fieldName); } catch (_a) { try { targetField = schemaComposer.Mutation.getField(linkObj.fieldName); } catch (_b) { } } if (!targetField) { logger.debug(`Field ${linkObj.fieldName} not found in ${name} for link ${linkName}`); } linkResolverFieldMap[linkName] = (root, args, context, info) => linkResolver(linkObj.args, targetField.resolve, root, args, context, info); return { ...targetField, args: linkObj.args ? {} : targetField.args, description: linkObj.description || targetField.description, // Pick the correct link resolver if there are many link for the same return type used by different operations resolve: (root, args, context, info) => { var _a; const linkResolverFieldMapForCurrentField = (_a = linkResolverMapByField.get(root.$field)) !== null && _a !== void 0 ? _a : linkResolverFieldMap; return linkResolverFieldMapForCurrentField[linkName](root, args, context, info); }, }; }, }); } }; if ('links' in operationConfig) { const typeTC = schemaComposer.getOTC(field.type.getTypeName()); handleLinkMap(operationConfig.links, typeTC); } if ('exposeResponseMetadata' in operationConfig && operationConfig.exposeResponseMetadata) { const typeTC = schemaComposer.getOTC(field.type.getTypeName()); typeTC.addFields({ _response: { type: responseMetadataType, resolve: root => ({ url: root.$url, headers: root.$response.header, method: root.$method, status: root.$statusCode, statusText: root.$statusText, body: root.$response.body, }), }, }); } if ('responseByStatusCode' in operationConfig) { const unionOrSingleTC = schemaComposer.getAnyTC(getNamedType(field.type.getType())); const types = 'getTypes' in unionOrSingleTC ? unionOrSingleTC.getTypes() : [unionOrSingleTC]; const statusCodeOneOfIndexMap = unionOrSingleTC.getExtension('statusCodeOneOfIndexMap') || {}; for (const statusCode in operationConfig.responseByStatusCode) { const responseConfig = operationConfig.responseByStatusCode[statusCode]; if (responseConfig.links || responseConfig.exposeResponseMetadata) { const typeTCThunked = types[statusCodeOneOfIndexMap[statusCode] || 0]; const originalName = typeTCThunked.getTypeName(); let typeTC = schemaComposer.getAnyTC(originalName); if (!('addFieldArgs' in typeTC)) { typeTC = schemaComposer.createObjectTC({ name: `${operationConfig.field}_${statusCode}_response`, fields: { [originalName]: { type: typeTC, resolve: root => root, }, }, }); // If it is a scalar or enum type, it cannot be a union type, so we can set it directly types[0] = typeTC; field.type = typeTC; } if (responseConfig.exposeResponseMetadata) { typeTC.addFields({ _response: { type: responseMetadataType, resolve: root => root.$response, }, }); } if (responseConfig.links) { handleLinkMap(responseConfig.links, typeTC); } } } } } } logger.debug(`Building the executable schema.`); return schemaComposer; } function getValidTypeName({ schemaComposer, isInput, subSchema, }) { if (!subSchema.title) { throw new Error('Missing title for schema; ' + inspect(subSchema)); } const sanitizedName = sanitizeNameForGraphQL(isInput ? subSchema.title + '_Input' : subSchema.title); if (schemaComposer.has(sanitizedName)) { let i = 2; while (schemaComposer.has(sanitizedName + i)) { i++; } return sanitizedName + i; } return sanitizedName; } function getStringScalarWithMinMaxLength({ schemaComposer, subSchema, }) { const name = getValidTypeName({ schemaComposer, isInput: false, subSchema, }); function coerceString(value) { if (value != null) { const vStr = value.toString(); if (typeof subSchema.minLength !== 'undefined' && vStr.length < subSchema.minLength) { throw new Error(`${name} cannot be less than ${subSchema.minLength} but given ${vStr}`); } if (typeof subSchema.maxLength !== 'undefined' && vStr.length > subSchema.maxLength) { throw new Error(`${name} cannot be more than ${subSchema.maxLength} but given ${vStr}`); } return vStr; } } return schemaComposer.createScalarTC({ name, description: subSchema.description, serialize: coerceString, parseValue: coerceString, parseLiteral: ast => { if ('value' in ast) { return coerceString(ast.value); } return null; }, extensions: { codegenScalarType: 'string', }, }); } const JSONSchemaStringFormats = [ 'date', 'hostname', 'regex', 'json-pointer', 'relative-json-pointer', 'uri-reference', 'uri-template', ]; function getJSONSchemaStringFormatScalarMap(ajv) { const map = new Map(); for (const format of JSONSchemaStringFormats) { const schema = { type: 'string', format, }; let validate; try { validate = ajv.compile(schema); } catch (e) { validate = (value) => ajv.validate(schema, value); } const coerceString = (value) => { if (validate(value)) { return value; } throw new Error(`Expected ${format} but got: ${value}`); }; const scalar = new GraphQLScalarType({ name: pascalCase(format), description: `Represents ${format} values`, serialize: coerceString, parseValue: coerceString, parseLiteral: ast => { if (ast.kind === Kind.STRING) { return coerceString(ast.value); } throw new Error(`Expected string in ${format} format but got: ${ast.value}`); }, extensions: { codegenScalarType: 'string', }, }); map.set(format, scalar); } return map; } function getTypeResolverFromOutputTCs(ajv, outputTypeComposers, subSchemaAndTypeComposers, statusCodeOneOfIndexMap) { var _a; const statusCodeTypeMap = new Map(); for (const statusCode in statusCodeOneOfIndexMap) { statusCodeTypeMap.set(statusCode.toString(), outputTypeComposers[statusCodeOneOfIndexMap[statusCode]]); } const discriminatorField = (_a = subSchemaAndTypeComposers.discriminator) === null || _a === void 0 ? void 0 : _a.propertyName; return function resolveType(data, context, info) { if (data.__typename) { return data.__typename; } else if (discriminatorField != null && data[discriminatorField]) { return data[discriminatorField]; } if (data.$statusCode && statusCodeOneOfIndexMap) { const type = statusCodeTypeMap.get(data.$statusCode.toString()) || statusCodeTypeMap.get('default'); if (type) { if ('getFields' in type) { return type.getTypeName(); } else { return type.getResolveType()(data, context, info, type.getType()); } } } const validationErrors = {}; const dataKeys = typeof data === 'object' ? Object.keys(data) // Remove metadata fields used to pass data .filter(property => !property.toString().startsWith('$')) : null; const allOutputTypeComposers = outputTypeComposers.flatMap(typeComposer => 'getFields' in typeComposer ? typeComposer : typeComposer.getTypeComposers()); for (const outputTypeComposer of allOutputTypeComposers) { const typeName = outputTypeComposer.getTypeName(); if (dataKeys != null) { const typeFields = outputTypeComposer.getFieldNames(); if (dataKeys.length <= typeFields.length && dataKeys.every(property => typeFields.includes(property.toString()))) { return typeName; } } else { const validateFn = outputTypeComposer.getExtension('validateWithJSONSchema'); if (validateFn) { const isValid = validateFn(data); if (isValid) { return typeName; } validationErrors[typeName] = ajv.errors || validateFn.errors; } } } if (data.$response) { const error = new GraphQLError(`HTTP Error: ${data.$statusCode}`, undefined, undefined, undefined, undefined, undefined, { $url: data.$url, $method: data.$method, $statusCode: data.$statusCode, $request: { query: data.$request.query, header: data.$request.header, }, $response: { header: data.$response.header, body: data.$response.body, }, }); return error; } const error = new GraphQLError(`Received data doesn