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,155 lines (1,141 loc) • 87.2 kB
import { DefaultLogger, readFileOrUrl, defaultImportFn, sanitizeNameForGraphQL, getHeadersObj } from '@graphql-mesh/utils'; import toJsonSchema from 'to-json-schema'; import { dereferenceObject, healJSONSchema, resolvePath, visitJSONSchema, referenceJSONSchema } from 'json-machete'; import { getInterpolatedHeadersFactory, stringInterpolator, parseInterpolationStrings } from '@graphql-mesh/string-interpolation'; 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, UnionTypeComposer, ObjectTypeComposer } from 'graphql-compose'; import { asArray, memoize1, memoize2 } from '@graphql-tools/utils'; import urlJoin from 'url-join'; import { stringify, parse } from 'qs'; import lodashSet from 'lodash.set'; import { GraphQLJSON as GraphQLJSON$1, RegularExpression, GraphQLURL, GraphQLIPv6, GraphQLIPv4, GraphQLEmailAddress, GraphQLTime, GraphQLDateTime, GraphQLBigInt } from 'graphql-scalars'; import Ajv from 'ajv'; import addFormats from 'ajv-formats'; import { pascalCase } from 'pascal-case'; import { fetch } from '@whatwg-node/fetch'; 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 cleanObject(obj) { if (typeof obj === 'object' && obj != null) { const newObj = Array.isArray(obj) ? [] : {}; for (const key in obj) { const newObjForKey = cleanObject(obj[key]); if (newObjForKey != null) { newObj[key] = newObjForKey; } } return newObj; } return obj; } function isFileUpload(obj) { return typeof obj.createReadStream === 'function'; } const anySchema = { title: 'Any', anyOf: [ { type: 'object', additionalProperties: true, }, { type: 'string', }, { type: 'number', }, { type: 'boolean', }, ], }; 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 { const generatedSchema = operationResponseConfig.responseTypeName ? { ...anySchema, title: operationResponseConfig.responseTypeName, } : anySchema; return generatedSchema; } } async function getReferencedJSONSchemaFromOperations({ operations, cwd, schemaHeaders, ignoreErrorResponses, logger = new DefaultLogger('getReferencedJSONSchemaFromOperations'), fetchFn, }) { 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: {}, }); rootTypeDefinition.properties = rootTypeDefinition.properties || {}; 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: {}, }); if ('binary' in operationConfig) { const generatedSchema = { title: operationConfig.requestTypeName || 'File', type: 'file', }; rootTypeInputTypeDefinition.properties[fieldName] = generatedSchema; } else if ('requestSchema' in operationConfig && operationConfig.requestSchema) { rootTypeInputTypeDefinition.properties[fieldName] = typeof operationConfig.requestSchema === 'string' ? { $ref: operationConfig.requestSchema, title: operationConfig.requestTypeName, } : operationConfig.requestSchema; if (operationConfig.requestSample) { rootTypeInputTypeDefinition.properties[fieldName].examples = rootTypeInputTypeDefinition.properties[fieldName] .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(`requestSample:${operationConfig.requestSample} cannot be read - ${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] = generatedSchema; } } return finalJsonSchema; } async function getDereferencedJSONSchemaFromOperations({ operations, cwd = process$1.cwd(), logger, fetchFn, schemaHeaders, ignoreErrorResponses, noDeduplication = false, }) { const referencedJSONSchema = await getReferencedJSONSchemaFromOperations({ operations, cwd, schemaHeaders, ignoreErrorResponses, fetchFn, }); logger.debug(`Dereferencing JSON Schema to resolve all $refs`); const schemaHeadersFactory = getInterpolatedHeadersFactory(schemaHeaders); const fullyDeferencedSchema = await dereferenceObject(referencedJSONSchema, { cwd, fetchFn, logger, headers: schemaHeadersFactory({ env: process$1.env }), }); logger.debug(`Healing JSON Schema`); const healedSchema = await healJSONSchema(fullyDeferencedSchema, { noDeduplication }); 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 = stringInterpolator.parse(argInterpolation, { root, args, context, info, env: process$1.env, }); lodashSet(args, argKey, actualValue); lodashSet(args, `input.${argKey}`, actualValue); } return actualResolver(root, args, context, info); } const responseMetadataType = new GraphQLObjectType({ name: 'ResponseMetadata', fields: { url: { type: GraphQLString }, method: { type: GraphQLString }, status: { type: GraphQLInt }, statusTest: { type: GraphQLString }, headers: { type: GraphQLJSON }, body: { type: GraphQLJSON }, }, }); async function addExecutionLogicToComposer(schemaComposer, { fetch: globalFetch, logger, operations, operationHeaders, baseUrl, pubsub: globalPubsub, queryParams, queryStringOptions = {}, }) { logger.debug(`Attaching execution logic to the schema`); const qsOptions = { ...defaultQsOptions, ...queryStringOptions }; for (const operationConfig of operations) { const { httpMethod, rootTypeName, fieldName } = getOperationMetadata(operationConfig); const operationLogger = logger.child(`${rootTypeName}.${fieldName}`); const interpolationStrings = [ ...Object.values(operationHeaders || {}), ...Object.values(queryParams || {}), baseUrl, ]; 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 }; const pubsubTopic = stringInterpolator.parse(operationConfig.pubsubTopic, interpolationData); operationLogger.debug(`=> Subscribing to pubSubTopic: ${pubsubTopic}`); return pubsub.asyncIterator(pubsubTopic); }; field.resolve = root => { operationLogger.debug('Received ', root, ' from ', operationConfig.pubsubTopic); return root; }; interpolationStrings.push(operationConfig.pubsubTopic); } else if (operationConfig.path) { if (process$1.env.DEBUG) { field.description = ` ***Original Description***: ${operationConfig.description || '(none)'} ***Method***: ${operationConfig.method} ***Base URL***: ${baseUrl} ***Path***: ${operationConfig.path} `; } else { field.description = operationConfig.description; } field.resolve = async (root, args, context) => { var _a, _b, _c; 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 nonInterpolatedHeaders = { ...operationHeaders, ...operationConfig === null || operationConfig === void 0 ? void 0 : operationConfig.headers, }; const headers = {}; for (const headerName in nonInterpolatedHeaders) { const nonInterpolatedValue = nonInterpolatedHeaders[headerName]; const interpolatedValue = stringInterpolator.parse(nonInterpolatedValue, interpolationData); if (interpolatedValue) { headers[headerName] = interpolatedValue; } } const requestInit = { method: httpMethod, headers, }; if (queryParams) { const interpolatedQueryParams = {}; for (const queryParamName in queryParams) { interpolatedQueryParams[queryParamName] = stringInterpolator.parse(queryParams[queryParamName], interpolationData); } const queryParamsString = stringify(interpolatedQueryParams, qsOptions); fullPath += fullPath.includes('?') ? '&' : '?'; fullPath += queryParamsString; } // 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(cleanObject(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) { switch (httpMethod) { case 'GET': case 'HEAD': case 'CONNECT': case 'OPTIONS': case 'TRACE': { fullPath += fullPath.includes('?') ? '&' : '?'; fullPath += stringify(input, qsOptions); break; } case 'POST': case 'PUT': case 'PATCH': case 'DELETE': { 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, qsOptions); } else { requestInit.body = JSON.stringify(input); } break; } default: return createError(`Unknown HTTP Method: ${httpMethod}`, { url: fullPath, method: httpMethod, }); } } } // Delete unused queryparams const [actualPath, queryString] = fullPath.split('?'); if (queryString) { const queryParams = parse(queryString); const cleanedQueryParams = cleanObject(queryParams); fullPath = actualPath + '?' + stringify(cleanedQueryParams, qsOptions); } 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, }); } const response = await fetch(fullPath, requestInit); // 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 { 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}`, { url: fullPath, method: httpMethod, ...(response.statusText ? { status: response.statusText } : {}), 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) => { return { ...obj, $url: fullPath, $method: httpMethod, $request: { query: { ...obj, ...args, ...args.input, }, path: { ...obj, ...args, }, header: requestInit.headers, }, $response: { url: fullPath, method: httpMethod, status: response.status, statusText: response.statusText, headers: getHeadersObj(response.headers), body: obj, }, }; }; operationLogger.debug(`Adding response metadata to the response object`); return Array.isArray(responseJson) ? responseJson.map(obj => addResponseMetadata(obj)) : addResponseMetadata(responseJson); }; interpolationStrings.push(...Object.values(operationConfig.headers || {})); interpolationStrings.push(operationConfig.path); if ('links' in operationConfig) { for (const linkName in operationConfig.links) { const linkObj = operationConfig.links[linkName]; const typeTC = schemaComposer.getOTC(field.type.getTypeName()); typeTC.addFields({ [linkName]: () => { const targetField = schemaComposer.Query.getField(linkObj.fieldName); return { ...targetField, args: {}, description: linkObj.description || targetField.description, resolve: (root, args, context, info) => linkResolver(linkObj.args, targetField.resolve, root, args, context, info), }; }, }); } } if ('exposeResponseMetadata' in operationConfig && operationConfig.exposeResponseMetadata) { const typeTC = schemaComposer.getOTC(field.type.getTypeName()); typeTC.addFields({ _response: { type: responseMetadataType, resolve: root => root.$response, }, }); } 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 typeTC = schemaComposer.getAnyTC(typeTCThunked.getTypeName()); if ('addFieldArgs' in typeTC) { if (responseConfig.exposeResponseMetadata) { typeTC.addFields({ _response: { type: responseMetadataType, resolve: root => root.$response, }, }); } for (const linkName in responseConfig.links || []) { typeTC.addFields({ [linkName]: () => { const linkObj = responseConfig.links[linkName]; const targetField = schemaComposer.Query.getField(linkObj.fieldName); return { ...targetField, args: {}, description: linkObj.description || targetField.description, resolve: (root, args, context, info) => linkResolver(linkObj.args, targetField.resolve, root, args, context, info), }; }, }); } } } } } } const { args: globalArgs } = parseInterpolationStrings(interpolationStrings, operationConfig.argTypeMap); rootTypeComposer.addFieldArgs(fieldName, globalArgs); } logger.debug(`Building the executable schema.`); return schemaComposer; } function getValidTypeName({ schemaComposer, isInput, 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, statusCodeOneOfIndexMap) { const statusCodeTypeMap = new Map(); for (const statusCode in statusCodeOneOfIndexMap) { statusCodeTypeMap.set(statusCode.toString(), outputTypeComposers[statusCodeOneOfIndexMap[statusCode]]); } return function resolveType(data, context, info) { if (data.__typename) { return data.__typename; } else if (data.resourceType) { return data.resourceType; } if (data.$response && statusCodeOneOfIndexMap) { const responseData = data.$response; const type = statusCodeTypeMap.get(responseData.status.toString()) || statusCodeTypeMap.get('default'); if (type) { if ('getFields' in type) { return type.getTypeName(); } else { return type.getResolveType()(data, context, info, type.getType()); } } } const validationErrors = {}; for (const outputTypeComposer of outputTypeComposers) { const validateFn = outputTypeComposer.getExtension('validateWithJSONSchema'); if (validateFn) { const isValid = validateFn(data); const typeName = outputTypeComposer.getTypeName(); if (isValid) { if ('getFields' in outputTypeComposer) { return outputTypeComposer.getTypeName(); } else { return outputTypeComposer.getResolveType()(data, context, info, outputTypeComposer.getType()); } } validationErrors[typeName] = ajv.errors || validateFn.errors; } } if (data.$response) { const responseData = data.$response; const error = new GraphQLError(`HTTP Error: ${responseData.status}`, undefined, undefined, undefined, undefined, undefined, { ...responseData, responseJson: data, }); console.error(error); return error; } const error = new GraphQLError(`Received data doesn't met the union`, null, null, null, null, null, { validationErrors, }); console.error(error); return error; }; } const ONE_OF_DEFINITION = /* GraphQL */ ` directive @oneOf on INPUT_OBJECT | FIELD_DEFINITION `; function getUnionTypeComposers({ schemaComposer, ajv, typeComposersList, subSchemaAndTypeComposers, }) { if (typeComposersList.length === 1) { return typeComposersList[0]; } const unionInputFields = {}; const outputTypeComposers = []; typeComposersList.forEach(typeComposers => { const { input, output } = typeComposers; if (isSomeInputTypeComposer(output)) { const containerTypeName = `${output.getTypeName()}_container`; outputTypeComposers.push(schemaComposer.getOrCreateOTC(containerTypeName, otc => otc.addFields({ [output.getTypeName()]: { type: output, resolve: root => root, }, }))); } else { outputTypeComposers.push(output); } unionInputFields[input.getTypeName()] = { type: input, }; }); subSchemaAndTypeComposers.input.addFields(unionInputFields); if (!schemaComposer.hasDirective('oneOf')) { schemaComposer.addTypeDefs(ONE_OF_DEFINITION); } const resolveType = getTypeResolverFromOutputTCs(ajv, outputTypeComposers, subSchemaAndTypeComposers.output.getExtension('statusCodeOneOfIndexMap')); subSchemaAndTypeComposers.output.setResolveType(resolveType); for (const outputTypeComposer of outputTypeComposers) { if ('getFields' in outputTypeComposer) { subSchemaAndTypeComposers.output.addType(outputTypeComposer); } else { for (const possibleType of outputTypeComposer.getTypes()) { subSchemaAndTypeComposers.output.addType(possibleType); } } } return { input: subSchemaAndTypeComposers.input, output: subSchemaAndTypeComposers.output, }; } function getGenericJSONScalar({ isInput, subSchema, schemaComposer, validateWithJSONSchema, }) { function coerceGenericJSONScalar(value) { if (!validateWithJSONSchema(value)) { throw new Error(`${util.inspect(value)} is not valid!`); } return value; } const name = getValidTypeName({ schemaComposer, isInput, subSchema, }); return schemaComposer.createScalarTC({ name, description: subSchema.description, serialize: coerceGenericJSONScalar, parseValue: coerceGenericJSONScalar, parseLiteral(...args) { const value = GraphQLJSON.parseLiteral(...args); return coerceGenericJSONScalar(value); }, extensions: { codegenScalarType: 'any', examples: subSchema.examples, default: subSchema.default, }, }); } const ajvMemoizedCompile = memoize2(function ajvCompile(ajv, jsonSchema) { const schema = typeof jsonSchema === 'object' ? { ...jsonSchema, $schema: undefined, } : jsonSchema; try { return ajv.compile(schema); } catch (_a) { // eslint-disable-next-line no-inner-declarations function validateFn(value) { return ajv.validate(schema, value); } Object.defineProperty(validateFn, 'errors', { get() { return ajv.errors; }, }); return validateFn; } }); function getValidateFnForSchemaPath(ajv, path, schema) { const subSchema = resolvePath(path, schema); const fn = function validateFn(data) { const ajvValidateFn = ajvMemoizedCompile(ajv, subSchema); return ajvValidateFn(data); }; Object.defineProperty(fn, 'errors', { get() { return ajvMemoizedCompile(ajv, subSchema).errors; }, }); return fn; } /* eslint-disable no-case-declarations */ const isListTC = memoize1(function isListTC(type) { return type instanceof ListComposer; }); const GraphQLVoid = new GraphQLScalarType({ name: 'Void', description: 'Represents empty values', serialize: () => '', extensions: { codegenScalarType: 'void', }, }); const GraphQLFile = new GraphQLScalarType({ name: 'File', extensions: { codegenScalarType: 'File', }, }); function getComposerFromJSONSchema(schema, logger) { const schemaComposer = new SchemaComposer(); const ajv = new Ajv({ strict: false, }); addFormats(ajv); const formatScalarMap = getJSONSchemaStringFormatScalarMap(ajv); const rootInputTypeNameComposerMap = { QueryInput: () => schemaComposer.Query, MutationInput: () => schemaComposer.Mutation, SubscriptionInput: () => schemaComposer.Subscription, }; return visitJSONSchema(schema, { enter(subSchema, { path, visitedSubschemaResultMap }) { var _a; logger === null || logger === void 0 ? void 0 : logger.debug(`Entering ${path} for GraphQL Schema`); if (typeof subSchema === 'boolean') { const typeComposer = schemaComposer.getAnyTC(GraphQLJSON$1); return subSchema ? { input: typeComposer, output: typeComposer, } : undefined; } const validateWithJSONSchema = getValidateFnForSchemaPath(ajv, path, schema); if (!subSchema) { throw new Error(`Something is wrong with ${path}`); } if (subSchema.pattern) { const scalarType = new RegularExpression(getValidTypeName({ schemaComposer, isInput: false, subSchema, }), new RegExp(subSchema.pattern), { description: subSchema.description, }); const typeComposer = schemaComposer.getAnyTC(scalarType); return { input: typeComposer, output: typeComposer, nullable: subSchema.nullable, }; } if (subSchema.const) { const tsTypeName = JSON.stringify(subSchema.const); const scalarTypeName = getValidTypeName({ schemaComposer, isInput: false, subSchema, }); const scalarType = new RegularExpression(scalarTypeName, new RegExp(subSchema.const), { description: subSchema.description || `A field whose value is ${tsTypeName}`, errorMessage: (_r, v) => `Expected ${tsTypeName} but got ${JSON.stringify(v)}`, }); scalarType.extensions = { codegenScalarType: tsTypeName, }; const typeComposer = schemaComposer.createScalarTC(scalarType); return { input: typeComposer, output: typeComposer, nullable: subSchema.nullable, }; } if (subSchema.enum && subSchema.type !== 'boolean') { const values = {}; for (const value of subSchema.enum) { let enumKey = sanitizeNameForGraphQL(value.toString()); if (enumKey === 'false' || enumKey === 'true' || enumKey === 'null') { enumKey = enumKey.toUpperCase(); } if (typeof enumKey === 'string' && enumKey.length === 0) { enumKey = '_'; } values[enumKey] = { // Falsy values are ignored by GraphQL // eslint-disable-next-line no-unneeded-ternary value: value ? value : value === null || value === void 0 ? void 0 : value.toString(), }; } const typeComposer = schemaComposer.createEnumTC({ name: getValidTypeName({ schemaComposer, isInput: false, subSchema, }), values, description: subSchema.description, extensions: { examples: subSchema.examples, default: subSchema.default, }, }); return { input: typeComposer, output: typeComposer, nullable: subSchema.nullable, }; } if (Array.isArray(subSchema.type)) { const validTypes = subSchema.type.filter((typeName) => typeName !== 'null'); if (validTypes.length === 1) { subSchema.type = validTypes[0]; // continue with the single type } else if (validTypes.length === 0) { const typeComposer = schemaComposer.getAnyTC(GraphQLVoid); return { input: typeComposer, output: typeComposer, nullable: subSchema.nullable, }; } else { const typeComposer = getGenericJSONScalar({ isInput: true, subSchema, schemaComposer, validateWithJSONSchema, }); return { input: typeComposer, output: typeComposer, nullable: subSchema.nullable, }; } } switch (subSchema.type) { case 'file': { const typeComposer = schemaComposer.getAnyTC(GraphQLFile); return { input: typeComposer, output: typeComposer, description: subSchema.description, nullable: subSchema.nullable, }; } case 'boolean': { const typeComposer = schemaComposer.getAnyTC(GraphQLBoolean); return { input: typeComposer, output: typeComposer, description: subSchema.description, nullable: subSchema.nullable, }; } case 'null': { const typeComposer = schemaComposer.getAnyTC(GraphQLVoid); return { input: typeComposer, output: typeComposer, description: subSchema.description, nullable: subSchema.nullable, }; } case 'integer': { if (subSchema.format === 'int64') { const typeComposer = schemaComposer.getAnyTC(GraphQLBigInt); return { input: typeComposer, output: typeComposer, description: subSchema.description, nullable: subSchema.nullable, }; } const typeComposer = schemaComposer.getAnyTC(GraphQLInt); return { input: typeComposer, output: typeComposer, description: subSchema.description, nullable: subSchema.nullable, }; } case 'number': { const typeComposer = schemaComposer.getAnyTC(GraphQLFloat); return { input: typeComposer, output: typeComposer, description: subSchema.description, nullable: subSchema.nullable, }; } case 'string': { if (subSchema.minLength || subSchema.maxLength) { const scalarType = getStringScalarWithMinMaxLength({ schemaComposer, subSchema, }); const typeComposer = schemaComposer.getAnyTC(scalarType); return { input: typeComposer, output: typeComposer, description: subSchema.description, nullable: subSchema.nullable, }; } switch (subSchema.format) { case 'date-time': { const typeComposer = schemaComposer.getAnyTC(GraphQLDateTime); return { input: typeComposer,