Skip to content

Commit

Permalink
fix(stitching): delegateToSchema args specification
Browse files Browse the repository at this point in the history
BREAKING CHANGE:

Allow specification of args even with root field transformations. Includes changes to createRequestFromInfo and createRequest signatures and related interfaces, streamlining them to only use the targetOperation and targetFieldName, returning to the original upstream graphql-tools behavior of adding args later as a transform.

args passed to delegateToSchema, however, are still optional. All args passed to delegateToSchema are serialized using the targetSchema serialization, if available.
  • Loading branch information
yaacovCR committed Mar 26, 2020
1 parent 5049022 commit f3b30da
Show file tree
Hide file tree
Showing 8 changed files with 211 additions and 357 deletions.
9 changes: 1 addition & 8 deletions src/Interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,16 +180,12 @@ export interface IDelegateToSchemaOptions<TContext = { [key: string]: any }> {
transforms?: Array<Transform>;
skipValidation?: boolean;
skipTypeMerging?: boolean;
transformedSchema?: GraphQLSchema;
}

export interface ICreateRequestFromInfo {
info: IGraphQLToolsResolveInfo;
schema: GraphQLSchema | SubschemaConfig;
transformedSchema: GraphQLSchema;
operation: Operation;
fieldName: string;
args?: Record<string, any>;
selectionSet?: SelectionSetNode;
fieldNodes?: ReadonlyArray<FieldNode>;
}
Expand All @@ -201,13 +197,10 @@ export interface ICreateRequest {
fragments: Record<string, FragmentDefinitionNode>;
variableDefinitions: ReadonlyArray<VariableDefinitionNode>;
variableValues: Record<string, any>;
targetSchema: GraphQLSchema;
targetOperation: Operation;
targetField: string;
args: Record<string, any>;
targetFieldName: string;
selectionSet: SelectionSetNode;
fieldNodes: ReadonlyArray<FieldNode>;
defaultArgs: Record<string, any>;
}

export interface IDelegateRequestOptions extends IDelegateToSchemaOptions {
Expand Down
266 changes: 51 additions & 215 deletions src/delegate/createRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,14 @@ import {
typeFromAST,
NamedTypeNode,
GraphQLInputType,
GraphQLField,
GraphQLArgument,
VariableDefinitionNode,
TypeNode,
GraphQLType,
SelectionSetNode,
isNonNullType,
isListType,
} from 'graphql';

import {
ICreateRequestFromInfo,
Request,
isSubschemaConfig,
ICreateRequest,
} from '../Interfaces';
import { ICreateRequestFromInfo, Request, ICreateRequest } from '../Interfaces';
import { serializeInputValue } from '../utils/index';
import { updateArgument } from '../utils/updateArgument';

export function getDelegatingOperation(
parentType: GraphQLObjectType,
Expand All @@ -44,53 +35,27 @@ export function getDelegatingOperation(

export function createRequestFromInfo({
info,
schema: subschemaOrSubschemaConfig,
transformedSchema,
operation = getDelegatingOperation(info.parentType, info.schema),
fieldName = info.fieldName,
args,
selectionSet,
fieldNodes,
}: ICreateRequestFromInfo): Request {
const sourceParentType = info.parentType;
const sourceFieldName = info.fieldName;

const fieldArguments = sourceParentType.getFields()[sourceFieldName].args;
const defaultArgs = {};
fieldArguments.forEach((argument) => {
if (argument.defaultValue != null) {
defaultArgs[argument.name] = argument.defaultValue;
}
});

let targetSchema;
if (transformedSchema != null) {
targetSchema = transformedSchema;
} else {
targetSchema = isSubschemaConfig(subschemaOrSubschemaConfig)
? subschemaOrSubschemaConfig.schema
: subschemaOrSubschemaConfig;
}

return createRequest({
sourceSchema: info.schema,
sourceParentType,
sourceFieldName,
sourceParentType: info.parentType,
sourceFieldName: info.fieldName,
fragments: info.fragments,
variableDefinitions: info.operation.variableDefinitions,
variableValues: info.variableValues,
targetSchema,
targetOperation: operation,
targetField: fieldName,
args,
targetFieldName: fieldName,
selectionSet,
fieldNodes:
selectionSet != null
? undefined
: fieldNodes != null
? fieldNodes
: info.fieldNodes,
defaultArgs,
});
}

Expand All @@ -101,19 +66,13 @@ export function createRequest({
fragments,
variableDefinitions,
variableValues,
targetSchema,
targetOperation,
targetField,
args,
targetFieldName,
selectionSet,
fieldNodes,
defaultArgs,
}: ICreateRequest): Request {
let argumentNodes: ReadonlyArray<ArgumentNode>;

let newSelectionSet: SelectionSetNode = selectionSet;
let newVariableDefinitions: ReadonlyArray<VariableDefinitionNode> = variableDefinitions;

if (!selectionSet && fieldNodes != null) {
const selections: Array<SelectionNode> = fieldNodes.reduce(
(acc, fieldNode) =>
Expand All @@ -135,51 +94,53 @@ export function createRequest({
argumentNodes = [];
}

let variables = {};
for (const variableDefinition of variableDefinitions) {
const varName = variableDefinition.variable.name.value;
const newVariables = {};
const variableDefinitionMap = {};
variableDefinitions.forEach((def) => {
const varName = def.variable.name.value;
variableDefinitionMap[varName] = def;
const varType = typeFromAST(
sourceSchema,
variableDefinition.type as NamedTypeNode,
def.type as NamedTypeNode,
) as GraphQLInputType;
variables[varName] = serializeInputValue(varType, variableValues[varName]);
}
newVariables[varName] = serializeInputValue(
varType,
variableValues[varName],
);
});

const argumentNodeMap: Record<string, ArgumentNode> = {};
argumentNodes.forEach((argument: ArgumentNode) => {
argumentNodeMap[argument.name.value] = argument;
});

const {
arguments: updatedArguments,
variableDefinitions: updatedVariableDefinitions,
variableValues: updatedVariableValues,
} = updateArguments(
updateArgumentsWithDefaults(
sourceParentType,
sourceFieldName,
targetSchema,
targetOperation,
targetField,
argumentNodes,
variableDefinitions,
variables,
args,
defaultArgs,
argumentNodeMap,
variableDefinitionMap,
newVariables,
);
argumentNodes = updatedArguments;
newVariableDefinitions = updatedVariableDefinitions;
variables = updatedVariableValues;

const rootfieldNode: FieldNode = {
kind: Kind.FIELD,
alias: null,
arguments: argumentNodes,
arguments: Object.keys(argumentNodeMap).map(
(argName) => argumentNodeMap[argName],
),
selectionSet: newSelectionSet,
name: {
kind: Kind.NAME,
value: targetField || fieldNodes[0].name.value,
value: targetFieldName || fieldNodes[0].name.value,
},
};

const operationDefinition: OperationDefinitionNode = {
kind: Kind.OPERATION_DEFINITION,
operation: targetOperation,
variableDefinitions: newVariableDefinitions,
variableDefinitions: Object.keys(variableDefinitionMap).map(
(varName) => variableDefinitionMap[varName],
),
selectionSet: {
kind: Kind.SELECTION_SET,
selections: [rootfieldNode],
Expand All @@ -197,160 +158,35 @@ export function createRequest({

return {
document,
variables,
variables: newVariables,
};
}

function updateArguments(
function updateArgumentsWithDefaults(
sourceParentType: GraphQLObjectType,
sourceFieldName: string,
targetSchema: GraphQLSchema,
operation: OperationTypeNode,
fieldName: string,
argumentNodes: ReadonlyArray<ArgumentNode> = [],
variableDefinitions: ReadonlyArray<VariableDefinitionNode> = [],
variableValues: Record<string, any> = {},
newArgs: Record<string, any> = {},
defaultArgs: Record<string, any> = {},
): {
arguments: Array<ArgumentNode>;
variableDefinitions: Array<VariableDefinitionNode>;
variableValues: Record<string, any>;
} {
let type: GraphQLObjectType;
if (operation === 'subscription') {
type = targetSchema.getSubscriptionType();
} else if (operation === 'mutation') {
type = targetSchema.getMutationType();
} else {
type = targetSchema.getQueryType();
}

const updatedVariableDefinitions = {};
const varNames = {};
variableDefinitions.forEach((def) => {
const varName = def.variable.name.value;
updatedVariableDefinitions[varName] = def;
varNames[varName] = true;
});
let numGeneratedVariables = 0;

const updatedArgs: Record<string, ArgumentNode> = {};
argumentNodes.forEach((argument: ArgumentNode) => {
updatedArgs[argument.name.value] = argument;
});

const field: GraphQLField<any, any> = type.getFields()[fieldName];
if (field != null) {
field.args.forEach((argument: GraphQLArgument) => {
const argName = argument.name;
argumentNodeMap: Record<string, ArgumentNode>,
variableDefinitionMap: Record<string, VariableDefinitionNode>,
variableValues: Record<string, any>,
): void {
const sourceField = sourceParentType.getFields()[sourceFieldName];
sourceField.args.forEach((argument: GraphQLArgument) => {
const argName = argument.name;
const sourceArgType = argument.type;

let newArg;
let argType;
if (newArgs[argName] != null) {
newArg = newArgs[argName];
argType = argument.type;
} else if (updatedArgs[argName] == null && defaultArgs[argName] != null) {
newArg = defaultArgs[argName];
const sourcefield = sourceParentType.getFields()[sourceFieldName];
argType = sourcefield.args.find((arg) => arg.name === argName).type;
}
if (argumentNodeMap[argName] == null) {
const defaultValue = argument.defaultValue;

if (newArg != null) {
numGeneratedVariables++;
if (defaultValue != null) {
updateArgument(
argName,
argType,
numGeneratedVariables,
varNames,
updatedArgs,
updatedVariableDefinitions,
sourceArgType,
argumentNodeMap,
variableDefinitionMap,
variableValues,
newArg,
serializeInputValue(sourceArgType, defaultValue),
);
}
});
}

return {
arguments: Object.keys(updatedArgs).map((argName) => updatedArgs[argName]),
variableDefinitions: Object.keys(updatedVariableDefinitions).map(
(varName) => updatedVariableDefinitions[varName],
),
variableValues,
};
}

function updateArgument(
argName: string,
argType: GraphQLInputType,
numGeneratedVariables: number,
varNames: Record<string, boolean>,
updatedArgs: Record<string, ArgumentNode>,
updatedVariableDefinitions: Record<string, VariableDefinitionNode>,
variableValues: Record<string, any>,
newArg: any,
) {
let varName;
do {
varName = `_v${numGeneratedVariables.toString()}_${argName}`;
} while (varNames[varName]);

updatedArgs[argName] = {
kind: Kind.ARGUMENT,
name: {
kind: Kind.NAME,
value: argName,
},
value: {
kind: Kind.VARIABLE,
name: {
kind: Kind.NAME,
value: varName,
},
},
};
varNames[varName] = true;
updatedVariableDefinitions[varName] = {
kind: Kind.VARIABLE_DEFINITION,
variable: {
kind: Kind.VARIABLE,
name: {
kind: Kind.NAME,
value: varName,
},
},
type: astFromType(argType),
};
variableValues[varName] = serializeInputValue(argType, newArg);
}

function astFromType(type: GraphQLType): TypeNode {
if (isNonNullType(type)) {
const innerType = astFromType(type.ofType);
if (innerType.kind === Kind.NON_NULL_TYPE) {
throw new Error(
`Invalid type node ${JSON.stringify(
type,
)}. Inner type of non-null type cannot be a non-null type.`,
);
}
return {
kind: Kind.NON_NULL_TYPE,
type: innerType,
};
} else if (isListType(type)) {
return {
kind: Kind.LIST_TYPE,
type: astFromType(type.ofType),
};
}

return {
kind: Kind.NAMED_TYPE,
name: {
kind: Kind.NAME,
value: type.name,
},
};
});
}

0 comments on commit f3b30da

Please sign in to comment.