Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

createRequest with transforms API #724

Closed
wants to merge 40 commits into from
Closed
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
f9c46be
createBatchOperation with transforms API
Apr 9, 2018
33dcd04
Merge branch 'next-api' of git://github.com/apollographql/graphql-too…
Apr 9, 2018
db3a233
Fix tests for adjacent fields in fragments
Apr 9, 2018
b3a97c6
remove createDocument that is no longer used or exported
Apr 9, 2018
34d4eb5
WIP fix createOperation API (thanks freiksenet)
Apr 10, 2018
5552a3e
WIP: address more review comments
Apr 11, 2018
02db0a4
Remove hack that will cause bugs
Apr 12, 2018
251d0dc
Fix tests
Apr 12, 2018
8c5e3c8
Clean up
Apr 12, 2018
a726453
Fix bugs related to passing no arguments
Apr 13, 2018
a98d8cc
Add two tests (let me know if i should move them)
Apr 13, 2018
6b28177
Return `Request` type, createOperation -> createDocument
Apr 17, 2018
242cde0
More tests for createDocument
Apr 17, 2018
a8704ac
Merge branch 'next-api' of git://github.com/apollographql/graphql-too…
Apr 17, 2018
fade3fb
Add execution test
Apr 17, 2018
4be53ed
createDocuments docs
Apr 17, 2018
5bb040b
createDocument -> createRequest
Apr 18, 2018
6592810
Export createRequest
Apr 18, 2018
11697ae
Merge branch 'next-api' of https://github.com/apollographql/graphql-t…
Apr 20, 2018
e657fae
Merge branch 'master' into apollographql-next-api
Apr 24, 2018
ce7a18d
Merge branch 'master' into apollographql-next-api
Apr 27, 2018
eeb1a84
Merge branch 'master' into apollographql-next-api
May 1, 2018
bbc0265
Merge branch 'master' into apollographql-next-api
May 7, 2018
30b9119
Merge branch 'master' into apollographql-next-api
May 7, 2018
188442d
Merge branch 'master' into apollographql-next-api
May 8, 2018
88407e4
Merge branch 'master' into apollographql-next-api
May 15, 2018
1b8cafd
Merge branch 'master' into apollographql-next-api
May 21, 2018
8fd2a46
Address review comments
May 23, 2018
9e92ba6
Reword docs (typo)
May 23, 2018
5f46b3e
Merge branch 'master' into apollographql-next-api
Jun 4, 2018
779100b
Merge branch 'master' into apollographql-next-api
Jun 18, 2018
fed0f01
Merge branch 'master' into apollographql-next-api
Jun 18, 2018
948355e
Merge branch 'master' into apollographql-next-api
Jun 26, 2018
a61afae
Merge branch 'master' into apollographql-next-api
mfix22 Aug 1, 2018
ffd5f4c
Merge branch 'master' into apollographql-next-api
Aug 12, 2018
a8e5038
Merge branch 'master' into apollographql-next-api
Aug 29, 2018
2a8e82c
Merge branch 'master' into apollographql-next-api
hwillson Sep 7, 2018
536805b
Merge branch 'master' into apollographql-next-api
mfix22 Oct 13, 2018
e08cc10
Merge branch 'master' into apollographql-next-api
mfix22 Oct 23, 2018
5607de4
Merge branch 'master' into apollographql-next-api
mfix22 Nov 30, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/Interfaces.ts
Expand Up @@ -189,3 +189,10 @@ export type GraphQLParseOptions = {
allowLegacySDLImplementsInterfaces?: boolean;
experimentalFragmentVariables?: boolean;
};

export type OperationRootDefinition = {
fieldName: string,
alias?: string,
args?: { [key: string]: any },
info?: GraphQLResolveInfo
};
210 changes: 117 additions & 93 deletions src/stitching/delegateToSchema.ts
@@ -1,134 +1,158 @@
import {
ArgumentNode,
DocumentNode,
FieldNode,
FragmentDefinitionNode,
ArgumentNode,
Kind,
OperationDefinitionNode,
SelectionSetNode,
SelectionNode,
subscribe,
execute,
validate,
VariableDefinitionNode,
GraphQLSchema,
GraphQLResolveInfo
} from 'graphql';
import { Operation, Request, IDelegateToSchemaOptions } from '../Interfaces';
import {
applyRequestTransforms,
applyResultTransforms,
} from '../transforms/transforms';
import { FetcherOperation } from './makeRemoteExecutableSchema';
import { Request, Transform, IDelegateToSchemaOptions, OperationRootDefinition } from '../Interfaces';
import { applyRequestTransforms, applyResultTransforms } from '../transforms/transforms';
import AddArgumentsAsVariables from '../transforms/AddArgumentsAsVariables';
import FilterToSchema from '../transforms/FilterToSchema';
import AddTypenameToAbstract from '../transforms/AddTypenameToAbstract';
import CheckResultAndHandleErrors from '../transforms/CheckResultAndHandleErrors';

export default async function delegateToSchema(
options: IDelegateToSchemaOptions,
): Promise<any> {
const { info, args = {} } = options;
const rawDocument: DocumentNode = createDocument(
options.fieldName,
options.operation,
info.fieldNodes,
Object.keys(info.fragments).map(
fragmentName => info.fragments[fragmentName],
),
info.operation.variableDefinitions,
export function createOperation(
targetSchema: GraphQLSchema,
targetOperation: 'query' | 'mutation' | 'subscription',
roots: Array<OperationRootDefinition>,
graphqlContext: { [key: string]: any },
documentInfo: GraphQLResolveInfo,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left this for now, but let me know if you want me to change this. documentInfo was a prop i though users could use to define properties that are global to that operation, and do not affect specific root fields. Like fragments, for example.

transforms?: Array<Transform>,
): FetcherOperation {
const selections: Array<SelectionNode> = roots.map(({ fieldName, info, alias }) => {
const newSelections: Array<SelectionNode> = info
? [].concat(...info.fieldNodes.map((field: FieldNode) => field.selectionSet ? field.selectionSet.selections : []))
: [];

const args: Array<ArgumentNode> = info
? [].concat( ...info.fieldNodes.map((field: FieldNode) => field.arguments || []))
: [];

const rootSelectionSet = newSelections.length > 0
? {
kind: Kind.SELECTION_SET,
selections: newSelections
}
: null;

const rootField: FieldNode = {
kind: Kind.FIELD,
name: {
kind: Kind.NAME,
value: fieldName,
},
alias: alias
? {
kind: Kind.NAME,
value: alias
}
: null,
selectionSet: rootSelectionSet,
arguments: args
};

return rootField;
}, []);

const selectionSet: SelectionSetNode = {
kind: Kind.SELECTION_SET,
selections,
};

const operationDefinition: OperationDefinitionNode = {
kind: Kind.OPERATION_DEFINITION,
operation: targetOperation,
variableDefinitions: documentInfo.operation.variableDefinitions,
selectionSet,
};

const fragments = Object.keys(documentInfo.fragments).map(
fragmentName => documentInfo.fragments[fragmentName],
);

const document = {
kind: Kind.DOCUMENT,
definitions: [operationDefinition, ...fragments],
};

const rawRequest: Request = {
document: rawDocument,
variables: info.variableValues as Record<string, any>,
document,
variables: documentInfo.variableValues as Record<string, any>,
};

const transforms = [
...(options.transforms || []),
AddArgumentsAsVariables(options.schema, args),
FilterToSchema(options.schema),
AddTypenameToAbstract(options.schema),
CheckResultAndHandleErrors(info, options.fieldName),
transforms = [
...(transforms || []),
AddArgumentsAsVariables(targetSchema, roots),
FilterToSchema(targetSchema),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty damn proud this still works without any problem :D

AddTypenameToAbstract(targetSchema)
];

const processedRequest = applyRequestTransforms(rawRequest, transforms);
const { document: query, variables } = applyRequestTransforms(rawRequest, transforms);

const errors = validate(options.schema, processedRequest.document);
return {
query,
variables,
context: graphqlContext,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we return context: graphqlContext or context: { graphqlContext }? My original intention was the second because then you can take the return object and pass it directly to a fetcher.

Copy link
Contributor Author

@mfix22 mfix22 Apr 13, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or should we remove context all together, and just have this function return { query, variables or { document, variables }?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just document and variables I think.

operationName: documentInfo.operation && documentInfo.operation.name && documentInfo.operation.name.value
};
}

export default async function delegateToSchema(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pet peeve - could you move default export on top?

options: IDelegateToSchemaOptions,
): Promise<any> {
const processedRequest = createOperation(
options.schema,
options.operation,
[
{
fieldName: options.fieldName,
args: options.args || {},
info: options.info
}
],
options.context,
options.info,
options.transforms
);

const errors = validate(options.schema, processedRequest.query);
if (errors.length > 0) {
throw errors;
}

if (options.operation === 'query' ||
options.operation === 'mutation') {
return applyResultTransforms(await execute(
if (options.operation === 'query' || options.operation === 'mutation') {
const rawResult = await execute(
options.schema,
processedRequest.document,
info.rootValue,
processedRequest.query,
options.info.rootValue,
options.context,
processedRequest.variables,
), transforms);
);

const result = applyResultTransforms(rawResult, [
...(options.transforms || []),
CheckResultAndHandleErrors(options.info, options.fieldName),
]);

return result;
}

if (options.operation === 'subscription') {
// apply result processing ???
return subscribe(
options.schema,
processedRequest.document,
info.rootValue,
processedRequest.query,
options.info.rootValue,
options.context,
processedRequest.variables,
);
}
}

function createDocument(
targetField: string,
targetOperation: Operation,
originalSelections: Array<SelectionNode>,
fragments: Array<FragmentDefinitionNode>,
variables: Array<VariableDefinitionNode>,
): DocumentNode {
let selections: Array<SelectionNode> = [];
let args: Array<ArgumentNode> = [];

originalSelections.forEach((field: FieldNode) => {
const fieldSelections = field.selectionSet
? field.selectionSet.selections
: [];
selections = selections.concat(fieldSelections);
args = args.concat(field.arguments || []);
});

let selectionSet = null;
if (selections.length > 0) {
selectionSet = {
kind: Kind.SELECTION_SET,
selections: selections,
};
}

const rootField: FieldNode = {
kind: Kind.FIELD,
alias: null,
arguments: args,
selectionSet,
name: {
kind: Kind.NAME,
value: targetField,
},
};
const rootSelectionSet: SelectionSetNode = {
kind: Kind.SELECTION_SET,
selections: [rootField],
};

const operationDefinition: OperationDefinitionNode = {
kind: Kind.OPERATION_DEFINITION,
operation: targetOperation,
variableDefinitions: variables,
selectionSet: rootSelectionSet,
};

return {
kind: Kind.DOCUMENT,
definitions: [operationDefinition, ...fragments],
};
}