@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;
592 lines (591 loc) • 21.4 kB
JavaScript
import { dset } from 'dset';
import { DirectiveLocation, GraphQLBoolean, GraphQLDirective, GraphQLEnumType, GraphQLID, GraphQLInt, GraphQLString, isEnumType, isInterfaceType, isListType, isNonNullType, isScalarType, isUnionType, } from 'graphql';
import { resolvers as scalarResolvers } from 'graphql-scalars';
import { stringInterpolator } from '@graphql-mesh/string-interpolation';
import { createGraphQLError, getDirective, getDirectives } from '@graphql-tools/utils';
import { addHTTPRootFieldResolver, } from './addRootFieldResolver.js';
import { getTypeResolverFromOutputTCs } from './getTypeResolverFromOutputTCs.js';
import { ObjMapScalar } from './scalars.js';
export const LengthDirective = new GraphQLDirective({
name: 'length',
locations: [DirectiveLocation.SCALAR],
args: {
min: {
type: GraphQLInt,
},
max: {
type: GraphQLInt,
},
},
});
export function processLengthAnnotations(scalar, { min: minLength, max: maxLength, }) {
function coerceString(value) {
if (value != null) {
const vStr = value.toString();
if (typeof minLength !== 'undefined' && vStr.length < minLength) {
throw new Error(`${scalar.name} cannot be less than ${minLength} but given ${vStr}`);
}
if (typeof maxLength !== 'undefined' && vStr.length > maxLength) {
throw new Error(`${scalar.name} cannot be more than ${maxLength} but given ${vStr}`);
}
return vStr;
}
}
scalar.serialize = coerceString;
scalar.parseValue = coerceString;
scalar.parseLiteral = ast => {
if ('value' in ast) {
return coerceString(ast.value);
}
return null;
};
}
export const DiscriminatorDirective = new GraphQLDirective({
name: 'discriminator',
locations: [DirectiveLocation.INTERFACE, DirectiveLocation.UNION],
args: {
field: {
type: GraphQLString,
},
mapping: {
type: ObjMapScalar,
},
},
});
export function processDiscriminatorAnnotations({ interfaceType, discriminatorFieldName, }) {
interfaceType.resolveType = root => root[discriminatorFieldName];
}
export const ResolveRootDirective = new GraphQLDirective({
name: 'resolveRoot',
locations: [DirectiveLocation.FIELD_DEFINITION],
});
function rootResolver(root) {
return root;
}
export function processResolveRootAnnotations(field) {
field.resolve = rootResolver;
}
export const ResolveRootFieldDirective = new GraphQLDirective({
name: 'resolveRootField',
locations: [
DirectiveLocation.FIELD_DEFINITION,
DirectiveLocation.ARGUMENT_DEFINITION,
DirectiveLocation.INPUT_FIELD_DEFINITION,
],
args: {
field: {
type: GraphQLString,
},
},
});
function isOriginallyListType(type) {
if (isNonNullType(type)) {
return isOriginallyListType(type.ofType);
}
return isListType(type);
}
export function processResolveRootFieldAnnotations(field, propertyName) {
if (!field.resolve || field.resolve.name === 'defaultFieldResolver') {
field.resolve = (root, args, context, info) => {
const actualFieldObj = root[propertyName];
if (actualFieldObj != null) {
const isArray = Array.isArray(actualFieldObj);
const isListType = isOriginallyListType(info.returnType);
if (isListType && !isArray) {
return [actualFieldObj];
}
else if (!isListType && isArray) {
return actualFieldObj[0];
}
}
return actualFieldObj;
};
}
}
export const RegExpDirective = new GraphQLDirective({
name: 'regexp',
locations: [DirectiveLocation.SCALAR],
args: {
pattern: {
type: GraphQLString,
},
},
});
export function processRegExpAnnotations(scalar, pattern) {
function coerceString(value) {
if (value != null) {
const vStr = value.toString();
const regexp = new RegExp(pattern);
if (!regexp.test(vStr)) {
throw new Error(`${scalar.name} must match ${pattern} but given ${vStr}`);
}
return vStr;
}
}
scalar.serialize = coerceString;
scalar.parseValue = coerceString;
scalar.parseLiteral = ast => {
if ('value' in ast) {
return coerceString(ast.value);
}
return null;
};
}
export const PubSubOperationDirective = new GraphQLDirective({
name: 'pubsubOperation',
locations: [DirectiveLocation.FIELD_DEFINITION],
args: {
pubsubTopic: {
type: GraphQLString,
},
},
});
export function processPubSubOperationAnnotations({ field, globalPubsub, pubsubTopic, logger, }) {
field.subscribe = (root, args, context, info) => {
const operationLogger = logger.child(`${info.parentType.name}.${field.name}`);
const pubsub = context?.pubsub || globalPubsub;
if (!pubsub) {
return createGraphQLError(`You should have PubSub defined in either the config or the context!`);
}
const interpolationData = { root, args, context, info, env: process.env };
let interpolatedPubSubTopic = stringInterpolator.parse(pubsubTopic, interpolationData);
if (interpolatedPubSubTopic.startsWith('webhook:')) {
const [, expectedMethod, expectedUrl] = interpolatedPubSubTopic.split(':');
const expectedPath = new URL(expectedUrl, 'http://localhost').pathname;
interpolatedPubSubTopic = `webhook:${expectedMethod}:${expectedPath}`;
}
operationLogger.debug(`=> Subscribing to pubSubTopic: ${interpolatedPubSubTopic}`);
return pubsub.asyncIterator(interpolatedPubSubTopic);
};
field.resolve = (root, args, context, info) => {
const operationLogger = logger.child(`${info.parentType.name}.${field.name}`);
operationLogger.debug('Received ', root, ' from ', pubsubTopic);
return root;
};
}
export const TypeScriptDirective = new GraphQLDirective({
name: 'typescript',
locations: [DirectiveLocation.SCALAR, DirectiveLocation.ENUM],
args: {
type: {
type: GraphQLString,
},
},
});
export function processTypeScriptAnnotations(type, typeDefinition) {
type.extensions = type.extensions || {};
type.extensions.codegenScalarType = typeDefinition;
}
function addExecutionLogicToScalar(nonExecutableScalar, actualScalar) {
Object.defineProperties(nonExecutableScalar, {
serialize: {
value: actualScalar.serialize,
},
parseValue: {
value: actualScalar.parseValue,
},
parseLiteral: {
value: actualScalar.parseLiteral,
},
extensions: {
value: {
...actualScalar.extensions,
...nonExecutableScalar.extensions,
},
},
});
}
export function processScalarType(schema, type) {
if (type.name in scalarResolvers) {
const actualScalar = scalarResolvers[type.name];
addExecutionLogicToScalar(type, actualScalar);
}
if (type.name === 'ObjMap') {
addExecutionLogicToScalar(type, ObjMapScalar);
}
const directiveAnnotations = getDirectives(schema, type);
for (const directiveAnnotation of directiveAnnotations) {
switch (directiveAnnotation.name) {
case 'length':
processLengthAnnotations(type, directiveAnnotation.args);
break;
case 'regexp':
processRegExpAnnotations(type, directiveAnnotation.args.pattern);
break;
case 'typescript':
processTypeScriptAnnotations(type, directiveAnnotation.args.type);
break;
}
}
}
export const HTTPOperationDirective = new GraphQLDirective({
name: 'httpOperation',
locations: [DirectiveLocation.FIELD_DEFINITION],
args: {
path: {
type: GraphQLString,
},
operationSpecificHeaders: {
type: ObjMapScalar,
},
httpMethod: {
type: new GraphQLEnumType({
name: 'HTTPMethod',
values: {
GET: { value: 'GET' },
HEAD: { value: 'HEAD' },
POST: { value: 'POST' },
PUT: { value: 'PUT' },
DELETE: { value: 'DELETE' },
CONNECT: { value: 'CONNECT' },
OPTIONS: { value: 'OPTIONS' },
TRACE: { value: 'TRACE' },
PATCH: { value: 'PATCH' },
},
}),
},
isBinary: {
type: GraphQLBoolean,
},
requestBaseBody: {
type: ObjMapScalar,
},
queryParamArgMap: {
type: ObjMapScalar,
},
queryStringOptionsByParam: {
type: ObjMapScalar,
},
},
});
export const GlobalOptionsDirective = new GraphQLDirective({
name: 'globalOptions',
locations: [DirectiveLocation.OBJECT],
args: {
sourceName: {
type: GraphQLString,
},
endpoint: {
type: GraphQLString,
},
operationHeaders: {
type: ObjMapScalar,
},
queryStringOptions: {
type: ObjMapScalar,
},
queryParams: {
type: ObjMapScalar,
},
},
});
export const ResponseMetadataDirective = new GraphQLDirective({
name: 'responseMetadata',
locations: [DirectiveLocation.FIELD_DEFINITION],
});
export function processResponseMetadataAnnotations(field) {
field.resolve = function responseMetadataResolver(root) {
return {
url: root.$url,
headers: root.$response.header,
method: root.$method,
status: root.$statusCode,
statusText: root.$statusText,
body: root.$response.body,
};
};
}
export const LinkDirective = new GraphQLDirective({
name: 'link',
locations: [DirectiveLocation.FIELD_DEFINITION],
args: {
defaultRootType: {
type: GraphQLString,
},
defaultField: {
type: GraphQLString,
},
},
});
export const LinkResolverDirective = new GraphQLDirective({
name: 'linkResolver',
locations: [DirectiveLocation.FIELD_DEFINITION],
args: {
linkResolverMap: {
type: ObjMapScalar,
},
},
});
function linkResolver({ linkObjArgs, targetTypeName, targetFieldName }, { root, args, context, info, env }) {
for (const argKey in linkObjArgs) {
const argInterpolation = linkObjArgs[argKey];
const actualValue = typeof argInterpolation === 'string'
? stringInterpolator.parse(argInterpolation, {
root,
args,
context,
info,
env,
})
: argInterpolation;
dset(args, argKey, actualValue);
}
const type = info.schema.getType(targetTypeName);
const field = type.getFields()[targetFieldName];
return field.resolve(root, args, context, info);
}
function getLinkResolverMap(schema, field) {
const parentFieldLinkResolverDirectives = getDirective(schema, field, 'linkResolver');
if (parentFieldLinkResolverDirectives?.length) {
const linkResolverMap = parentFieldLinkResolverDirectives[0].linkResolverMap;
if (linkResolverMap) {
return linkResolverMap;
}
}
}
function findLinkResolverMap({ schema, operationType, defaultRootTypeName, defaultFieldName, }) {
const parentType = schema.getRootType(operationType);
const parentField = parentType.getFields()[operationType];
if (parentField) {
const linkResolverMap = getLinkResolverMap(schema, parentField);
if (linkResolverMap) {
return linkResolverMap;
}
}
const defaultRootType = schema.getType(defaultRootTypeName);
if (defaultRootType) {
const defaultField = defaultRootType.getFields()[defaultFieldName];
if (defaultField) {
const linkResolverMap = getLinkResolverMap(schema, defaultField);
if (linkResolverMap) {
return linkResolverMap;
}
}
}
}
export function processLinkFieldAnnotations(field, defaultRootTypeName, defaultFieldName) {
field.resolve = (root, args, context, info) => {
const linkResolverMap = findLinkResolverMap({
schema: info.schema,
defaultRootTypeName,
defaultFieldName,
parentFieldName: root.$field,
operationType: info.operation.operation,
});
const linkResolverOpts = linkResolverMap[field.name];
return linkResolver(linkResolverOpts, { root, args, context, info, env: process.env });
};
}
export const DictionaryDirective = new GraphQLDirective({
name: 'dictionary',
locations: [DirectiveLocation.FIELD_DEFINITION],
});
export function processDictionaryDirective(fieldMap, field) {
field.resolve = root => {
const result = [];
for (const key in root) {
if (key in fieldMap) {
continue;
}
result.push({
key,
value: root[key],
});
}
return result;
};
}
export const FlattenDirective = new GraphQLDirective({
name: 'flatten',
locations: [DirectiveLocation.FIELD_DEFINITION],
});
export function processFlattenAnnotations(field) {
if (!field.resolve || field.resolve.name === 'defaultFieldResolver') {
const fieldName = field.name;
field.resolve = root => {
let result = root[fieldName];
if (!Array.isArray(root)) {
result = [result];
}
return result.flat(Infinity);
};
}
}
export function processDirectives({ schema, globalFetch, logger, pubsub, ...extraGlobalOptions }) {
const nonExecutableObjMapScalar = schema.getType('ObjMap');
if (nonExecutableObjMapScalar && isScalarType(nonExecutableObjMapScalar)) {
addExecutionLogicToScalar(nonExecutableObjMapScalar, ObjMapScalar);
}
let [globalOptions = {}] = (getDirective(schema, schema.getQueryType(), 'globalOptions') ||
[]);
globalOptions = {
...globalOptions,
...extraGlobalOptions,
};
const typeMap = schema.getTypeMap();
for (const typeName in typeMap) {
const type = typeMap[typeName];
const exampleAnnotations = getDirective(schema, type, 'example');
if (exampleAnnotations?.length) {
const examples = [];
for (const exampleAnnotation of exampleAnnotations) {
if (exampleAnnotation?.value) {
examples.push(exampleAnnotation.value);
}
}
type.extensions = type.extensions || {};
type.extensions.examples = examples;
}
if (isScalarType(type)) {
processScalarType(schema, type);
}
if (isInterfaceType(type)) {
const directiveAnnotations = getDirectives(schema, type);
for (const directiveAnnotation of directiveAnnotations) {
switch (directiveAnnotation.name) {
case 'discriminator':
processDiscriminatorAnnotations({
interfaceType: type,
discriminatorFieldName: directiveAnnotation.args.field,
});
break;
}
}
}
if (isUnionType(type)) {
const directiveAnnotations = getDirectives(schema, type);
let statusCodeTypeNameIndexMap;
let discriminatorField;
let discriminatorMapping;
for (const directiveAnnotation of directiveAnnotations) {
switch (directiveAnnotation.name) {
case 'statusCodeTypeName':
statusCodeTypeNameIndexMap = statusCodeTypeNameIndexMap || {};
statusCodeTypeNameIndexMap[directiveAnnotation.args.statusCode] =
directiveAnnotation.args.typeName;
break;
case 'discriminator':
discriminatorField = directiveAnnotation.args.field;
discriminatorMapping = directiveAnnotation.args.mapping;
break;
}
}
type.resolveType = getTypeResolverFromOutputTCs({
possibleTypes: type.getTypes(),
discriminatorField,
discriminatorMapping,
statusCodeTypeNameMap: statusCodeTypeNameIndexMap,
});
}
if (isEnumType(type)) {
const directiveAnnotations = getDirectives(schema, type);
for (const directiveAnnotation of directiveAnnotations) {
switch (directiveAnnotation.name) {
case 'typescript':
processTypeScriptAnnotations(type, directiveAnnotation.args.type);
break;
}
}
const enumValues = type.getValues();
for (const enumValue of enumValues) {
const directiveAnnotations = getDirectives(schema, enumValue);
for (const directiveAnnotation of directiveAnnotations) {
switch (directiveAnnotation.name) {
case 'enum': {
const realValue = JSON.parse(directiveAnnotation.args.value);
enumValue.value = realValue;
type._valueLookup.set(realValue, enumValue);
break;
}
}
}
}
}
if ('getFields' in type) {
const fields = type.getFields();
for (const fieldName in fields) {
const field = fields[fieldName];
const directiveAnnotations = getDirectives(schema, field);
for (const directiveAnnotation of directiveAnnotations) {
switch (directiveAnnotation.name) {
case 'resolveRoot':
processResolveRootAnnotations(field);
break;
case 'resolveRootField':
processResolveRootFieldAnnotations(field, directiveAnnotation.args.field);
break;
case 'flatten':
processFlattenAnnotations(field);
break;
case 'pubsubOperation':
processPubSubOperationAnnotations({
field: field,
pubsubTopic: directiveAnnotation.args.pubsubTopic,
globalPubsub: pubsub,
logger,
});
break;
case 'httpOperation':
addHTTPRootFieldResolver(schema, field, logger, globalFetch, directiveAnnotation.args, globalOptions);
break;
case 'responseMetadata':
processResponseMetadataAnnotations(field);
break;
case 'link':
processLinkFieldAnnotations(field, directiveAnnotation.args.defaultRootType, directiveAnnotation.args.defaultField);
break;
case 'dictionary':
processDictionaryDirective(fields, field);
}
}
}
}
}
return schema;
}
export const StatusCodeTypeNameDirective = new GraphQLDirective({
name: 'statusCodeTypeName',
locations: [DirectiveLocation.UNION],
isRepeatable: true,
args: {
typeName: {
type: GraphQLString,
},
statusCode: {
type: GraphQLID,
},
},
});
export const EnumDirective = new GraphQLDirective({
name: 'enum',
locations: [DirectiveLocation.ENUM_VALUE],
args: {
value: {
type: GraphQLString,
},
},
});
export const OneOfDirective = new GraphQLDirective({
name: 'oneOf',
locations: [
DirectiveLocation.OBJECT,
DirectiveLocation.INTERFACE,
DirectiveLocation.INPUT_OBJECT,
],
});
export const ExampleDirective = new GraphQLDirective({
name: 'example',
locations: [
DirectiveLocation.FIELD_DEFINITION,
DirectiveLocation.OBJECT,
DirectiveLocation.INPUT_OBJECT,
DirectiveLocation.ENUM,
DirectiveLocation.SCALAR,
],
args: {
value: {
type: ObjMapScalar,
},
},
isRepeatable: true,
});