Skip to content

Commit

Permalink
add payload support for createMutationFromQuery
Browse files Browse the repository at this point in the history
  • Loading branch information
mattkrick committed May 20, 2016
1 parent 5afdf45 commit 9b13e07
Show file tree
Hide file tree
Showing 10 changed files with 158 additions and 63 deletions.
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,8 @@ node_modules
.idea
coverage
.nyc_output
actualResult.json
expectedResult.json
debug.js
debug.babel.js

18 changes: 14 additions & 4 deletions __tests__/clientSchema.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,16 @@
"kind": "OBJECT",
"name": "BlogSchema",
"fields": {
"getPostCount": {
"name": "getPostCount",
"type": {
"kind": "NON_NULL",
"ofType": {
"kind": "SCALAR",
"name": "Int"
}
}
},
"getLatestPost": {
"name": "getLatestPost",
"type": {
Expand Down Expand Up @@ -113,6 +123,10 @@
}
},
"types": {
"Int": {
"kind": "SCALAR",
"name": "Int"
},
"PostType": {
"kind": "OBJECT",
"name": "PostType",
Expand Down Expand Up @@ -329,10 +343,6 @@
"kind": "SCALAR",
"name": "Boolean"
},
"Int": {
"kind": "SCALAR",
"name": "Int"
},
"CommentType": {
"kind": "OBJECT",
"name": "CommentType",
Expand Down
7 changes: 7 additions & 0 deletions __tests__/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,13 @@ const Query = new GraphQLObjectType({
name: 'BlogSchema',
description: "Root of the Blog Schema",
fields: () => ({
getPostCount: {
type: new GraphQLNonNull(GraphQLInt),
description: "the number of posts currently in the db",
resolve() {
return Object.keys(PostDB).length;
}
},
getLatestPost: {
type: PostType,
description: "Latest post in the blog",
Expand Down
2 changes: 1 addition & 1 deletion debug.babel.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
require('babel-register');
require('babel-polyfill');
// require('babel-polyfill');
require('./debug.js');
24 changes: 12 additions & 12 deletions debug.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import fs from 'fs';
import 'babel-polyfill'
import clientSchema from './__tests__/clientSchema.json';
// import {mergeMutationASTs} from './src/mutate/mergeMutations';
import namespaceMutation from './src/mutate/namespaceMutation';
Expand All @@ -15,16 +14,17 @@ import {
mutationForMultipleComments,
queryCommentsForPostId,
queryMultipleComments,
queryPost,
mutatePost,
queryPostCount,
mutatePostCount

} from './src/mutate/__tests__/createMutationFromQuery-data';
const queryAST = parse(queryMultipleComments);
const expected = parseSortPrint(mutationForMultipleComments);
const variables = {
_id: 'a321',
postId: 'p123',
content: 'X'
};
const actualAST = createMutationFromQuery(queryAST, 'createComment', variables, clientSchema);
const actual = print(actualAST);
const queryAST = parse(queryPostCount);
const expected = parseSortPrint(mutatePostCount);
const actualAST = createMutationFromQuery(queryAST, 'createPost', {}, clientSchema);
const expectedAST = parse(mutatePostCount);
// const actual = sortPrint(actualAST);

// fs.writeFileSync('./actualResult.json', JSON.stringify(actualAST, null, 2).split("\n").join("\n "));
// fs.writeFileSync('./expectedResult.json', JSON.stringify(expectedAST, null, 2).split("\n").join("\n "));
fs.writeFileSync('./actualResult.json', JSON.stringify(actualAST, null, 2).split("\n").join("\n "));
fs.writeFileSync('./expectedResult.json', JSON.stringify(expectedAST, null, 2).split("\n").join("\n "));
20 changes: 11 additions & 9 deletions src/helperClasses.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,16 @@ class TypeCondition {
}
}

export class Field {
constructor({alias, args, directives, name, selections}) {
this.kind = FIELD;
this.alias = alias;
this.arguments = args;
this.directives = directives;
this.name = new Name(name);
this.selectionSet = selections ? new SelectionSet(selections) : null;
}
}
export class MutationShell {
constructor(mutationName, mutationArgs, variableDefinitions = []) {
this.kind = DOCUMENT;
Expand All @@ -104,15 +114,7 @@ export class MutationShell {
operation: 'mutation',
variableDefinitions,
directives: [],
selectionSet: new SelectionSet([{
alias: null,
arguments: mutationArgs,
// TODO add directives support
directives: [],
kind: FIELD,
name: new Name(mutationName),
selectionSet: new SelectionSet()
}])
selectionSet: new SelectionSet([new Field({args: mutationArgs, name: mutationName, selections: []})])
}]
}
}
Expand Down
17 changes: 17 additions & 0 deletions src/mutate/__tests__/createMutationFromQuery-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,20 @@ mutation {
}
}
}`;

export const queryPostCount = `
query {
getPostCount
}`;

export const mutatePostCount = `
mutation {
createPost {
postCount
}
}`;

export const queryPostCountAliased = `
query {
postCount: getPostCount
}`;
31 changes: 29 additions & 2 deletions src/mutate/__tests__/createMutationFromQuery-test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'babel-register';
import 'babel-polyfill';
// import 'babel-polyfill';
import test from 'ava';
import clientSchema from '../../../__tests__/clientSchema.json';
import createMutationFromQuery from '../createMutationFromQuery';
Expand All @@ -15,7 +15,10 @@ import {
queryPost,
mutatePost,
queryPostWithFieldVars,
mutatePostWithFieldVars
mutatePostWithFieldVars,
queryPostCount,
mutatePostCount,
queryPostCountAliased
} from './createMutationFromQuery-data';

test('creates basic mutation from a query of many comments', t => {
Expand Down Expand Up @@ -59,3 +62,27 @@ test('creates payload mutation including an object', t => {
const actual = sortPrint(actualAST);
t.is(actual, expected);
});

test('creates payload mutation including a scalar', t => {
const queryAST = parse(queryPostCount);
const expected = parseSortPrint(mutatePostCount);
const actualAST = createMutationFromQuery(queryAST, 'createPost', {}, clientSchema);
const actual = sortPrint(actualAST);
t.is(actual, expected);
});

test('creates payload mutation including a scalar matched by name', t => {
const queryAST = parse(queryPostCountAliased);
const expected = parseSortPrint(mutatePostCount);
const actualAST = createMutationFromQuery(queryAST, 'createPost', {}, clientSchema);
const actual = sortPrint(actualAST);
t.is(actual, expected);
});

test('creates payload mutation including an object with args', t => {
const queryAST = parse(queryPostWithFieldVars);
const expected = parseSortPrint(mutatePostWithFieldVars);
const actualAST = createMutationFromQuery(queryAST, 'createPost', {}, clientSchema);
const actual = sortPrint(actualAST);
t.is(actual, expected);
});
96 changes: 62 additions & 34 deletions src/mutate/createMutationFromQuery.js
Original file line number Diff line number Diff line change
@@ -1,55 +1,40 @@
import {ensureRootType} from '../utils';
import {CachedMutation, CachedQuery, MutationShell, RequestArgument} from '../helperClasses';
import {ensureTypeFromNonNull, clone} from '../utils';
import {MutationShell, RequestArgument, Field} from '../helperClasses';
import {clone} from '../utils';
import {mergeSelections} from './mergeMutations';
import {
OPERATION_DEFINITION,
DOCUMENT,
SELECTION_SET,
NAME,
ARGUMENT,
VARIABLE,
NAMED_TYPE,
FIELD,
INLINE_FRAGMENT,
VARIABLE_DEFINITION,
LIST_TYPE
} from 'graphql/language/kinds';
import {VARIABLE, INLINE_FRAGMENT} from 'graphql/language/kinds';
import {TypeKind} from 'graphql/type/introspection';

const {SCALAR} = TypeKind;

export default (queryAST, mutationName, mutationVariables = {}, schema) => {
const operation = queryAST.definitions[0];
//createComment
const mutationFieldSchema = schema.mutationSchema.fields[mutationName];
//commentType
const mutationRootReturnType = ensureRootType(mutationFieldSchema.type);
// commentTypeSchema
const mutationReturnSchema = schema.types[mutationRootReturnType.name];

const mutationArgs = makeArgsFromVars(mutationFieldSchema, mutationVariables);
const mutationAST = new MutationShell(mutationName, mutationArgs);


// Assume the mutationReturnSchema is a single type (opposed to a payload full of many types)
const selectionsInQuery = findTypeInQuery(mutationReturnSchema.name, operation, schema);
if (selectionsInQuery) {
debugger
const newSelections = selectionsInQuery.pop();
for (let srcSelections of selectionsInQuery) {
mergeSelections(newSelections, srcSelections);
}
mutationAST.definitions[0].selectionSet.selections[0].selectionSet.selections = newSelections;
} else {

// TODO treat as a payload
const simpleObject = trySimpleObject(selectionsInQuery);
if (simpleObject) {
mutationAST.definitions[0].selectionSet.selections[0].selectionSet.selections = simpleObject;
return mutationAST;
}
return mutationAST;
return tryPayloadObject(mutationAST, operation, mutationReturnSchema, schema);
};

const findTypeInQuery = (typeName, initialReqAST, schema) => {
/**
* Uses a BFS since types are likely high up the tree & scalars can possibly break early
*/
const findTypeInQuery = (typeName, queryAST, schema, matchName) => {
const bag = [];
const queue = [];
let next = {
reqAST: initialReqAST,
reqAST: queryAST,
typeSchema: schema.querySchema
};
while (next) {
Expand All @@ -62,11 +47,18 @@ const findTypeInQuery = (typeName, initialReqAST, schema) => {
} else {
const selectionName = selection.name.value;
const fieldSchema = typeSchema.fields[selectionName];
if (!fieldSchema) debugger
const rootFieldType = ensureRootType(fieldSchema.type);
subSchema = ensureRootType(schema.types[rootFieldType.name]);
if (subSchema.name === typeName) {
bag.push(clone(selection.selectionSet.selections));
if (matchName) {
bag[0] = clone(selection);
const fieldNameOrAlias = selection.alias && selection.alias.value || selectionName;
if (matchName === fieldNameOrAlias) {
return bag;
}
} else {
bag.push(clone(selection.selectionSet.selections));
}
}
}
queue.push({
Expand All @@ -77,7 +69,43 @@ const findTypeInQuery = (typeName, initialReqAST, schema) => {
}
next = queue.shift();
}
return bag.length && bag;
return bag;
};

const trySimpleObject = selectionsInQuery => {
if (selectionsInQuery.length) {
const newSelections = selectionsInQuery.pop();
for (let srcSelections of selectionsInQuery) {
mergeSelections(newSelections, srcSelections);
}
return newSelections;
}
};

const tryPayloadObject = (mutationAST, operation, mutationReturnSchema, schema) => {
const payloadFieldKeys = Object.keys(mutationReturnSchema.fields);
for (let payloadFieldKey of payloadFieldKeys) {
const payloadField = mutationReturnSchema.fields[payloadFieldKey];
const rootPayloadFieldType = ensureRootType(payloadField.type);
// 2 strings probably don't refer to the same field, so for scalars the name has to match, too
const matchName = rootPayloadFieldType.kind === SCALAR && payloadField.name;
const selectionsInQuery = findTypeInQuery(rootPayloadFieldType.name, operation, schema, matchName);
if (selectionsInQuery.length) {
let mutationField;
if (matchName) {
mutationField = new Field({name: matchName});
} else {
const newSelections = selectionsInQuery.pop();
for (let srcSelections of selectionsInQuery) {
mergeSelections(newSelections, srcSelections);
}
// make a payload field into a mutation field
mutationField = new Field({name: payloadField.name, selections: newSelections});
}
mutationAST.definitions[0].selectionSet.selections[0].selectionSet.selections.push(mutationField);
}
}
return mutationAST;
};

const makeArgsFromVars = (mutationFieldSchema, variables) => {
Expand Down
1 change: 0 additions & 1 deletion src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ export const ensureTypeFromNonNull = type => type.kind === NON_NULL ? type.ofTyp

//const ensureTypeFromList = type => type.kind === LIST ? ensureTypeFromNonNull(type.ofType) : type;
export const ensureRootType = type => {
if (!type) debugger
while (type.ofType) type = type.ofType;
return type;
};
Expand Down

0 comments on commit 9b13e07

Please sign in to comment.