diff --git a/packages/batch-delegate/src/getLoader.ts b/packages/batch-delegate/src/getLoader.ts index 03d2d1b2742..80182a41361 100644 --- a/packages/batch-delegate/src/getLoader.ts +++ b/packages/batch-delegate/src/getLoader.ts @@ -9,18 +9,34 @@ import { BatchDelegateOptions } from './types'; const cache1: WeakMap< ReadonlyArray, - WeakMap>> + WeakMap, Record>> > = new WeakMap(); function createBatchFn(options: BatchDelegateOptions) { const argsFromKeys = options.argsFromKeys ?? ((keys: ReadonlyArray) => ({ ids: keys })); + const fieldName = options.fieldName ?? options.info.fieldName; const { valuesFromResults, lazyOptionsFn } = options; return async (keys: ReadonlyArray) => { const results = await delegateToSchema({ returnType: new GraphQLList(getNamedType(options.info.returnType) as GraphQLOutputType), - onLocatedError: originalError => - relocatedError(originalError, originalError.path.slice(0, 0).concat(originalError.path.slice(2))), + onLocatedError: originalError => { + if (originalError.path == null) { + return originalError; + } + + const [pathFieldName, pathNumber] = originalError.path; + + if (pathFieldName !== fieldName) { + throw new Error(`Error path value at index 0 should be '${fieldName}', received '${pathFieldName}'.`); + } + const pathNumberType = typeof pathNumber; + if (pathNumberType !== 'number') { + throw new Error(`Error path value at index 1 should be of type number, received '${pathNumberType}'.`); + } + + return relocatedError(originalError, originalError.path.slice(0, 0).concat(originalError.path.slice(2))); + }, args: argsFromKeys(keys), ...(lazyOptionsFn == null ? options : lazyOptionsFn(options)), }); @@ -35,10 +51,10 @@ function createBatchFn(options: BatchDelegateOptions) { }; } -export function getLoader(options: BatchDelegateOptions): DataLoader { +export function getLoader(options: BatchDelegateOptions): DataLoader { const fieldName = options.fieldName ?? options.info.fieldName; - let cache2: WeakMap>> = cache1.get( + let cache2: WeakMap>> | undefined = cache1.get( options.info.fieldNodes ); @@ -56,7 +72,7 @@ export function getLoader(options: BatchDelegateOptions let loaders = cache2.get(options.schema); if (loaders === undefined) { - loaders = Object.create(null); + loaders = Object.create(null) as Record>; cache2.set(options.schema, loaders); const batchFn = createBatchFn(options); const loader = new DataLoader(keys => batchFn(keys), options.dataLoaderOptions); diff --git a/packages/batch-delegate/tests/basic.example.test.ts b/packages/batch-delegate/tests/basic.example.test.ts index 042a693567a..b05d07039b3 100644 --- a/packages/batch-delegate/tests/basic.example.test.ts +++ b/packages/batch-delegate/tests/basic.example.test.ts @@ -89,7 +89,7 @@ describe('batch delegation within basic stitching example', () => { expect(numCalls).toEqual(1); expect(result.errors).toBeUndefined(); - expect(result.data.trendingChirps[0].chirpedAtUser.email).not.toBe(null); + expect(result.data!.trendingChirps[0].chirpedAtUser.email).not.toBe(null); }); test('works with key arrays', async () => { @@ -108,9 +108,9 @@ describe('batch delegation within basic stitching example', () => { `, resolvers: { Query: { - posts: (obj, args) => { + posts: (_, args) => { numCalls += 1; - return args.ids.map(id => ({ id, title: `Post ${id}` })); + return args.ids.map((id: unknown) => ({ id, title: `Post ${id}` })); } } } @@ -129,8 +129,8 @@ describe('batch delegation within basic stitching example', () => { `, resolvers: { Query: { - users: (obj, args) => { - return args.ids.map(id => { + users: (_, args) => { + return args.ids.map((id: unknown) => { return { id, postIds: [Number(id)+1, Number(id)+2] }; }); } diff --git a/packages/batch-delegate/tests/withTransforms.test.ts b/packages/batch-delegate/tests/withTransforms.test.ts index e204dcf8db6..a325d146996 100644 --- a/packages/batch-delegate/tests/withTransforms.test.ts +++ b/packages/batch-delegate/tests/withTransforms.test.ts @@ -100,7 +100,7 @@ describe('works with complex transforms', () => { context, info, transforms: [queryTransform], - returnType: new GraphQLList(new GraphQLList(info.schema.getType('Book'))) + returnType: new GraphQLList(new GraphQLList(info.schema.getType('Book')!)) }), }, }, diff --git a/packages/batch-execute/src/createBatchingExecutor.ts b/packages/batch-execute/src/createBatchingExecutor.ts index b3ccbe1381c..4a92f5ab6ba 100644 --- a/packages/batch-execute/src/createBatchingExecutor.ts +++ b/packages/batch-execute/src/createBatchingExecutor.ts @@ -12,28 +12,37 @@ import { splitResult } from './splitResult'; export function createBatchingExecutor( executor: Executor, dataLoaderOptions?: DataLoader.Options, - extensionsReducer?: (mergedExtensions: Record, executionParams: ExecutionParams) => Record + extensionsReducer: ( + mergedExtensions: Record, + executionParams: ExecutionParams + ) => Record = defaultExtensionsReducer ): Executor { - const loader = new DataLoader( - createLoadFn(executor, extensionsReducer ?? defaultExtensionsReducer), - dataLoaderOptions - ); + const loader = new DataLoader(createLoadFn(executor, extensionsReducer), dataLoaderOptions); return (executionParams: ExecutionParams) => loader.load(executionParams); } function createLoadFn( - executor: ({ document, context, variables, info }: ExecutionParams) => ExecutionResult | Promise, + executor: Executor, extensionsReducer: (mergedExtensions: Record, executionParams: ExecutionParams) => Record ) { - return async (execs: Array): Promise> => { + return async (execs: ReadonlyArray): Promise> => { const execBatches: Array> = []; let index = 0; const exec = execs[index]; let currentBatch: Array = [exec]; execBatches.push(currentBatch); - const operationType = getOperationAST(exec.document, undefined).operation; + + const operationType = getOperationAST(exec.document, undefined)?.operation; + if (operationType == null) { + throw new Error('Could not identify operation type of document.'); + } + while (++index < execs.length) { - const currentOperationType = getOperationAST(execs[index].document, undefined).operation; + const currentOperationType = getOperationAST(execs[index].document, undefined)?.operation; + if (operationType == null) { + throw new Error('Could not identify operation type of document.'); + } + if (operationType === currentOperationType) { currentBatch.push(execs[index]); } else { @@ -48,13 +57,15 @@ function createLoadFn( executionResults.push(new ValueOrPromise(() => executor(mergedExecutionParams))); }); - return ValueOrPromise.all(executionResults).then(resultBatches => { - let results: Array = []; - resultBatches.forEach((resultBatch, index) => { - results = results.concat(splitResult(resultBatch, execBatches[index].length)); - }); - return results; - }).resolve(); + return ValueOrPromise.all(executionResults) + .then(resultBatches => { + let results: Array = []; + resultBatches.forEach((resultBatch, index) => { + results = [...results, ...splitResult(resultBatch!, execBatches[index].length)]; + }); + return results; + }) + .resolve(); }; } diff --git a/packages/batch-execute/src/getBatchingExecutor.ts b/packages/batch-execute/src/getBatchingExecutor.ts index ba267b0fd0c..8ea01bb7547 100644 --- a/packages/batch-execute/src/getBatchingExecutor.ts +++ b/packages/batch-execute/src/getBatchingExecutor.ts @@ -7,8 +7,10 @@ import { memoize2of4 } from './memoize'; export const getBatchingExecutor = memoize2of4(function ( _context: Record = self ?? window ?? global, executor: Executor, - dataLoaderOptions?: DataLoader.Options, - extensionsReducer?: (mergedExtensions: Record, executionParams: ExecutionParams) => Record + dataLoaderOptions?: DataLoader.Options | undefined, + extensionsReducer?: + | undefined + | ((mergedExtensions: Record, executionParams: ExecutionParams) => Record) ): Executor { return createBatchingExecutor(executor, dataLoaderOptions, extensionsReducer); }); diff --git a/packages/batch-execute/src/mergeExecutionParams.ts b/packages/batch-execute/src/mergeExecutionParams.ts index 61b4184a634..1dd6cbc0375 100644 --- a/packages/batch-execute/src/mergeExecutionParams.ts +++ b/packages/batch-execute/src/mergeExecutionParams.ts @@ -18,7 +18,7 @@ import { OperationTypeNode, } from 'graphql'; -import { ExecutionParams } from '@graphql-tools/utils'; +import { ExecutionParams, Maybe } from '@graphql-tools/utils'; import { createPrefix } from './prefix'; @@ -66,7 +66,7 @@ export function mergeExecutionParams( const mergedFragmentDefinitions: Array = []; let mergedExtensions: Record = Object.create(null); - let operation: OperationTypeNode; + let operation: Maybe; execs.forEach((executionParams, index) => { const prefixedExecutionParams = prefixExecutionParams(createPrefix(index), executionParams); @@ -85,6 +85,10 @@ export function mergeExecutionParams( mergedExtensions = extensionsReducer(mergedExtensions, executionParams); }); + if (operation == null) { + throw new Error('Could not identify operation type. Did the document only include fragment definitions?'); + } + const mergedOperationDefinition: OperationDefinitionNode = { kind: Kind.OPERATION_DEFINITION, operation, @@ -109,7 +113,8 @@ export function mergeExecutionParams( function prefixExecutionParams(prefix: string, executionParams: ExecutionParams): ExecutionParams { let document = aliasTopLevelFields(prefix, executionParams.document); - const variableNames = Object.keys(executionParams.variables); + const executionVariables = executionParams.variables ?? {}; + const variableNames = Object.keys(executionVariables); if (variableNames.length === 0) { return { ...executionParams, document }; @@ -122,7 +127,7 @@ function prefixExecutionParams(prefix: string, executionParams: ExecutionParams) }); const prefixedVariables = variableNames.reduce((acc, name) => { - acc[prefix + name] = executionParams.variables[name]; + acc[prefix + name] = executionVariables[name]; return acc; }, Object.create(null)); @@ -150,9 +155,9 @@ function aliasTopLevelFields(prefix: string, document: DocumentNode): DocumentNo }; }, }; - return visit(document, transformer, ({ + return visit(document, transformer, { [Kind.DOCUMENT]: [`definitions`], - } as unknown) as VisitorKeyMap); + } as unknown as VisitorKeyMap); } /** diff --git a/packages/batch-execute/src/prefix.ts b/packages/batch-execute/src/prefix.ts index 73cb9d52323..4da94fc8e00 100644 --- a/packages/batch-execute/src/prefix.ts +++ b/packages/batch-execute/src/prefix.ts @@ -4,7 +4,7 @@ export function createPrefix(index: number): string { return `graphqlTools${index}_`; } -export function parseKey(prefixedKey: string): { index: number; originalKey: string } { +export function parseKey(prefixedKey: string): null | { index: number; originalKey: string } { const match = /^graphqlTools([\d]+)_(.*)$/.exec(prefixedKey); if (match && match.length === 3 && !isNaN(Number(match[1])) && match[2]) { return { index: Number(match[1]), originalKey: match[2] }; diff --git a/packages/batch-execute/src/splitResult.ts b/packages/batch-execute/src/splitResult.ts index fd2682a8e60..0cee17f31c6 100644 --- a/packages/batch-execute/src/splitResult.ts +++ b/packages/batch-execute/src/splitResult.ts @@ -2,7 +2,7 @@ import { ExecutionResult, GraphQLError } from 'graphql'; -import { relocatedError } from '@graphql-tools/utils'; +import { assertSome, relocatedError } from '@graphql-tools/utils'; import { parseKey } from './prefix'; @@ -18,11 +18,17 @@ export function splitResult(mergedResult: ExecutionResult, numResults: number): const data = mergedResult.data; if (data) { Object.keys(data).forEach(prefixedKey => { - const { index, originalKey } = parseKey(prefixedKey); - if (!splitResults[index].data) { - splitResults[index].data = { [originalKey]: data[prefixedKey] }; + const parsedKey = parseKey(prefixedKey); + assertSome(parsedKey, "'parsedKey' should not be null."); + const { index, originalKey } = parsedKey; + const result = splitResults[index]; + if (result == null) { + return; + } + if (result.data == null) { + result.data = { [originalKey]: data[prefixedKey] }; } else { - splitResults[index].data[originalKey] = data[prefixedKey]; + result.data[originalKey] = data[prefixedKey]; } }); } diff --git a/packages/delegate/src/Subschema.ts b/packages/delegate/src/Subschema.ts index 01754e00e43..bcf53286a8e 100644 --- a/packages/delegate/src/Subschema.ts +++ b/packages/delegate/src/Subschema.ts @@ -15,7 +15,8 @@ interface ISubschema> } export class Subschema> - implements ISubschema { + implements ISubschema +{ public schema: GraphQLSchema; public rootValue?: Record; @@ -25,7 +26,7 @@ export class Subschema> public batchingOptions?: BatchingOptions; public createProxyingResolver?: CreateProxyingResolverFn; - public transforms: Array; + public transforms: Array>; public transformedSchema: GraphQLSchema; public merge?: Record>; diff --git a/packages/delegate/src/Transformer.ts b/packages/delegate/src/Transformer.ts index 59bf49a83ec..d8b0403edb2 100644 --- a/packages/delegate/src/Transformer.ts +++ b/packages/delegate/src/Transformer.ts @@ -9,11 +9,11 @@ interface Transformation { context: Record; } -export class Transformer { +export class Transformer> { private transformations: Array = []; - private delegationContext: DelegationContext; + private delegationContext: DelegationContext; - constructor(context: DelegationContext, binding: DelegationBinding = defaultDelegationBinding) { + constructor(context: DelegationContext, binding: DelegationBinding = defaultDelegationBinding) { this.delegationContext = context; const delegationTransforms: Array = binding(this.delegationContext); delegationTransforms.forEach(transform => this.addTransform(transform, {})); diff --git a/packages/delegate/src/applySchemaTransforms.ts b/packages/delegate/src/applySchemaTransforms.ts index 4f6dbd84713..225cb59920b 100644 --- a/packages/delegate/src/applySchemaTransforms.ts +++ b/packages/delegate/src/applySchemaTransforms.ts @@ -2,11 +2,11 @@ import { GraphQLSchema } from 'graphql'; import { cloneSchema } from '@graphql-tools/utils'; -import { SubschemaConfig, Transform } from './types'; +import { SubschemaConfig } from './types'; export function applySchemaTransforms( originalWrappingSchema: GraphQLSchema, - subschemaConfig: SubschemaConfig, + subschemaConfig: SubschemaConfig, transformedSchema?: GraphQLSchema ): GraphQLSchema { const schemaTransforms = subschemaConfig.transforms; @@ -16,7 +16,7 @@ export function applySchemaTransforms( } return schemaTransforms.reduce( - (schema: GraphQLSchema, transform: Transform) => + (schema: GraphQLSchema, transform) => transform.transformSchema != null ? transform.transformSchema(cloneSchema(schema), subschemaConfig, transformedSchema) : schema, diff --git a/packages/delegate/src/createRequest.ts b/packages/delegate/src/createRequest.ts index 9c2b053cb5d..73f205e5c3c 100644 --- a/packages/delegate/src/createRequest.ts +++ b/packages/delegate/src/createRequest.ts @@ -53,6 +53,10 @@ export function createRequestFromInfo({ }); } +const raiseError = (message: string) => { + throw new Error(message); +}; + export function createRequest({ sourceSchema, sourceParentType, @@ -66,16 +70,16 @@ export function createRequest({ selectionSet, fieldNodes, }: ICreateRequest): Request { - let newSelectionSet: SelectionSetNode; + let newSelectionSet: SelectionSetNode | undefined; let argumentNodeMap: Record; if (selectionSet != null) { newSelectionSet = selectionSet; argumentNodeMap = Object.create(null); } else { - const selections: Array = fieldNodes.reduce( + const selections: Array = (fieldNodes ?? []).reduce( (acc, fieldNode) => (fieldNode.selectionSet != null ? acc.concat(fieldNode.selectionSet.selections) : acc), - [] + [] as Array ); newSelectionSet = selections.length @@ -87,7 +91,7 @@ export function createRequest({ argumentNodeMap = {}; - const args = fieldNodes[0]?.arguments; + const args = fieldNodes?.[0]?.arguments; if (args) { argumentNodeMap = args.reduce( (prev, curr) => ({ @@ -107,14 +111,14 @@ export function createRequest({ const varName = def.variable.name.value; variableDefinitionMap[varName] = def; const varType = typeFromAST(sourceSchema, def.type as NamedTypeNode) as GraphQLInputType; - const serializedValue = serializeInputValue(varType, variableValues[varName]); + const serializedValue = serializeInputValue(varType, variableValues?.[varName]); if (serializedValue !== undefined) { newVariables[varName] = serializedValue; } }); } - if (sourceParentType != null) { + if (sourceParentType != null && sourceFieldName != null) { updateArgumentsWithDefaults( sourceParentType, sourceFieldName, @@ -129,7 +133,10 @@ export function createRequest({ arguments: Object.keys(argumentNodeMap).map(argName => argumentNodeMap[argName]), name: { kind: Kind.NAME, - value: targetFieldName || fieldNodes[0].name.value, + value: + targetFieldName ?? + fieldNodes?.[0]?.name.value ?? + raiseError("Either 'targetFieldName' or a non empty 'fieldNodes' array must be provided."), }, selectionSet: newSelectionSet, }; diff --git a/packages/delegate/src/delegateToSchema.ts b/packages/delegate/src/delegateToSchema.ts index a120ae46db1..079bd44107f 100644 --- a/packages/delegate/src/delegateToSchema.ts +++ b/packages/delegate/src/delegateToSchema.ts @@ -18,9 +18,23 @@ import AggregateError from '@ardatan/aggregate-error'; import { getBatchingExecutor } from '@graphql-tools/batch-execute'; -import { mapAsyncIterator, ExecutionResult, Executor, ExecutionParams, Subscriber } from '@graphql-tools/utils'; +import { + mapAsyncIterator, + ExecutionResult, + Executor, + ExecutionParams, + Subscriber, + Maybe, + assertSome, +} from '@graphql-tools/utils'; -import { IDelegateToSchemaOptions, IDelegateRequestOptions, StitchingInfo, DelegationContext } from './types'; +import { + IDelegateToSchemaOptions, + IDelegateRequestOptions, + StitchingInfo, + DelegationContext, + SubschemaConfig, +} from './types'; import { isSubschemaConfig } from './subschemaConfig'; import { Subschema } from './Subschema'; @@ -64,7 +78,7 @@ function getDelegationReturnType( operation: OperationTypeNode, fieldName: string ): GraphQLOutputType { - let rootType: GraphQLObjectType; + let rootType: Maybe>; if (operation === 'query') { rootType = targetSchema.getQueryType(); } else if (operation === 'mutation') { @@ -72,14 +86,17 @@ function getDelegationReturnType( } else { rootType = targetSchema.getSubscriptionType(); } + assertSome(rootType); return rootType.getFields()[fieldName].type; } -export function delegateRequest, TArgs = any>(options: IDelegateRequestOptions) { +export function delegateRequest, TArgs = any>( + options: IDelegateRequestOptions +) { const delegationContext = getDelegationContext(options); - const transformer = new Transformer(delegationContext, options.binding); + const transformer = new Transformer(delegationContext, options.binding); const processedRequest = transformer.transformRequest(options.request); @@ -92,11 +109,15 @@ export function delegateRequest, TArgs = any>(opt if (operation === 'query' || operation === 'mutation') { const executor = getExecutor(delegationContext); - return new ValueOrPromise(() => executor({ - ...processedRequest, - context, - info, - })).then(originalResult => transformer.transformResult(originalResult)).resolve(); + return new ValueOrPromise(() => + executor({ + ...processedRequest, + context, + info, + }) + ) + .then(originalResult => transformer.transformResult(originalResult)) + .resolve(); } const subscriber = getSubscriber(delegationContext); @@ -105,7 +126,7 @@ export function delegateRequest, TArgs = any>(opt ...processedRequest, context, info, - }).then((subscriptionResult: AsyncIterableIterator | ExecutionResult) => { + }).then(subscriptionResult => { if (Symbol.asyncIterator in subscriptionResult) { // "subscribe" to the subscription result and map the result through the transforms return mapAsyncIterator( @@ -122,26 +143,28 @@ export function delegateRequest, TArgs = any>(opt const emptyObject = {}; -function getDelegationContext({ +function getDelegationContext({ request, schema, operation, fieldName, returnType, - args, + args = {}, context, info, rootValue, transforms = [], transformedSchema, skipTypeMerging, -}: IDelegateRequestOptions): DelegationContext { - let operationDefinition: OperationDefinitionNode; - let targetOperation: OperationTypeNode; +}: IDelegateRequestOptions): DelegationContext { + let operationDefinition: Maybe; + let targetOperation: Maybe; let targetFieldName: string; + skipTypeMerging = skipTypeMerging ?? false; if (operation == null) { operationDefinition = getOperationAST(request.document, undefined); + assertSome(operationDefinition, 'Could not identify the main operation of the document.'); targetOperation = operationDefinition.operation; } else { targetOperation = operation; @@ -149,14 +172,15 @@ function getDelegationContext({ if (fieldName == null) { operationDefinition = operationDefinition ?? getOperationAST(request.document, undefined); - targetFieldName = ((operationDefinition.selectionSet.selections[0] as unknown) as FieldDefinitionNode).name.value; + targetFieldName = (operationDefinition?.selectionSet.selections[0] as unknown as FieldDefinitionNode).name.value; } else { targetFieldName = fieldName; } - const stitchingInfo: StitchingInfo = info?.schema.extensions?.stitchingInfo; + const stitchingInfo: Maybe> = info?.schema.extensions?.['stitchingInfo']; - const subschemaOrSubschemaConfig = stitchingInfo?.subschemaMap.get(schema) ?? schema; + const subschemaOrSubschemaConfig: GraphQLSchema | SubschemaConfig = + stitchingInfo?.subschemaMap.get(schema) ?? schema; if (isSubschemaConfig(subschemaOrSubschemaConfig)) { const targetSchema = subschemaOrSubschemaConfig.schema; @@ -170,12 +194,15 @@ function getDelegationContext({ context, info, rootValue: rootValue ?? subschemaOrSubschemaConfig?.rootValue ?? info?.rootValue ?? emptyObject, - returnType: returnType ?? info?.returnType ?? getDelegationReturnType(targetSchema, targetOperation, targetFieldName), + returnType: + returnType ?? info?.returnType ?? getDelegationReturnType(targetSchema, targetOperation, targetFieldName), transforms: subschemaOrSubschemaConfig.transforms != null ? subschemaOrSubschemaConfig.transforms.concat(transforms) : transforms, - transformedSchema: transformedSchema ?? (subschemaOrSubschemaConfig as Subschema)?.transformedSchema ?? targetSchema, + transformedSchema: + transformedSchema ?? + (subschemaOrSubschemaConfig instanceof Subschema ? subschemaOrSubschemaConfig.transformedSchema : targetSchema), skipTypeMerging, }; } @@ -190,14 +217,17 @@ function getDelegationContext({ context, info, rootValue: rootValue ?? info?.rootValue ?? emptyObject, - returnType: returnType ?? info?.returnType ?? getDelegationReturnType(subschemaOrSubschemaConfig, targetOperation, targetFieldName), + returnType: + returnType ?? + info?.returnType ?? + getDelegationReturnType(subschemaOrSubschemaConfig, targetOperation, targetFieldName), transforms, transformedSchema: transformedSchema ?? subschemaOrSubschemaConfig, skipTypeMerging, }; } -function validateRequest(delegationContext: DelegationContext, document: DocumentNode) { +function validateRequest(delegationContext: DelegationContext, document: DocumentNode) { const errors = validate(delegationContext.targetSchema, document); if (errors.length > 0) { if (errors.length > 1) { @@ -209,16 +239,21 @@ function validateRequest(delegationContext: DelegationContext, document: Documen } } -function getExecutor(delegationContext: DelegationContext): Executor { +// Since the memo function relies on WeakMap which needs an object. +// TODO: clarify whether this has been a potential runtime error +const executorFallbackRootValue = {}; + +function getExecutor(delegationContext: DelegationContext): Executor { const { subschemaConfig, targetSchema, context, rootValue } = delegationContext; let executor: Executor = - subschemaConfig?.executor || createDefaultExecutor(targetSchema, subschemaConfig?.rootValue || rootValue); + subschemaConfig?.executor || + createDefaultExecutor(targetSchema, subschemaConfig?.rootValue ?? rootValue ?? executorFallbackRootValue); if (subschemaConfig?.batch) { const batchingOptions = subschemaConfig?.batchingOptions; executor = getBatchingExecutor( - context, + context as any, executor, batchingOptions?.dataLoaderOptions, batchingOptions?.extensionsReducer @@ -228,12 +263,12 @@ function getExecutor(delegationContext: DelegationContext): Executor { return executor; } -function getSubscriber(delegationContext: DelegationContext): Subscriber { +function getSubscriber(delegationContext: DelegationContext): Subscriber { const { subschemaConfig, targetSchema, rootValue } = delegationContext; return subschemaConfig?.subscriber || createDefaultSubscriber(targetSchema, subschemaConfig?.rootValue || rootValue); } -const createDefaultExecutor = memoize2(function (schema: GraphQLSchema, rootValue: Record): Executor { +const createDefaultExecutor = memoize2(function (schema: GraphQLSchema, rootValue?: Record): Executor { return (({ document, context, variables, info }: ExecutionParams) => execute({ schema, @@ -244,7 +279,7 @@ const createDefaultExecutor = memoize2(function (schema: GraphQLSchema, rootValu })) as Executor; }); -function createDefaultSubscriber(schema: GraphQLSchema, rootValue: Record) { +function createDefaultSubscriber(schema: GraphQLSchema, rootValue?: Record) { return ({ document, context, variables, info }: ExecutionParams) => subscribe({ schema, diff --git a/packages/delegate/src/delegationBindings.ts b/packages/delegate/src/delegationBindings.ts index d5946f1f94f..23bffcc73ee 100644 --- a/packages/delegate/src/delegationBindings.ts +++ b/packages/delegate/src/delegationBindings.ts @@ -1,3 +1,4 @@ +import { assertSome, Maybe } from '@graphql-tools/utils'; import { Transform, StitchingInfo, DelegationContext } from './types'; import AddSelectionSets from './transforms/AddSelectionSets'; @@ -8,13 +9,17 @@ import AddTypenameToAbstract from './transforms/AddTypenameToAbstract'; import CheckResultAndHandleErrors from './transforms/CheckResultAndHandleErrors'; import AddArgumentsAsVariables from './transforms/AddArgumentsAsVariables'; -export function defaultDelegationBinding(delegationContext: DelegationContext): Array { - let delegationTransforms: Array = [new CheckResultAndHandleErrors()]; +export function defaultDelegationBinding( + delegationContext: DelegationContext +): Array> { + let delegationTransforms: Array> = [new CheckResultAndHandleErrors()]; const info = delegationContext.info; - const stitchingInfo: StitchingInfo = info?.schema.extensions?.stitchingInfo; + const stitchingInfo: Maybe = info?.schema.extensions?.['stitchingInfo']; if (stitchingInfo != null) { + assertSome(stitchingInfo.selectionSetsByType); + assertSome(stitchingInfo.dynamicSelectionSetsByField); delegationTransforms = delegationTransforms.concat([ new ExpandAbstractTypes(), new AddSelectionSets( diff --git a/packages/delegate/src/externalObjects.ts b/packages/delegate/src/externalObjects.ts index 631afc45114..81e2a9679e5 100644 --- a/packages/delegate/src/externalObjects.ts +++ b/packages/delegate/src/externalObjects.ts @@ -12,7 +12,7 @@ export function isExternalObject(data: any): data is ExternalObject { export function annotateExternalObject( object: any, errors: Array, - subschema: GraphQLSchema | SubschemaConfig + subschema: GraphQLSchema | SubschemaConfig | undefined ): ExternalObject { Object.defineProperties(object, { [OBJECT_SUBSCHEMA_SYMBOL]: { value: subschema }, diff --git a/packages/delegate/src/getFieldsNotInSubschema.ts b/packages/delegate/src/getFieldsNotInSubschema.ts index c3a2249304a..6b81dad4db9 100644 --- a/packages/delegate/src/getFieldsNotInSubschema.ts +++ b/packages/delegate/src/getFieldsNotInSubschema.ts @@ -1,6 +1,6 @@ import { GraphQLSchema, FieldNode, GraphQLObjectType, GraphQLResolveInfo } from 'graphql'; -import { collectFields, GraphQLExecutionContext } from '@graphql-tools/utils'; +import { collectFields, GraphQLExecutionContext, Maybe } from '@graphql-tools/utils'; import { isSubschemaConfig } from './subschemaConfig'; import { MergedTypeInfo, SubschemaConfig, StitchingInfo } from './types'; @@ -11,24 +11,27 @@ function collectSubFields(info: GraphQLResolveInfo, typeName: string): Record { - subFieldNodes = collectFields( - partialExecutionContext, - type, - fieldNode.selectionSet, - subFieldNodes, - visitedFragmentNames - ); + if (fieldNode.selectionSet) { + subFieldNodes = collectFields( + partialExecutionContext, + type, + fieldNode.selectionSet, + subFieldNodes, + visitedFragmentNames + ); + } }); - const stitchingInfo = info.schema.extensions.stitchingInfo as StitchingInfo; - const selectionSetsByField = stitchingInfo.selectionSetsByField; + // TODO: Verify whether it is safe that extensions always exists. + const stitchingInfo: Maybe = info.schema.extensions?.['stitchingInfo']; + const selectionSetsByField = stitchingInfo?.selectionSetsByField; Object.keys(subFieldNodes).forEach(responseName => { const fieldName = subFieldNodes[responseName][0].name.value; @@ -49,10 +52,13 @@ function collectSubFields(info: GraphQLResolveInfo, typeName: string): Record, mergedTypeInfo: MergedTypeInfo ): Array { const typeMap = isSubschemaConfig(subschema) ? mergedTypeInfo.typeMaps.get(subschema) : subschema.getTypeMap(); + if (!typeMap) { + return []; + } const typeName = mergedTypeInfo.typeName; const fields = (typeMap[typeName] as GraphQLObjectType).getFields(); diff --git a/packages/delegate/src/mergeFields.ts b/packages/delegate/src/mergeFields.ts index 3b44f3fb867..8e711a6d058 100644 --- a/packages/delegate/src/mergeFields.ts +++ b/packages/delegate/src/mergeFields.ts @@ -118,7 +118,8 @@ const buildDelegationPlan = memoize3(function ( const existingSubschema = nonUniqueSubschemas.find(s => delegationMap.has(s)); if (existingSubschema != null) { - delegationMap.get(existingSubschema).push(fieldNode); + // It is okay we previously explicitly check whether the map has the element. + delegationMap.get(existingSubschema)!.push(fieldNode); } else { delegationMap.set(nonUniqueSubschemas[0], [fieldNode]); } @@ -153,9 +154,9 @@ export function mergeFields( typeName: string, object: any, fieldNodes: Array, - sourceSubschemaOrSourceSubschemas: Subschema | Array, - targetSubschemas: Array, - context: Record, + sourceSubschemaOrSourceSubschemas: Subschema | Array>, + targetSubschemas: Array>, + context: any, info: GraphQLResolveInfo ): any { if (!fieldNodes.length) { @@ -177,30 +178,35 @@ export function mergeFields( const resultMap: Map, SelectionSetNode> = new Map(); delegationMap.forEach((selectionSet: SelectionSetNode, s: Subschema) => { - const resolver = mergedTypeInfo.resolvers.get(s); - const valueOrPromise = new ValueOrPromise(() => resolver(object, context, info, s, selectionSet)).catch(error => error); + // TODO: Verify whether it is safe that resolver always exists. + const resolver = mergedTypeInfo.resolvers.get(s)!; + const valueOrPromise = new ValueOrPromise(() => resolver(object, context, info, s, selectionSet)).catch( + error => error + ); resultMap.set(valueOrPromise, selectionSet); }); - return ValueOrPromise.all(Array.from(resultMap.keys())).then(results => - mergeFields( - mergedTypeInfo, - typeName, - mergeExternalObjects( - info.schema, - responsePathAsArray(info.path), - object.__typename, - object, - results, - Array.from(resultMap.values()) - ), - unproxiableFieldNodes, - combineSubschemas(sourceSubschemaOrSourceSubschemas, proxiableSubschemas), - nonProxiableSubschemas, - context, - info + return ValueOrPromise.all(Array.from(resultMap.keys())) + .then(results => + mergeFields( + mergedTypeInfo, + typeName, + mergeExternalObjects( + info.schema, + responsePathAsArray(info.path), + object.__typename, + object, + results, + Array.from(resultMap.values()) + ), + unproxiableFieldNodes, + combineSubschemas(sourceSubschemaOrSourceSubschemas, proxiableSubschemas), + nonProxiableSubschemas, + context, + info + ) ) - ).resolve(); + .resolve(); } const subschemaTypesContainSelectionSet = memoize3(function ( @@ -239,7 +245,7 @@ function typesContainSelectionSet(types: Array, selectionSet: selection.selectionSet ); } - } else if (selection.kind === Kind.INLINE_FRAGMENT && selection.typeCondition.name.value === types[0].name) { + } else if (selection.kind === Kind.INLINE_FRAGMENT && selection.typeCondition?.name.value === types[0].name) { return typesContainSelectionSet(types, selection.selectionSet); } } diff --git a/packages/delegate/src/resolveExternalValue.ts b/packages/delegate/src/resolveExternalValue.ts index 73495d174ce..954a024a30c 100644 --- a/packages/delegate/src/resolveExternalValue.ts +++ b/packages/delegate/src/resolveExternalValue.ts @@ -20,6 +20,7 @@ import { annotateExternalObject, isExternalObject } from './externalObjects'; import { getFieldsNotInSubschema } from './getFieldsNotInSubschema'; import { mergeFields } from './mergeFields'; import { Subschema } from './Subschema'; +import { Maybe } from '@graphql-tools/utils'; export function resolveExternalValue( result: any, @@ -66,8 +67,8 @@ function resolveExternalObject( annotateExternalObject(object, unpathedErrors, subschema); - const stitchingInfo: StitchingInfo = info?.schema.extensions?.stitchingInfo; - if (skipTypeMerging || !stitchingInfo) { + const stitchingInfo: Maybe = info?.schema.extensions?.['stitchingInfo']; + if (skipTypeMerging || stitchingInfo == null) { return object; } @@ -86,7 +87,7 @@ function resolveExternalObject( } const mergedTypeInfo = stitchingInfo.mergedTypes[typeName]; - let targetSubschemas: Array; + let targetSubschemas: undefined | Array; // Within the stitching context, delegation to a stitched GraphQLSchema or SubschemaConfig // will be redirected to the appropriate Subschema object, from which merge targets can be queried. @@ -106,7 +107,7 @@ function resolveExternalObject( typeName, object, fieldNodes, - subschema as Subschema, + subschema as Subschema | Array, targetSubschemas, context, info @@ -179,7 +180,11 @@ function reportUnpathedErrorsViaNull(unpathedErrors: Array) { } const combinedError = new AggregateError(unreportedErrors); - return locatedError(combinedError, undefined, unreportedErrors[0].path); + // We cast path as any for GraphQL.js 14 compat + // locatedError path argument must be defined, but it is just forwarded to a constructor that allows a undefined value + // https://github.com/graphql/graphql-js/blob/b4bff0ba9c15c9d7245dd68556e754c41f263289/src/error/locatedError.js#L25 + // https://github.com/graphql/graphql-js/blob/b4bff0ba9c15c9d7245dd68556e754c41f263289/src/error/GraphQLError.js#L19 + return locatedError(combinedError, undefined as any, unreportedErrors[0].path as any); } } diff --git a/packages/delegate/src/subschemaConfig.ts b/packages/delegate/src/subschemaConfig.ts index e4efb5a14b7..2c9fd247f11 100644 --- a/packages/delegate/src/subschemaConfig.ts +++ b/packages/delegate/src/subschemaConfig.ts @@ -1,6 +1,6 @@ import { SubschemaConfig } from './types'; -export function isSubschemaConfig(value: any): value is SubschemaConfig { +export function isSubschemaConfig(value: any): value is SubschemaConfig { return Boolean(value?.schema); } @@ -12,8 +12,8 @@ export function cloneSubschemaConfig(subschemaConfig: SubschemaConfig): Subschem if (newSubschemaConfig.merge != null) { newSubschemaConfig.merge = { ...subschemaConfig.merge }; - Object.keys(newSubschemaConfig.merge).forEach(typeName => { - const mergedTypeConfig = (newSubschemaConfig.merge[typeName] = { ...subschemaConfig.merge[typeName] }); + for (const typeName of Object.keys(newSubschemaConfig.merge)) { + const mergedTypeConfig = (newSubschemaConfig.merge[typeName] = { ...(subschemaConfig.merge?.[typeName] ?? {}) }); if (mergedTypeConfig.entryPoints != null) { mergedTypeConfig.entryPoints = mergedTypeConfig.entryPoints.map(entryPoint => ({ ...entryPoint })); @@ -25,7 +25,7 @@ export function cloneSubschemaConfig(subschemaConfig: SubschemaConfig): Subschem fields[fieldName] = { ...fields[fieldName] }; }); } - }); + } } return newSubschemaConfig; diff --git a/packages/delegate/src/transforms/AddArgumentsAsVariables.ts b/packages/delegate/src/transforms/AddArgumentsAsVariables.ts index 53a576a42a8..33fb3881de1 100644 --- a/packages/delegate/src/transforms/AddArgumentsAsVariables.ts +++ b/packages/delegate/src/transforms/AddArgumentsAsVariables.ts @@ -12,7 +12,7 @@ import { VariableDefinitionNode, } from 'graphql'; -import { Request, serializeInputValue, updateArgument } from '@graphql-tools/utils'; +import { Maybe, Request, serializeInputValue, updateArgument, assertSome } from '@graphql-tools/utils'; import { Transform, DelegationContext } from '../types'; @@ -63,7 +63,7 @@ function addVariablesToRootField( ) as Array; const newOperations = operations.map((operation: OperationDefinitionNode) => { - const variableDefinitionMap: Record = operation.variableDefinitions.reduce( + const variableDefinitionMap: Record = (operation.variableDefinitions ?? []).reduce( (prev, def) => ({ ...prev, [def.variable.name.value]: def, @@ -71,7 +71,7 @@ function addVariablesToRootField( {} ); - let type: GraphQLObjectType | null | undefined; + let type: Maybe; if (operation.operation === 'subscription') { type = targetSchema.getSubscriptionType(); } else if (operation.operation === 'mutation') { @@ -79,9 +79,12 @@ function addVariablesToRootField( } else { type = targetSchema.getQueryType(); } + + assertSome(type); + const newSelectionSet: Array = []; - operation.selectionSet.selections.forEach((selection: SelectionNode) => { + for (const selection of operation.selectionSet.selections) { if (selection.kind === Kind.FIELD) { const argumentNodes = selection.arguments ?? []; const argumentNodeMap: Record = argumentNodes.reduce( @@ -106,7 +109,7 @@ function addVariablesToRootField( } else { newSelectionSet.push(selection); } - }); + } return { ...operation, diff --git a/packages/delegate/src/transforms/AddSelectionSets.ts b/packages/delegate/src/transforms/AddSelectionSets.ts index d725fd1c364..f663ad6651b 100644 --- a/packages/delegate/src/transforms/AddSelectionSets.ts +++ b/packages/delegate/src/transforms/AddSelectionSets.ts @@ -1,6 +1,6 @@ import { SelectionSetNode, TypeInfo, Kind, FieldNode, SelectionNode, print } from 'graphql'; -import { Request } from '@graphql-tools/utils'; +import { Maybe, Request } from '@graphql-tools/utils'; import { Transform, DelegationContext } from '../types'; import { memoize2 } from '../memoize'; @@ -35,7 +35,7 @@ function visitSelectionSet( selectionSetsByType: Record, selectionSetsByField: Record>, dynamicSelectionSetsByField: Record SelectionSetNode>>> -): SelectionSetNode { +): Maybe { const parentType = typeInfo.getParentType(); const newSelections: Map = new Map(); diff --git a/packages/delegate/src/transforms/AddTypenameToAbstract.ts b/packages/delegate/src/transforms/AddTypenameToAbstract.ts index 4ffb31f57a7..0eb25a2416c 100644 --- a/packages/delegate/src/transforms/AddTypenameToAbstract.ts +++ b/packages/delegate/src/transforms/AddTypenameToAbstract.ts @@ -10,7 +10,7 @@ import { isAbstractType, } from 'graphql'; -import { Request } from '@graphql-tools/utils'; +import { Maybe, Request } from '@graphql-tools/utils'; import { Transform, DelegationContext } from '../types'; @@ -34,7 +34,7 @@ function addTypenameToAbstract(targetSchema: GraphQLSchema, document: DocumentNo document, visitWithTypeInfo(typeInfo, { [Kind.SELECTION_SET](node: SelectionSetNode): SelectionSetNode | null | undefined { - const parentType: GraphQLType = typeInfo.getParentType(); + const parentType: Maybe = typeInfo.getParentType(); let selections = node.selections; if (parentType != null && isAbstractType(parentType)) { selections = selections.concat({ diff --git a/packages/delegate/src/transforms/CheckResultAndHandleErrors.ts b/packages/delegate/src/transforms/CheckResultAndHandleErrors.ts index 264541de177..912da07bc46 100644 --- a/packages/delegate/src/transforms/CheckResultAndHandleErrors.ts +++ b/packages/delegate/src/transforms/CheckResultAndHandleErrors.ts @@ -38,7 +38,7 @@ export function checkResultAndHandleErrors( context: Record, info: GraphQLResolveInfo, responseKey: string = getResponseKeyFromInfo(info), - subschema?: GraphQLSchema | SubschemaConfig, + subschema: GraphQLSchema | SubschemaConfig, returnType: GraphQLOutputType = info.returnType, skipTypeMerging?: boolean, onLocatedError?: (originalError: GraphQLError) => GraphQLError @@ -46,7 +46,7 @@ export function checkResultAndHandleErrors( const { data, unpathedErrors } = mergeDataAndErrors( result.data == null ? undefined : result.data[responseKey], result.errors == null ? [] : result.errors, - info ? responsePathAsArray(info.path) : undefined, + info != null && info.path ? responsePathAsArray(info.path) : undefined, onLocatedError ); @@ -56,8 +56,8 @@ export function checkResultAndHandleErrors( export function mergeDataAndErrors( data: any, errors: ReadonlyArray, - path: Array, - onLocatedError: (originalError: GraphQLError) => GraphQLError, + path: Array | undefined, + onLocatedError?: (originalError: GraphQLError) => GraphQLError, index = 1 ): { data: any; unpathedErrors: Array } { if (data == null) { @@ -73,7 +73,11 @@ export function mergeDataAndErrors( return { data: relocatedError(errors[0], newPath), unpathedErrors: [] }; } - const newError = locatedError(new AggregateError(errors), undefined, path); + // We cast path as any for GraphQL.js 14 compat + // locatedError path argument must be defined, but it is just forwarded to a constructor that allows a undefined value + // https://github.com/graphql/graphql-js/blob/b4bff0ba9c15c9d7245dd68556e754c41f263289/src/error/locatedError.js#L25 + // https://github.com/graphql/graphql-js/blob/b4bff0ba9c15c9d7245dd68556e754c41f263289/src/error/GraphQLError.js#L19 + const newError = locatedError(new AggregateError(errors), undefined as any, path as any); return { data: newError, unpathedErrors: [] }; } diff --git a/packages/delegate/src/transforms/FilterToSchema.ts b/packages/delegate/src/transforms/FilterToSchema.ts index 6f236ddcb05..553f3618903 100644 --- a/packages/delegate/src/transforms/FilterToSchema.ts +++ b/packages/delegate/src/transforms/FilterToSchema.ts @@ -19,9 +19,10 @@ import { getNamedType, isObjectType, isInterfaceType, + GraphQLObjectType, } from 'graphql'; -import { Request, implementsAbstractType, TypeMap } from '@graphql-tools/utils'; +import { Request, implementsAbstractType, TypeMap, assertSome, Maybe } from '@graphql-tools/utils'; import { Transform, DelegationContext } from '../types'; @@ -71,7 +72,7 @@ function filterToSchema( let fragmentSet = Object.create(null); operations.forEach((operation: OperationDefinitionNode) => { - let type; + let type: Maybe>; if (operation.operation === 'subscription') { type = targetSchema.getSubscriptionType(); } else if (operation.operation === 'mutation') { @@ -79,6 +80,7 @@ function filterToSchema( } else { type = targetSchema.getQueryType(); } + assertSome(type); const { selectionSet, @@ -98,7 +100,7 @@ function filterToSchema( newFragments = collectedNewFragments; fragmentSet = collectedFragmentSet; - const variableDefinitions = operation.variableDefinitions.filter( + const variableDefinitions = (operation.variableDefinitions ?? []).filter( (variable: VariableDefinitionNode) => operationOrFragmentVariables.indexOf(variable.variable.name.value) !== -1 ); @@ -148,6 +150,7 @@ function collectFragmentVariables( const name = nextFragmentName; const typeName = fragment.typeCondition.name.value; const type = targetSchema.getType(typeName); + assertSome(type); const { selectionSet, usedFragments: fragmentUsedFragments, @@ -156,7 +159,7 @@ function collectFragmentVariables( remainingFragments = union(remainingFragments, fragmentUsedFragments); usedVariables = union(usedVariables, fragmentUsedVariables); - if (!(name in fragmentSet)) { + if (name && !(name in fragmentSet)) { fragmentSet[name] = true; newFragments.push({ kind: Kind.FRAGMENT_DEFINITION, @@ -214,7 +217,9 @@ function filterSelectionSet( } }, leave(node: FieldNode): null | undefined | FieldNode { - const resolvedType = getNamedType(typeInfo.getType()); + const type = typeInfo.getType(); + assertSome(type); + const resolvedType = getNamedType(type); if (isObjectType(resolvedType) || isInterfaceType(resolvedType)) { const selections = node.selectionSet != null ? node.selectionSet.selections : null; if (selections == null || selections.length === 0) { diff --git a/packages/delegate/src/transforms/VisitSelectionSets.ts b/packages/delegate/src/transforms/VisitSelectionSets.ts index 3f807241b0c..02c547e9e20 100644 --- a/packages/delegate/src/transforms/VisitSelectionSets.ts +++ b/packages/delegate/src/transforms/VisitSelectionSets.ts @@ -13,14 +13,16 @@ import { DefinitionNode, } from 'graphql'; -import { Request, collectFields, GraphQLExecutionContext } from '@graphql-tools/utils'; +import { Request, collectFields, GraphQLExecutionContext, assertSome, Maybe } from '@graphql-tools/utils'; import { Transform, DelegationContext } from '../types'; +type VisitSelectionSetsVisitor = (node: SelectionSetNode, typeInfo: TypeInfo) => Maybe; + export default class VisitSelectionSets implements Transform { - private readonly visitor: (node: SelectionSetNode, typeInfo: TypeInfo) => SelectionSetNode; + private readonly visitor: VisitSelectionSetsVisitor; - constructor(visitor: (node: SelectionSetNode, typeInfo: TypeInfo) => SelectionSetNode) { + constructor(visitor: VisitSelectionSetsVisitor) { this.visitor = visitor; } @@ -46,7 +48,7 @@ function visitSelectionSets( request: Request, schema: GraphQLSchema, initialType: GraphQLOutputType, - visitor: (node: SelectionSetNode, typeInfo: TypeInfo) => SelectionSetNode + visitor: VisitSelectionSetsVisitor ): DocumentNode { const { document, variables } = request; @@ -74,6 +76,7 @@ function visitSelectionSets( : operation.operation === 'mutation' ? schema.getMutationType() : schema.getSubscriptionType(); + assertSome(type); const fields = collectFields( partialExecutionContext, diff --git a/packages/delegate/src/transforms/WrapConcreteTypes.ts b/packages/delegate/src/transforms/WrapConcreteTypes.ts index 069aa81e868..7c7bb5b0352 100644 --- a/packages/delegate/src/transforms/WrapConcreteTypes.ts +++ b/packages/delegate/src/transforms/WrapConcreteTypes.ts @@ -63,7 +63,8 @@ function wrapConcreteTypes( } }, [Kind.FIELD]: (node: FieldNode) => { - if (isAbstractType(getNamedType(typeInfo.getType()))) { + const type = typeInfo.getType(); + if (type != null && isAbstractType(getNamedType(type))) { return { ...node, selectionSet: { diff --git a/packages/delegate/src/types.ts b/packages/delegate/src/types.ts index 419f11bd075..272c421c857 100644 --- a/packages/delegate/src/types.ts +++ b/packages/delegate/src/types.ts @@ -19,9 +19,9 @@ import { ExecutionParams, ExecutionResult, Executor, Request, Subscriber, TypeMa import { Subschema } from './Subschema'; import { OBJECT_SUBSCHEMA_SYMBOL, FIELD_SUBSCHEMA_MAP_SYMBOL, UNPATHED_ERRORS_SYMBOL } from './symbols'; -export type SchemaTransform = ( +export type SchemaTransform> = ( originalWrappingSchema: GraphQLSchema, - subschemaConfig: SubschemaConfig, + subschemaConfig: SubschemaConfig, transformedSchema?: GraphQLSchema ) => GraphQLSchema; export type RequestTransform> = ( @@ -35,30 +35,32 @@ export type ResultTransform> = ( transformationContext: T ) => ExecutionResult; -export interface Transform> { - transformSchema?: SchemaTransform; +export interface Transform> { + transformSchema?: SchemaTransform; transformRequest?: RequestTransform; transformResult?: ResultTransform; } -export interface DelegationContext { - subschema: GraphQLSchema | SubschemaConfig; - subschemaConfig: SubschemaConfig; +export interface DelegationContext> { + subschema: GraphQLSchema | SubschemaConfig; + subschemaConfig?: SubschemaConfig; targetSchema: GraphQLSchema; operation: OperationTypeNode; fieldName: string; args: Record; - context: Record; + context?: TContext; info: GraphQLResolveInfo; - rootValue?: Record, + rootValue?: Record; returnType: GraphQLOutputType; onLocatedError?: (originalError: GraphQLError) => GraphQLError; - transforms: Array; + transforms: Array>; transformedSchema: GraphQLSchema; skipTypeMerging: boolean; } -export type DelegationBinding = (delegationContext: DelegationContext) => Array; +export type DelegationBinding> = ( + delegationContext: DelegationContext +) => Array>; export interface IDelegateToSchemaOptions, TArgs = Record> { schema: GraphQLSchema | SubschemaConfig; @@ -73,17 +75,16 @@ export interface IDelegateToSchemaOptions, TArgs context?: TContext; info: GraphQLResolveInfo; rootValue?: Record; - transforms?: Array; + transforms?: Array>; transformedSchema?: GraphQLSchema; skipValidation?: boolean; skipTypeMerging?: boolean; - binding?: DelegationBinding; + binding?: DelegationBinding; } export interface IDelegateRequestOptions, TArgs = Record> - extends Omit, 'info'> { + extends IDelegateToSchemaOptions { request: Request; - info?: GraphQLResolveInfo; } export interface ICreateRequestFromInfo { @@ -112,13 +113,13 @@ export interface ICreateRequest { export interface MergedTypeInfo> { typeName: string; selectionSet?: SelectionSetNode; - targetSubschemas: Map>; - uniqueFields: Record; - nonUniqueFields: Record>; - typeMaps: Map; - selectionSets: Map; - fieldSelectionSets: Map>; - resolvers: Map>; + targetSubschemas: Map, Array>>; + uniqueFields: Record>; + nonUniqueFields: Record>>; + typeMaps: Map, TypeMap>; + selectionSets: Map, SelectionSetNode>; + fieldSelectionSets: Map, Record>; + resolvers: Map, MergedTypeResolver>; } export interface ICreateProxyingResolverOptions> { @@ -140,7 +141,7 @@ export interface BatchingOptions { export interface SubschemaConfig> { schema: GraphQLSchema; createProxyingResolver?: CreateProxyingResolverFn; - transforms?: Array; + transforms?: Array>; merge?: Record>; rootValue?: Record; executor?: Executor; @@ -149,14 +150,16 @@ export interface SubschemaConfig; } -export interface MergedTypeConfig> extends MergedTypeEntryPoint { +export interface MergedTypeConfig> + extends MergedTypeEntryPoint { entryPoints?: Array; fields?: Record; computedFields?: Record; canonical?: boolean; } -export interface MergedTypeEntryPoint> extends MergedTypeResolverOptions { +export interface MergedTypeEntryPoint> + extends MergedTypeResolverOptions { selectionSet?: string; key?: (originalResult: any) => K; resolve?: MergedTypeResolver; @@ -186,9 +189,9 @@ export type MergedTypeResolver> = ( export interface StitchingInfo> { subschemaMap: Map, Subschema>; - selectionSetsByType: Record; + selectionSetsByType: Record | undefined; selectionSetsByField: Record>; - dynamicSelectionSetsByField: Record SelectionSetNode>>>; + dynamicSelectionSetsByField: Record SelectionSetNode>>> | undefined; mergedTypes: Record>; } diff --git a/packages/delegate/tests/createRequest.test.ts b/packages/delegate/tests/createRequest.test.ts index d5f4431a001..36e0348be05 100644 --- a/packages/delegate/tests/createRequest.test.ts +++ b/packages/delegate/tests/createRequest.test.ts @@ -36,7 +36,7 @@ describe('bare requests', () => { `, resolvers: { Query: { - delegate: (_root, args) => { + delegate: (_root, args, _context, info) => { const request = createRequest({ fieldNodes: [{ kind: Kind.FIELD, @@ -72,6 +72,7 @@ describe('bare requests', () => { return delegateRequest({ request, schema: innerSchema, + info }); }, }, @@ -129,7 +130,7 @@ describe('bare requests', () => { `, resolvers: { Query: { - delegate: (_root, args) => { + delegate: (_root, args, _context, info) => { const request = createRequest({ fieldNodes: [{ kind: Kind.FIELD, @@ -155,6 +156,7 @@ describe('bare requests', () => { request, schema: innerSchema, args, + info, }); }, }, @@ -203,7 +205,7 @@ describe('bare requests', () => { `, resolvers: { Query: { - delegate: () => { + delegate: (_source, _args, _context, info) => { const request = createRequest({ fieldNodes: [{ kind: Kind.FIELD, @@ -218,6 +220,7 @@ describe('bare requests', () => { return delegateRequest({ request, schema: innerSchema, + info }); }, }, diff --git a/packages/delegate/tests/delegateToSchema.test..ts b/packages/delegate/tests/delegateToSchema.test..ts index 11b54026171..4c51bf400c4 100644 --- a/packages/delegate/tests/delegateToSchema.test..ts +++ b/packages/delegate/tests/delegateToSchema.test..ts @@ -3,6 +3,12 @@ import { graphql } from 'graphql'; import { delegateToSchema } from '../src/delegateToSchema'; import { makeExecutableSchema } from '@graphql-tools/schema'; +function assertSome(input: T): asserts input is Exclude{ + if (input == null) { + throw new Error("Value should be neither null nor undefined.") + } +} + describe('delegateToSchema', () => { test('should work', async () => { const innerSchema = makeExecutableSchema({ @@ -47,6 +53,7 @@ describe('delegateToSchema', () => { `, ); + assertSome(result.data) expect(result.data.delegateToSchema).toEqual('test'); }); @@ -93,6 +100,7 @@ describe('delegateToSchema', () => { `, ); + assertSome(result.data) expect(result.data.delegateToSchema).toEqual('test'); }); @@ -144,6 +152,7 @@ describe('delegateToSchema', () => { }, ); + assertSome(result.data) expect(result.data.delegateToSchema).toEqual('test'); }); }); diff --git a/packages/delegate/tests/errors.test.ts b/packages/delegate/tests/errors.test.ts index faf8d5b2ff0..56833c27c62 100644 --- a/packages/delegate/tests/errors.test.ts +++ b/packages/delegate/tests/errors.test.ts @@ -33,6 +33,19 @@ describe('Errors', () => { }); describe('checkResultAndHandleErrors', () => { + const fakeInfo: GraphQLResolveInfo = { + fieldName: "foo", + fieldNodes: [], + returnType: {} as any, + parentType: {} as any, + path: {prev: undefined, key: "foo", typename: undefined }, + schema: {} as any, + fragments: {}, + rootValue: {}, + operation: {} as any, + variableValues: {} + } + test('persists single error', () => { const result = { errors: [new GraphQLError('Test error')], @@ -41,8 +54,9 @@ describe('Errors', () => { checkResultAndHandleErrors( result, {}, - ({} as unknown) as GraphQLResolveInfo, + fakeInfo, 'responseKey', + {} as any, ); } catch (e) { expect(e.message).toEqual('Test error'); @@ -58,8 +72,9 @@ describe('Errors', () => { checkResultAndHandleErrors( result, {}, - ({} as unknown) as GraphQLResolveInfo, + fakeInfo, 'responseKey', + {} as any ); } catch (e) { expect(e.message).toEqual('Test error'); @@ -76,8 +91,9 @@ describe('Errors', () => { checkResultAndHandleErrors( result, {}, - ({} as unknown) as GraphQLResolveInfo, + fakeInfo, 'responseKey', + {} as any ); } catch (e) { expect(e.message).toEqual('Error1\nError2'); diff --git a/packages/graphql-tag-pluck/src/utils.ts b/packages/graphql-tag-pluck/src/utils.ts index 47c7163d2cb..c302045182f 100644 --- a/packages/graphql-tag-pluck/src/utils.ts +++ b/packages/graphql-tag-pluck/src/utils.ts @@ -22,7 +22,10 @@ export const freeText = (text: string | string[], skipIndentation = false) => { const minIndent = lines .filter(line => line.trim()) .reduce((minIndent, line) => { - const currIndent = line.match(/^ */)[0].length; + const currIndent = line.match(/^ */)?.[0].length; + if (currIndent == null) { + return minIndent; + } return currIndent < minIndent ? currIndent : minIndent; }, Infinity); @@ -47,7 +50,7 @@ export const toUpperFirst = (str: string) => { // foo-bar-baz -> fooBarBaz export const toCamelCase = (str: string) => { const words = splitWords(str); - const first = words.shift().toLowerCase(); + const first = words.shift()?.toLowerCase() ?? ''; const rest = words.map(toUpperFirst); return [first, ...rest].join(''); diff --git a/packages/graphql-tag-pluck/src/visitor.ts b/packages/graphql-tag-pluck/src/visitor.ts index e4e8537a7f4..d3e722c0611 100644 --- a/packages/graphql-tag-pluck/src/visitor.ts +++ b/packages/graphql-tag-pluck/src/visitor.ts @@ -8,6 +8,7 @@ import { isImportSpecifier, } from '@babel/types'; import { asArray } from '@graphql-tools/utils'; +import { Visitor } from '@babel/traverse'; const defaults: GraphQLTagPluckOptions = { modules: [ @@ -140,13 +141,17 @@ export type PluckedContent = { export default (code: string, out: any, options: GraphQLTagPluckOptions = {}) => { // Apply defaults to options - let { modules, globalGqlIdentifierName, gqlMagicComment } = { + let { + modules = [], + globalGqlIdentifierName, + gqlMagicComment, + } = { ...defaults, ...options, }; // Prevent case related potential errors - gqlMagicComment = gqlMagicComment.toLowerCase(); + gqlMagicComment = gqlMagicComment!.toLowerCase(); // normalize `name` and `identifier` values modules = modules.map(mod => { return { @@ -154,7 +159,7 @@ export default (code: string, out: any, options: GraphQLTagPluckOptions = {}) => identifier: mod.identifier && mod.identifier.toLowerCase(), }; }); - globalGqlIdentifierName = asArray(globalGqlIdentifierName).map(s => s.toLowerCase()); + globalGqlIdentifierName = asArray(globalGqlIdentifierName).map(s => s!.toLowerCase()); // Keep imported identifiers // import gql from 'graphql-tag' -> gql @@ -167,12 +172,12 @@ export default (code: string, out: any, options: GraphQLTagPluckOptions = {}) => // Check if package is registered function isValidPackage(name: string) { - return modules.some(pkg => pkg.name && name && pkg.name.toLowerCase() === name.toLowerCase()); + return modules!.some(pkg => pkg.name && name && pkg.name.toLowerCase() === name.toLowerCase()); } // Check if identifier is defined and imported from registered packages function isValidIdentifier(name: string) { - return definedIdentifierNames.some(id => id === name) || globalGqlIdentifierName.includes(name); + return definedIdentifierNames.some(id => id === name) || globalGqlIdentifierName!.includes(name); } const pluckStringFromFile = ({ start, end }: { start: number; end: number }) => { @@ -221,12 +226,20 @@ export default (code: string, out: any, options: GraphQLTagPluckOptions = {}) => } }; - return { + const visitor: Visitor = { CallExpression: { - enter(path: any) { + enter(path) { // Find the identifier name used from graphql-tag, commonJS // e.g. import gql from 'graphql-tag' -> gql - if (path.node.callee.name === 'require' && isValidPackage(path.node.arguments[0].value)) { + const arg0 = path.node.arguments[0]; + + if ( + 'name' in path.node.callee && + path.node.callee.name === 'require' && + 'value' in arg0 && + typeof arg0.value === 'string' && + isValidPackage(arg0.value) + ) { if (!isVariableDeclarator(path.parent)) { return; } @@ -239,29 +252,30 @@ export default (code: string, out: any, options: GraphQLTagPluckOptions = {}) => return; } - const arg0 = path.node.arguments[0]; - // Push strings template literals to gql calls // e.g. gql(`query myQuery {}`) -> query myQuery {} if (isIdentifier(path.node.callee) && isValidIdentifier(path.node.callee.name) && isTemplateLiteral(arg0)) { - const gqlTemplateLiteral = pluckStringFromFile(arg0); - - // If the entire template was made out of interpolations it should be an empty - // string by now and thus should be ignored - if (gqlTemplateLiteral) { - gqlTemplateLiterals.push({ - content: gqlTemplateLiteral, - loc: arg0.loc, - end: arg0.end, - start: arg0.start, - }); + const { start, end, loc } = arg0; + if (start != null && end != null && start != null && loc != null) { + const gqlTemplateLiteral = pluckStringFromFile({ start, end }); + + // If the entire template was made out of interpolations it should be an empty + // string by now and thus should be ignored + if (gqlTemplateLiteral) { + gqlTemplateLiterals.push({ + content: gqlTemplateLiteral, + loc, + end, + start, + }); + } } } }, }, ImportDeclaration: { - enter(path: any) { + enter(path) { // Find the identifier name used from graphql-tag, es6 // e.g. import gql from 'graphql-tag' -> gql if (!isValidPackage(path.node.source.value)) { @@ -270,6 +284,10 @@ export default (code: string, out: any, options: GraphQLTagPluckOptions = {}) => const moduleNode = modules.find(pkg => pkg.name.toLowerCase() === path.node.source.value.toLowerCase()); + if (moduleNode == null) { + return; + } + const gqlImportSpecifier = path.node.specifiers.find((importSpecifier: any) => { // When it's a default import and registered package has no named identifier if (isImportDefaultSpecifier(importSpecifier) && !moduleNode.identifier) { @@ -340,4 +358,6 @@ export default (code: string, out: any, options: GraphQLTagPluckOptions = {}) => out.returnValue = gqlTemplateLiterals; }, }; + + return visitor; }; diff --git a/packages/import/src/index.ts b/packages/import/src/index.ts index d69a20e4735..a00455b846d 100644 --- a/packages/import/src/index.ts +++ b/packages/import/src/index.ts @@ -70,12 +70,14 @@ export function processImport( const set = visitFile(filePath, join(cwd + '/root.graphql'), visitedFiles, predefinedImports); const definitionStrSet = new Set(); let definitionsStr = ''; - for (const defs of set.values()) { - for (const def of defs) { - const defStr = print(def); - if (!definitionStrSet.has(defStr)) { - definitionStrSet.add(defStr); - definitionsStr += defStr + '\n'; + if (set != null) { + for (const defs of set.values()) { + for (const def of defs) { + const defStr = print(def); + if (!definitionStrSet.has(defStr)) { + definitionStrSet.add(defStr); + definitionsStr += defStr + '\n'; + } } } } @@ -120,16 +122,18 @@ function visitFile( }); for (const definition of document.definitions) { if ('name' in definition || definition.kind === Kind.SCHEMA_DEFINITION) { - const definitionName = 'name' in definition ? definition.name.value : 'schema'; + const definitionName = 'name' in definition && definition.name ? definition.name.value : 'schema'; if (!definitionsByName.has(definitionName)) { definitionsByName.set(definitionName, new Set()); } const definitionsSet = definitionsByName.get(definitionName); - definitionsSet.add(definition); - if (!dependenciesByDefinitionName.has(definitionName)) { - dependenciesByDefinitionName.set(definitionName, new Set()); + definitionsSet?.add(definition); + + let dependencySet = dependenciesByDefinitionName.get(definitionName); + if (!dependencySet) { + dependencySet = new Set(); + dependenciesByDefinitionName.set(definitionName, dependencySet); } - const dependencySet = dependenciesByDefinitionName.get(definitionName); switch (definition.kind) { case Kind.OPERATION_DEFINITION: visitOperationDefinitionNode(definition, dependencySet); @@ -180,21 +184,22 @@ function visitFile( visitScalarExtensionNode(definition, dependencySet); break; } - if ('fields' in definition) { + if ('fields' in definition && definition.fields) { for (const field of definition.fields) { const definitionName = definition.name.value + '.' + field.name.value; if (!definitionsByName.has(definitionName)) { definitionsByName.set(definitionName, new Set()); } - const definitionsSet = definitionsByName.get(definitionName); - definitionsSet.add({ + definitionsByName.get(definitionName)?.add({ ...definition, fields: [field as any], }); - if (!dependenciesByDefinitionName.has(definitionName)) { - dependenciesByDefinitionName.set(definitionName, new Set()); + + let dependencySet = dependenciesByDefinitionName.get(definitionName); + if (!dependencySet) { + dependencySet = new Set(); + dependenciesByDefinitionName.set(definitionName, dependencySet); } - const dependencySet = dependenciesByDefinitionName.get(definitionName); switch (field.kind) { case Kind.FIELD_DEFINITION: visitFieldDefinitionNode(field, dependencySet, dependenciesByDefinitionName); @@ -208,19 +213,24 @@ function visitFile( } } for (const [definitionName, definitions] of definitionsByName) { - if (!fileDefinitionMap.has(definitionName)) { - fileDefinitionMap.set(definitionName, new Set()); + let definitionsWithDependencies = fileDefinitionMap.get(definitionName); + if (definitionsWithDependencies == null) { + definitionsWithDependencies = new Set(); + fileDefinitionMap.set(definitionName, definitionsWithDependencies); } - const definitionsWithDependencies = fileDefinitionMap.get(definitionName); for (const definition of definitions) { definitionsWithDependencies.add(definition); } const dependenciesOfDefinition = dependenciesByDefinitionName.get(definitionName); - for (const dependencyName of dependenciesOfDefinition) { - const dependencyDefinitions = definitionsByName.get(dependencyName); - dependencyDefinitions?.forEach(dependencyDefinition => { - definitionsWithDependencies.add(dependencyDefinition); - }); + if (dependenciesOfDefinition) { + for (const dependencyName of dependenciesOfDefinition) { + const dependencyDefinitions = definitionsByName.get(dependencyName); + if (dependencyDefinitions != null) { + for (const dependencyDefinition of dependencyDefinitions) { + definitionsWithDependencies.add(dependencyDefinition); + } + } + } } } } @@ -228,34 +238,40 @@ function visitFile( for (const line of importLines) { const { imports, from } = parseImportLine(line.replace('#', '').trim()); const importFileDefinitionMap = visitFile(from, filePath, visitedFiles, predefinedImports); - if (imports.includes('*')) { - for (const [importedDefinitionName, importedDefinitions] of importFileDefinitionMap) { - const [importedDefinitionTypeName] = importedDefinitionName.split('.'); - if (!allImportedDefinitionsMap.has(importedDefinitionTypeName)) { - allImportedDefinitionsMap.set(importedDefinitionTypeName, new Set()); - } - const allImportedDefinitions = allImportedDefinitionsMap.get(importedDefinitionTypeName); - for (const importedDefinition of importedDefinitions) { - allImportedDefinitions.add(importedDefinition); - } - } - } else { - for (let importedDefinitionName of imports) { - if (importedDefinitionName.endsWith('.*')) { - // Adding whole type means the same thing with adding every single field - importedDefinitionName = importedDefinitionName.replace('.*', ''); - } - const [importedDefinitionTypeName] = importedDefinitionName.split('.'); - if (!allImportedDefinitionsMap.has(importedDefinitionTypeName)) { - allImportedDefinitionsMap.set(importedDefinitionTypeName, new Set()); - } - const allImportedDefinitions = allImportedDefinitionsMap.get(importedDefinitionTypeName); - const importedDefinitions = importFileDefinitionMap.get(importedDefinitionName); - if (!importedDefinitions) { - throw new Error(`${importedDefinitionName} is not exported by ${from} imported by ${filePath}`); + if (importFileDefinitionMap != null) { + if (imports.includes('*')) { + for (const [importedDefinitionName, importedDefinitions] of importFileDefinitionMap) { + const [importedDefinitionTypeName] = importedDefinitionName.split('.'); + if (!allImportedDefinitionsMap.has(importedDefinitionTypeName)) { + allImportedDefinitionsMap.set(importedDefinitionTypeName, new Set()); + } + const allImportedDefinitions = allImportedDefinitionsMap.get(importedDefinitionTypeName); + if (allImportedDefinitions) { + for (const importedDefinition of importedDefinitions) { + allImportedDefinitions.add(importedDefinition); + } + } } - for (const importedDefinition of importedDefinitions) { - allImportedDefinitions.add(importedDefinition); + } else { + for (let importedDefinitionName of imports) { + if (importedDefinitionName.endsWith('.*')) { + // Adding whole type means the same thing with adding every single field + importedDefinitionName = importedDefinitionName.replace('.*', ''); + } + const [importedDefinitionTypeName] = importedDefinitionName.split('.'); + if (!allImportedDefinitionsMap.has(importedDefinitionTypeName)) { + allImportedDefinitionsMap.set(importedDefinitionTypeName, new Set()); + } + const allImportedDefinitions = allImportedDefinitionsMap.get(importedDefinitionTypeName); + const importedDefinitions = importFileDefinitionMap.get(importedDefinitionName); + if (!importedDefinitions) { + throw new Error(`${importedDefinitionName} is not exported by ${from} imported by ${filePath}`); + } + if (allImportedDefinitions != null) { + for (const importedDefinition of importedDefinitions) { + allImportedDefinitions.add(importedDefinition); + } + } } } } @@ -264,61 +280,69 @@ function visitFile( visitedFiles.set(filePath, allImportedDefinitionsMap); } else { const fileDefinitionMap = visitedFiles.get(filePath); - const addDefinition = ( - definition: DefinitionNode, - definitionName: string, - definitionSet: Set - ) => { - if (!definitionSet.has(definition)) { - definitionSet.add(definition); - // Regenerate field exports if some fields are imported after visitor - if ('fields' in definition) { - for (const field of definition.fields) { - const fieldName = field.name.value; - const fieldDefinitionName = definition.name.value + '.' + fieldName; + if (fileDefinitionMap) { + const addDefinition = ( + definition: DefinitionNode, + definitionName: string, + definitionSet: Set + ) => { + if (!definitionSet.has(definition)) { + definitionSet.add(definition); + // Regenerate field exports if some fields are imported after visitor + if ('fields' in definition && definition.fields) { + for (const field of definition.fields) { + const fieldName = field.name.value; + const fieldDefinitionName = definition.name.value + '.' + fieldName; + const allImportedDefinitions = allImportedDefinitionsMap.get(definitionName); + allImportedDefinitions?.forEach(importedDefinition => { + if (!fileDefinitionMap.has(fieldDefinitionName)) { + fileDefinitionMap.set(fieldDefinitionName, new Set()); + } + const definitionsWithDeps = fileDefinitionMap.get(fieldDefinitionName); + if (definitionsWithDeps) { + addDefinition(importedDefinition, fieldDefinitionName, definitionsWithDeps); + } + }); + const newDependencySet = new Set(); + switch (field.kind) { + case Kind.FIELD_DEFINITION: + visitFieldDefinitionNode(field, newDependencySet, dependenciesByDefinitionName); + break; + case Kind.INPUT_VALUE_DEFINITION: + visitInputValueDefinitionNode(field, newDependencySet, dependenciesByDefinitionName); + break; + } + newDependencySet.forEach(dependencyName => { + const definitionsInCurrentFile = fileDefinitionMap.get(dependencyName); + definitionsInCurrentFile?.forEach(def => addDefinition(def, definitionName, definitionSet)); + const definitionsFromImports = allImportedDefinitionsMap.get(dependencyName); + definitionsFromImports?.forEach(def => addDefinition(def, definitionName, definitionSet)); + }); + } + } + } + }; + for (const [definitionName] of definitionsByName) { + const definitionsWithDependencies = fileDefinitionMap.get(definitionName); + if (definitionsWithDependencies) { const allImportedDefinitions = allImportedDefinitionsMap.get(definitionName); allImportedDefinitions?.forEach(importedDefinition => { - if (!fileDefinitionMap.has(fieldDefinitionName)) { - fileDefinitionMap.set(fieldDefinitionName, new Set()); - } - const definitionsWithDeps = fileDefinitionMap.get(fieldDefinitionName); - addDefinition(importedDefinition, fieldDefinitionName, definitionsWithDeps); + addDefinition(importedDefinition, definitionName, definitionsWithDependencies); }); - const newDependencySet = new Set(); - switch (field.kind) { - case Kind.FIELD_DEFINITION: - visitFieldDefinitionNode(field, newDependencySet, dependenciesByDefinitionName); - break; - case Kind.INPUT_VALUE_DEFINITION: - visitInputValueDefinitionNode(field, newDependencySet, dependenciesByDefinitionName); - break; + const dependenciesOfDefinition = dependenciesByDefinitionName.get(definitionName); + if (dependenciesOfDefinition) { + for (const dependencyName of dependenciesOfDefinition) { + // If that dependency cannot be found both in imports and this file, throw an error + if (!allImportedDefinitionsMap.has(dependencyName) && !definitionsByName.has(dependencyName)) { + throw new Error(`Couldn't find type ${dependencyName} in any of the schemas.`); + } + const dependencyDefinitionsFromImports = allImportedDefinitionsMap.get(dependencyName); + dependencyDefinitionsFromImports?.forEach(dependencyDefinition => { + addDefinition(dependencyDefinition, definitionName, definitionsWithDependencies); + }); + } } - newDependencySet.forEach(dependencyName => { - const definitionsInCurrentFile = fileDefinitionMap.get(dependencyName); - definitionsInCurrentFile?.forEach(def => addDefinition(def, definitionName, definitionSet)); - const definitionsFromImports = allImportedDefinitionsMap.get(dependencyName); - definitionsFromImports?.forEach(def => addDefinition(def, definitionName, definitionSet)); - }); - } - } - } - }; - for (const [definitionName] of definitionsByName) { - const definitionsWithDependencies = fileDefinitionMap.get(definitionName); - const allImportedDefinitions = allImportedDefinitionsMap.get(definitionName); - allImportedDefinitions?.forEach(importedDefinition => { - addDefinition(importedDefinition, definitionName, definitionsWithDependencies); - }); - const dependenciesOfDefinition = dependenciesByDefinitionName.get(definitionName); - for (const dependencyName of dependenciesOfDefinition) { - // If that dependency cannot be found both in imports and this file, throw an error - if (!allImportedDefinitionsMap.has(dependencyName) && !definitionsByName.has(dependencyName)) { - throw new Error(`Couldn't find type ${dependencyName} in any of the schemas.`); } - const dependencyDefinitionsFromImports = allImportedDefinitionsMap.get(dependencyName); - dependencyDefinitionsFromImports?.forEach(dependencyDefinition => { - addDefinition(dependencyDefinition, definitionName, definitionsWithDependencies); - }); } } } @@ -327,10 +351,11 @@ function visitFile( } export function parseImportLine(importLine: string): { imports: string[]; from: string } { - if (IMPORT_FROM_REGEX.test(importLine)) { + let regexMatch = importLine.match(IMPORT_FROM_REGEX); + if (regexMatch != null) { // Apply regex to import line // Extract matches into named variables - const [, wildcard, importsString, , from] = importLine.match(IMPORT_FROM_REGEX); + const [, wildcard, importsString, , from] = regexMatch; if (from) { // Extract imported types @@ -339,12 +364,16 @@ export function parseImportLine(importLine: string): { imports: string[]; from: // Return information about the import line return { imports, from }; } - } else if (IMPORT_DEFAULT_REGEX.test(importLine)) { - const [, , from] = importLine.match(IMPORT_DEFAULT_REGEX); + } + + regexMatch = importLine.match(IMPORT_DEFAULT_REGEX); + if (regexMatch != null) { + const [, , from] = regexMatch; if (from) { return { imports: ['*'], from }; } } + throw new Error(` Import statement is not valid: > ${importLine} @@ -354,7 +383,7 @@ export function parseImportLine(importLine: string): { imports: string[]; from: `); } -function resolveFilePath(filePath: string, importFrom: string) { +function resolveFilePath(filePath: string, importFrom: string): string { const dirName = dirname(filePath); try { const fullPath = join(dirName, importFrom); @@ -363,11 +392,14 @@ function resolveFilePath(filePath: string, importFrom: string) { if (e.code === 'ENOENT') { return resolveFrom(dirName, importFrom); } + throw e; } } function visitOperationDefinitionNode(node: OperationDefinitionNode, dependencySet: Set) { - dependencySet.add(node.name.value); + if (node.name?.value) { + dependencySet.add(node.name.value); + } node.selectionSet.selections.forEach(selectionNode => visitSelectionNode(selectionNode, dependencySet)); } @@ -416,11 +448,13 @@ function visitObjectTypeDefinitionNode( node.interfaces?.forEach(namedTypeNode => { visitNamedTypeNode(namedTypeNode, dependencySet); const interfaceName = namedTypeNode.name.value; + let set = dependenciesByDefinitionName.get(interfaceName); // interface should be dependent to the type as well - if (!dependenciesByDefinitionName.has(interfaceName)) { - dependenciesByDefinitionName.set(interfaceName, new Set()); + if (set == null) { + set = new Set(); + dependenciesByDefinitionName.set(interfaceName, set); } - dependenciesByDefinitionName.get(interfaceName).add(typeName); + set.add(typeName); }); } @@ -507,18 +541,20 @@ function visitInterfaceTypeDefinitionNode( (node as any).interfaces?.forEach((namedTypeNode: NamedTypeNode) => { visitNamedTypeNode(namedTypeNode, dependencySet); const interfaceName = namedTypeNode.name.value; + let set = dependenciesByDefinitionName.get(interfaceName); // interface should be dependent to the type as well - if (!dependenciesByDefinitionName.has(interfaceName)) { - dependenciesByDefinitionName.set(interfaceName, new Set()); + if (set == null) { + set = new Set(); + dependenciesByDefinitionName.set(interfaceName, set); } - dependenciesByDefinitionName.get(interfaceName).add(typeName); + set.add(typeName); }); } function visitUnionTypeDefinitionNode(node: UnionTypeDefinitionNode, dependencySet: Set) { dependencySet.add(node.name.value); node.directives?.forEach(directiveNode => visitDirectiveNode(directiveNode, dependencySet)); - node.types.forEach(namedTypeNode => visitNamedTypeNode(namedTypeNode, dependencySet)); + node.types?.forEach(namedTypeNode => visitNamedTypeNode(namedTypeNode, dependencySet)); } function visitEnumTypeDefinitionNode(node: EnumTypeDefinitionNode, dependencySet: Set) { @@ -563,11 +599,13 @@ function visitObjectTypeExtensionNode( node.interfaces?.forEach(namedTypeNode => { visitNamedTypeNode(namedTypeNode, dependencySet); const interfaceName = namedTypeNode.name.value; + let set = dependenciesByDefinitionName.get(interfaceName); // interface should be dependent to the type as well - if (!dependenciesByDefinitionName.has(interfaceName)) { - dependenciesByDefinitionName.set(interfaceName, new Set()); + if (set == null) { + set = new Set(); + dependenciesByDefinitionName.set(interfaceName, set); } - dependenciesByDefinitionName.get(interfaceName).add(typeName); + set.add(typeName); }); } @@ -585,18 +623,20 @@ function visitInterfaceTypeExtensionNode( (node as any).interfaces?.forEach((namedTypeNode: NamedTypeNode) => { visitNamedTypeNode(namedTypeNode, dependencySet); const interfaceName = namedTypeNode.name.value; + let set = dependenciesByDefinitionName.get(interfaceName); // interface should be dependent to the type as well - if (!dependenciesByDefinitionName.has(interfaceName)) { - dependenciesByDefinitionName.set(interfaceName, new Set()); + if (set == null) { + set = new Set(); + dependenciesByDefinitionName.set(interfaceName, set); } - dependenciesByDefinitionName.get(interfaceName).add(typeName); + set.add(typeName); }); } function visitUnionTypeExtensionNode(node: UnionTypeExtensionNode, dependencySet: Set) { dependencySet.add(node.name.value); node.directives?.forEach(directiveNode => visitDirectiveNode(directiveNode, dependencySet)); - node.types.forEach(namedTypeNode => visitNamedTypeNode(namedTypeNode, dependencySet)); + node.types?.forEach(namedTypeNode => visitNamedTypeNode(namedTypeNode, dependencySet)); } function visitEnumTypeExtensionNode(node: EnumTypeExtensionNode, dependencySet: Set) { diff --git a/packages/jest-transform/src/index.ts b/packages/jest-transform/src/index.ts index d2d143554f4..0c071c1a648 100644 --- a/packages/jest-transform/src/index.ts +++ b/packages/jest-transform/src/index.ts @@ -21,7 +21,7 @@ class GraphQLTransformer implements SyncTransformer { { cacheable() {}, query: config, - }, + } as any, input ); } diff --git a/packages/links/src/linkToSubscriber.ts b/packages/links/src/linkToSubscriber.ts index 0b66c661b1c..ddd228872f7 100644 --- a/packages/links/src/linkToSubscriber.ts +++ b/packages/links/src/linkToSubscriber.ts @@ -3,20 +3,22 @@ import { Observable } from '@apollo/client/utilities'; import { Subscriber, ExecutionParams, ExecutionResult, observableToAsyncIterable } from '@graphql-tools/utils'; -export const linkToSubscriber = (link: ApolloLink): Subscriber => async ( - params: ExecutionParams -): Promise | AsyncIterator>> => { - const { document, variables, extensions, context, info } = params; - return observableToAsyncIterable>( - execute(link, { - query: document, - variables, - context: { - graphqlContext: context, - graphqlResolveInfo: info, - clientAwareness: {}, - }, - extensions, - }) as Observable> - )[Symbol.asyncIterator](); -}; +export const linkToSubscriber = + (link: ApolloLink): Subscriber => + async ( + params: ExecutionParams + ): Promise | AsyncIterableIterator>> => { + const { document, variables, extensions, context, info } = params; + return observableToAsyncIterable>( + execute(link, { + query: document, + variables, + context: { + graphqlContext: context, + graphqlResolveInfo: info, + clientAwareness: {}, + }, + extensions, + }) as Observable> + )[Symbol.asyncIterator](); + }; diff --git a/packages/load-files/src/index.ts b/packages/load-files/src/index.ts index 51086ee71bc..d6ea2da95e0 100644 --- a/packages/load-files/src/index.ts +++ b/packages/load-files/src/index.ts @@ -8,29 +8,31 @@ const { readFile, stat } = fsPromises; const DEFAULT_IGNORED_EXTENSIONS = ['spec', 'test', 'd', 'map']; const DEFAULT_EXTENSIONS = ['gql', 'graphql', 'graphqls', 'ts', 'js']; const DEFAULT_EXPORT_NAMES = ['schema', 'typeDef', 'typeDefs', 'resolver', 'resolvers']; -const DEFAULT_EXTRACT_EXPORTS_FACTORY = (exportNames: string[]) => (fileExport: any): any | null => { - if (!fileExport) { - return null; - } +const DEFAULT_EXTRACT_EXPORTS_FACTORY = + (exportNames: string[]) => + (fileExport: any): any | null => { + if (!fileExport) { + return null; + } - if (fileExport.default) { - for (const exportName of exportNames) { - if (fileExport.default[exportName]) { - return fileExport.default[exportName]; + if (fileExport.default) { + for (const exportName of exportNames) { + if (fileExport.default[exportName]) { + return fileExport.default[exportName]; + } } - } - return fileExport.default; - } + return fileExport.default; + } - for (const exportName of exportNames) { - if (fileExport[exportName]) { - return fileExport[exportName]; + for (const exportName of exportNames) { + if (fileExport[exportName]) { + return fileExport[exportName]; + } } - } - return fileExport; -}; + return fileExport; + }; function asArray(obj: T | T[]): T[] { if (obj instanceof Array) { @@ -68,9 +70,9 @@ function formatExtension(extension: string): string { function buildGlob( basePath: string, - extensions: string[], + extensions: string[] = [], ignoredExtensions: string[] = [], - recursive: boolean + recursive?: boolean ): string { const ignored = ignoredExtensions.length > 0 ? `!(${ignoredExtensions.map(e => `*${formatExtension(e)}`).join('|')})` : '*'; @@ -135,7 +137,7 @@ export function loadFilesSync( options.globOptions ); - const extractExports = execOptions.extractExports || DEFAULT_EXTRACT_EXPORTS_FACTORY(execOptions.exportNames); + const extractExports = execOptions.extractExports || DEFAULT_EXTRACT_EXPORTS_FACTORY(execOptions.exportNames ?? []); const requireMethod = execOptions.requireMethod || require; return relevantPaths @@ -220,7 +222,7 @@ export async function loadFiles( options.globOptions ); - const extractExports = execOptions.extractExports || DEFAULT_EXTRACT_EXPORTS_FACTORY(execOptions.exportNames); + const extractExports = execOptions.extractExports || DEFAULT_EXTRACT_EXPORTS_FACTORY(execOptions.exportNames ?? []); const defaultRequireMethod = (path: string) => import(path).catch(async () => require(path)); const requireMethod = execOptions.requireMethod || defaultRequireMethod; diff --git a/packages/load/src/documents.ts b/packages/load/src/documents.ts index d30e508d939..46b9f88e402 100644 --- a/packages/load/src/documents.ts +++ b/packages/load/src/documents.ts @@ -2,16 +2,18 @@ import { Source } from '@graphql-tools/utils'; import { Kind } from 'graphql'; import { LoadTypedefsOptions, loadTypedefs, loadTypedefsSync, UnnormalizedTypeDefPointer } from './load-typedefs'; +type KindList = Array; + /** * Kinds of AST nodes that are included in executable documents */ -export const OPERATION_KINDS = [Kind.OPERATION_DEFINITION, Kind.FRAGMENT_DEFINITION]; +export const OPERATION_KINDS: KindList = [Kind.OPERATION_DEFINITION, Kind.FRAGMENT_DEFINITION]; /** * Kinds of AST nodes that are included in type system definition documents */ export const NON_OPERATION_KINDS = Object.keys(Kind) - .reduce((prev, v) => [...prev, Kind[v]], []) + .reduce((prev, v) => [...prev, Kind[v]], [] as KindList) .filter(v => !OPERATION_KINDS.includes(v)); /** diff --git a/packages/load/src/filter-document-kind.ts b/packages/load/src/filter-document-kind.ts index 21fd1f715a3..0798ff35254 100644 --- a/packages/load/src/filter-document-kind.ts +++ b/packages/load/src/filter-document-kind.ts @@ -4,7 +4,7 @@ import { DocumentNode, DefinitionNode, Kind } from 'graphql'; /** * @internal */ -export const filterKind = (content: DocumentNode, filterKinds: null | string[]) => { +export const filterKind = (content: DocumentNode | undefined, filterKinds: null | string[]) => { if (content && content.definitions && content.definitions.length && filterKinds && filterKinds.length > 0) { const invalidDefinitions: DefinitionNode[] = []; const validDefinitions: DefinitionNode[] = []; diff --git a/packages/load/src/load-typedefs/collect-sources.ts b/packages/load/src/load-typedefs/collect-sources.ts index ebdba0b69b8..1bdd8e02d6e 100644 --- a/packages/load/src/load-typedefs/collect-sources.ts +++ b/packages/load/src/load-typedefs/collect-sources.ts @@ -6,6 +6,7 @@ import { getDocumentNodeFromSchema, Loader, ResolverGlobs, + isSome, } from '@graphql-tools/utils'; import { isSchema, Kind } from 'graphql'; import isGlob from 'is-glob'; @@ -53,7 +54,7 @@ export async function collectSources({ options, addSource, addGlob, - queue: queue.add, + queue: queue.add as AddToQueue, }); } @@ -67,7 +68,7 @@ export async function collectSources({ globOptions, pointerOptionMap, addSource, - queue: queue.add, + queue: queue.add as AddToQueue, }); } @@ -154,7 +155,7 @@ function createHelpers({ }) => { sources.push(source); - if (!noCache) { + if (!noCache && options.cache) { options.cache[pointer] = source; } }; @@ -195,10 +196,12 @@ async function addGlobsToLoaders({ if (!loader) { throw new Error(`unable to find loader for glob "${glob}"`); } - if (!loadersForGlobs.has(loader)) { - loadersForGlobs.set(loader, { globs: [], ignores: [] }); + let resolverGlobs = loadersForGlobs.get(loader); + if (!isSome(resolverGlobs)) { + resolverGlobs = { globs: [], ignores: [] }; + loadersForGlobs.set(loader, resolverGlobs); } - loadersForGlobs.get(loader)[type].push(glob); + resolverGlobs[type].push(glob); } } @@ -216,7 +219,11 @@ function addGlobsToLoadersSync({ for (const glob of globs) { let loader; for (const candidateLoader of options.loaders) { - if (candidateLoader.resolveGlobs && candidateLoader.canLoadSync(glob, options)) { + if ( + isSome(candidateLoader.resolveGlobsSync) && + isSome(candidateLoader.canLoadSync) && + candidateLoader.canLoadSync(glob, options) + ) { loader = candidateLoader; break; } @@ -224,10 +231,12 @@ function addGlobsToLoadersSync({ if (!loader) { throw new Error(`unable to find loader for glob "${glob}"`); } - if (!loadersForGlobs.has(loader)) { - loadersForGlobs.set(loader, { globs: [], ignores: [] }); + let resolverGlobs = loadersForGlobs.get(loader); + if (!isSome(resolverGlobs)) { + resolverGlobs = { globs: [], ignores: [] }; + loadersForGlobs.set(loader, resolverGlobs); } - loadersForGlobs.get(loader)[type].push(glob); + resolverGlobs[type].push(glob); } } @@ -237,12 +246,19 @@ async function collectPathsFromGlobs(globs: string[], options: LoadTypedefsOptio const loadersForGlobs: Map = new Map(); await addGlobsToLoaders({ options, loadersForGlobs, globs, type: 'globs' }); - await addGlobsToLoaders({ options, loadersForGlobs, globs: asArray(options.ignore), type: 'ignores' }); + await addGlobsToLoaders({ + options, + loadersForGlobs, + globs: isSome(options.ignore) ? asArray(options.ignore) : [], + type: 'ignores', + }); for await (const [loader, globsAndIgnores] of loadersForGlobs.entries()) { - const resolvedPaths = await loader.resolveGlobs(globsAndIgnores, options); - if (resolvedPaths) { - paths.push(...resolvedPaths); + if (isSome(loader.resolveGlobs)) { + const resolvedPaths = await loader.resolveGlobs(globsAndIgnores, options); + if (resolvedPaths) { + paths.push(...resolvedPaths); + } } } @@ -255,12 +271,19 @@ function collectPathsFromGlobsSync(globs: string[], options: LoadTypedefsOptions const loadersForGlobs: Map = new Map(); addGlobsToLoadersSync({ options, loadersForGlobs, globs, type: 'globs' }); - addGlobsToLoadersSync({ options, loadersForGlobs, globs: asArray(options.ignore), type: 'ignores' }); + addGlobsToLoadersSync({ + options, + loadersForGlobs, + globs: isSome(options.ignore) ? asArray(options.ignore) : [], + type: 'ignores', + }); for (const [loader, globsAndIgnores] of loadersForGlobs.entries()) { - const resolvedPaths = loader.resolveGlobsSync(globsAndIgnores, options); - if (resolvedPaths) { - paths.push(...resolvedPaths); + if (isSome(loader.resolveGlobsSync)) { + const resolvedPaths = loader.resolveGlobsSync(globsAndIgnores, options); + if (resolvedPaths) { + paths.push(...resolvedPaths); + } } } @@ -421,6 +444,8 @@ function collectCustomLoader( ) { if (pointerOptions.loader) { return queue(async () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore TODO options.cwd is possibly undefined, but it seems like no test covers this path const loader = await useCustomLoader(pointerOptions.loader, options.cwd); const result = await loader(pointer, { ...options, ...pointerOptions }, pointerOptionMap); @@ -441,6 +466,8 @@ function collectCustomLoaderSync( ) { if (pointerOptions.loader) { return queue(() => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore TODO options.cwd is possibly undefined, but it seems like no test covers this path const loader = useCustomLoaderSync(pointerOptions.loader, options.cwd); const result = loader(pointer, { ...options, ...pointerOptions }, pointerOptionMap); diff --git a/packages/load/src/load-typedefs/load-file.ts b/packages/load/src/load-typedefs/load-file.ts index 9bca990a6d5..ef8efadfbc9 100644 --- a/packages/load/src/load-typedefs/load-file.ts +++ b/packages/load/src/load-typedefs/load-file.ts @@ -1,7 +1,7 @@ -import { Source, debugLog } from '@graphql-tools/utils'; +import { Source, debugLog, Maybe } from '@graphql-tools/utils'; import { LoadTypedefsOptions } from '../load-typedefs'; -export async function loadFile(pointer: string, options: LoadTypedefsOptions): Promise { +export async function loadFile(pointer: string, options: LoadTypedefsOptions): Promise> { const cached = useCache({ pointer, options }); if (cached) { @@ -25,7 +25,7 @@ export async function loadFile(pointer: string, options: LoadTypedefsOptions): P return undefined; } -export function loadFileSync(pointer: string, options: LoadTypedefsOptions): Source { +export function loadFileSync(pointer: string, options: LoadTypedefsOptions): Maybe { const cached = useCache({ pointer, options }); if (cached) { @@ -37,7 +37,8 @@ export function loadFileSync(pointer: string, options: LoadTypedefsOptions): Sou const canLoad = loader.canLoadSync && loader.loadSync && loader.canLoadSync(pointer, options); if (canLoad) { - return loader.loadSync(pointer, options); + // We check for the existence so it is okay to force non null + return loader.loadSync!(pointer, options); } } catch (error) { debugLog(`Failed to find any GraphQL type definitions in: ${pointer} - ${error.message}`); diff --git a/packages/load/src/load-typedefs/parse.ts b/packages/load/src/load-typedefs/parse.ts index 3bbf431a0c7..87ae377be06 100644 --- a/packages/load/src/load-typedefs/parse.ts +++ b/packages/load/src/load-typedefs/parse.ts @@ -9,7 +9,7 @@ type Input = { }; type AddValidSource = (source: Source) => void; type ParseOptions = { - partialSource: Partial; + partialSource: Source; options: any; globOptions: any; pointerOptionMap: any; @@ -44,16 +44,22 @@ function prepareInput({ globOptions, pointerOptionMap, }: { - source: Partial; + source: Source; options: any; globOptions: any; pointerOptionMap: any; }): Input { - const specificOptions = { + let specificOptions = { ...options, - ...(source.location in pointerOptionMap ? globOptions : pointerOptionMap[source.location]), }; + if (source.location) { + specificOptions = { + ...specificOptions, + ...(source.location in pointerOptionMap ? globOptions : pointerOptionMap[source.location]), + }; + } + return { source: { ...source }, options: specificOptions }; } @@ -77,14 +83,14 @@ function useKindsFilter(input: Input) { } function useComments(input: Input) { - if (!input.source.rawSDL) { + if (!input.source.rawSDL && input.source.document) { input.source.rawSDL = printWithComments(input.source.document); resetComments(); } } function collectValidSources(input: Input, addValidSource: AddValidSource) { - if (input.source.document.definitions && input.source.document.definitions.length > 0) { + if (input.source.document?.definitions && input.source.document.definitions.length > 0) { addValidSource(input.source); } } diff --git a/packages/load/src/schema.ts b/packages/load/src/schema.ts index cdc9d61f5eb..f18a3ea340b 100644 --- a/packages/load/src/schema.ts +++ b/packages/load/src/schema.ts @@ -76,11 +76,17 @@ export function loadSchemaSync( } function includeSources(schema: GraphQLSchema, sources: Source[]) { + const finalSources: Array = []; + for (const source of sources) { + if (source.rawSDL) { + finalSources.push(new GraphQLSource(source.rawSDL, source.location)); + } else if (source.document) { + finalSources.push(new GraphQLSource(print(source.document), source.location)); + } + } schema.extensions = { ...schema.extensions, - sources: sources - .filter(source => source.rawSDL || source.document) - .map(source => new GraphQLSource(source.rawSDL || print(source.document), source.location)), + sources: finalSources, }; } @@ -88,13 +94,13 @@ function collectSchemasAndTypeDefs(sources: Source[]) { const schemas: GraphQLSchema[] = []; const typeDefs: DocumentNode[] = []; - sources.forEach(source => { + for (const source of sources) { if (source.schema) { schemas.push(source.schema); - } else { + } else if (source.document) { typeDefs.push(source.document); } - }); + } return { schemas, diff --git a/packages/load/tests/loaders/schema/schema-from-export.spec.ts b/packages/load/tests/loaders/schema/schema-from-export.spec.ts index 80ca06e9bbf..0017c8ddf2f 100644 --- a/packages/load/tests/loaders/schema/schema-from-export.spec.ts +++ b/packages/load/tests/loaders/schema/schema-from-export.spec.ts @@ -7,6 +7,13 @@ const monorepo = useMonorepo({ dirname: __dirname }); +function assertNonMaybe(input: T): asserts input is Exclude{ + if (input == null) { + throw new Error("Value should be neither null nor undefined.") + } +} + + describe('Schema From Export', () => { monorepo.correctCWD(); @@ -26,7 +33,9 @@ describe('Schema From Export', () => { loaders: [new CodeFileLoader()] }); expect(isSchema(result)).toBeTruthy(); - expect(result.getQueryType().getFields().hello).toBeDefined(); + const QueryType = result.getQueryType() + assertNonMaybe(QueryType) + expect(QueryType.getFields().hello).toBeDefined(); }); test('should load the schema correctly from variable export', async () => { @@ -64,7 +73,9 @@ describe('Schema From Export', () => { const schema = await load(schemaPath, { loaders: [new CodeFileLoader()] }); - const queryFields = Object.keys(schema.getQueryType().getFields()); + const QueryType = schema.getQueryType() + assertNonMaybe(QueryType) + const queryFields = Object.keys(QueryType.getFields()); expect(queryFields).toContain('foo'); expect(queryFields).toContain('bar'); @@ -75,7 +86,9 @@ describe('Schema From Export', () => { const schema = await load(schemaPath, { loaders: [new CodeFileLoader()] }); - const queryFields = Object.keys(schema.getQueryType().getFields()); + const QueryType = schema.getQueryType() + assertNonMaybe(QueryType) + const queryFields = Object.keys(QueryType.getFields()); expect(queryFields).toContain('foo'); expect(queryFields).toContain('bar'); diff --git a/packages/load/tests/loaders/schema/schema-from-json.spec.ts b/packages/load/tests/loaders/schema/schema-from-json.spec.ts index 6785c6576c3..55ecbc63278 100644 --- a/packages/load/tests/loaders/schema/schema-from-json.spec.ts +++ b/packages/load/tests/loaders/schema/schema-from-json.spec.ts @@ -7,6 +7,12 @@ const monorepo = useMonorepo({ dirname: __dirname, }); +function assertNonMaybe(input: T): asserts input is Exclude{ + if (input == null) { + throw new Error("Value should be neither null nor undefined.") + } +} + describe('Schema From Export', () => { monorepo.correctCWD(); @@ -29,6 +35,7 @@ describe('Schema From Export', () => { for (const typeName in schema.getTypeMap()) { if (!typeName.startsWith('__')) { const type = schema.getType(typeName); + assertNonMaybe(type) const introspectionType = introspectionSchema.types.find((t: { name: string; }) => t.name === typeName); if (type.description || introspectionType.description) { expect(type.description).toBe(introspectionType.description); @@ -39,6 +46,7 @@ describe('Schema From Export', () => { const field = fieldMap[fieldName]; const introspectionField = introspectionType.fields.find((f: { name: string; }) => f.name === fieldName); if (field.description || introspectionField.description) { + assertNonMaybe(field.description) expect(field.description.trim()).toBe(introspectionField.description.trim()); } } diff --git a/packages/load/tests/loaders/schema/schema-from-typedefs.spec.ts b/packages/load/tests/loaders/schema/schema-from-typedefs.spec.ts index 54a5ce7227f..b35d89ce1c9 100644 --- a/packages/load/tests/loaders/schema/schema-from-typedefs.spec.ts +++ b/packages/load/tests/loaders/schema/schema-from-typedefs.spec.ts @@ -7,6 +7,12 @@ const monorepo = useMonorepo({ dirname: __dirname }); +function assertNonMaybe(input: T): asserts input is Exclude{ + if (input == null) { + throw new Error("Value should be neither null nor undefined.") + } +} + describe('schema from typedefs', () => { monorepo.correctCWD(); @@ -101,7 +107,9 @@ describe('schema from typedefs', () => { const schema = await load(schemaPath, { loaders: [new GraphQLFileLoader()] }); - const queryFields = Object.keys(schema.getQueryType().getFields()); + const QueryType = schema.getQueryType() + assertNonMaybe(QueryType) + const queryFields = Object.keys(QueryType.getFields()); expect(queryFields).toContain('foo'); expect(queryFields).toContain('bar'); @@ -112,7 +120,9 @@ describe('schema from typedefs', () => { const schema = await load(schemaPath, { loaders: [new GraphQLFileLoader()] }); - const queryFields = Object.keys(schema.getQueryType().getFields()); + const QueryType = schema.getQueryType() + assertNonMaybe(QueryType) + const queryFields = Object.keys(QueryType.getFields()); expect(queryFields).toContain('foo'); expect(queryFields).toContain('bar'); @@ -124,7 +134,9 @@ describe('schema from typedefs', () => { const schema = await load(schemaPath, { loaders: [new CodeFileLoader()] }); - const queryFields = Object.keys(schema.getQueryType().getFields()); + const QueryType = schema.getQueryType() + assertNonMaybe(QueryType) + const queryFields = Object.keys(QueryType.getFields()); expect(queryFields).toContain('foo'); expect(queryFields).toContain('bar'); @@ -136,7 +148,7 @@ describe('schema from typedefs', () => { loaders: [new GraphQLFileLoader()], includeSources: true, }); - + assertNonMaybe(schemaWithSources.extensions) expect(schemaWithSources.extensions.sources).toBeDefined(); expect(schemaWithSources.extensions.sources).toHaveLength(1); expect(schemaWithSources.extensions.sources[0]).toMatchObject(expect.objectContaining({ @@ -146,7 +158,7 @@ describe('schema from typedefs', () => { const schemaWithoutSources = await load(glob, { loaders: [new GraphQLFileLoader()] }); - + assertNonMaybe(schemaWithoutSources.extensions) expect(schemaWithoutSources.extensions.sources).not.toBeDefined(); }); }) diff --git a/packages/loaders/code-file/src/index.ts b/packages/loaders/code-file/src/index.ts index 9ac6f0c9845..37f042e916e 100644 --- a/packages/loaders/code-file/src/index.ts +++ b/packages/loaders/code-file/src/index.ts @@ -118,7 +118,10 @@ export class CodeFileLoader implements UniversalLoader { ); } - async load(pointer: SchemaPointerSingle | DocumentPointerSingle, options: CodeFileLoaderOptions): Promise { + async load( + pointer: SchemaPointerSingle | DocumentPointerSingle, + options: CodeFileLoaderOptions + ): Promise { const normalizedFilePath = ensureAbsolutePath(pointer, options); const errors: Error[] = []; @@ -161,7 +164,7 @@ export class CodeFileLoader implements UniversalLoader { return null; } - loadSync(pointer: SchemaPointerSingle | DocumentPointerSingle, options: CodeFileLoaderOptions): Source { + loadSync(pointer: SchemaPointerSingle | DocumentPointerSingle, options: CodeFileLoaderOptions): Source | null { const normalizedFilePath = ensureAbsolutePath(pointer, options); const errors: Error[] = []; @@ -207,7 +210,7 @@ export class CodeFileLoader implements UniversalLoader { function resolveSource( pointer: string, - value: GraphQLSchema | DocumentNode | string, + value: GraphQLSchema | DocumentNode | string | null, options: CodeFileLoaderOptions ): Source | null { if (typeof value === 'string') { diff --git a/packages/loaders/code-file/src/load-from-module.ts b/packages/loaders/code-file/src/load-from-module.ts index b5707f6cae3..b38ccca00e1 100644 --- a/packages/loaders/code-file/src/load-from-module.ts +++ b/packages/loaders/code-file/src/load-from-module.ts @@ -4,7 +4,7 @@ import { pickExportFromModule, pickExportFromModuleSync } from './exports'; /** * @internal */ -export async function tryToLoadFromExport(rawFilePath: string): Promise { +export async function tryToLoadFromExport(rawFilePath: string): Promise { try { const filepath = ensureFilepath(rawFilePath); @@ -19,7 +19,7 @@ export async function tryToLoadFromExport(rawFilePath: string): Promise(input: T): asserts input is Exclude{ + if (input == null) { + throw new Error("Value should be neither null nor undefined.") + } +} + test('load schema from GitHub', async () => { - let headers: Record = {}; - let query: string; - let variables: any; - let operationName: string; + let params: any = null; const server = nock('https://api.github.com').post('/graphql').reply(function reply(_, body: any) { - headers = this.req.headers; - query = body.query; - variables = body.variables; - operationName = body.operationName; + params = { + headers: this.req.headers, + query: body.query, + variables: body.variables, + operationName: body.operationName + } return [200, { data: { @@ -58,13 +63,15 @@ test('load schema from GitHub', async () => { server.done(); + assertNonMaybe(params); + // headers - expect(headers['content-type']).toContain('application/json; charset=utf-8'); - expect(headers.authorization).toContain(`bearer ${token}`); + expect(params.headers['content-type']).toContain('application/json; charset=utf-8'); + expect(params.headers.authorization).toContain(`bearer ${token}`); // query - expect(normalize(query)).toEqual( - normalize(` + expect(normalize(params.query)).toEqual( + normalize(/* GraphQL */` query GetGraphQLSchemaForGraphQLtools($owner: String!, $name: String!, $expression: String!) { repository(owner: $owner, name: $name) { object(expression: $expression) { @@ -78,15 +85,16 @@ test('load schema from GitHub', async () => { ); // variables - expect(variables).toEqual({ + expect(params.variables).toEqual({ owner, name, expression: ref + ':' + path, }); - + assertNonMaybe(params.operationName) // name - expect(operationName).toEqual('GetGraphQLSchemaForGraphQLtools'); + expect(params.operationName).toEqual('GetGraphQLSchemaForGraphQLtools'); + assertNonMaybe(schema.document) // schema expect(print(schema.document)).toEqual(printSchema(buildSchema(typeDefs))); }); diff --git a/packages/loaders/module/tests/loader.spec.ts b/packages/loaders/module/tests/loader.spec.ts index d26ff625afa..b5b821dfd90 100644 --- a/packages/loaders/module/tests/loader.spec.ts +++ b/packages/loaders/module/tests/loader.spec.ts @@ -24,15 +24,15 @@ describe('ModuleLoader', () => { sync: loader.canLoadSync.bind(loader), })(canLoad => { it('should return true for a valid pointer', async () => { - await expect(canLoad(getPointer('schema'), {})).resolves.toBe(true); + await expect(canLoad(getPointer('schema'))).resolves.toBe(true); }); it('should return false if missing prefix', async () => { - await expect(canLoad(getPointer('schema').substring(7), {})).resolves.toBe(false); + await expect(canLoad(getPointer('schema').substring(7))).resolves.toBe(false); }); it('should return false if pointer is not a string', async () => { - await expect(canLoad(42 as any, {})).resolves.toBe(false); + await expect(canLoad(42 as any)).resolves.toBe(false); }); }); }); @@ -43,39 +43,39 @@ describe('ModuleLoader', () => { sync: loader.loadSync.bind(loader), })(load => { it('should load GraphQLSchema object from a file', async () => { - const result: Source = await load(getPointer('schema'), {}); + const result: Source = await load(getPointer('schema')); expect(result.schema).toBeDefined(); }); it('should load DocumentNode object from a file', async () => { - const result: Source = await load(getPointer('type-defs'), {}); + const result: Source = await load(getPointer('type-defs')); expect(result.document).toBeDefined(); }); it('should load string from a file', async () => { - const result: Source = await load(getPointer('type-defs-string'), {}); + const result: Source = await load(getPointer('type-defs-string')); expect(result.rawSDL).toBeDefined(); }); it('should load using a named export', async () => { - const result: Source = await load(getPointer('type-defs-named-export', 'typeDefs'), {}); + const result: Source = await load(getPointer('type-defs-named-export', 'typeDefs')); expect(result.document).toBeDefined(); }); it('should throw error when using a bad pointer', async () => { - await expect(load(getPointer('type-defs-named-export', 'tooMany#'), {})).rejects.toThrowError( + await expect(load(getPointer('type-defs-named-export', 'tooMany#'))).rejects.toThrowError( 'Schema pointer should match' ); }); it('should throw error when using a bad identifier', async () => { - await expect(load(getPointer('type-defs-named-export', 'badIdentifier'), {})).rejects.toThrowError( + await expect(load(getPointer('type-defs-named-export', 'badIdentifier'))).rejects.toThrowError( 'Unable to load schema from module' ); }); it('should throw error when loaded object is not GraphQLSchema, DocumentNode or string', async () => { - await expect(load(getPointer('type-defs-named-export', 'favoriteNumber'), {})).rejects.toThrowError( + await expect(load(getPointer('type-defs-named-export', 'favoriteNumber'))).rejects.toThrowError( 'Imported object was not a string, DocumentNode or GraphQLSchema' ); }); diff --git a/packages/loaders/prisma/src/prisma-yml/Cluster.ts b/packages/loaders/prisma/src/prisma-yml/Cluster.ts index f74bf8ce058..626cb9b734b 100644 --- a/packages/loaders/prisma/src/prisma-yml/Cluster.ts +++ b/packages/loaders/prisma/src/prisma-yml/Cluster.ts @@ -13,7 +13,7 @@ export class Cluster { local: boolean; shared: boolean; clusterSecret?: string; - requiresAuth: boolean; + requiresAuth: boolean | undefined; out: IOutput; isPrivate: boolean; workspaceSlug?: string; @@ -58,10 +58,10 @@ export class Cluster { if (this.name === 'shared-public-demo') { return ''; } - if (this.isPrivate && process.env.PRISMA_MANAGEMENT_API_SECRET) { + if (this.isPrivate && process.env['PRISMA_MANAGEMENT_API_SECRET']) { return this.getLocalToken(); } - if (this.shared || (this.isPrivate && !process.env.PRISMA_MANAGEMENT_API_SECRET)) { + if (this.shared || (this.isPrivate && !process.env['PRISMA_MANAGEMENT_API_SECRET'])) { return this.generateClusterToken(serviceName, workspaceSlug, stageName); } else { return this.getLocalToken(); @@ -69,24 +69,30 @@ export class Cluster { } getLocalToken(): string | null { - if (!this.clusterSecret && !process.env.PRISMA_MANAGEMENT_API_SECRET) { + if (!this.clusterSecret && !process.env['PRISMA_MANAGEMENT_API_SECRET']) { return null; } if (!this.cachedToken) { const grants = [{ target: `*/*`, action: '*' }]; - const secret = process.env.PRISMA_MANAGEMENT_API_SECRET || this.clusterSecret; + const secret = process.env['PRISMA_MANAGEMENT_API_SECRET'] || this.clusterSecret; + + if (!secret) { + throw new Error( + `Could not generate token for cluster ${chalk.bold( + this.getDeployEndpoint() + )}. Did you provide the env var PRISMA_MANAGEMENT_API_SECRET?` + ); + } try { - const algorithm = process.env.PRISMA_MANAGEMENT_API_SECRET ? 'HS256' : 'RS256'; + const algorithm = process.env['PRISMA_MANAGEMENT_API_SECRET'] ? 'HS256' : 'RS256'; this.cachedToken = jwt.sign({ grants }, secret, { expiresIn: '5y', algorithm, }); } catch (e) { throw new Error( - `Could not generate token for cluster ${chalk.bold( - this.getDeployEndpoint() - )}. Did you provide the env var PRISMA_MANAGEMENT_API_SECRET? + `Could not generate token for cluster ${chalk.bold(this.getDeployEndpoint())}. Original error: ${e.message}` ); } diff --git a/packages/loaders/prisma/src/prisma-yml/Environment.test.ts b/packages/loaders/prisma/src/prisma-yml/Environment.test.ts index c9195bea387..7abb6fb1714 100644 --- a/packages/loaders/prisma/src/prisma-yml/Environment.test.ts +++ b/packages/loaders/prisma/src/prisma-yml/Environment.test.ts @@ -37,7 +37,7 @@ describe('Environment', () => { expect(env.clusters).toMatchSnapshot(); }); test('interpolates env vars', async () => { - process.env.SPECIAL_TEST_ENV_VAR = 'this-is-so-special'; + process.env['SPECIAL_TEST_ENV_VAR'] = 'this-is-so-special'; const env = makeEnv(`platformToken: \${env:SPECIAL_TEST_ENV_VAR}`); await env.load(); expect(env.clusters).toMatchSnapshot(); diff --git a/packages/loaders/prisma/src/prisma-yml/Environment.ts b/packages/loaders/prisma/src/prisma-yml/Environment.ts index a5dd773ee5a..7bb138edfb3 100644 --- a/packages/loaders/prisma/src/prisma-yml/Environment.ts +++ b/packages/loaders/prisma/src/prisma-yml/Environment.ts @@ -14,15 +14,16 @@ import { getProxyAgent } from './utils/getProxyAgent'; // eslint-disable-next-line // @ts-ignore import * as jwt from 'jsonwebtoken'; +import { assertSome } from '@graphql-tools/utils'; const debug = require('debug')('Environment'); export class Environment { sharedClusters: string[] = ['prisma-eu1', 'prisma-us1']; clusterEndpointMap = clusterEndpointMap; - args: Args; - activeCluster: Cluster; + args: Args | undefined; + activeCluster: Cluster | undefined; globalRC: RC = {}; - clusters: Cluster[]; + clusters: Cluster[] | undefined; out: IOutput; home: string; rcPath: string; @@ -37,12 +38,17 @@ export class Environment { fs.mkdirSync(path.dirname(this.rcPath), { recursive: true }); } + private _getClusters() { + assertSome(this.clusters); + return this.clusters; + } + async load() { await this.loadGlobalRC(); } get cloudSessionKey(): string | undefined { - return process.env.PRISMA_CLOUD_SESSION_KEY || this.globalRC.cloudSessionKey; + return process.env['PRISMA_CLOUD_SESSION_KEY'] || this.globalRC.cloudSessionKey; } async renewToken() { @@ -105,7 +111,7 @@ export class Environment { } if (res.me && res.me.memberships && Array.isArray(res.me.memberships)) { // clean up all prisma-eu1 and prisma-us1 clusters if they already exist - this.clusters = this.clusters.filter(c => c.name !== 'prisma-eu1' && c.name !== 'prisma-us1'); + this.clusters = this._getClusters().filter(c => c.name !== 'prisma-eu1' && c.name !== 'prisma-us1'); res.me.memberships.forEach((m: any) => { m.workspace.clusters.forEach((cluster: any) => { @@ -160,7 +166,8 @@ export class Environment { } addCluster(cluster: Cluster) { - const existingClusterIndex = this.clusters.findIndex(c => { + const clusters = this._getClusters(); + const existingClusterIndex = clusters.findIndex(c => { if (cluster.workspaceSlug) { return c.workspaceSlug === cluster.workspaceSlug && c.name === cluster.name; } else { @@ -168,13 +175,13 @@ export class Environment { } }); if (existingClusterIndex > -1) { - this.clusters.splice(existingClusterIndex, 1); + clusters.splice(existingClusterIndex, 1); } - this.clusters.push(cluster); + clusters.push(cluster); } removeCluster(name: string) { - this.clusters = this.clusters.filter(c => c.name !== name); + this.clusters = this._getClusters().filter(c => c.name !== name); } saveGlobalRC() { @@ -248,7 +255,7 @@ export class Environment { } private getLocalClusterConfig() { - return this.clusters + return this._getClusters() .filter(c => !c.shared && c.clusterSecret !== this.cloudSessionKey && !c.isPrivate) .reduce((acc, cluster) => { return { diff --git a/packages/loaders/prisma/src/prisma-yml/PrismaDefinition.test.ts b/packages/loaders/prisma/src/prisma-yml/PrismaDefinition.test.ts index e6a6db64c19..45ebc6f3c30 100644 --- a/packages/loaders/prisma/src/prisma-yml/PrismaDefinition.test.ts +++ b/packages/loaders/prisma/src/prisma-yml/PrismaDefinition.test.ts @@ -77,7 +77,7 @@ type User @model { }); test('load yml with secret and env var', async () => { const secret = 'this-is-a-long-secret'; - process.env.MY_TEST_SECRET = secret; + process.env['MY_TEST_SECRET'] = secret; const yml = `\ service: jj stage: dev @@ -127,7 +127,7 @@ type User @model { } `; const { definition, env } = makeDefinition(yml, datamodel, {}); - const envPath = path.join(definition.definitionDir, '.env'); + const envPath = path.join(definition.definitionDir!, '.env'); fs.mkdirSync(path.dirname(envPath), { recursive: true }); fs.writeFileSync(envPath, `MY_DOT_ENV_SECRET=this-is-very-secret,and-comma,seperated`); @@ -241,7 +241,7 @@ type User @model { } `; const { definition, env } = makeDefinition(yml, datamodel); - const envPath = path.join(definition.definitionDir, '.env'); + const envPath = path.join(definition.definitionDir!, '.env'); fs.mkdirSync(path.dirname(envPath), { recursive: true }); fs.writeFileSync(envPath, `MY_DOT_ENV_SECRET=this-is-very-secret,and-comma,seperated`); diff --git a/packages/loaders/prisma/src/prisma-yml/PrismaDefinition.ts b/packages/loaders/prisma/src/prisma-yml/PrismaDefinition.ts index f0ff636bf3b..71d11941b47 100644 --- a/packages/loaders/prisma/src/prisma-yml/PrismaDefinition.ts +++ b/packages/loaders/prisma/src/prisma-yml/PrismaDefinition.ts @@ -27,12 +27,12 @@ export class PrismaDefinitionClass { typesString?: string; secrets: string[] | null; definitionPath?: string | null; - definitionDir: string; + definitionDir: string | undefined; env: Environment; out?: IOutput; envVars: any; rawEndpoint?: string; - private definitionString: string; + private definitionString: string | undefined; constructor(env: Environment, definitionPath?: string | null, envVars: EnvVars = process.env, out?: IOutput) { this.secrets = null; this.definitionPath = definitionPath; @@ -45,8 +45,8 @@ export class PrismaDefinitionClass { } async load(args: Args, envPath?: string, graceful?: boolean) { - if (args.project) { - const flagPath = path.resolve(args.project as string); + if (args['project']) { + const flagPath = path.resolve(String(args['project'])); try { fs.accessSync(flagPath); @@ -97,7 +97,7 @@ export class PrismaDefinitionClass { } get endpoint(): string | undefined { - return (this.definition && this.definition.endpoint) || process.env.PRISMA_MANAGEMENT_API_ENDPOINT; + return (this.definition && this.definition.endpoint) || process.env['PRISMA_MANAGEMENT_API_ENDPOINT']; } get clusterBaseUrl(): string | undefined { @@ -212,11 +212,11 @@ and execute ${chalk.bold.green('prisma deploy')} again, to get that value auto-f } findClusterByBaseUrl(baseUrl: string) { - return this.env.clusters.find(c => c.baseUrl.toLowerCase() === baseUrl); + return this.env.clusters?.find(c => c.baseUrl.toLowerCase() === baseUrl); } async getClusterByEndpoint(data: ParseEndpointResult) { - if (data.clusterBaseUrl && !process.env.PRISMA_MANAGEMENT_API_SECRET) { + if (data.clusterBaseUrl && !process.env['PRISMA_MANAGEMENT_API_SECRET']) { const cluster = this.findClusterByBaseUrl(data.clusterBaseUrl); if (cluster) { return cluster; @@ -256,7 +256,7 @@ and execute ${chalk.bold.green('prisma deploy')} again, to get that value auto-f let allTypes = ''; typesPaths.forEach(unresolvedTypesPath => { - const typesPath = path.join(this.definitionDir, unresolvedTypesPath!); + const typesPath = path.join(this.definitionDir!, unresolvedTypesPath!); try { fs.accessSync(typesPath); const types = fs.readFileSync(typesPath, 'utf-8'); @@ -297,7 +297,7 @@ and execute ${chalk.bold.green('prisma deploy')} again, to get that value auto-f let query = subscription.query; if (subscription.query.endsWith('.graphql')) { - const queryPath = path.join(this.definitionDir, subscription.query); + const queryPath = path.join(this.definitionDir!, subscription.query); try { fs.accessSync(queryPath); } catch { @@ -326,7 +326,7 @@ and execute ${chalk.bold.green('prisma deploy')} again, to get that value auto-f addDatamodel(datamodel: any) { this.definitionString += `\ndatamodel: ${datamodel}`; - fs.writeFileSync(this.definitionPath!, this.definitionString); + fs.writeFileSync(this.definitionPath!, this.definitionString!); this.definition!.datamodel = datamodel; } diff --git a/packages/loaders/prisma/src/prisma-yml/Variables.ts b/packages/loaders/prisma/src/prisma-yml/Variables.ts index 5eecd5e2819..d7ed25325c7 100644 --- a/packages/loaders/prisma/src/prisma-yml/Variables.ts +++ b/packages/loaders/prisma/src/prisma-yml/Variables.ts @@ -166,6 +166,7 @@ export class Variables { ' You can check our docs for more info.', ].join(''); this.out.warn(this.out.getErrorPrefix(this.fileName, 'warning') + errorMessage); + return Promise.resolve(); } getValueFromEnv(variableString: any) { diff --git a/packages/loaders/prisma/src/prisma-yml/constants.ts b/packages/loaders/prisma/src/prisma-yml/constants.ts index 55640af751e..161a00f0683 100644 --- a/packages/loaders/prisma/src/prisma-yml/constants.ts +++ b/packages/loaders/prisma/src/prisma-yml/constants.ts @@ -1,6 +1,6 @@ import { invert } from 'lodash'; -export const cloudApiEndpoint = process.env.CLOUD_API_ENDPOINT || 'https://api.cloud.prisma.sh'; +export const cloudApiEndpoint = process.env['CLOUD_API_ENDPOINT'] || 'https://api.cloud.prisma.sh'; export const clusterEndpointMap: { [key: string]: string } = { 'prisma-eu1': 'https://eu1.prisma.sh', diff --git a/packages/loaders/prisma/src/prisma-yml/utils/getProxyAgent.ts b/packages/loaders/prisma/src/prisma-yml/utils/getProxyAgent.ts index 63c16116788..f2d3c6ab89a 100644 --- a/packages/loaders/prisma/src/prisma-yml/utils/getProxyAgent.ts +++ b/packages/loaders/prisma/src/prisma-yml/utils/getProxyAgent.ts @@ -46,7 +46,7 @@ function getProxyFromURI(uri: any) { // environmental variables (NO_PROXY, HTTP_PROXY, etc.) // respect NO_PROXY environment variables (see: http://lynx.isc.org/current/breakout/lynx_help/keystrokes/environments.html) - const noProxy = process.env.NO_PROXY || process.env.no_proxy || ''; + const noProxy = process.env['NO_PROXY'] || process.env['no_proxy'] || ''; // if the noProxy is a wildcard then return null @@ -63,12 +63,16 @@ function getProxyFromURI(uri: any) { // Check for HTTP or HTTPS Proxy in environment Else default to null if (uri.protocol === 'http:') { - return process.env.HTTP_PROXY || process.env.http_proxy || null; + return process.env['HTTP_PROXY'] || process.env['http_proxy'] || null; } if (uri.protocol === 'https:') { return ( - process.env.HTTPS_PROXY || process.env.https_proxy || process.env.HTTP_PROXY || process.env.http_proxy || null + process.env['HTTPS_PROXY'] || + process.env['https_proxy'] || + process.env['HTTP_PROXY'] || + process.env['http_proxy'] || + null ); } diff --git a/packages/loaders/url/src/index.ts b/packages/loaders/url/src/index.ts index c672328cc4a..2fa8cda6872 100644 --- a/packages/loaders/url/src/index.ts +++ b/packages/loaders/url/src/index.ts @@ -1,6 +1,7 @@ /* eslint-disable no-case-declarations */ /// -import { print, IntrospectionOptions, DocumentNode, Kind, GraphQLError } from 'graphql'; +import { print, IntrospectionOptions, Kind, GraphQLError } from 'graphql'; + import { AsyncExecutor, Executor, @@ -16,6 +17,7 @@ import { mapAsyncIterator, withCancel, parseGraphQLSDL, + Maybe, } from '@graphql-tools/utils'; import { isWebUri } from 'valid-url'; import { fetch as crossFetch } from 'cross-fetch'; @@ -49,13 +51,15 @@ type Headers = type BuildExecutorOptions = { pointer: string; fetch: TFetchFn; - extraHeaders: Headers; + extraHeaders?: Maybe; defaultMethod: 'GET' | 'POST'; - useGETForQueries: boolean; - multipart?: boolean; + useGETForQueries?: Maybe; + multipart?: Maybe; }; +// TODO: Should the types here be changed to T extends Record ? export type AsyncImportFn = (moduleName: string) => PromiseLike; +// TODO: Should the types here be changed to T extends Record ? export type SyncImportFn = (moduleName: string) => T; const asyncImport: AsyncImportFn = (moduleName: string) => import(moduleName); @@ -175,7 +179,8 @@ export class UrlLoader implements DocumentLoader { ); form.append('map', JSON.stringify(map)); await Promise.all( - Array.from(uploads.entries()).map(async ([i, u]) => { + Array.from(uploads.entries()).map(async (params: unknown) => { + let [i, u] = params as any; if (isPromise(u)) { u = await u; } @@ -371,14 +376,14 @@ export class UrlLoader implements DocumentLoader { connectionParams, lazy: true, }); - return async ({ document, variables }: { document: DocumentNode; variables: any }) => { + return async ({ document, variables }) => { const query = print(document); return observableToAsyncIterable({ subscribe: observer => { const unsubscribe = subscriptionClient.subscribe( { query, - variables, + variables: variables as Record, }, observer ); @@ -414,17 +419,17 @@ export class UrlLoader implements DocumentLoader { query: document, variables, }) - ) as AsyncIterator>; + ) as AsyncIterableIterator>; }; } buildSSESubscriber( pointer: string, - extraHeaders: Headers, + extraHeaders: Maybe, fetch: AsyncFetchFn, - options: FetchEventSourceInit + options: Maybe ): Subscriber { - return async ({ document, variables, ...rest }: { document: DocumentNode; variables: any }) => { + return async ({ document, variables, ...rest }) => { const controller = new AbortController(); const query = print(document); const finalUrl = this.prepareGETUrl({ baseUrl: pointer, query, variables }); @@ -493,9 +498,9 @@ export class UrlLoader implements DocumentLoader { const [moduleName, fetchFnName] = customFetch.split('#'); const moduleResult = importFn(moduleName); if (isPromise(moduleResult)) { - return moduleResult.then(module => (fetchFnName ? module[fetchFnName] : module)); + return moduleResult.then(module => (fetchFnName ? (module as Record)[fetchFnName] : module)); } else { - return fetchFnName ? moduleResult[fetchFnName] : moduleResult; + return fetchFnName ? (module as Record)[fetchFnName] : moduleResult; } } else { return customFetch as any; @@ -504,7 +509,10 @@ export class UrlLoader implements DocumentLoader { return async ? (typeof fetch === 'undefined' ? crossFetch : fetch) : syncFetch; } - private getHeadersFromOptions(customHeaders: Headers, executionParams: ExecutionParams): Record { + private getHeadersFromOptions( + customHeaders: Maybe, + executionParams: ExecutionParams + ): Record { let headers = {}; if (customHeaders) { if (typeof customHeaders === 'function') { @@ -540,7 +548,7 @@ export class UrlLoader implements DocumentLoader { if (isPromise(importedModule)) { return importedModule.then(webSocketImplName ? importedModule[webSocketImplName] : importedModule); } else { - return webSocketImplName ? importedModule[webSocketImplName] : importedModule; + return webSocketImplName ? (importedModule as Record)[webSocketImplName] : importedModule; } } else { const websocketImpl = options.webSocketImpl || WebSocket; diff --git a/packages/loaders/url/tests/url-loader.spec.ts b/packages/loaders/url/tests/url-loader.spec.ts index b5c72f6de3c..1f6687c7bfb 100644 --- a/packages/loaders/url/tests/url-loader.spec.ts +++ b/packages/loaders/url/tests/url-loader.spec.ts @@ -102,6 +102,12 @@ input TestInput { const testPathChecker = (path: string) => path.startsWith(testPath); const testUrl = `${testHost}${testPath}`; + function assertNonMaybe(input: T): asserts input is Exclude{ + if (input == null) { + throw new Error("Value should be neither null nor undefined.") + } + } + describe('handle', () => { let scope: nock.Scope; @@ -127,10 +133,9 @@ input TestInput { it('Should return a valid schema when request is valid', async () => { scope = mockGraphQLServer({ schema: testSchema, host: testHost, path: testPathChecker }); - const schema = await loader.load(testUrl, {}); - - expect(schema.schema).toBeDefined(); - expect(printSchemaWithDirectives(schema.schema)).toBeSimilarGqlDoc(testTypeDefs); + const source = await loader.load(testUrl, {}); + assertNonMaybe(source.schema) + expect(printSchemaWithDirectives(source.schema)).toBeSimilarGqlDoc(testTypeDefs); }); it('Should pass default headers', async () => { @@ -145,11 +150,11 @@ input TestInput { }, }); - const schema = await loader.load(testUrl, {}); + const source = await loader.load(testUrl, {}); - expect(schema).toBeDefined(); - expect(schema.schema).toBeDefined(); - expect(printSchemaWithDirectives(schema.schema)).toBeSimilarGqlDoc(testTypeDefs); + expect(source).toBeDefined(); + assertNonMaybe(source.schema) + expect(printSchemaWithDirectives(source.schema)).toBeSimilarGqlDoc(testTypeDefs); expect(Array.isArray(headers.accept) ? headers.accept.join(',') : headers.accept).toContain(`application/json`); expect(headers['content-type']).toContain(`application/json`); @@ -166,11 +171,11 @@ input TestInput { }, }); - const schema = await loader.load(testUrl, { headers: { Auth: '1' } }); + const source = await loader.load(testUrl, { headers: { Auth: '1' } }); - expect(schema).toBeDefined(); - expect(schema.schema).toBeDefined(); - expect(printSchemaWithDirectives(schema.schema)).toBeSimilarGqlDoc(testTypeDefs); + expect(source).toBeDefined(); + assertNonMaybe(source.schema) + expect(printSchemaWithDirectives(source.schema)).toBeSimilarGqlDoc(testTypeDefs); expect(Array.isArray(headers.accept) ? headers.accept.join(',') : headers.accept).toContain(`application/json`); expect(headers['content-type']).toContain(`application/json`); @@ -187,11 +192,11 @@ input TestInput { headers = ctx.req.headers; }, }); - const schema = await loader.load(testUrl, { headers: [{ A: '1' }, { B: '2', C: '3' }] }); + const source = await loader.load(testUrl, { headers: [{ A: '1' }, { B: '2', C: '3' }] }); - expect(schema).toBeDefined(); - expect(schema.schema).toBeDefined(); - expect(printSchemaWithDirectives(schema.schema)).toBeSimilarGqlDoc(testTypeDefs); + expect(source).toBeDefined(); + assertNonMaybe(source.schema) + expect(printSchemaWithDirectives(source.schema)).toBeSimilarGqlDoc(testTypeDefs); expect(Array.isArray(headers.accept) ? headers.accept.join(',') : headers.accept).toContain(`application/json`); expect(headers['content-type']).toContain(`application/json`); @@ -205,7 +210,8 @@ input TestInput { const source = await loader.load(testUrl, { descriptions: false }); expect(source).toBeDefined(); - expect(source.schema.getQueryType().description).toBeUndefined(); + assertNonMaybe(source.schema) + expect(source.schema.getQueryType()!.description).toBeUndefined(); }); it('Absolute file path should not be accepted as URL', async () => { @@ -224,7 +230,7 @@ input TestInput { scope.done(); scope = mockGraphQLServer({ schema: testSchema, host: testHost, path: testPathChecker, method: 'GET' }); - + assertNonMaybe(source.schema) const result = await execute({ schema: source.schema, document: parse(/* GraphQL */ ` @@ -253,7 +259,7 @@ input TestInput { scope = mockGraphQLServer({ schema: testSchema, host: address.host, path: address.path }); const result = await loader.load(url, {}); - expect(result.schema).toBeDefined(); + assertNonMaybe(result.schema) expect(printSchemaWithDirectives(result.schema)).toBeSimilarGqlDoc(testTypeDefs); }); @@ -270,7 +276,7 @@ input TestInput { }); const result = await loader.load(url, {}); - expect(result.schema).toBeDefined(); + assertNonMaybe(result.schema) expect(printSchemaWithDirectives(result.schema)).toBeSimilarGqlDoc(testTypeDefs); }); @@ -287,7 +293,7 @@ input TestInput { }); const result = await loader.load(url, {}); - expect(result.schema).toBeDefined(); + assertNonMaybe(result.schema) expect(printSchemaWithDirectives(result.schema)).toBeSimilarGqlDoc(testTypeDefs); }); it('should handle .graphql files', async () => { @@ -296,7 +302,7 @@ input TestInput { scope = nock(testHost).get(testPath).reply(200, testTypeDefs); const result = await loader.load(testHost + testPath, {}); - expect(result.document).toBeDefined(); + assertNonMaybe(result.document) expect(print(result.document)).toBeSimilarGqlDoc(testTypeDefs); }) it('should handle results with handleAsSDL option even if it doesn\'t end with .graphql', async () => { @@ -307,7 +313,7 @@ input TestInput { handleAsSDL: true, }); - expect(result.document).toBeDefined(); + assertNonMaybe(result.document) expect(print(result.document)).toBeSimilarGqlDoc(testTypeDefs); }) it('should handle subscriptions - new protocol', (done) => { @@ -344,7 +350,7 @@ input TestInput { ); httpServer.listen(8081); - + assertNonMaybe(schema) const asyncIterator = await subscribe({ schema, document: parse(/* GraphQL */` @@ -372,7 +378,7 @@ input TestInput { expect(await getNextResult()).toBe(1); expect(await getNextResult()).toBe(2); - await asyncIterator.return(); + await asyncIterator.return!(); await subscriptionServer.dispose(); wsServer.close(() => { httpServer.close(done); @@ -413,7 +419,7 @@ input TestInput { path: '/graphql', }, ); - + assertNonMaybe(schema) const asyncIterator = await subscribe({ schema, document: parse(/* GraphQL */` @@ -441,7 +447,7 @@ input TestInput { expect(await getNextResult()).toBe(1); expect(await getNextResult()).toBe(2); - await asyncIterator.return(); + await asyncIterator.return!(); subscriptionServer.close(); httpServer.close(done); }); @@ -460,7 +466,7 @@ input TestInput { const fileName = 'testfile.txt'; const absoluteFilePath = join(__dirname, fileName); - + assertNonMaybe(schema) const result = await execute({ schema, document: parse(/* GraphQL */` @@ -481,6 +487,7 @@ input TestInput { const content = readFileSync(absoluteFilePath, 'utf8') expect(result.errors).toBeFalsy(); + assertNonMaybe(result.data) expect(result.data.uploadFile?.filename).toBe(fileName); expect(result.data.uploadFile?.content).toBe(content); }); diff --git a/packages/merge/src/extensions.ts b/packages/merge/src/extensions.ts index 40cfbc83759..58118ab0225 100644 --- a/packages/merge/src/extensions.ts +++ b/packages/merge/src/extensions.ts @@ -146,7 +146,10 @@ export function mergeExtensions(extensions: SchemaExtensions[]): SchemaExtension ); } -function applyExtensionObject(obj: { extensions: Maybe>> }, extensions: ExtensionsObject) { +function applyExtensionObject( + obj: Maybe<{ extensions: Maybe>> }>, + extensions: ExtensionsObject +) { if (!obj) { return; } diff --git a/packages/merge/src/merge-resolvers.ts b/packages/merge/src/merge-resolvers.ts index ed8ed384c2d..2173eba5911 100644 --- a/packages/merge/src/merge-resolvers.ts +++ b/packages/merge/src/merge-resolvers.ts @@ -1,4 +1,4 @@ -import { IResolvers, mergeDeep } from '@graphql-tools/utils'; +import { IResolvers, Maybe, mergeDeep } from '@graphql-tools/utils'; export type ResolversFactory = (...args: any[]) => IResolvers; export type ResolversDefinition = IResolvers | ResolversFactory; @@ -40,7 +40,7 @@ export interface MergeResolversOptions { * ``` */ export function mergeResolvers>( - resolversDefinitions: T[], + resolversDefinitions: Maybe, options?: MergeResolversOptions ): T { if (!resolversDefinitions || resolversDefinitions.length === 0) { diff --git a/packages/merge/src/typedefs-mergers/arguments.ts b/packages/merge/src/typedefs-mergers/arguments.ts index 1916b275a9f..c1e13be5438 100644 --- a/packages/merge/src/typedefs-mergers/arguments.ts +++ b/packages/merge/src/typedefs-mergers/arguments.ts @@ -1,13 +1,13 @@ import { InputValueDefinitionNode } from 'graphql'; import { Config } from '.'; -import { compareNodes } from '@graphql-tools/utils'; +import { compareNodes, isSome } from '@graphql-tools/utils'; export function mergeArguments( args1: InputValueDefinitionNode[], args2: InputValueDefinitionNode[], - config: Config + config?: Config ): InputValueDefinitionNode[] { - const result = deduplicateArguments([].concat(args2, args1).filter(a => a)); + const result = deduplicateArguments([...args2, ...args1].filter(isSome)); if (config && config.sort) { result.sort(compareNodes); } diff --git a/packages/merge/src/typedefs-mergers/comments.ts b/packages/merge/src/typedefs-mergers/comments.ts index 3dd2294de4f..dade851edc9 100644 --- a/packages/merge/src/typedefs-mergers/comments.ts +++ b/packages/merge/src/typedefs-mergers/comments.ts @@ -20,12 +20,16 @@ export function resetComments(): void { } export function collectComment(node: NamedDefinitionNode): void { - const entityName = node.name.value; + const entityName = node.name?.value; + if (entityName == null) { + return; + } + pushComment(node, entityName); switch (node.kind) { case 'EnumTypeDefinition': - node.values.forEach(value => { + node.values?.forEach(value => { pushComment(value, entityName, value.name.value); }); break; diff --git a/packages/merge/src/typedefs-mergers/directives.ts b/packages/merge/src/typedefs-mergers/directives.ts index a247dc5df33..2808edbf62e 100644 --- a/packages/merge/src/typedefs-mergers/directives.ts +++ b/packages/merge/src/typedefs-mergers/directives.ts @@ -1,5 +1,6 @@ import { ArgumentNode, DirectiveNode, DirectiveDefinitionNode, ListValueNode, NameNode, print } from 'graphql'; import { Config } from './merge-typedefs'; +import { isSome } from '@graphql-tools/utils'; function directiveAlreadyExists(directivesArr: ReadonlyArray, otherDirective: DirectiveNode): boolean { return !!directivesArr.find(directive => directive.name.value === otherDirective.name.value); @@ -52,7 +53,7 @@ function deduplicateDirectives(directives: ReadonlyArray): Direct return directive; }) - .filter(d => d); + .filter(isSome); } export function mergeDirectives( @@ -60,7 +61,7 @@ export function mergeDirectives( d2: ReadonlyArray = [], config?: Config ): DirectiveNode[] { - const reverseOrder: boolean = config && config.reverseDirectives; + const reverseOrder: boolean | undefined = config && config.reverseDirectives; const asNext = reverseOrder ? d1 : d2; const asFirst = reverseOrder ? d2 : d1; const result = deduplicateDirectives([...asNext]); diff --git a/packages/merge/src/typedefs-mergers/enum-values.ts b/packages/merge/src/typedefs-mergers/enum-values.ts index 8ac31168f85..c4b81a49ffb 100644 --- a/packages/merge/src/typedefs-mergers/enum-values.ts +++ b/packages/merge/src/typedefs-mergers/enum-values.ts @@ -4,29 +4,37 @@ import { Config } from './merge-typedefs'; import { compareNodes } from '@graphql-tools/utils'; export function mergeEnumValues( - first: ReadonlyArray, - second: ReadonlyArray, + first: ReadonlyArray | undefined, + second: ReadonlyArray | undefined, config?: Config ): EnumValueDefinitionNode[] { if (config?.consistentEnumMerge) { - const reversed: ReadonlyArray = first; + const reversed: Array = []; + if (first) { + reversed.push(...first); + } first = second; second = reversed; } const enumValueMap = new Map(); - for (const firstValue of first) { - enumValueMap.set(firstValue.name.value, firstValue); + if (first) { + for (const firstValue of first) { + enumValueMap.set(firstValue.name.value, firstValue); + } } - for (const secondValue of second) { - const enumValue = secondValue.name.value; - if (enumValueMap.has(enumValue)) { - const firstValue: any = enumValueMap.get(enumValue); - firstValue.description = secondValue.description || firstValue.description; - firstValue.directives = mergeDirectives(secondValue.directives, firstValue.directives); - } else { - enumValueMap.set(enumValue, secondValue); + if (second) { + for (const secondValue of second) { + const enumValue = secondValue.name.value; + if (enumValueMap.has(enumValue)) { + const firstValue: any = enumValueMap.get(enumValue); + firstValue.description = secondValue.description || firstValue.description; + firstValue.directives = mergeDirectives(secondValue.directives, firstValue.directives); + } else { + enumValueMap.set(enumValue, secondValue); + } } } + const result = [...enumValueMap.values()]; if (config && config.sort) { result.sort(compareNodes); diff --git a/packages/merge/src/typedefs-mergers/fields.ts b/packages/merge/src/typedefs-mergers/fields.ts index 98146f90817..be3673c69b0 100644 --- a/packages/merge/src/typedefs-mergers/fields.ts +++ b/packages/merge/src/typedefs-mergers/fields.ts @@ -24,40 +24,45 @@ function fieldAlreadyExists(fieldsArr: ReadonlyArray, otherField: any, conf export function mergeFields( type: { name: NameNode }, - f1: ReadonlyArray, - f2: ReadonlyArray, + f1: ReadonlyArray | undefined, + f2: ReadonlyArray | undefined, config?: Config ): T[] { - const result: T[] = [...f2]; - - for (const field of f1) { - if (fieldAlreadyExists(result, field, config)) { - const existing: any = result.find((f: any) => f.name.value === (field as any).name.value); - - if (!config?.ignoreFieldConflicts) { - if (config?.throwOnConflict) { - preventConflicts(type, existing, field, false); - } else { - preventConflicts(type, existing, field, true); + const result: T[] = []; + if (f2 != null) { + result.push(...f2); + } + if (f1 != null) { + for (const field of f1) { + if (fieldAlreadyExists(result, field, config)) { + const existing: any = result.find((f: any) => f.name.value === (field as any).name.value); + + if (!config?.ignoreFieldConflicts) { + if (config?.throwOnConflict) { + preventConflicts(type, existing, field, false); + } else { + preventConflicts(type, existing, field, true); + } + + if (isNonNullTypeNode(field.type) && !isNonNullTypeNode(existing.type)) { + existing.type = field.type; + } } - if (isNonNullTypeNode(field.type) && !isNonNullTypeNode(existing.type)) { - existing.type = field.type; - } + existing.arguments = mergeArguments(field['arguments'] || [], existing.arguments || [], config); + existing.directives = mergeDirectives(field.directives, existing.directives, config); + existing.description = field.description || existing.description; + } else { + result.push(field); } - - existing.arguments = mergeArguments(field['arguments'] || [], existing.arguments || [], config); - existing.directives = mergeDirectives(field.directives, existing.directives, config); - existing.description = field.description || existing.description; - } else { - result.push(field); } } if (config && config.sort) { result.sort(compareNodes); } if (config && config.exclusions) { - return result.filter(field => !config.exclusions.includes(`${type.name.value}.${field.name.value}`)); + const exclusions = config.exclusions; + return result.filter(field => !exclusions.includes(`${type.name.value}.${field.name.value}`)); } return result; } diff --git a/packages/merge/src/typedefs-mergers/interface.ts b/packages/merge/src/typedefs-mergers/interface.ts index fb5a1d2d6a9..996299b1b65 100644 --- a/packages/merge/src/typedefs-mergers/interface.ts +++ b/packages/merge/src/typedefs-mergers/interface.ts @@ -6,7 +6,7 @@ import { mergeDirectives } from './directives'; export function mergeInterface( node: InterfaceTypeDefinitionNode | InterfaceTypeExtensionNode, existingNode: InterfaceTypeDefinitionNode | InterfaceTypeExtensionNode, - config: Config + config?: Config ): InterfaceTypeDefinitionNode | InterfaceTypeExtensionNode { if (existingNode) { try { diff --git a/packages/merge/src/typedefs-mergers/merge-nodes.ts b/packages/merge/src/typedefs-mergers/merge-nodes.ts index 8fceaa72eba..264323f70f3 100644 --- a/packages/merge/src/typedefs-mergers/merge-nodes.ts +++ b/packages/merge/src/typedefs-mergers/merge-nodes.ts @@ -10,6 +10,8 @@ import { mergeDirective } from './directives'; import { collectComment } from './comments'; import { mergeSchemaDefs } from './schema-def'; +export const schemaDefSymbol = 'SCHEMA_DEF_SYMBOL'; + export type MergedResultMap = Record & { [schemaDefSymbol]: SchemaDefinitionNode | SchemaExtensionNode; }; @@ -19,17 +21,19 @@ export function isNamedDefinitionNode(definitionNode: DefinitionNode): definitio return 'name' in definitionNode; } -export const schemaDefSymbol = 'SCHEMA_DEF_SYMBOL'; - export function mergeGraphQLNodes(nodes: ReadonlyArray, config?: Config): MergedResultMap { const mergedResultMap = {} as MergedResultMap; for (const nodeDefinition of nodes) { if (isNamedDefinitionNode(nodeDefinition)) { - const name = nodeDefinition.name.value; + const name = nodeDefinition.name?.value; if (config?.commentDescriptions) { collectComment(nodeDefinition); } + if (name == null) { + continue; + } + if (config?.exclusions?.includes(name + '.*') || config?.exclusions?.includes(name)) { delete mergedResultMap[name]; } else { diff --git a/packages/merge/src/typedefs-mergers/merge-typedefs.ts b/packages/merge/src/typedefs-mergers/merge-typedefs.ts index 0760a62aadf..2b4a6707ee6 100644 --- a/packages/merge/src/typedefs-mergers/merge-typedefs.ts +++ b/packages/merge/src/typedefs-mergers/merge-typedefs.ts @@ -161,7 +161,7 @@ export function mergeGraphQLTypes( if (!opTypeDefNode) { const possibleRootTypeName = DEFAULT_OPERATION_TYPE_NAME_MAP[opTypeDefNodeType]; const existingPossibleRootType = mergedNodes[possibleRootTypeName]; - if (existingPossibleRootType) { + if (existingPossibleRootType != null && existingPossibleRootType.name != null) { operationTypes.push({ kind: Kind.OPERATION_TYPE_DEFINITION, type: { @@ -174,7 +174,7 @@ export function mergeGraphQLTypes( } } - if (schemaDef.operationTypes?.length > 0) { + if (schemaDef?.operationTypes?.length != null && schemaDef.operationTypes.length > 0) { mergedNodes[schemaDefSymbol] = schemaDef; } } diff --git a/packages/merge/src/typedefs-mergers/utils.ts b/packages/merge/src/typedefs-mergers/utils.ts index 9093afd243d..d072742da5f 100644 --- a/packages/merge/src/typedefs-mergers/utils.ts +++ b/packages/merge/src/typedefs-mergers/utils.ts @@ -45,9 +45,18 @@ export enum CompareVal { A_EQUALS_B = 0, A_GREATER_THAN_B = 1, } -export type CompareFn = (a: T, b: T) => -1 | 0 | 1; +export type CompareFn = (a: T | undefined, b: T | undefined) => -1 | 0 | 1; -export function defaultStringComparator(a: string, b: string): CompareVal { +export function defaultStringComparator(a: string | undefined, b: string | undefined): CompareVal { + if (a == null && b == null) { + return CompareVal.A_EQUALS_B; + } + if (a == null) { + return CompareVal.A_SMALLER_THAN_B; + } + if (b == null) { + return CompareVal.A_GREATER_THAN_B; + } if (a < b) return CompareVal.A_SMALLER_THAN_B; if (a > b) return CompareVal.A_GREATER_THAN_B; return CompareVal.A_EQUALS_B; diff --git a/packages/merge/tests/extract-extensions-from-schema.spec.ts b/packages/merge/tests/extract-extensions-from-schema.spec.ts index 354af4b653c..1823fec4d7b 100644 --- a/packages/merge/tests/extract-extensions-from-schema.spec.ts +++ b/packages/merge/tests/extract-extensions-from-schema.spec.ts @@ -1,5 +1,7 @@ import { buildSchema, GraphQLEnumType, GraphQLSchema, printSchema, buildClientSchema, buildASTSchema, parse } from 'graphql'; +import { assertSome } from '@graphql-tools/utils'; import { extractExtensionsFromSchema, mergeExtensions, applyExtensions } from '../src/extensions' +import { assertGraphQLEnumType, assertGraphQLInputObjectType, assertGraphQLInterfaceType, assertGraphQLObjectType, assertGraphQLScalerType, assertGraphQLUnionType } from '../../testing/assertion'; describe('extensions', () => { let schema: GraphQLSchema; @@ -47,12 +49,24 @@ describe('extensions', () => { }); it('Should extract extensions correctly for all possible types', () => { - schema.getType('MyInput').extensions = { input: true }; - schema.getType('MyType').extensions = { type: true }; - schema.getType('Node').extensions = { interface: true }; - schema.getType('MyEnum').extensions = { enum: true }; - schema.getType('MyUnion').extensions = { union: true }; - schema.getType('MyScalar').extensions = { scalar: true }; + const MyInput = schema.getType('MyInput') + assertSome(MyInput) + MyInput.extensions = { input: true }; + const MyType = schema.getType('MyType') + assertSome(MyType) + MyType.extensions = { type: true }; + const Node = schema.getType('Node') + assertSome(Node) + Node.extensions = { interface: true }; + const MyEnum = schema.getType('MyEnum') + assertSome(MyEnum) + MyEnum.extensions = { enum: true }; + const MyUnion = schema.getType('MyUnion') + assertSome(MyUnion) + MyUnion.extensions = { union: true }; + const MyScalar = schema.getType('MyScalar') + assertSome(MyScalar) + MyScalar.extensions = { scalar: true }; const { types: extensions } = extractExtensionsFromSchema(schema); expect(extensions.MyInput.extensions).toEqual({ input: true }) @@ -64,45 +78,70 @@ describe('extensions', () => { }); it('Should extract extensions correctly for fields arguments', () => { - schema.getQueryType().getFields().t.args[0].extensions = { fieldArg: true }; + const queryType = schema.getQueryType() + assertSome(queryType) + queryType.getFields().t.args[0].extensions = { fieldArg: true }; - const { types: extensions } = extractExtensionsFromSchema(schema); - expect(extensions.Query.fields.t.arguments.i).toEqual({ fieldArg: true }) + const { types: extensions } = extractExtensionsFromSchema(schema); + if (extensions.Query.type !== "object") { + throw new Error("Unexpected type.") + } + + expect(extensions.Query.fields.t.arguments.i).toEqual({ fieldArg: true }) }); it('Should extract extensions correctly for enum values', () => { - (schema.getType('MyEnum') as GraphQLEnumType).getValues()[0].extensions = { enumValue: true }; + const MyEnum = schema.getType('MyEnum') + assertGraphQLEnumType(MyEnum) + MyEnum.getValues()[0].extensions = { enumValue: true }; const { types: extensions } = extractExtensionsFromSchema(schema); + if (extensions.MyEnum.type !== "enum") { + throw new Error("Unexpected type.") + } expect(extensions.MyEnum.values.A).toEqual({ enumValue: true }); expect(extensions.MyEnum.values.B).toEqual({}); expect(extensions.MyEnum.values.C).toEqual({}); }); it('Should extract extensions correctly for fields', () => { - schema.getQueryType().getFields().t.extensions = { field: true }; + const queryType = schema.getQueryType() + assertSome(queryType) + queryType.getFields().t.extensions = { field: true }; - const { types: extensions } = extractExtensionsFromSchema(schema); - expect(extensions.Query.fields.t.extensions).toEqual({ field: true }) + const { types: extensions } = extractExtensionsFromSchema(schema); + if (extensions.Query.type !== "object") { + throw new Error("Unexpected type.") + } + expect(extensions.Query.fields.t.extensions).toEqual({ field: true }) }); it('Should extract extensions correctly for input fields', () => { - (schema.getType('MyInput') as any).getFields().foo.extensions = { inputField: true }; + const MyInput = schema.getType('MyInput') +assertGraphQLInputObjectType(MyInput) + MyInput.getFields().foo.extensions = { inputField: true }; const { types: extensions } = extractExtensionsFromSchema(schema); + if (extensions.MyInput.type !== "input") { + throw new Error("Unexpected type.") + } expect(extensions.MyInput.fields.foo.extensions).toEqual({ inputField: true }) }); }); describe('mergeExtensions', () => { it('Should merge all extensions from 2 schemas correctly', () => { - schema.getQueryType().extensions = { queryTest: true }; + const queryType = schema.getQueryType() + assertSome(queryType) + queryType.extensions = { queryTest: true }; const secondSchema = buildSchema(/* GraphQL */` type Query { foo: String! } `); - secondSchema.getQueryType().extensions = { querySecondTest: true }; + const secondQueryType = secondSchema.getQueryType() + assertSome(secondQueryType) + secondQueryType.extensions = { querySecondTest: true }; const extensions = extractExtensionsFromSchema(schema); const secondExtensions = extractExtensionsFromSchema(secondSchema); @@ -114,36 +153,66 @@ describe('extensions', () => { describe('applyExtensionsToSchema', () => { it('Should re apply extensions to schema and types correctly', () => { schema.extensions = { schema: true }; - schema.getType('MyInput').extensions = { input: true }; - schema.getType('MyType').extensions = { type: true }; - schema.getType('Node').extensions = { interface: true }; - schema.getType('MyEnum').extensions = { enum: true }; - schema.getType('MyUnion').extensions = { union: true }; - schema.getType('MyScalar').extensions = { scalar: true }; - (schema.getType('MyInput') as any).getFields().foo.extensions = { inputField: true }; - schema.getQueryType().getFields().t.extensions = { field: true }; - (schema.getType('MyEnum') as GraphQLEnumType).getValues()[0].extensions = { enumValue: true }; - schema.getQueryType().getFields().t.args[0].extensions = { fieldArg: true }; + let MyInput = schema.getType('MyInput') + assertGraphQLInputObjectType(MyInput) + MyInput.extensions = { input: true }; + let MyType = schema.getType("MyType") + assertGraphQLObjectType(MyType) + MyType.extensions = { type: true }; + let Node = schema.getType("Node") + assertGraphQLInterfaceType(Node) + Node.extensions = { interface: true }; + let MyEnum = schema.getType('MyEnum') + assertGraphQLEnumType(MyEnum) + MyEnum.extensions = { enum: true }; + let MyUnion = schema.getType('MyUnion') + assertGraphQLUnionType(MyUnion) + MyUnion.extensions = { union: true }; + let MyScalar = schema.getType('MyScalar') + assertGraphQLScalerType(MyScalar) + MyScalar.extensions = { scalar: true }; + MyInput.getFields().foo.extensions = { inputField: true }; + let QueryType = schema.getQueryType(); + assertSome(QueryType) + QueryType.getFields().t.extensions = { field: true }; + MyEnum.getValues()[0].extensions = { enumValue: true }; + QueryType.getFields().t.args[0].extensions = { fieldArg: true }; const result = extractExtensionsFromSchema(schema); const cleanSchema = buildASTSchema(parse(printSchema(schema))); + MyInput = cleanSchema.getType('MyInput') + assertGraphQLInputObjectType(MyInput) // To make sure it's stripped - expect(cleanSchema.getType('MyInput').extensions).toBeUndefined(); + expect(MyInput.extensions).toBeUndefined(); const modifiedSchema = applyExtensions(cleanSchema, result); - + expect(modifiedSchema.extensions).toEqual({ schema: true }) - expect(modifiedSchema.getType('MyInput').extensions).toEqual({ input: true }); - expect(modifiedSchema.getType('MyType').extensions).toEqual({ type: true }); - expect(modifiedSchema.getType('Node').extensions).toEqual({ interface: true }); - expect(modifiedSchema.getType('MyEnum').extensions).toEqual({ enum: true }); - expect(modifiedSchema.getType('MyUnion').extensions).toEqual({ union: true }); - expect(modifiedSchema.getType('MyScalar').extensions).toEqual({ scalar: true }); - expect((modifiedSchema.getType('MyInput') as any).getFields().foo.extensions).toEqual({ inputField: true }); - expect(modifiedSchema.getQueryType().getFields().t.extensions).toEqual({ field: true }); - expect((modifiedSchema.getType('MyEnum') as GraphQLEnumType).getValues()[0].extensions).toEqual({ enumValue: true }); - expect(modifiedSchema.getQueryType().getFields().t.args[0].extensions).toEqual({ fieldArg: true }); + MyInput = modifiedSchema.getType('MyInput') + assertGraphQLInputObjectType(MyInput) + expect(MyInput.extensions).toEqual({ input: true }); + MyType = modifiedSchema.getType("MyType") + assertGraphQLObjectType(MyType) + expect(MyType.extensions).toEqual({ type: true }); + Node = modifiedSchema.getType("Node") + assertGraphQLInterfaceType(Node) + expect(Node.extensions).toEqual({ interface: true }); + MyEnum = modifiedSchema.getType('MyEnum') + assertGraphQLEnumType(MyEnum) + expect(MyEnum.extensions).toEqual({ enum: true }); + MyUnion = modifiedSchema.getType('MyUnion') + assertGraphQLUnionType(MyUnion) + expect(MyUnion.extensions).toEqual({ union: true }); + MyScalar = modifiedSchema.getType('MyScalar') + assertGraphQLScalerType(MyScalar) + expect(MyScalar.extensions).toEqual({ scalar: true }); + expect(MyInput.getFields().foo.extensions).toEqual({ inputField: true }); + QueryType = modifiedSchema.getQueryType(); + assertSome(QueryType) + expect(QueryType.getFields().t.extensions).toEqual({ field: true }); + expect(MyEnum.getValues()[0].extensions).toEqual({ enumValue: true }); + expect(QueryType.getFields().t.args[0].extensions).toEqual({ fieldArg: true }); }); }) -}); \ No newline at end of file +}); diff --git a/packages/merge/tests/merge-nodes.spec.ts b/packages/merge/tests/merge-nodes.spec.ts index ac7fcd12fcb..859851612f8 100644 --- a/packages/merge/tests/merge-nodes.spec.ts +++ b/packages/merge/tests/merge-nodes.spec.ts @@ -1,5 +1,7 @@ import { mergeGraphQLNodes } from '../src'; -import { parse, InputObjectTypeDefinitionNode, EnumTypeDefinitionNode } from 'graphql'; +import { parse, InputObjectTypeDefinitionNode } from 'graphql'; +import { assertEnumTypeDefinitionNode, assertInputObjectTypeDefinitionNode, assertInterfaceTypeDefinitionNode, assertNamedTypeNode, assertObjectTypeDefinitionNode, assertScalarTypeDefinitionNode, assertUnionTypeDefinitionNode } from '../../testing/assertion'; +import { assertSome } from '@graphql-tools/utils'; describe('Merge Nodes', () => { describe('type', () => { @@ -7,10 +9,12 @@ describe('Merge Nodes', () => { const type1 = parse(`type A { f1: String }`); const type2 = parse(`type A`); const merged = mergeGraphQLNodes([...type1.definitions, ...type2.definitions]); - const type: any = merged.A; - + const type = merged.A; + assertObjectTypeDefinitionNode(type) + assertSome(type.fields) expect(type.fields.length).toBe(1); expect(type.fields[0].name.value).toBe('f1'); + assertNamedTypeNode(type.fields[0].type) expect(type.fields[0].type.name.value).toBe('String'); }); @@ -18,12 +22,16 @@ describe('Merge Nodes', () => { const type1 = parse(`type A { f1: String }`); const type2 = parse(`type A { f2: Int }`); const merged = mergeGraphQLNodes([...type1.definitions, ...type2.definitions]); - const type: any = merged.A; + const type = merged.A; + assertObjectTypeDefinitionNode(type) + assertSome(type.fields) expect(type.fields.length).toBe(2); expect(type.fields[0].name.value).toBe('f1'); expect(type.fields[1].name.value).toBe('f2'); + assertNamedTypeNode(type.fields[0].type) expect(type.fields[0].type.name.value).toBe('String'); + assertNamedTypeNode(type.fields[1].type) expect(type.fields[1].type.name.value).toBe('Int'); }); @@ -31,12 +39,16 @@ describe('Merge Nodes', () => { const type1 = parse(`type A { f1: String }`); const type2 = parse(`type A { f1: String, f2: Int}`); const merged = mergeGraphQLNodes([...type1.definitions, ...type2.definitions]); - const type: any = merged.A; + const type = merged.A; + assertObjectTypeDefinitionNode(type) + assertSome(type.fields) expect(type.fields.length).toBe(2); expect(type.fields[0].name.value).toBe('f1'); expect(type.fields[1].name.value).toBe('f2'); + assertNamedTypeNode(type.fields[0].type) expect(type.fields[0].type.name.value).toBe('String'); + assertNamedTypeNode(type.fields[1].type) expect(type.fields[1].type.name.value).toBe('Int'); }); @@ -44,7 +56,9 @@ describe('Merge Nodes', () => { const type1 = parse(`interface Base { f1: String } type A implements Base { f1: String }`); const type2 = parse(`interface Base { f1: String } type A implements Base { f2: Int}`); const merged = mergeGraphQLNodes([...type1.definitions, ...type2.definitions]); - const type: any = merged.A; + const type = merged.A; + assertObjectTypeDefinitionNode(type) + assertSome(type.interfaces) expect(type.interfaces.length).toBe(1); expect(type.interfaces[0].name.value).toBe('Base'); @@ -54,7 +68,9 @@ describe('Merge Nodes', () => { const type1 = parse(`interface Base { f1: String } type A implements Base { f1: String }`); const type2 = parse(`type A { f2: Int}`); const merged = mergeGraphQLNodes([...type1.definitions, ...type2.definitions]); - const type: any = merged.A; + const type = merged.A; + assertObjectTypeDefinitionNode(type) + assertSome(type.interfaces) expect(type.interfaces.length).toBe(1); expect(type.interfaces[0].name.value).toBe('Base'); @@ -64,7 +80,9 @@ describe('Merge Nodes', () => { const type1 = parse(`type A @test { f1: String }`); const type2 = parse(`type A { f2: Int}`); const merged = mergeGraphQLNodes([...type1.definitions, ...type2.definitions]); - const type: any = merged.A; + const type = merged.A; + assertObjectTypeDefinitionNode(type) + assertSome(type.directives) expect(type.directives.length).toBe(1); expect(type.directives[0].name.value).toBe('test'); @@ -74,7 +92,9 @@ describe('Merge Nodes', () => { const type1 = parse(`type A @test { f1: String }`); const type2 = parse(`type A @other { f2: Int}`); const merged = mergeGraphQLNodes([...type1.definitions, ...type2.definitions]); - const type: any = merged.A; + const type = merged.A; + assertObjectTypeDefinitionNode(type) + assertSome(type.directives) expect(type.directives.length).toBe(2); expect(type.directives[0].name.value).toBe('test'); @@ -85,7 +105,9 @@ describe('Merge Nodes', () => { const type1 = parse(`type A @test { f1: String }`); const type2 = parse(`type A @test { f2: Int}`); const merged = mergeGraphQLNodes([...type1.definitions, ...type2.definitions]); - const type: any = merged.A; + const type = merged.A; + assertObjectTypeDefinitionNode(type) + assertSome(type.directives) expect(type.directives.length).toBe(1); expect(type.directives[0].name.value).toBe('test'); @@ -95,7 +117,9 @@ describe('Merge Nodes', () => { const type1 = parse(`type A @test { f1: String }`); const type2 = parse(`type A @test2 { f2: Int}`); const merged = mergeGraphQLNodes([...type1.definitions, ...type2.definitions]); - const type: any = merged.A; + const type = merged.A; + assertObjectTypeDefinitionNode(type) + assertSome(type.directives) expect(type.directives.length).toBe(2); expect(type.directives[0].name.value).toBe('test'); @@ -108,7 +132,9 @@ describe('Merge Nodes', () => { const merged = mergeGraphQLNodes([...type1.definitions, ...type2.definitions], { reverseDirectives: true, }); - const type: any = merged.A; + const type = merged.A; + assertObjectTypeDefinitionNode(type) + assertSome(type.directives) expect(type.directives.length).toBe(2); expect(type.directives[0].name.value).toBe('test2'); @@ -119,7 +145,9 @@ describe('Merge Nodes', () => { const type1 = parse(`interface Base1 { f1: String } type A implements Base1 { f1: String }`); const type2 = parse(`interface Base2 { f2: Int } type A implements Base2 { f2: Int}`); const merged = mergeGraphQLNodes([...type1.definitions, ...type2.definitions]); - const type: any = merged.A; + const type = merged.A; + assertObjectTypeDefinitionNode(type) + assertSome(type.interfaces) expect(type.interfaces.length).toBe(2); expect(type.interfaces[0].name.value).toBe('Base1'); @@ -140,8 +168,9 @@ describe('Merge Nodes', () => { const type1 = parse(`enum A { T }`); const type2 = parse(`enum A { S }`); const merged = mergeGraphQLNodes([...type1.definitions, ...type2.definitions]); - const result = merged.A as EnumTypeDefinitionNode; - + const result = merged.A + assertEnumTypeDefinitionNode(result) + assertSome(result.values) expect(result.values.length).toBe(2); expect(result.values.findIndex(v => v.name.value === 'T')).not.toBe(-1); expect(result.values.findIndex(v => v.name.value === 'S')).not.toBe(-1); @@ -151,7 +180,9 @@ describe('Merge Nodes', () => { const type1 = parse(`enum A { T }`); const type2 = parse(`enum A { T }`); const merged = mergeGraphQLNodes([...type1.definitions, ...type2.definitions]); - const result: any = merged.A; + const result = merged.A; + assertEnumTypeDefinitionNode(result) + assertSome(result.values) expect(result.values.length).toBe(1); expect(result.values[0].name.value).toBe('T'); @@ -161,7 +192,9 @@ describe('Merge Nodes', () => { const type1 = parse(`enum A @test { T }`); const type2 = parse(`enum A @test2 { T }`); const merged = mergeGraphQLNodes([...type1.definitions, ...type2.definitions]); - const result: any = merged.A; + const result = merged.A; + assertEnumTypeDefinitionNode(result) + assertSome(result.directives) expect(result.directives.length).toBe(2); expect(result.directives[0].name.value).toBe('test'); @@ -172,7 +205,9 @@ describe('Merge Nodes', () => { const type1 = parse(`enum A @test { T }`); const type2 = parse(`enum A { S }`); const merged = mergeGraphQLNodes([...type1.definitions, ...type2.definitions]); - const result: any = merged.A; + const result = merged.A; + assertEnumTypeDefinitionNode(result) + assertSome(result.directives) expect(result.directives.length).toBe(1); expect(result.directives[0].name.value).toBe('test'); @@ -184,7 +219,9 @@ describe('Merge Nodes', () => { const type1 = parse(`type A union C = A`); const type2 = parse(`type B union C = B`); const merged = mergeGraphQLNodes([...type1.definitions, ...type2.definitions]); - const result: any = merged.C; + const result = merged.C; + assertUnionTypeDefinitionNode(result) + assertSome(result.types) expect(result.types.length).toBe(2); expect(result.types[0].name.value).toBe('A'); @@ -197,7 +234,8 @@ describe('Merge Nodes', () => { const type1 = parse(`scalar A`); const type2 = parse(`scalar A`); const merged = mergeGraphQLNodes([...type1.definitions, ...type2.definitions]); - const result: any = merged.A; + const result = merged.A; + assertScalarTypeDefinitionNode(result) expect(result.name.value).toBe('A'); }); @@ -208,7 +246,9 @@ describe('Merge Nodes', () => { const type1 = parse(`input A { f1: String }`); const type2 = parse(`input A { f2: String }`); const merged = mergeGraphQLNodes([...type1.definitions, ...type2.definitions]); - const result: any = merged.A; + const result = merged.A; + assertInputObjectTypeDefinitionNode(result) + assertSome(result.fields) expect(result.fields.length).toBe(2); expect(result.fields[0].name.value).toBe('f1'); @@ -219,7 +259,9 @@ describe('Merge Nodes', () => { const type1 = parse(`input A { f1: String }`); const type2 = parse(`input A { f1: String! }`); const merged = mergeGraphQLNodes([...type1.definitions, ...type2.definitions]); - const result: InputObjectTypeDefinitionNode = merged.A as any; + const result = merged.A; + assertInputObjectTypeDefinitionNode(result) + assertSome(result.fields) expect(result.fields.length).toBe(1); expect(result.fields[0].name.value).toBe('f1'); @@ -232,12 +274,16 @@ describe('Merge Nodes', () => { const type1 = parse(`type Query { f1: String }`); const type2 = parse(`type Query { f2: String }`); const merged = mergeGraphQLNodes([...type1.definitions, ...type2.definitions]); - const type: any = merged.Query; + const type = merged.Query; + assertObjectTypeDefinitionNode(type) + assertSome(type.fields) expect(type.fields.length).toBe(2); expect(type.fields[0].name.value).toBe('f1'); expect(type.fields[1].name.value).toBe('f2'); + assertNamedTypeNode(type.fields[0].type) expect(type.fields[0].type.name.value).toBe('String'); + assertNamedTypeNode(type.fields[1].type) expect(type.fields[1].type.name.value).toBe('String'); }); diff --git a/packages/merge/tests/merge-schemas.spec.ts b/packages/merge/tests/merge-schemas.spec.ts index 24aee3cd1ad..5b5b4de7c4a 100644 --- a/packages/merge/tests/merge-schemas.spec.ts +++ b/packages/merge/tests/merge-schemas.spec.ts @@ -1,7 +1,8 @@ import { makeExecutableSchema } from '@graphql-tools/schema'; -import { graphql, buildSchema, GraphQLScalarType, Kind, GraphQLSchema, ListValueNode, print } from 'graphql'; +import { graphql, buildSchema, GraphQLScalarType, Kind, GraphQLSchema, print } from 'graphql'; import { mergeSchemas, mergeSchemasAsync } from '../src/merge-schemas'; -import { printSchemaWithDirectives } from '@graphql-tools/utils'; +import { assertSome, printSchemaWithDirectives } from '@graphql-tools/utils'; +import { assertListValueNode } from '../../testing/assertion'; describe('Merge Schemas', () => { it('Should include extensions in merged schemas', () => { @@ -76,6 +77,7 @@ describe('Merge Schemas', () => { ` }); expect(errors).toBeFalsy(); + assertSome(data) expect(data.foo).toBe('FOO'); expect(data.bar).toBe('BAR'); }); @@ -116,6 +118,7 @@ describe('Merge Schemas', () => { ` }); expect(errors).toBeFalsy(); + assertSome(data) expect(data.foo).toBe('FOO'); expect(data.bar).toBe('BAR'); }); @@ -163,6 +166,7 @@ describe('Merge Schemas', () => { ` }); expect(errors).toBeFalsy(); + assertSome(data) expect(data.foo).toBe('FOO'); expect(data.bar).toBe('BAR'); expect(data.qux).toBe('QUX'); @@ -215,6 +219,7 @@ describe('Merge Schemas', () => { ` }); expect(errors).toBeFalsy(); + assertSome(data) expect(data.foo).toBe('FOO'); expect(data.bar).toBe('BAR'); expect(data.qux).toBe('QUX'); @@ -268,6 +273,7 @@ describe('Merge Schemas', () => { ` }); expect(errors).toBeFalsy(); + assertSome(data) expect(data.foo).toBe('FOO'); expect(data.bar).toBe('BAR'); expect(data.qux).toBe('QUX'); @@ -338,6 +344,7 @@ describe('Merge Schemas', () => { ` }); expect(errors).toBeFalsy(); + assertSome(data) expect(data.bar.foo).toBe('foo'); expect(data.bar.bar).toBe('bar'); expect(data.qux.foo).toBe('foo'); @@ -381,6 +388,7 @@ describe('Merge Schemas', () => { source: /* GraphQL */` { a } ` }); + assertSome(dataA) expect(dataA.a).toEqual(now.toISOString()); // merged schema @@ -388,7 +396,7 @@ describe('Merge Schemas', () => { schema, source: /* GraphQL */` { a } ` }); - + assertSome(data) expect(data.a).toEqual(now.toISOString()); }); @@ -436,8 +444,16 @@ describe('Merge Schemas', () => { while (num--) { prev = mergeSchemas({schemas: [prev, base]}); } + const QueryType = prev.getQueryType() + assertSome(QueryType) + const fields = QueryType.getFields() + assertSome(fields.test.astNode) + assertSome(fields.test.astNode.directives) + assertSome(fields.test.astNode.directives[0]) + assertSome(fields.test.astNode.directives[0].arguments) + assertListValueNode(fields.test.astNode.directives[0].arguments[0].value) - expect((prev.getQueryType().getFields().test.astNode.directives[0].arguments[0].value as ListValueNode).values).toHaveLength(1); + expect(fields.test.astNode.directives[0].arguments[0].value.values).toHaveLength(1); }); it('should merge schemas with custom scalars', () => { const GraphQLUUID = new GraphQLScalarType({ diff --git a/packages/merge/tests/merge-typedefs.spec.ts b/packages/merge/tests/merge-typedefs.spec.ts index 7dfc1f55afd..bb52a8c0a8b 100644 --- a/packages/merge/tests/merge-typedefs.spec.ts +++ b/packages/merge/tests/merge-typedefs.spec.ts @@ -7,7 +7,8 @@ import { stripWhitespaces } from './utils'; import gql from 'graphql-tag'; import { readFileSync } from 'fs'; import { join } from 'path'; -import {jest} from '@jest/globals'; +import { jest } from '@jest/globals'; +import { assertSome } from '@graphql-tools/utils'; const introspectionSchema = JSON.parse(readFileSync(join(__dirname, './schema.json'), 'utf8')); @@ -202,7 +203,7 @@ describe('Merge TypeDefs', () => { const queryType = schema.getQueryType(); expect(queryType).toBeDefined(); - expect(queryType).not.toBeNull(); + assertSome(queryType) expect(queryType.name).toEqual('RootQuery'); }); diff --git a/packages/mock/src/MockList.ts b/packages/mock/src/MockList.ts index 7da5e7446ad..6af0f9f23f5 100644 --- a/packages/mock/src/MockList.ts +++ b/packages/mock/src/MockList.ts @@ -17,7 +17,7 @@ export function isMockList(obj: any): obj is MockList { */ export class MockList { private readonly len: number | Array; - private readonly wrappedFunction: () => unknown; + private readonly wrappedFunction: undefined | (() => unknown); /** * @param length Either the exact length of items to return or an inclusive diff --git a/packages/mock/src/MockStore.ts b/packages/mock/src/MockStore.ts index 14d982eda8c..b21eac7e9b5 100644 --- a/packages/mock/src/MockStore.ts +++ b/packages/mock/src/MockStore.ts @@ -333,7 +333,7 @@ export class MockStore implements IMockStore { return this.normalizeValueToStore( nullableFieldType.ofType, v, - currentValue && currentValue[index] ? currentValue : undefined, + typeof currentValue === 'object' && currentValue != null && currentValue[index] ? currentValue : undefined, onInsertType ); }); @@ -418,7 +418,7 @@ export class MockStore implements IMockStore { value = (values as any)[fieldName]; if (typeof value === 'function') value = value(); - } else if (typeof mock[fieldName] === 'function') { + } else if (typeof mock === 'object' && mock != null && typeof mock[fieldName] === 'function') { value = mock[fieldName](); } } @@ -488,7 +488,7 @@ export class MockStore implements IMockStore { throw new Error(`Please return a __typename in "${nullableType.name}"`); } typeName = values['__typename']; - } else if (typeof mock['__typename'] === 'function') { + } else if (typeof mock === 'object' && mock != null && typeof mock['__typename'] === 'function') { const mockRes = mock['__typename'](); if (typeof mockRes !== 'string') throw new Error(`'__typename' returned by the mock for abstract type ${nullableType.name} is not a string`); diff --git a/packages/mock/src/types.ts b/packages/mock/src/types.ts index 0a366c22d01..92db067b0ba 100644 --- a/packages/mock/src/types.ts +++ b/packages/mock/src/types.ts @@ -190,7 +190,7 @@ export type Ref = { }; export function isRef(maybeRef: unknown): maybeRef is Ref { - return maybeRef && typeof maybeRef === 'object' && '$ref' in maybeRef; + return !!(maybeRef && typeof maybeRef === 'object' && '$ref' in maybeRef); } export function assertIsRef( diff --git a/packages/node-require/package.json b/packages/node-require/package.json index c84f004cb9e..b7c5ba56be8 100644 --- a/packages/node-require/package.json +++ b/packages/node-require/package.json @@ -34,6 +34,7 @@ "dependencies": { "@graphql-tools/load": "^6.2.4", "@graphql-tools/graphql-file-loader": "^6.2.4", + "@graphql-tools/utils": "^7.10.0", "tslib": "~2.3.0" }, "publishConfig": { diff --git a/packages/node-require/src/index.ts b/packages/node-require/src/index.ts index 3b7ee84a928..37006940120 100644 --- a/packages/node-require/src/index.ts +++ b/packages/node-require/src/index.ts @@ -7,6 +7,7 @@ import { loadTypedefsSync } from '@graphql-tools/load'; import { GraphQLFileLoader } from '@graphql-tools/graphql-file-loader'; import { concatAST } from 'graphql'; +import { isSome } from '@graphql-tools/utils'; const VALID_EXTENSIONS = ['graphql', 'graphqls', 'gql', 'gqls']; @@ -16,7 +17,7 @@ function handleModule(m: NodeModule, filename: string) { loaders: [new GraphQLFileLoader()], }); - const documents = sources.map(source => source.document); + const documents = sources.map(source => source.document).filter(isSome); const mergedDoc = concatAST(documents); m.exports = mergedDoc; } diff --git a/packages/resolvers-composition/src/resolvers-composition.ts b/packages/resolvers-composition/src/resolvers-composition.ts index 1ab9acdd68d..927c4b2875d 100644 --- a/packages/resolvers-composition/src/resolvers-composition.ts +++ b/packages/resolvers-composition/src/resolvers-composition.ts @@ -122,7 +122,7 @@ export function composeResolvers>( }); } else if (resolverPathMapping) { Object.keys(resolverPathMapping).forEach(fieldName => { - const composeFns = mapping[resolverPath][fieldName]; + const composeFns = resolverPathMapping[fieldName]; const relevantFields = resolveRelevantMappings(resolvers, resolverPath + '.' + fieldName, mapping); relevantFields.forEach((path: string) => { diff --git a/packages/resolvers-composition/tests/resolvers-composition.spec.ts b/packages/resolvers-composition/tests/resolvers-composition.spec.ts index 38de1ed9468..123f7582100 100644 --- a/packages/resolvers-composition/tests/resolvers-composition.spec.ts +++ b/packages/resolvers-composition/tests/resolvers-composition.spec.ts @@ -53,7 +53,7 @@ describe('Resolvers composition', () => { `, }); expect(result.errors).toBeFalsy(); - expect(result.data.foo).toBe('FOOFOO'); + expect(result.data!.foo).toBe('FOOFOO'); }); it('should compose resolvers with resolve field', async () => { const getFoo = () => 'FOO'; @@ -91,7 +91,7 @@ describe('Resolvers composition', () => { `, }); expect(result.errors).toBeFalsy(); - expect(result.data.foo).toBe('FOOFOO'); + expect(result.data!.foo).toBe('FOOFOO'); }); it('should compose subscription resolvers', async () => { const array1 = [1, 2]; @@ -170,7 +170,7 @@ describe('Resolvers composition', () => { `, }); expect(result.errors).toBeFalsy(); - expect(result.data.foo).toBe('FOOFOO'); + expect(result.data!.foo).toBe('FOOFOO'); }); it('should be able to take nested composition objects for subscription resolvers', async () => { const array1 = [1, 2]; diff --git a/packages/schema/src/addCatchUndefinedToSchema.ts b/packages/schema/src/addCatchUndefinedToSchema.ts index fc3444d9c65..806f81e4a1f 100644 --- a/packages/schema/src/addCatchUndefinedToSchema.ts +++ b/packages/schema/src/addCatchUndefinedToSchema.ts @@ -1,7 +1,10 @@ import { GraphQLFieldResolver, defaultFieldResolver, GraphQLSchema } from 'graphql'; -import { mapSchema, MapperKind } from '@graphql-tools/utils'; +import { mapSchema, MapperKind, Maybe } from '@graphql-tools/utils'; -function decorateToCatchUndefined(fn: GraphQLFieldResolver, hint: string): GraphQLFieldResolver { +function decorateToCatchUndefined( + fn: Maybe>, + hint: string +): GraphQLFieldResolver { const resolve = fn == null ? defaultFieldResolver : fn; return (root, args, ctx, info) => { const result = resolve(root, args, ctx, info); diff --git a/packages/schema/src/addResolversToSchema.ts b/packages/schema/src/addResolversToSchema.ts index 6e486382d37..5364bacf13b 100644 --- a/packages/schema/src/addResolversToSchema.ts +++ b/packages/schema/src/addResolversToSchema.ts @@ -41,7 +41,7 @@ export function addResolversToSchema( const options: IAddResolversToSchemaOptions = isSchema(schemaOrOptions) ? { schema: schemaOrOptions, - resolvers: legacyInputResolvers, + resolvers: legacyInputResolvers ?? {}, resolverValidationOptions: legacyInputValidationOptions, } : schemaOrOptions; @@ -162,7 +162,7 @@ export function addResolversToSchema( function addResolversToExistingSchema( schema: GraphQLSchema, resolvers: IResolvers, - defaultFieldResolver: GraphQLFieldResolver + defaultFieldResolver?: GraphQLFieldResolver ): GraphQLSchema { const typeMap = schema.getTypeMap(); getAllPropertyNames(resolvers).forEach(typeName => { @@ -183,7 +183,7 @@ function addResolversToExistingSchema( ), }; } else if (fieldName === 'extensionASTNodes' && type.extensionASTNodes != null) { - type.extensionASTNodes = ([] ?? type.extensionASTNodes).concat( + type.extensionASTNodes = type.extensionASTNodes.concat( (resolverValue as GraphQLScalarType)?.extensionASTNodes ?? [] ); } else if ( @@ -279,7 +279,7 @@ function addResolversToExistingSchema( function createNewSchemaWithResolvers( schema: GraphQLSchema, resolvers: IResolvers, - defaultFieldResolver: GraphQLFieldResolver + defaultFieldResolver?: GraphQLFieldResolver ): GraphQLSchema { schema = mapSchema(schema, { [MapperKind.SCALAR_TYPE]: type => { diff --git a/packages/schema/src/assertResolversPresent.ts b/packages/schema/src/assertResolversPresent.ts index 9026b858a27..3bb4dcf0376 100644 --- a/packages/schema/src/assertResolversPresent.ts +++ b/packages/schema/src/assertResolversPresent.ts @@ -6,11 +6,8 @@ export function assertResolversPresent( schema: GraphQLSchema, resolverValidationOptions: IResolverValidationOptions = {} ): void { - const { - requireResolversForArgs, - requireResolversForNonScalar, - requireResolversForAllFields, - } = resolverValidationOptions; + const { requireResolversForArgs, requireResolversForNonScalar, requireResolversForAllFields } = + resolverValidationOptions; if (requireResolversForAllFields && (requireResolversForArgs || requireResolversForNonScalar)) { throw new TypeError( @@ -40,7 +37,7 @@ export function assertResolversPresent( function expectResolver( validator: string, - behavior: ValidatorBehavior, + behavior: ValidatorBehavior | undefined, field: GraphQLField, typeName: string, fieldName: string diff --git a/packages/schema/src/chainResolvers.ts b/packages/schema/src/chainResolvers.ts index 903382e6db4..8daf3e9fd14 100644 --- a/packages/schema/src/chainResolvers.ts +++ b/packages/schema/src/chainResolvers.ts @@ -1,7 +1,10 @@ import { defaultFieldResolver, GraphQLResolveInfo, GraphQLFieldResolver } from 'graphql'; +import { Maybe } from '@graphql-tools/utils'; -export function chainResolvers(resolvers: Array>) { - return (root: any, args: { [argName: string]: any }, ctx: any, info: GraphQLResolveInfo) => +export function chainResolvers( + resolvers: Array>> +) { + return (root: any, args: TArgs, ctx: any, info: GraphQLResolveInfo) => resolvers.reduce((prev, curResolver) => { if (curResolver != null) { return curResolver(prev, args, ctx, info); diff --git a/packages/schema/src/checkForResolveTypeResolver.ts b/packages/schema/src/checkForResolveTypeResolver.ts index 595776b1dfc..6dcf9508f7d 100644 --- a/packages/schema/src/checkForResolveTypeResolver.ts +++ b/packages/schema/src/checkForResolveTypeResolver.ts @@ -3,7 +3,7 @@ import { GraphQLSchema } from 'graphql'; import { MapperKind, mapSchema, ValidatorBehavior } from '@graphql-tools/utils'; // If we have any union or interface types throw if no there is no resolveType resolver -export function checkForResolveTypeResolver(schema: GraphQLSchema, requireResolversForResolveType: ValidatorBehavior) { +export function checkForResolveTypeResolver(schema: GraphQLSchema, requireResolversForResolveType?: ValidatorBehavior) { mapSchema(schema, { [MapperKind.ABSTRACT_TYPE]: type => { if (!type.resolveType) { diff --git a/packages/schema/src/decorateWithLogger.ts b/packages/schema/src/decorateWithLogger.ts index 3d1be51cdce..3345a1e8de3 100644 --- a/packages/schema/src/decorateWithLogger.ts +++ b/packages/schema/src/decorateWithLogger.ts @@ -1,4 +1,5 @@ import { defaultFieldResolver, GraphQLFieldResolver } from 'graphql'; +import { Maybe } from 'packages/graphql-tools/src'; import { ILogger } from './types'; /* @@ -7,7 +8,7 @@ import { ILogger } from './types'; * hint: an optional hint to add to the error's message */ export function decorateWithLogger( - fn: GraphQLFieldResolver, + fn: Maybe>, logger: ILogger, hint: string ): GraphQLFieldResolver { diff --git a/packages/schema/tests/Logger.ts b/packages/schema/tests/Logger.ts index 8495a16c371..5a50ace70e9 100644 --- a/packages/schema/tests/Logger.ts +++ b/packages/schema/tests/Logger.ts @@ -7,7 +7,7 @@ import { ILogger } from '@graphql-tools/schema'; export class Logger implements ILogger { public errors: Array; public name: string | undefined; - private readonly callback: (...args: any[]) => any | undefined; + private readonly callback: undefined | ((...args: any[]) => any); constructor(name?: string, callback?: (...args: any[]) => any) { this.name = name; diff --git a/packages/schema/tests/logger.test.ts b/packages/schema/tests/logger.test.ts index 4171bce3533..d7fc6c0e1e0 100644 --- a/packages/schema/tests/logger.test.ts +++ b/packages/schema/tests/logger.test.ts @@ -63,7 +63,7 @@ describe('Logger', () => { }, }, }; - let loggedErr: Error = null; + let loggedErr: Error | null = null; const logger = new Logger('LoggyMcLogface', (e: Error) => { loggedErr = e; }); @@ -172,7 +172,7 @@ describe('Logger', () => { }, }; - let loggedErr: Error = null; + let loggedErr: Error | null = null; const logger = new Logger('LoggyMcLogface', (e: Error) => { loggedErr = e; }); @@ -237,7 +237,7 @@ describe('providing useful errors from resolvers', () => { `; const resolve = { RootQuery: { - species: (): string => undefined, + species: (): string | undefined => undefined, stuff: () => 'stuff', }, }; @@ -251,7 +251,7 @@ describe('providing useful errors from resolvers', () => { }); const testQuery = '{ species, stuff }'; const expectedErr = /Resolver for "RootQuery.species" returned undefined/; - const expectedResData = { species: null as string, stuff: 'stuff' }; + const expectedResData = { species: null as string | null, stuff: 'stuff' }; return graphql(jsSchema, testQuery).then((res) => { expect(logger.errors.length).toEqual(1); expect(logger.errors[0].message).toMatch(expectedErr); @@ -330,7 +330,7 @@ describe('providing useful errors from resolvers', () => { } }`; return graphql(jsSchema, testQuery).then((res) => { - expect(res.errors[0].originalError.message).toBe( + expect(res.errors?.[0].originalError?.message).toBe( 'Resolver for "Thread.name" returned undefined', ); }); @@ -388,7 +388,7 @@ describe('providing useful errors from resolvers', () => { `; const resolve = { RootQuery: { - species: (): string => undefined, + species: (): string | undefined => undefined, stuff: () => 'stuff', }, }; @@ -400,7 +400,7 @@ describe('providing useful errors from resolvers', () => { logger, }); const testQuery = '{ species, stuff }'; - const expectedResData = { species: null as string, stuff: 'stuff' }; + const expectedResData = { species: null as string | null, stuff: 'stuff' }; return graphql(jsSchema, testQuery).then((res) => { expect(logger.errors.length).toEqual(0); expect(res.data).toEqual(expectedResData); diff --git a/packages/schema/tests/schemaGenerator.test.ts b/packages/schema/tests/schemaGenerator.test.ts index cf4295d2661..483192650a7 100644 --- a/packages/schema/tests/schemaGenerator.test.ts +++ b/packages/schema/tests/schemaGenerator.test.ts @@ -21,6 +21,7 @@ import { GraphQLBoolean, graphqlSync, GraphQLSchema, + GraphQLFieldResolver, } from 'graphql'; import { @@ -55,7 +56,7 @@ interface Bird { function expectWarning(fn: () => void, warnMatcher?: string) { // eslint-disable-next-line no-console const originalWarn = console.warn; - let warning: string = null; + let warning: string | null = null; try { // eslint-disable-next-line no-console @@ -97,11 +98,13 @@ const testResolvers = { describe('generating schema from shorthand', () => { test('throws an error if no schema is provided', () => { + // @ts-expect-error: we call it with invalid params expect(() => makeExecutableSchema(undefined)).toThrowError('undefined'); }); test('throws an error if typeDefinitionNodes are not provided', () => { expect(() => + // @ts-expect-error: we call it with invalid params makeExecutableSchema({ typeDefs: undefined, resolvers: {} }), ).toThrowError('Must provide typeDefs'); }); @@ -218,7 +221,7 @@ describe('generating schema from shorthand', () => { name: 'name', type: { kind: 'NON_NULL', - name: null as string, + name: null as string | null, ofType: { name: 'String', }, @@ -242,7 +245,7 @@ describe('generating schema from shorthand', () => { name: 'species', type: { kind: 'LIST', - name: null as string, + name: null as string | null, ofType: { name: 'BirdSpecies', }, @@ -251,7 +254,7 @@ describe('generating schema from shorthand', () => { { name: 'name', type: { - name: null as string, + name: null as string | null, kind: 'NON_NULL', ofType: { name: 'String', @@ -293,7 +296,7 @@ describe('generating schema from shorthand', () => { typeDefs: typeDefAry, resolvers: {}, }); - expect(jsSchema.getQueryType().name).toBe('Query'); + expect(jsSchema.getQueryType()?.name).toBe('Query'); }); test('can generate a schema from a parsed type definition', () => { @@ -310,7 +313,7 @@ describe('generating schema from shorthand', () => { typeDefs: typeDefSchema, resolvers: {}, }); - expect(jsSchema.getQueryType().name).toBe('Query'); + expect(jsSchema.getQueryType()?.name).toBe('Query'); }); test('can generate a schema from an array of parsed and none parsed type definitions', () => { @@ -330,7 +333,7 @@ describe('generating schema from shorthand', () => { typeDefs: typeDefSchema, resolvers: {}, }); - expect(jsSchema.getQueryType().name).toBe('Query'); + expect(jsSchema.getQueryType()?.name).toBe('Query'); }); test('can generate a schema from an array of types with extensions', () => { @@ -356,9 +359,9 @@ describe('generating schema from shorthand', () => { typeDefs: typeDefAry, resolvers: {}, }); - expect(jsSchema.getQueryType().name).toBe('Query'); - expect(jsSchema.getQueryType().getFields().foo).toBeDefined(); - expect(jsSchema.getQueryType().getFields().bar).toBeDefined(); + expect(jsSchema.getQueryType()?.name).toBe('Query'); + expect(jsSchema.getQueryType()?.getFields().foo).toBeDefined(); + expect(jsSchema.getQueryType()?.getFields().bar).toBeDefined(); }); test('allow for a map of extensions in field resolver', () => { @@ -381,9 +384,9 @@ describe('generating schema from shorthand', () => { }, }, }); - const extensions = jsSchema.getQueryType().getFields().foo.extensions; + const extensions = jsSchema.getQueryType()?.getFields().foo.extensions; expect(extensions).toHaveProperty('verbose'); - expect(extensions.verbose).toBe(true); + expect(extensions!.verbose).toBe(true); }); test('can concatenateTypeDefs created by a function inside a closure', () => { @@ -433,7 +436,7 @@ describe('generating schema from shorthand', () => { typeDefs: typeDefAry, resolvers: {}, }); - expect(jsSchema.getQueryType().name).toBe('Query'); + expect(jsSchema.getQueryType()?.name).toBe('Query'); }); test('works with imports, even circular ones', () => { @@ -459,7 +462,7 @@ describe('generating schema from shorthand', () => { TypeB: { a: () => null }, }, }); - expect(jsSchema.getQueryType().name).toBe('Query'); + expect(jsSchema.getQueryType()?.name).toBe('Query'); }); test('can generate a schema with resolvers', () => { @@ -804,13 +807,13 @@ describe('generating schema from shorthand', () => { typeDefs: shorthand, resolvers: resolveFunctions, }); - expect(jsSchema.getQueryType().name).toBe('Query'); + expect(jsSchema.getQueryType()?.name).toBe('Query'); for (const scalarName of scalarNames) { expect(jsSchema.getType(scalarName)).toBeInstanceOf(GraphQLScalarType); expect(jsSchema.getType(scalarName)).toHaveProperty('description'); - expect(typeof jsSchema.getType(scalarName).description).toBe('string'); + expect(typeof jsSchema.getType(scalarName)?.description).toBe('string'); expect( - jsSchema.getType(scalarName).description.length, + jsSchema.getType(scalarName)?.description?.length, ).toBeGreaterThan(0); } }); @@ -832,7 +835,7 @@ describe('generating schema from shorthand', () => { typeDefs: shorthand, resolvers: resolveFunctions, }); - expect(jsSchema.getQueryType().name).toBe('Query'); + expect(jsSchema.getQueryType()?.name).toBe('Query'); expect(jsSchema.getType('Boolean')).toBe(GraphQLBoolean); }); @@ -868,7 +871,7 @@ describe('generating schema from shorthand', () => { } `; const result = graphqlSync(jsSchema, testQuery); - expect(result.data.foo.aField).toBe(false); + expect(result.data!.foo.aField).toBe(false); jsSchema = addResolversToSchema({ schema: jsSchema, resolvers: { @@ -905,7 +908,7 @@ describe('generating schema from shorthand', () => { const testType = schema.getType('Test'); expect(testType).toBeInstanceOf(GraphQLScalarType); - expect(testType.astNode.directives.length).toBe(1); + expect(testType!.astNode!.directives!.length).toBe(1); }); test('retains scalars after walking/recreating the schema', () => { @@ -967,7 +970,7 @@ describe('generating schema from shorthand', () => { ); expect(walkedSchema.getType('Test')).toBeInstanceOf(GraphQLScalarType); expect(walkedSchema.getType('Test')).toHaveProperty('description'); - expect(walkedSchema.getType('Test').description).toBe('Test resolver'); + expect(walkedSchema.getType('Test')!.description).toBe('Test resolver'); const testQuery = ` { test @@ -1096,7 +1099,7 @@ describe('generating schema from shorthand', () => { `; const resultPromise = graphql(jsSchema, testQuery); return resultPromise.then((result) => { - expect(result.data.post.something).toEqual(testValue); + expect(result.data!.post.something).toEqual(testValue); expect(result.errors).toEqual(undefined); }); }); @@ -1166,7 +1169,7 @@ describe('generating schema from shorthand', () => { `; const resultPromise = graphql(jsSchema, testQuery); return resultPromise.then((result) => { - expect(result.data.post.something).toEqual(testDate.getTime()); + expect(result.data!.post.something).toEqual(testDate.getTime()); expect(result.errors).toEqual(undefined); }); }); @@ -1207,7 +1210,7 @@ describe('generating schema from shorthand', () => { resolvers: resolveFunctions, }); - expect(jsSchema.getQueryType().name).toBe('Query'); + expect(jsSchema.getQueryType()!.name).toBe('Query'); expect(jsSchema.getType('Color')).toBeInstanceOf(GraphQLEnumType); expect(jsSchema.getType('NumericEnum')).toBeInstanceOf(GraphQLEnumType); }); @@ -1268,9 +1271,9 @@ describe('generating schema from shorthand', () => { const resultPromise = graphql(jsSchema, testQuery); return resultPromise.then((result) => { - expect(result.data.redColor).toEqual('RED'); - expect(result.data.blueColor).toEqual('BLUE'); - expect(result.data.numericEnum).toEqual('TEST'); + expect(result.data!.redColor).toEqual('RED'); + expect(result.data!.blueColor).toEqual('BLUE'); + expect(result.data!.numericEnum).toEqual('TEST'); expect(result.errors).toEqual(undefined); }); }); @@ -1327,9 +1330,9 @@ describe('generating schema from shorthand', () => { const resultPromise = graphql(jsSchema, testQuery); return resultPromise.then((result) => { - expect(result.data.red).toEqual(resolveFunctions.Color.RED); - expect(result.data.blue).toEqual(resolveFunctions.Color.BLUE); - expect(result.data.num).toEqual(resolveFunctions.NumericEnum.TEST); + expect(result.data!.red).toEqual(resolveFunctions.Color.RED); + expect(result.data!.blue).toEqual(resolveFunctions.Color.BLUE); + expect(result.data!.num).toEqual(resolveFunctions.NumericEnum.TEST); expect(result.errors).toEqual(undefined); }); }); @@ -1373,7 +1376,7 @@ describe('generating schema from shorthand', () => { const resultPromise = graphql(jsSchema, testQuery); return resultPromise.then((result) => { - expect(result.data.red).toEqual(resolveFunctions.Color.RED); + expect(result.data!.red).toEqual(resolveFunctions.Color.RED); expect(result.errors).toEqual(undefined); }); }); @@ -1424,7 +1427,7 @@ describe('generating schema from shorthand', () => { const resultPromise = graphql(jsSchema, testQuery); return resultPromise.then((result) => { - expect(result.data.red).toEqual('override'); + expect(result.data!.red).toEqual('override'); expect(result.errors).toEqual(undefined); }); }); @@ -1472,7 +1475,7 @@ describe('generating schema from shorthand', () => { const resultPromise = graphql(jsSchema, testQuery); return resultPromise.then((result) => { - expect(result.data.red).toEqual('#EA3232'); + expect(result.data!.red).toEqual('#EA3232'); expect(result.errors).toEqual(undefined); }); }); @@ -1648,7 +1651,7 @@ To disable this validator, use: makeExecutableSchema.bind(null, { typeDefs: short, resolvers: rf, - resolverValidationOptions: { requireResolversForNonScalar: false }, + resolverValidationOptions: { requireResolversForNonScalar: 'ignore' }, }), ).not.toThrow(); }); @@ -2038,7 +2041,7 @@ describe('Attaching external data fetchers to schema', () => { species(name: "strix") }`; return graphql(jsSchema, query).then((res) => { - expect(res.data.species).toBe('ROOTstrix'); + expect(res.data!.species).toBe('ROOTstrix'); }); }); @@ -2053,7 +2056,7 @@ describe('Attaching external data fetchers to schema', () => { stuff }`; return graphql(jsSchema, query).then((res) => { - expect(res.data.stuff).toBe('stuff'); + expect(res.data!.stuff).toBe('stuff'); }); }); @@ -2184,11 +2187,15 @@ describe('Generating a full graphQL schema with resolvers and connectors', () => describe('chainResolvers', () => { test('can chain two resolvers', () => { - const r1 = (root: number) => root + 1; - const r2 = (root: number, { addend }: { addend: number }) => root + addend; + const r1: GraphQLFieldResolver = (root: number) => root + 1; + const r2: GraphQLFieldResolver = (root: number, { addend }) => root + addend; + + const info: GraphQLResolveInfo = ({ + fieldName: 'addend', + } as unknown) as GraphQLResolveInfo; const rChained = chainResolvers([r1, r2]); - expect(rChained(0, { addend: 2 }, null, null)).toBe(3); + expect(rChained(0, { addend: 2 }, null, info)).toBe(3); }); test('uses default resolver when a resolver is undefined', () => { @@ -2464,7 +2471,7 @@ describe('can specify lexical parser options', () => { }, }); - expect(schema.astNode.loc).toBeUndefined(); + expect(schema.astNode!.loc).toBeUndefined(); }); test("can specify 'experimentalFragmentVariables' option", () => { @@ -2528,7 +2535,7 @@ describe('can specify lexical parser options', () => { document.definitions.forEach((def) => { if (def.kind === Kind.FRAGMENT_DEFINITION) { - variableDefs = variableDefs.concat(def.variableDefinitions); + variableDefs = variableDefs.concat(def.variableDefinitions!); } }); diff --git a/packages/stitch/src/createMergedTypeResolver.ts b/packages/stitch/src/createMergedTypeResolver.ts index 21bb0dbc1ed..dfae35b8abf 100644 --- a/packages/stitch/src/createMergedTypeResolver.ts +++ b/packages/stitch/src/createMergedTypeResolver.ts @@ -2,7 +2,9 @@ import { getNamedType, GraphQLOutputType, GraphQLList } from 'graphql'; import { delegateToSchema, MergedTypeResolver, MergedTypeResolverOptions } from '@graphql-tools/delegate'; import { batchDelegateToSchema } from '@graphql-tools/batch-delegate'; -export function createMergedTypeResolver(mergedTypeResolverOptions: MergedTypeResolverOptions): MergedTypeResolver { +export function createMergedTypeResolver( + mergedTypeResolverOptions: MergedTypeResolverOptions +): MergedTypeResolver | undefined { const { fieldName, argsFromKeys, valuesFromResults, args } = mergedTypeResolverOptions; if (argsFromKeys != null) { diff --git a/packages/stitch/src/mergeCandidates.ts b/packages/stitch/src/mergeCandidates.ts index 9d4173b8c85..bc69006f02c 100644 --- a/packages/stitch/src/mergeCandidates.ts +++ b/packages/stitch/src/mergeCandidates.ts @@ -26,6 +26,12 @@ import { GraphQLScalarSerializer, GraphQLScalarValueParser, GraphQLScalarLiteralParser, + ObjectTypeExtensionNode, + InputObjectTypeExtensionNode, + InterfaceTypeExtensionNode, + UnionTypeExtensionNode, + EnumTypeExtensionNode, + ScalarTypeExtensionNode, } from 'graphql'; import { mergeType, mergeInputType, mergeInterface, mergeUnion, mergeEnum, mergeScalar } from '@graphql-tools/merge'; @@ -44,13 +50,13 @@ import { validateInputObjectConsistency, } from './mergeValidations'; -import { fieldToFieldConfig, inputFieldToFieldConfig } from '@graphql-tools/utils'; +import { fieldToFieldConfig, inputFieldToFieldConfig, Maybe } from '@graphql-tools/utils'; import { isSubschemaConfig } from '@graphql-tools/delegate'; -export function mergeCandidates( +export function mergeCandidates>( typeName: string, - candidates: Array, - typeMergingOptions: TypeMergingOptions + candidates: Array>, + typeMergingOptions?: TypeMergingOptions ): GraphQLNamedType { const initialCandidateType = candidates[0].type; if (candidates.some(candidate => candidate.type.constructor !== initialCandidateType.constructor)) { @@ -74,10 +80,10 @@ export function mergeCandidates( } } -function mergeObjectTypeCandidates( +function mergeObjectTypeCandidates>( typeName: string, - candidates: Array, - typeMergingOptions: TypeMergingOptions + candidates: Array>, + typeMergingOptions?: TypeMergingOptions ): GraphQLObjectType { candidates = orderedTypeCandidates(candidates, typeMergingOptions); @@ -116,7 +122,7 @@ function mergeObjectTypeCandidates( astNodes[0] ); - const extensionASTNodes = [].concat(pluck>('extensionASTNodes', candidates)); + const extensionASTNodes = pluck('extensionASTNodes', candidates); const extensions = Object.assign({}, ...pluck>('extensions', candidates)); @@ -133,10 +139,10 @@ function mergeObjectTypeCandidates( return new GraphQLObjectType(typeConfig); } -function mergeInputObjectTypeCandidates( +function mergeInputObjectTypeCandidates>( typeName: string, - candidates: Array, - typeMergingOptions: TypeMergingOptions + candidates: Array>, + typeMergingOptions?: TypeMergingOptions ): GraphQLInputObjectType { candidates = orderedTypeCandidates(candidates, typeMergingOptions); @@ -163,7 +169,7 @@ function mergeInputObjectTypeCandidates( astNodes[0] ); - const extensionASTNodes = [].concat(pluck>('extensionASTNodes', candidates)); + const extensionASTNodes = pluck('extensionASTNodes', candidates); const extensions = Object.assign({}, ...pluck>('extensions', candidates)); @@ -179,14 +185,14 @@ function mergeInputObjectTypeCandidates( return new GraphQLInputObjectType(typeConfig); } -function pluck(typeProperty: string, candidates: Array): Array { +function pluck(typeProperty: string, candidates: Array>): Array { return candidates.map(candidate => candidate.type[typeProperty]).filter(value => value != null) as Array; } -function mergeInterfaceTypeCandidates( +function mergeInterfaceTypeCandidates>( typeName: string, - candidates: Array, - typeMergingOptions: TypeMergingOptions + candidates: Array>, + typeMergingOptions?: TypeMergingOptions ): GraphQLInterfaceType { candidates = orderedTypeCandidates(candidates, typeMergingOptions); @@ -194,7 +200,7 @@ function mergeInterfaceTypeCandidates( const fields = fieldConfigMapFromTypeCandidates(candidates, typeMergingOptions); const typeConfigs = candidates.map(candidate => (candidate.type as GraphQLInterfaceType).toConfig()); const interfaceMap = typeConfigs - .map(typeConfig => ((typeConfig as unknown) as { interfaces: Array }).interfaces) + .map(typeConfig => (typeConfig as unknown as { interfaces: Array }).interfaces) .reduce((acc, interfaces) => { if (interfaces != null) { interfaces.forEach(iface => { @@ -225,7 +231,7 @@ function mergeInterfaceTypeCandidates( astNodes[0] ); - const extensionASTNodes = [].concat(pluck>('extensionASTNodes', candidates)); + const extensionASTNodes = pluck('extensionASTNodes', candidates); const extensions = Object.assign({}, ...pluck>('extensions', candidates)); @@ -242,10 +248,10 @@ function mergeInterfaceTypeCandidates( return new GraphQLInterfaceType(typeConfig); } -function mergeUnionTypeCandidates( +function mergeUnionTypeCandidates>( typeName: string, - candidates: Array, - typeMergingOptions: TypeMergingOptions + candidates: Array>, + typeMergingOptions?: TypeMergingOptions ): GraphQLUnionType { candidates = orderedTypeCandidates(candidates, typeMergingOptions); const description = mergeTypeDescriptions(candidates, typeMergingOptions); @@ -267,7 +273,7 @@ function mergeUnionTypeCandidates( astNodes[0] ); - const extensionASTNodes = [].concat(pluck>('extensionASTNodes', candidates)); + const extensionASTNodes = pluck('extensionASTNodes', candidates); const extensions = Object.assign({}, ...pluck>('extensions', candidates)); @@ -283,10 +289,10 @@ function mergeUnionTypeCandidates( return new GraphQLUnionType(typeConfig); } -function mergeEnumTypeCandidates( +function mergeEnumTypeCandidates>( typeName: string, - candidates: Array, - typeMergingOptions: TypeMergingOptions + candidates: Array>, + typeMergingOptions?: TypeMergingOptions ): GraphQLEnumType { candidates = orderedTypeCandidates(candidates, typeMergingOptions); @@ -302,7 +308,7 @@ function mergeEnumTypeCandidates( astNodes[0] ); - const extensionASTNodes = [].concat(pluck>('extensionASTNodes', candidates)); + const extensionASTNodes = pluck('extensionASTNodes', candidates); const extensions = Object.assign({}, ...pluck>('extensions', candidates)); @@ -319,8 +325,8 @@ function mergeEnumTypeCandidates( } function enumValueConfigMapFromTypeCandidates( - candidates: Array, - typeMergingOptions: TypeMergingOptions + candidates: Array>, + typeMergingOptions?: TypeMergingOptions ): GraphQLEnumValueConfigMap { const enumValueConfigCandidatesMap: Record> = Object.create(null); @@ -361,10 +367,10 @@ function defaultEnumValueConfigMerger(candidates: Array>( typeName: string, - candidates: Array, - typeMergingOptions: TypeMergingOptions + candidates: Array>, + typeMergingOptions?: TypeMergingOptions ): GraphQLScalarType { candidates = orderedTypeCandidates(candidates, typeMergingOptions); @@ -386,7 +392,7 @@ function mergeScalarTypeCandidates( astNodes[0] ); - const extensionASTNodes = [].concat(pluck>('extensionASTNodes', candidates)); + const extensionASTNodes = pluck('extensionASTNodes', candidates); const extensions = Object.assign({}, ...pluck>('extensions', candidates)); @@ -404,17 +410,19 @@ function mergeScalarTypeCandidates( return new GraphQLScalarType(typeConfig); } -function orderedTypeCandidates( - candidates: Array, - typeMergingOptions: TypeMergingOptions -): Array { +function orderedTypeCandidates>( + candidates: Array>, + typeMergingOptions?: TypeMergingOptions +): Array> { const typeCandidateMerger = typeMergingOptions?.typeCandidateMerger ?? defaultTypeCandidateMerger; const candidate = typeCandidateMerger(candidates); return candidates.filter(c => c !== candidate).concat([candidate]); } -function defaultTypeCandidateMerger(candidates: Array): MergeTypeCandidate { - const canonical: Array = candidates.filter(({ type, transformedSubschema }) => +function defaultTypeCandidateMerger>( + candidates: Array> +): MergeTypeCandidate { + const canonical: Array> = candidates.filter(({ type, transformedSubschema }) => isSubschemaConfig(transformedSubschema) ? transformedSubschema.merge?.[type.name]?.canonical : false ); @@ -427,20 +435,25 @@ function defaultTypeCandidateMerger(candidates: Array): Merg return candidates[candidates.length - 1]; } -function mergeTypeDescriptions(candidates: Array, typeMergingOptions: TypeMergingOptions): string { +function mergeTypeDescriptions>( + candidates: Array>, + typeMergingOptions?: TypeMergingOptions +): Maybe { const typeDescriptionsMerger = typeMergingOptions?.typeDescriptionsMerger ?? defaultTypeDescriptionMerger; return typeDescriptionsMerger(candidates); } -function defaultTypeDescriptionMerger(candidates: Array): string { +function defaultTypeDescriptionMerger>( + candidates: Array> +): Maybe { return candidates[candidates.length - 1].type.description; } -function fieldConfigMapFromTypeCandidates( - candidates: Array, - typeMergingOptions: TypeMergingOptions +function fieldConfigMapFromTypeCandidates>( + candidates: Array>, + typeMergingOptions?: TypeMergingOptions ): GraphQLFieldConfigMap { - const fieldConfigCandidatesMap: Record> = Object.create(null); + const fieldConfigCandidatesMap: Record>> = Object.create(null); candidates.forEach(candidate => { const fieldMap = (candidate.type as GraphQLObjectType | GraphQLInterfaceType).getFields(); @@ -470,14 +483,19 @@ function fieldConfigMapFromTypeCandidates( return fieldConfigMap; } -function mergeFieldConfigs(candidates: Array, typeMergingOptions: TypeMergingOptions) { +function mergeFieldConfigs>( + candidates: Array>, + typeMergingOptions?: TypeMergingOptions +) { const fieldConfigMerger = typeMergingOptions?.fieldConfigMerger ?? defaultFieldConfigMerger; const finalFieldConfig = fieldConfigMerger(candidates); validateFieldConsistency(finalFieldConfig, candidates, typeMergingOptions); return finalFieldConfig; } -function defaultFieldConfigMerger(candidates: Array) { +function defaultFieldConfigMerger>( + candidates: Array> +) { const canonicalByField: Array> = []; const canonicalByType: Array> = []; @@ -501,11 +519,12 @@ function defaultFieldConfigMerger(candidates: Array) return candidates[candidates.length - 1].fieldConfig; } -function inputFieldConfigMapFromTypeCandidates( - candidates: Array, - typeMergingOptions: TypeMergingOptions +function inputFieldConfigMapFromTypeCandidates>( + candidates: Array>, + typeMergingOptions?: TypeMergingOptions ): GraphQLInputFieldConfigMap { - const inputFieldConfigCandidatesMap: Record> = Object.create(null); + const inputFieldConfigCandidatesMap: Record>> = + Object.create(null); const fieldInclusionMap: Record = Object.create(null); candidates.forEach(candidate => { @@ -547,7 +566,9 @@ function inputFieldConfigMapFromTypeCandidates( return inputFieldConfigMap; } -function defaultInputFieldConfigMerger(candidates: Array) { +function defaultInputFieldConfigMerger>( + candidates: Array> +) { const canonicalByField: Array = []; const canonicalByType: Array = []; @@ -571,16 +592,16 @@ function defaultInputFieldConfigMerger(candidates: Array): Array { +function canonicalFieldNamesForType(candidates: Array>): Array { const canonicalFieldNames: Record = Object.create(null); candidates.forEach(({ type, transformedSubschema }) => { - if ( - isSubschemaConfig(transformedSubschema) && - transformedSubschema.merge?.[type.name]?.fields && - !transformedSubschema.merge[type.name].canonical - ) { - Object.entries(transformedSubschema.merge[type.name].fields).forEach(([fieldName, mergedFieldConfig]) => { + if (!isSubschemaConfig(transformedSubschema)) { + return; + } + const mergeConfig = transformedSubschema.merge?.[type.name]; + if (mergeConfig != null && mergeConfig.fields != null && !mergeConfig.canonical) { + Object.entries(mergeConfig.fields).forEach(([fieldName, mergedFieldConfig]) => { if (mergedFieldConfig.canonical) { canonicalFieldNames[fieldName] = true; } diff --git a/packages/stitch/src/mergeValidations.ts b/packages/stitch/src/mergeValidations.ts index 862ef8fa000..c79a03bb1e7 100644 --- a/packages/stitch/src/mergeValidations.ts +++ b/packages/stitch/src/mergeValidations.ts @@ -21,10 +21,10 @@ import { ValidationLevel, } from './types'; -export function validateFieldConsistency( +export function validateFieldConsistency>( finalFieldConfig: GraphQLFieldConfig, - candidates: Array, - typeMergingOptions: TypeMergingOptions + candidates: Array>, + typeMergingOptions?: TypeMergingOptions ): void { const fieldNamespace = `${candidates[0].type.name}.${candidates[0].fieldName}`; const finalFieldNull = isNonNullType(finalFieldConfig.type); @@ -56,6 +56,9 @@ export function validateFieldConsistency( const argCandidatesMap: Record> = Object.create(null); candidates.forEach(({ fieldConfig }) => { + if (fieldConfig.args == null) { + return; + } Object.entries(fieldConfig.args).forEach(([argName, arg]) => { argCandidatesMap[argName] = argCandidatesMap[argName] || []; argCandidatesMap[argName].push(arg); @@ -71,6 +74,9 @@ export function validateFieldConsistency( } Object.entries(argCandidatesMap).forEach(([argName, argCandidates]) => { + if (finalFieldConfig.args == null) { + return; + } const argNamespace = `${fieldNamespace}.${argName}`; const finalArgConfig = finalFieldConfig.args[argName] || argCandidates[argCandidates.length - 1]; const finalArgType = getNamedType(finalArgConfig.type); @@ -101,10 +107,10 @@ export function validateFieldConsistency( }); } -export function validateInputObjectConsistency( +export function validateInputObjectConsistency>( fieldInclusionMap: Record, - candidates: Array, - typeMergingOptions: TypeMergingOptions + candidates: Array>, + typeMergingOptions?: TypeMergingOptions ): void { Object.entries(fieldInclusionMap).forEach(([fieldName, count]) => { if (candidates.length !== count) { @@ -118,10 +124,10 @@ export function validateInputObjectConsistency( }); } -export function validateInputFieldConsistency( +export function validateInputFieldConsistency>( finalInputFieldConfig: GraphQLInputFieldConfig, - candidates: Array, - typeMergingOptions: TypeMergingOptions + candidates: Array>, + typeMergingOptions?: TypeMergingOptions ): void { const inputFieldNamespace = `${candidates[0].type.name}.${candidates[0].fieldName}`; const inputFieldConfigs = candidates.map(c => c.inputFieldConfig); @@ -158,12 +164,12 @@ export function validateInputFieldConsistency( } } -export function validateTypeConsistency( +export function validateTypeConsistency>( finalElementConfig: GraphQLFieldConfig | GraphQLArgumentConfig | GraphQLInputFieldConfig, candidates: Array | GraphQLArgumentConfig | GraphQLInputFieldConfig>, definitionType: string, settingNamespace: string, - typeMergingOptions: TypeMergingOptions + typeMergingOptions?: TypeMergingOptions ): void { const finalNamedType = getNamedType(finalElementConfig.type); const finalIsScalar = isScalarType(finalNamedType); @@ -202,10 +208,10 @@ function hasListType(type: GraphQLType): boolean { return isListType(getNullableType(type)); } -export function validateInputEnumConsistency( +export function validateInputEnumConsistency>( inputEnumType: GraphQLEnumType, candidates: Array, - typeMergingOptions: TypeMergingOptions + typeMergingOptions?: TypeMergingOptions ): void { const enumValueInclusionMap: Record = Object.create(null); @@ -228,7 +234,11 @@ export function validateInputEnumConsistency( } } -function validationMessage(message: string, settingNamespace: string, typeMergingOptions: TypeMergingOptions): void { +function validationMessage>( + message: string, + settingNamespace: string, + typeMergingOptions?: TypeMergingOptions +): void { const override = `typeMergingOptions.validationScopes['${settingNamespace}'].validationLevel`; const settings = getValidationSettings(settingNamespace, typeMergingOptions); @@ -244,7 +254,10 @@ function validationMessage(message: string, settingNamespace: string, typeMergin } } -function getValidationSettings(settingNamespace: string, typeMergingOptions: TypeMergingOptions): ValidationSettings { +function getValidationSettings>( + settingNamespace: string, + typeMergingOptions?: TypeMergingOptions +): ValidationSettings { return { ...(typeMergingOptions?.validationSettings ?? {}), ...(typeMergingOptions?.validationScopes?.[settingNamespace] ?? {}), diff --git a/packages/stitch/src/selectionSetArgs.ts b/packages/stitch/src/selectionSetArgs.ts index 4dbc164d5b2..ab011e9b7de 100644 --- a/packages/stitch/src/selectionSetArgs.ts +++ b/packages/stitch/src/selectionSetArgs.ts @@ -7,22 +7,20 @@ export const forwardArgsToSelectionSet: ( ) => (field: FieldNode) => SelectionSetNode = (selectionSet: string, mapping?: Record) => { const selectionSetDef = parseSelectionSet(selectionSet, { noLocation: true }); return (field: FieldNode): SelectionSetNode => { - const selections = selectionSetDef.selections.map( - (selectionNode): SelectionNode => { - if (selectionNode.kind === Kind.FIELD) { - if (!mapping) { - return { ...selectionNode, arguments: field.arguments.slice() }; - } else if (selectionNode.name.value in mapping) { - const selectionArgs = mapping[selectionNode.name.value]; - return { - ...selectionNode, - arguments: field.arguments.filter((arg): boolean => selectionArgs.includes(arg.name.value)), - }; - } + const selections = selectionSetDef.selections.map((selectionNode): SelectionNode => { + if (selectionNode.kind === Kind.FIELD) { + if (!mapping) { + return { ...selectionNode, arguments: field.arguments?.slice() }; + } else if (selectionNode.name.value in mapping) { + const selectionArgs = mapping[selectionNode.name.value]; + return { + ...selectionNode, + arguments: field.arguments?.filter((arg): boolean => selectionArgs.includes(arg.name.value)), + }; } - return selectionNode; } - ); + return selectionNode; + }); return { ...selectionSetDef, selections }; }; diff --git a/packages/stitch/src/stitchSchemas.ts b/packages/stitch/src/stitchSchemas.ts index 40d63a88da6..0530e79235d 100644 --- a/packages/stitch/src/stitchSchemas.ts +++ b/packages/stitch/src/stitchSchemas.ts @@ -56,9 +56,15 @@ export function stitchSchemas>({ throw new Error('Expected `resolverValidationOptions` to be an object'); } - let transformedSubschemas: Array = []; - const subschemaMap: Map, Subschema> = new Map(); - const originalSubschemaMap: Map> = new Map(); + let transformedSubschemas: Array> = []; + const subschemaMap: Map< + GraphQLSchema | SubschemaConfig, + Subschema + > = new Map(); + const originalSubschemaMap: Map< + Subschema, + GraphQLSchema | SubschemaConfig + > = new Map(); subschemas.forEach(subschemaOrSubschemaArray => { if (Array.isArray(subschemaOrSubschemaArray)) { @@ -195,37 +201,51 @@ export function stitchSchemas>({ return schema; } -const subschemaConfigTransformerPresets = [isolateComputedFieldsTransformer, splitMergedTypeEntryPointsTransformer]; +const subschemaConfigTransformerPresets: Array> = [ + isolateComputedFieldsTransformer, + splitMergedTypeEntryPointsTransformer, +]; function applySubschemaConfigTransforms>( subschemaConfigTransforms: Array>, subschemaOrSubschemaConfig: GraphQLSchema | SubschemaConfig, - subschemaMap: Map, - originalSubschemaMap: Map> -): Array { - const subschemaConfig = isSubschemaConfig(subschemaOrSubschemaConfig) - ? subschemaOrSubschemaConfig - : { schema: subschemaOrSubschemaConfig }; - - let transformedSubschemaConfigs: Array = [subschemaConfig]; - subschemaConfigTransforms.concat(subschemaConfigTransformerPresets).forEach(subschemaConfigTransform => { - const mapped: Array> = transformedSubschemaConfigs.map(ssConfig => - subschemaConfigTransform(ssConfig) - ); - - transformedSubschemaConfigs = mapped.reduce( - (acc: Array, configOrList: SubschemaConfig | Array) => { - if (Array.isArray(configOrList)) { - return acc.concat(configOrList as Array); - } - acc.push(configOrList as SubschemaConfig); - return acc; - }, - [] - ) as Array; - }); + subschemaMap: Map, Subschema>, + originalSubschemaMap: Map< + Subschema, + GraphQLSchema | SubschemaConfig + > +): Array> { + let subschemaConfig: SubschemaConfig; + if (isSubschemaConfig(subschemaOrSubschemaConfig)) { + subschemaConfig = subschemaOrSubschemaConfig; + } else if (subschemaOrSubschemaConfig instanceof GraphQLSchema) { + subschemaConfig = { schema: subschemaOrSubschemaConfig }; + } else { + throw new TypeError('Received invalid input.'); + } + + let transformedSubschemaConfigs: Array> = [subschemaConfig]; + subschemaConfigTransforms + .concat(subschemaConfigTransformerPresets as Array>) + .forEach(subschemaConfigTransform => { + const mapped: Array | Array>> = + transformedSubschemaConfigs.map(ssConfig => subschemaConfigTransform(ssConfig)); + + transformedSubschemaConfigs = mapped.reduce( + (acc: Array>, configOrList) => { + if (Array.isArray(configOrList)) { + return acc.concat(configOrList); + } + acc.push(configOrList); + return acc; + }, + [] + ); + }); - const transformedSubschemas = transformedSubschemaConfigs.map(ssConfig => new Subschema(ssConfig)); + const transformedSubschemas = transformedSubschemaConfigs.map( + ssConfig => new Subschema(ssConfig) + ); const baseSubschema = transformedSubschemas[0]; diff --git a/packages/stitch/src/stitchingInfo.ts b/packages/stitch/src/stitchingInfo.ts index d34ebcd4e46..255a074ad19 100644 --- a/packages/stitch/src/stitchingInfo.ts +++ b/packages/stitch/src/stitchingInfo.ts @@ -13,7 +13,7 @@ import { isLeafType, } from 'graphql'; -import { parseSelectionSet, TypeMap, IResolvers, IFieldResolverOptions } from '@graphql-tools/utils'; +import { parseSelectionSet, TypeMap, IResolvers, IFieldResolverOptions, isSome } from '@graphql-tools/utils'; import { MergedTypeResolver, Subschema, SubschemaConfig, MergedTypeInfo, StitchingInfo } from '@graphql-tools/delegate'; @@ -21,11 +21,11 @@ import { MergeTypeCandidate, MergeTypeFilter } from './types'; import { createMergedTypeResolver } from './createMergedTypeResolver'; -export function createStitchingInfo( - subschemaMap: Map, - typeCandidates: Record>, - mergeTypes?: boolean | Array | MergeTypeFilter -): StitchingInfo { +export function createStitchingInfo>( + subschemaMap: Map, Subschema>, + typeCandidates: Record>>, + mergeTypes?: boolean | Array | MergeTypeFilter +): StitchingInfo { const mergedTypes = createMergedTypes(typeCandidates, mergeTypes); const selectionSetsByField: Record> = Object.create(null); @@ -82,11 +82,11 @@ export function createStitchingInfo( }; } -function createMergedTypes( - typeCandidates: Record>, - mergeTypes?: boolean | Array | MergeTypeFilter -): Record { - const mergedTypes: Record = Object.create(null); +function createMergedTypes>( + typeCandidates: Record>>, + mergeTypes?: boolean | Array | MergeTypeFilter +): Record> { + const mergedTypes: Record> = Object.create(null); Object.keys(typeCandidates).forEach(typeName => { if ( @@ -106,13 +106,13 @@ function createMergedTypes( (Array.isArray(mergeTypes) && mergeTypes.includes(typeName)) || typeCandidatesWithMergedTypeConfig.length ) { - const targetSubschemas: Array = []; + const targetSubschemas: Array> = []; - const typeMaps: Map = new Map(); - const supportedBySubschemas: Record> = Object.create({}); - const selectionSets: Map = new Map(); - const fieldSelectionSets: Map> = new Map(); - const resolvers: Map = new Map(); + const typeMaps: Map, TypeMap> = new Map(); + const supportedBySubschemas: Record>> = Object.create({}); + const selectionSets: Map, SelectionSetNode> = new Map(); + const fieldSelectionSets: Map, Record> = new Map(); + const resolvers: Map, MergedTypeResolver> = new Map(); typeCandidates[typeName].forEach(typeCandidate => { const subschema = typeCandidate.transformedSubschema; @@ -136,12 +136,14 @@ function createMergedTypes( if (mergedTypeConfig.fields) { const parsedFieldSelectionSets = Object.create(null); - Object.keys(mergedTypeConfig.fields).forEach(fieldName => { + for (const fieldName in mergedTypeConfig.fields) { if (mergedTypeConfig.fields[fieldName].selectionSet) { const rawFieldSelectionSet = mergedTypeConfig.fields[fieldName].selectionSet; - parsedFieldSelectionSets[fieldName] = parseSelectionSet(rawFieldSelectionSet, { noLocation: true }); + parsedFieldSelectionSets[fieldName] = rawFieldSelectionSet + ? parseSelectionSet(rawFieldSelectionSet, { noLocation: true }) + : undefined; } - }); + } fieldSelectionSets.set(subschema, parsedFieldSelectionSets); } @@ -181,9 +183,12 @@ function createMergedTypes( }); const sourceSubschemas = typeCandidates[typeName] - .filter(typeCandidate => typeCandidate.transformedSubschema != null) - .map(typeCandidate => typeCandidate.transformedSubschema); - const targetSubschemasBySubschema: Map> = new Map(); + .map(typeCandidate => typeCandidate?.transformedSubschema) + .filter(isSome); + const targetSubschemasBySubschema: Map< + Subschema, + Array> + > = new Map(); sourceSubschemas.forEach(subschema => { const filteredSubschemas = targetSubschemas.filter(s => s !== subschema); if (filteredSubschemas.length) { @@ -216,11 +221,11 @@ function createMergedTypes( return mergedTypes; } -export function completeStitchingInfo( - stitchingInfo: StitchingInfo, +export function completeStitchingInfo>( + stitchingInfo: StitchingInfo, resolvers: IResolvers, schema: GraphQLSchema -): StitchingInfo { +): StitchingInfo { const selectionSetsByType = Object.create(null); [schema.getQueryType(), schema.getMutationType()].forEach(rootType => { if (rootType) { @@ -288,7 +293,10 @@ export function completeStitchingInfo( return stitchingInfo; } -export function addStitchingInfo(stitchedSchema: GraphQLSchema, stitchingInfo: StitchingInfo): GraphQLSchema { +export function addStitchingInfo>( + stitchedSchema: GraphQLSchema, + stitchingInfo: StitchingInfo +): GraphQLSchema { return new GraphQLSchema({ ...stitchedSchema.toConfig(), extensions: { diff --git a/packages/stitch/src/subschemaConfigTransforms/index.ts b/packages/stitch/src/subschemaConfigTransforms/index.ts index ebc854cc46d..ebf70699ab4 100644 --- a/packages/stitch/src/subschemaConfigTransforms/index.ts +++ b/packages/stitch/src/subschemaConfigTransforms/index.ts @@ -1,7 +1,10 @@ +import { SubschemaConfigTransform } from 'packages/graphql-tools/src'; import { computedDirectiveTransformer } from './computedDirectiveTransformer'; export { computedDirectiveTransformer } from './computedDirectiveTransformer'; export { isolateComputedFieldsTransformer } from './isolateComputedFieldsTransformer'; export { splitMergedTypeEntryPointsTransformer } from './splitMergedTypeEntryPointsTransformer'; -export const defaultSubschemaConfigTransforms = [computedDirectiveTransformer('computed')]; +export const defaultSubschemaConfigTransforms: Array> = [ + computedDirectiveTransformer('computed'), +]; diff --git a/packages/stitch/src/subschemaConfigTransforms/isolateComputedFieldsTransformer.ts b/packages/stitch/src/subschemaConfigTransforms/isolateComputedFieldsTransformer.ts index c39e5126b2d..0090fc714e3 100644 --- a/packages/stitch/src/subschemaConfigTransforms/isolateComputedFieldsTransformer.ts +++ b/packages/stitch/src/subschemaConfigTransforms/isolateComputedFieldsTransformer.ts @@ -18,18 +18,19 @@ export function isolateComputedFieldsTransformer(subschemaConfig: SubschemaConfi baseSchemaTypes[typeName] = mergedTypeConfig; if (mergedTypeConfig.computedFields) { - mergedTypeConfig.fields = mergedTypeConfig.fields ?? Object.create(null); + const mergeConfigFields = mergedTypeConfig.fields ?? Object.create(null); Object.entries(mergedTypeConfig.computedFields).forEach(([fieldName, mergedFieldConfig]) => { console.warn( `The "computedFields" setting is deprecated. Update your @graphql-tools/stitching-directives package, and/or update static merged type config to "${typeName}.fields.${fieldName} = { selectionSet: '${mergedFieldConfig.selectionSet}', computed: true }"` ); - mergedTypeConfig.fields[fieldName] = { - ...(mergedTypeConfig.fields[fieldName] ?? {}), + mergeConfigFields[fieldName] = { + ...(mergeConfigFields[fieldName] ?? {}), ...mergedFieldConfig, computed: true, }; }); delete mergedTypeConfig.computedFields; + mergedTypeConfig.fields = mergeConfigFields; } if (mergedTypeConfig.fields) { @@ -82,13 +83,13 @@ function filterBaseSubschema( const filteredSchema = pruneSchema( filterSchema({ schema, - objectFieldFilter: (typeName, fieldName) => !isolatedSchemaTypes[typeName]?.fields[fieldName], + objectFieldFilter: (typeName, fieldName) => !isolatedSchemaTypes[typeName]?.fields?.[fieldName], interfaceFieldFilter: (typeName, fieldName) => { if (!typesForInterface[typeName]) { typesForInterface[typeName] = getImplementingTypes(typeName, schema); } return !typesForInterface[typeName].some( - implementingTypeName => isolatedSchemaTypes[implementingTypeName]?.fields[fieldName] + implementingTypeName => isolatedSchemaTypes[implementingTypeName]?.fields?.[fieldName] ); }, }) @@ -122,26 +123,35 @@ function filterBaseSubschema( }; const remainingTypes = filteredSchema.getTypeMap(); - Object.keys(filteredSubschema.merge).forEach(mergeType => { - if (!remainingTypes[mergeType]) { - delete filteredSubschema.merge[mergeType]; - } - }); + const mergeConfig = filteredSubschema.merge; + if (mergeConfig) { + Object.keys(mergeConfig).forEach(mergeType => { + if (!remainingTypes[mergeType]) { + delete mergeConfig[mergeType]; + } + }); - if (!Object.keys(filteredSubschema.merge).length) { - delete filteredSubschema.merge; + if (!Object.keys(mergeConfig).length) { + delete filteredSubschema.merge; + } } return filteredSubschema; } -function filterIsolatedSubschema(subschemaConfig: SubschemaConfig): SubschemaConfig { +type IsolatedSubschemaInput = Exclude & { + merge: Exclude; +}; + +function filterIsolatedSubschema(subschemaConfig: IsolatedSubschemaInput): SubschemaConfig { const rootFields: Record = {}; Object.values(subschemaConfig.merge).forEach(mergedTypeConfig => { const entryPoints = mergedTypeConfig.entryPoints ?? [mergedTypeConfig]; entryPoints.forEach(entryPoint => { - rootFields[entryPoint.fieldName] = true; + if (entryPoint.fieldName != null) { + rootFields[entryPoint.fieldName] = true; + } }); }); @@ -150,7 +160,7 @@ function filterIsolatedSubschema(subschemaConfig: SubschemaConfig): SubschemaCon (subschemaConfig.schema.getType(typeName) as GraphQLObjectType).getInterfaces().forEach(int => { Object.keys((subschemaConfig.schema.getType(int.name) as GraphQLInterfaceType).getFields()).forEach( intFieldName => { - if (subschemaConfig.merge[typeName].fields[intFieldName]) { + if (subschemaConfig.merge[typeName].fields?.[intFieldName]) { interfaceFields[int.name] = interfaceFields[int.name] || {}; interfaceFields[int.name][intFieldName] = true; } @@ -163,7 +173,7 @@ function filterIsolatedSubschema(subschemaConfig: SubschemaConfig): SubschemaCon filterSchema({ schema: subschemaConfig.schema, rootFieldFilter: (operation, fieldName) => operation === 'Query' && rootFields[fieldName] != null, - objectFieldFilter: (typeName, fieldName) => subschemaConfig.merge[typeName]?.fields[fieldName] != null, + objectFieldFilter: (typeName, fieldName) => subschemaConfig.merge[typeName]?.fields?.[fieldName] != null, interfaceFieldFilter: (typeName, fieldName) => interfaceFields[typeName]?.[fieldName] != null, }) ); diff --git a/packages/stitch/src/subschemaConfigTransforms/splitMergedTypeEntryPointsTransformer.ts b/packages/stitch/src/subschemaConfigTransforms/splitMergedTypeEntryPointsTransformer.ts index b655fc9f135..d4499f47e83 100644 --- a/packages/stitch/src/subschemaConfigTransforms/splitMergedTypeEntryPointsTransformer.ts +++ b/packages/stitch/src/subschemaConfigTransforms/splitMergedTypeEntryPointsTransformer.ts @@ -1,4 +1,4 @@ -import { cloneSubschemaConfig, SubschemaConfig } from '@graphql-tools/delegate'; +import { cloneSubschemaConfig, MergedTypeConfig, SubschemaConfig } from '@graphql-tools/delegate'; export function splitMergedTypeEntryPointsTransformer(subschemaConfig: SubschemaConfig): Array { if (!subschemaConfig.merge) return [subschemaConfig]; @@ -13,10 +13,12 @@ export function splitMergedTypeEntryPointsTransformer(subschemaConfig: Subschema for (let i = 0; i < maxEntryPoints; i += 1) { const subschemaPermutation = cloneSubschemaConfig(subschemaConfig); - const mergedTypesCopy = subschemaPermutation.merge; + const mergedTypesCopy: Record> = + subschemaPermutation.merge ?? Object.create(null); + let currentMerge = mergedTypesCopy; if (i > 0) { - subschemaPermutation.merge = Object.create(null); + subschemaPermutation.merge = currentMerge = Object.create(null); } Object.keys(mergedTypesCopy).forEach(typeName => { @@ -25,7 +27,9 @@ export function splitMergedTypeEntryPointsTransformer(subschemaConfig: Subschema if (mergedTypeEntryPoint) { if (mergedTypeConfig.selectionSet ?? mergedTypeConfig.fieldName ?? mergedTypeConfig.resolve) { - throw new Error(`Merged type ${typeName} may not define entryPoints in addition to selectionSet, fieldName, or resolve`); + throw new Error( + `Merged type ${typeName} may not define entryPoints in addition to selectionSet, fieldName, or resolve` + ); } Object.assign(mergedTypeConfig, mergedTypeEntryPoint); @@ -40,7 +44,7 @@ export function splitMergedTypeEntryPointsTransformer(subschemaConfig: Subschema } } - subschemaPermutation.merge[typeName] = mergedTypeConfig; + currentMerge[typeName] = mergedTypeConfig; } }); diff --git a/packages/stitch/src/typeCandidates.ts b/packages/stitch/src/typeCandidates.ts index e681149ca92..363667e4f5a 100644 --- a/packages/stitch/src/typeCandidates.ts +++ b/packages/stitch/src/typeCandidates.ts @@ -20,9 +20,11 @@ import { MergeTypeCandidate, MergeTypeFilter, OnTypeConflict, TypeMergingOptions import { mergeCandidates } from './mergeCandidates'; import { extractDefinitions } from './definitions'; -type CandidateSelector = (candidates: Array) => MergeTypeCandidate; +type CandidateSelector> = ( + candidates: Array> +) => MergeTypeCandidate; -export function buildTypeCandidates({ +export function buildTypeCandidates>({ subschemas, originalSubschemaMap, types, @@ -34,10 +36,13 @@ export function buildTypeCandidates({ operationTypeNames, mergeDirectives, }: { - subschemas: Array; - originalSubschemaMap: Map; + subschemas: Array>; + originalSubschemaMap: Map< + Subschema, + GraphQLSchema | SubschemaConfig + >; types: Array; - typeDefs: ITypeDefinitions; + typeDefs: ITypeDefinitions | undefined; parseOptions: GraphQLParseOptions; extensions: Array; directiveMap: Record; @@ -46,15 +51,15 @@ export function buildTypeCandidates({ schemaExtensions: Array; }; operationTypeNames: Record; - mergeDirectives: boolean; -}): Record> { - const typeCandidates: Record> = Object.create(null); + mergeDirectives?: boolean | undefined; +}): Record>> { + const typeCandidates: Record>> = Object.create(null); - let schemaDef: SchemaDefinitionNode; + let schemaDef: SchemaDefinitionNode | undefined; let schemaExtensions: Array = []; - let document: DocumentNode; - let extraction: ReturnType; + let document: DocumentNode | undefined; + let extraction: ReturnType | undefined; if ((typeDefs && !Array.isArray(typeDefs)) || (Array.isArray(typeDefs) && typeDefs.length)) { document = buildDocumentFromTypeDefinitions(typeDefs, parseOptions); extraction = extractDefinitions(document); @@ -62,7 +67,7 @@ export function buildTypeCandidates({ schemaExtensions = schemaExtensions.concat(extraction.schemaExtensions); } - schemaDefs.schemaDef = schemaDef; + schemaDefs.schemaDef = schemaDef ?? schemaDefs.schemaDef; schemaDefs.schemaExtensions = schemaExtensions; setOperationTypeNames(schemaDefs, operationTypeNames); @@ -86,7 +91,7 @@ export function buildTypeCandidates({ } }); - if (mergeDirectives) { + if (mergeDirectives === true) { schema.getDirectives().forEach(directive => { directiveMap[directive.name] = directive; }); @@ -111,7 +116,7 @@ export function buildTypeCandidates({ }); }); - if (document !== undefined) { + if (document != null && extraction != null) { extraction.typeDefinitions.forEach(def => { const type = typeFromAST(def) as GraphQLNamedType; if (type != null) { @@ -161,10 +166,10 @@ function setOperationTypeNames( }); } -function addTypeCandidate( - typeCandidates: Record>, +function addTypeCandidate>( + typeCandidates: Record>>, name: string, - typeCandidate: MergeTypeCandidate + typeCandidate: MergeTypeCandidate ) { if (!(name in typeCandidates)) { typeCandidates[name] = []; @@ -172,7 +177,7 @@ function addTypeCandidate( typeCandidates[name].push(typeCandidate); } -export function buildTypes({ +export function buildTypes>({ typeCandidates, directives, stitchingInfo, @@ -181,21 +186,21 @@ export function buildTypes({ mergeTypes, typeMergingOptions, }: { - typeCandidates: Record>; + typeCandidates: Record>>; directives: Array; - stitchingInfo: StitchingInfo; + stitchingInfo: StitchingInfo; operationTypeNames: Record; - onTypeConflict: OnTypeConflict; - mergeTypes: boolean | Array | MergeTypeFilter; - typeMergingOptions: TypeMergingOptions; + onTypeConflict?: OnTypeConflict; + mergeTypes: boolean | Array | MergeTypeFilter; + typeMergingOptions?: TypeMergingOptions; }): { typeMap: TypeMap; directives: Array } { const typeMap: TypeMap = Object.create(null); Object.keys(typeCandidates).forEach(typeName => { if ( - typeName === operationTypeNames.query || - typeName === operationTypeNames.mutation || - typeName === operationTypeNames.subscription || + typeName === operationTypeNames['query'] || + typeName === operationTypeNames['mutation'] || + typeName === operationTypeNames['subscription'] || (mergeTypes === true && !typeCandidates[typeName].some(candidate => isSpecifiedScalarType(candidate.type))) || (typeof mergeTypes === 'function' && mergeTypes(typeCandidates[typeName], typeName)) || (Array.isArray(mergeTypes) && mergeTypes.includes(typeName)) || @@ -206,7 +211,7 @@ export function buildTypes({ const candidateSelector = onTypeConflict != null ? onTypeConflictToCandidateSelector(onTypeConflict) - : (cands: Array) => cands[cands.length - 1]; + : (cands: Array>) => cands[cands.length - 1]; typeMap[typeName] = candidateSelector(typeCandidates[typeName]).type; } }); @@ -214,7 +219,9 @@ export function buildTypes({ return rewireTypes(typeMap, directives); } -function onTypeConflictToCandidateSelector(onTypeConflict: OnTypeConflict): CandidateSelector { +function onTypeConflictToCandidateSelector>( + onTypeConflict: OnTypeConflict +): CandidateSelector { return cands => cands.reduce((prev, next) => { const type = onTypeConflict(prev.type, next.type, { diff --git a/packages/stitch/src/typeFromAST.ts b/packages/stitch/src/typeFromAST.ts index 7178f324a6e..0f17a8d830e 100644 --- a/packages/stitch/src/typeFromAST.ts +++ b/packages/stitch/src/typeFromAST.ts @@ -32,7 +32,7 @@ import { GraphQLDeprecatedDirective, } from 'graphql'; -import { createStub, createNamedStub } from '@graphql-tools/utils'; +import { createStub, createNamedStub, Maybe } from '@graphql-tools/utils'; const backcompatOptions = { commentDescriptions: true }; @@ -61,8 +61,8 @@ function makeObjectType(node: ObjectTypeDefinitionNode): GraphQLObjectType { const config = { name: node.name.value, description: getDescription(node, backcompatOptions), - interfaces: () => node.interfaces.map(iface => createNamedStub(iface.name.value, 'interface')), - fields: () => makeFields(node.fields), + interfaces: () => node.interfaces?.map(iface => createNamedStub(iface.name.value, 'interface')), + fields: () => (node.fields != null ? makeFields(node.fields) : {}), astNode: node, }; return new GraphQLObjectType(config); @@ -72,27 +72,28 @@ function makeInterfaceType(node: InterfaceTypeDefinitionNode): GraphQLInterfaceT const config = { name: node.name.value, description: getDescription(node, backcompatOptions), - interfaces: ((node as unknown) as ObjectTypeDefinitionNode).interfaces?.map(iface => + interfaces: (node as unknown as ObjectTypeDefinitionNode).interfaces?.map(iface => createNamedStub(iface.name.value, 'interface') ), - fields: () => makeFields(node.fields), + fields: () => (node.fields != null ? makeFields(node.fields) : {}), astNode: node, }; return new GraphQLInterfaceType(config); } function makeEnumType(node: EnumTypeDefinitionNode): GraphQLEnumType { - const values = node.values.reduce( - (prev, value) => ({ - ...prev, - [value.name.value]: { - description: getDescription(value, backcompatOptions), - deprecationReason: getDeprecationReason(value), - astNode: value, - }, - }), - {} - ); + const values = + node.values?.reduce( + (prev, value) => ({ + ...prev, + [value.name.value]: { + description: getDescription(value, backcompatOptions), + deprecationReason: getDeprecationReason(value), + astNode: value, + }, + }), + {} + ) ?? {}; return new GraphQLEnumType({ name: node.name.value, @@ -106,7 +107,7 @@ function makeUnionType(node: UnionTypeDefinitionNode): GraphQLUnionType { return new GraphQLUnionType({ name: node.name.value, description: getDescription(node, backcompatOptions), - types: () => node.types.map(type => createNamedStub(type.name.value, 'object')), + types: () => node.types?.map(type => createNamedStub(type.name.value, 'object')) ?? [], astNode: node, }); } @@ -126,7 +127,7 @@ function makeInputObjectType(node: InputObjectTypeDefinitionNode): GraphQLInputO return new GraphQLInputObjectType({ name: node.name.value, description: getDescription(node, backcompatOptions), - fields: () => makeValues(node.fields), + fields: () => (node.fields ? makeValues(node.fields) : {}), astNode: node, }); } @@ -138,7 +139,7 @@ function makeFields(nodes: ReadonlyArray): Record { const deprecated = getDirectiveValues(GraphQLDeprecatedDirective, node); - return deprecated?.reason; + return deprecated?.['reason']; } diff --git a/packages/stitch/src/types.ts b/packages/stitch/src/types.ts index 1251afdd843..d8a49b16a72 100644 --- a/packages/stitch/src/types.ts +++ b/packages/stitch/src/types.ts @@ -11,7 +11,7 @@ import { GraphQLEnumValueConfig, GraphQLEnumType, } from 'graphql'; -import { ITypeDefinitions } from '@graphql-tools/utils'; +import { ITypeDefinitions, Maybe } from '@graphql-tools/utils'; import { Subschema, SubschemaConfig } from '@graphql-tools/delegate'; import { IExecutableSchemaDefinition } from '@graphql-tools/schema'; @@ -57,8 +57,8 @@ export interface IStitchSchemasOptions> >; typeDefs?: ITypeDefinitions; types?: Array; - onTypeConflict?: OnTypeConflict; - mergeDirectives?: boolean; + onTypeConflict?: OnTypeConflict; + mergeDirectives?: boolean | undefined; mergeTypes?: boolean | Array | MergeTypeFilter; typeMergingOptions?: TypeMergingOptions; subschemaConfigTransforms?: Array>; @@ -72,8 +72,8 @@ export interface TypeMergingOptions> { validationSettings?: ValidationSettings; validationScopes?: Record; typeCandidateMerger?: (candidates: Array>) => MergeTypeCandidate; - typeDescriptionsMerger?: (candidates: Array>) => string; - fieldConfigMerger?: (candidates: Array>) => GraphQLFieldConfig; + typeDescriptionsMerger?: (candidates: Array>) => Maybe; + fieldConfigMerger?: (candidates: Array>) => GraphQLFieldConfig; inputFieldConfigMerger?: (candidates: Array>) => GraphQLInputFieldConfig; enumValueConfigMerger?: (candidates: Array>) => GraphQLEnumValueConfig; } diff --git a/packages/stitch/tests/alternateStitchSchemas.test.ts b/packages/stitch/tests/alternateStitchSchemas.test.ts index 3ae5cb889f1..e957cdb841e 100644 --- a/packages/stitch/tests/alternateStitchSchemas.test.ts +++ b/packages/stitch/tests/alternateStitchSchemas.test.ts @@ -33,7 +33,7 @@ import { import { delegateToSchema, SubschemaConfig } from '@graphql-tools/delegate'; import { makeExecutableSchema } from '@graphql-tools/schema'; import { addMocksToSchema } from '@graphql-tools/mock'; -import { filterSchema, ExecutionResult } from '@graphql-tools/utils'; +import { filterSchema, ExecutionResult, assertSome } from '@graphql-tools/utils'; import { stitchSchemas } from '../src/stitchSchemas'; @@ -440,9 +440,11 @@ describe('optional arguments', () => { `; const originalResult = await graphql(schema, query); + assertSome(originalResult.data) expect(originalResult.data.test).toEqual(true); const stitchedResult = await graphql(stitchedSchema, query); + assertSome(stitchedResult.data) expect(stitchedResult.data.test).toEqual(true); }); @@ -454,9 +456,11 @@ describe('optional arguments', () => { `; const originalResult = await graphql(schema, query); + assertSome(originalResult.data) expect(originalResult.data.test).toEqual(true); const stitchedResult = await graphql(stitchedSchema, query); + assertSome(stitchedResult.data) expect(stitchedResult.data.test).toEqual(true); }); @@ -475,6 +479,7 @@ describe('optional arguments', () => { {}, { arg: undefined }, ); + assertSome(originalResult.data) expect(originalResult.data.test).toEqual(false); const stitchedResult = await graphql( @@ -484,6 +489,7 @@ describe('optional arguments', () => { {}, { arg: undefined }, ); + assertSome(stitchedResult.data) expect(stitchedResult.data.test).toEqual(false); }); }); @@ -495,11 +501,12 @@ describe('default values', () => { transforms: [ new TransformRootFields( ( - typeName: string, - fieldName: string, - fieldConfig: GraphQLFieldConfig, + typeName, + fieldName, + fieldConfig, ) => { if (typeName === 'Query' && fieldName === 'jsonTest') { + assertSome(fieldConfig.args) return [ 'renamedJsonTest', { @@ -1508,6 +1515,7 @@ describe('schema transformation with wrapping of object fields', () => { const query = '{ wrapped { user { dummy } } }'; const result = await graphql(stitchedSchema, query); + assertSome(result.data) expect(result.data.wrapped.user.dummy).not.toEqual(null); }); }); @@ -1579,6 +1587,7 @@ describe('interface resolver inheritance', () => { }); const query = '{ user { id name } }'; const response = await graphql(stitchedSchema, query); + assertSome(response.errors) expect(response.errors.length).toBe(1); expect(response.errors[0].message).toBe( 'Cannot return null for non-nullable field User.id.', @@ -1598,6 +1607,7 @@ describe('interface resolver inheritance', () => { }); const query = '{ user { id name } }'; const response = await graphql(stitchedSchema, query); + assertSome(response.errors) expect(response.errors.length).toBe(1); expect(response.errors[0].message).toBe( 'Cannot return null for non-nullable field User.id.', @@ -1629,6 +1639,7 @@ describe('stitchSchemas', () => { const query = '{ test { field } }'; const response = await graphql(stitchedSchema, query); + assertSome(response.data) expect(response.data.test).toBe(null); expect(response.errors).toBeUndefined(); }); diff --git a/packages/stitch/tests/dataloader.test.ts b/packages/stitch/tests/dataloader.test.ts index f7692b3dbbe..2c5389ea2ae 100644 --- a/packages/stitch/tests/dataloader.test.ts +++ b/packages/stitch/tests/dataloader.test.ts @@ -68,7 +68,7 @@ describe('dataloader', () => { }); const usersLoader = new DataLoader( - async (keys: Array<{ id: any; info: GraphQLResolveInfo }>) => { + async (keys: ReadonlyArray<{ id: any; info: GraphQLResolveInfo }>) => { const users = await delegateToSchema({ schema: userSchema, operation: 'query', diff --git a/packages/stitch/tests/errors.test.ts b/packages/stitch/tests/errors.test.ts index aba766bce8c..b9571d460df 100644 --- a/packages/stitch/tests/errors.test.ts +++ b/packages/stitch/tests/errors.test.ts @@ -3,7 +3,7 @@ import { graphql, GraphQLError, buildSchema } from 'graphql'; import { Executor } from '@graphql-tools/delegate'; import { makeExecutableSchema } from '@graphql-tools/schema'; import { stitchSchemas } from '@graphql-tools/stitch'; -import { ExecutionResult } from '@graphql-tools/utils'; +import { assertSome, ExecutionResult } from '@graphql-tools/utils'; describe('passes along errors for missing fields on list', () => { test('if non-null', async () => { @@ -38,6 +38,8 @@ describe('passes along errors for missing fields on list', () => { const originalResult = await graphql(schema, query); const stitchedResult = await graphql(stitchedSchema, query); expect(stitchedResult).toEqual(originalResult); + assertSome(stitchedResult.errors) + assertSome(originalResult.errors) expect(stitchedResult.errors[0].path).toEqual(originalResult.errors[0].path); }); @@ -73,6 +75,8 @@ describe('passes along errors for missing fields on list', () => { const originalResult = await graphql(schema, query); const stitchedResult = await graphql(stitchedSchema, query); expect(stitchedResult).toEqual(originalResult); + assertSome(originalResult.errors) + assertSome(stitchedResult.errors) expect(stitchedResult.errors[0].path).toEqual(originalResult.errors[0].path); }); }); @@ -110,6 +114,8 @@ describe('passes along errors when list field errors', () => { const originalResult = await graphql(schema, query); const stitchedResult = await graphql(stitchedSchema, query); expect(stitchedResult).toEqual(originalResult); + assertSome(stitchedResult.errors) + assertSome(originalResult.errors) expect(stitchedResult.errors[0].path).toEqual(originalResult.errors[0].path); }); @@ -145,6 +151,8 @@ describe('passes along errors when list field errors', () => { const originalResult = await graphql(schema, query); const stitchedResult = await graphql(stitchedSchema, query); expect(stitchedResult).toEqual(originalResult); + assertSome(stitchedResult.errors) + assertSome(originalResult.errors) expect(stitchedResult.errors[0].path).toEqual(originalResult.errors[0].path); }); @@ -175,6 +183,8 @@ describe('passes along errors when list field errors', () => { const originalResult = await graphql(schema, query); const stitchedResult = await graphql(stitchedSchema, query); expect(stitchedResult).toEqual(originalResult); + assertSome(stitchedResult.errors) + assertSome(originalResult.errors) expect(stitchedResult.errors[0].path).toEqual(originalResult.errors[0].path); }); }); diff --git a/packages/stitch/tests/example.test.ts b/packages/stitch/tests/example.test.ts index 8d2b48cc46d..6757bd0af7b 100644 --- a/packages/stitch/tests/example.test.ts +++ b/packages/stitch/tests/example.test.ts @@ -4,6 +4,7 @@ import { makeExecutableSchema } from '@graphql-tools/schema'; import { delegateToSchema } from '@graphql-tools/delegate'; import { addMocksToSchema } from '@graphql-tools/mock'; import { stitchSchemas } from '@graphql-tools/stitch'; +import { assertSome } from '@graphql-tools/utils'; describe('basic stitching example', () => { test('works', async () => { @@ -107,6 +108,7 @@ describe('basic stitching example', () => { const result = await graphql(stitchedSchema, query); expect(result.errors).toBeUndefined(); + assertSome(result.data) expect(result.data.userById.chirps[1].id).not.toBe(null); expect(result.data.userById.chirps[1].text).not.toBe(null); expect(result.data.userById.chirps[1].author.email).not.toBe(null); @@ -229,6 +231,7 @@ describe('stitching to interfaces', () => { const resultWithFragments = await graphql(stitchedSchema, queryWithFragments); expect(resultWithFragments.errors).toBeUndefined(); + assertSome(resultWithFragments.data) expect(resultWithFragments.data.node.chirps[1].id).not.toBe(null); expect(resultWithFragments.data.node.chirps[1].text).not.toBe(null); expect(resultWithFragments.data.node.chirps[1].author.email).not.toBe(null); @@ -253,6 +256,7 @@ describe('stitching to interfaces', () => { const resultWithoutFragments = await graphql(stitchedSchema, queryWithoutFragments); expect(resultWithoutFragments.errors).toBeUndefined(); + assertSome(resultWithoutFragments.data) expect(resultWithoutFragments.data.node.chirps[1].id).not.toBe(null); expect(resultWithoutFragments.data.node.chirps[1].text).not.toBe(null); expect(resultWithoutFragments.data.node.chirps[1].author.email).not.toBe(null); diff --git a/packages/stitch/tests/extendedInterface.test.ts b/packages/stitch/tests/extendedInterface.test.ts index 28ce3e7bfa7..45c5205b3b3 100644 --- a/packages/stitch/tests/extendedInterface.test.ts +++ b/packages/stitch/tests/extendedInterface.test.ts @@ -1,6 +1,7 @@ import { graphql } from 'graphql'; import { makeExecutableSchema } from '@graphql-tools/schema'; import { stitchSchemas } from '../src/stitchSchemas'; +import { assertSome } from '@graphql-tools/utils'; describe('extended interfaces', () => { test('expands extended interface types for subservices', async () => { @@ -19,7 +20,7 @@ describe('extended interfaces', () => { `, resolvers: { Query: { - slot(obj, args, context, info) { + slot() { return { __typename: 'Item', id: '23', name: 'The Item' }; } } @@ -45,7 +46,7 @@ describe('extended interfaces', () => { } } `); - + assertSome(data) expect(data.slot).toEqual({ id: '23', name: 'The Item' }); }); @@ -119,7 +120,7 @@ describe('extended interfaces', () => { } } `); - + assertSome(result.data) expect(result.data.placement).toEqual({ id: '23', name: 'Item 23' }); }); }); diff --git a/packages/stitch/tests/fixtures/schemas.ts b/packages/stitch/tests/fixtures/schemas.ts index 191fb01c2ae..140d5d9b11e 100644 --- a/packages/stitch/tests/fixtures/schemas.ts +++ b/packages/stitch/tests/fixtures/schemas.ts @@ -21,6 +21,7 @@ import { IResolvers, ExecutionResult, mapAsyncIterator, + isAsyncIterable, } from '@graphql-tools/utils'; import { makeExecutableSchema } from '@graphql-tools/schema'; @@ -698,6 +699,12 @@ function makeExecutorFromSchema(schema: GraphQLSchema) { }; } +function assertAsyncIterable(input: unknown): asserts input is AsyncIterableIterator { + if (isAsyncIterable(input) === false) { + throw new Error("Expected AsyncIterable.") + } +} + function makeSubscriberFromSchema(schema: GraphQLSchema) { return async ({ document, variables, context }: ExecutionParams) => { const result = subscribe( @@ -708,8 +715,10 @@ function makeSubscriberFromSchema(schema: GraphQLSchema) { variables, ) as Promise> | ExecutionResult>; if (isPromise(result)) { - return result.then(asyncIterator => - mapAsyncIterator(asyncIterator as AsyncIterator, (originalResult: ExecutionResult) => JSON.parse(JSON.stringify(originalResult)))); + return result.then(asyncIterator => { + assertAsyncIterable(asyncIterator) + return mapAsyncIterator(asyncIterator, (originalResult: ExecutionResult) => JSON.parse(JSON.stringify(originalResult))) + }); } return JSON.parse(JSON.stringify(result)); }; diff --git a/packages/stitch/tests/isolateComputedFieldsTransformer.test.ts b/packages/stitch/tests/isolateComputedFieldsTransformer.test.ts index 49a6c2fe489..e8ffeb88df8 100644 --- a/packages/stitch/tests/isolateComputedFieldsTransformer.test.ts +++ b/packages/stitch/tests/isolateComputedFieldsTransformer.test.ts @@ -2,6 +2,7 @@ import { makeExecutableSchema } from '@graphql-tools/schema'; import { isolateComputedFieldsTransformer } from '@graphql-tools/stitch'; import { Subschema } from '@graphql-tools/delegate'; import { GraphQLObjectType, GraphQLInterfaceType } from 'graphql'; +import { assertSome } from '@graphql-tools/utils'; describe('isolateComputedFieldsTransformer', () => { describe('basic isolation', () => { @@ -73,11 +74,12 @@ describe('isolateComputedFieldsTransformer', () => { expect(Object.keys((computedSubschema.transformedSchema.getType('Storefront') as GraphQLObjectType).getFields()).length).toEqual(0); expect(computedSubschema.transformedSchema.getType('ProductRepresentation')).toBeDefined(); + assertSome(baseSubschema.merge) expect(baseSubschema.merge.Product.canonical).toEqual(true); expect(baseSubschema.merge.Product.fields).toEqual({ deliveryService: { canonical: true }, }); - + assertSome(computedSubschema.merge) expect(computedSubschema.merge.Product.canonical).toBeUndefined(); expect(computedSubschema.merge.Product.fields).toEqual({ shippingEstimate: { selectionSet: '{ price }', computed: true, canonical: true }, @@ -195,11 +197,13 @@ describe('isolateComputedFieldsTransformer', () => { expect(Object.keys((baseSubschema.transformedSchema.getType('Query') as GraphQLObjectType).getFields())).toEqual(['storefront', '_products']); expect(Object.keys((baseSubschema.transformedSchema.getType('Product') as GraphQLObjectType).getFields())).toEqual(['base']); expect(Object.keys((baseSubschema.transformedSchema.getType('Storefront') as GraphQLObjectType).getFields())).toEqual(['base']); + assertSome(baseSubschema.merge) expect(baseSubschema.merge.Storefront.fields).toEqual({}); expect(Object.keys((computedSubschema.transformedSchema.getType('Query') as GraphQLObjectType).getFields())).toEqual(['storefront', '_products']); expect(Object.keys((computedSubschema.transformedSchema.getType('Product') as GraphQLObjectType).getFields())).toEqual(['computeMe']); expect(Object.keys((computedSubschema.transformedSchema.getType('Storefront') as GraphQLObjectType).getFields())).toEqual(['computeMe']); + assertSome(computedSubschema.merge) expect(computedSubschema.merge.Storefront.fields).toEqual({ computeMe: { selectionSet: '{ availableProductIds }', computed: true }, }); diff --git a/packages/stitch/tests/mergeAbstractTypes.test.ts b/packages/stitch/tests/mergeAbstractTypes.test.ts index 8f435e73192..51628593d52 100644 --- a/packages/stitch/tests/mergeAbstractTypes.test.ts +++ b/packages/stitch/tests/mergeAbstractTypes.test.ts @@ -1,6 +1,7 @@ import { makeExecutableSchema } from '@graphql-tools/schema'; import { stitchSchemas } from '@graphql-tools/stitch'; import { graphql } from 'graphql'; +import { assertSome } from '@graphql-tools/utils'; describe('Abstract type merge', () => { it('merges with abstract type definitions', async () => { @@ -84,7 +85,7 @@ describe('Abstract type merge', () => { } } `); - + assertSome(data) expect(data.post.leadArt).toEqual({ __typename: 'Image', url: '/path/to/23', @@ -183,6 +184,7 @@ describe('Merged associations', () => { } `); + assertSome(data) expect(data.slots).toEqual([{ id: '55', network: { domain: 'network56.com' } diff --git a/packages/stitch/tests/mergeComputedFields.test.ts b/packages/stitch/tests/mergeComputedFields.test.ts index 635c64a78df..0016e87135d 100644 --- a/packages/stitch/tests/mergeComputedFields.test.ts +++ b/packages/stitch/tests/mergeComputedFields.test.ts @@ -1,6 +1,7 @@ import { makeExecutableSchema } from '@graphql-tools/schema'; import { stitchSchemas } from '@graphql-tools/stitch'; import { graphql } from 'graphql'; +import { assertSome } from '@graphql-tools/utils'; const productSchema = makeExecutableSchema({ typeDefs: ` @@ -108,6 +109,7 @@ describe('merge computed fields via config', () => { } `); + assertSome(data) expect(data.product).toEqual({ id: '77', price: 77.99, @@ -132,6 +134,7 @@ describe('merge computed fields via config', () => { } `); + assertSome(data) expect(data.storefront.availableProducts).toEqual([ { id: '23', @@ -184,6 +187,7 @@ describe('merge computed fields via config', () => { } `); + assertSome(data) expect(data.product).toEqual({ id: '77', price: 77.99, @@ -271,6 +275,7 @@ describe('merge computed fields via SDL (Apollo Federation-style directive annot } `); + assertSome(data) expect(data.storefront.availableProducts).toEqual([ { id: '23', diff --git a/packages/stitch/tests/mergeConflicts.test.ts b/packages/stitch/tests/mergeConflicts.test.ts index d2be75e291a..b99d122a3b7 100644 --- a/packages/stitch/tests/mergeConflicts.test.ts +++ b/packages/stitch/tests/mergeConflicts.test.ts @@ -1,5 +1,25 @@ import { makeExecutableSchema } from '@graphql-tools/schema'; import { stitchSchemas } from '@graphql-tools/stitch'; +import { GraphQLInputObjectType, GraphQLObjectType, GraphQLInterfaceType, } from 'graphql'; + +function assertGraphQLObjectType(input: unknown): asserts input is GraphQLObjectType { + if (input instanceof GraphQLObjectType) { + return + } + throw new Error("Expected GraphQLObjectType.") +} +function assertGraphQLInterfaceType(input: unknown): asserts input is GraphQLInterfaceType { + if (input instanceof GraphQLInterfaceType) { + return + } + throw new Error("Expected GraphQLInterfaceType.") +} +function assertGraphQLInputObjectType(input: unknown): asserts input is GraphQLInputObjectType { + if (input instanceof GraphQLInputObjectType) { + return + } + throw new Error("Expected GraphQLInputObjectType.") +} describe('merge conflict handlers', () => { const listings1Schema = makeExecutableSchema({ @@ -69,12 +89,17 @@ describe('merge conflict handlers', () => { } }, }); - - expect(gatewaySchema.getType('Listing').description).toEqual('A type'); - expect(gatewaySchema.getType('IListing').description).toEqual('An interface'); - expect(gatewaySchema.getType('ListingInput').description).toEqual('An input'); - expect(gatewaySchema.getType('Listing').getFields().id.description).toEqual('type identifier'); - expect(gatewaySchema.getType('IListing').getFields().id.description).toEqual('interface identifier'); - expect(gatewaySchema.getType('ListingInput').getFields().id.description).toEqual('input identifier'); + const Listing = gatewaySchema.getType('Listing') + assertGraphQLObjectType(Listing) + const IListing = gatewaySchema.getType('IListing') + assertGraphQLInterfaceType(IListing) + const ListingInput = gatewaySchema.getType('ListingInput') + assertGraphQLInputObjectType(ListingInput) + expect(Listing.description).toEqual('A type'); + expect(IListing.description).toEqual('An interface'); + expect(ListingInput.description).toEqual('An input'); + expect(Listing.getFields().id.description).toEqual('type identifier'); + expect(IListing.getFields().id.description).toEqual('interface identifier'); + expect(ListingInput.getFields().id.description).toEqual('input identifier'); }); }); diff --git a/packages/stitch/tests/mergeDefinitions.test.ts b/packages/stitch/tests/mergeDefinitions.test.ts index cd70fc06da1..3b4f2def57c 100644 --- a/packages/stitch/tests/mergeDefinitions.test.ts +++ b/packages/stitch/tests/mergeDefinitions.test.ts @@ -4,13 +4,11 @@ import { getDirectives } from '@graphql-tools/utils'; import { stitchingDirectives } from '@graphql-tools/stitching-directives'; import { GraphQLObjectType, - GraphQLInterfaceType, GraphQLInputObjectType, GraphQLEnumType, - GraphQLUnionType, - GraphQLScalarType, - graphql + graphql, } from 'graphql'; +import { assertGraphQLEnumType, assertGraphQLInputObjectType, assertGraphQLInterfaceType, assertGraphQLObjectType, assertGraphQLScalerType, assertGraphQLUnionType } from '../../testing/assertion'; describe('merge canonical types', () => { const firstSchema = makeExecutableSchema({ @@ -235,19 +233,24 @@ describe('merge canonical types', () => { }); it('merges prioritized descriptions', () => { - expect(gatewaySchema.getQueryType().description).toEqual('first'); - expect(gatewaySchema.getType('Product').description).toEqual('first'); - expect(gatewaySchema.getType('IProduct').description).toEqual('first'); - expect(gatewaySchema.getType('ProductInput').description).toEqual('first'); - expect(gatewaySchema.getType('ProductEnum').description).toEqual('first'); - expect(gatewaySchema.getType('ProductUnion').description).toEqual('first'); - expect(gatewaySchema.getType('ProductScalar').description).toEqual('first'); + expect(gatewaySchema.getQueryType()?.description).toEqual('first'); + expect(gatewaySchema.getType('Product')?.description).toEqual('first'); + expect(gatewaySchema.getType('IProduct')?.description).toEqual('first'); + expect(gatewaySchema.getType('ProductInput')?.description).toEqual('first'); + expect(gatewaySchema.getType('ProductEnum')?.description).toEqual('first'); + expect(gatewaySchema.getType('ProductUnion')?.description).toEqual('first'); + expect(gatewaySchema.getType('ProductScalar')?.description).toEqual('first'); const queryType = gatewaySchema.getQueryType(); - const objectType = gatewaySchema.getType('Product') as GraphQLObjectType; - const interfaceType = gatewaySchema.getType('IProduct') as GraphQLInterfaceType; - const inputType = gatewaySchema.getType('ProductInput') as GraphQLInputObjectType; - const enumType = gatewaySchema.getType('ProductEnum') as GraphQLEnumType; + assertGraphQLObjectType(queryType) + const objectType = gatewaySchema.getType('Product') + assertGraphQLObjectType(objectType) + const interfaceType = gatewaySchema.getType('IProduct') + assertGraphQLInterfaceType(interfaceType) + const inputType = gatewaySchema.getType('ProductInput'); + assertGraphQLInputObjectType(inputType) + const enumType = gatewaySchema.getType('ProductEnum'); + assertGraphQLEnumType(enumType) expect(queryType.getFields().field1.description).toEqual('first'); expect(queryType.getFields().field2.description).toEqual('second'); @@ -267,12 +270,19 @@ describe('merge canonical types', () => { it('merges prioritized ASTs', () => { const queryType = gatewaySchema.getQueryType(); - const objectType = gatewaySchema.getType('Product') as GraphQLObjectType; - const interfaceType = gatewaySchema.getType('IProduct') as GraphQLInterfaceType; - const inputType = gatewaySchema.getType('ProductInput') as GraphQLInputObjectType; - const enumType = gatewaySchema.getType('ProductEnum') as GraphQLEnumType; - const unionType = gatewaySchema.getType('ProductUnion') as GraphQLUnionType; - const scalarType = gatewaySchema.getType('ProductScalar') as GraphQLScalarType; + assertGraphQLObjectType(queryType) + const objectType = gatewaySchema.getType('Product'); + assertGraphQLObjectType(objectType) + const interfaceType = gatewaySchema.getType('IProduct') + assertGraphQLInterfaceType(interfaceType) + const inputType = gatewaySchema.getType('ProductInput'); + assertGraphQLInputObjectType(inputType) + const enumType = gatewaySchema.getType('ProductEnum'); + assertGraphQLEnumType(enumType) + const unionType = gatewaySchema.getType('ProductUnion'); + assertGraphQLUnionType(unionType) + const scalarType = gatewaySchema.getType('ProductScalar'); + assertGraphQLScalerType(scalarType) expect(getDirectives(firstSchema, queryType.toConfig()).mydir.value).toEqual('first'); expect(getDirectives(firstSchema, objectType.toConfig()).mydir.value).toEqual('first'); @@ -291,10 +301,10 @@ describe('merge canonical types', () => { expect(getDirectives(firstSchema, inputType.getFields().id).mydir.value).toEqual('first'); expect(getDirectives(firstSchema, inputType.getFields().url).mydir.value).toEqual('second'); - expect(enumType.toConfig().astNode.values.map(v => v.description.value)).toEqual(['first', 'first', 'second']); - expect(enumType.toConfig().values.YES.astNode.description.value).toEqual('first'); - expect(enumType.toConfig().values.NO.astNode.description.value).toEqual('first'); - expect(enumType.toConfig().values.MAYBE.astNode.description.value).toEqual('second'); + expect(enumType.toConfig().astNode?.values?.map(v => v.description?.value)).toEqual(['first', 'first', 'second']); + expect(enumType.toConfig().values.YES.astNode?.description?.value).toEqual('first'); + expect(enumType.toConfig().values.NO.astNode?.description?.value).toEqual('first'); + expect(enumType.toConfig().values.MAYBE.astNode?.description?.value).toEqual('second'); }); it('merges prioritized deprecations', () => { @@ -377,13 +387,18 @@ describe('merge @canonical directives', () => { }); it('merges with directive', async () => { - const objectType = gatewaySchema.getType('Product') as GraphQLObjectType; - const inputType = gatewaySchema.getType('ProductInput') as GraphQLInputObjectType; - const enumType = gatewaySchema.getType('ProductEnum') as GraphQLEnumType; + const objectType = gatewaySchema.getType('Product') ; + assertGraphQLObjectType(objectType) + const inputType = gatewaySchema.getType('ProductInput'); + assertGraphQLInputObjectType(inputType) + const enumType = gatewaySchema.getType('ProductEnum'); + assertGraphQLEnumType(enumType) + const queryType = gatewaySchema.getQueryType() + assertGraphQLObjectType(queryType) expect(objectType.description).toEqual('first'); expect(inputType.description).toEqual('first'); expect(enumType.description).toEqual('first'); - expect(gatewaySchema.getQueryType().getFields().product.description).toEqual('first'); + expect(queryType.getFields().product.description).toEqual('first'); expect(objectType.getFields().id.description).toEqual('first'); expect(objectType.getFields().name.description).toEqual('second'); expect(inputType.getFields().value.description).toEqual('second'); diff --git a/packages/stitch/tests/mergeFailures.test.ts b/packages/stitch/tests/mergeFailures.test.ts index 6deadd9c147..d7a1055d309 100644 --- a/packages/stitch/tests/mergeFailures.test.ts +++ b/packages/stitch/tests/mergeFailures.test.ts @@ -1,6 +1,6 @@ import { makeExecutableSchema } from '@graphql-tools/schema'; import { stitchSchemas } from '@graphql-tools/stitch'; -import { ExecutionResult } from '@graphql-tools/utils'; +import { assertSome, ExecutionResult } from '@graphql-tools/utils'; import { graphql, GraphQLError, GraphQLSchema } from 'graphql'; describe('merge failures', () => { @@ -139,6 +139,7 @@ describe('merge failures', () => { }); const stitchedResult = await graphql(stitchedSchema, '{ parent { thing { name desc id } } }'); + assertSome(stitchedResult.errors) expect(stitchedResult.errors[0].path).toEqual(['parent', 'thing', 'name']); }); diff --git a/packages/stitch/tests/mergeInterfaces.test.ts b/packages/stitch/tests/mergeInterfaces.test.ts index e8587630a7f..04d7a9e1593 100644 --- a/packages/stitch/tests/mergeInterfaces.test.ts +++ b/packages/stitch/tests/mergeInterfaces.test.ts @@ -1,6 +1,7 @@ import { graphql } from 'graphql'; import { makeExecutableSchema } from '@graphql-tools/schema'; import { stitchSchemas } from '../src/stitchSchemas'; +import { assertSome } from '@graphql-tools/utils'; describe('merged interfaces via concrete type', () => { const namedItemSchema = makeExecutableSchema({ @@ -77,6 +78,7 @@ describe('merged interfaces via concrete type', () => { } `); + assertSome(result.data) expect(result.data.placement).toEqual({ id: '23', index: 23, name: 'Item 23' }); }); @@ -90,6 +92,7 @@ describe('merged interfaces via concrete type', () => { } `); + assertSome(result.data) expect(result.data.placement).toEqual({ index: 23, name: 'Item 23' }); }); }); @@ -169,6 +172,8 @@ describe('merged interfaces via abstract type', () => { } `); + + assertSome(result.data) expect(result.data.placement).toEqual({ id: '23', index: 23, name: 'Item 23' }); }); @@ -181,7 +186,7 @@ describe('merged interfaces via abstract type', () => { } } `); - + assertSome(result.data) expect(result.data.placement).toEqual({ index: 23, name: 'Item 23' }); }); }); diff --git a/packages/stitch/tests/mergeMultipleEntryPoints.test.ts b/packages/stitch/tests/mergeMultipleEntryPoints.test.ts index 6711adaf2d8..410d29928a0 100644 --- a/packages/stitch/tests/mergeMultipleEntryPoints.test.ts +++ b/packages/stitch/tests/mergeMultipleEntryPoints.test.ts @@ -1,5 +1,6 @@ import { graphql } from 'graphql'; import { makeExecutableSchema } from '@graphql-tools/schema'; +import { assertSome } from '@graphql-tools/utils'; import { stitchSchemas } from '../src/stitchSchemas'; describe('merge on multiple keys', () => { @@ -124,7 +125,7 @@ describe('merge on multiple keys', () => { } } `); - + assertSome(data) expect(data.productsByKey).toEqual(result); }); @@ -140,7 +141,7 @@ describe('merge on multiple keys', () => { } } `); - + assertSome(data) expect(data.productsByUpc).toEqual(result); }); @@ -156,7 +157,7 @@ describe('merge on multiple keys', () => { } } `); - + assertSome(data) expect(data.productsById).toEqual(result); }); }); diff --git a/packages/stitch/tests/mergeResolvers.test.ts b/packages/stitch/tests/mergeResolvers.test.ts index 6de2c84ec0f..c5d7e50e99b 100644 --- a/packages/stitch/tests/mergeResolvers.test.ts +++ b/packages/stitch/tests/mergeResolvers.test.ts @@ -2,6 +2,7 @@ import { makeExecutableSchema } from '@graphql-tools/schema'; import { stitchSchemas, createMergedTypeResolver } from '@graphql-tools/stitch'; import { MergedTypeResolver, MergedTypeResolverOptions } from '@graphql-tools/delegate'; import { graphql } from 'graphql'; +import { assertSome } from '@graphql-tools/utils'; describe('Merge resolvers', () => { const firstSchema = makeExecutableSchema({ @@ -91,6 +92,7 @@ describe('Merge resolvers', () => { it('works with wrapped resolvers', async () => { function wrapResolve(mergedTypeResolverOptions: MergedTypeResolverOptions): MergedTypeResolver { const defaultResolve = createMergedTypeResolver(mergedTypeResolverOptions); + assertSome(defaultResolve) return async (obj, ctx, inf, sch, sel, key) => { const result = await defaultResolve(obj, ctx, inf, sch, sel, key); result.source += '->resolve'; diff --git a/packages/stitch/tests/selectionSetArgs.test.ts b/packages/stitch/tests/selectionSetArgs.test.ts index 0397a21c0dc..69c70b92ee5 100644 --- a/packages/stitch/tests/selectionSetArgs.test.ts +++ b/packages/stitch/tests/selectionSetArgs.test.ts @@ -1,19 +1,36 @@ -import { parseSelectionSet } from '@graphql-tools/utils'; +import { assertSome, parseSelectionSet } from '@graphql-tools/utils'; +import { FieldNode, SelectionNode, IntValueNode, ValueNode, } from 'graphql'; import { forwardArgsToSelectionSet } from '../src'; -describe('forwardArgsToSelectionSet', () => { +function assertIntValueNode(input: ValueNode): asserts input is IntValueNode { + if (input.kind !== "IntValue"){ + throw new Error(`Expected "StringValue", got "${input.kind}".`) + } +} + +function assertFieldNode(input: SelectionNode): asserts input is FieldNode { + if (input.kind !== "Field"){ + throw new Error(`Expected "SelectionNode", got "${input.kind}".`) + } +} +describe('forwardArgsToSelectionSet', () => { + // TODO: assert this const GATEWAY_FIELD = parseSelectionSet('{ posts(pageNumber: 1, perPage: 7) }').selections[0]; + assertFieldNode(GATEWAY_FIELD) test('passes all arguments to a hint selection set', () => { const buildSelectionSet = forwardArgsToSelectionSet('{ postIds }'); const result = buildSelectionSet(GATEWAY_FIELD).selections[0]; - + assertFieldNode(result) expect(result.name.value).toEqual('postIds'); + assertSome(result.arguments) expect(result.arguments.length).toEqual(2); expect(result.arguments[0].name.value).toEqual('pageNumber'); + assertIntValueNode(result.arguments[0].value) expect(result.arguments[0].value.value).toEqual('1'); expect(result.arguments[1].name.value).toEqual('perPage'); + assertIntValueNode(result.arguments[1].value) expect(result.arguments[1].value.value).toEqual('7'); }); @@ -22,12 +39,16 @@ describe('forwardArgsToSelectionSet', () => { const result = buildSelectionSet(GATEWAY_FIELD); expect(result.selections.length).toEqual(2); + assertFieldNode(result.selections[0]) expect(result.selections[0].name.value).toEqual('id'); + assertSome(result.selections[0].arguments) expect(result.selections[0].arguments.length).toEqual(0); - + assertFieldNode(result.selections[1]) expect(result.selections[1].name.value).toEqual('postIds'); + assertSome(result.selections[1].arguments) expect(result.selections[1].arguments.length).toEqual(1); expect(result.selections[1].arguments[0].name.value).toEqual('pageNumber'); + assertIntValueNode(result.selections[1].arguments[0].value) expect(result.selections[1].arguments[0].value.value).toEqual('1'); }); }); diff --git a/packages/stitch/tests/selectionSets.test.ts b/packages/stitch/tests/selectionSets.test.ts index 45962680a32..665342bc219 100644 --- a/packages/stitch/tests/selectionSets.test.ts +++ b/packages/stitch/tests/selectionSets.test.ts @@ -3,7 +3,7 @@ import { graphql } from 'graphql'; import { delegateToSchema } from '@graphql-tools/delegate'; import { batchDelegateToSchema } from '@graphql-tools/batch-delegate'; import { makeExecutableSchema } from '@graphql-tools/schema'; -import { IResolvers } from '@graphql-tools/utils'; +import { assertSome, IResolvers } from '@graphql-tools/utils'; import { stitchSchemas } from '../src/stitchSchemas'; @@ -61,7 +61,7 @@ describe('delegateToSchema ', () => { selectionSet: '{ name }', resolve: (location) => { const name = location.name; - return findPropertyByLocationName(sampleData.Property, name).location + return findPropertyByLocationName(sampleData.Property, name)?.location .coordinates; }, }, @@ -213,6 +213,7 @@ describe('delegateToSchema ', () => { `, ); + assertSome(data) expect(data.posts).toEqual(expectedData); }); @@ -237,7 +238,7 @@ describe('delegateToSchema ', () => { } `, ); - + assertSome(data) expect(data.posts).toEqual(expectedData); }); @@ -262,7 +263,7 @@ describe('delegateToSchema ', () => { } `, ); - + assertSome(data) expect(data.posts).toEqual(expectedData); }); @@ -291,7 +292,7 @@ describe('delegateToSchema ', () => { } `, ) - + assertSome(data) expect(data.posts).toEqual(expectedData); }); }); diff --git a/packages/stitch/tests/splitMergedTypeEntryPointsTransformer.test.ts b/packages/stitch/tests/splitMergedTypeEntryPointsTransformer.test.ts index e871178db9c..bda4d300c5f 100644 --- a/packages/stitch/tests/splitMergedTypeEntryPointsTransformer.test.ts +++ b/packages/stitch/tests/splitMergedTypeEntryPointsTransformer.test.ts @@ -1,5 +1,6 @@ import { makeExecutableSchema } from '@graphql-tools/schema'; import { splitMergedTypeEntryPointsTransformer } from '@graphql-tools/stitch'; +import { assertSome } from '@graphql-tools/utils'; const schema = makeExecutableSchema({ typeDefs: 'type Query { go:Int }' }); @@ -18,6 +19,7 @@ describe('splitMergedTypeEntryPointsTransformer', () => { }); expect(results.length).toEqual(1); + assertSome(results[0].merge) expect(results[0].merge.Product).toEqual({ selectionSet: '{ yep }', fieldName: 'yep', @@ -79,10 +81,12 @@ describe('splitMergedTypeEntryPointsTransformer', () => { }); expect(results.length).toEqual(2); + assertSome(results[0].merge) expect(results[0].merge.Product).toEqual({ selectionSet: '{ id }', fieldName: 'productById', }); + assertSome(results[1].merge) expect(results[1].merge.Product).toEqual({ selectionSet: '{ upc }', fieldName: 'productByUpc', diff --git a/packages/stitch/tests/stitchSchemas.test.ts b/packages/stitch/tests/stitchSchemas.test.ts index 9ca6140bdd0..beaf1a7b1ac 100644 --- a/packages/stitch/tests/stitchSchemas.test.ts +++ b/packages/stitch/tests/stitchSchemas.test.ts @@ -21,6 +21,7 @@ import { SchemaDirectiveVisitor, IResolvers, ExecutionResult, + assertSome, } from '@graphql-tools/utils'; import { addMocksToSchema } from '@graphql-tools/mock'; @@ -2238,6 +2239,7 @@ fragment BookingFragment on Booking { expect(originalResult.errors).toBeUndefined(); expect(originalResult.data).toBeDefined(); + assertSome(originalResult.data) expect(originalResult.data.persona.transactions.items.length).toBe(2); expect(originalResult.data.persona.transactions.items[1].debt).toBeDefined(); @@ -2337,6 +2339,7 @@ fragment BookingFragment on Booking { return ast; }, transformResult: (originalResult: ExecutionResult) => { + assertSome(originalResult.data) originalResult.data.persona = { page: originalResult.data.persona.transactions.items, }; @@ -2379,7 +2382,7 @@ fragment BookingFragment on Booking { }); expect(result.errors).toBeUndefined(); - expect(result.data).toBeDefined(); + assertSome(result.data) expect(result.data.flattenedTransactions.page.length).toBe(2); expect(result.data.flattenedTransactions.page[1].debt).toBeDefined(); }); @@ -2558,6 +2561,8 @@ fragment BookingFragment on Booking { ...propertyResult.data, ...bookingResult.data, }); + assertSome(stitchedResult.errors) + assertSome(propertyResult.errors) expect(stitchedResult.errors.map(removeLocations)).toEqual( propertyResult.errors.map(removeLocations), ); @@ -2575,6 +2580,7 @@ fragment BookingFragment on Booking { ); expect(stitchedResult2.data).toBe(null); + assertSome(stitchedResult2.errors) expect(stitchedResult2.errors.map(removeLocations)).toEqual([ { message: 'Sample error non-null!', @@ -2627,6 +2633,7 @@ fragment BookingFragment on Booking { }, }); + assertSome(result.errors) const errorsWithoutLocations = result.errors.map(removeLocations); const expectedErrors: Array = [ @@ -2690,10 +2697,10 @@ fragment BookingFragment on Booking { const stitchedResult = await graphql(stitchedSchema, propertyQuery, undefined, {}); [propertyResult, stitchedResult].forEach((result) => { - expect(result.errors).toBeDefined(); + assertSome(result.errors) expect(result.errors.length > 0).toBe(true); const error = result.errors[0]; - expect(error.extensions).toBeDefined(); + assertSome(error.extensions) expect(error.extensions.code).toBe('SOME_CUSTOM_CODE'); }); }, @@ -2702,33 +2709,34 @@ fragment BookingFragment on Booking { describe('types in schema extensions', () => { test('should parse descriptions on new types', () => { - expect(stitchedSchema.getType('AnotherNewScalar').description).toBe( + expect(stitchedSchema.getType('AnotherNewScalar')?.description).toBe( 'Description of AnotherNewScalar.', ); - expect(stitchedSchema.getType('TestingScalar').description).toBe( + expect(stitchedSchema.getType('TestingScalar')?.description).toBe( 'A type that uses TestScalar.', ); - expect(stitchedSchema.getType('Color').description).toBe( + expect(stitchedSchema.getType('Color')?.description).toBe( 'A type that uses an Enum.', ); - expect(stitchedSchema.getType('NumericEnum').description).toBe( + expect(stitchedSchema.getType('NumericEnum')?.description).toBe( 'A type that uses an Enum with a numeric constant.', ); - expect(stitchedSchema.getType('LinkType').description).toBe( + expect(stitchedSchema.getType('LinkType')?.description).toBe( 'A new type linking the Property type.', ); - expect(stitchedSchema.getType('LinkType').description).toBe( + expect(stitchedSchema.getType('LinkType')?.description).toBe( 'A new type linking the Property type.', ); }); test('should parse descriptions on new fields', () => { const Query = stitchedSchema.getQueryType(); + assertSome(Query) expect(Query.getFields().linkTest.description).toBe( 'A new field on the root query.', ); @@ -3152,7 +3160,7 @@ fragment BookingFragment on Booking { }); const result = await graphql(schema, '{ book { cat: category } }'); - +assertSome(result.data) expect(result.data.book.cat).toBe('Test'); }); }); @@ -3285,7 +3293,7 @@ fragment BookingFragment on Booking { }); const result = await graphql(schema, '{ book { cat: category } }'); - +assertSome(result.data) expect(result.data.book.cat).toBe('Test'); }); }); diff --git a/packages/stitch/tests/stitchingFromSubschemas.test.ts b/packages/stitch/tests/stitchingFromSubschemas.test.ts index e5a333d5e41..a30a2e0afe9 100644 --- a/packages/stitch/tests/stitchingFromSubschemas.test.ts +++ b/packages/stitch/tests/stitchingFromSubschemas.test.ts @@ -15,6 +15,7 @@ import { graphql, GraphQLSchema } from 'graphql'; import { delegateToSchema } from '@graphql-tools/delegate'; import { addMocksToSchema } from '@graphql-tools/mock'; +import { assertSome } from '@graphql-tools/utils'; import { stitchSchemas } from '../src/stitchSchemas'; @@ -139,6 +140,7 @@ describe('merging without specifying fragments', () => { const result = await graphql(stitchedSchema, query); expect(result.errors).toBeUndefined(); + assertSome(result.data) expect(result.data.userById.chirps[1].id).not.toBe(null); expect(result.data.userById.chirps[1].text).not.toBe(null); expect(result.data.userById.chirps[1].author.email).not.toBe(null); diff --git a/packages/stitch/tests/typeMerging.test.ts b/packages/stitch/tests/typeMerging.test.ts index 6946d8020ab..1b6db5ce120 100644 --- a/packages/stitch/tests/typeMerging.test.ts +++ b/packages/stitch/tests/typeMerging.test.ts @@ -10,6 +10,8 @@ import { addMocksToSchema } from '@graphql-tools/mock'; import { delegateToSchema } from '@graphql-tools/delegate'; import { RenameRootFields, RenameTypes } from '@graphql-tools/wrap'; +import { assertSome } from '@graphql-tools/utils'; + import { stitchSchemas } from '../src/stitchSchemas'; @@ -107,6 +109,7 @@ describe('merging using type merging', () => { ); expect(result.errors).toBeUndefined(); + assertSome(result.data) expect(result.data.userById.__typename).toBe('User'); expect(result.data.userById.chirps[1].id).not.toBe(null); expect(result.data.userById.chirps[1].text).not.toBe(null); @@ -422,7 +425,7 @@ describe('Merged associations', () => { } } `); - + assertSome(data) expect(data.posts).toEqual([{ title: 'Post 55', network: { domain: 'network57.com' }, @@ -527,6 +530,7 @@ describe('merging using type merging when renaming', () => { ); expect(result.errors).toBeUndefined(); + assertSome(result.data) expect(result.data.User_userById.__typename).toBe('Gateway_User'); expect(result.data.User_userById.chirps[1].id).not.toBe(null); expect(result.data.User_userById.chirps[1].text).not.toBe(null); @@ -552,7 +556,7 @@ describe('external object annotation with batchDelegateToSchema', () => { resolvers: { Query: { networks: (_root, { ids }) => - ids.map((id) => ({ id, domains: [{ id: Number(id) + 3, name: `network${id}.com` }] })), + ids.map((id: unknown) => ({ id, domains: [{ id: Number(id) + 3, name: `network${id}.com` }] })), }, }, }) @@ -573,7 +577,7 @@ describe('external object annotation with batchDelegateToSchema', () => { resolvers: { Query: { posts: (_root, { ids }) => - ids.map((id) => ({ + ids.map((id: unknown) => ({ id, network: { id: Number(id) + 2 }, })), @@ -617,7 +621,7 @@ describe('external object annotation with batchDelegateToSchema', () => { } `, ) - + assertSome(data) expect(data.posts).toEqual([ { network: { id: '57', domains: [{ id: '60', name: 'network57.com' }] }, diff --git a/packages/stitching-directives/src/defaultStitchingDirectiveOptions.ts b/packages/stitching-directives/src/defaultStitchingDirectiveOptions.ts index 59dbe7376b2..4a36846226e 100644 --- a/packages/stitching-directives/src/defaultStitchingDirectiveOptions.ts +++ b/packages/stitching-directives/src/defaultStitchingDirectiveOptions.ts @@ -1,6 +1,6 @@ -import { StitchingDirectivesOptions } from './types'; +import { StitchingDirectivesFinalOptions } from './types'; -export const defaultStitchingDirectiveOptions: StitchingDirectivesOptions = { +export const defaultStitchingDirectiveOptions: StitchingDirectivesFinalOptions = { keyDirectiveName: 'key', computedDirectiveName: 'computed', canonicalDirectiveName: 'canonical', diff --git a/packages/stitching-directives/src/extractVariables.ts b/packages/stitching-directives/src/extractVariables.ts index 7a81ce64c3e..9da1fac8bf4 100644 --- a/packages/stitching-directives/src/extractVariables.ts +++ b/packages/stitching-directives/src/extractVariables.ts @@ -7,12 +7,12 @@ export function extractVariables(inputValue: ValueNode): { inputValue: ValueNode const variablePaths = Object.create(null); const keyPathVisitor = { - enter: (_node: any, key: string | number) => { + enter: (_node: any, key: string | number | undefined) => { if (typeof key === 'number') { path.push(key); } }, - leave: (_node: any, key: string | number) => { + leave: (_node: any, key: string | number | undefined) => { if (typeof key === 'number') { path.pop(); } @@ -29,7 +29,7 @@ export function extractVariables(inputValue: ValueNode): { inputValue: ValueNode }; const variableVisitor = { - enter: (node: VariableNode, key: string | number) => { + enter: (node: VariableNode, key: string | number | undefined) => { if (typeof key === 'number') { variablePaths[node.name.value] = path.concat([key]); } else { diff --git a/packages/stitching-directives/src/pathsFromSelectionSet.ts b/packages/stitching-directives/src/pathsFromSelectionSet.ts index 0bd077cc698..bca1586562f 100644 --- a/packages/stitching-directives/src/pathsFromSelectionSet.ts +++ b/packages/stitching-directives/src/pathsFromSelectionSet.ts @@ -3,12 +3,13 @@ import { Kind, SelectionNode, SelectionSetNode } from 'graphql'; export function pathsFromSelectionSet(selectionSet: SelectionSetNode, path: Array = []): Array> { let paths: Array> = []; selectionSet.selections.forEach(selection => { - paths = paths.concat(pathsFromSelection(selection, path)); + const addition = pathsFromSelection(selection, path) ?? []; + paths = [...paths, ...addition]; }); return paths; } -function pathsFromSelection(selection: SelectionNode, path: Array): Array> { +function pathsFromSelection(selection: SelectionNode, path: Array): Array> | undefined { if (selection.kind === Kind.FIELD) { const responseKey = selection.alias?.value ?? selection.name.value; if (selection.selectionSet) { diff --git a/packages/stitching-directives/src/properties.ts b/packages/stitching-directives/src/properties.ts index 6c74dd3230c..b0166b3e6e0 100644 --- a/packages/stitching-directives/src/properties.ts +++ b/packages/stitching-directives/src/properties.ts @@ -29,6 +29,9 @@ export function getProperty(object: Record, path: Array): a const newPath = path.slice(); const key = newPath.shift(); + if (key == null) { + return; + } const prop = object[key]; return getProperty(prop, newPath); @@ -49,7 +52,7 @@ export function getProperties(object: Record, propertyTree: Propert const prop = object[key]; - newObject[key] = deepMap(prop, (item) => getProperties(item, subKey)); + newObject[key] = deepMap(prop, item => getProperties(item, subKey)); }); return newObject; diff --git a/packages/stitching-directives/src/stitchingDirectives.ts b/packages/stitching-directives/src/stitchingDirectives.ts index 19f72d9586b..c460c562bf2 100644 --- a/packages/stitching-directives/src/stitchingDirectives.ts +++ b/packages/stitching-directives/src/stitchingDirectives.ts @@ -2,15 +2,13 @@ import { GraphQLDirective, GraphQLList, GraphQLNonNull, GraphQLSchema, GraphQLSt import { SubschemaConfig } from '@graphql-tools/delegate'; -import { StitchingDirectivesOptions } from './types'; +import { StitchingDirectivesFinalOptions, StitchingDirectivesOptions } from './types'; import { defaultStitchingDirectiveOptions } from './defaultStitchingDirectiveOptions'; import { stitchingDirectivesValidator } from './stitchingDirectivesValidator'; import { stitchingDirectivesTransformer } from './stitchingDirectivesTransformer'; -export function stitchingDirectives( - options: StitchingDirectivesOptions = {} -): { +export function stitchingDirectives(options: StitchingDirectivesOptions = {}): { keyDirectiveTypeDefs: string; computedDirectiveTypeDefs: string; mergeDirectiveTypeDefs: string; @@ -25,7 +23,7 @@ export function stitchingDirectives( canonicalDirective: GraphQLDirective; allStitchingDirectives: Array; } { - const finalOptions = { + const finalOptions: StitchingDirectivesFinalOptions = { ...defaultStitchingDirectiveOptions, ...options, }; diff --git a/packages/stitching-directives/src/stitchingDirectivesTransformer.ts b/packages/stitching-directives/src/stitchingDirectivesTransformer.ts index 68284c1cb11..78cb22311c4 100644 --- a/packages/stitching-directives/src/stitchingDirectivesTransformer.ts +++ b/packages/stitching-directives/src/stitchingDirectivesTransformer.ts @@ -15,8 +15,9 @@ import { valueFromASTUntyped, } from 'graphql'; -import { cloneSubschemaConfig, SubschemaConfig } from '@graphql-tools/delegate'; +import { cloneSubschemaConfig, SubschemaConfig, MergedTypeConfig, MergedFieldConfig } from '@graphql-tools/delegate'; import { + assertSome, getDirectives, getImplementingTypes, MapperKind, @@ -52,9 +53,8 @@ export function stitchingDirectivesTransformer( const selectionSetsByType: Record = Object.create(null); const computedFieldSelectionSets: Record> = Object.create(null); const mergedTypesResolversInfo: Record = Object.create(null); - const canonicalTypesInfo: Record }> = Object.create( - null - ); + const canonicalTypesInfo: Record }> = + Object.create(null); const schema = subschemaConfig.schema; @@ -64,8 +64,9 @@ export function stitchingDirectivesTransformer( function setCanonicalDefinition(typeName: string, fieldName?: string): void { canonicalTypesInfo[typeName] = canonicalTypesInfo[typeName] || Object.create(null); if (fieldName) { - canonicalTypesInfo[typeName].fields = canonicalTypesInfo[typeName].fields || Object.create(null); - canonicalTypesInfo[typeName].fields[fieldName] = true; + const fields: Record = canonicalTypesInfo[typeName].fields ?? Object.create(null); + canonicalTypesInfo[typeName].fields = fields; + fields[fieldName] = true; } else { canonicalTypesInfo[typeName].canonical = true; } @@ -75,13 +76,13 @@ export function stitchingDirectivesTransformer( [MapperKind.OBJECT_TYPE]: type => { const directives = getDirectives(schema, type, pathToDirectivesInExtensions); - const keyDirective = directives[keyDirectiveName]; - if (keyDirective) { + if (keyDirectiveName != null && directives[keyDirectiveName] != null) { + const keyDirective = directives[keyDirectiveName]; const selectionSet = parseSelectionSet(keyDirective.selectionSet, { noLocation: true }); selectionSetsByType[type.name] = selectionSet; } - if (directives[canonicalDirectiveName]) { + if (canonicalDirectiveName != null && directives[canonicalDirectiveName]) { setCanonicalDefinition(type.name); } @@ -90,8 +91,8 @@ export function stitchingDirectivesTransformer( [MapperKind.OBJECT_FIELD]: (fieldConfig, fieldName, typeName) => { const directives = getDirectives(schema, fieldConfig, pathToDirectivesInExtensions); - const computedDirective = directives[computedDirectiveName]; - if (computedDirective) { + if (computedDirectiveName != null && directives[computedDirectiveName] != null) { + const computedDirective = directives[computedDirectiveName]; const selectionSet = parseSelectionSet(computedDirective.selectionSet, { noLocation: true }); if (!computedFieldSelectionSets[typeName]) { computedFieldSelectionSets[typeName] = Object.create(null); @@ -99,11 +100,15 @@ export function stitchingDirectivesTransformer( computedFieldSelectionSets[typeName][fieldName] = selectionSet; } - const mergeDirectiveKeyField = directives[mergeDirectiveName]?.keyField; - if (mergeDirectiveKeyField) { + if ( + mergeDirectiveName != null && + directives[mergeDirectiveName] != null && + directives[mergeDirectiveName].keyField + ) { + const mergeDirectiveKeyField = directives[mergeDirectiveName].keyField; const selectionSet = parseSelectionSet(`{ ${mergeDirectiveKeyField}}`, { noLocation: true }); - const typeNames: Array = directives[mergeDirectiveName]?.types; + const typeNames: Array = directives[mergeDirectiveName].types; const returnType = getNamedType(fieldConfig.type); @@ -117,7 +122,7 @@ export function stitchingDirectivesTransformer( }); } - if (directives[canonicalDirectiveName]) { + if (canonicalDirectiveName != null && directives[canonicalDirectiveName] != null) { setCanonicalDefinition(typeName, fieldName); } @@ -126,7 +131,7 @@ export function stitchingDirectivesTransformer( [MapperKind.INTERFACE_TYPE]: type => { const directives = getDirectives(schema, type, pathToDirectivesInExtensions); - if (directives[canonicalDirectiveName]) { + if (canonicalDirectiveName != null && directives[canonicalDirectiveName] != null) { setCanonicalDefinition(type.name); } @@ -135,7 +140,7 @@ export function stitchingDirectivesTransformer( [MapperKind.INTERFACE_FIELD]: (fieldConfig, fieldName, typeName) => { const directives = getDirectives(schema, fieldConfig, pathToDirectivesInExtensions); - if (directives[canonicalDirectiveName]) { + if (canonicalDirectiveName != null && directives[canonicalDirectiveName]) { setCanonicalDefinition(typeName, fieldName); } @@ -144,7 +149,7 @@ export function stitchingDirectivesTransformer( [MapperKind.INPUT_OBJECT_TYPE]: type => { const directives = getDirectives(schema, type, pathToDirectivesInExtensions); - if (directives[canonicalDirectiveName]) { + if (canonicalDirectiveName != null && directives[canonicalDirectiveName] != null) { setCanonicalDefinition(type.name); } @@ -153,7 +158,7 @@ export function stitchingDirectivesTransformer( [MapperKind.INPUT_OBJECT_FIELD]: (inputFieldConfig, fieldName, typeName) => { const directives = getDirectives(schema, inputFieldConfig, pathToDirectivesInExtensions); - if (directives[canonicalDirectiveName]) { + if (canonicalDirectiveName != null && directives[canonicalDirectiveName] != null) { setCanonicalDefinition(typeName, fieldName); } @@ -162,7 +167,7 @@ export function stitchingDirectivesTransformer( [MapperKind.UNION_TYPE]: type => { const directives = getDirectives(schema, type, pathToDirectivesInExtensions); - if (directives[canonicalDirectiveName]) { + if (canonicalDirectiveName != null && directives[canonicalDirectiveName] != null) { setCanonicalDefinition(type.name); } @@ -171,7 +176,7 @@ export function stitchingDirectivesTransformer( [MapperKind.ENUM_TYPE]: type => { const directives = getDirectives(schema, type, pathToDirectivesInExtensions); - if (directives[canonicalDirectiveName]) { + if (canonicalDirectiveName != null && directives[canonicalDirectiveName] != null) { setCanonicalDefinition(type.name); } @@ -180,7 +185,7 @@ export function stitchingDirectivesTransformer( [MapperKind.SCALAR_TYPE]: type => { const directives = getDirectives(schema, type, pathToDirectivesInExtensions); - if (directives[canonicalDirectiveName]) { + if (canonicalDirectiveName != null && directives[canonicalDirectiveName] != null) { setCanonicalDefinition(type.name); } @@ -247,7 +252,7 @@ export function stitchingDirectivesTransformer( [MapperKind.OBJECT_FIELD]: (fieldConfig, fieldName) => { const directives = getDirectives(schema, fieldConfig, pathToDirectivesInExtensions); - if (directives[mergeDirectiveName]) { + if (mergeDirectiveName != null && directives[mergeDirectiveName] != null) { const directiveArgumentMap = directives[mergeDirectiveName]; const returnType = getNullableType(fieldConfig.type); @@ -262,7 +267,7 @@ export function stitchingDirectivesTransformer( const keyExpr = key != null ? buildKeyExpr(key) : keyField != null ? `$key.${keyField}` : '$key'; const keyArg: string = directiveArgumentMap.keyArg; - const argNames = keyArg == null ? [Object.keys(fieldConfig.args)[0]] : keyArg.split('.'); + const argNames = keyArg == null ? [Object.keys(fieldConfig.args ?? {})[0]] : keyArg.split('.'); const lastArgName = argNames.pop(); mergeArgsExpr = returnsList ? `${lastArgName}: [[${keyExpr}]]` : `${lastArgName}: ${keyExpr}`; @@ -277,7 +282,9 @@ export function stitchingDirectivesTransformer( forEachConcreteTypeName(namedType, schema, typeNames, typeName => { const parsedMergeArgsExpr = parseMergeArgsExpr( mergeArgsExpr, - allSelectionSetsByType[typeName] == null ? undefined : mergeSelectionSets(...allSelectionSetsByType[typeName]), + allSelectionSetsByType[typeName] == null + ? undefined + : mergeSelectionSets(...allSelectionSetsByType[typeName]) ); const additionalArgs = directiveArgumentMap.additionalArgs; @@ -301,48 +308,45 @@ export function stitchingDirectivesTransformer( }); Object.entries(selectionSetsByType).forEach(([typeName, selectionSet]) => { - if (newSubschemaConfig.merge == null) { - newSubschemaConfig.merge = Object.create(null); - } + const mergeConfig: Record> = + newSubschemaConfig.merge ?? Object.create(null); + newSubschemaConfig.merge = mergeConfig; - if (newSubschemaConfig.merge[typeName] == null) { + if (mergeConfig[typeName] == null) { newSubschemaConfig.merge[typeName] = Object.create(null); } - const mergeTypeConfig = newSubschemaConfig.merge[typeName]; + const mergeTypeConfig = mergeConfig[typeName]; mergeTypeConfig.selectionSet = print(selectionSet); }); Object.entries(computedFieldSelectionSets).forEach(([typeName, selectionSets]) => { - if (newSubschemaConfig.merge == null) { - newSubschemaConfig.merge = Object.create(null); - } + const mergeConfig: Record> = + newSubschemaConfig.merge ?? Object.create(null); + newSubschemaConfig.merge = mergeConfig; - if (newSubschemaConfig.merge[typeName] == null) { - newSubschemaConfig.merge[typeName] = Object.create(null); + if (mergeConfig[typeName] == null) { + mergeConfig[typeName] = Object.create(null); } const mergeTypeConfig = newSubschemaConfig.merge[typeName]; - - if (mergeTypeConfig.fields == null) { - mergeTypeConfig.fields = Object.create(null); - } + const mergeTypeConfigFields: Record = mergeTypeConfig.fields ?? Object.create(null); + mergeTypeConfig.fields = mergeTypeConfigFields; Object.entries(selectionSets).forEach(([fieldName, selectionSet]) => { - if (mergeTypeConfig.fields[fieldName] == null) { - mergeTypeConfig.fields[fieldName] = Object.create(null); - } + const fieldConfig: MergedFieldConfig = mergeTypeConfigFields[fieldName] ?? Object.create(null); + mergeTypeConfigFields[fieldName] = fieldConfig; - mergeTypeConfig.fields[fieldName].selectionSet = print(selectionSet); - mergeTypeConfig.fields[fieldName].computed = true; + fieldConfig.selectionSet = print(selectionSet); + fieldConfig.computed = true; }); }); Object.entries(mergedTypesResolversInfo).forEach(([typeName, mergedTypeResolverInfo]) => { - if (newSubschemaConfig.merge == null) { - newSubschemaConfig.merge = Object.create(null); - } + const mergeConfig: Record> = + newSubschemaConfig.merge ?? Object.create(null); + newSubschemaConfig.merge = mergeConfig; if (newSubschemaConfig.merge[typeName] == null) { newSubschemaConfig.merge[typeName] = Object.create(null); @@ -361,9 +365,9 @@ export function stitchingDirectivesTransformer( }); Object.entries(canonicalTypesInfo).forEach(([typeName, canonicalTypeInfo]) => { - if (newSubschemaConfig.merge == null) { - newSubschemaConfig.merge = Object.create(null); - } + const mergeConfig: Record> = + newSubschemaConfig.merge ?? Object.create(null); + newSubschemaConfig.merge = mergeConfig; if (newSubschemaConfig.merge[typeName] == null) { newSubschemaConfig.merge[typeName] = Object.create(null); @@ -376,14 +380,13 @@ export function stitchingDirectivesTransformer( } if (canonicalTypeInfo.fields) { - if (mergeTypeConfig.fields == null) { - mergeTypeConfig.fields = Object.create(null); - } + const mergeTypeConfigFields: Record = mergeTypeConfig.fields ?? Object.create(null); + mergeTypeConfig.fields = mergeTypeConfigFields; Object.keys(canonicalTypeInfo.fields).forEach(fieldName => { - if (mergeTypeConfig.fields[fieldName] == null) { - mergeTypeConfig.fields[fieldName] = Object.create(null); + if (mergeTypeConfigFields[fieldName] == null) { + mergeTypeConfigFields[fieldName] = Object.create(null); } - mergeTypeConfig.fields[fieldName].canonical = true; + mergeTypeConfigFields[fieldName].canonical = true; }); } }); @@ -421,11 +424,11 @@ function generateKeyFn(mergedTypeResolverInfo: MergedTypeResolverInfo): (origina function generateArgsFromKeysFn( mergedTypeResolverInfo: MergedTypeResolverInfo -): (keys: Array) => Record { +): (keys: ReadonlyArray) => Record { const { expansions, args } = mergedTypeResolverInfo; - return (keys: Array): Record => { + return (keys: ReadonlyArray): Record => { const newArgs = mergeDeep({}, args); - expansions.forEach(expansion => { + expansions?.forEach(expansion => { const mappingInstructions = expansion.mappingInstructions; const expanded: Array = []; keys.forEach(key => { @@ -452,7 +455,7 @@ function generateArgsFn(mergedTypeResolverInfo: MergedTypeResolverInfo): (origin return (originalResult: any): Record => { const newArgs = mergeDeep({}, args); const filteredResult = getProperties(originalResult, usedProperties); - mappingInstructions.forEach(mappingInstruction => { + mappingInstructions?.forEach(mappingInstruction => { const { destinationPath, sourcePath } = mappingInstruction; addProperty(newArgs, destinationPath, getProperty(filteredResult, sourcePath)); }); @@ -472,7 +475,8 @@ function buildKeyExpr(key: Array): string { } const aliasParts = aliasPath.split('.'); const lastAliasPart = aliasParts.pop(); - let object: any = { [lastAliasPart]: `$key.${keyPath}` }; + assertSome(lastAliasPart); + let object: Record = { [lastAliasPart]: `$key.${keyPath}` }; aliasParts.reverse().forEach(aliasPart => { object = { [aliasPart]: object }; diff --git a/packages/stitching-directives/src/stitchingDirectivesValidator.ts b/packages/stitching-directives/src/stitchingDirectivesValidator.ts index 65e76784762..bda6241d88b 100644 --- a/packages/stitching-directives/src/stitchingDirectivesValidator.ts +++ b/packages/stitching-directives/src/stitchingDirectivesValidator.ts @@ -10,7 +10,14 @@ import { parseValue, } from 'graphql'; -import { getDirectives, getImplementingTypes, MapperKind, mapSchema, parseSelectionSet } from '@graphql-tools/utils'; +import { + getDirectives, + getImplementingTypes, + isSome, + MapperKind, + mapSchema, + parseSelectionSet, +} from '@graphql-tools/utils'; import { StitchingDirectivesOptions } from './types'; @@ -34,7 +41,7 @@ export function stitchingDirectivesValidator( [MapperKind.OBJECT_TYPE]: type => { const directives = getDirectives(schema, type, pathToDirectivesInExtensions); - if (directives[keyDirectiveName]) { + if (keyDirectiveName != null && directives[keyDirectiveName]) { const directiveArgumentMap = directives[keyDirectiveName]; parseSelectionSet(directiveArgumentMap.selectionSet); } @@ -44,12 +51,12 @@ export function stitchingDirectivesValidator( [MapperKind.OBJECT_FIELD]: (fieldConfig, _fieldName, typeName) => { const directives = getDirectives(schema, fieldConfig, pathToDirectivesInExtensions); - if (directives[computedDirectiveName]) { + if (computedDirectiveName != null && directives[computedDirectiveName]) { const directiveArgumentMap = directives[computedDirectiveName]; parseSelectionSet(directiveArgumentMap.selectionSet); } - if (directives[mergeDirectiveName]) { + if (mergeDirectiveName != null && directives[mergeDirectiveName]) { const directiveArgumentMap = directives[mergeDirectiveName]; if (typeName !== queryTypeName) { @@ -71,7 +78,7 @@ export function stitchingDirectivesValidator( parseMergeArgsExpr(mergeArgsExpr); } - const args = Object.keys(fieldConfig.args); + const args = Object.keys(fieldConfig.args ?? {}); const keyArg = directiveArgumentMap.keyArg; if (keyArg == null) { @@ -148,7 +155,7 @@ export function stitchingDirectivesValidator( const implementingTypes = isInterfaceType(returnType) ? getImplementingTypes(returnType.name, schema).map(typeName => schema.getType(typeName)) : returnType.getTypes(); - const implementingTypeNames = implementingTypes.map(type => type.name); + const implementingTypeNames = implementingTypes.filter(isSome).map(type => type.name); typeNames.forEach(typeName => { if (!implementingTypeNames.includes(typeName)) { throw new Error( diff --git a/packages/stitching-directives/src/types.ts b/packages/stitching-directives/src/types.ts index e549fce20f4..4bcf978eb72 100644 --- a/packages/stitching-directives/src/types.ts +++ b/packages/stitching-directives/src/types.ts @@ -30,6 +30,12 @@ export interface StitchingDirectivesOptions { pathToDirectivesInExtensions?: Array; } +type Complete = { + [P in keyof Required]: Exclude extends Required> ? T[P] : T[P] | undefined, undefined>; +}; + +export type StitchingDirectivesFinalOptions = Complete; + export interface MergedTypeResolverInfo extends ParsedMergeArgsExpr { fieldName: string; returnsList: boolean; diff --git a/packages/testing/assertion.ts b/packages/testing/assertion.ts new file mode 100644 index 00000000000..2055925abc4 --- /dev/null +++ b/packages/testing/assertion.ts @@ -0,0 +1,103 @@ +import { + ASTNode, + EnumTypeDefinitionNode, + GraphQLEnumType, + GraphQLInputObjectType, + GraphQLInterfaceType, + GraphQLObjectType, + GraphQLScalarType, + GraphQLUnionType, + InputObjectTypeDefinitionNode, + InterfaceTypeDefinitionNode, + ListValueNode, + NamedTypeNode, + ObjectTypeDefinitionNode, + ScalarTypeDefinitionNode, + UnionTypeDefinitionNode, +} from 'graphql'; + +export function assertGraphQLObjectType(input: unknown): asserts input is GraphQLObjectType { + if (input instanceof GraphQLObjectType) { + return; + } + throw new Error('Expected GraphQLObjectType.'); +} +export function assertGraphQLEnumType(input: unknown): asserts input is GraphQLEnumType { + if (input instanceof GraphQLEnumType) { + return; + } + throw new Error('Expected GraphQLObjectType.'); +} +export function assertGraphQLScalerType(input: unknown): asserts input is GraphQLScalarType { + if (input instanceof GraphQLScalarType) { + return; + } + throw new Error('Expected GraphQLScalerType.'); +} +export function assertGraphQLInterfaceType(input: unknown): asserts input is GraphQLInterfaceType { + if (input instanceof GraphQLInterfaceType) { + return; + } + throw new Error('Expected GraphQLInterfaceType.'); +} +export function assertGraphQLUnionType(input: unknown): asserts input is GraphQLUnionType { + if (input instanceof GraphQLUnionType) { + return; + } + throw new Error('Expected GraphQLUnionType.'); +} +export function assertGraphQLInputObjectType(input: unknown): asserts input is GraphQLInputObjectType { + if (input instanceof GraphQLInputObjectType) { + return; + } + throw new Error('Expected GraphQLInputObjectType.'); +} + +export function assertEnumTypeDefinitionNode(input: ASTNode): asserts input is EnumTypeDefinitionNode { + if (input.kind === 'EnumTypeDefinition') { + return; + } + throw new Error('Expected EnumTypeDefinitionNode.'); +} +export function assertObjectTypeDefinitionNode(input: ASTNode): asserts input is ObjectTypeDefinitionNode { + if (input.kind === 'ObjectTypeDefinition') { + return; + } + throw new Error(`Expected ObjectTypeDefinitionNode. Got ${input.kind}`); +} +export function assertInterfaceTypeDefinitionNode(input: ASTNode): asserts input is InterfaceTypeDefinitionNode { + if (input.kind === 'InterfaceTypeDefinition') { + return; + } + throw new Error(`Expected InterfaceTypeDefinitionNode. Got ${input.kind}`); +} +export function assertUnionTypeDefinitionNode(input: ASTNode): asserts input is UnionTypeDefinitionNode { + if (input.kind === 'UnionTypeDefinition') { + return; + } + throw new Error(`Expected InterfaceTypeDefinitionNode. Got ${input.kind}`); +} +export function assertNamedTypeNode(input: ASTNode): asserts input is NamedTypeNode { + if (input.kind === 'NamedType') { + return; + } + throw new Error(`Expected NamedTypeNode. Got ${input.kind}`); +} +export function assertScalarTypeDefinitionNode(input: ASTNode): asserts input is ScalarTypeDefinitionNode { + if (input.kind === 'ScalarTypeDefinition') { + return; + } + throw new Error(`Expected ScalarTypeDefinitionNode. Got ${input.kind}`); +} +export function assertInputObjectTypeDefinitionNode(input: ASTNode): asserts input is InputObjectTypeDefinitionNode { + if (input.kind === 'InputObjectTypeDefinition') { + return; + } + throw new Error(`Expected InputObjectTypeDefinitionNode. Got ${input.kind}`); +} +export function assertListValueNode(input: ASTNode): asserts input is ListValueNode { + if (input.kind === 'ListValue') { + return; + } + throw new Error(`Expected ListValueNode. Got ${input.kind}`); +} diff --git a/packages/testing/utils.ts b/packages/testing/utils.ts index 579de0a85e1..f761e79ddff 100644 --- a/packages/testing/utils.ts +++ b/packages/testing/utils.ts @@ -16,9 +16,10 @@ export function normalizeString(str: string) { type PromiseOf any> = T extends (...args: any[]) => Promise ? R : ReturnType; export function runTests< - TSync extends (...args: any[]) => TResult, - TAsync extends (...args: any[]) => Promise, - TResult = ReturnType + TResult extends any, + TArgs extends Array, + TSync extends (...args: TArgs) => TResult, + TAsync extends (...args: TArgs) => Promise >({ sync: executeSync, async: executeAsync }: { sync?: TSync; async?: TAsync }) { return ( testRunner: ( @@ -29,7 +30,7 @@ export function runTests< if (executeSync) { // sync describe('sync', () => { - testRunner((...args: Parameters) => { + testRunner((...args: Parameters) => { return new Promise>((resolve, reject) => { try { const result: any = executeSync(...args); @@ -98,7 +99,7 @@ export function mockGraphQLServer({ path: string | RegExp | ((path: string) => boolean); intercept?: (obj: nock.ReplyFnContext) => void; method?: string; -}) { +}): nock.Scope { const handler = async function (this: nock.ReplyFnContext, uri: string, body: any) { if (intercept) { intercept(this); @@ -155,4 +156,5 @@ export function mockGraphQLServer({ case 'POST': return nock(host).post(path).reply(handler); } + throw new Error(`Unsupported method: ${method}`); } diff --git a/packages/utils/src/Interfaces.ts b/packages/utils/src/Interfaces.ts index e8f0aade45b..bcd11634d6a 100644 --- a/packages/utils/src/Interfaces.ts +++ b/packages/utils/src/Interfaces.ts @@ -199,15 +199,21 @@ export type InputFieldFilter = ( ) => boolean; export type FieldFilter = ( - typeName?: string, - fieldName?: string, - fieldConfig?: GraphQLFieldConfig | GraphQLInputFieldConfig + typeName: string, + fieldName: string, + fieldConfig: GraphQLFieldConfig | GraphQLInputFieldConfig +) => boolean; + +export type ObjectFieldFilter = ( + typeName: string, + fieldName: string, + fieldConfig: GraphQLFieldConfig ) => boolean; export type RootFieldFilter = ( - operation?: 'Query' | 'Mutation' | 'Subscription', - rootFieldName?: string, - fieldConfig?: GraphQLFieldConfig + operation: 'Query' | 'Mutation' | 'Subscription', + rootFieldName: string, + fieldConfig: GraphQLFieldConfig ) => boolean; export type TypeFilter = (typeName: string, type: GraphQLType) => boolean; @@ -459,6 +465,18 @@ export interface SchemaMapper { [MapperKind.DIRECTIVE]?: DirectiveMapper; } +export type SchemaFieldMapperTypes = Array< + | MapperKind.FIELD + | MapperKind.COMPOSITE_FIELD + | MapperKind.OBJECT_FIELD + | MapperKind.ROOT_FIELD + | MapperKind.QUERY_ROOT_FIELD + | MapperKind.MUTATION_ROOT_FIELD + | MapperKind.SUBSCRIPTION_ROOT_FIELD + | MapperKind.INTERFACE_FIELD + | MapperKind.INPUT_OBJECT_FIELD +>; + export type NamedTypeMapper = (type: GraphQLNamedType, schema: GraphQLSchema) => GraphQLNamedType | null | undefined; export type ScalarTypeMapper = (type: GraphQLScalarType, schema: GraphQLSchema) => GraphQLScalarType | null | undefined; diff --git a/packages/utils/src/SchemaDirectiveVisitor.ts b/packages/utils/src/SchemaDirectiveVisitor.ts index 8ca764837c2..2add8dd28d3 100644 --- a/packages/utils/src/SchemaDirectiveVisitor.ts +++ b/packages/utils/src/SchemaDirectiveVisitor.ts @@ -11,6 +11,7 @@ import { VisitableSchemaType } from './Interfaces'; import { SchemaVisitor } from './SchemaVisitor'; import { visitSchema } from './visitSchema'; import { getArgumentValues } from './getArgumentValues'; +import { Maybe } from 'packages/graphql-tools/src'; // This class represents a reusable implementation of a @directive that may // appear in a GraphQL schema written in Schema Definition Language. @@ -152,9 +153,11 @@ export class SchemaDirectiveVisitor extends SchemaV } else { let directiveNodes = type?.astNode?.directives ?? []; - const extensionASTNodes: ReadonlyArray = (type as { - extensionASTNodes?: Array; - }).extensionASTNodes; + const extensionASTNodes: Maybe> = ( + type as { + extensionASTNodes?: Array; + } + ).extensionASTNodes; if (extensionASTNodes != null) { extensionASTNodes.forEach(extensionASTNode => { diff --git a/packages/utils/src/astFromValueUntyped.ts b/packages/utils/src/astFromValueUntyped.ts index d11ed47ccd4..2719adb05ab 100644 --- a/packages/utils/src/astFromValueUntyped.ts +++ b/packages/utils/src/astFromValueUntyped.ts @@ -15,7 +15,7 @@ import { Kind, ObjectFieldNode, ValueNode } from 'graphql'; * | null | NullValue | * */ -export function astFromValueUntyped(value: any): ValueNode { +export function astFromValueUntyped(value: any): ValueNode | null { // only explicit null, not undefined, NaN if (value === null) { return { kind: Kind.NULL }; diff --git a/packages/utils/src/build-operation-for-field.ts b/packages/utils/src/build-operation-for-field.ts index 3209968d150..15e17e24fc1 100644 --- a/packages/utils/src/build-operation-for-field.ts +++ b/packages/utils/src/build-operation-for-field.ts @@ -340,15 +340,13 @@ function resolveSelectionSet({ } return null; }) - .filter(f => { - if (f) { - if ('selectionSet' in f) { - return f.selectionSet?.selections?.length; - } else { - return true; - } + .filter((f): f is SelectionNode => { + if (f == null) { + return false; + } else if ('selectionSet' in f) { + return !!f.selectionSet?.selections?.length; } - return false; + return true; }), }; } diff --git a/packages/utils/src/collectFields.ts b/packages/utils/src/collectFields.ts index aef6ab5d9b9..120121c3d32 100644 --- a/packages/utils/src/collectFields.ts +++ b/packages/utils/src/collectFields.ts @@ -83,13 +83,13 @@ function shouldIncludeNode( ): boolean { const skip = getDirectiveValues(GraphQLSkipDirective, node, exeContext.variableValues); - if (skip?.if === true) { + if (skip?.['if'] === true) { return false; } const include = getDirectiveValues(GraphQLIncludeDirective, node, exeContext.variableValues); - if (include?.if === false) { + if (include?.['if'] === false) { return false; } diff --git a/packages/utils/src/create-schema-definition.ts b/packages/utils/src/create-schema-definition.ts index dbb974c2338..a685aff021f 100644 --- a/packages/utils/src/create-schema-definition.ts +++ b/packages/utils/src/create-schema-definition.ts @@ -9,7 +9,7 @@ export function createSchemaDefinition( config?: { force?: boolean; } -): string { +): string | undefined { const schemaRoot: { query?: string; mutation?: string; diff --git a/packages/utils/src/debug-log.ts b/packages/utils/src/debug-log.ts index 8478cc07ea2..292ec974cee 100644 --- a/packages/utils/src/debug-log.ts +++ b/packages/utils/src/debug-log.ts @@ -1,5 +1,5 @@ export function debugLog(...args: any[]): void { - if (process && process.env && process.env.DEBUG && !process.env.GQL_tools_NODEBUG) { + if (process && process.env && process.env['DEBUG'] && !process.env['GQL_tools_NODEBUG']) { // tslint:disable-next-line: no-console console.log(...args); } diff --git a/packages/utils/src/executor.ts b/packages/utils/src/executor.ts index 7ff1d6422a8..f91a36d6840 100644 --- a/packages/utils/src/executor.ts +++ b/packages/utils/src/executor.ts @@ -1,7 +1,7 @@ import { DocumentNode, GraphQLResolveInfo } from 'graphql'; import { ExecutionResult } from './Interfaces'; -export interface ExecutionParams, TContext = any> { +export interface ExecutionParams = Record, TContext = any> { document: DocumentNode; variables?: TArgs; extensions?: Record; @@ -36,4 +36,4 @@ export type Subscriber> = < TContext extends TBaseContext = TBaseContext >( params: ExecutionParams -) => Promise> | ExecutionResult>; +) => Promise> | ExecutionResult>; diff --git a/packages/utils/src/filterSchema.ts b/packages/utils/src/filterSchema.ts index 5941bd21d7b..5648d5ee15a 100644 --- a/packages/utils/src/filterSchema.ts +++ b/packages/utils/src/filterSchema.ts @@ -84,7 +84,7 @@ function filterRootFields( Object.entries(config.fields).forEach(([fieldName, field]) => { if (rootFieldFilter && !rootFieldFilter(operation, fieldName, config.fields[fieldName])) { delete config.fields[fieldName]; - } else if (argumentFilter) { + } else if (argumentFilter && field.args) { for (const argName of Object.keys(field.args)) { if (!argumentFilter(operation, fieldName, argName, field.args[argName])) { delete field.args[argName]; diff --git a/packages/utils/src/fix-schema-ast.ts b/packages/utils/src/fix-schema-ast.ts index efccdff9e74..acf7739a848 100644 --- a/packages/utils/src/fix-schema-ast.ts +++ b/packages/utils/src/fix-schema-ast.ts @@ -10,15 +10,16 @@ function buildFixedSchema(schema: GraphQLSchema, options: BuildSchemaOptions & S } export function fixSchemaAst(schema: GraphQLSchema, options: BuildSchemaOptions & SchemaPrintOptions) { - let schemaWithValidAst: GraphQLSchema; + // eslint-disable-next-line no-undef-init + let schemaWithValidAst: GraphQLSchema | undefined = undefined; if (!schema.astNode || !schema.extensionASTNodes) { schemaWithValidAst = buildFixedSchema(schema, options); } - if (!schema.astNode) { + if (!schema.astNode && schemaWithValidAst?.astNode) { schema.astNode = schemaWithValidAst.astNode; } - if (!schema.extensionASTNodes) { + if (!schema.extensionASTNodes && schemaWithValidAst?.astNode) { schema.extensionASTNodes = schemaWithValidAst.extensionASTNodes; } return schema; diff --git a/packages/utils/src/get-directives.ts b/packages/utils/src/get-directives.ts index c96b0965602..24668414f05 100644 --- a/packages/utils/src/get-directives.ts +++ b/packages/utils/src/get-directives.ts @@ -23,6 +23,7 @@ import { GraphQLEnumValueConfig, EnumValueDefinitionNode, } from 'graphql'; +import { Maybe } from 'packages/graphql-tools/src'; import { getArgumentValues } from './getArgumentValues'; @@ -57,7 +58,7 @@ type DirectableGraphQLObject = export function getDirectivesInExtensions( node: DirectableGraphQLObject, pathToDirectivesInExtensions = ['directives'] -): DirectiveUseMap { +): Maybe { const directivesInExtensions = pathToDirectivesInExtensions.reduce( (acc, pathSegment) => (acc == null ? acc : acc[pathSegment]), node?.extensions diff --git a/packages/utils/src/get-fields-with-directives.ts b/packages/utils/src/get-fields-with-directives.ts index 19b3ca5f72c..5f6eeb9d311 100644 --- a/packages/utils/src/get-fields-with-directives.ts +++ b/packages/utils/src/get-fields-with-directives.ts @@ -60,6 +60,10 @@ export function getFieldsWithDirectives(documentNode: DocumentNode, options: Opt for (const type of allTypes) { const typeName = type.name.value; + if (type.fields == null) { + continue; + } + for (const field of type.fields) { if (field.directives && field.directives.length > 0) { const fieldName = field.name.value; diff --git a/packages/utils/src/get-user-types-from-schema.ts b/packages/utils/src/get-user-types-from-schema.ts index 9f3583740a1..d0b6f8e51c2 100644 --- a/packages/utils/src/get-user-types-from-schema.ts +++ b/packages/utils/src/get-user-types-from-schema.ts @@ -12,19 +12,22 @@ export function getUserTypesFromSchema(schema: GraphQLSchema): GraphQLObjectType const allTypesMap = schema.getTypeMap(); // tslint:disable-next-line: no-unnecessary-local-variable - const modelTypes = Object.values(allTypesMap).filter((graphqlType: GraphQLObjectType) => { + const modelTypes = Object.values(allTypesMap).filter((graphqlType): graphqlType is GraphQLObjectType => { if (isObjectType(graphqlType)) { // Filter out private types if (graphqlType.name.startsWith('__')) { return false; } - if (schema.getMutationType() && graphqlType.name === schema.getMutationType().name) { + const schemaMutationType = schema.getMutationType(); + if (schemaMutationType && graphqlType.name === schemaMutationType.name) { return false; } - if (schema.getQueryType() && graphqlType.name === schema.getQueryType().name) { + const schemaQueryType = schema.getMutationType(); + if (schemaQueryType && graphqlType.name === schemaQueryType.name) { return false; } - if (schema.getSubscriptionType() && graphqlType.name === schema.getSubscriptionType().name) { + const schemaSubscriptionType = schema.getMutationType(); + if (schemaSubscriptionType && graphqlType.name === schemaSubscriptionType.name) { return false; } @@ -34,5 +37,5 @@ export function getUserTypesFromSchema(schema: GraphQLSchema): GraphQLObjectType return false; }); - return modelTypes as GraphQLObjectType[]; + return modelTypes; } diff --git a/packages/utils/src/helpers.ts b/packages/utils/src/helpers.ts index 9bf75a42409..fbcdea600c5 100644 --- a/packages/utils/src/helpers.ts +++ b/packages/utils/src/helpers.ts @@ -48,27 +48,30 @@ export function isValidPath(str: string): boolean { } export function compareStrings(a: A, b: B) { - if (a.toString() < b.toString()) { + if (String(a) < String(b)) { return -1; } - if (a.toString() > b.toString()) { + if (String(a) > String(b)) { return 1; } return 0; } -export function nodeToString(a: ASTNode) { +export function nodeToString(a: ASTNode): string { + let name: string | undefined; if ('alias' in a) { - return a.alias.value; + name = a.alias?.value; } - - if ('name' in a) { - return a.name.value; + if (name == null && 'name' in a) { + name = a.name?.value; + } + if (name == null) { + name = a.kind; } - return a.kind; + return name; } export function compareNodes(a: ASTNode, b: ASTNode, customFn?: (a: any, b: any) => number) { @@ -81,3 +84,16 @@ export function compareNodes(a: ASTNode, b: ASTNode, customFn?: (a: any, b: any) return compareStrings(aStr, bStr); } + +export function isSome(input: T): input is Exclude { + return input != null; +} + +export function assertSome( + input: T, + message = 'Value should be something' +): asserts input is Exclude { + if (input == null) { + throw new Error(message); + } +} diff --git a/packages/utils/src/implementsAbstractType.ts b/packages/utils/src/implementsAbstractType.ts index f2b2630346e..c8554a8a59d 100644 --- a/packages/utils/src/implementsAbstractType.ts +++ b/packages/utils/src/implementsAbstractType.ts @@ -1,7 +1,10 @@ import { GraphQLType, GraphQLSchema, doTypesOverlap, isCompositeType } from 'graphql'; +import { Maybe } from '@graphql-tools/utils'; -export function implementsAbstractType(schema: GraphQLSchema, typeA: GraphQLType, typeB: GraphQLType) { - if (typeA === typeB) { +export function implementsAbstractType(schema: GraphQLSchema, typeA: Maybe, typeB: Maybe) { + if (typeB == null || typeA == null) { + return false; + } else if (typeA === typeB) { return true; } else if (isCompositeType(typeA) && isCompositeType(typeB)) { return doTypesOverlap(schema, typeA, typeB); diff --git a/packages/utils/src/loaders.ts b/packages/utils/src/loaders.ts index 21a1eb2b485..56efc329de4 100644 --- a/packages/utils/src/loaders.ts +++ b/packages/utils/src/loaders.ts @@ -27,8 +27,8 @@ export interface Loader; resolveGlobsSync?(globs: ResolverGlobs, options?: TOptions): TPointer[]; - load(pointer: TPointer, options?: TOptions): Promise; - loadSync?(pointer: TPointer, options?: TOptions): Source | never; + load(pointer: TPointer, options?: TOptions): Promise; + loadSync?(pointer: TPointer, options?: TOptions): Source | null | never; } export type SchemaLoader = Loader< diff --git a/packages/utils/src/mapSchema.ts b/packages/utils/src/mapSchema.ts index 656d8424127..a561b6890f3 100644 --- a/packages/utils/src/mapSchema.ts +++ b/packages/utils/src/mapSchema.ts @@ -40,6 +40,7 @@ import { IDefaultValueIteratorFn, ArgumentMapper, EnumValueMapper, + SchemaFieldMapperTypes, } from './Interfaces'; import { rewireTypes } from './rewire'; @@ -351,17 +352,17 @@ function mapArguments(originalTypeMap: TypeMap, schema: GraphQLSchema, schemaMap if (isObjectType(originalType)) { newTypeMap[typeName] = new GraphQLObjectType({ - ...((config as unknown) as GraphQLObjectTypeConfig), + ...(config as unknown as GraphQLObjectTypeConfig), fields: newFieldConfigMap, }); } else if (isInterfaceType(originalType)) { newTypeMap[typeName] = new GraphQLInterfaceType({ - ...((config as unknown) as GraphQLInterfaceTypeConfig), + ...(config as unknown as GraphQLInterfaceTypeConfig), fields: newFieldConfigMap, }); } else { newTypeMap[typeName] = new GraphQLInputObjectType({ - ...((config as unknown) as GraphQLInputObjectTypeConfig), + ...(config as unknown as GraphQLInputObjectTypeConfig), fields: newFieldConfigMap, }); } @@ -431,16 +432,17 @@ function getTypeMapper(schema: GraphQLSchema, schemaMapper: SchemaMapper, typeNa let typeMapper: NamedTypeMapper | undefined; const stack = [...specifiers]; while (!typeMapper && stack.length > 0) { - const next = stack.pop(); + // It is safe to use the ! operator here as we check the length. + const next = stack.pop()!; typeMapper = schemaMapper[next] as NamedTypeMapper; } return typeMapper != null ? typeMapper : null; } -function getFieldSpecifiers(schema: GraphQLSchema, typeName: string): Array { +function getFieldSpecifiers(schema: GraphQLSchema, typeName: string): SchemaFieldMapperTypes { const type = schema.getType(typeName); - const specifiers = [MapperKind.FIELD]; + const specifiers: SchemaFieldMapperTypes = [MapperKind.FIELD]; if (isObjectType(type)) { specifiers.push(MapperKind.COMPOSITE_FIELD, MapperKind.OBJECT_FIELD); @@ -472,11 +474,13 @@ function getFieldMapper | GraphQLInputFie let fieldMapper: GenericFieldMapper | undefined; const stack = [...specifiers]; while (!fieldMapper && stack.length > 0) { - const next = stack.pop(); - fieldMapper = schemaMapper[next] as GenericFieldMapper; + // It is safe to use the ! operator here as we check the length. + const next = stack.pop()!; + // TODO: fix this as unknown cast + fieldMapper = schemaMapper[next] as unknown as GenericFieldMapper; } - return fieldMapper != null ? fieldMapper : null; + return fieldMapper ?? null; } function getArgumentMapper(schemaMapper: SchemaMapper): ArgumentMapper | null { diff --git a/packages/utils/src/mergeDeep.ts b/packages/utils/src/mergeDeep.ts index 041a7ac1af8..780bdd4815f 100644 --- a/packages/utils/src/mergeDeep.ts +++ b/packages/utils/src/mergeDeep.ts @@ -1,3 +1,4 @@ +import { isSome } from './helpers'; import { isScalarType } from 'graphql'; type BoxedTupleTypes = { [P in keyof T]: [T[P]] }[Exclude]; @@ -20,7 +21,9 @@ export function mergeDeep( if (sourcePrototype) { Object.getOwnPropertyNames(sourcePrototype).forEach(key => { const descriptor = Object.getOwnPropertyDescriptor(sourcePrototype, key); - Object.defineProperty(outputPrototype, key, descriptor); + if (isSome(descriptor)) { + Object.defineProperty(outputPrototype, key, descriptor); + } }); } diff --git a/packages/utils/src/observableToAsyncIterable.ts b/packages/utils/src/observableToAsyncIterable.ts index 09d5f00ca16..3fbe7322853 100644 --- a/packages/utils/src/observableToAsyncIterable.ts +++ b/packages/utils/src/observableToAsyncIterable.ts @@ -5,9 +5,7 @@ export interface Observer { } export interface Observable { - subscribe( - observer: Observer - ): { + subscribe(observer: Observer): { unsubscribe: () => void; }; } @@ -22,7 +20,8 @@ export function observableToAsyncIterable(observable: Observable): AsyncIt const pushValue = (value: any) => { if (pullQueue.length !== 0) { - pullQueue.shift()({ value, done: false }); + // It is safe to use the ! operator here as we check the length. + pullQueue.shift()!({ value, done: false }); } else { pushQueue.push({ value, done: false }); } @@ -30,7 +29,8 @@ export function observableToAsyncIterable(observable: Observable): AsyncIt const pushError = (error: any) => { if (pullQueue.length !== 0) { - pullQueue.shift()({ value: { errors: [error] }, done: false }); + // It is safe to use the ! operator here as we check the length. + pullQueue.shift()!({ value: { errors: [error] }, done: false }); } else { pushQueue.push({ value: { errors: [error] }, done: false }); } @@ -38,14 +38,15 @@ export function observableToAsyncIterable(observable: Observable): AsyncIt const pushDone = () => { if (pullQueue.length !== 0) { - pullQueue.shift()({ done: true }); + // It is safe to use the ! operator here as we check the length. + pullQueue.shift()!({ done: true }); } else { pushQueue.push({ done: true }); } }; const pullValue = () => - new Promise(resolve => { + new Promise>(resolve => { if (pushQueue.length !== 0) { const element = pushQueue.shift(); // either {value: {errors: [...]}} or {value: ...} @@ -79,7 +80,8 @@ export function observableToAsyncIterable(observable: Observable): AsyncIt return { next() { - return listening ? pullValue() : this.return(); + // return is a defined method, so it is safe to call it. + return listening ? pullValue() : this.return!(); }, return() { emptyQueue(); diff --git a/packages/utils/src/parse-graphql-sdl.ts b/packages/utils/src/parse-graphql-sdl.ts index f8fd4eb8378..8e9ef461eac 100644 --- a/packages/utils/src/parse-graphql-sdl.ts +++ b/packages/utils/src/parse-graphql-sdl.ts @@ -13,7 +13,7 @@ import { import { dedentBlockStringValue } from 'graphql/language/blockString.js'; import { GraphQLParseOptions } from './Interfaces'; -export function parseGraphQLSDL(location: string, rawSDL: string, options: GraphQLParseOptions = {}) { +export function parseGraphQLSDL(location: string | undefined, rawSDL: string, options: GraphQLParseOptions = {}) { let document: DocumentNode; const sdl: string = rawSDL; @@ -73,10 +73,7 @@ export function getLeadingCommentBlock(node: ASTNode): void | string { return comments.length > 0 ? comments.reverse().join('\n') : undefined; } -export function transformCommentsToDescriptions( - sourceSdl: string, - options: GraphQLParseOptions = {} -): DocumentNode | null { +export function transformCommentsToDescriptions(sourceSdl: string, options: GraphQLParseOptions = {}): DocumentNode { const parsedDoc = parse(sourceSdl, { ...options, noLocation: false, diff --git a/packages/utils/src/print-schema-with-directives.ts b/packages/utils/src/print-schema-with-directives.ts index a7d98477b51..4e1da45b0b2 100644 --- a/packages/utils/src/print-schema-with-directives.ts +++ b/packages/utils/src/print-schema-with-directives.ts @@ -44,15 +44,16 @@ import { EnumTypeDefinitionNode, GraphQLScalarType, ScalarTypeDefinitionNode, - StringValueNode, DefinitionNode, DocumentNode, + StringValueNode, } from 'graphql'; -import { GetDocumentNodeFromSchemaOptions, PrintSchemaWithDirectivesOptions } from './types'; +import { GetDocumentNodeFromSchemaOptions, PrintSchemaWithDirectivesOptions, Maybe } from './types'; import { astFromType } from './astFromType'; import { getDirectivesInExtensions } from './get-directives'; import { astFromValueUntyped } from './astFromValueUntyped'; +import { isSome } from './helpers'; export function getDocumentNodeFromSchema( schema: GraphQLSchema, @@ -118,9 +119,9 @@ export function printSchemaWithDirectives( export function astFromSchema( schema: GraphQLSchema, - pathToDirectivesInExtensions: Array -): SchemaDefinitionNode | SchemaExtensionNode { - const operationTypeMap: Record = { + pathToDirectivesInExtensions?: Array +): SchemaDefinitionNode | SchemaExtensionNode | null { + const operationTypeMap: Record> = { query: undefined, mutation: undefined, subscription: undefined, @@ -142,7 +143,7 @@ export function astFromSchema( } }); - const rootTypeMap: Record = { + const rootTypeMap: Record> = { query: schema.getQueryType(), mutation: schema.getMutationType(), subscription: schema.getSubscriptionType(), @@ -162,9 +163,7 @@ export function astFromSchema( } }); - const operationTypes = Object.values(operationTypeMap).filter( - operationTypeDefinitionNode => operationTypeDefinitionNode != null - ); + const operationTypes = Object.values(operationTypeMap).filter(isSome); const directives = getDirectiveNodes(schema, schema, pathToDirectivesInExtensions); @@ -178,12 +177,14 @@ export function astFromSchema( directives, }; - ((schemaNode as unknown) as { description: StringValueNode }).description = - ((schema.astNode as unknown) as { description: string })?.description ?? - ((schema as unknown) as { description: string }).description != null + // This code is so weird because it needs to support GraphQL.js 14 + // In GraphQL.js 14 there is no `description` value on schemaNode + (schemaNode as unknown as { description?: StringValueNode }).description = + (schema.astNode as unknown as { description: string })?.description ?? + (schema as unknown as { description: string }).description != null ? { kind: Kind.STRING, - value: ((schema as unknown) as { description: string }).description, + value: (schema as unknown as { description: string }).description, block: true, } : undefined; @@ -219,14 +220,14 @@ export function astFromDirective( kind: Kind.NAME, value: location, })) - : undefined, + : [], }; } export function getDirectiveNodes( entity: GraphQLSchema | GraphQLNamedType | GraphQLEnumValue, schema: GraphQLSchema, - pathToDirectivesInExtensions: Array + pathToDirectivesInExtensions?: Array ): Array { const directivesInExtensions = getDirectivesInExtensions(entity, pathToDirectivesInExtensions); @@ -244,7 +245,12 @@ export function getDirectiveNodes( if (directivesInExtensions != null) { directives = makeDirectiveNodes(schema, directivesInExtensions); } else { - directives = [].concat(...nodes.filter(node => node.directives != null).map(node => node.directives)); + directives = []; + for (const node of nodes) { + if (node.directives) { + directives.push(...node.directives); + } + } } return directives; @@ -252,15 +258,15 @@ export function getDirectiveNodes( export function getDeprecatableDirectiveNodes( entity: GraphQLArgument | GraphQLField | GraphQLInputField, - schema: GraphQLSchema, - pathToDirectivesInExtensions: Array + schema?: GraphQLSchema, + pathToDirectivesInExtensions?: Array ): Array { let directiveNodesBesidesDeprecated: Array = []; - let deprecatedDirectiveNode: DirectiveNode; + let deprecatedDirectiveNode: Maybe = null; const directivesInExtensions = getDirectivesInExtensions(entity, pathToDirectivesInExtensions); - let directives: ReadonlyArray; + let directives: Maybe>; if (directivesInExtensions != null) { directives = makeDirectiveNodes(schema, directivesInExtensions); } else { @@ -269,17 +275,17 @@ export function getDeprecatableDirectiveNodes( if (directives != null) { directiveNodesBesidesDeprecated = directives.filter(directive => directive.name.value !== 'deprecated'); - if (((entity as unknown) as { deprecationReason: string }).deprecationReason != null) { + if ((entity as unknown as { deprecationReason: string }).deprecationReason != null) { deprecatedDirectiveNode = directives.filter(directive => directive.name.value === 'deprecated')?.[0]; } } if ( - ((entity as unknown) as { deprecationReason: string }).deprecationReason != null && + (entity as unknown as { deprecationReason: string }).deprecationReason != null && deprecatedDirectiveNode == null ) { deprecatedDirectiveNode = makeDeprecatedDirective( - ((entity as unknown) as { deprecationReason: string }).deprecationReason + (entity as unknown as { deprecationReason: string }).deprecationReason ); } @@ -290,25 +296,26 @@ export function getDeprecatableDirectiveNodes( export function astFromArg( arg: GraphQLArgument, - schema: GraphQLSchema, - pathToDirectivesInExtensions: Array + schema?: GraphQLSchema, + pathToDirectivesInExtensions?: Array ): InputValueDefinitionNode { return { kind: Kind.INPUT_VALUE_DEFINITION, description: - arg.astNode?.description ?? arg.description + arg.astNode?.description ?? + (arg.description ? { kind: Kind.STRING, value: arg.description, block: true, } - : undefined, + : undefined), name: { kind: Kind.NAME, value: arg.name, }, type: astFromType(arg.type), - defaultValue: arg.defaultValue !== undefined ? astFromValue(arg.defaultValue, arg.type) : undefined, + defaultValue: arg.defaultValue !== undefined ? astFromValue(arg.defaultValue, arg.type) ?? undefined : undefined, directives: getDeprecatableDirectiveNodes(arg, schema, pathToDirectivesInExtensions), }; } @@ -316,18 +323,19 @@ export function astFromArg( export function astFromObjectType( type: GraphQLObjectType, schema: GraphQLSchema, - pathToDirectivesInExtensions: Array + pathToDirectivesInExtensions?: Array ): ObjectTypeDefinitionNode { return { kind: Kind.OBJECT_TYPE_DEFINITION, description: - type.astNode?.description ?? type.description + type.astNode?.description ?? + (type.description ? { kind: Kind.STRING, value: type.description, block: true, } - : undefined, + : undefined), name: { kind: Kind.NAME, value: type.name, @@ -341,18 +349,19 @@ export function astFromObjectType( export function astFromInterfaceType( type: GraphQLInterfaceType, schema: GraphQLSchema, - pathToDirectivesInExtensions: Array + pathToDirectivesInExtensions?: Array ): InterfaceTypeDefinitionNode { - const node = { + const node: InterfaceTypeDefinitionNode = { kind: Kind.INTERFACE_TYPE_DEFINITION, description: - type.astNode?.description ?? type.description + type.astNode?.description ?? + (type.description ? { kind: Kind.STRING, value: type.description, block: true, } - : undefined, + : undefined), name: { kind: Kind.NAME, value: type.name, @@ -362,8 +371,8 @@ export function astFromInterfaceType( }; if ('getInterfaces' in type) { - ((node as unknown) as { interfaces: Array }).interfaces = Object.values( - ((type as unknown) as GraphQLObjectType).getInterfaces() + (node as unknown as { interfaces: Array }).interfaces = Object.values( + (type as unknown as GraphQLObjectType).getInterfaces() ).map(iFace => astFromType(iFace) as NamedTypeNode); } @@ -373,18 +382,19 @@ export function astFromInterfaceType( export function astFromUnionType( type: GraphQLUnionType, schema: GraphQLSchema, - pathToDirectivesInExtensions: Array + pathToDirectivesInExtensions?: Array ): UnionTypeDefinitionNode { return { kind: Kind.UNION_TYPE_DEFINITION, description: - type.astNode?.description ?? type.description + type.astNode?.description ?? + (type.description ? { kind: Kind.STRING, value: type.description, block: true, } - : undefined, + : undefined), name: { kind: Kind.NAME, value: type.name, @@ -397,18 +407,19 @@ export function astFromUnionType( export function astFromInputObjectType( type: GraphQLInputObjectType, schema: GraphQLSchema, - pathToDirectivesInExtensions: Array + pathToDirectivesInExtensions?: Array ): InputObjectTypeDefinitionNode { return { kind: Kind.INPUT_OBJECT_TYPE_DEFINITION, description: - type.astNode?.description ?? type.description + type.astNode?.description ?? + (type.description ? { kind: Kind.STRING, value: type.description, block: true, } - : undefined, + : undefined), name: { kind: Kind.NAME, value: type.name, @@ -423,18 +434,19 @@ export function astFromInputObjectType( export function astFromEnumType( type: GraphQLEnumType, schema: GraphQLSchema, - pathToDirectivesInExtensions: Array + pathToDirectivesInExtensions?: Array ): EnumTypeDefinitionNode { return { kind: Kind.ENUM_TYPE_DEFINITION, description: - type.astNode?.description ?? type.description + type.astNode?.description ?? + (type.description ? { kind: Kind.STRING, value: type.description, block: true, } - : undefined, + : undefined), name: { kind: Kind.NAME, value: type.name, @@ -447,14 +459,14 @@ export function astFromEnumType( export function astFromScalarType( type: GraphQLScalarType, schema: GraphQLSchema, - pathToDirectivesInExtensions: Array + pathToDirectivesInExtensions?: Array ): ScalarTypeDefinitionNode { let directiveNodesBesidesSpecifiedBy: Array = []; - let specifiedByDirectiveNode: DirectiveNode; + let specifiedByDirectiveNode: Maybe = null; const directivesInExtensions = getDirectivesInExtensions(type, pathToDirectivesInExtensions); - let allDirectives: ReadonlyArray; + let allDirectives: Maybe>; if (directivesInExtensions != null) { allDirectives = makeDirectiveNodes(schema, directivesInExtensions); } else { @@ -463,14 +475,14 @@ export function astFromScalarType( if (allDirectives != null) { directiveNodesBesidesSpecifiedBy = allDirectives.filter(directive => directive.name.value !== 'specifiedBy'); - if (((type as unknown) as { specifiedByUrl: string }).specifiedByUrl != null) { + if ((type as unknown as { specifiedByUrl: string }).specifiedByUrl != null) { specifiedByDirectiveNode = allDirectives.filter(directive => directive.name.value === 'specifiedBy')?.[0]; } } - if (((type as unknown) as { specifiedByUrl: string }).specifiedByUrl != null && specifiedByDirectiveNode == null) { + if ((type as unknown as { specifiedByUrl: string }).specifiedByUrl != null && specifiedByDirectiveNode == null) { specifiedByDirectiveNode = makeDirectiveNode('specifiedBy', { - url: ((type as unknown) as { specifiedByUrl: string }).specifiedByUrl, + url: (type as unknown as { specifiedByUrl: string }).specifiedByUrl, }); } @@ -482,13 +494,14 @@ export function astFromScalarType( return { kind: Kind.SCALAR_TYPE_DEFINITION, description: - type.astNode?.description ?? type.description + type.astNode?.description ?? + (type.description ? { kind: Kind.STRING, value: type.description, block: true, } - : undefined, + : undefined), name: { kind: Kind.NAME, value: type.name, @@ -500,18 +513,19 @@ export function astFromScalarType( export function astFromField( field: GraphQLField, schema: GraphQLSchema, - pathToDirectivesInExtensions: Array + pathToDirectivesInExtensions?: Array ): FieldDefinitionNode { return { kind: Kind.FIELD_DEFINITION, description: - field.astNode?.description ?? field.description + field.astNode?.description ?? + (field.description ? { kind: Kind.STRING, value: field.description, block: true, } - : undefined, + : undefined), name: { kind: Kind.NAME, value: field.name, @@ -525,43 +539,45 @@ export function astFromField( export function astFromInputField( field: GraphQLInputField, schema: GraphQLSchema, - pathToDirectivesInExtensions: Array + pathToDirectivesInExtensions?: Array ): InputValueDefinitionNode { return { kind: Kind.INPUT_VALUE_DEFINITION, description: - field.astNode?.description ?? field.description + field.astNode?.description ?? + (field.description ? { kind: Kind.STRING, value: field.description, block: true, } - : undefined, + : undefined), name: { kind: Kind.NAME, value: field.name, }, type: astFromType(field.type), directives: getDeprecatableDirectiveNodes(field, schema, pathToDirectivesInExtensions), - defaultValue: astFromValue(field.defaultValue, field.type), + defaultValue: astFromValue(field.defaultValue, field.type) ?? undefined, }; } export function astFromEnumValue( value: GraphQLEnumValue, schema: GraphQLSchema, - pathToDirectivesInExtensions: Array + pathToDirectivesInExtensions?: Array ): EnumValueDefinitionNode { return { kind: Kind.ENUM_VALUE_DEFINITION, description: - value.astNode?.description ?? value.description + value.astNode?.description ?? + (value.description ? { kind: Kind.STRING, value: value.description, block: true, } - : undefined, + : undefined), name: { kind: Kind.NAME, value: value.name, @@ -577,7 +593,7 @@ export function makeDeprecatedDirective(deprecationReason: string): DirectiveNod export function makeDirectiveNode( name: string, args: Record, - directive?: GraphQLDirective + directive?: Maybe ): DirectiveNode { const directiveArguments: Array = []; @@ -586,27 +602,33 @@ export function makeDirectiveNode( const argName = arg.name; const argValue = args[argName]; if (argValue !== undefined) { + const value = astFromValue(argValue, arg.type); + if (value) { + directiveArguments.push({ + kind: Kind.ARGUMENT, + name: { + kind: Kind.NAME, + value: argName, + }, + value, + }); + } + } + }); + } else { + Object.entries(args).forEach(([argName, argValue]) => { + const value = astFromValueUntyped(argValue); + if (value) { directiveArguments.push({ kind: Kind.ARGUMENT, name: { kind: Kind.NAME, value: argName, }, - value: astFromValue(argValue, arg.type), + value, }); } }); - } else { - Object.entries(args).forEach(([argName, argValue]) => { - directiveArguments.push({ - kind: Kind.ARGUMENT, - name: { - kind: Kind.NAME, - value: argName, - }, - value: astFromValueUntyped(argValue), - }); - }); } return { @@ -619,7 +641,10 @@ export function makeDirectiveNode( }; } -export function makeDirectiveNodes(schema: GraphQLSchema, directiveValues: Record): Array { +export function makeDirectiveNodes( + schema: Maybe, + directiveValues: Record +): Array { const directiveNodes: Array = []; Object.entries(directiveValues).forEach(([directiveName, arrayOrSingleValue]) => { const directive = schema?.getDirective(directiveName); diff --git a/packages/utils/src/prune.ts b/packages/utils/src/prune.ts index 0bd5cff45f1..5d046383054 100644 --- a/packages/utils/src/prune.ts +++ b/packages/utils/src/prune.ts @@ -18,6 +18,7 @@ import { PruneSchemaOptions } from './types'; import { mapSchema } from './mapSchema'; import { MapperKind } from './Interfaces'; +import { isSome } from './helpers'; type NamedOutputType = | GraphQLObjectType @@ -47,7 +48,7 @@ export function pruneSchema(schema: GraphQLSchema, options: PruneSchemaOptions = Object.keys(schema.getTypeMap()).forEach(typeName => { const type = schema.getType(typeName); - if ('getInterfaces' in type) { + if (type && 'getInterfaces' in type) { type.getInterfaces().forEach(iface => { const implementations = getImplementations(pruningContext, iface); if (implementations == null) { @@ -187,9 +188,7 @@ function visitTypes(pruningContext: PruningContext, schema: GraphQLSchema): void const visitedTypes: Record = Object.create(null); - const rootTypes = [schema.getQueryType(), schema.getMutationType(), schema.getSubscriptionType()].filter( - type => type != null - ); + const rootTypes = [schema.getQueryType(), schema.getMutationType(), schema.getSubscriptionType()].filter(isSome); rootTypes.forEach(rootType => visitOutputType(visitedTypes, pruningContext, rootType)); diff --git a/packages/utils/src/rewire.ts b/packages/utils/src/rewire.ts index 930a8ee701f..4e257714569 100644 --- a/packages/utils/src/rewire.ts +++ b/packages/utils/src/rewire.ts @@ -111,7 +111,7 @@ export function rewireTypes( }; if ('interfaces' in newConfig) { newConfig.interfaces = () => - rewireNamedTypes(((config as unknown) as { interfaces: Array }).interfaces); + rewireNamedTypes((config as unknown as { interfaces: Array }).interfaces); } return new GraphQLInterfaceType(newConfig); } else if (isUnionType(type)) { @@ -139,7 +139,7 @@ export function rewireTypes( return new GraphQLScalarType(scalarConfig); } - throw new Error(`Unexpected schema type: ${(type as unknown) as string}`); + throw new Error(`Unexpected schema type: ${type as unknown as string}`); } function rewireFields(fields: GraphQLFieldConfigMap): GraphQLFieldConfigMap { @@ -147,7 +147,7 @@ export function rewireTypes( Object.keys(fields).forEach(fieldName => { const field = fields[fieldName]; const rewiredFieldType = rewireType(field.type); - if (rewiredFieldType != null) { + if (rewiredFieldType != null && field.args) { field.type = rewiredFieldType; field.args = rewireArgs(field.args); rewiredFields[fieldName] = field; diff --git a/packages/utils/src/transformInputValue.ts b/packages/utils/src/transformInputValue.ts index 6fa15c00ce4..db95d48d71c 100644 --- a/packages/utils/src/transformInputValue.ts +++ b/packages/utils/src/transformInputValue.ts @@ -1,12 +1,12 @@ import { GraphQLInputType, getNullableType, isLeafType, isListType, isInputObjectType } from 'graphql'; -import { InputLeafValueTransformer, InputObjectValueTransformer } from './types'; +import { InputLeafValueTransformer, InputObjectValueTransformer, Maybe } from './types'; export function transformInputValue( type: GraphQLInputType, value: any, - inputLeafValueTransformer: InputLeafValueTransformer = null, - inputObjectValueTransformer: InputObjectValueTransformer = null + inputLeafValueTransformer: Maybe = null, + inputObjectValueTransformer: Maybe = null ): any { if (value == null) { return value; diff --git a/packages/utils/src/validate-documents.ts b/packages/utils/src/validate-documents.ts index b2d429c8266..0c24bd25f3e 100644 --- a/packages/utils/src/validate-documents.ts +++ b/packages/utils/src/validate-documents.ts @@ -14,7 +14,7 @@ import AggregateError from '@ardatan/aggregate-error'; export type ValidationRule = (context: ValidationContext) => ASTVisitor; export interface LoadDocumentError { - readonly filePath: string; + readonly filePath?: string; readonly errors: ReadonlyArray; } @@ -42,20 +42,22 @@ export async function validateGraphQlDocuments( documentFiles.map(async documentFile => { const documentToValidate = { kind: Kind.DOCUMENT, - definitions: [...allFragments, ...documentFile.document.definitions].filter((definition, index, list) => { - if (definition.kind === Kind.FRAGMENT_DEFINITION) { - const firstIndex = list.findIndex( - def => def.kind === Kind.FRAGMENT_DEFINITION && def.name.value === definition.name.value - ); - const isDuplicated = firstIndex !== index; - - if (isDuplicated) { - return false; + definitions: [...allFragments, ...(documentFile.document?.definitions ?? [])].filter( + (definition, index, list) => { + if (definition.kind === Kind.FRAGMENT_DEFINITION) { + const firstIndex = list.findIndex( + def => def.kind === Kind.FRAGMENT_DEFINITION && def.name.value === definition.name.value + ); + const isDuplicated = firstIndex !== index; + + if (isDuplicated) { + return false; + } } - } - return true; - }), + return true; + } + ), }; const errors = validate(schema, documentToValidate, effectiveRules); @@ -82,7 +84,7 @@ export function checkValidationErrors(loadDocumentErrors: ReadonlyArray (error.stack += `\n at ${loadDocumentError.filePath}:${location.line}:${location.column}`) ); diff --git a/packages/utils/src/visitResult.ts b/packages/utils/src/visitResult.ts index 4c94956d7aa..fc870ea29aa 100644 --- a/packages/utils/src/visitResult.ts +++ b/packages/utils/src/visitResult.ts @@ -17,6 +17,7 @@ import { import { Request, GraphQLExecutionContext, ExecutionResult } from './Interfaces'; import { collectFields } from './collectFields'; +import { Maybe } from 'packages/graphql-tools/src'; export type ValueVisitor = (value: any) => any; @@ -101,10 +102,12 @@ export function visitResult( const errors = result.errors; const visitingErrors = errors != null && errorVisitorMap != null; - if (data != null) { + const operationDocumentNode = getOperationAST(request.document, undefined); + + if (data != null && operationDocumentNode != null) { result.data = visitRoot( data, - getOperationAST(request.document, undefined), + operationDocumentNode, partialExecutionContext, resultVisitorMap, visitingErrors ? errors : undefined, @@ -112,7 +115,7 @@ export function visitResult( ); } - if (visitingErrors) { + if (errors != null && errorVisitorMap) { result.errors = visitErrorsByType(errors, errorVisitorMap, errorInfo); } @@ -155,8 +158,8 @@ function visitRoot( root: any, operation: OperationDefinitionNode, exeContext: GraphQLExecutionContext, - resultVisitorMap: ResultVisitorMap, - errors: ReadonlyArray, + resultVisitorMap: Maybe, + errors: Maybe>, errorInfo: ErrorInfo ): any { const operationRootType = getOperationRootType(exeContext.schema, operation); @@ -176,9 +179,9 @@ function visitObjectValue( type: GraphQLObjectType, fieldNodeMap: Record>, exeContext: GraphQLExecutionContext, - resultVisitorMap: ResultVisitorMap, + resultVisitorMap: Maybe, pathIndex: number, - errors: ReadonlyArray, + errors: Maybe>, errorInfo: ErrorInfo ): Record { const fieldMap = type.getFields(); @@ -188,7 +191,7 @@ function visitObjectValue( const newObject = enterObject != null ? enterObject(object) : object; let sortedErrors: SortedErrors; - let errorMap: Record>; + let errorMap: Maybe>> = null; if (errors != null) { sortedErrors = sortErrorsByPathSegment(errors, pathIndex); errorMap = sortedErrors.errorMap; @@ -202,8 +205,8 @@ function visitObjectValue( const newPathIndex = pathIndex + 1; - let fieldErrors: Array; - if (errors != null) { + let fieldErrors: Array | undefined; + if (errorMap) { fieldErrors = errorMap[responseKey]; if (fieldErrors != null) { delete errorMap[responseKey]; @@ -230,10 +233,12 @@ function visitObjectValue( updateObject(newObject, '__typename', oldTypename, typeVisitorMap, '__typename'); } - if (errors != null) { - Object.keys(errorMap).forEach(unknownResponseKey => { - errorMap[unknownResponseKey].forEach(error => errorInfo.unpathedErrors.add(error)); - }); + if (errorMap) { + for (const errors of Object.values(errorMap)) { + for (const error of errors) { + errorInfo.unpathedErrors.add(error); + } + } } const leaveObject = typeVisitorMap?.__leave as ValueVisitor; @@ -273,7 +278,7 @@ function visitListValue( returnType: GraphQLOutputType, fieldNodes: Array, exeContext: GraphQLExecutionContext, - resultVisitorMap: ResultVisitorMap, + resultVisitorMap: Maybe, pathIndex: number, errors: ReadonlyArray, errorInfo: ErrorInfo @@ -288,9 +293,9 @@ function visitFieldValue( returnType: GraphQLOutputType, fieldNodes: Array, exeContext: GraphQLExecutionContext, - resultVisitorMap: ResultVisitorMap, + resultVisitorMap: Maybe, pathIndex: number, - errors: ReadonlyArray = [], + errors: ReadonlyArray | undefined = [], errorInfo: ErrorInfo ): any { if (value == null) { @@ -399,7 +404,9 @@ function collectSubFields( const visitedFragmentNames = Object.create(null); fieldNodes.forEach(fieldNode => { - subFieldNodes = collectFields(exeContext, type, fieldNode.selectionSet, subFieldNodes, visitedFragmentNames); + if (fieldNode.selectionSet) { + subFieldNodes = collectFields(exeContext, type, fieldNode.selectionSet, subFieldNodes, visitedFragmentNames); + } }); return subFieldNodes; diff --git a/packages/utils/src/visitSchema.ts b/packages/utils/src/visitSchema.ts index 72d16aa6a28..7878992e8e7 100644 --- a/packages/utils/src/visitSchema.ts +++ b/packages/utils/src/visitSchema.ts @@ -28,6 +28,7 @@ import { import { healSchema } from './heal'; import { SchemaVisitor } from './SchemaVisitor'; +import { isSome } from './helpers'; function isSchemaVisitor(obj: any): obj is SchemaVisitor { if ('schema' in obj && isSchema(obj.schema)) { @@ -162,12 +163,14 @@ export function visitSchema( if (newInputObject != null) { const fieldMap = newInputObject.getFields() as Record; for (const key of Object.keys(fieldMap)) { - fieldMap[key] = callMethod('visitInputFieldDefinition', fieldMap[key], { + const result = callMethod('visitInputFieldDefinition', fieldMap[key], { // Since we call a different method for input object fields, we // can't reuse the visitFields function here. objectType: newInputObject, }); - if (!fieldMap[key]) { + if (result) { + fieldMap[key] = result; + } else { delete fieldMap[key]; } } @@ -195,10 +198,10 @@ export function visitSchema( enumType: newEnum, }) ) - .filter(Boolean); + .filter(isSome); // Recreate the enum type if any of the values changed - const valuesUpdated = newValues.some((value, index) => value !== newEnum.getValues()[index]); + const valuesUpdated = newValues.some((value, index) => value !== newEnum!.getValues()[index]); if (valuesUpdated) { newEnum = new GraphQLEnumType({ ...(newEnum as GraphQLEnumType).toConfig(), @@ -221,7 +224,7 @@ export function visitSchema( return newEnum; } - throw new Error(`Unexpected schema type: ${(type as unknown) as string}`); + throw new Error(`Unexpected schema type: ${type as unknown as string}`); } function visitFields(type: GraphQLObjectType | GraphQLInterfaceType) { @@ -254,7 +257,7 @@ export function visitSchema( objectType: type, }) ) - .filter(Boolean); + .filter(isSome); } // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition @@ -309,7 +312,7 @@ function getVisitor(visitorDef: SchemaVisitorMap, specifiers: Array 0) { - const next = stack.pop(); + const next = stack.pop()!; typeVisitor = visitorDef[next] as NamedTypeVisitor; } diff --git a/packages/utils/tests/directives.test.ts b/packages/utils/tests/directives.test.ts index 2b3094dbe50..7ade95d4bcc 100644 --- a/packages/utils/tests/directives.test.ts +++ b/packages/utils/tests/directives.test.ts @@ -13,7 +13,6 @@ import { GraphQLScalarType, GraphQLSchema, GraphQLString, - StringValueNode, defaultFieldResolver, graphql, GraphQLNonNull, @@ -40,6 +39,7 @@ import { ExecutionResult, astFromDirective, } from '@graphql-tools/utils'; +import { assertGraphQLEnumType, assertGraphQLInputObjectType, assertGraphQLInterfaceType, assertGraphQLObjectType, assertGraphQLScalerType, assertGraphQLUnionType } from '../../testing/assertion'; const typeDefs = ` directive @schemaDirective(role: String) on SCHEMA @@ -147,14 +147,14 @@ describe('@directives', () => { } function getDirectiveNames(type: VisitableSchemaType): Array { - let directives = type.astNode.directives.map((d) => d.name.value); + let directives = (type.astNode?.directives ?? []).map((d) => d.name.value); const extensionASTNodes = (type as { extensionASTNodes?: Array; }).extensionASTNodes; if (extensionASTNodes != null) { extensionASTNodes.forEach((extensionASTNode) => { directives = directives.concat( - extensionASTNode.directives.map((d) => d.name.value), + (extensionASTNode.directives ?? []).map((d) => d.name.value), ); }); } @@ -166,15 +166,19 @@ describe('@directives', () => { 'schemaExtensionDirective', ]); + const queryType = schema.getQueryType() + assertGraphQLObjectType(queryType) checkDirectives( - schema.getQueryType(), + queryType, ['queryTypeDirective', 'queryTypeExtensionDirective'], { people: ['queryFieldDirective'], }, ); - expect(getDirectiveNames(schema.getType('Gender'))).toEqual([ + const GenderType = schema.getType('Gender') + assertGraphQLEnumType(GenderType) + expect(getDirectiveNames(GenderType)).toEqual([ 'enumTypeDirective', 'enumTypeExtensionDirective', ]); @@ -184,41 +188,50 @@ describe('@directives', () => { ) as GraphQLEnumType).getValues()[0]; expect(getDirectiveNames(nonBinary)).toEqual(['enumValueDirective']); - checkDirectives(schema.getType('Date') as GraphQLObjectType, [ + const DateType = schema.getType('Date') + assertGraphQLScalerType(DateType) + checkDirectives(DateType, [ 'dateDirective', 'dateExtensionDirective', ]); + const NamedType = schema.getType('Named') + assertGraphQLInterfaceType(NamedType) checkDirectives( - schema.getType('Named') as GraphQLObjectType, + NamedType, ['interfaceDirective', 'interfaceExtensionDirective'], { name: ['interfaceFieldDirective'], }, ); + const PersonInput = schema.getType('PersonInput') + assertGraphQLInputObjectType(PersonInput) checkDirectives( - schema.getType('PersonInput') as GraphQLObjectType, + PersonInput, ['inputTypeDirective', 'inputTypeExtensionDirective'], { name: ['inputFieldDirective'], gender: [], }, ); - + const MutationType = schema.getMutationType() + assertGraphQLObjectType(MutationType) checkDirectives( - schema.getMutationType(), + MutationType, ['mutationTypeDirective', 'mutationTypeExtensionDirective'], { addPerson: ['mutationMethodDirective'], }, ); expect( - getDirectiveNames(schema.getMutationType().getFields().addPerson.args[0]), + getDirectiveNames(MutationType.getFields().addPerson.args[0]), ).toEqual(['mutationArgumentDirective']); + const PersonType = schema.getType('Person') + assertGraphQLObjectType(PersonType) checkDirectives( - schema.getType('Person'), + PersonType, ['objectTypeDirective', 'objectTypeExtensionDirective'], { id: ['objectFieldDirective'], @@ -226,7 +239,9 @@ describe('@directives', () => { }, ); - checkDirectives(schema.getType('WhateverUnion'), [ + const WhateverUnionType = schema.getType('WhateverUnion') + assertGraphQLUnionType(WhateverUnionType) + checkDirectives(WhateverUnionType, [ 'unionDirective', 'unionExtensionDirective', ]); @@ -552,7 +567,7 @@ describe('@directives', () => { ) { expect(theSchema).toBe(schema); const prev = schema.getDirective(name); - prev.args.some((arg) => { + prev?.args.some((arg) => { if (arg.name === 'times') { // Override the default value of the times argument to be 3 // instead of 5. @@ -793,7 +808,7 @@ describe('@directives', () => { }, ) { const { resolve = defaultFieldResolver } = field; - field.resolve = async function (...args: Array) { + field.resolve = async function (...args: Parameters) { const defaultText = await resolve.apply(this, args); // In this example, path would be ["Query", "greeting"]: const path = [details.objectType.name, field.name]; @@ -871,7 +886,7 @@ describe('@directives', () => { Object.keys(fields).forEach((fieldName) => { const field = fields[fieldName]; const { resolve = defaultFieldResolver } = field; - field.resolve = function (...args: Array) { + field.resolve = function (...args: Parameters) { // Get the required Role from the field first, falling back // to the objectType if no Role is required by the field: const requiredRole = @@ -957,6 +972,12 @@ describe('@directives', () => { ); } + function assertStringArray(input: Array): asserts input is Array { + if (input.some(item => typeof item !== "string")) { + throw new Error("All items in array should be strings.") + } + } + function checkErrors( expectedCount: number, ...expectedNames: Array @@ -964,15 +985,13 @@ describe('@directives', () => { return function ({ errors = [], data, - }: { - errors: Array; - data: any; - }) { + }: ExecutionResult) { expect(errors.length).toBe(expectedCount); expect( errors.every((error) => error.message === 'not authorized'), ).toBeTruthy(); - const actualNames = errors.map((error) => error.path.slice(-1)[0]); + const actualNames = errors.map((error) => error.path!.slice(-1)[0]); + assertStringArray(actualNames) expect(expectedNames.sort((a, b) => a.localeCompare(b))).toEqual( actualNames.sort((a, b) => a.localeCompare(b)), ); @@ -987,10 +1006,10 @@ describe('@directives', () => { execWithRole('ADMIN') .then(checkErrors(0)) .then((data) => { - expect(data.users.length).toBe(1); - expect(data.users[0].banned).toBe(true); - expect(data.users[0].canPost).toBe(false); - expect(data.users[0].name).toBe('Ben'); + expect(data?.users.length).toBe(1); + expect(data?.users[0].banned).toBe(true); + expect(data?.users[0].canPost).toBe(false); + expect(data?.users[0].name).toBe('Ben'); }), ]); }); @@ -1018,7 +1037,7 @@ describe('@directives', () => { return type.parseValue(value); }, - parseLiteral(ast: StringValueNode) { + parseLiteral(ast) { return type.parseLiteral(ast, {}); }, }); @@ -1097,8 +1116,8 @@ describe('@directives', () => { } `, ); - expect(errors.length).toBe(1); - expect(errors[0].message).toBe('expected 26 to be at most 10'); + expect(errors?.length).toBe(1); + expect(errors?.[0].message).toBe('expected 26 to be at most 10'); const result = await graphql( schema, @@ -1207,7 +1226,7 @@ describe('@directives', () => { ).then((result) => { const { data } = result; - expect(data.people).toEqual([ + expect(data?.people).toEqual([ { uid: '580a207c8e94f03b93a2b01217c3cc218490571a', personID: 1, @@ -1215,7 +1234,7 @@ describe('@directives', () => { }, ]); - expect(data.locations).toEqual([ + expect(data?.locations).toEqual([ { uid: 'c31b71e6e23a7ae527f94341da333590dd7cba96', locationID: 1, @@ -1284,7 +1303,7 @@ describe('@directives', () => { schemaDirectives: { remove: class extends SchemaDirectiveVisitor { - public visitEnumValue(): null { + public visitEnumValue(): any { if (this.args.if) { return null; } @@ -1418,7 +1437,7 @@ describe('@directives', () => { const { resolve = defaultFieldResolver } = field; const newField = { ...field }; - newField.resolve = async function (...args: Array) { + newField.resolve = async function (...args: Parameters) { const result = await resolve.apply(this, args); if (typeof result === 'string') { return result.toUpperCase(); @@ -1432,7 +1451,7 @@ describe('@directives', () => { reverse: class extends SchemaDirectiveVisitor { public visitFieldDefinition(field: GraphQLField) { const { resolve = defaultFieldResolver } = field; - field.resolve = async function (...args: Array) { + field.resolve = async function (...args: Parameters) { const result = await resolve.apply(this, args); if (typeof result === 'string') { return result.split('').reverse().join(''); @@ -1470,8 +1489,8 @@ describe('@directives', () => { min = null, message = null, } : { - min: number, - message: string, + min: null | number, + message: null | string, }) { if(min && value.length < min) { throw new Error(message || `Please ensure the value is at least ${min} characters.`); @@ -1558,7 +1577,7 @@ describe('@directives', () => { } `, ).then(({ errors }) => { - expect(errors[0].originalError).toEqual(new Error('Author input error')); + expect(errors?.[0].originalError).toEqual(new Error('Author input error')); }); }); it('should print a directive correctly from GraphQLDirective object using astFromDirective and print', () => { diff --git a/packages/utils/tests/filterSchema.test.ts b/packages/utils/tests/filterSchema.test.ts index 8745a0dd61d..54c8781efd2 100644 --- a/packages/utils/tests/filterSchema.test.ts +++ b/packages/utils/tests/filterSchema.test.ts @@ -19,7 +19,7 @@ describe('filterSchema', () => { const filtered = filterSchema({ schema, - rootFieldFilter: (_opName, fieldName) => fieldName.startsWith('keep'), + rootFieldFilter: (_opName, fieldName) => fieldName?.startsWith('keep') ?? false, }); expect((filtered.getType('Query') as GraphQLObjectType).getFields()['keep']).toBeDefined(); @@ -97,7 +97,7 @@ describe('filterSchema', () => { const filtered = filterSchema({ schema, - objectFieldFilter: (_typeName, fieldName) => fieldName.startsWith('keep'), + objectFieldFilter: (_typeName, fieldName) => fieldName?.startsWith('keep') ?? false, }); expect((filtered.getType('Thing') as GraphQLObjectType).getFields()['keep']).toBeDefined(); @@ -120,7 +120,7 @@ describe('filterSchema', () => { const filtered = filterSchema({ schema, - interfaceFieldFilter: (_typeName, fieldName) => fieldName.startsWith('keep'), + interfaceFieldFilter: (_typeName, fieldName) => fieldName?.startsWith('keep') ?? false, }); expect((filtered.getType('IThing') as GraphQLInterfaceType).getFields()['keep']).toBeDefined(); @@ -143,7 +143,7 @@ describe('filterSchema', () => { const filtered = filterSchema({ schema, - inputObjectFieldFilter: (_typeName, fieldName) => fieldName.startsWith('keep'), + inputObjectFieldFilter: (_typeName, fieldName) => fieldName?.startsWith('keep') ?? false, }); expect((filtered.getType('ThingInput') as GraphQLInputObjectType).getFields()['keep']).toBeDefined(); @@ -171,7 +171,7 @@ describe('filterSchema', () => { const filtered = filterSchema({ schema, - fieldFilter: (_typeName, fieldName) => fieldName.startsWith('keep'), + fieldFilter: (_typeName, fieldName) => fieldName?.startsWith('keep') ?? false, }); expect((filtered.getType('Thing') as GraphQLObjectType).getFields()['keep']).toBeDefined(); @@ -199,7 +199,7 @@ describe('filterSchema', () => { const filtered = filterSchema({ schema, - argumentFilter: (_typeName, _fieldName, argName) => argName.startsWith('keep'), + argumentFilter: (_typeName, _fieldName, argName) => argName?.startsWith('keep') ?? false, }); expect((filtered.getType('Query') as GraphQLObjectType).getFields()['field'].args.map(arg => arg.name)).toEqual(['keep']); diff --git a/packages/utils/tests/get-directives.spec.ts b/packages/utils/tests/get-directives.spec.ts index 83facfd4238..d1ef5d501e6 100644 --- a/packages/utils/tests/get-directives.spec.ts +++ b/packages/utils/tests/get-directives.spec.ts @@ -1,6 +1,6 @@ -import { GraphQLSchema } from 'graphql'; import { makeExecutableSchema } from '@graphql-tools/schema'; import { getDirectives } from '../src'; +import { assertGraphQLObjectType } from '../../testing/assertion'; describe('getDirectives', () => { it('should return the correct directives map when no directives specified', () => { @@ -9,8 +9,10 @@ describe('getDirectives', () => { test: String } `; - const schema = makeExecutableSchema({ typeDefs, resolvers: {}, allowUndefinedInResolve: true }) as GraphQLSchema; - const directivesMap = getDirectives(schema, schema.getQueryType()); + const schema = makeExecutableSchema({ typeDefs, resolvers: {}, allowUndefinedInResolve: true }); + const QueryType = schema.getQueryType() + assertGraphQLObjectType(QueryType) + const directivesMap = getDirectives(schema, QueryType); expect(directivesMap).toEqual({}); }); @@ -22,8 +24,10 @@ describe('getDirectives', () => { } `; - const schema = makeExecutableSchema({ typeDefs, resolvers: {}, allowUndefinedInResolve: true }) as GraphQLSchema; - const directivesMap = getDirectives(schema, schema.getQueryType().getFields().test); + const schema = makeExecutableSchema({ typeDefs, resolvers: {}, allowUndefinedInResolve: true }); + const QueryType = schema.getQueryType() + assertGraphQLObjectType(QueryType) + const directivesMap = getDirectives(schema, QueryType.getFields().test); expect(directivesMap).toEqual({ deprecated: { reason: 'No longer supported', @@ -40,8 +44,10 @@ describe('getDirectives', () => { directive @mydir on FIELD_DEFINITION `; - const schema = makeExecutableSchema({ typeDefs, resolvers: {}, allowUndefinedInResolve: true }) as GraphQLSchema; - const directivesMap = getDirectives(schema, schema.getQueryType().getFields().test); + const schema = makeExecutableSchema({ typeDefs, resolvers: {}, allowUndefinedInResolve: true }); + const QueryType = schema.getQueryType() + assertGraphQLObjectType(QueryType) + const directivesMap = getDirectives(schema, QueryType.getFields().test); expect(directivesMap).toEqual({ mydir: {}, }); @@ -56,8 +62,10 @@ describe('getDirectives', () => { directive @mydir(f1: String) on FIELD_DEFINITION `; - const schema = makeExecutableSchema({ typeDefs, resolvers: {}, allowUndefinedInResolve: true }) as GraphQLSchema; - const directivesMap = getDirectives(schema, schema.getQueryType().getFields().test); + const schema = makeExecutableSchema({ typeDefs, resolvers: {}, allowUndefinedInResolve: true }) + const QueryType = schema.getQueryType() + assertGraphQLObjectType(QueryType) + const directivesMap = getDirectives(schema, QueryType.getFields().test); expect(directivesMap).toEqual({ mydir: { f1: 'test', @@ -74,8 +82,10 @@ describe('getDirectives', () => { directive @mydir(f1: String) on FIELD_DEFINITION `; - const schema = makeExecutableSchema({ typeDefs, resolvers: {}, allowUndefinedInResolve: true }) as GraphQLSchema; - const directivesMap = getDirectives(schema, schema.getQueryType().getFields().test); + const schema = makeExecutableSchema({ typeDefs, resolvers: {}, allowUndefinedInResolve: true }) + const QueryType = schema.getQueryType() + assertGraphQLObjectType(QueryType) + const directivesMap = getDirectives(schema, QueryType.getFields().test); expect(directivesMap).toEqual({ mydir: {}, }); @@ -93,8 +103,9 @@ describe('getDirectives', () => { } ` }); - - expect(getDirectives(schema, schema.getQueryType())).toEqual({ mydir: { arg: 'ext1' } }); + const QueryType = schema.getQueryType() + assertGraphQLObjectType(QueryType) + expect(getDirectives(schema,QueryType)).toEqual({ mydir: { arg: 'ext1' } }); }); it('builds proper repeatable directives listing', () => { @@ -106,8 +117,9 @@ describe('getDirectives', () => { } ` }); - - expect(getDirectives(schema, schema.getQueryType())).toEqual({ + const QueryType = schema.getQueryType() + assertGraphQLObjectType(QueryType) + expect(getDirectives(schema, QueryType)).toEqual({ mydir: [{ arg: "first" }, { arg: "second" }] }); }); diff --git a/packages/utils/tests/mapSchema.test.ts b/packages/utils/tests/mapSchema.test.ts index 982bc5479cc..6054485db3c 100644 --- a/packages/utils/tests/mapSchema.test.ts +++ b/packages/utils/tests/mapSchema.test.ts @@ -37,7 +37,7 @@ describe('mapSchema', () => { expect(newSchema).toBeInstanceOf(GraphQLSchema); const result = graphqlSync(newSchema, '{ version }'); - expect(result.data.version).toBe(1); + expect(result.data?.version).toBe(1); }); test('can change the root query name', () => { @@ -58,6 +58,6 @@ describe('mapSchema', () => { }); expect(newSchema).toBeInstanceOf(GraphQLSchema); - expect(newSchema.getQueryType().name).toBe('RootQuery'); + expect(newSchema.getQueryType()?.name).toBe('RootQuery'); }); }); diff --git a/packages/utils/tests/parse-graphql-sdl.spec.ts b/packages/utils/tests/parse-graphql-sdl.spec.ts index a00669b19ac..ecbe6526984 100644 --- a/packages/utils/tests/parse-graphql-sdl.spec.ts +++ b/packages/utils/tests/parse-graphql-sdl.spec.ts @@ -76,9 +76,9 @@ describe('parse sdl', () => { it('should transform comments to descriptions correctly on all available nodes with noLocation=true', () => { const transformed = parseGraphQLSDL('test.graphql', ast, { noLocation: true, commentDescriptions: true }); - const type = transformed.document.definitions.find(d => (d as any)?.name?.value === 'Type'); - expect((type as ObjectTypeDefinitionNode).description.value).toBe('test type comment'); - expect((type as ObjectTypeDefinitionNode).loc).not.toBeDefined(); + const type = transformed.document.definitions.find((d): d is ObjectTypeDefinitionNode => "name" in d && d.name?.value === 'Type'); + expect(type?.description?.value).toBe('test type comment'); + expect(type?.loc).not.toBeDefined(); const printed = print(transformed.document); expect(printed).toMatchSnapshot(); }); diff --git a/packages/utils/tests/schemaTransforms.test.ts b/packages/utils/tests/schemaTransforms.test.ts index 9bf74b72774..c0b18263499 100644 --- a/packages/utils/tests/schemaTransforms.test.ts +++ b/packages/utils/tests/schemaTransforms.test.ts @@ -8,7 +8,6 @@ import { graphql, GraphQLString, GraphQLScalarType, - StringValueNode, GraphQLInputFieldConfig, GraphQLFieldConfig, isNonNullType, @@ -125,7 +124,7 @@ describe('@directives', () => { const directives = getDirectives(schema, type); Object.keys(directives).forEach(directiveName => { if (directiveNames.includes(directiveName)) { - expect(type.name).toBe(schema.getQueryType().name); + expect(type.name).toBe(schema.getQueryType()?.name); visited.add(type); } }); @@ -345,6 +344,10 @@ describe('@directives', () => { const { resolve = defaultFieldResolver } = fieldConfig; const { defaultFormat } = directiveArgumentMap; + if (!fieldConfig.args) { + throw new Error("Unexpected Error. args should be defined.") + } + fieldConfig.args['format'] = { type: GraphQLString, }; @@ -515,6 +518,12 @@ describe('@directives', () => { ); } + function assertStringArray(input: Array): asserts input is Array { + if (input.some(item => typeof item !== "string")) { + throw new Error("All items in array should be strings.") + } + } + function checkErrors( expectedCount: number, ...expectedNames: Array @@ -522,15 +531,13 @@ describe('@directives', () => { return function ({ errors = [], data, - }: { - errors: Array; - data: any; - }) { + }: ExecutionResult) { expect(errors.length).toBe(expectedCount); expect( errors.every((error) => error.message === 'not authorized'), ).toBeTruthy(); - const actualNames = errors.map((error) => error.path.slice(-1)[0]); + const actualNames = errors.map((error) => error.path!.slice(-1)[0]); + assertStringArray(actualNames) expect(expectedNames.sort((a, b) => a.localeCompare(b))).toEqual( actualNames.sort((a, b) => a.localeCompare(b)), ); @@ -545,10 +552,10 @@ describe('@directives', () => { execWithRole('ADMIN') .then(checkErrors(0)) .then((data) => { - expect(data.users.length).toBe(1); - expect(data.users[0].banned).toBe(true); - expect(data.users[0].canPost).toBe(false); - expect(data.users[0].name).toBe('Ben'); + expect(data?.users.length).toBe(1); + expect(data?.users[0].banned).toBe(true); + expect(data?.users[0].canPost).toBe(false); + expect(data?.users[0].name).toBe('Ben'); }), ]); }); @@ -577,7 +584,7 @@ describe('@directives', () => { return type.parseValue(value); }, - parseLiteral(ast: StringValueNode) { + parseLiteral(ast) { return type.parseLiteral(ast, {}); }, }); @@ -679,8 +686,8 @@ describe('@directives', () => { } `, ); - expect(errors.length).toBe(1); - expect(errors[0].message).toBe('expected 26 to be at most 10'); + expect(errors?.length).toBe(1); + expect(errors?.[0].message).toBe('expected 26 to be at most 10'); const result = await graphql( schema, @@ -798,7 +805,7 @@ describe('@directives', () => { ).then((result) => { const { data } = result; - expect(data.people).toEqual([ + expect(data?.people).toEqual([ { uid: '580a207c8e94f03b93a2b01217c3cc218490571a', personID: 1, @@ -806,7 +813,7 @@ describe('@directives', () => { }, ]); - expect(data.locations).toEqual([ + expect(data?.locations).toEqual([ { uid: 'c31b71e6e23a7ae527f94341da333590dd7cba96', locationID: 1, @@ -1038,7 +1045,7 @@ describe('@directives', () => { const directives = getDirectives(schema, type); const directiveArgumentMap = directives[directiveName]; if (directiveArgumentMap) { - expect(type.name).toBe(schema.getQueryType().name); + expect(type.name).toBe(schema.getQueryType()?.name); visited.add(type); } return undefined; diff --git a/packages/utils/tests/visitResult.test.ts b/packages/utils/tests/visitResult.test.ts index ae6817a4f5d..e67ca1a38fd 100644 --- a/packages/utils/tests/visitResult.test.ts +++ b/packages/utils/tests/visitResult.test.ts @@ -389,7 +389,7 @@ describe('visiting errors', () => { const visitedResult = visitResult(result, request, schema, undefined, { Query: { test: (error, pathIndex) => { - const oldPath = error.path; + const oldPath = error.path ?? []; const newPath = [...oldPath.slice(0, pathIndex), 'inserted', ...oldPath.slice(pathIndex)]; return relocatedError(error, newPath); }, diff --git a/packages/webpack-loader/package.json b/packages/webpack-loader/package.json index 06c8ae78513..e08f6187e84 100644 --- a/packages/webpack-loader/package.json +++ b/packages/webpack-loader/package.json @@ -36,6 +36,9 @@ "@graphql-tools/webpack-loader-runtime": "^6.2.4", "tslib": "~2.3.0" }, + "devDependencies": { + "@types/webpack": "5.28.0" + }, "publishConfig": { "access": "public", "directory": "dist" diff --git a/packages/webpack-loader/src/index.ts b/packages/webpack-loader/src/index.ts index 40683ad93d0..1bc6fc5e103 100644 --- a/packages/webpack-loader/src/index.ts +++ b/packages/webpack-loader/src/index.ts @@ -3,6 +3,7 @@ import { isExecutableDefinitionNode, Kind, DocumentNode } from 'graphql'; import { uniqueCode } from '@graphql-tools/webpack-loader-runtime'; import { parseDocument } from './parser'; import { optimizeDocumentNode, removeDescriptions, removeEmptyNodes } from '@graphql-tools/optimize'; +import type { LoaderContext } from 'webpack'; function isSDL(doc: DocumentNode) { return !doc.definitions.some(def => isExecutableDefinitionNode(def)); @@ -41,9 +42,10 @@ function expandImports(source: string, options: Options) { return outputCode; } -export default function graphqlLoader(this: { query: Options; cacheable: VoidFunction }, source: string) { +export default function graphqlLoader(this: LoaderContext, source: string) { this.cacheable(); - const options: Options = this.query || {}; + // TODO: This should probably use this.getOptions() + const options = (this.query as Options) || {}; let doc = parseDocument(source); const optimizers = []; @@ -62,9 +64,8 @@ export default function graphqlLoader(this: { query: Options; cacheable: VoidFun let stringifiedDoc = JSON.stringify(doc); if (options.replaceKinds) { - Object.keys(Kind).forEach((identifier: keyof typeof Kind) => { - const value = Kind[identifier]; - + Object.keys(Kind).forEach(identifier => { + const value = Kind[identifier as keyof typeof Kind]; stringifiedDoc = stringifiedDoc.replace(new RegExp(`"kind":"${value}"`, 'g'), `"kind": Kind.${identifier}`); }); } diff --git a/packages/webpack-loader/tests/loader.test.ts b/packages/webpack-loader/tests/loader.test.ts index eefd1c4f95a..1e719cf2c72 100644 --- a/packages/webpack-loader/tests/loader.test.ts +++ b/packages/webpack-loader/tests/loader.test.ts @@ -4,7 +4,7 @@ import {uniqueCode} from '@graphql-tools/webpack-loader-runtime'; import loader from '../src/index'; function useLoader(source: string, options: any): string { - return loader.call({cacheable() {}, query: options}, source) + return loader.call({cacheable() {}, query: options} as any, source) } test('basic query', () => { diff --git a/packages/wrap/src/generateProxyingResolvers.ts b/packages/wrap/src/generateProxyingResolvers.ts index c2e07c0d6f7..c71dccb2705 100644 --- a/packages/wrap/src/generateProxyingResolvers.ts +++ b/packages/wrap/src/generateProxyingResolvers.ts @@ -1,6 +1,6 @@ import { GraphQLFieldResolver, GraphQLObjectType, GraphQLResolveInfo, OperationTypeNode } from 'graphql'; -import { getResponseKeyFromInfo } from '@graphql-tools/utils'; +import { Maybe, getResponseKeyFromInfo } from '@graphql-tools/utils'; import { delegateToSchema, getSubschema, @@ -12,21 +12,22 @@ import { getUnpathedErrors, } from '@graphql-tools/delegate'; -export function generateProxyingResolvers( - subschemaConfig: SubschemaConfig +export function generateProxyingResolvers( + subschemaConfig: SubschemaConfig ): Record>> { const targetSchema = subschemaConfig.schema; const createProxyingResolver = subschemaConfig.createProxyingResolver ?? defaultCreateProxyingResolver; const transformedSchema = applySchemaTransforms(targetSchema, subschemaConfig); - const operationTypes: Record = { + const operationTypes: Record> = { query: targetSchema.getQueryType(), mutation: targetSchema.getMutationType(), subscription: targetSchema.getSubscriptionType(), }; const resolvers = {}; + // @ts-expect-error: Object.keys typings suck. Object.keys(operationTypes).forEach((operation: OperationTypeNode) => { const rootType = operationTypes[operation]; if (rootType != null) { @@ -62,10 +63,10 @@ export function generateProxyingResolvers( return resolvers; } -function createPossiblyNestedProxyingResolver( - subschemaConfig: SubschemaConfig, +function createPossiblyNestedProxyingResolver( + subschemaConfig: SubschemaConfig, proxyingResolver: GraphQLFieldResolver -): GraphQLFieldResolver { +): GraphQLFieldResolver { return (parent, args, context, info) => { if (parent != null) { const responseKey = getResponseKeyFromInfo(info); @@ -88,11 +89,11 @@ function createPossiblyNestedProxyingResolver( }; } -export function defaultCreateProxyingResolver({ +export function defaultCreateProxyingResolver({ subschemaConfig, operation, transformedSchema, -}: ICreateProxyingResolverOptions): GraphQLFieldResolver { +}: ICreateProxyingResolverOptions): GraphQLFieldResolver { return (_parent, _args, context, info) => delegateToSchema({ schema: subschemaConfig, diff --git a/packages/wrap/src/makeRemoteExecutableSchema.ts b/packages/wrap/src/makeRemoteExecutableSchema.ts index 0245e53ba6a..7fcf10bd59f 100644 --- a/packages/wrap/src/makeRemoteExecutableSchema.ts +++ b/packages/wrap/src/makeRemoteExecutableSchema.ts @@ -24,7 +24,7 @@ export function makeRemoteExecutableSchema({ export function defaultCreateRemoteResolver( executor: Executor, - subscriber: Subscriber + subscriber?: Subscriber | undefined ): GraphQLFieldResolver { return (_parent, _args, context, info) => delegateToSchema({ diff --git a/packages/wrap/src/transforms/FilterObjectFieldDirectives.ts b/packages/wrap/src/transforms/FilterObjectFieldDirectives.ts index 4d471964613..74e7ec64be9 100644 --- a/packages/wrap/src/transforms/FilterObjectFieldDirectives.ts +++ b/packages/wrap/src/transforms/FilterObjectFieldDirectives.ts @@ -20,13 +20,17 @@ export default class FilterObjectFieldDirectives implements Transform { ): GraphQLSchema { const transformer = new TransformObjectFields( (_typeName: string, _fieldName: string, fieldConfig: GraphQLFieldConfig) => { - const keepDirectives = fieldConfig.astNode.directives.filter(dir => { - const directiveDef = originalWrappingSchema.getDirective(dir.name.value); - const directiveValue = directiveDef ? getArgumentValues(directiveDef, dir) : undefined; - return this.filter(dir.name.value, directiveValue); - }); - - if (keepDirectives.length !== fieldConfig.astNode.directives.length) { + const keepDirectives = + fieldConfig.astNode?.directives?.filter(dir => { + const directiveDef = originalWrappingSchema.getDirective(dir.name.value); + const directiveValue = directiveDef ? getArgumentValues(directiveDef, dir) : undefined; + return this.filter(dir.name.value, directiveValue); + }) ?? []; + + if ( + fieldConfig.astNode?.directives != null && + keepDirectives.length !== fieldConfig.astNode.directives.length + ) { fieldConfig = { ...fieldConfig, astNode: { diff --git a/packages/wrap/src/transforms/FilterObjectFields.ts b/packages/wrap/src/transforms/FilterObjectFields.ts index d02dc59d956..ca652937ffe 100644 --- a/packages/wrap/src/transforms/FilterObjectFields.ts +++ b/packages/wrap/src/transforms/FilterObjectFields.ts @@ -1,6 +1,6 @@ import { GraphQLSchema, GraphQLFieldConfig } from 'graphql'; -import { FieldFilter } from '@graphql-tools/utils'; +import { ObjectFieldFilter } from '@graphql-tools/utils'; import { SubschemaConfig, Transform } from '@graphql-tools/delegate'; @@ -9,7 +9,7 @@ import TransformObjectFields from './TransformObjectFields'; export default class FilterObjectFields implements Transform { private readonly transformer: TransformObjectFields; - constructor(filter: FieldFilter) { + constructor(filter: ObjectFieldFilter) { this.transformer = new TransformObjectFields( (typeName: string, fieldName: string, fieldConfig: GraphQLFieldConfig) => filter(typeName, fieldName, fieldConfig) ? undefined : null diff --git a/packages/wrap/src/transforms/HoistField.ts b/packages/wrap/src/transforms/HoistField.ts index fedb4735ec8..b9463510028 100644 --- a/packages/wrap/src/transforms/HoistField.ts +++ b/packages/wrap/src/transforms/HoistField.ts @@ -9,7 +9,14 @@ import { GraphQLFieldResolver, } from 'graphql'; -import { appendObjectFields, removeObjectFields, Request, ExecutionResult, relocatedError } from '@graphql-tools/utils'; +import { + appendObjectFields, + removeObjectFields, + Request, + ExecutionResult, + relocatedError, + assertSome, +} from '@graphql-tools/utils'; import { Transform, defaultMergedResolver, DelegationContext, SubschemaConfig } from '@graphql-tools/delegate'; @@ -24,7 +31,7 @@ export default class HoistField implements Transform { private readonly oldFieldName: string; private readonly argFilters: Array<(arg: GraphQLArgument) => boolean>; private readonly argLevels: Record; - private readonly transformer: Transform; + private readonly transformer: MapFields; constructor( typeName: string, @@ -45,6 +52,7 @@ export default class HoistField implements Transform { const pathToField = path.slice(); const oldFieldName = pathToField.pop(); + assertSome(oldFieldName); this.oldFieldName = oldFieldName; this.pathToField = pathToField; @@ -59,7 +67,7 @@ export default class HoistField implements Transform { { [typeName]: value => unwrapValue(value, alias), }, - errors => unwrapErrors(errors, alias) + errors => (errors != null ? unwrapErrors(errors, alias) : undefined) ); this.argLevels = argLevels; } @@ -97,7 +105,7 @@ export default class HoistField implements Transform { if (hoistingToRootField) { const targetSchema = subschemaConfig.schema; - const operation = this.typeName === targetSchema.getQueryType().name ? 'query' : 'mutation'; + const operation = this.typeName === targetSchema.getQueryType()?.name ? 'query' : 'mutation'; const createProxyingResolver = subschemaConfig.createProxyingResolver ?? defaultCreateProxyingResolver; resolve = createProxyingResolver({ subschemaConfig, @@ -112,26 +120,32 @@ export default class HoistField implements Transform { const newTargetField = { ...targetField, - resolve, + resolve: resolve!, }; const level = this.pathToField.length; - Object.keys(targetField.args).forEach(argName => { - const argConfig = targetField.args[argName]; - const arg = { - ...argConfig, - name: argName, - description: argConfig.description, - defaultValue: argConfig.defaultValue, - extensions: argConfig.extensions, - astNode: argConfig.astNode, - } as GraphQLArgument; - if (this.argFilters[level](arg)) { - argsMap[argName] = arg; - this.argLevels[arg.name] = level; + const args = targetField.args; + if (args != null) { + for (const argName in args) { + const argConfig = args[argName]; + if (argConfig == null) { + continue; + } + const arg = { + ...argConfig, + name: argName, + description: argConfig.description, + defaultValue: argConfig.defaultValue, + extensions: argConfig.extensions, + astNode: argConfig.astNode, + } as GraphQLArgument; + if (this.argFilters[level](arg)) { + argsMap[argName] = arg; + this.argLevels[arg.name] = level; + } } - }); + } newTargetField.args = argsMap; @@ -180,11 +194,17 @@ export function wrapFieldNode( kind: Kind.SELECTION_SET, selections: [acc], }, - arguments: fieldNode.arguments.filter(arg => argLevels[arg.name.value] === index), + arguments: + fieldNode.arguments != null + ? fieldNode.arguments.filter(arg => argLevels[arg.name.value] === index) + : undefined, }), { ...fieldNode, - arguments: fieldNode.arguments.filter(arg => argLevels[arg.name.value] === path.length), + arguments: + fieldNode.arguments != null + ? fieldNode.arguments.filter(arg => argLevels[arg.name.value] === path.length) + : undefined, } ); } @@ -218,7 +238,7 @@ export function unwrapValue(originalValue: any, alias: string): any { return originalValue; } -function unwrapErrors(errors: ReadonlyArray, alias: string): Array { +function unwrapErrors(errors: ReadonlyArray | undefined, alias: string): Array | undefined { if (errors === undefined) { return undefined; } diff --git a/packages/wrap/src/transforms/MapFields.ts b/packages/wrap/src/transforms/MapFields.ts index 638ec88eef9..d63af39f41e 100644 --- a/packages/wrap/src/transforms/MapFields.ts +++ b/packages/wrap/src/transforms/MapFields.ts @@ -1,6 +1,6 @@ import { GraphQLSchema } from 'graphql'; -import { Request, FieldNodeMappers, ExecutionResult } from '@graphql-tools/utils'; +import { Request, FieldNodeMappers, ExecutionResult, assertSome } from '@graphql-tools/utils'; import { Transform, DelegationContext, SubschemaConfig } from '@graphql-tools/delegate'; @@ -8,11 +8,11 @@ import { ObjectValueTransformerMap, ErrorsTransformer } from '../types'; import TransformCompositeFields from './TransformCompositeFields'; -export default class MapFields implements Transform { +export default class MapFields implements Transform { private fieldNodeTransformerMap: FieldNodeMappers; private objectValueTransformerMap?: ObjectValueTransformerMap; private errorsTransformer?: ErrorsTransformer; - private transformer: TransformCompositeFields; + private transformer: TransformCompositeFields | undefined; constructor( fieldNodeTransformerMap: FieldNodeMappers, @@ -24,12 +24,18 @@ export default class MapFields implements Transform { this.errorsTransformer = errorsTransformer; } + private _getTransformer() { + assertSome(this.transformer); + return this.transformer; + } + public transformSchema( originalWrappingSchema: GraphQLSchema, - subschemaConfig: SubschemaConfig, + subschemaConfig: SubschemaConfig, transformedSchema?: GraphQLSchema ): GraphQLSchema { const subscriptionTypeName = originalWrappingSchema.getSubscriptionType()?.name; + const objectValueTransformerMap = this.objectValueTransformerMap; this.transformer = new TransformCompositeFields( () => undefined, (typeName, fieldName, fieldNode, fragments, transformationContext) => { @@ -45,7 +51,7 @@ export default class MapFields implements Transform { return fieldNodeTransformer(fieldNode, fragments, transformationContext); }, - this.objectValueTransformerMap != null + objectValueTransformerMap != null ? (data, transformationContext) => { if (data == null) { return data; @@ -60,7 +66,7 @@ export default class MapFields implements Transform { } } - const transformer = this.objectValueTransformerMap[typeName]; + const transformer = objectValueTransformerMap[typeName]; if (transformer == null) { return data; } @@ -78,7 +84,7 @@ export default class MapFields implements Transform { delegationContext: DelegationContext, transformationContext: Record ): Request { - return this.transformer.transformRequest(originalRequest, delegationContext, transformationContext); + return this._getTransformer().transformRequest(originalRequest, delegationContext, transformationContext); } public transformResult( @@ -86,6 +92,6 @@ export default class MapFields implements Transform { delegationContext: DelegationContext, transformationContext: Record ): ExecutionResult { - return this.transformer.transformResult(originalResult, delegationContext, transformationContext); + return this._getTransformer().transformResult(originalResult, delegationContext, transformationContext); } } diff --git a/packages/wrap/src/transforms/MapLeafValues.ts b/packages/wrap/src/transforms/MapLeafValues.ts index a9f4cd402e1..028736d6605 100644 --- a/packages/wrap/src/transforms/MapLeafValues.ts +++ b/packages/wrap/src/transforms/MapLeafValues.ts @@ -21,6 +21,7 @@ import { ResultVisitorMap, updateArgument, transformInputValue, + assertSome, } from '@graphql-tools/utils'; import { Transform, DelegationContext, SubschemaConfig } from '@graphql-tools/delegate'; @@ -35,8 +36,8 @@ export default class MapLeafValues implements Transform ): Array { return operations.map((operation: OperationDefinitionNode) => { - const variableDefinitionMap: Record = operation.variableDefinitions.reduce( + const variableDefinitionMap: Record = ( + operation.variableDefinitions ?? [] + ).reduce( (prev, def) => ({ ...prev, [def.variable.name.value]: def, @@ -122,7 +135,7 @@ export default class MapLeafValues implements Transform this.transformFieldNode(node, variableDefinitionMap, variableValues), }) ); @@ -138,8 +151,8 @@ export default class MapLeafValues implements Transform, variableValues: Record - ): FieldNode { - const targetField = this.typeInfo.getFieldDef(); + ): FieldNode | undefined { + const targetField = this._getTypeInfo().getFieldDef(); if (!targetField.name.startsWith('__')) { const argumentNodes = field.arguments; diff --git a/packages/wrap/src/transforms/RemoveObjectFieldsWithDeprecation.ts b/packages/wrap/src/transforms/RemoveObjectFieldsWithDeprecation.ts index 958f32f3911..2e25ebb043a 100644 --- a/packages/wrap/src/transforms/RemoveObjectFieldsWithDeprecation.ts +++ b/packages/wrap/src/transforms/RemoveObjectFieldsWithDeprecation.ts @@ -1,4 +1,4 @@ -import { GraphQLSchema, GraphQLFieldConfig } from 'graphql'; +import { GraphQLSchema } from 'graphql'; import { valueMatchesCriteria } from '@graphql-tools/utils'; @@ -10,14 +10,12 @@ export default class RemoveObjectFieldsWithDeprecation implements Transform { private readonly transformer: FilterObjectFields; constructor(reason: string | RegExp) { - this.transformer = new FilterObjectFields( - (_typeName: string, _fieldName: string, fieldConfig: GraphQLFieldConfig) => { - if (fieldConfig.deprecationReason) { - return !valueMatchesCriteria(fieldConfig.deprecationReason, reason); - } - return true; + this.transformer = new FilterObjectFields((_typeName, _fieldName, fieldConfig) => { + if (fieldConfig.deprecationReason) { + return !valueMatchesCriteria(fieldConfig.deprecationReason, reason); } - ); + return true; + }); } public transformSchema( diff --git a/packages/wrap/src/transforms/RemoveObjectFieldsWithDirective.ts b/packages/wrap/src/transforms/RemoveObjectFieldsWithDirective.ts index d44aee5b7c1..2633df6e282 100644 --- a/packages/wrap/src/transforms/RemoveObjectFieldsWithDirective.ts +++ b/packages/wrap/src/transforms/RemoveObjectFieldsWithDirective.ts @@ -1,4 +1,4 @@ -import { GraphQLSchema, GraphQLFieldConfig } from 'graphql'; +import { GraphQLSchema } from 'graphql'; import { getDirectives, valueMatchesCriteria } from '@graphql-tools/utils'; @@ -20,18 +20,16 @@ export default class RemoveObjectFieldsWithDirective implements Transform { subschemaConfig: SubschemaConfig, transformedSchema?: GraphQLSchema ): GraphQLSchema { - const transformer = new FilterObjectFields( - (_typeName: string, _fieldName: string, fieldConfig: GraphQLFieldConfig) => { - const valueMap = getDirectives(originalWrappingSchema, fieldConfig); - return !Object.keys(valueMap).some( - directiveName => - valueMatchesCriteria(directiveName, this.directiveName) && - ((Array.isArray(valueMap[directiveName]) && - valueMap[directiveName].some((value: any) => valueMatchesCriteria(value, this.args))) || - valueMatchesCriteria(valueMap[directiveName], this.args)) - ); - } - ); + const transformer = new FilterObjectFields((_typeName, _fieldName, fieldConfig) => { + const valueMap = getDirectives(originalWrappingSchema, fieldConfig); + return !Object.keys(valueMap).some( + directiveName => + valueMatchesCriteria(directiveName, this.directiveName) && + ((Array.isArray(valueMap[directiveName]) && + valueMap[directiveName].some((value: any) => valueMatchesCriteria(value, this.args))) || + valueMatchesCriteria(valueMap[directiveName], this.args)) + ); + }); return transformer.transformSchema(originalWrappingSchema, subschemaConfig, transformedSchema); } diff --git a/packages/wrap/src/transforms/RenameInputObjectFields.ts b/packages/wrap/src/transforms/RenameInputObjectFields.ts index 6e2d80ecbc8..a7084ea3a15 100644 --- a/packages/wrap/src/transforms/RenameInputObjectFields.ts +++ b/packages/wrap/src/transforms/RenameInputObjectFields.ts @@ -6,18 +6,27 @@ import { Transform, DelegationContext, SubschemaConfig } from '@graphql-tools/de import TransformInputObjectFields from './TransformInputObjectFields'; +type RenamerFunction = ( + typeName: string, + fieldName: string, + inputFieldConfig: GraphQLInputFieldConfig +) => string | undefined; + export default class RenameInputObjectFields implements Transform { - private readonly renamer: (typeName: string, fieldName: string, inputFieldConfig: GraphQLInputFieldConfig) => string; + private readonly renamer: RenamerFunction; private readonly transformer: TransformInputObjectFields; private reverseMap: Record>; - constructor(renamer: (typeName: string, fieldName: string, inputFieldConfig: GraphQLInputFieldConfig) => string) { + constructor(renamer: RenamerFunction) { this.renamer = renamer; this.transformer = new TransformInputObjectFields( - (typeName: string, inputFieldName: string, inputFieldConfig: GraphQLInputFieldConfig) => { + (typeName, inputFieldName, inputFieldConfig) => { const newName = renamer(typeName, inputFieldName, inputFieldConfig); if (newName !== undefined && newName !== inputFieldName) { - return [renamer(typeName, inputFieldName, inputFieldConfig), inputFieldConfig]; + const value = renamer(typeName, inputFieldName, inputFieldConfig); + if (value != null) { + return [value, inputFieldConfig]; + } } }, (typeName: string, inputFieldName: string, inputFieldNode: ObjectFieldNode) => { diff --git a/packages/wrap/src/transforms/TransformCompositeFields.ts b/packages/wrap/src/transforms/TransformCompositeFields.ts index 537b3e20d92..c8feffe87e2 100644 --- a/packages/wrap/src/transforms/TransformCompositeFields.ts +++ b/packages/wrap/src/transforms/TransformCompositeFields.ts @@ -11,21 +11,21 @@ import { FragmentDefinitionNode, } from 'graphql'; -import { Request, MapperKind, mapSchema, visitData, ExecutionResult } from '@graphql-tools/utils'; +import { Request, MapperKind, mapSchema, visitData, ExecutionResult, Maybe, assertSome } from '@graphql-tools/utils'; import { Transform, DelegationContext, SubschemaConfig } from '@graphql-tools/delegate'; import { FieldTransformer, FieldNodeTransformer, DataTransformer, ErrorsTransformer } from '../types'; -export default class TransformCompositeFields implements Transform { +export default class TransformCompositeFields> implements Transform { private readonly fieldTransformer: FieldTransformer; - private readonly fieldNodeTransformer: FieldNodeTransformer; - private readonly dataTransformer: DataTransformer; - private readonly errorsTransformer: ErrorsTransformer; - private transformedSchema: GraphQLSchema; - private typeInfo: TypeInfo; + private readonly fieldNodeTransformer: FieldNodeTransformer | undefined; + private readonly dataTransformer: DataTransformer | undefined; + private readonly errorsTransformer: ErrorsTransformer | undefined; + private transformedSchema: GraphQLSchema | undefined; + private typeInfo: TypeInfo | undefined; private mapping: Record>; - private subscriptionTypeName: string; + private subscriptionTypeName: string | undefined; constructor( fieldTransformer: FieldTransformer, @@ -40,9 +40,14 @@ export default class TransformCompositeFields implements Transform { this.mapping = {}; } + private _getTypeInfo() { + assertSome(this.typeInfo); + return this.typeInfo; + } + public transformSchema( originalWrappingSchema: GraphQLSchema, - _subschemaConfig: SubschemaConfig, + _subschemaConfig: SubschemaConfig, _transformedSchema?: GraphQLSchema ): GraphQLSchema { this.transformedSchema = mapSchema(originalWrappingSchema, { @@ -90,10 +95,11 @@ export default class TransformCompositeFields implements Transform { _delegationContext: DelegationContext, transformationContext: Record ): ExecutionResult { - if (this.dataTransformer != null) { - result.data = visitData(result.data, value => this.dataTransformer(value, transformationContext)); + const dataTransformer = this.dataTransformer; + if (dataTransformer != null) { + result.data = visitData(result.data, value => dataTransformer(value, transformationContext)); } - if (this.errorsTransformer != null) { + if (this.errorsTransformer != null && Array.isArray(result.errors)) { result.errors = this.errorsTransformer(result.errors, transformationContext); } return result; @@ -106,10 +112,10 @@ export default class TransformCompositeFields implements Transform { ): DocumentNode { return visit( document, - visitWithTypeInfo(this.typeInfo, { + visitWithTypeInfo(this._getTypeInfo(), { leave: { [Kind.SELECTION_SET]: node => - this.transformSelectionSet(node, this.typeInfo, fragments, transformationContext), + this.transformSelectionSet(node, this._getTypeInfo(), fragments, transformationContext), }, }) ); @@ -120,8 +126,8 @@ export default class TransformCompositeFields implements Transform { typeInfo: TypeInfo, fragments: Record, transformationContext: Record - ): SelectionSetNode { - const parentType: GraphQLType = typeInfo.getParentType(); + ): SelectionSetNode | undefined { + const parentType: Maybe = typeInfo.getParentType(); if (parentType == null) { return undefined; } @@ -151,7 +157,7 @@ export default class TransformCompositeFields implements Transform { }); } - let transformedSelection: SelectionNode | Array; + let transformedSelection: Maybe>; if (this.fieldNodeTransformer == null) { transformedSelection = selection; } else { diff --git a/packages/wrap/src/transforms/TransformEnumValues.ts b/packages/wrap/src/transforms/TransformEnumValues.ts index 009cf5e0d56..29a6fba90ce 100644 --- a/packages/wrap/src/transforms/TransformEnumValues.ts +++ b/packages/wrap/src/transforms/TransformEnumValues.ts @@ -1,6 +1,6 @@ import { GraphQLSchema, GraphQLEnumValueConfig, ExecutionResult } from 'graphql'; -import { Request, MapperKind, mapSchema } from '@graphql-tools/utils'; +import { Request, MapperKind, mapSchema, Maybe } from '@graphql-tools/utils'; import { Transform, DelegationContext, SubschemaConfig } from '@graphql-tools/delegate'; @@ -11,7 +11,7 @@ import MapLeafValues, { MapLeafValuesTransformationContext } from './MapLeafValu export default class TransformEnumValues implements Transform { private readonly enumValueTransformer: EnumValueTransformer; private readonly transformer: MapLeafValues; - private transformedSchema: GraphQLSchema; + private transformedSchema: GraphQLSchema | undefined; private mapping: Record>; private reverseMapping: Record>; @@ -62,7 +62,7 @@ export default class TransformEnumValues implements Transform { const transformedEnumValue = this.enumValueTransformer(typeName, externalValue, enumValueConfig); if (Array.isArray(transformedEnumValue)) { const newExternalValue = transformedEnumValue[0]; @@ -86,7 +86,7 @@ function mapEnumValues(typeName: string, value: string, mapping: Record, mapping: Record> ): LeafValueTransformer { if (valueTransformer == null) { diff --git a/packages/wrap/src/transforms/TransformInputObjectFields.ts b/packages/wrap/src/transforms/TransformInputObjectFields.ts index f823a511a33..9adeb7cda0d 100644 --- a/packages/wrap/src/transforms/TransformInputObjectFields.ts +++ b/packages/wrap/src/transforms/TransformInputObjectFields.ts @@ -1,6 +1,5 @@ import { GraphQLSchema, - GraphQLType, DocumentNode, typeFromAST, TypeInfo, @@ -16,7 +15,7 @@ import { NamedTypeNode, } from 'graphql'; -import { Request, MapperKind, mapSchema, transformInputValue } from '@graphql-tools/utils'; +import { Maybe, Request, MapperKind, mapSchema, transformInputValue, assertSome } from '@graphql-tools/utils'; import { Transform, DelegationContext, SubschemaConfig } from '@graphql-tools/delegate'; @@ -24,9 +23,9 @@ import { InputFieldTransformer, InputFieldNodeTransformer, InputObjectNodeTransf export default class TransformInputObjectFields implements Transform { private readonly inputFieldTransformer: InputFieldTransformer; - private readonly inputFieldNodeTransformer: InputFieldNodeTransformer; - private readonly inputObjectNodeTransformer: InputObjectNodeTransformer; - private transformedSchema: GraphQLSchema; + private readonly inputFieldNodeTransformer: InputFieldNodeTransformer | undefined; + private readonly inputObjectNodeTransformer: InputObjectNodeTransformer | undefined; + private transformedSchema: GraphQLSchema | undefined; private mapping: Record>; constructor( @@ -40,6 +39,11 @@ export default class TransformInputObjectFields implements Transform { this.mapping = {}; } + private _getTransformedSchema() { + assertSome(this.transformedSchema); + return this.transformedSchema; + } + public transformSchema( originalWrappingSchema: GraphQLSchema, _subschemaConfig: SubschemaConfig, @@ -141,18 +145,19 @@ export default class TransformInputObjectFields implements Transform { private transformDocument( document: DocumentNode, mapping: Record>, - inputFieldNodeTransformer: InputFieldNodeTransformer, - inputObjectNodeTransformer: InputObjectNodeTransformer, + inputFieldNodeTransformer: InputFieldNodeTransformer | undefined, + inputObjectNodeTransformer: InputObjectNodeTransformer | undefined, request: Request, delegationContext?: DelegationContext ): DocumentNode { - const typeInfo = new TypeInfo(this.transformedSchema); + const typeInfo = new TypeInfo(this._getTransformedSchema()); const newDocument: DocumentNode = visit( document, visitWithTypeInfo(typeInfo, { leave: { - [Kind.OBJECT]: (node: ObjectValueNode): ObjectValueNode => { - const parentType: GraphQLType = typeInfo.getInputType() as GraphQLInputObjectType; + [Kind.OBJECT]: (node: ObjectValueNode): ObjectValueNode | undefined => { + // The casting is kind of legit here as we are in a visitor + const parentType = typeInfo.getInputType() as Maybe; if (parentType != null) { const parentTypeName = parentType.name; const newInputFields: Array = []; diff --git a/packages/wrap/src/transforms/TransformInterfaceFields.ts b/packages/wrap/src/transforms/TransformInterfaceFields.ts index 956ac4e5673..9af6ef85754 100644 --- a/packages/wrap/src/transforms/TransformInterfaceFields.ts +++ b/packages/wrap/src/transforms/TransformInterfaceFields.ts @@ -1,6 +1,6 @@ import { GraphQLSchema, isInterfaceType, GraphQLFieldConfig } from 'graphql'; -import { Request, ExecutionResult } from '@graphql-tools/utils'; +import { Request, ExecutionResult, assertSome } from '@graphql-tools/utils'; import { Transform, DelegationContext, SubschemaConfig } from '@graphql-tools/delegate'; @@ -10,14 +10,19 @@ import TransformCompositeFields from './TransformCompositeFields'; export default class TransformInterfaceFields implements Transform { private readonly interfaceFieldTransformer: FieldTransformer; - private readonly fieldNodeTransformer: FieldNodeTransformer; - private transformer: TransformCompositeFields; + private readonly fieldNodeTransformer: FieldNodeTransformer | undefined; + private transformer: TransformCompositeFields | undefined; constructor(interfaceFieldTransformer: FieldTransformer, fieldNodeTransformer?: FieldNodeTransformer) { this.interfaceFieldTransformer = interfaceFieldTransformer; this.fieldNodeTransformer = fieldNodeTransformer; } + private _getTransformer() { + assertSome(this.transformer); + return this.transformer; + } + public transformSchema( originalWrappingSchema: GraphQLSchema, subschemaConfig: SubschemaConfig, @@ -45,7 +50,7 @@ export default class TransformInterfaceFields implements Transform { delegationContext: DelegationContext, transformationContext: Record ): Request { - return this.transformer.transformRequest(originalRequest, delegationContext, transformationContext); + return this._getTransformer().transformRequest(originalRequest, delegationContext, transformationContext); } public transformResult( @@ -53,6 +58,6 @@ export default class TransformInterfaceFields implements Transform { delegationContext: DelegationContext, transformationContext: Record ): ExecutionResult { - return this.transformer.transformResult(originalResult, delegationContext, transformationContext); + return this._getTransformer().transformResult(originalResult, delegationContext, transformationContext); } } diff --git a/packages/wrap/src/transforms/TransformObjectFields.ts b/packages/wrap/src/transforms/TransformObjectFields.ts index 7ced6e12f2c..ac88e6e7103 100644 --- a/packages/wrap/src/transforms/TransformObjectFields.ts +++ b/packages/wrap/src/transforms/TransformObjectFields.ts @@ -1,6 +1,6 @@ import { GraphQLSchema, isObjectType, GraphQLFieldConfig } from 'graphql'; -import { Request, ExecutionResult } from '@graphql-tools/utils'; +import { Request, ExecutionResult, assertSome } from '@graphql-tools/utils'; import { Transform, DelegationContext, SubschemaConfig } from '@graphql-tools/delegate'; @@ -10,14 +10,19 @@ import TransformCompositeFields from './TransformCompositeFields'; export default class TransformObjectFields implements Transform { private readonly objectFieldTransformer: FieldTransformer; - private readonly fieldNodeTransformer: FieldNodeTransformer; - private transformer: TransformCompositeFields; + private readonly fieldNodeTransformer: FieldNodeTransformer | undefined; + private transformer: TransformCompositeFields | undefined; constructor(objectFieldTransformer: FieldTransformer, fieldNodeTransformer?: FieldNodeTransformer) { this.objectFieldTransformer = objectFieldTransformer; this.fieldNodeTransformer = fieldNodeTransformer; } + private _getTransformer() { + assertSome(this.transformer); + return this.transformer; + } + public transformSchema( originalWrappingSchema: GraphQLSchema, subschemaConfig: SubschemaConfig, @@ -45,7 +50,7 @@ export default class TransformObjectFields implements Transform { delegationContext: DelegationContext, transformationContext: Record ): Request { - return this.transformer.transformRequest(originalRequest, delegationContext, transformationContext); + return this._getTransformer().transformRequest(originalRequest, delegationContext, transformationContext); } public transformResult( @@ -53,6 +58,6 @@ export default class TransformObjectFields implements Transform { delegationContext: DelegationContext, transformationContext: Record ): ExecutionResult { - return this.transformer.transformResult(originalResult, delegationContext, transformationContext); + return this._getTransformer().transformResult(originalResult, delegationContext, transformationContext); } } diff --git a/packages/wrap/src/transforms/TransformQuery.ts b/packages/wrap/src/transforms/TransformQuery.ts index 06221726df6..6a1690a4b53 100644 --- a/packages/wrap/src/transforms/TransformQuery.ts +++ b/packages/wrap/src/transforms/TransformQuery.ts @@ -8,10 +8,14 @@ export type QueryTransformer = ( selectionSet: SelectionSetNode, fragments: Record, delegationContext: DelegationContext, - transformationContext: Record, + transformationContext: Record ) => SelectionSetNode; -export type ResultTransformer = (result: any, delegationContext: DelegationContext, transformationContext: Record) => any; +export type ResultTransformer = ( + result: any, + delegationContext: DelegationContext, + transformationContext: Record +) => any; export type ErrorPathTransformer = (path: ReadonlyArray) => Array; @@ -26,7 +30,7 @@ export default class TransformQuery implements Transform { path, queryTransformer, resultTransformer = result => result, - errorPathTransformer = errorPath => [].concat(errorPath), + errorPathTransformer = errorPath => [...errorPath], fragments = {}, }: { path: Array; @@ -52,14 +56,19 @@ export default class TransformQuery implements Transform { const document = visit(originalRequest.document, { [Kind.FIELD]: { enter: node => { - if (index === pathLength || node.name.value !== this.path[index]) { + if (index === pathLength || node.name.value !== this.path[index] || node.selectionSet == null) { return false; } index++; if (index === pathLength) { - const selectionSet = this.queryTransformer(node.selectionSet, this.fragments, delegationContext, transformationContext); + const selectionSet = this.queryTransformer( + node.selectionSet, + this.fragments, + delegationContext, + transformationContext + ); return { ...node, @@ -92,7 +101,11 @@ export default class TransformQuery implements Transform { }; } - private transformData(data: any, delegationContext: DelegationContext, transformationContext: Record): any { + private transformData( + data: any, + delegationContext: DelegationContext, + transformationContext: Record + ): any { const leafIndex = this.path.length - 1; let index = 0; let newData = data; @@ -114,7 +127,11 @@ export default class TransformQuery implements Transform { private transformErrors(errors: ReadonlyArray): ReadonlyArray { return errors.map(error => { - const path: ReadonlyArray = error.path; + const path: ReadonlyArray | undefined = error.path; + + if (path == null) { + return error; + } let match = true; let index = 0; diff --git a/packages/wrap/src/transforms/TransformRootFields.ts b/packages/wrap/src/transforms/TransformRootFields.ts index ba224efbf5c..bd0193108ea 100644 --- a/packages/wrap/src/transforms/TransformRootFields.ts +++ b/packages/wrap/src/transforms/TransformRootFields.ts @@ -1,6 +1,6 @@ import { GraphQLSchema, GraphQLFieldConfig } from 'graphql'; -import { Request, ExecutionResult } from '@graphql-tools/utils'; +import { Request, ExecutionResult, assertSome } from '@graphql-tools/utils'; import { Transform, DelegationContext, SubschemaConfig } from '@graphql-tools/delegate'; @@ -10,14 +10,19 @@ import TransformObjectFields from './TransformObjectFields'; export default class TransformRootFields implements Transform { private readonly rootFieldTransformer: RootFieldTransformer; - private readonly fieldNodeTransformer: FieldNodeTransformer; - private transformer: TransformObjectFields; + private readonly fieldNodeTransformer: FieldNodeTransformer | undefined; + private transformer: TransformObjectFields | undefined; constructor(rootFieldTransformer: RootFieldTransformer, fieldNodeTransformer?: FieldNodeTransformer) { this.rootFieldTransformer = rootFieldTransformer; this.fieldNodeTransformer = fieldNodeTransformer; } + private _getTransformer() { + assertSome(this.transformer); + return this.transformer; + } + public transformSchema( originalWrappingSchema: GraphQLSchema, subschemaConfig: SubschemaConfig, @@ -57,7 +62,7 @@ export default class TransformRootFields implements Transform { delegationContext: DelegationContext, transformationContext: Record ): Request { - return this.transformer.transformRequest(originalRequest, delegationContext, transformationContext); + return this._getTransformer().transformRequest(originalRequest, delegationContext, transformationContext); } public transformResult( @@ -65,6 +70,6 @@ export default class TransformRootFields implements Transform { delegationContext: DelegationContext, transformationContext: Record ): ExecutionResult { - return this.transformer.transformResult(originalResult, delegationContext, transformationContext); + return this._getTransformer().transformResult(originalResult, delegationContext, transformationContext); } } diff --git a/packages/wrap/src/transforms/WrapFields.ts b/packages/wrap/src/transforms/WrapFields.ts index ee989ae9b9d..817e692fea8 100644 --- a/packages/wrap/src/transforms/WrapFields.ts +++ b/packages/wrap/src/transforms/WrapFields.ts @@ -18,6 +18,7 @@ import { modifyObjectFields, ExecutionResult, relocatedError, + assertSome, } from '@graphql-tools/utils'; import { Transform, defaultMergedResolver, DelegationContext, SubschemaConfig } from '@graphql-tools/delegate'; @@ -30,13 +31,13 @@ interface WrapFieldsTransformationContext { paths: Record; alias: string }>; } -export default class WrapFields implements Transform { +export default class WrapFields implements Transform { private readonly outerTypeName: string; private readonly wrappingFieldNames: Array; private readonly wrappingTypeNames: Array; private readonly numWraps: number; - private readonly fieldNames: Array; - private readonly transformer: Transform; + private readonly fieldNames: Array | undefined; + private readonly transformer: MapFields; constructor( outerTypeName: string, @@ -53,40 +54,38 @@ export default class WrapFields implements Transform( { [outerTypeName]: { - [outerMostWrappingFieldName]: ( - fieldNode, - fragments, - transformationContext: WrapFieldsTransformationContext - ) => + [outerMostWrappingFieldName]: (fieldNode, fragments, transformationContext) => hoistFieldNodes({ fieldNode, path: remainingWrappingFieldNames, fieldNames, fragments, - transformationContext, + transformationContext: transformationContext as WrapFieldsTransformationContext, prefix, }), }, }, { - [outerTypeName]: (value, context: WrapFieldsTransformationContext) => dehoistValue(value, context), + [outerTypeName]: (value, context) => dehoistValue(value, context as WrapFieldsTransformationContext), }, - (errors, context: WrapFieldsTransformationContext) => dehoistErrors(errors, context) + (errors, context) => dehoistErrors(errors, context as WrapFieldsTransformationContext) ); } public transformSchema( originalWrappingSchema: GraphQLSchema, - subschemaConfig: SubschemaConfig, + subschemaConfig: SubschemaConfig, transformedSchema?: GraphQLSchema ): GraphQLSchema { + const fieldNames = this.fieldNames; const targetFieldConfigMap = selectObjectFields( originalWrappingSchema, this.outerTypeName, - !this.fieldNames ? () => true : fieldName => this.fieldNames.includes(fieldName) + !fieldNames ? () => true : fieldName => fieldNames.includes(fieldName) ); const newTargetFieldConfigMap: GraphQLFieldConfigMap = Object.create(null); @@ -123,11 +122,11 @@ export default class WrapFields implements Transform; + let resolve: GraphQLFieldResolver | undefined; if (transformedSchema) { if (wrappingRootField) { const targetSchema = subschemaConfig.schema; - const operation = this.outerTypeName === targetSchema.getQueryType().name ? 'query' : 'mutation'; + const operation = this.outerTypeName === targetSchema.getQueryType()?.name ? 'query' : 'mutation'; const createProxyingResolver = subschemaConfig.createProxyingResolver ?? defaultCreateProxyingResolver; resolve = createProxyingResolver({ subschemaConfig, @@ -176,7 +175,7 @@ export default class WrapFields implements Transform, fields: Array = [], visitedFragmentNames = {} @@ -307,9 +306,9 @@ export function dehoistValue(originalValue: any, context: WrapFieldsTransformati } function dehoistErrors( - errors: ReadonlyArray, + errors: ReadonlyArray | undefined, context: WrapFieldsTransformationContext -): Array { +): Array | undefined { if (errors === undefined) { return undefined; } diff --git a/packages/wrap/src/transforms/WrapQuery.ts b/packages/wrap/src/transforms/WrapQuery.ts index 04b7dbc84ba..978d2a68962 100644 --- a/packages/wrap/src/transforms/WrapQuery.ts +++ b/packages/wrap/src/transforms/WrapQuery.ts @@ -28,7 +28,7 @@ export default class WrapQuery implements Transform { [Kind.FIELD]: { enter: (node: FieldNode) => { fieldPath.push(node.name.value); - if (ourPath === JSON.stringify(fieldPath)) { + if (node.selectionSet != null && ourPath === JSON.stringify(fieldPath)) { const wrapResult = this.wrapper(node.selectionSet); // Selection can be either a single selection or a selection set. If it's just one selection, @@ -68,7 +68,7 @@ export default class WrapQuery implements Transform { let data = rootData; const path = [...this.path]; while (path.length > 1) { - const next = path.shift(); + const next = path.shift()!; if (data[next]) { data = data[next]; } diff --git a/packages/wrap/src/transforms/WrapType.ts b/packages/wrap/src/transforms/WrapType.ts index 83d99089e1a..8d32d768675 100644 --- a/packages/wrap/src/transforms/WrapType.ts +++ b/packages/wrap/src/transforms/WrapType.ts @@ -7,7 +7,7 @@ import { Transform, DelegationContext, SubschemaConfig } from '@graphql-tools/de import WrapFields from './WrapFields'; export default class WrapType implements Transform { - private readonly transformer: Transform; + private readonly transformer: WrapFields; constructor(outerTypeName: string, innerTypeName: string, fieldName: string) { this.transformer = new WrapFields(outerTypeName, [fieldName], [innerTypeName]); @@ -26,7 +26,7 @@ export default class WrapType implements Transform { delegationContext: DelegationContext, transformationContext: Record ): Request { - return this.transformer.transformRequest(originalRequest, delegationContext, transformationContext); + return this.transformer.transformRequest(originalRequest, delegationContext, transformationContext as any); } public transformResult( @@ -34,6 +34,6 @@ export default class WrapType implements Transform { delegationContext: DelegationContext, transformationContext: Record ): ExecutionResult { - return this.transformer.transformResult(originalResult, delegationContext, transformationContext); + return this.transformer.transformResult(originalResult, delegationContext, transformationContext as any); } } diff --git a/packages/wrap/src/types.ts b/packages/wrap/src/types.ts index 57bec411122..3eb9c7ef05e 100644 --- a/packages/wrap/src/types.ts +++ b/packages/wrap/src/types.ts @@ -13,15 +13,15 @@ import { GraphQLEnumValueConfig, } from 'graphql'; import { DelegationContext } from '@graphql-tools/delegate'; -import { Executor, Subscriber, Request } from '@graphql-tools/utils'; +import { Executor, Subscriber, Request, Maybe } from '@graphql-tools/utils'; export interface IMakeRemoteExecutableSchemaOptions> { schema: GraphQLSchema | string; - executor?: Executor; + executor: Executor; subscriber?: Subscriber; createResolver?: ( executor: Executor, - subscriber: Subscriber + subscriber?: Subscriber | undefined ) => GraphQLFieldResolver; buildSchemaOptions?: BuildSchemaOptions; } @@ -45,25 +45,25 @@ export type InputObjectNodeTransformer = ( inputObjectNode: ObjectValueNode, request: Request, delegationContext?: DelegationContext -) => ObjectValueNode; +) => ObjectValueNode | undefined; export type FieldTransformer> = ( typeName: string, fieldName: string, fieldConfig: GraphQLFieldConfig -) => GraphQLFieldConfig | [string, GraphQLFieldConfig] | null | undefined; +) => Maybe | [string, GraphQLFieldConfig]>; export type RootFieldTransformer> = ( operation: 'Query' | 'Mutation' | 'Subscription', fieldName: string, fieldConfig: GraphQLFieldConfig -) => GraphQLFieldConfig | [string, GraphQLFieldConfig] | null | undefined; +) => Maybe | [string, GraphQLFieldConfig]>; export type EnumValueTransformer = ( typeName: string, externalValue: string, enumValueConfig: GraphQLEnumValueConfig -) => GraphQLEnumValueConfig | [string, GraphQLEnumValueConfig] | null | undefined; +) => Maybe; export type FieldNodeTransformer = ( typeName: string, @@ -71,7 +71,7 @@ export type FieldNodeTransformer = ( fieldNode: FieldNode, fragments: Record, transformationContext: Record -) => SelectionNode | Array; +) => Maybe>; export type LeafValueTransformer = (typeName: string, value: any) => any; @@ -80,6 +80,6 @@ export type DataTransformer = (value: any, transformationContext: Record; export type ErrorsTransformer = ( - errors: ReadonlyArray, + errors: ReadonlyArray | undefined, transformationContext: Record -) => Array; +) => Array | undefined; diff --git a/packages/wrap/src/wrapSchema.ts b/packages/wrap/src/wrapSchema.ts index 12f55f7bcde..6d7f9d1ed9c 100644 --- a/packages/wrap/src/wrapSchema.ts +++ b/packages/wrap/src/wrapSchema.ts @@ -11,7 +11,9 @@ import { MapperKind, mapSchema } from '@graphql-tools/utils'; import { SubschemaConfig, defaultMergedResolver, applySchemaTransforms } from '@graphql-tools/delegate'; import { generateProxyingResolvers } from './generateProxyingResolvers'; -export function wrapSchema(subschemaConfig: SubschemaConfig): GraphQLSchema { +export function wrapSchema>( + subschemaConfig: SubschemaConfig +): GraphQLSchema { const targetSchema = subschemaConfig.schema; const proxyingResolvers = generateProxyingResolvers(subschemaConfig); @@ -32,9 +34,13 @@ function createWrappingSchema( const fieldConfigMap = config.fields; Object.keys(fieldConfigMap).forEach(fieldName => { + const field = fieldConfigMap[fieldName]; + if (field == null) { + return; + } fieldConfigMap[fieldName] = { - ...fieldConfigMap[fieldName], - ...proxyingResolvers[type.name][fieldName], + ...field, + ...proxyingResolvers[type.name]?.[fieldName], }; }); @@ -45,8 +51,12 @@ function createWrappingSchema( config.isTypeOf = undefined; Object.keys(config.fields).forEach(fieldName => { - config.fields[fieldName].resolve = defaultMergedResolver; - config.fields[fieldName].subscribe = null; + const field = config.fields[fieldName]; + if (field == null) { + return; + } + field.resolve = defaultMergedResolver; + field.subscribe = undefined; }); return new GraphQLObjectType(config); diff --git a/packages/wrap/tests/gatsbyTransforms.test.ts b/packages/wrap/tests/gatsbyTransforms.test.ts index 3831e37be25..9ebb579ef2e 100644 --- a/packages/wrap/tests/gatsbyTransforms.test.ts +++ b/packages/wrap/tests/gatsbyTransforms.test.ts @@ -7,7 +7,7 @@ import { GraphQLFieldConfigMap, } from 'graphql'; -import { mapSchema, MapperKind, addTypes, modifyObjectFields } from '@graphql-tools/utils'; +import { mapSchema, MapperKind, addTypes, modifyObjectFields, assertSome } from '@graphql-tools/utils'; import { wrapSchema, RenameTypes } from '../src'; import { makeExecutableSchema } from '@graphql-tools/schema'; import { addMocksToSchema } from '@graphql-tools/mock'; @@ -36,7 +36,9 @@ class NamespaceUnderFieldTransform { } transformSchema(schema: GraphQLSchema) { - const queryConfig = schema.getQueryType().toConfig(); + const QueryType = schema.getQueryType(); + assertSome(QueryType) + const queryConfig = QueryType.toConfig(); const nestedQuery = new GraphQLObjectType({ ...queryConfig, diff --git a/packages/wrap/tests/transformFilterInputObjectFields.test.ts b/packages/wrap/tests/transformFilterInputObjectFields.test.ts index 44c050fe8bc..810d9b60858 100644 --- a/packages/wrap/tests/transformFilterInputObjectFields.test.ts +++ b/packages/wrap/tests/transformFilterInputObjectFields.test.ts @@ -1,6 +1,7 @@ import { wrapSchema, FilterInputObjectFields } from '@graphql-tools/wrap'; import { makeExecutableSchema } from '@graphql-tools/schema'; import { graphql, astFromValue, Kind, GraphQLString } from 'graphql'; +import { assertSome } from '@graphql-tools/utils'; describe('FilterInputObjectFields', () => { test('filtering works', async () => { @@ -36,6 +37,8 @@ describe('FilterInputObjectFields', () => { (typeName, fieldName) => (typeName !== 'InputObject' || fieldName !== 'field2'), (typeName, inputObjectNode) => { if (typeName === 'InputObject') { + const value = astFromValue('field2', GraphQLString) + assertSome(value) return { ...inputObjectNode, fields: [...inputObjectNode.fields, { @@ -44,7 +47,7 @@ describe('FilterInputObjectFields', () => { kind: Kind.NAME, value: 'field2', }, - value: astFromValue('field2', GraphQLString), + value, }], }; } @@ -63,6 +66,7 @@ describe('FilterInputObjectFields', () => { }`; const result = await graphql(transformedSchema, query); + assertSome(result.data) expect(result.data.test.field1).toBe('field1'); expect(result.data.test.field2).toBe('field2'); }); diff --git a/packages/wrap/tests/transformFilterTypes.test.ts b/packages/wrap/tests/transformFilterTypes.test.ts index bfde574fd8d..931445475e7 100644 --- a/packages/wrap/tests/transformFilterTypes.test.ts +++ b/packages/wrap/tests/transformFilterTypes.test.ts @@ -1,5 +1,6 @@ import { wrapSchema, FilterTypes } from '@graphql-tools/wrap'; import { graphql, GraphQLSchema, GraphQLNamedType } from 'graphql'; +import { assertSome } from '@graphql-tools/utils'; import { bookingSchema } from './fixtures/schemas'; describe('FilterTypes', () => { @@ -62,6 +63,7 @@ describe('FilterTypes', () => { `, ); expect(result.errors).toBeDefined(); + assertSome(result.errors) expect(result.errors.length).toBe(1); expect(result.errors[0].message).toBe( 'Cannot query field "customer" on type "Booking".', diff --git a/packages/wrap/tests/transformMapLeafValues.test.ts b/packages/wrap/tests/transformMapLeafValues.test.ts index 9850a6ad21d..09650ac9cbb 100644 --- a/packages/wrap/tests/transformMapLeafValues.test.ts +++ b/packages/wrap/tests/transformMapLeafValues.test.ts @@ -1,6 +1,7 @@ import { wrapSchema, MapLeafValues } from '@graphql-tools/wrap'; import { makeExecutableSchema } from '@graphql-tools/schema'; import { graphql } from 'graphql'; +import { assertSome } from '@graphql-tools/utils'; describe('MapLeafValues', () => { test('works', async () => { @@ -48,6 +49,7 @@ describe('MapLeafValues', () => { }`; const result = await graphql(transformedSchema, query); + assertSome(result.data) expect(result.data.testEnum).toBe('THREE'); expect(result.data.testScalar).toBe(15); }); diff --git a/packages/wrap/tests/transformRemoveObjectFieldDeprecations.test.ts b/packages/wrap/tests/transformRemoveObjectFieldDeprecations.test.ts index 8fd64f6b53a..f4b06c18ad9 100644 --- a/packages/wrap/tests/transformRemoveObjectFieldDeprecations.test.ts +++ b/packages/wrap/tests/transformRemoveObjectFieldDeprecations.test.ts @@ -1,5 +1,7 @@ import { wrapSchema, RemoveObjectFieldDeprecations } from '@graphql-tools/wrap'; import { makeExecutableSchema } from '@graphql-tools/schema'; +import { assertGraphQLObjectType } from '../../testing/assertion'; +import { assertSome } from '@graphql-tools/utils'; describe('RemoveObjectFieldDeprecations', () => { const originalSchema = makeExecutableSchema({ @@ -20,11 +22,15 @@ describe('RemoveObjectFieldDeprecations', () => { ], }); - const fields = transformedSchema.getType('Test').getFields(); + const Test = transformedSchema.getType('Test') + assertGraphQLObjectType(Test) + const fields = Test.getFields(); + assertSome(fields.first) expect(fields.first.deprecationReason).toEqual('do not remove'); + assertSome(fields.second) expect(fields.second.deprecationReason).toBeUndefined(); - expect(fields.first.astNode.directives.length).toEqual(1); - expect(fields.second.astNode.directives.length).toEqual(0); + expect(fields.first.astNode?.directives?.length).toEqual(1); + expect(fields.second.astNode?.directives?.length).toEqual(0); }); test('removes deprecations by reason regex', async () => { @@ -35,10 +41,14 @@ describe('RemoveObjectFieldDeprecations', () => { ], }); - const fields = transformedSchema.getType('Test').getFields(); + const Test = transformedSchema.getType('Test') + assertGraphQLObjectType(Test) + const fields = Test.getFields(); + assertSome(fields.first) expect(fields.first.deprecationReason).toBeUndefined(); + assertSome(fields.second) expect(fields.second.deprecationReason).toBeUndefined(); - expect(fields.first.astNode.directives.length).toEqual(0); - expect(fields.second.astNode.directives.length).toEqual(0); + expect(fields.first.astNode?.directives?.length).toEqual(0); + expect(fields.second.astNode?.directives?.length).toEqual(0); }); }); diff --git a/packages/wrap/tests/transformRemoveObjectFieldDirectives.test.ts b/packages/wrap/tests/transformRemoveObjectFieldDirectives.test.ts index 331c78eaa4a..6186cada3a6 100644 --- a/packages/wrap/tests/transformRemoveObjectFieldDirectives.test.ts +++ b/packages/wrap/tests/transformRemoveObjectFieldDirectives.test.ts @@ -1,5 +1,7 @@ import { wrapSchema, RemoveObjectFieldDirectives } from '@graphql-tools/wrap'; import { makeExecutableSchema } from '@graphql-tools/schema'; +import { assertGraphQLObjectType } from '../../testing/assertion'; +import { assertSome } from '@graphql-tools/utils'; describe('RemoveObjectFieldDirectives', () => { const originalSchema = makeExecutableSchema({ @@ -24,11 +26,17 @@ describe('RemoveObjectFieldDirectives', () => { ], }); - const fields = transformedSchema.getType('Test').getFields(); - expect(fields.id.astNode.directives.length).toEqual(1); - expect(fields.first.astNode.directives.length).toEqual(0); - expect(fields.second.astNode.directives.length).toEqual(0); - expect(fields.third.astNode.directives.length).toEqual(0); + const Test = transformedSchema.getType('Test') + assertGraphQLObjectType(Test) + const fields = Test.getFields(); + assertSome(fields.id) + expect(fields.id.astNode?.directives?.length).toEqual(1); + assertSome(fields.first) + expect(fields.first.astNode?.directives?.length).toEqual(0); + assertSome(fields.second) + expect(fields.second.astNode?.directives?.length).toEqual(0); + assertSome(fields.third) + expect(fields.third.astNode?.directives?.length).toEqual(0); }); test('removes directives by name regex', async () => { @@ -39,11 +47,17 @@ describe('RemoveObjectFieldDirectives', () => { ], }); - const fields = transformedSchema.getType('Test').getFields(); - expect(fields.id.astNode.directives.length).toEqual(1); - expect(fields.first.astNode.directives.length).toEqual(0); - expect(fields.second.astNode.directives.length).toEqual(0); - expect(fields.third.astNode.directives.length).toEqual(0); + const Test = transformedSchema.getType('Test') + assertGraphQLObjectType(Test) + const fields = Test.getFields(); + assertSome(fields.id) + expect(fields.id.astNode?.directives?.length).toEqual(1); + assertSome(fields.first) + expect(fields.first.astNode?.directives?.length).toEqual(0); + assertSome(fields.second) + expect(fields.second.astNode?.directives?.length).toEqual(0); + assertSome(fields.third) + expect(fields.third.astNode?.directives?.length).toEqual(0); }); test('removes directives by argument', async () => { @@ -54,11 +68,17 @@ describe('RemoveObjectFieldDirectives', () => { ], }); - const fields = transformedSchema.getType('Test').getFields(); - expect(fields.id.astNode.directives.length).toEqual(0); - expect(fields.first.astNode.directives.length).toEqual(1); - expect(fields.second.astNode.directives.length).toEqual(0); - expect(fields.third.astNode.directives.length).toEqual(0); + const Test = transformedSchema.getType('Test') + assertGraphQLObjectType(Test) + const fields = Test.getFields(); + assertSome(fields.id) + expect(fields.id.astNode?.directives?.length).toEqual(0); + assertSome(fields.first) + expect(fields.first.astNode?.directives?.length).toEqual(1); + assertSome(fields.second) + expect(fields.second.astNode?.directives?.length).toEqual(0); + assertSome(fields.third) + expect(fields.third.astNode?.directives?.length).toEqual(0); }); test('removes directives by argument regex', async () => { @@ -69,10 +89,16 @@ describe('RemoveObjectFieldDirectives', () => { ], }); - const fields = transformedSchema.getType('Test').getFields(); - expect(fields.id.astNode.directives.length).toEqual(0); - expect(fields.first.astNode.directives.length).toEqual(0); - expect(fields.second.astNode.directives.length).toEqual(0); - expect(fields.third.astNode.directives.length).toEqual(0); + const Test = transformedSchema.getType('Test') + assertGraphQLObjectType(Test) + const fields = Test.getFields(); + assertSome(fields.id) + expect(fields.id.astNode?.directives?.length).toEqual(0); + assertSome(fields.first) + expect(fields.first.astNode?.directives?.length).toEqual(0); + assertSome(fields.second) + expect(fields.second.astNode?.directives?.length).toEqual(0); + assertSome(fields.third) + expect(fields.third.astNode?.directives?.length).toEqual(0); }); }); diff --git a/packages/wrap/tests/transformRemoveObjectFieldsWithDeprecation.test.ts b/packages/wrap/tests/transformRemoveObjectFieldsWithDeprecation.test.ts index 27781011cfd..1989214acfa 100644 --- a/packages/wrap/tests/transformRemoveObjectFieldsWithDeprecation.test.ts +++ b/packages/wrap/tests/transformRemoveObjectFieldsWithDeprecation.test.ts @@ -1,5 +1,6 @@ import { wrapSchema, RemoveObjectFieldsWithDeprecation } from '@graphql-tools/wrap'; import { makeExecutableSchema } from '@graphql-tools/schema'; +import { assertGraphQLObjectType } from '../../testing/assertion'; describe('RemoveObjectFieldsWithDeprecation', () => { const originalSchema = makeExecutableSchema({ @@ -20,7 +21,9 @@ describe('RemoveObjectFieldsWithDeprecation', () => { ], }); - const fields = transformedSchema.getType('Test').getFields(); + const Test = transformedSchema.getType('Test') + assertGraphQLObjectType(Test) + const fields = Test.getFields(); expect(fields.first).toBeDefined(); expect(fields.second).toBeUndefined(); }); @@ -33,7 +36,9 @@ describe('RemoveObjectFieldsWithDeprecation', () => { ], }); - const fields = transformedSchema.getType('Test').getFields(); + const Test = transformedSchema.getType('Test') + assertGraphQLObjectType(Test) + const fields = Test.getFields(); expect(fields.first).toBeUndefined(); expect(fields.second).toBeUndefined(); }); diff --git a/packages/wrap/tests/transformRemoveObjectFieldsWithDirective.test.ts b/packages/wrap/tests/transformRemoveObjectFieldsWithDirective.test.ts index 6e661ea1b56..8edc9c142ff 100644 --- a/packages/wrap/tests/transformRemoveObjectFieldsWithDirective.test.ts +++ b/packages/wrap/tests/transformRemoveObjectFieldsWithDirective.test.ts @@ -1,5 +1,6 @@ import { wrapSchema, RemoveObjectFieldsWithDirective } from '@graphql-tools/wrap'; import { makeExecutableSchema } from '@graphql-tools/schema'; +import { assertGraphQLObjectType } from '../../testing/assertion'; describe('RemoveObjectFieldsWithDirective', () => { const originalSchema = makeExecutableSchema({ @@ -25,7 +26,9 @@ describe('RemoveObjectFieldsWithDirective', () => { ], }); - const fields = transformedSchema.getType('Test').getFields(); + const Test = transformedSchema.getType('Test') + assertGraphQLObjectType(Test) + const fields = Test.getFields(); expect(fields.first).toBeUndefined(); expect(fields.second).toBeUndefined(); expect(fields.third).toBeUndefined(); @@ -40,7 +43,9 @@ describe('RemoveObjectFieldsWithDirective', () => { ], }); - const fields = transformedSchema.getType('Test').getFields(); + const Test = transformedSchema.getType('Test') + assertGraphQLObjectType(Test) + const fields = Test.getFields(); expect(fields.first).toBeUndefined(); expect(fields.second).toBeUndefined(); expect(fields.third).toBeUndefined(); @@ -55,7 +60,9 @@ describe('RemoveObjectFieldsWithDirective', () => { ], }); - const fields = transformedSchema.getType('Test').getFields(); + const Test = transformedSchema.getType('Test') + assertGraphQLObjectType(Test) + const fields = Test.getFields(); expect(fields.first).toBeDefined(); expect(fields.second).toBeUndefined(); expect(fields.third).toBeUndefined(); @@ -70,7 +77,9 @@ describe('RemoveObjectFieldsWithDirective', () => { ], }); - const fields = transformedSchema.getType('Test').getFields(); + const Test = transformedSchema.getType('Test') + assertGraphQLObjectType(Test) + const fields = Test.getFields(); expect(fields.first).toBeUndefined(); expect(fields.second).toBeUndefined(); expect(fields.third).toBeUndefined(); diff --git a/packages/wrap/tests/transformRenameInputObjectFields.test.ts b/packages/wrap/tests/transformRenameInputObjectFields.test.ts index 3ebbe735dc4..4ad2abd005e 100644 --- a/packages/wrap/tests/transformRenameInputObjectFields.test.ts +++ b/packages/wrap/tests/transformRenameInputObjectFields.test.ts @@ -1,6 +1,7 @@ import { wrapSchema, RenameInputObjectFields } from '@graphql-tools/wrap'; import { makeExecutableSchema } from '@graphql-tools/schema'; import { graphql } from 'graphql'; +import { assertSome } from '@graphql-tools/utils'; describe('RenameInputObjectFields', () => { test('renaming with arguments works', async () => { @@ -33,7 +34,7 @@ describe('RenameInputObjectFields', () => { schema, transforms: [ new RenameInputObjectFields( - (typeName: string, fieldName: string) => { + (typeName, fieldName) => { if (typeName === 'InputObject' && fieldName === 'field2') { return 'field3'; } @@ -53,6 +54,7 @@ describe('RenameInputObjectFields', () => { }`; const result = await graphql(transformedSchema, query); + assertSome(result.data) expect(result.data.test.field1).toBe('field1'); expect(result.data.test.field2).toBe('field2'); }); @@ -115,6 +117,7 @@ describe('RenameInputObjectFields', () => { } } const result = await graphql(transformedSchema, query, {}, {}, variables); + assertSome(result.data) expect(result.data.test.field1).toBe('field1'); expect(result.data.test.field2).toBe('field2'); }); diff --git a/packages/wrap/tests/transformTransformEnumValues.test.ts b/packages/wrap/tests/transformTransformEnumValues.test.ts index d5b2a935a5c..164d6ee3025 100644 --- a/packages/wrap/tests/transformTransformEnumValues.test.ts +++ b/packages/wrap/tests/transformTransformEnumValues.test.ts @@ -2,6 +2,13 @@ import { wrapSchema, TransformEnumValues } from '@graphql-tools/wrap'; import { makeExecutableSchema } from '@graphql-tools/schema'; import { graphql, GraphQLEnumType } from 'graphql'; +function assertGraphQLEnumType(input: unknown): asserts input is GraphQLEnumType { + if (input instanceof GraphQLEnumType) { + return + } + throw new Error("Expected GraphQLEnumType.") +} + describe('TransformEnumValues', () => { test('works', async () => { const schema = makeExecutableSchema({ @@ -74,7 +81,9 @@ describe('TransformEnumValues', () => { const result = await graphql(transformedSchema, query); expect(result.errors).toBeUndefined(); - expect((transformedSchema.getType('TestEnum') as GraphQLEnumType).getValue('UNO').value).toBe('ONE'); + const TestEnum = transformedSchema.getType('TestEnum') + assertGraphQLEnumType(TestEnum) + expect(TestEnum.getValue('UNO')?.value).toBe('ONE'); }); test('works with variables', async () => { diff --git a/tsconfig.json b/tsconfig.json index e4c59c0525b..86feb85ea87 100755 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,11 +18,14 @@ "downlevelIteration": true, "suppressImplicitAnyIndexErrors": true, - "noImplicitAny": true, - "alwaysStrict": true, + + "skipLibCheck": true, + + "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, - "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noPropertyAccessFromIndexSignature": true, "paths": { "@graphql-tools/*-loader": ["packages/loaders/*/src/index.ts", "packages/*-loader/src/index.ts"], diff --git a/yarn.lock b/yarn.lock index 46755647803..4e51053a40f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2841,6 +2841,15 @@ resolved "https://registry.yarnpkg.com/@types/valid-url/-/valid-url-1.0.3.tgz#a124389fb953559c7f889795a98620e91adb3687" integrity sha512-+33x29mg+ecU88ODdWpqaie2upIuRkhujVLA7TuJjM823cNMbeggfI6NhxewaRaRF8dy+g33e4uIg/m5Mb3xDQ== +"@types/webpack@5.28.0": + version "5.28.0" + resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-5.28.0.tgz#78dde06212f038d77e54116cfe69e88ae9ed2c03" + integrity sha512-8cP0CzcxUiFuA9xGJkfeVpqmWTk9nx6CWwamRGCj95ph1SmlRRk9KlCZ6avhCbZd4L68LvYT6l1kpdEnQXrF8w== + dependencies: + "@types/node" "*" + tapable "^2.2.0" + webpack "^5" + "@types/websocket@1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@types/websocket/-/websocket-1.0.2.tgz#d2855c6a312b7da73ed16ba6781815bf30c6187a" @@ -13436,7 +13445,7 @@ webpack-sources@^2.1.1: source-list-map "^2.0.1" source-map "^0.6.1" -webpack@^5.28.0, webpack@^5.37.0: +webpack@^5, webpack@^5.28.0, webpack@^5.37.0: version "5.37.1" resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.37.1.tgz#2deb5acd350583c1ab9338471f323381b0b0c14b" integrity sha512-btZjGy/hSjCAAVHw+cKG+L0M+rstlyxbO2C+BOTaQ5/XAnxkDrP5sVbqWhXgo4pL3X2dcOib6rqCP20Zr9PLow==